From 9aa254709d10b7b3618169eb26ae2ffda063e1c1 Mon Sep 17 00:00:00 2001 From: Niels Mokkenstorm Date: Sun, 17 May 2026 17:31:43 +0200 Subject: [PATCH] feat(cvg-118): expose version/gitSha/buildDate on health endpoint (#42) --- .docker/api.Dockerfile | 11 ++++- .docker/worker.Dockerfile | 11 ++++- .../modules/app/__tests__/app.service.spec.ts | 49 +++++++++++++++++++ apps/api/src/modules/app/app.service.ts | 11 +++++ apps/api/src/modules/app/health.type.ts | 15 ++++++ ci/build-images.sh | 8 +++ 6 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/modules/app/__tests__/app.service.spec.ts diff --git a/.docker/api.Dockerfile b/.docker/api.Dockerfile index b980d22..0cdec14 100644 --- a/.docker/api.Dockerfile +++ b/.docker/api.Dockerfile @@ -110,7 +110,16 @@ RUN pnpm --filter @cv/core run prisma:generate # Copy the api's tsc -b output. COPY --from=builder /app/apps/api/dist/ ./apps/api/dist/ -ENV NODE_ENV=production +# Build-info baked into the image, surfaced at runtime via AppService -> +# /health + Query.health. Defaults keep local docker-builds usable without +# --build-arg. See CVG-118. +ARG VERSION=dev +ARG GIT_SHA=unknown +ARG BUILD_DATE=unknown +ENV APP_VERSION=$VERSION \ + APP_GIT_SHA=$GIT_SHA \ + APP_BUILD_DATE=$BUILD_DATE \ + NODE_ENV=production EXPOSE 3000 WORKDIR /app/apps/api diff --git a/.docker/worker.Dockerfile b/.docker/worker.Dockerfile index 204a4cb..ea4a411 100644 --- a/.docker/worker.Dockerfile +++ b/.docker/worker.Dockerfile @@ -97,7 +97,16 @@ RUN cd apps/worker && pnpm exec playwright install --with-deps chromium # Copy the worker's tsc -b output. COPY --from=builder /app/apps/worker/dist/ ./apps/worker/dist/ -ENV NODE_ENV=production +# Mirrors the api Dockerfile's build-info env vars (CVG-118) so worker +# log lines / Sentry events tag which build emitted them. No HTTP surface +# here, but the convention stays uniform across both images. +ARG VERSION=dev +ARG GIT_SHA=unknown +ARG BUILD_DATE=unknown +ENV APP_VERSION=$VERSION \ + APP_GIT_SHA=$GIT_SHA \ + APP_BUILD_DATE=$BUILD_DATE \ + NODE_ENV=production WORKDIR /app/apps/worker CMD ["node", "dist/main.js", "project-q:work", "async"] diff --git a/apps/api/src/modules/app/__tests__/app.service.spec.ts b/apps/api/src/modules/app/__tests__/app.service.spec.ts new file mode 100644 index 0000000..20575a9 --- /dev/null +++ b/apps/api/src/modules/app/__tests__/app.service.spec.ts @@ -0,0 +1,49 @@ +import type { ClockService } from "@cv/core"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { AppService } from "../app.service"; + +const fixedNow = new Date("2026-05-17T12:00:00.000Z"); +const clockStub: ClockService = { now: () => fixedNow }; + +const ORIGINAL_ENV = { ...process.env }; + +const resetEnv = (): void => { + delete process.env["APP_VERSION"]; + delete process.env["APP_GIT_SHA"]; + delete process.env["APP_BUILD_DATE"]; +}; + +describe("AppService.getHealth", () => { + beforeEach(resetEnv); + afterEach(() => { + process.env = { ...ORIGINAL_ENV }; + }); + + it("defaults version/gitSha/buildDate to dev sentinels when env is unset", () => { + const service = new AppService(clockStub); + const health = service.getHealth(); + expect(health.version).toBe("dev"); + expect(health.gitSha).toBe("unknown"); + expect(health.buildDate).toBe("unknown"); + }); + + it("reflects APP_* env vars when set at construction", () => { + process.env["APP_VERSION"] = "0.1.13"; + process.env["APP_GIT_SHA"] = "abc123def"; + process.env["APP_BUILD_DATE"] = "2026-05-17T14:00:00Z"; + const service = new AppService(clockStub); + const health = service.getHealth(); + expect(health.version).toBe("0.1.13"); + expect(health.gitSha).toBe("abc123def"); + expect(health.buildDate).toBe("2026-05-17T14:00:00Z"); + }); + + it("returns status/timestamp/timezone/uptime as before", () => { + const service = new AppService(clockStub); + const health = service.getHealth(); + expect(health.status).toBe("ok"); + expect(health.timestamp).toBe(fixedNow.toISOString()); + expect(typeof health.timezone).toBe("string"); + expect(health.uptime).toBe(0); + }); +}); diff --git a/apps/api/src/modules/app/app.service.ts b/apps/api/src/modules/app/app.service.ts index c1eb738..47478ce 100644 --- a/apps/api/src/modules/app/app.service.ts +++ b/apps/api/src/modules/app/app.service.ts @@ -6,11 +6,19 @@ export interface HealthResponse { timestamp: string; timezone: string; uptime: number; + version: string; + gitSha: string; + buildDate: string; } @Injectable() export class AppService { private readonly startedAt: Date; + // APP_* env vars are injected by .docker/api.Dockerfile at build time + // (see ci/build-images.sh). Local dev falls back to the literal strings. + private readonly version = process.env["APP_VERSION"] ?? "dev"; + private readonly gitSha = process.env["APP_GIT_SHA"] ?? "unknown"; + private readonly buildDate = process.env["APP_BUILD_DATE"] ?? "unknown"; constructor(private readonly clock: ClockService) { this.startedAt = clock.now(); @@ -23,6 +31,9 @@ export class AppService { timestamp: now.toISOString(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, uptime: now.getTime() - this.startedAt.getTime(), + version: this.version, + gitSha: this.gitSha, + buildDate: this.buildDate, }; } } diff --git a/apps/api/src/modules/app/health.type.ts b/apps/api/src/modules/app/health.type.ts index 36e22ae..853f4be 100644 --- a/apps/api/src/modules/app/health.type.ts +++ b/apps/api/src/modules/app/health.type.ts @@ -14,15 +14,30 @@ export class HealthResponse { @Field(() => Number) uptime: number; + @Field(() => String) + version: string; + + @Field(() => String) + gitSha: string; + + @Field(() => String) + buildDate: string; + constructor(data: { status: string; timestamp: string; timezone: string; uptime: number; + version: string; + gitSha: string; + buildDate: string; }) { this.status = data.status; this.timestamp = data.timestamp; this.timezone = data.timezone; this.uptime = data.uptime; + this.version = data.version; + this.gitSha = data.gitSha; + this.buildDate = data.buildDate; } } diff --git a/ci/build-images.sh b/ci/build-images.sh index 1b21866..26b6167 100755 --- a/ci/build-images.sh +++ b/ci/build-images.sh @@ -15,6 +15,11 @@ set -euo pipefail REGISTRY="${REGISTRY:-ghcr.io}" IMAGE_BASE="${IMAGE_BASE:-${REGISTRY}/riotbyte-com/cv-generator}" VERSION="${1:-${VERSION:-$(git rev-parse --short HEAD)}}" +# Build-info metadata baked into the image (CVG-118). Exposed at runtime +# via process.env.APP_VERSION/APP_GIT_SHA/APP_BUILD_DATE for the health +# endpoint + SPA footer. +GIT_SHA="${GIT_SHA:-$(git rev-parse HEAD)}" +BUILD_DATE="${BUILD_DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" : "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" @@ -52,6 +57,9 @@ build_and_push() { --platform "${PLATFORM:-linux/amd64}" \ --target production \ --secret "id=github_token,env=GITHUB_TOKEN" \ + --build-arg "VERSION=${VERSION}" \ + --build-arg "GIT_SHA=${GIT_SHA}" \ + --build-arg "BUILD_DATE=${BUILD_DATE}" \ "${tag_args[@]}" \ --push \ . -- 2.51.2