From 62155081ddbd6811a2aae09dbd0b76279c0442a8 Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Fri, 12 Jun 2026 22:29:57 +0800 Subject: [PATCH] ci: stamp builds with their git SHA and expose it for deploy verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deploys repeatedly drifted from the branch HEAD (a manual build run before the latest commit, a stale `:latest` pull, the #534-vs-#535 mixup), and the only way to tell what was actually live was to hand-inspect rendered DOM classes. Make the deployed commit verifiable at a glance. - build-main: pass the short commit SHA as a `BUILD_SHA` build-arg and also tag the pushed image `:git-` alongside `:latest`, so the image is identifiable by commit. - Dockerfile: accept `BUILD_SHA` (default `unknown`) and set it as a runtime env in the runner stage. - Add GET /api/public/version returning `{ sha }` (the build's commit) so a running deploy can be checked with `curl /api/public/version` — it should report the commit you expect — instead of guessing whether `:latest` is the branch HEAD. Local/dev builds without the arg report "unknown". No app behaviour change. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-main.yaml | 11 ++++++++++- Dockerfile | 5 +++++ app/api/[[...route]]/route.ts | 2 ++ hono/open/version.ts | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 hono/open/version.ts diff --git a/.github/workflows/build-main.yaml b/.github/workflows/build-main.yaml index 685be8a6..1f98a77c 100644 --- a/.github/workflows/build-main.yaml +++ b/.github/workflows/build-main.yaml @@ -15,6 +15,9 @@ jobs: steps: - name: Checkout PicImpact uses: actions/checkout@v4 + - name: Resolve build SHA + id: sha + run: echo "short=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -33,4 +36,10 @@ jobs: file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/picimpact:latest + build-args: | + BUILD_SHA=${{ steps.sha.outputs.short }} + # Also tag with the commit so the pushed image is identifiable, and the + # running container reports the same value at GET /api/public/version. + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/picimpact:latest + ${{ secrets.DOCKERHUB_USERNAME }}/picimpact:git-${{ steps.sha.outputs.short }} diff --git a/Dockerfile b/Dockerfile index 310bd1ac..a7fc3e07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,6 +51,11 @@ WORKDIR /app ENV NODE_ENV=production +# Git commit this image was built from, passed by the build workflow. Exposed at +# runtime via GET /api/public/version so a deploy's running commit is verifiable. +ARG BUILD_SHA=unknown +ENV BUILD_SHA=${BUILD_SHA} + RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs diff --git a/app/api/[[...route]]/route.ts b/app/api/[[...route]]/route.ts index dfeab85a..a1490099 100644 --- a/app/api/[[...route]]/route.ts +++ b/app/api/[[...route]]/route.ts @@ -5,6 +5,7 @@ import route from '~/hono' import download from '~/hono/open/download' import images from '~/hono/open/images' import cameraLens from '~/hono/open/camera-lens' +import version from '~/hono/open/version' const app = new Hono().basePath('/api') @@ -13,6 +14,7 @@ app.route('/v1', route) app.route('/public/download', download) app.route('/public/images', images) app.route('/public/camera-lens', cameraLens) +app.route('/public/version', version) app.notFound((c) => { return c.text('not found', 404) }) diff --git a/hono/open/version.ts b/hono/open/version.ts new file mode 100644 index 00000000..7e2db4d0 --- /dev/null +++ b/hono/open/version.ts @@ -0,0 +1,18 @@ +import 'server-only' + +import { Hono } from 'hono' +import { ok } from '~/hono/_lib/response' + +const app = new Hono() + +// Public deployment marker. Returns the git commit the running image was built +// from (`BUILD_SHA`, injected as a Docker build-arg by the build-main workflow). +// Lets a deploy be verified at a glance — `curl /api/public/version` reports the +// commit actually running — instead of guessing whether `:latest` is the branch +// HEAD (which has repeatedly caused half-deploy confusion). Falls back to +// "unknown" for local/dev builds where the arg isn't set. +app.get('/', (c) => { + return ok(c, { sha: process.env.BUILD_SHA ?? 'unknown' }) +}) + +export default app