From b38260939b404d3129ae5adfd70a75ec4ab251b8 Mon Sep 17 00:00:00 2001 From: azank Date: Thu, 15 Oct 2026 14:28:07 +0500 Subject: [PATCH] api: /api/version names the commit a deployment was built from It returns VERCEL_GIT_COMMIT_SHA, and its ref when known, so a deploy can be read back against the history. A deployment that does not know its commit answers 503 rather than guess. It is one more op on the site router, so the function count stays under the Hobby plan's 12. --- web/api/site.ts | 7 ++++--- web/lib/routes/version.ts | 10 ++++++++++ web/test/routes.test.ts | 22 ++++++++++++++++++++++ web/vercel.json | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 web/lib/routes/version.ts diff --git a/web/api/site.ts b/web/api/site.ts index 9e12558..981a7f3 100644 --- a/web/api/site.ts +++ b/web/api/site.ts @@ -1,11 +1,12 @@ -// Small reads for the pages in one function. vercel.json rewrites /api/city, /api/gallery, /api/stars, /api/live -// and /api/pulse here as ?op=. +// Small reads for the pages in one function. vercel.json rewrites /api/city, /api/gallery, /api/stars, /api/live, +// /api/pulse and /api/version here as ?op=. import { dispatch } from '../lib/http.js'; import * as city from '../lib/routes/city.js'; import * as gallery from '../lib/routes/gallery.js'; import * as live from '../lib/routes/live.js'; import * as pulse from '../lib/routes/pulse.js'; import * as stars from '../lib/routes/stars.js'; +import * as version from '../lib/routes/version.js'; -const routes = { city, gallery, stars, live, pulse }; +const routes = { city, gallery, stars, live, pulse, version }; export const GET = dispatch(routes, 'GET'); diff --git a/web/lib/routes/version.ts b/web/lib/routes/version.ts new file mode 100644 index 0000000..ea5fd53 --- /dev/null +++ b/web/lib/routes/version.ts @@ -0,0 +1,10 @@ +// Which commit this deployment was built from, so a deploy can be read back and checked against the history. +// Vercel sets VERCEL_GIT_COMMIT_SHA for deployments made from git; a deployment without it says so rather than guess. +import { handle, HttpError, json } from '../http.js'; + +export const GET = handle(async () => { + const commit = process.env.VERCEL_GIT_COMMIT_SHA; + if (!commit || !/^[0-9a-f]{40}$/.test(commit)) throw new HttpError(503, 'this deployment does not know its commit'); + const ref = process.env.VERCEL_GIT_COMMIT_REF; + return json({ commit, source: 'env:VERCEL_GIT_COMMIT_SHA', ...(ref ? { ref } : {}) }); +}); diff --git a/web/test/routes.test.ts b/web/test/routes.test.ts index e191e5b..1607625 100644 --- a/web/test/routes.test.ts +++ b/web/test/routes.test.ts @@ -35,6 +35,7 @@ test('a GitHub URL with commit.city swapped in lands on the city', () => { ['/orgs/vercel', 'redirect /vercel'], ['/orgs/vercel/repositories', 'redirect /vercel'], // the site's own paths are left alone + ['/api/version', 'rewrite /api/site?op=version'], ['/api/build/repo', null], ['/s/f/inter.woff2', null], ['/v/app.a63cdb5695.js', null], @@ -43,3 +44,24 @@ test('a GitHub URL with commit.city swapped in lands on the city', () => { ]; for (const [p, want] of cases) assert.equal(route(p), want, p); }); + +test('/api/version names the commit this deployment was built from, and never guesses', async () => { + const { GET } = await import('../lib/routes/version'); + const saved = { sha: process.env.VERCEL_GIT_COMMIT_SHA, ref: process.env.VERCEL_GIT_COMMIT_REF }; + try { + process.env.VERCEL_GIT_COMMIT_SHA = 'a'.repeat(40); + process.env.VERCEL_GIT_COMMIT_REF = 'main'; + const r = await GET(new Request('https://commit.city/api/site?op=version')); + assert.equal(r.status, 200); + assert.equal(r.headers.get('cache-control'), 'no-store'); + assert.deepEqual(await r.json(), { commit: 'a'.repeat(40), source: 'env:VERCEL_GIT_COMMIT_SHA', ref: 'main' }); + for (const bad of [undefined, '', 'abc1234']) { + if (bad === undefined) delete process.env.VERCEL_GIT_COMMIT_SHA; else process.env.VERCEL_GIT_COMMIT_SHA = bad; + assert.equal((await GET(new Request('https://commit.city/api/site?op=version'))).status, 503); + } + } finally { + for (const [k, v] of [['VERCEL_GIT_COMMIT_SHA', saved.sha], ['VERCEL_GIT_COMMIT_REF', saved.ref]] as const) { + if (v === undefined) delete process.env[k]; else process.env[k] = v; + } + } +}); diff --git a/web/vercel.json b/web/vercel.json index 7e1f9df..8ea66f1 100644 --- a/web/vercel.json +++ b/web/vercel.json @@ -16,7 +16,7 @@ "rewrites": [ { "source": "/api/auth/:op(login|callback|logout)", "destination": "/api/account?op=:op" }, { "source": "/api/:op(me|settings)", "destination": "/api/account?op=:op" }, - { "source": "/api/:op(city|gallery|stars|live|pulse)", "destination": "/api/site?op=:op" }, + { "source": "/api/:op(city|gallery|stars|live|pulse|version)", "destination": "/api/site?op=:op" }, { "source": "/:owner((?!(?:api|v|s|u|fonts|local-store|explore|events|readme|about|404|index)(?:/|$))[A-Za-z0-9][A-Za-z0-9-]{0,38})", "destination": "/api/page?t=:owner" }, { "source": "/:owner((?!(?:api|v|s|u|fonts|local-store|explore|events|readme|about|404|index)(?:/|$))[A-Za-z0-9][A-Za-z0-9-]{0,38})/:repo([A-Za-z0-9._-]{1,100})", "destination": "/api/page?t=:owner/:repo" } ],