feat(canvas): add /api/buildinfo for version-display parity with tenant

Workspace-server has GET /buildinfo (PR #2398) — `curl https://<slug>.
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://<slug>.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) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-30 11:59:24 -07:00
parent 46ca5aa6d3
commit fc3b5fd385
2 changed files with 66 additions and 0 deletions

View File

@ -0,0 +1,48 @@
/**
* Canvas /api/buildinfo version-display endpoint mirroring
* workspace-server's /buildinfo. Lets `curl <url>/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<string, string | undefined>;
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");
});
});

View File

@ -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 <url>/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",
});
}