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" } ],