diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5e5c19bdf3b..86dfaf20025 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index d21a58a29c1..fd2dbb8e6e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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