ci: only rebuild images whose inputs changed
Server Deploy / deploy (push) Successful in 2m38s

Every push rebuilt all six images regardless of what it touched. A git diff
against github.event.before now gates each build step.

Two things this needs to work at all: fetch-depth 0, because the default
shallow clone has a single commit and nothing to diff against, and git
installed in the dind container, which had node and npm but not git.

The path mapping follows the build contexts rather than intuition — the Go
images use a root context and COPY shared/, so shared/ fans out to all
three, while the Next images use their own directory and cannot be affected
from outside it. Anything that leaves no trustworthy base commit — manual
run, new branch, force-push whose old head is gone — lists every tracked
file instead, so the fallback is one code path rather than two.

Known gap, documented: a repo variable change pushes no commit, so nothing
rebuilds. workflow_dispatch is the escape hatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrhid6
2026-07-26 18:15:26 +01:00
co-authored by Claude Opus 5
parent 3a6c5ebae8
commit 11ebd324ad
2 changed files with 94 additions and 3 deletions
+80 -2
View File
@@ -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 \
+14 -1
View File
@@ -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