2 Commits
Author SHA1 Message Date
christianmanivong 451a98c14a Merge pull request 'feat(ci): publish to Gitea's registry, and stop deploying to a random host' (#1) from feature/gitea-registry into main
CI / TypeScript — type-check (push) Successful in 9s
CI / Publish — build & push image (push) Successful in 12s
2026-09-08 21:51:49 +00:00
Christian ManivongandClaude Opus 5 c40fa97cd1 feat(ci): publish to Gitea's registry, and stop deploying to a random host
CI / TypeScript — type-check (pull_request) Successful in 10s
CI / Publish — build & push image (pull_request) Skipped
CI / TypeScript — type-check (push) Successful in 9m48s
CI / Publish — build & push image (push) Skipped
The image moves off registry.netork.io. That registry is plain registry:2 with
htpasswd auth, which knows nothing about repositories: every account that can log
in reads and writes everything on it, including the accounts issued to customer
instances. Verified -- a customer server's credentials list the whole catalogue.
It keeps the images those instances are meant to pull; the marketing site is not
one of them. Gitea scopes packages to their owning account, and no customer has
one. netOrk #172.

Login uses a REGISTRY_TOKEN secret (a Gitea token with write:package). The token
Actions injects per run does not work here -- the package registry rejects it
with a bare "unauthorized", which is a confusing way to spend an afternoon.

The deploy job is removed rather than migrated, because it had quietly stopped
being correct. It ran `docker run` against whatever runner picked the job up,
which worked while exactly one runner existed. There are now several --
netork-runner-12 on .12, netork-runner-13 on .13, plus the original
netork-runner -- and none of them is on 10.7.224.11, where this site runs and
where the proxy-net it attaches to lives. The next push would have started a
second website container on the wrong host and reported success while netork.io
went on serving the old one. Nothing had failed yet; the last deploy was
2026-07-17, back when the pool was one runner.

scripts/deploy.sh replaces it: it names the target, pulls before it removes
anything, compares the running container's image id against what was pulled, and
finishes by checking that netork.io actually answers 200.

Push-to-deploy can come back by registering a runner on .11 with a label of its
own and pinning `runs-on:` to it, or by giving CI an ssh key. Both decide where a
credential lives, so neither was decided here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 23:41:10 +02:00
3 changed files with 110 additions and 27 deletions
+45 -27
View File
@@ -33,41 +33,59 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Gitea's registry, not registry.netork.io.
#
# registry.netork.io is plain registry:2 with htpasswd auth, which knows
# nothing about repositories: every account that can log in reads and
# writes everything on it, including the accounts issued to customer
# instances. It keeps the images those instances are meant to pull
# (netork/engine, netork/ui, netork/satellite); the marketing site is not
# one of them. Gitea scopes packages to their owning account, and no
# customer has one. netOrk issue #172.
#
# REGISTRY_TOKEN is a Gitea access token with write:package — the token
# Actions injects per run is scoped to the repository API and the package
# registry rejects it outright.
- name: Login to registry - name: Login to registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.netork.io -u "${{ secrets.REGISTRY_USER }}" --password-stdin run: |
set -euo pipefail
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "::error::REGISTRY_TOKEN is not set (Gitea token with write:package)."
exit 1
fi
LOGIN_USER="${{ secrets.REGISTRY_USER }}"
[ -n "$LOGIN_USER" ] || LOGIN_USER="${{ github.actor }}"
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.netork.io -u "$LOGIN_USER" --password-stdin
- name: Build & push - name: Build & push
run: | run: |
set -euo pipefail
SHA=$(git rev-parse --short HEAD) SHA=$(git rev-parse --short HEAD)
docker build \ docker build \
-t registry.netork.io/netork/website:latest \ -t git.netork.io/netork/website:latest \
-t registry.netork.io/netork/website:main-${SHA} \ -t git.netork.io/netork/website:main-${SHA} \
. .
docker push registry.netork.io/netork/website:latest docker push git.netork.io/netork/website:latest
docker push registry.netork.io/netork/website:main-${SHA} docker push git.netork.io/netork/website:main-${SHA}
- name: Logout - name: Logout
if: always() if: always()
run: docker logout registry.netork.io run: docker logout git.netork.io
deploy: # The deploy job that used to live here has been removed, deliberately.
name: Deploy — pull & restart on host #
runs-on: ubuntu-latest # It ran `docker run` against whatever runner picked the job up, which worked
needs: [publish] # while exactly one runner existed. There are now several (netork-runner-12 on
steps: # .12, netork-runner-13 on .13, and the original netork-runner) and none of them
- name: Login to registry # is on 10.7.224.11, where this site actually runs and where the proxy-net it
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.netork.io -u "${{ secrets.REGISTRY_USER }}" --password-stdin # attaches to lives. The job would therefore have started a second website
# container on the wrong host and reported success, while netork.io went on
- name: Pull & restart # serving the old one.
run: | #
docker pull registry.netork.io/netork/website:latest # Deployment is now an explicit step: scripts/deploy.sh, run from a workstation,
docker rm -f netork-website 2>/dev/null || true # which targets .11 by name and verifies afterwards that the container really is
docker run -d \ # on the image that was pulled.
--name netork-website \ #
--restart unless-stopped \ # To get push-to-deploy back, either register a runner on .11 with a label of its
--network proxy-net \ # own and pin `runs-on:` to it, or give CI an ssh key for .11. Both are choices
registry.netork.io/netork/website:latest # about where a credential lives, so neither was made here.
- name: Logout
if: always()
run: docker logout registry.netork.io
+3
View File
@@ -5,3 +5,6 @@ memory/
.env.local .env.local
*.local *.local
.DS_Store .DS_Store
# Deploy target + registry token
deploy.env
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Deploy the marketing site to the host that actually serves it.
#
# ./scripts/deploy.sh [--version=<tag>] [server]
#
# This used to be a CI job. It ran `docker run` on whichever runner picked the
# job up, which was correct while exactly one runner existed — there are now
# several, none of them on the host this site runs on, so the job would have
# started a second container in the wrong place and reported success. Naming the
# target is the whole point of this script.
set -euo pipefail
SERVER="${DEPLOY_SERVER:-10.7.224.11}"
REGISTRY="${REGISTRY:-git.netork.io/netork}"
VERSION="${VERSION:-latest}"
NAME="${CONTAINER_NAME:-netork-website}"
ENV_FILE="$(cd "$(dirname "$0")/.." && pwd)/deploy.env"
# shellcheck source=/dev/null
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
for arg in "$@"; do
case "$arg" in
--version=*) VERSION="${arg#--version=}" ;;
*) SERVER="$arg" ;;
esac
done
IMAGE="${REGISTRY}/website:${VERSION}"
echo "[${SERVER}] Deploying ${IMAGE}"
if [[ -n "${REGISTRY_TOKEN:-}" ]]; then
ssh -n "$SERVER" "echo '${REGISTRY_TOKEN}' | docker login git.netork.io -u '${REGISTRY_USER:-christianmanivong}' --password-stdin" \
| sed "s/^/[${SERVER}] /"
fi
# Pull first, and let a failure stop the script here: the container is only
# removed once there is something to replace it with.
echo "[${SERVER}] Pulling..."
ssh -n "$SERVER" "docker pull '${IMAGE}'" | tail -2 | sed "s/^/[${SERVER}] /"
echo "[${SERVER}] Recreating..."
ssh -n "$SERVER" "docker rm -f '${NAME}' >/dev/null 2>&1 || true; \
docker run -d --name '${NAME}' --restart unless-stopped --network proxy-net '${IMAGE}' >/dev/null && echo started" \
| sed "s/^/[${SERVER}] /"
# `docker run` cannot silently reuse an old container the way `compose up -d`
# can, but the tag it resolved might still not be the one that was just pulled.
# Compare, rather than trust.
echo "[${SERVER}] Verifying..."
WANT=$(ssh -n "$SERVER" "docker image inspect --format '{{.Id}}' '${IMAGE}'")
GOT=$(ssh -n "$SERVER" "docker inspect --format '{{.Image}}' '${NAME}'")
if [[ "$WANT" != "$GOT" ]]; then
echo "[${SERVER}] ERROR: container runs ${GOT}, expected ${WANT}" >&2
exit 1
fi
echo "[${SERVER}] Verified: ${NAME} runs ${IMAGE}."
echo "[${SERVER}] Checking the site answers..."
CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 https://netork.io/ || echo 000)
echo "[${SERVER}] https://netork.io -> ${CODE}"
[[ "$CODE" == "200" ]] || { echo "[${SERVER}] ERROR: site is not answering 200" >&2; exit 1; }