refactor(docker): pre-build install stamp in CI instead of inside the image

CI runs scripts/write_install_stamp.py before  to produce
install-stamp.json with full git provenance. The stamp arrives via the
bulk COPY . . and a late RUN moves it to .hermes_build_info.json —
placed late so a stamp change only re-runs the final layer, not the
expensive build layers above. Local builds without the stamp file
build fine; runtime falls through to 'unknown' source.
This commit is contained in:
ethernet 2026-07-24 18:02:51 -04:00
parent 17c7dfea8c
commit dfec7198f7
2 changed files with 30 additions and 12 deletions

View file

@ -53,6 +53,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Write install stamp
run: python3 scripts/write_install_stamp.py --output install-stamp.json
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
@ -141,6 +144,9 @@ jobs:
- name: Checkout trusted source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Write install stamp
run: python3 scripts/write_install_stamp.py --output install-stamp.json
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

View file

@ -40,6 +40,22 @@ RUN apt-get -o Acquire::Retries=3 update && \
make -j"$(nproc)" && \
make install
# ---------- Install stamp stages ----------
# CI pre-builds install-stamp.json (scripts/write_install_stamp.py) with full
# git provenance before `docker build`. The stamp is COPY'd into the image
# so version_info.py can read it at runtime — .dockerignore excludes .git,
# so the stamp can't be resolved inside the image.
#
# The stamp arrives via the bulk `COPY . .` below as /opt/hermes/install-stamp.json.
# A late RUN moves it to the canonical
# .hermes_build_info.json path so it doesn't bust the docker cache for
# the expensive build layers above — only the final metadata layer
# changes when the stamp changes.
#
# If the file is absent (local `docker build` without CI), the mv is
# a no-op and runtime falls through to "unknown" source with no crash.
# ---------- Base image ----------
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source
# Node 22 LTS source stage. Debian trixie's bundled nodejs is pinned to 20.x
# which reached EOL in April 2026 — we copy node + npm + corepack from the
@ -277,6 +293,14 @@ RUN mkdir -p /opt/hermes/bin && \
cp /opt/hermes/docker/hermes-exec-shim.sh /opt/hermes/bin/hermes && \
chmod 0755 /opt/hermes/bin/hermes && \
printf 'docker\n' > /opt/hermes/.install_method
# Move the pre-built install stamp (if CI provided one) to the canonical
# path. Placed late so a stamp change doesn't bust the docker cache for
# the expensive build layers above — only this final layer re-runs.
# If install-stamp.json is absent (local build without CI), this is a no-op.
RUN if [ -f /opt/hermes/install-stamp.json ] && grep -q '"commit"' /opt/hermes/install-stamp.json; then \
mv /opt/hermes/install-stamp.json /opt/hermes/.hermes_build_info.json; \
fi
# The ``.install_method`` stamp is baked next to the running code (the install
# tree), NOT into $HERMES_HOME. $HERMES_HOME (/opt/data) is a shared data
# volume that is commonly bind-mounted from the host and even shared with a
@ -288,18 +312,6 @@ RUN mkdir -p /opt/hermes/bin && \
# `s6-setuidgid hermes` in its run script. If HERMES_UID is unset, services
# run as the default hermes user (UID 10000).
# ---------- Bake build-time install stamp ----------
# .dockerignore excludes .git, so runtime git lookups always fail. The shared
# stamp script writes a canonical install-stamp.json that version_info.py
# reads at runtime — no env vars or separate build_info module needed.
ARG HERMES_GIT_SHA=
RUN if [ -n "${HERMES_GIT_SHA}" ]; then \
python3 scripts/write_install_stamp.py \
--output /opt/hermes/.hermes_build_info.json \
--commit "${HERMES_GIT_SHA}" \
--source docker; \
fi
# ---------- s6-overlay service wiring ----------
# Static services declared at build time: main-hermes + dashboard.
# Per-profile gateway services are registered dynamically at runtime by