diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0e7683e..79bab1e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -84,6 +84,12 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Resolve build metadata + run: | + version=$(bun -e 'const pkg = JSON.parse(await Bun.file("package.json").text()); console.log(pkg.version)') + echo "NEXT_PUBLIC_APP_VERSION=$version" >> "$GITHUB_ENV" + echo "NEXT_PUBLIC_GIT_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV" + - name: Install Playwright browser deps run: bunx playwright install --with-deps chromium diff --git a/TODO.md b/TODO.md index a999fba..10f3bd8 100644 --- a/TODO.md +++ b/TODO.md @@ -101,8 +101,8 @@ This file is the single source of truth for roadmap and delivery progress. - [x] [P1] Source of truth for version (`package.json` root) and release tagging rules (`vX.Y.Z`) - [x] [P1] Build metadata policy for git hash (`+sha.`) in app runtime footer - [x] [P1] App footer implementation plan for version + commit hash (admin + web) -- [~] [P2] Automated version injection in CI (stamping build from tag + commit hash) -- [ ] [P2] Validation tests for displayed version/hash consistency per deployment +- [x] [P2] Automated version injection in CI (stamping build from tag + commit hash) +- [x] [P2] Validation tests for displayed version/hash consistency per deployment - [x] [P1] Release tagging and changelog publication policy in CI ### MVP0 Close-Out Checklist @@ -111,8 +111,8 @@ This file is the single source of truth for roadmap and delivery progress. - [ ] [P1] Run first staging deployment against a real host with deploy workflow and document result - [ ] [P1] Replace release workflow placeholders with real release-notes and rollback execution steps - [x] [P1] Expose runtime version + short git hash in admin and public app footer -- [ ] [P2] Add CI build stamping for version/hash values consumed by app footers -- [ ] [P2] Add automated tests validating displayed version/hash format and consistency +- [x] [P2] Add CI build stamping for version/hash values consumed by app footers +- [x] [P2] Add automated tests validating displayed version/hash format and consistency ## MVP 1: Core CMS Business Features diff --git a/apps/admin/src/lib/build-info.test.ts b/apps/admin/src/lib/build-info.test.ts new file mode 100644 index 0000000..493b9c7 --- /dev/null +++ b/apps/admin/src/lib/build-info.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it, vi } from "vitest" + +import { getBuildInfo } from "./build-info" + +afterEach(() => { + vi.unstubAllEnvs() +}) + +describe("getBuildInfo (admin)", () => { + it("returns fallback values when env is missing", () => { + vi.stubEnv("NEXT_PUBLIC_APP_VERSION", "") + vi.stubEnv("NEXT_PUBLIC_GIT_SHA", "") + + expect(getBuildInfo()).toEqual({ + version: "0.0.1-dev", + sha: "local", + }) + }) + + it("uses env values and truncates git sha", () => { + vi.stubEnv("NEXT_PUBLIC_APP_VERSION", "0.2.0") + vi.stubEnv("NEXT_PUBLIC_GIT_SHA", "abcdef123456") + + expect(getBuildInfo()).toEqual({ + version: "0.2.0", + sha: "abcdef1", + }) + }) +}) diff --git a/apps/web/src/lib/build-info.test.ts b/apps/web/src/lib/build-info.test.ts new file mode 100644 index 0000000..d895275 --- /dev/null +++ b/apps/web/src/lib/build-info.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it, vi } from "vitest" + +import { getBuildInfo } from "./build-info" + +afterEach(() => { + vi.unstubAllEnvs() +}) + +describe("getBuildInfo (web)", () => { + it("returns fallback values when env is missing", () => { + vi.stubEnv("NEXT_PUBLIC_APP_VERSION", "") + vi.stubEnv("NEXT_PUBLIC_GIT_SHA", "") + + expect(getBuildInfo()).toEqual({ + version: "0.0.1-dev", + sha: "local", + }) + }) + + it("uses env values and truncates git sha", () => { + vi.stubEnv("NEXT_PUBLIC_APP_VERSION", "0.2.0") + vi.stubEnv("NEXT_PUBLIC_GIT_SHA", "123456789abc") + + expect(getBuildInfo()).toEqual({ + version: "0.2.0", + sha: "1234567", + }) + }) +}) diff --git a/e2e/smoke.pw.ts b/e2e/smoke.pw.ts index 7db0cb0..0efd71a 100644 --- a/e2e/smoke.pw.ts +++ b/e2e/smoke.pw.ts @@ -1,10 +1,13 @@ import { expect, test } from "@playwright/test" +const BUILD_INFO_PATTERN = /Build v\S+ \+sha\.[a-z0-9]{5,7}/i + test("smoke", async ({ page }, testInfo) => { await page.goto("/") if (testInfo.project.name === "web-chromium") { await expect(page.getByRole("heading", { name: /your next\.js cms frontend/i })).toBeVisible() + await expect(page.getByText(BUILD_INFO_PATTERN)).toBeVisible() return } @@ -12,6 +15,7 @@ test("smoke", async ({ page }, testInfo) => { if (await dashboardHeading.isVisible({ timeout: 2000 })) { await expect(dashboardHeading).toBeVisible() + await expect(page.getByText(BUILD_INFO_PATTERN)).toBeVisible() return }