From fc3b5fd385da7f07c3e5d84c6594d39e2d79389f Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 30 Apr 2026 11:59:24 -0700 Subject: [PATCH] feat(canvas): add /api/buildinfo for version-display parity with tenant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace-server has GET /buildinfo (PR #2398) — `curl https://. moleculesai.app/buildinfo` returns the live git SHA. Canvas had no parallel: debugging "is this the deployed code?" required reading Vercel's UI or response headers (deployment ID, not git SHA). Add canvas /api/buildinfo returning {git_sha, git_ref, vercel_env} sourced from VERCEL_GIT_COMMIT_SHA / _REF / VERCEL_ENV — Vercel injects these at build time from the deploying commit. Outside Vercel (local `next dev`, harness) all three are unset and the endpoint returns `git_sha: "dev"`, the same sentinel workspace-server uses pre-ldflags- injection. Now both surfaces speak the same vocabulary: curl https://.moleculesai.app/buildinfo curl https://canvas.moleculesai.app/api/buildinfo 3 tests cover dev-fallback, Vercel-injected SHA pass-through, and JSON content type. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/api/buildinfo/__tests__/route.test.ts | 48 +++++++++++++++++++ canvas/src/app/api/buildinfo/route.ts | 18 +++++++ 2 files changed, 66 insertions(+) create mode 100644 canvas/src/app/api/buildinfo/__tests__/route.test.ts create mode 100644 canvas/src/app/api/buildinfo/route.ts diff --git a/canvas/src/app/api/buildinfo/__tests__/route.test.ts b/canvas/src/app/api/buildinfo/__tests__/route.test.ts new file mode 100644 index 00000000..ac2d8f7b --- /dev/null +++ b/canvas/src/app/api/buildinfo/__tests__/route.test.ts @@ -0,0 +1,48 @@ +/** + * Canvas /api/buildinfo — version-display endpoint mirroring + * workspace-server's /buildinfo. Lets `curl /api/buildinfo` + * confirm which git SHA is live on a canvas deployment. + */ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { GET } from "../route"; + +const ENV_KEYS = ["VERCEL_GIT_COMMIT_SHA", "VERCEL_GIT_COMMIT_REF", "VERCEL_ENV"]; + +describe("GET /api/buildinfo", () => { + let saved: Record; + + beforeEach(() => { + saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + for (const k of ENV_KEYS) delete process.env[k]; + }); + + afterEach(() => { + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + }); + + it("returns dev sentinel when Vercel env vars are unset", async () => { + const res = await GET(); + const body = await res.json(); + expect(body).toEqual({ git_sha: "dev", git_ref: "", vercel_env: "local" }); + }); + + it("reports the SHA Vercel injected at build time", async () => { + process.env.VERCEL_GIT_COMMIT_SHA = "abc1234567890"; + process.env.VERCEL_GIT_COMMIT_REF = "main"; + process.env.VERCEL_ENV = "production"; + const res = await GET(); + const body = await res.json(); + expect(body.git_sha).toBe("abc1234567890"); + expect(body.git_ref).toBe("main"); + expect(body.vercel_env).toBe("production"); + }); + + it("returns 200 status and JSON content type", async () => { + const res = await GET(); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("application/json"); + }); +}); diff --git a/canvas/src/app/api/buildinfo/route.ts b/canvas/src/app/api/buildinfo/route.ts new file mode 100644 index 00000000..a8ff8aab --- /dev/null +++ b/canvas/src/app/api/buildinfo/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; + +// Mirror of workspace-server's GET /buildinfo (PR #2398). Lets a developer +// confirm which git SHA is live on a canvas deployment with the same +// `curl /buildinfo` flow they use against tenant workspaces. +// +// Vercel injects VERCEL_GIT_COMMIT_SHA / _REF / VERCEL_ENV at build time +// from the deploying commit; outside Vercel (local `next dev`, harness) +// these are unset and the endpoint reports `git_sha: "dev"`. Same sentinel +// the workspace-server uses pre-ldflags-injection so both surfaces speak +// the same vocabulary. +export async function GET() { + return NextResponse.json({ + git_sha: process.env.VERCEL_GIT_COMMIT_SHA ?? "dev", + git_ref: process.env.VERCEL_GIT_COMMIT_REF ?? "", + vercel_env: process.env.VERCEL_ENV ?? "local", + }); +}