From 9a4775b200b92a5730dd10faa5c3656ddd95e58a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 4 Oct 2026 15:00:24 +0000 Subject: [PATCH] feat(api,web): add fleet metadata to GET /api/health Co-authored-by: Ricardo Vega --- packages/api/src/routes/health/health.test.ts | 40 ++++++++++++++++ packages/api/src/routes/health/health.ts | 12 ++++- packages/web/app/api/health/route.test.ts | 48 +++++++++++++++++++ packages/web/app/api/health/route.ts | 17 ++++++- 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packages/web/app/api/health/route.test.ts diff --git a/packages/api/src/routes/health/health.test.ts b/packages/api/src/routes/health/health.test.ts index ef385ea..69769df 100644 --- a/packages/api/src/routes/health/health.test.ts +++ b/packages/api/src/routes/health/health.test.ts @@ -12,6 +12,19 @@ describe('Health Router', () => { }); describe('GET /api/health', () => { + const savedRailwaySha = process.env['RAILWAY_GIT_COMMIT_SHA']; + const savedVercelSha = process.env['VERCEL_GIT_COMMIT_SHA']; + const savedGitCommit = process.env['GIT_COMMIT']; + + afterEach(() => { + if (savedRailwaySha === undefined) delete process.env['RAILWAY_GIT_COMMIT_SHA']; + else process.env['RAILWAY_GIT_COMMIT_SHA'] = savedRailwaySha; + if (savedVercelSha === undefined) delete process.env['VERCEL_GIT_COMMIT_SHA']; + else process.env['VERCEL_GIT_COMMIT_SHA'] = savedVercelSha; + if (savedGitCommit === undefined) delete process.env['GIT_COMMIT']; + else process.env['GIT_COMMIT'] = savedGitCommit; + }); + it('should return 200 with health status', async () => { // Act const response = await request(app).get('/api/health'); @@ -20,7 +33,12 @@ describe('Health Router', () => { expect(response.status).toBe(200); expect(response.body).toEqual({ status: 'ok', + ok: true, + service: 'scholarmancy-api', + commit: expect.any(String), + env: expect.any(String), timestamp: expect.any(String), + utc: expect.any(String), }); }); @@ -31,6 +49,28 @@ describe('Health Router', () => { // Assert expect(response.body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); expect(() => new Date(response.body.timestamp)).not.toThrow(); + expect(response.body.utc).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + expect(() => new Date(response.body.utc)).not.toThrow(); + }); + + it('should return commit from RAILWAY_GIT_COMMIT_SHA when set', async () => { + process.env['RAILWAY_GIT_COMMIT_SHA'] = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + + const response = await request(app).get('/api/health'); + + expect(response.status).toBe(200); + expect(response.body.commit).toBe('deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'); + }); + + it('should report unknown commit when deploy env vars are absent', async () => { + delete process.env['RAILWAY_GIT_COMMIT_SHA']; + delete process.env['VERCEL_GIT_COMMIT_SHA']; + delete process.env['GIT_COMMIT']; + + const response = await request(app).get('/api/health'); + + expect(response.status).toBe(200); + expect(response.body.commit).toBe('unknown'); }); }); diff --git a/packages/api/src/routes/health/health.ts b/packages/api/src/routes/health/health.ts index 74def9f..c0c71a8 100644 --- a/packages/api/src/routes/health/health.ts +++ b/packages/api/src/routes/health/health.ts @@ -33,9 +33,19 @@ export function readBuiltAt(): string { * Returns server status and current timestamp. */ healthRouter.get('/', (_req: Request, res: Response) => { + const now = new Date().toISOString(); res.status(200).json({ status: 'ok', - timestamp: new Date().toISOString(), + ok: true, + service: 'scholarmancy-api', + commit: + process.env['RAILWAY_GIT_COMMIT_SHA'] ?? + process.env['VERCEL_GIT_COMMIT_SHA'] ?? + process.env['GIT_COMMIT'] ?? + 'unknown', + env: process.env['APP_ENV'] ?? process.env['RAILWAY_ENVIRONMENT_NAME'] ?? 'dev', + timestamp: now, + utc: now, }); }); diff --git a/packages/web/app/api/health/route.test.ts b/packages/web/app/api/health/route.test.ts new file mode 100644 index 0000000..30428d6 --- /dev/null +++ b/packages/web/app/api/health/route.test.ts @@ -0,0 +1,48 @@ +import { GET } from './route'; + +describe('GET /api/health', () => { + const savedRailwaySha = process.env['RAILWAY_GIT_COMMIT_SHA']; + const savedVercelSha = process.env['VERCEL_GIT_COMMIT_SHA']; + const savedGitCommit = process.env['GIT_COMMIT']; + + afterEach(() => { + if (savedRailwaySha === undefined) delete process.env['RAILWAY_GIT_COMMIT_SHA']; + else process.env['RAILWAY_GIT_COMMIT_SHA'] = savedRailwaySha; + if (savedVercelSha === undefined) delete process.env['VERCEL_GIT_COMMIT_SHA']; + else process.env['VERCEL_GIT_COMMIT_SHA'] = savedVercelSha; + if (savedGitCommit === undefined) delete process.env['GIT_COMMIT']; + else process.env['GIT_COMMIT'] = savedGitCommit; + }); + + it('returns fleet health metadata with scholarmancy-web service', async () => { + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual({ + status: 'ok', + ok: true, + service: 'scholarmancy-web', + commit: expect.any(String), + env: expect.any(String), + utc: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/), + }); + }); + + it('returns commit from RAILWAY_GIT_COMMIT_SHA when set', async () => { + process.env['RAILWAY_GIT_COMMIT_SHA'] = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + + const res = await GET(); + const body = await res.json(); + expect(body.commit).toBe('deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'); + }); + + it('reports unknown commit when deploy env vars are absent', async () => { + delete process.env['RAILWAY_GIT_COMMIT_SHA']; + delete process.env['VERCEL_GIT_COMMIT_SHA']; + delete process.env['GIT_COMMIT']; + + const res = await GET(); + const body = await res.json(); + expect(body.commit).toBe('unknown'); + }); +}); diff --git a/packages/web/app/api/health/route.ts b/packages/web/app/api/health/route.ts index ea066b3..d2ee8eb 100644 --- a/packages/web/app/api/health/route.ts +++ b/packages/web/app/api/health/route.ts @@ -4,5 +4,20 @@ export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; export async function GET() { - return NextResponse.json({ status: 'ok' }, { status: 200 }); + const utc = new Date().toISOString(); + return NextResponse.json( + { + status: 'ok', + ok: true, + service: 'scholarmancy-web', + commit: + process.env['RAILWAY_GIT_COMMIT_SHA'] ?? + process.env['VERCEL_GIT_COMMIT_SHA'] ?? + process.env['GIT_COMMIT'] ?? + 'unknown', + env: process.env['APP_ENV'] ?? process.env['RAILWAY_ENVIRONMENT_NAME'] ?? 'dev', + utc, + }, + { status: 200 } + ); }