From 3fc71d20d6b6cdafb1ae5b214c72635055b0fc7c Mon Sep 17 00:00:00 2001 From: Bailey Dixon Date: Sat, 25 Jul 2026 11:37:59 -0400 Subject: [PATCH] fix: unify Forge build version reporting --- DEVLOG.md | 11 +++++ docs/guide/instance-admin.md | 8 ++-- src/app/(auth)/signin/page.tsx | 6 +-- .../__tests__/instance-admin-system.test.ts | 48 +++++++++++++++++++ src/server/routers/instance-admin.ts | 30 +++++++----- tests/unit/build-info.test.ts | 8 ++++ 6 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 src/server/routers/__tests__/instance-admin-system.test.ts diff --git a/DEVLOG.md b/DEVLOG.md index 597f1b17..0a9c386a 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,17 @@ > Append-only session log. Read at session start. Update at session end. +## 2026-07-25 — Canonical build version across system surfaces + +- Replaced the Admin System router's independent `npm_package_version` / + `1.0.0` fallback with Forge's shared build-identity reader, so its version, + SHA, and build time now use the same runtime source as the application shell + and MCP server metadata. +- Removed the sign-in footer's hardcoded `1.0.0`; unauthenticated operators now + see the packaged application version too. +- Added unit and instance-admin integration coverage for the standalone-runtime + case where npm lifecycle metadata is absent. + ## 2026-07-25 — v0.29.7 release preparation Prepared the serialized patch release for AXI-151 after implementation PR #87 diff --git a/docs/guide/instance-admin.md b/docs/guide/instance-admin.md index 74878897..d160b50f 100644 --- a/docs/guide/instance-admin.md +++ b/docs/guide/instance-admin.md @@ -33,9 +33,11 @@ The admin shell has a fixed set of pages, each backed by an ### Overview / System (`/admin/system`) Build identity and instance-wide rollups. Reports the running -`version`, `buildSha`, and `buildTime` (baked into the Docker image as -`FORGE_GIT_SHA` / `FORGE_BUILD_TIME` — the same source as -`system.buildInfo` and the Settings → About line), plus counts: +`version`, `buildSha`, and `buildTime`. The packaged `package.json` version is +canonical when npm lifecycle metadata is unavailable; the Docker image bakes +`FORGE_GIT_SHA` / `FORGE_BUILD_TIME`. These are the same sources used by the +sign-in footer, `system.buildInfo`, MCP metadata, and the Settings → About line. +The page also reports counts for: tenants, users, admins, runtimes, profiles, connections, and runs in the last 24h. diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx index a6a8b4ac..cb43a53a 100644 --- a/src/app/(auth)/signin/page.tsx +++ b/src/app/(auth)/signin/page.tsx @@ -6,8 +6,7 @@ import { Input } from "@/components/ui/input"; import { Kbd } from "@/components/ui/kbd"; import { LiveStatusPanel, LiveLoopCard } from "./live-status-panel"; import { listEnabledSsoProviders, type PublicSsoProvider } from "@/server/sso"; - -const APP_VERSION = "1.0.0"; +import { readPackageVersion } from "@/server/build-info"; /** Short mono badge shown in the provider button. */ function providerBadge(p: PublicSsoProvider): string { @@ -65,6 +64,7 @@ export default async function SignInPage({ const providers = await listEnabledSsoProviders(); const hasProviders = providers.length > 0; const host = instanceHost(); + const appVersion = await readPackageVersion(); async function credentialsAction(formData: FormData) { "use server"; @@ -126,7 +126,7 @@ export default async function SignInPage({
- forge v{APP_VERSION} · self-hosted + forge v{appVersion} · self-hosted {host}
diff --git a/src/server/routers/__tests__/instance-admin-system.test.ts b/src/server/routers/__tests__/instance-admin-system.test.ts new file mode 100644 index 00000000..72c4f763 --- /dev/null +++ b/src/server/routers/__tests__/instance-admin-system.test.ts @@ -0,0 +1,48 @@ +import { InstanceRole } from "@prisma/client"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { instanceAdminRouter } from "@/server/routers/instance-admin"; +import { + buildContext, + createWorkspaceFixture, + disconnectPrisma, + getPrisma, + type TestFixture, +} from "@/server/routers/__tests__/helpers"; +import { _resetBuildInfoCacheForTests } from "@/server/build-info"; +import packageJson from "../../../../package.json"; + +describe("instanceAdmin.system", () => { + let fixture: TestFixture; + + beforeEach(async () => { + fixture = await createWorkspaceFixture({ keyPrefix: "IA" }); + await getPrisma().user.update({ + where: { id: fixture.user.id }, + data: { instanceRole: InstanceRole.INSTANCE_ADMIN }, + }); + }); + + afterEach(async () => { + vi.unstubAllEnvs(); + _resetBuildInfoCacheForTests(); + await fixture.cleanup(); + }); + + afterAll(async () => { + await disconnectPrisma(); + }); + + it("uses the canonical packaged version and baked build metadata", async () => { + vi.stubEnv("npm_package_version", ""); + vi.stubEnv("FORGE_GIT_SHA", "abc1234"); + vi.stubEnv("FORGE_BUILD_TIME", "2026-07-25T15:05:43Z"); + + const caller = instanceAdminRouter.createCaller(await buildContext(fixture)); + + await expect(caller.system()).resolves.toMatchObject({ + version: packageJson.version, + buildSha: "abc1234", + buildTime: "2026-07-25T15:05:43Z", + }); + }); +}); diff --git a/src/server/routers/instance-admin.ts b/src/server/routers/instance-admin.ts index 8fa1ad23..989c9647 100644 --- a/src/server/routers/instance-admin.ts +++ b/src/server/routers/instance-admin.ts @@ -11,6 +11,7 @@ import { resolveSubjectLabels, subjectKey, } from "@/server/services/subject-labels"; +import { forgeBuildIdentity } from "@/server/build-info"; const slugSchema = z .string() @@ -185,20 +186,23 @@ export const instanceAdminRouter = router({ /** System / build info + instance-wide counts for the admin overview. */ system: instanceAdminProcedure.query(async ({ ctx }) => { - const [tenants, users, admins, runtimes, profiles, connections, runs24] = await Promise.all([ - ctx.db.workspace.count({ where: { deletedAt: null } }), - ctx.db.user.count(), - ctx.db.user.count({ where: { instanceRole: "INSTANCE_ADMIN" } }), - ctx.db.runtime.count({ where: { archivedAt: null } }), - ctx.db.agentProfile.count({ where: { archivedAt: null } }), - ctx.db.connection.count(), - ctx.db.agentRun.count({ where: { startedAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } } }), - ]); + const [build, tenants, users, admins, runtimes, profiles, connections, runs24] = + await Promise.all([ + forgeBuildIdentity(), + ctx.db.workspace.count({ where: { deletedAt: null } }), + ctx.db.user.count(), + ctx.db.user.count({ where: { instanceRole: "INSTANCE_ADMIN" } }), + ctx.db.runtime.count({ where: { archivedAt: null } }), + ctx.db.agentProfile.count({ where: { archivedAt: null } }), + ctx.db.connection.count(), + ctx.db.agentRun.count({ + where: { startedAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } }, + }), + ]); return { - // Match system.buildInfo: the Dockerfile bakes FORGE_GIT_SHA / FORGE_BUILD_TIME. - version: process.env.npm_package_version ?? "1.0.0", - buildSha: process.env.FORGE_GIT_SHA || null, - buildTime: process.env.FORGE_BUILD_TIME || null, + version: build.version, + buildSha: build.gitSha, + buildTime: build.buildTime, counts: { tenants, users, admins, runtimes, profiles, connections, runs24 }, }; }), diff --git a/tests/unit/build-info.test.ts b/tests/unit/build-info.test.ts index cf323771..9ac9fc1d 100644 --- a/tests/unit/build-info.test.ts +++ b/tests/unit/build-info.test.ts @@ -3,7 +3,9 @@ import { _resetBuildInfoCacheForTests, forgeBuildIdentity, mcpServerInfo, + readPackageVersion, } from "@/server/build-info"; +import packageJson from "../../package.json"; describe("build identity", () => { afterEach(() => { @@ -23,6 +25,12 @@ describe("build identity", () => { }); }); + it("reads the packaged application version when npm lifecycle metadata is absent", async () => { + vi.stubEnv("npm_package_version", ""); + + await expect(readPackageVersion()).resolves.toBe(packageJson.version); + }); + it("includes build identity in MCP serverInfo", async () => { vi.stubEnv("npm_package_version", "9.8.7"); vi.stubEnv("FORGE_GIT_SHA", "abc1234");