diff --git a/.gitea/workflows/server-deploy.yml b/.gitea/workflows/server-deploy.yml index 90d9b7e..b640793 100644 --- a/.gitea/workflows/server-deploy.yml +++ b/.gitea/workflows/server-deploy.yml @@ -4,16 +4,88 @@ on: push: branches: - main + # Manual runs rebuild everything: there is no "before" commit to diff + # against, which the change detection below treats as "build it all". That + # is also the escape hatch for a repo VARIABLE change — editing API_URL or + # ADMIN_ENV pushes no commit, so nothing would rebuild on its own. + workflow_dispatch: jobs: deploy: runs-on: ubuntu-docker container: docker:dind steps: - - name: Setup Node - run: apk add --update nodejs npm + # git is needed twice over: actions/checkout clones with it, and the + # change detection below diffs with it. + - name: Setup + run: apk add --update nodejs npm git + - name: Checkout uses: actions/checkout@v4 + with: + # The default shallow clone has one commit, which cannot be + # diffed against the previous push. + fetch-depth: 0 + + - name: Work out what changed + id: changed + run: | + set -eu + + BEFORE="${{ github.event.before }}" + ZERO="0000000000000000000000000000000000000000" + + # Build everything whenever the comparison cannot be trusted: + # a manual run, a brand-new branch, or a force-push whose old + # head is no longer in the repository. Listing every tracked + # file makes every filter below match, so there is no second + # code path to keep correct. + if [ -z "$BEFORE" ] || [ "$BEFORE" = "$ZERO" ] || ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then + echo "No usable base commit — building every image." + git ls-files > /tmp/changed.txt + else + git diff --name-only "$BEFORE" HEAD > /tmp/changed.txt + fi + + echo "--- changed files ---" + cat /tmp/changed.txt + echo "---------------------" + + # A change to the workflow itself can change a build arg, and + # a build arg is baked into the image, so it rebuilds all. + if grep -qE '^\.gitea/workflows/' /tmp/changed.txt; then + ALL=1 + else + ALL=0 + fi + + flag() { + name="$1" + pattern="$2" + if [ "$ALL" = "1" ] || grep -qE "$pattern" /tmp/changed.txt; then + echo "$name=true" >> "$GITHUB_OUTPUT" + echo "build $name" + else + echo "$name=false" >> "$GITHUB_OUTPUT" + echo "skip $name" + fi + } + + # The three Go images build from the repo root and COPY + # shared/ plus their own directory, so shared/ rebuilds all + # three. proto/ is in server's list as insurance: the + # generated pb is committed under server/, but a proto change + # that someone regenerates in the same push should not depend + # on that ordering. + flag server '^(server/|shared/|proto/|go\.work)' + flag sitesvc '^(sitesvc/|shared/|go\.work)' + flag admin '^(admin/|shared/|go\.work)' + + # The three Next images use their own directory as the build + # context, so nothing outside it can affect them. + flag web '^web/' + flag site '^site/' + flag adminsite '^adminsite/' - name: Log in to registry run: | @@ -22,6 +94,7 @@ jobs: -u "${{ secrets.REGISTRY_USER }}" --password-stdin - name: Build and push server image + if: steps.changed.outputs.server == 'true' run: | IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/server:latest" # Root context: server depends on the shared module. @@ -29,6 +102,7 @@ jobs: docker push "$IMAGE" - name: Build and push web image + if: steps.changed.outputs.web == 'true' run: | IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/web:latest" docker build \ @@ -39,6 +113,7 @@ jobs: docker push "$IMAGE" - name: Build and push site image + if: steps.changed.outputs.site == 'true' run: | IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/site:latest" docker build \ @@ -50,6 +125,7 @@ jobs: docker push "$IMAGE" - name: Build and push sitesvc image + if: steps.changed.outputs.sitesvc == 'true' run: | IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/sitesvc:latest" # Root context: sitesvc depends on the shared module. @@ -57,6 +133,7 @@ jobs: docker push "$IMAGE" - name: Build and push admin image + if: steps.changed.outputs.admin == 'true' run: | IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/admin:latest" # Root context: admin depends on the shared module. @@ -64,6 +141,7 @@ jobs: docker push "$IMAGE" - name: Build and push adminsite image + if: steps.changed.outputs.adminsite == 'true' run: | IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/adminsite:latest" docker build \ diff --git a/claude.md b/claude.md index 39acd94..7aa567e 100644 --- a/claude.md +++ b/claude.md @@ -542,13 +542,26 @@ GOOS=linux GOARCH=amd64 go build \ Builds and pushes six images to the Gitea container registry: `server`, `web`, `site`, `sitesvc`, `admin` and `adminsite`. -Note that despite the name, **this workflow does not deploy** — it only builds and pushes. There is no SSH step and no path filter; every push to `main` rebuilds all three images. Rolling them out is a separate manual step on the host: +Note that despite the name, **this workflow does not deploy** — it only builds and pushes. There is no SSH step. Rolling images out is a separate manual step on the host: ```bash cd /opt/vantage && docker compose -f docker-compose.yml -f docker-compose.site.yml pull && \ docker compose -f docker-compose.yml -f docker-compose.site.yml up -d --remove-orphans ``` +**Each image only rebuilds when its own inputs changed.** A `git diff` against `github.event.before` decides, which is why the checkout uses `fetch-depth: 0` — the default shallow clone has one commit and nothing to diff — and why `git` is installed in the `docker:dind` container. The mapping follows the build contexts exactly: + +| Image | Rebuilds when | +| --- | --- | +| `server` | `server/`, `shared/`, `proto/`, `go.work` | +| `admin` | `admin/`, `shared/`, `go.work` | +| `sitesvc` | `sitesvc/`, `shared/`, `go.work` | +| `web` · `site` · `adminsite` | their own directory only | + +`shared/` fans out to all three Go images because each of their Dockerfiles copies `shared/` from a root context — **if a fourth service ever imports `shared/`, add it to that list or it will ship stale**. A change to the workflow file rebuilds everything, since a build arg is baked into the image. So does anything that leaves no trustworthy base commit: a manual `workflow_dispatch`, a new branch, or a force-push whose old head is gone. + +The gap this leaves: **changing a repo variable pushes no commit, so nothing rebuilds.** After editing `API_URL`, `ADMIN_API_URL`, `HQ_URL` or `ADMIN_ENV`, run the workflow manually — that is what `workflow_dispatch` is there for. Base images also stop being refreshed on a service nobody touches; a periodic manual run covers that. + ### Tagging ```bash