commit 539d615aef3390a8eae087be907cded1328bbdc8
parent 6db57f62bd7cd8c2d2fbf5893f7f3197fca25e90
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 08:50:04 -0400
publish: stamps fall back to the image's ARCHILYZER_COMMIT/BRANCH without a .git; deploy-homepage --to local writes ARCHILYZER_HOMEPAGE_OUT
Both names are release 18 S5's (declared in lib/envVars.ts there, with
imageBuildFacts); read by name here until the two slices are merged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 42 insertions(+), 12 deletions(-)
diff --git a/common/publish/stageBodies.ts b/common/publish/stageBodies.ts
@@ -33,6 +33,7 @@ import {
ALL_TARGET,
HOMEPAGE_TARGET,
HUB_TARGET,
+ imageBuildFacts,
newStampId,
readBuiltStamp,
readDeployedFile,
@@ -93,12 +94,16 @@ async function git(cwd: string, args: string[]): Promise<string | null> {
}
}
-/** The checkout's commit and branch (null where there is no repository). */
-export async function checkoutInfo(paths: Pick<Paths, "monorepoRoot">): Promise<{
- commit: string | null;
- branch: string | null;
-}> {
+/**
+ * The checkout's commit and branch; where there is no repository (the runtime
+ * image), the commit and branch the image was built from; else null.
+ */
+export async function checkoutInfo(
+ paths: Pick<Paths, "monorepoRoot">,
+ env: NodeJS.ProcessEnv = process.env,
+): Promise<{ commit: string | null; branch: string | null }> {
const commit = await git(paths.monorepoRoot, ["rev-parse", "HEAD"]);
+ if (commit === null) return imageBuildFacts(env);
const branch = await git(paths.monorepoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
return { commit, branch: branch === "HEAD" ? null : branch };
}
@@ -506,10 +511,12 @@ export function localSiteOut(env: NodeJS.ProcessEnv = process.env): string | nul
return env.ARCHILYZER_SITE_OUT?.trim() || null;
}
-/** …and the homepage: `homepage` beside it (the `homepage` service's mount). */
+// …and the homepage: what the `homepage` service serves (release 18 S5
+// declares the name; read by name here until both slices are merged).
+const HOMEPAGE_OUT_NAME = "ARCHILYZER_HOMEPAGE_OUT";
+
export function localHomepageOut(env: NodeJS.ProcessEnv = process.env): string | null {
- const site = localSiteOut(env);
- return site ? path.join(path.dirname(site), "homepage") : null;
+ return env[HOMEPAGE_OUT_NAME]?.trim() || null;
}
// Replace the CONTENTS of `dest` (a volume mount: never the directory itself).
@@ -537,7 +544,7 @@ async function deployStage(
r: StageRequest,
target: string,
ship: (o: { onLog: (l: string) => void; previewBranch?: string }) => Promise<void>,
- local: { src: string; dest: string | null } | null,
+ local: { src: string; dest: string | null; needs: string } | null,
project: string | null,
): Promise<StageOutcome> {
const { paths, onLog, signal } = ctx;
@@ -548,7 +555,7 @@ async function deployStage(
if (kind === "local") {
if (!local) throw new StageFailure(`${target} has no local target`, 2);
if (!local.dest) {
- throw new StageFailure("--to local needs ARCHILYZER_SITE_OUT (the directory the docker `site` service serves)", 3);
+ throw new StageFailure(`--to local needs ${local.needs}`, 3);
}
// A bundle built private is never published, here either.
const priv = builtAudienceProblem(local.src);
@@ -660,7 +667,7 @@ export async function runStageBody(ctx: StageContext, r: StageRequest): Promise<
outDir: out,
stagingDir: b.dockerSiteStagingDir(paths, site.siteId),
}),
- { src: out, dest: localSiteOut() },
+ { src: out, dest: localSiteOut(), needs: "ARCHILYZER_SITE_OUT (the directory the docker `site` service serves)" },
site.cloudflareProject?.trim() || null,
);
}
@@ -686,7 +693,7 @@ export async function runStageBody(ctx: StageContext, r: StageRequest): Promise<
r,
HOMEPAGE_TARGET,
(o) => b.deployHomepage({ paths, signal: ctx.signal, onLog: o.onLog, previewBranch: o.previewBranch }),
- { src: b.homepageOutDir(paths), dest: localHomepageOut() },
+ { src: b.homepageOutDir(paths), dest: localHomepageOut(), needs: "ARCHILYZER_HOMEPAGE_OUT (the directory the docker `homepage` service serves)" },
b.HOMEPAGE_PAGES_PROJECT,
);
}
diff --git a/common/publish/stamps.test.ts b/common/publish/stamps.test.ts
@@ -10,6 +10,7 @@ import {
builtStampPath,
deployRecordFor,
deployedPath,
+ imageBuildFacts,
indexStampPath,
newStampId,
readBuiltStamp,
@@ -123,6 +124,12 @@ test("recordDeploy keeps every other record, and deployRecordFor finds each kind
}
});
+test("imageBuildFacts: the image's commit and branch, empty = null", () => {
+ assert.deepEqual(imageBuildFacts({ ARCHILYZER_COMMIT: " abc ", ARCHILYZER_BRANCH: "" }), { commit: "abc", branch: null });
+ assert.deepEqual(imageBuildFacts({}), { commit: null, branch: null });
+ assert.deepEqual(imageBuildFacts({ ARCHILYZER_COMMIT: "c", ARCHILYZER_BRANCH: "main" }), { commit: "c", branch: "main" });
+});
+
test("newStampId is unique and sorts by time", () => {
const a = newStampId(1_000);
const b = newStampId(2_000);
diff --git a/common/publish/stamps.ts b/common/publish/stamps.ts
@@ -135,6 +135,22 @@ export function deployedPath(paths: Pick<Paths, "exportBuildsDir">, target: stri
return path.join(targetDir(paths, target), "deployed.json");
}
+// The runtime image's build facts (Dockerfile build args → ENV): the stamps'
+// `commit` / `branch` where there is no .git to ask. Release 18 S5 declares
+// the two names and exports the same helper (`imageBuildFacts`,
+// lib/envVars.ts); this local one is swapped for it in one line once both
+// slices are merged. Empty = null.
+const IMAGE_COMMIT_NAME = "ARCHILYZER_COMMIT";
+const IMAGE_BRANCH_NAME = "ARCHILYZER_BRANCH";
+
+export function imageBuildFacts(env: NodeJS.ProcessEnv = process.env): {
+ commit: string | null;
+ branch: string | null;
+} {
+ const v = (k: string) => env[k]?.trim() || null;
+ return { commit: v(IMAGE_COMMIT_NAME), branch: v(IMAGE_BRANCH_NAME) };
+}
+
/** A fresh, sortable, unique stamp id. */
export function newStampId(now = Date.now()): string {
return `${now.toString(36).padStart(9, "0")}-${randomBytes(4).toString("hex")}`;