Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions docs/guide/instance-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions src/app/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -126,7 +126,7 @@ export default async function SignInPage({
</div>

<div className="flex items-center justify-between pt-6 font-mono text-[0.6875rem] text-muted-foreground">
<span>forge v{APP_VERSION} · self-hosted</span>
<span>forge v{appVersion} · self-hosted</span>
<span>{host}</span>
</div>
</section>
Expand Down
48 changes: 48 additions & 0 deletions src/server/routers/__tests__/instance-admin-system.test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
30 changes: 17 additions & 13 deletions src/server/routers/instance-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
resolveSubjectLabels,
subjectKey,
} from "@/server/services/subject-labels";
import { forgeBuildIdentity } from "@/server/build-info";

const slugSchema = z
.string()
Expand Down Expand Up @@ -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 },
};
}),
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/build-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
_resetBuildInfoCacheForTests,
forgeBuildIdentity,
mcpServerInfo,
readPackageVersion,
} from "@/server/build-info";
import packageJson from "../../package.json";

describe("build identity", () => {
afterEach(() => {
Expand All @@ -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");
Expand Down
Loading