feat(canvas): add /buildinfo endpoint exposing the build SHA (core#2235) #2253

Merged
hongming merged 1 commits from feat/core2235-canvas-buildinfo into main 2026-06-04 21:25:45 +00:00
Owner

Closes #2235.

Problem

The platform image exposes GET /buildinfo ({git_sha}) so the fleet redeploy workflow can poll each tenant and assert it runs the merge SHA before reporting success. The Next.js canvas had a GET /api/buildinfo route, but it only read VERCEL_GIT_COMMIT_SHA — which the canvas's ECR-image Docker deploy never sets (the fleet deploys the Docker image, not via Vercel). So the served SHA always reported "dev" and canvas deploys could only be verified indirectly (CI-green gate + immutable :staging-<sha> + digest-pin), never by the served SHA.

Change

Bake the merge SHA into the canvas image at build time and surface it through the existing route.

  • canvas/DockerfileARG BUILD_SHA=dev -> ENV BUILD_SHA in the final runtime stage. Server-only (not NEXT_PUBLIC_) so it stays out of the client bundle. Default "dev" matches workspace-server's sentinel, so an unwired build fails the SHA comparison closed.
  • canvas/src/app/api/buildinfo/route.ts — priority BUILD_SHA -> VERCEL_GIT_COMMIT_SHA -> "dev". Added export const dynamic = "force-dynamic" so the route reads BUILD_SHA from the standalone Node server's runtime env per request rather than freezing it at next build.
  • .gitea/workflows/publish-canvas-image.yml — pass BUILD_SHA=${{ github.sha }} (full 40-char SHA) in build-args, mirroring how the platform image wires GIT_SHA.
  • docker-compose.ymlBUILD_SHA build arg (default dev) for local compose builds.
  • test — assert BUILD_SHA wins over the Vercel var, the Vercel fallback, and the dev default.

Server-vs-static

canvas/next.config.ts sets output: "standalone" — the canvas ships a Node server, so this is a server route reading a server-only env, not NEXT_PUBLIC_ (which would leak the SHA into the client bundle and freeze it into the static JS). next build confirms /api/buildinfo renders as ƒ (Dynamic) server-rendered on demand, so ENV BUILD_SHA in the final image is read at request time.

Route shape

GET /api/buildinfo -> 200 application/json

{ "git_sha": "<40-char merge sha | dev>", "git_ref": "", "vercel_env": "local" }

Fleet verification reads .git_sha, same field name as workspace-server.

Validation

  • npm ci + npx tsc --noEmit — no new errors in buildinfo files (223 pre-existing tsc errors on clean main in unrelated test files; this PR adds zero).
  • npx vitest run src/app/api/buildinfo — 4/4 pass.
  • npm run build — compiles; /api/buildinfo is ƒ Dynamic.
  • eslint clean on the route + test; publish-canvas-image.yml and docker-compose.yml parse as valid YAML.

Follow-up (flagged, not in scope)

core#2226's canvas deploy could poll this /api/buildinfo per-tenant to assert the served SHA, the same way the platform redeploy workflow polls workspace-server's /buildinfo — a small wiring follow-up.

🤖 Generated with Claude Code

Closes #2235. ## Problem The platform image exposes `GET /buildinfo` (`{git_sha}`) so the fleet redeploy workflow can poll each tenant and assert it runs the merge SHA before reporting success. The Next.js canvas had a `GET /api/buildinfo` route, but it only read `VERCEL_GIT_COMMIT_SHA` — which the canvas's **ECR-image Docker deploy never sets** (the fleet deploys the Docker image, not via Vercel). So the served SHA always reported `"dev"` and canvas deploys could only be verified indirectly (CI-green gate + immutable `:staging-<sha>` + digest-pin), never by the served SHA. ## Change Bake the merge SHA into the canvas image at build time and surface it through the existing route. - **`canvas/Dockerfile`** — `ARG BUILD_SHA=dev` -> `ENV BUILD_SHA` in the **final runtime stage**. Server-only (not `NEXT_PUBLIC_`) so it stays out of the client bundle. Default `"dev"` matches workspace-server's sentinel, so an unwired build fails the SHA comparison closed. - **`canvas/src/app/api/buildinfo/route.ts`** — priority `BUILD_SHA` -> `VERCEL_GIT_COMMIT_SHA` -> `"dev"`. Added `export const dynamic = "force-dynamic"` so the route reads `BUILD_SHA` from the standalone Node server's **runtime** env per request rather than freezing it at `next build`. - **`.gitea/workflows/publish-canvas-image.yml`** — pass `BUILD_SHA=${{ github.sha }}` (full 40-char SHA) in `build-args`, mirroring how the platform image wires `GIT_SHA`. - **`docker-compose.yml`** — `BUILD_SHA` build arg (default `dev`) for local compose builds. - **test** — assert `BUILD_SHA` wins over the Vercel var, the Vercel fallback, and the `dev` default. ## Server-vs-static `canvas/next.config.ts` sets `output: "standalone"` — the canvas ships a Node server, so this is a **server route** reading a **server-only** env, not `NEXT_PUBLIC_` (which would leak the SHA into the client bundle and freeze it into the static JS). `next build` confirms `/api/buildinfo` renders as `ƒ (Dynamic) server-rendered on demand`, so `ENV BUILD_SHA` in the final image is read at request time. ## Route shape `GET /api/buildinfo` -> `200 application/json` ```json { "git_sha": "<40-char merge sha | dev>", "git_ref": "", "vercel_env": "local" } ``` Fleet verification reads `.git_sha`, same field name as workspace-server. ## Validation - `npm ci` + `npx tsc --noEmit` — no new errors in buildinfo files (223 pre-existing tsc errors on clean `main` in unrelated test files; this PR adds zero). - `npx vitest run src/app/api/buildinfo` — 4/4 pass. - `npm run build` — compiles; `/api/buildinfo` is `ƒ Dynamic`. - `eslint` clean on the route + test; `publish-canvas-image.yml` and `docker-compose.yml` parse as valid YAML. ## Follow-up (flagged, not in scope) core#2226's canvas deploy could poll this `/api/buildinfo` per-tenant to assert the served SHA, the same way the platform redeploy workflow polls workspace-server's `/buildinfo` — a small wiring follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hongming added 1 commit 2026-06-04 20:58:58 +00:00
feat(canvas): bake build SHA into image for /api/buildinfo (core#2235)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Harness Replays / detect-changes (pull_request) Successful in 4s
CI / Python Lint & Test (pull_request) Successful in 9s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 10s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
E2E Chat / detect-changes (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 10s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 10s
CI / Detect changes (pull_request) Successful in 14s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 2s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 9s
Harness Replays / Harness Replays (pull_request) Successful in 5s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2s
gate-check-v3 / gate-check (pull_request_target) Successful in 12s
E2E Chat / E2E Chat (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
qa-review / approved (pull_request_target) Failing after 17s
security-review / approved (pull_request_target) Failing after 16s
CI / Platform (Go) (pull_request) Successful in 2s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 11s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 58s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m13s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m11s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m25s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m50s
CI / Canvas (Next.js) (pull_request) Successful in 5m53s
CI / Canvas Deploy Status (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 1s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, l
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 3s
sop-tier-check / tier-check (pull_request_target) Successful in 5s
qa-review / approved (pull_request_review) Has been skipped
security-review / approved (pull_request_review) Has been skipped
sop-tier-check / tier-check (pull_request_review) Successful in 6s
audit-force-merge / audit (pull_request_target) Successful in 3s
a409d9032a
The canvas /api/buildinfo route existed but only read VERCEL_GIT_COMMIT_SHA,
which the fleet's ECR-image Docker deploy never sets — so the served SHA
always reported "dev" and canvas deploys could not be verified by the
served SHA the way the platform's /buildinfo is.

Bake the merge SHA into the canvas image at build time and surface it:

- canvas/Dockerfile: ARG BUILD_SHA=dev -> ENV BUILD_SHA in the final
  runtime stage (server-only, not NEXT_PUBLIC_, so it stays out of the
  client bundle). Default "dev" matches workspace-server's sentinel so an
  unwired build fails the SHA comparison closed.
- route.ts: BUILD_SHA takes priority, then VERCEL_GIT_COMMIT_SHA, then
  "dev". force-dynamic so the route reads BUILD_SHA from the standalone
  Node server's runtime env per request (confirmed via next build: the
  route renders as Dynamic / server-rendered on demand).
- publish-canvas-image.yml: pass BUILD_SHA=${{ github.sha }} (full 40-char
  SHA) so the fleet redeploy verification can match exactly.
- docker-compose.yml: BUILD_SHA build arg (default "dev") for local builds.
- test: assert BUILD_SHA wins over the Vercel var + the dev fallback.

Follow-up (flagged, not in scope): core#2226's canvas deploy could poll
this /api/buildinfo per-tenant to assert the served SHA, the same way the
platform redeploy workflow polls workspace-server's /buildinfo.

Closes #2235

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hongming added the tier:low label 2026-06-04 21:21:13 +00:00
core-qa approved these changes 2026-06-04 21:22:12 +00:00
core-qa left a comment
Member

QA review (core#2235 canvas /buildinfo). Additive observability route. Reviewed: BUILD_SHA priority chain (BUILD_SHA→VERCEL→dev), force-dynamic ensures runtime-env read under output:standalone, tests assert BUILD_SHA wins + dev fallback, server-only var stays out of client bundle, Dockerfile/workflow/compose wiring consistent. CI green (Canvas build + Playwright E2E). Approve.

QA review (core#2235 canvas /buildinfo). Additive observability route. Reviewed: BUILD_SHA priority chain (BUILD_SHA→VERCEL→dev), force-dynamic ensures runtime-env read under output:standalone, tests assert BUILD_SHA wins + dev fallback, server-only var stays out of client bundle, Dockerfile/workflow/compose wiring consistent. CI green (Canvas build + Playwright E2E). Approve.
core-security approved these changes 2026-06-04 21:22:14 +00:00
core-security left a comment
Member

Security review (core#2235). No secret/authz surface: /api/buildinfo exposes only the build SHA (same as workspace-server /buildinfo). BUILD_SHA is non-sensitive build metadata, server-only (not NEXT_PUBLIC). No new auth paths, no user input, no injection surface. Approve.

Security review (core#2235). No secret/authz surface: /api/buildinfo exposes only the build SHA (same as workspace-server /buildinfo). BUILD_SHA is non-sensitive build metadata, server-only (not NEXT_PUBLIC). No new auth paths, no user input, no injection surface. Approve.
hongming merged commit aa9ea5f99f into main 2026-06-04 21:25:45 +00:00
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2253