diff --git a/apps/api/__tests__/integration/health.test.ts b/apps/api/__tests__/integration/health.test.ts index 4f0da75..0d73704 100644 --- a/apps/api/__tests__/integration/health.test.ts +++ b/apps/api/__tests__/integration/health.test.ts @@ -14,5 +14,12 @@ describe('Health Endpoint', () => { expect(response.body.checks).toBeDefined(); expect(response.body.checks.database).toBeDefined(); expect(response.body.checks.redis).toBeDefined(); + + expect(typeof response.body.ok).toBe('boolean'); + expect(response.body.service).toBe('fieldview-api'); + expect(typeof response.body.commit).toBe('string'); + expect(typeof response.body.env).toBe('string'); + expect(typeof response.body.utc).toBe('string'); + expect(response.body.ok).toBe(response.body.status === 'healthy'); }); }); diff --git a/apps/api/src/lib/__tests__/health-fleet.test.ts b/apps/api/src/lib/__tests__/health-fleet.test.ts new file mode 100644 index 0000000..62d5c8c --- /dev/null +++ b/apps/api/src/lib/__tests__/health-fleet.test.ts @@ -0,0 +1,45 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { resolveHealthCommit } from '../health-fleet'; + +const COMMIT_KEYS = [ + 'RAILWAY_GIT_COMMIT_SHA', + 'VERCEL_GIT_COMMIT_SHA', + 'GIT_COMMIT', +] as const; + +describe('resolveHealthCommit', () => { + const saved: Partial> = {}; + + beforeEach(() => { + for (const key of COMMIT_KEYS) { + saved[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of COMMIT_KEYS) { + const value = saved[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); + + it('returns RAILWAY_GIT_COMMIT_SHA when set', () => { + process.env.RAILWAY_GIT_COMMIT_SHA = 'abc123fullsha'; + expect(resolveHealthCommit()).toBe('abc123fullsha'); + }); + + it('returns unknown when all commit env vars are unset', () => { + expect(resolveHealthCommit()).toBe('unknown'); + }); + + it('falls back to GIT_COMMIT when Railway and Vercel are unset', () => { + process.env.GIT_COMMIT = 'localsha'; + expect(resolveHealthCommit()).toBe('localsha'); + }); +}); diff --git a/apps/api/src/lib/health-fleet.ts b/apps/api/src/lib/health-fleet.ts new file mode 100644 index 0000000..a5f78af --- /dev/null +++ b/apps/api/src/lib/health-fleet.ts @@ -0,0 +1,30 @@ +/** Fleet health contract: build identity fields for GET /health. */ + +export function resolveHealthCommit(): string { + return ( + process.env.RAILWAY_GIT_COMMIT_SHA ?? + process.env.VERCEL_GIT_COMMIT_SHA ?? + process.env.GIT_COMMIT ?? + 'unknown' + ); +} + +export function resolveHealthEnv(): string { + return process.env.APP_ENV ?? process.env.RAILWAY_ENVIRONMENT_NAME ?? 'dev'; +} + +export function buildFleetHealthFields(ok: boolean, service: string): { + ok: boolean; + service: string; + commit: string; + env: string; + utc: string; +} { + return { + ok, + service, + commit: resolveHealthCommit(), + env: resolveHealthEnv(), + utc: new Date().toISOString(), + }; +} diff --git a/apps/api/src/routes/health.ts b/apps/api/src/routes/health.ts index 0876ad3..cf866cd 100644 --- a/apps/api/src/routes/health.ts +++ b/apps/api/src/routes/health.ts @@ -6,6 +6,7 @@ import express, { type Request, Response, Router } from 'express'; +import { buildFleetHealthFields } from '../lib/health-fleet'; import { prisma } from '../lib/prisma'; import { redisClient } from '../lib/redis'; @@ -90,6 +91,7 @@ export function createHealthRouter(): Router { timestamp: new Date().toISOString(), checks, email: checkEmailProvider(), + ...buildFleetHealthFields(isHealthy, 'fieldview-api'), }); }); diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts new file mode 100644 index 0000000..a2118d4 --- /dev/null +++ b/apps/web/app/api/health/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server'; + +import { resolveHealthCommit, resolveHealthEnv } from '@/lib/health-fleet'; + +export async function GET(): Promise { + return NextResponse.json({ + ok: true, + service: 'fieldview-web', + commit: resolveHealthCommit(), + env: resolveHealthEnv(), + utc: new Date().toISOString(), + }); +} diff --git a/apps/web/lib/health-fleet.test.ts b/apps/web/lib/health-fleet.test.ts new file mode 100644 index 0000000..d00abf5 --- /dev/null +++ b/apps/web/lib/health-fleet.test.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { resolveHealthCommit } from './health-fleet'; + +const COMMIT_KEYS = [ + 'RAILWAY_GIT_COMMIT_SHA', + 'VERCEL_GIT_COMMIT_SHA', + 'GIT_COMMIT', +] as const; + +describe('resolveHealthCommit', () => { + const saved: Partial> = {}; + + beforeEach(() => { + for (const key of COMMIT_KEYS) { + saved[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of COMMIT_KEYS) { + const value = saved[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); + + it('returns RAILWAY_GIT_COMMIT_SHA when set', () => { + process.env.RAILWAY_GIT_COMMIT_SHA = 'abc123fullsha'; + expect(resolveHealthCommit()).toBe('abc123fullsha'); + }); + + it('returns unknown when all commit env vars are unset', () => { + expect(resolveHealthCommit()).toBe('unknown'); + }); +}); diff --git a/apps/web/lib/health-fleet.ts b/apps/web/lib/health-fleet.ts new file mode 100644 index 0000000..294ffe5 --- /dev/null +++ b/apps/web/lib/health-fleet.ts @@ -0,0 +1,14 @@ +/** Fleet health contract: build identity fields for GET /api/health. */ + +export function resolveHealthCommit(): string { + return ( + process.env.RAILWAY_GIT_COMMIT_SHA ?? + process.env.VERCEL_GIT_COMMIT_SHA ?? + process.env.GIT_COMMIT ?? + 'unknown' + ); +} + +export function resolveHealthEnv(): string { + return process.env.APP_ENV ?? process.env.RAILWAY_ENVIRONMENT_NAME ?? 'dev'; +}