Skip to content

feat(deploy): stamp both dashboard images with their git revision (#3315) - #3386

Merged
Xore merged 2 commits into
mainfrom
oc/3315-image-revision-stamp
Sep 27, 2026
Merged

Xore merged 2 commits into
mainfrom
oc/3315-image-revision-stamp

Conversation

@Xore

@Xore Xore commented Sep 27, 2026

Copy link
Copy Markdown
Owner

What

apiary-backend:latest reports Labels: null, /healthz returns {ok, es}, and
"is the merge on the host?" is answered by comparing docker images creation time
with the merge time. docs/ARCANE-GIT-SYNC.md already names what that misses:
green, healthy, running the old code.

This makes the answer a field both tiers report, plus a tool that compares it.

Where the revision is

backend /healthz {"ok":true,"es":true,"revision":"3dca4457…"}, compiled in by build.rs (GIT_SHA → APIARY_GIT_SHA) and also in the boot log line
frontend /build.json {"revision":…,"built":…,"source":…}, static, generated into public/ before npm run build so vite copies it
both images org.opencontainers.image.{revision,source,created}

build.rs emits cargo:rerun-if-env-changed=GIT_SHA. That line is load-bearing:
without it, cargo reuses a cached build and the second build of an identical tree
keeps the first revision — the exact failure this issue is about, reproduced by
the fix itself.

ARG GIT_SHA is declared in both stages of each Dockerfile, because Docker
scopes an ARG to its stage: the build stage needs it for build.rs, the runtime
stage again for the label. The frontend's stamp is written before the build that
copies public/ into the output, and is gitignored — a committed build.json
would be a revision claim about nobody's build.

Normalization

Both tiers reduce the value to a bare lowercase hex object name (optional
sha256: prefix stripped, 7–64 hex chars) or unknown. They are held to one
table, backend-service/src/revision-corpus.json, read by a cargo test and by
scripts/tests/test_3315_image_revision.py — two implementations in two languages
in two CI lanes drift silently, and the failure mode of their disagreeing is a
deploy mismatch that is not one.

Verification

scripts/verify-deploy.sh --healthz-exec \
  'docker exec hp-apiary-backend curl -sf http://127.0.0.1:8081/healthz' \
  --image apiary-backend:latest

Exit 0 agrees, 1 is a finding (missing / unstamped / malformed / stale
past --behind-days), 2 is "could not tell". That third code is the point: an
unreadable body, a rewired endpoint or an image that is not on this host must
never read as a stale deploy. A short git rev-parse --short revision is
accepted and reported as an abbreviation rather than as a mismatch.

diagnostics.yml runs it --warn-only against $GITHUB_SHA: a scheduled
diagnostics red X is the alert, and a stale deploy is not on #2222's list.

What an operator still has to do (deliberately not hidden)

Nothing on the homeserver writes GIT_SHA yet, and it cannot be computed in
place — /opt/stacks/apiary is a working-tree rsync with .git/ excluded, and
Arcane's directory sync has no .git either. So the first run reports
not stamped, which is true. Turning it on is one line per stack
(echo "GIT_SHA=$(git rev-parse HEAD)" >> …/.env) plus the
POST /projects/{id}/build the sync does not do. The docs say so in the same
place they say why the image will not invent a value.

Gates

scripts/tests/test_3315_image_revision.py (40 tests) — the generator run for
real over the corpus, the Dockerfile/compose/CI wiring asserted as text, and
verify-deploy.sh driven against a throwaway git repo with stub curl/docker
across its pass / mismatch / unstamped / malformed / lag / warn-only / exit-2
paths. Plus the existing cargo test (535), actionlint, hadolint, shellcheck,
zizmor, and the doc/compose lint gates.

Note

scripts/tests/test_compose_drift_watch_sweep.py has two failures
(…privileged_fallback, both 0 != 1) that reproduce on a clean checkout of the
base commit with this branch stashed — pre-existing, unrelated, and left alone
here rather than folded into this PR.

)

Deploy verification is inference today: compare `docker images` creation
time with the merge time, or grep a string out of the shipped binary.
`apiary-backend:latest` had `Labels: null`, so every merge since the last
rebuild looked identical to the one before it, and
docs/ARCANE-GIT-SYNC.md already names the state this produces -- "green,
healthy, running the old code".

Both images now carry the revision three ways:

- backend-service compiles it in (build.rs: GIT_SHA -> APIARY_GIT_SHA) and
  /healthz returns it as `revision`, so the question becomes a curl rather
  than a comparison of timestamps. build.rs also emits
  `cargo:rerun-if-env-changed=GIT_SHA`; without that, cargo reuses a
  cached build and the second build of an identical tree reports the first
  revision -- the exact failure this fixes, reproduced by the fix.
- frontend-next bakes it into a static /build.json, generated in the build
  stage before `npm run build` so vite copies it into the output. A static
  file rather than a route because Nitro serving from public/ is already
  proven in this image (the container healthcheck reads
  /static/theme.css the same way).
- both Dockerfiles declare ARG GIT_SHA per stage and set
  org.opencontainers.image.{revision,source,created}.

Both tiers normalize to a bare lowercase hex object name or "unknown", and
are held to one shared table (backend-service/src/revision-corpus.json) so
two normalizers in two languages, in two CI lanes, cannot drift -- the
failure mode of their disagreeing is a deploy mismatch that is not one.

scripts/verify-deploy.sh compares the live /healthz revision and the image
labels against an expected revision, defaulting to origin/main. Its exit
codes are the point: 0 agrees, 1 is a finding, and 2 is "could not tell" --
deliberately distinct, so an unreadable body or an image that is not on the
host can never read as a stale deploy. diagnostics.yml runs it in
--warn-only mode, against $GITHUB_SHA, because the home runner has no clone
to measure against (deploy.yml rsyncs into /opt/stacks/apiary with
--exclude .git/).

Unset stays the honest default: nothing on the homeserver writes GIT_SHA
yet, so the first run reports "not stamped", which is true, and the docs
say how to turn it on.
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@Xore

Xore commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Holding this one per Niklas's instruction to not dispatch on the dashboard
rewrite epic (#3279).

#3279 names arcane/home/honeypot-dashboard/frontend-next as its canonical
behavioural specification -- the source of truth the rewrite is measured
against. This PR adds three files inside that directory (Dockerfile,
.gitignore, scripts/write-build-info.mjs), so it edits the very tree the
epic treats as reference.

The backend half of #3315 is sound and the same revision stamp is the right
idea; the concern is only the epic overlap. Splitting the frontend-next
changes out and re-dispatching the backend half alone would keep the work and
respect the hold.

Not merged. #3315 stays open.

@Xore
Xore enabled auto-merge (squash) September 27, 2026 09:16
Both files conflicted because #3385 (liveness/readiness split) and #3315
(revision stamp) landed on the same two places.

backend-service/src/main.rs — #3385 replaced the single `Health { ok, es }`
struct with `Liveness`/`Readiness` and three handlers, so #3315's `revision`
field had nowhere to land. Rather than resurrect the old struct, the field
moves onto `Liveness`: /healthz stays an alias of /livez, so the deploy
verifier and diagnostics both still read `revision` off the same body, and
#3317's property (liveness never touches Elasticsearch) is preserved because
git_revision() reads a compile-time env, not the cluster. `built` and
`revision` are both kept -- they are not redundant: a timestamp can only be
compared, an object name can be looked up, and an image rebuilt from an old
commit passes the timestamp check while running month-old code.

containers.yml — #3316 added `boot_smoke` to the same two matrix rows #3315
added `stamp` to, and both needed the `labels:` line. Resolution is the union:
both rows carry both flags, `labels` keeps #3316's per-row build-row handle,
and `build-args` is added alongside it.

Local: cargo test 543 passed / 0 failed / 1 pre-existing ignored (544 total,
unchanged from main); cargo clippy --all-targets -- -D warnings clean;
cargo build --all-targets clean; also green with GIT_SHA set.
test_3315_image_revision.py 40 passed; test_3316_image_boot_smoke.py 44 passed.
zizmor --offline --min-severity medium reports the same single allowlisted
dangerous-triggers advisory as main, and no new findings.
@Xore
Xore merged commit 81e9ae8 into main Sep 27, 2026
120 of 121 checks passed
@Xore
Xore deleted the oc/3315-image-revision-stamp branch September 27, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant