commit 948de03474bd936018a0669a57bf451a09c730dc
parent f4d1e563d917676154d6df091cbfcabbccd6627c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 08:57:56 -0400
common: the homepage's local deploy is ARCHILYZER_HOMEPAGE_OUT; the preflight is a token or a wrangler login; a stale posts manifest goes
- deploy-homepage --to local copies into ARCHILYZER_HOMEPAGE_OUT (the
container sets it) and is refused, in its own sentence, without it — no
fallback beside ARCHILYZER_SITE_OUT. Its test also pins the source gate.
- cloudflareCredentialProblem takes CLOUDFLARE_API_TOKEN or a wrangler
login on disk; the global key pair is not offered.
- compose-site: with tombstones to write and no posts manifest from the
index, the posts manifest an earlier compose left is replaced by an
empty one (it could list another site's channels).
- envVars.ts: WRANGLER_BIN, E2E_LIVE_CHECK and E2E_FAKE_WRANGLER_AUTH_FAIL
are this slice's rows; CLOUDFLARE_API_TOKEN, XDG_CONFIG_HOME and
ARCHILYZER_HOMEPAGE_OUT are S5's, carried here (marked) until S5 merges,
away from S5's hunks. ENVIRONMENT.md regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
8 files changed, 95 insertions(+), 40 deletions(-)
diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md
@@ -59,11 +59,6 @@ Tokens, credentials and knobs a running process reads. Most configuration is not
| `SYNC_TICK_TOKEN` | unset (no auth) | Bearer token for the tick endpoint; set on both the editor and the cron job. | common/bin/sync-tick.ts, editor/app/scheduler/auth.ts |
| `R2_ACCESS_KEY_ID` | — | R2 S3 credentials for uploading oversize archives at deploy time (with `R2_SECRET_ACCESS_KEY` and `CLOUDFLARE_ACCOUNT_ID`). See [PUBLISH.md](PUBLISH.md). | common/publish/build.ts |
| `R2_SECRET_ACCESS_KEY` | — | See `R2_ACCESS_KEY_ID`. | common/publish/build.ts |
-| `CLOUDFLARE_API_TOKEN` | — (a `wrangler login` on this machine instead) | The Cloudflare API token every Pages deploy runs wrangler with. With no token, no key pair and no `wrangler login`, a deploy is refused before wrangler runs; a token Cloudflare rejects ends the deploy on "REFUSED by Cloudflare". Never printed. | common/lib/pagesDeploy.ts, wrangler |
-| `CLOUDFLARE_API_KEY` | — | wrangler's global-key alternative to `CLOUDFLARE_API_TOKEN`, with `CLOUDFLARE_EMAIL`. | common/lib/pagesDeploy.ts, wrangler |
-| `CLOUDFLARE_EMAIL` | — | See `CLOUDFLARE_API_KEY`. | common/lib/pagesDeploy.ts, wrangler |
-| `WRANGLER_BIN` | `common/node_modules/.bin/wrangler` (the pinned devDependency) | The wrangler a deploy spawns. The editor's e2e suite points it at its fake. | common/lib/pagesDeploy.ts (wranglerBin) |
-| `XDG_CONFIG_HOME` | `~/.config` | Where a deploy's credential check looks for a `wrangler login` (`<it>/.wrangler/config/default.toml`, beside `~/.wrangler`). | common/lib/pagesDeploy.ts |
| `CLOUDFLARE_ACCOUNT_ID` | — | The account the R2 endpoint belongs to. wrangler reads its own credentials. | common/publish/build.ts |
| `DOCKER_BIN` | `docker` | The container engine for docker-mode builds (e.g. `podman`). | common/publish/build.ts |
| `DOCKER_BUILD_MEMORY` | no cap | Per-container memory cap for a docker-mode build (`--memory`). | common/publish/build.ts |
@@ -71,6 +66,9 @@ Tokens, credentials and knobs a running process reads. Most configuration is not
| `ARCHIVE_CHANNEL_CONCURRENCY` | `4` | How many channels' archive zips `build archives` builds at once. | common/bin/build-archives.ts |
| `HOST` | every interface | The address `pnpm start:export` (serve-out) listens on; `127.0.0.1` keeps a private site on this machine. | export/scripts/serve-out.mjs |
| `MAX_ARCHIVE_BYTES` | the Cloudflare-safe cap | The served-file size cap for archives, in bytes; `0` = no cap. A site's own `archiveMaxBytes` wins. | common/bin/compose-site.ts |
+| `WRANGLER_BIN` | `common/node_modules/.bin/wrangler` (the pinned devDependency) | The wrangler a deploy spawns. The editor's e2e suite points it at its fake. | common/lib/pagesDeploy.ts (wranglerBin), common/publish/deployStage.ts |
+| `CLOUDFLARE_API_TOKEN` | unset (a `wrangler login` on a host) | The API token every deploy runs wrangler with. With none and no `wrangler login`, a deploy is refused before wrangler runs; one Cloudflare rejects ends the deploy on "REFUSED by Cloudflare". Never printed. | wrangler (every deploy), common/lib/pagesDeploy.ts (set or not, never the value) |
+| `XDG_CONFIG_HOME` | `~/.config` | Where `wrangler login` keeps its config (`<it>/.wrangler/config/default.toml`); a deploy's credential check looks for it there, by path. | common/lib/pagesDeploy.ts |
| `CHOUGH_BIN` | `chough` on PATH | The chough transcription engine, when a worker names no binary. | common/lib/transcriptionApps.ts |
| `CHOUGH_MODEL` | chough's own | Passed to chough from a worker's model field; chough auto-downloads one when unset. | chough (set by common/lib/transcriptionApps.ts) |
| `CHOUGH_URL` | local | Passed to chough from a worker's remote-server field. | chough (set by common/lib/transcriptionApps.ts) |
@@ -154,8 +152,7 @@ The container's own set, read by `docker/*.sh`, the compose files and Caddy —
| `ARCHILYZER_FETCH_MODEL` | per transcriber | Which model the first boot downloads; `none` skips it. | docker/entrypoint.sh |
| `ARCHILYZER_MODELS_DIR` | `/data/models` | Where models live in the container. | docker/entrypoint.sh |
| `ARCHILYZER_BUILDS_DIR` | `/data/builds` | Where the container keeps built sites. | docker/entrypoint.sh |
-| `ARCHILYZER_SITE_OUT` | `/data/builds/site` | The built export site the `site` service serves; `deploy <id> --to local` copies a site's bundle here. | docker/entrypoint.sh, docker/publish-site.sh, common/publish/deployStage.ts |
-| `ARCHILYZER_HOMEPAGE_OUT` | `homepage/` beside `ARCHILYZER_SITE_OUT` | Where `deploy homepage --to local` copies the homepage's bundle. | common/publish/deployStage.ts |
+| `ARCHILYZER_SITE_OUT` | `/data/builds/site` | The built export site the `site` service serves. | docker/entrypoint.sh, docker/publish-site.sh |
| `ARCHILYZER_IDLE_BOOT` | off | `1` boots the editor without arming the heartbeat or any auto-queue runner. | common/lib/idleBoot.ts (the editor) |
| `ARCHILYZER_AUTH_MODE` | `basic` | `basic`, `forward` or `none` — the only escape hatch from the exposure guard. | docker/guard-exposure.sh, docker/caddy-start.sh |
| `ARCHILYZER_AUTH_USER` | `archilyzer` | Basic-auth user. | docker/Caddyfile |
@@ -164,6 +161,7 @@ The container's own set, read by `docker/*.sh`, the compose files and Caddy —
| `ARCHILYZER_FORWARD_AUTH_UPSTREAM` | — | Forward-auth server (Authelia, tinyauth, …), `host:port`. | docker/Caddyfile |
| `ARCHILYZER_FORWARD_AUTH_URI` | `/api/auth/caddy` | The forward-auth server's verify path. | docker/Caddyfile |
| `ARCHILYZER_TAG` | `local` | The image tag the compose files build and run. | docker-compose*.yml |
+| `ARCHILYZER_HOMEPAGE_OUT` | `/data/builds/homepage` | The locally deployed homepage: `deploy homepage --to local` copies its bundle here. | common/publish/deployStage.ts |
## Tests only
diff --git a/common/bin/compose-site.postsVisibility.test.ts b/common/bin/compose-site.postsVisibility.test.ts
@@ -391,4 +391,20 @@ test("a public site whose only posts were X posts ships an empty posts manifest
assert.deepEqual(xonly.tombstones, [X]);
assert.equal(xonly.corpus.postScheme, undefined);
assert.deepEqual(slugs(xonly.corpus.channels), [VIDEOS]);
+
+ // With no posts manifest from the index at all, the one an earlier compose
+ // left in public/ (here: listing the Bluesky channel) is replaced by an
+ // empty one beside the tombstone.
+ rmSync(path.join(paths.exportSitesIndexDir, "xonly", "posts", "manifest.json"));
+ writeJson(path.join(paths.exportPostsDir, "manifest.json"), {
+ version: 1,
+ channels: [{ slug: SKY, name: SKY, postCount: 1, platform: "bluesky" }],
+ totalCount: 1,
+ generatedAt: "2026-01-01T00:00:00.000Z",
+ });
+ const bare = await compose("xonly");
+ assert.deepEqual(bare.postsManifest.channels, []);
+ assert.equal(bare.postsManifest.totalCount, 0);
+ assert.deepEqual(bare.tombstones, [X]);
+ assert.equal(bare.corpus.postScheme, undefined);
});
diff --git a/common/bin/compose-site.ts b/common/bin/compose-site.ts
@@ -1009,8 +1009,10 @@ export async function main(
// may still be cached at the edge from a build when X was public): every
// such path is REPLACED with an empty object of the same shape and served
// no-store, never left out (publish/tombstones.ts). The site posts manifest
- // above already lists no withheld channel; a site whose only posts were X
- // posts, with no manifest from the index, still ships an empty one.
+ // above already lists no withheld channel; a site with no posts manifest from
+ // the index (its only posts were X posts) ships an empty one — replacing
+ // whatever an earlier compose left at that path (another site's, listing its
+ // channels).
const memberSet = new Set(memberSlugs);
const tombstones = await writePostsTombstones({
postsDir: paths.exportPostsDir,
@@ -1018,10 +1020,9 @@ export async function main(
slugs: site.channels.map((c) => c.slug).filter((slug) => !memberSet.has(slug)),
});
if (tombstones.length > 0) {
- const postsManifest = path.join(paths.exportPostsDir, "manifest.json");
- if (!(await exists(postsManifest))) {
+ if (!(await exists(postsManifestSrc))) {
await writePublicFile(
- postsManifest,
+ path.join(paths.exportPostsDir, "manifest.json"),
JSON.stringify(emptyPostsManifest(new Date().toISOString(), siteId)),
);
}
diff --git a/common/lib/envVars.ts b/common/lib/envVars.ts
@@ -95,11 +95,6 @@ const DECLARED: EnvVarDecl[] = [
{ name: "SYNC_TICK_TOKEN", audience: "runtime", default: "unset (no auth)", readBy: "common/bin/sync-tick.ts, editor/app/scheduler/auth.ts", doc: "Bearer token for the tick endpoint; set on both the editor and the cron job." },
{ name: "R2_ACCESS_KEY_ID", audience: "runtime", default: "—", readBy: "common/publish/build.ts", doc: "R2 S3 credentials for uploading oversize archives at deploy time (with `R2_SECRET_ACCESS_KEY` and `CLOUDFLARE_ACCOUNT_ID`). See [PUBLISH.md](PUBLISH.md)." },
{ name: "R2_SECRET_ACCESS_KEY", audience: "runtime", default: "—", readBy: "common/publish/build.ts", doc: "See `R2_ACCESS_KEY_ID`." },
- { name: "CLOUDFLARE_API_TOKEN", audience: "runtime", default: "— (a `wrangler login` on this machine instead)", readBy: "common/lib/pagesDeploy.ts, wrangler", doc: "The Cloudflare API token every Pages deploy runs wrangler with. With no token, no key pair and no `wrangler login`, a deploy is refused before wrangler runs; a token Cloudflare rejects ends the deploy on \"REFUSED by Cloudflare\". Never printed." },
- { name: "CLOUDFLARE_API_KEY", audience: "runtime", default: "—", readBy: "common/lib/pagesDeploy.ts, wrangler", doc: "wrangler's global-key alternative to `CLOUDFLARE_API_TOKEN`, with `CLOUDFLARE_EMAIL`." },
- { name: "CLOUDFLARE_EMAIL", audience: "runtime", default: "—", readBy: "common/lib/pagesDeploy.ts, wrangler", doc: "See `CLOUDFLARE_API_KEY`." },
- { name: "WRANGLER_BIN", audience: "runtime", default: "`common/node_modules/.bin/wrangler` (the pinned devDependency)", readBy: "common/lib/pagesDeploy.ts (wranglerBin)", doc: "The wrangler a deploy spawns. The editor's e2e suite points it at its fake." },
- { name: "XDG_CONFIG_HOME", audience: "runtime", default: "`~/.config`", readBy: "common/lib/pagesDeploy.ts", doc: "Where a deploy's credential check looks for a `wrangler login` (`<it>/.wrangler/config/default.toml`, beside `~/.wrangler`)." },
{ name: "CLOUDFLARE_ACCOUNT_ID", audience: "runtime", default: "—", readBy: "common/publish/build.ts", doc: "The account the R2 endpoint belongs to. wrangler reads its own credentials." },
{ name: "DOCKER_BIN", audience: "runtime", default: "`docker`", readBy: "common/publish/build.ts", doc: "The container engine for docker-mode builds (e.g. `podman`)." },
{ name: "DOCKER_BUILD_MEMORY", audience: "runtime", default: "no cap", readBy: "common/publish/build.ts", doc: "Per-container memory cap for a docker-mode build (`--memory`)." },
@@ -107,6 +102,12 @@ const DECLARED: EnvVarDecl[] = [
{ name: "ARCHIVE_CHANNEL_CONCURRENCY", audience: "runtime", default: "`4`", readBy: "common/bin/build-archives.ts", doc: "How many channels' archive zips `build archives` builds at once." },
{ name: "HOST", audience: "runtime", default: "every interface", readBy: "export/scripts/serve-out.mjs", doc: "The address `pnpm start:export` (serve-out) listens on; `127.0.0.1` keeps a private site on this machine." },
{ name: "MAX_ARCHIVE_BYTES", audience: "runtime", default: "the Cloudflare-safe cap", readBy: "common/bin/compose-site.ts", doc: "The served-file size cap for archives, in bytes; `0` = no cap. A site's own `archiveMaxBytes` wins." },
+ { name: "WRANGLER_BIN", audience: "runtime", default: "`common/node_modules/.bin/wrangler` (the pinned devDependency)", readBy: "common/lib/pagesDeploy.ts (wranglerBin), common/publish/deployStage.ts", doc: "The wrangler a deploy spawns. The editor's e2e suite points it at its fake." },
+ // Release 18 S2, until S5 is merged: S5 declares these two itself (the
+ // credential and wrangler-login rows); on that merge keep S5's rows, delete
+ // these, and add common/lib/pagesDeploy.ts to their readBy.
+ { name: "CLOUDFLARE_API_TOKEN", audience: "runtime", default: "unset (a `wrangler login` on a host)", readBy: "wrangler (every deploy), common/lib/pagesDeploy.ts (set or not, never the value)", doc: "The API token every deploy runs wrangler with. With none and no `wrangler login`, a deploy is refused before wrangler runs; one Cloudflare rejects ends the deploy on \"REFUSED by Cloudflare\". Never printed." },
+ { name: "XDG_CONFIG_HOME", audience: "runtime", default: "`~/.config`", readBy: "common/lib/pagesDeploy.ts", doc: "Where `wrangler login` keeps its config (`<it>/.wrangler/config/default.toml`); a deploy's credential check looks for it there, by path." },
{ name: "CHOUGH_BIN", audience: "runtime", default: "`chough` on PATH", readBy: "common/lib/transcriptionApps.ts", doc: "The chough transcription engine, when a worker names no binary." },
{ name: "CHOUGH_MODEL", audience: "runtime", default: "chough's own", readBy: "chough (set by common/lib/transcriptionApps.ts)", doc: "Passed to chough from a worker's model field; chough auto-downloads one when unset." },
{ name: "CHOUGH_URL", audience: "runtime", default: "local", readBy: "chough (set by common/lib/transcriptionApps.ts)", doc: "Passed to chough from a worker's remote-server field." },
@@ -157,8 +158,7 @@ const DECLARED: EnvVarDecl[] = [
{ name: "ARCHILYZER_FETCH_MODEL", audience: "docker", default: "per transcriber", readBy: "docker/entrypoint.sh", doc: "Which model the first boot downloads; `none` skips it." },
{ name: "ARCHILYZER_MODELS_DIR", audience: "docker", default: "`/data/models`", readBy: "docker/entrypoint.sh", doc: "Where models live in the container." },
{ name: "ARCHILYZER_BUILDS_DIR", audience: "docker", default: "`/data/builds`", readBy: "docker/entrypoint.sh", doc: "Where the container keeps built sites." },
- { name: "ARCHILYZER_SITE_OUT", audience: "docker", default: "`/data/builds/site`", readBy: "docker/entrypoint.sh, docker/publish-site.sh, common/publish/deployStage.ts", doc: "The built export site the `site` service serves; `deploy <id> --to local` copies a site's bundle here." },
- { name: "ARCHILYZER_HOMEPAGE_OUT", audience: "docker", default: "`homepage/` beside `ARCHILYZER_SITE_OUT`", readBy: "common/publish/deployStage.ts", doc: "Where `deploy homepage --to local` copies the homepage's bundle." },
+ { name: "ARCHILYZER_SITE_OUT", audience: "docker", default: "`/data/builds/site`", readBy: "docker/entrypoint.sh, docker/publish-site.sh", doc: "The built export site the `site` service serves." },
{ name: "ARCHILYZER_IDLE_BOOT", audience: "docker", default: "off", readBy: "common/lib/idleBoot.ts (the editor)", doc: "`1` boots the editor without arming the heartbeat or any auto-queue runner." },
{ name: "ARCHILYZER_AUTH_MODE", audience: "docker", default: "`basic`", readBy: "docker/guard-exposure.sh, docker/caddy-start.sh", doc: "`basic`, `forward` or `none` — the only escape hatch from the exposure guard." },
{ name: "ARCHILYZER_AUTH_USER", audience: "docker", default: "`archilyzer`", readBy: "docker/Caddyfile", doc: "Basic-auth user." },
@@ -167,6 +167,9 @@ const DECLARED: EnvVarDecl[] = [
{ name: "ARCHILYZER_FORWARD_AUTH_UPSTREAM", audience: "docker", default: "—", readBy: "docker/Caddyfile", doc: "Forward-auth server (Authelia, tinyauth, …), `host:port`." },
{ name: "ARCHILYZER_FORWARD_AUTH_URI", audience: "docker", default: "`/api/auth/caddy`", readBy: "docker/Caddyfile", doc: "The forward-auth server's verify path." },
{ name: "ARCHILYZER_TAG", audience: "docker", default: "`local`", readBy: "docker-compose*.yml", doc: "The image tag the compose files build and run." },
+ // Release 18 S2, until S5 is merged: S5 declares it (with docker/entrypoint.sh);
+ // keep S5's row, delete this one, add common/publish/deployStage.ts to its readBy.
+ { name: "ARCHILYZER_HOMEPAGE_OUT", audience: "docker", default: "`/data/builds/homepage`", readBy: "common/publish/deployStage.ts", doc: "The locally deployed homepage: `deploy homepage --to local` copies its bundle here." },
// ── test: harnesses, fakes and test-mode branches ──────────────────────
{ name: "E2E_TEST_ROUTES", audience: "test", default: "off", readBy: "editor/app/api/test/_guard.ts, editor/instrumentation.ts", doc: "`1` opens the editor's `/api/test/*` routes and marks a test server at boot. Set by `editor/playwright.config.ts` on its test server, and by nothing else." },
diff --git a/common/lib/pagesDeploy.test.ts b/common/lib/pagesDeploy.test.ts
@@ -153,14 +153,10 @@ test("wranglerAuthFailureIn: Cloudflare's refusals and wrangler's missing-login
assert.equal(CLOUDFLARE_AUTH_REFUSED, "[deploy] REFUSED by Cloudflare — the API token was not accepted");
});
-test("cloudflareCredentialProblem: a token, the key pair or an OAuth login; else the .env sentence", () => {
+test("cloudflareCredentialProblem: a token or an OAuth login; else the .env sentence", () => {
assert.equal(cloudflareCredentialProblem({ CLOUDFLARE_API_TOKEN: "t" }, false), null);
- assert.equal(
- cloudflareCredentialProblem({ CLOUDFLARE_API_KEY: "k", CLOUDFLARE_EMAIL: "e@x" }, false),
- null,
- );
assert.equal(cloudflareCredentialProblem({}, true), null);
- const none = cloudflareCredentialProblem({ CLOUDFLARE_API_TOKEN: " ", CLOUDFLARE_API_KEY: "k" }, false);
+ const none = cloudflareCredentialProblem({ CLOUDFLARE_API_TOKEN: " " }, false);
assert.equal(none, CLOUDFLARE_NO_CREDENTIALS);
assert.match(none ?? "", /set CLOUDFLARE_API_TOKEN in \.env/);
});
diff --git a/common/lib/pagesDeploy.ts b/common/lib/pagesDeploy.ts
@@ -158,16 +158,16 @@ export function wranglerOAuthConfigFiles(
/**
* Why a deploy has no credential to offer Cloudflare, as the refusal sentence —
- * or null when it has one: `CLOUDFLARE_API_TOKEN`, the global key pair
- * (`CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL`), or a wrangler OAuth login on
- * disk (`oauthLoginPresent`). Never reads or prints a value.
+ * or null when it has one: `CLOUDFLARE_API_TOKEN` (what `.env` carries, the one
+ * way in the container), or a wrangler OAuth login on disk
+ * (`oauthLoginPresent`, a host's `wrangler login`). Never reads or prints a
+ * value: set or not.
*/
export function cloudflareCredentialProblem(
env: Record<string, string | undefined>,
oauthLoginPresent: boolean,
): string | null {
if (env.CLOUDFLARE_API_TOKEN?.trim()) return null;
- if (env.CLOUDFLARE_API_KEY?.trim() && env.CLOUDFLARE_EMAIL?.trim()) return null;
if (oauthLoginPresent) return null;
return CLOUDFLARE_NO_CREDENTIALS;
}
diff --git a/common/publish/deployStage.test.ts b/common/publish/deployStage.test.ts
@@ -401,6 +401,50 @@ test("--to local copies the bundle into ARCHILYZER_SITE_OUT (its contents replac
}
});
+test("the homepage's local deploy copies homepage/out into ARCHILYZER_HOMEPAGE_OUT, and refuses without it", async () => {
+ const fx = fixture();
+ try {
+ const out = path.join(fx.root, "homepage", "out");
+ mkdirSync(out, { recursive: true });
+ writeFileSync(path.join(out, "index.html"), "<!doctype html>");
+ writeJson(path.join(fx.paths.exportBuildsDir, "_homepage", "built.json"), {
+ v: 1,
+ stampId: "home-1",
+ target: "_homepage",
+ kind: "homepage",
+ builtAt: "2026-10-06T09:30:00.000Z",
+ branch: "main",
+ corpusGeneratedAt: null,
+ });
+ const req: DeployStageRequest = { kind: "deploy-homepage", target: "_homepage", to: "local" };
+ const dest = path.join(fx.root, "homepage-out");
+ // The source gate is asked first, local or not: a build with no /source
+ // page (its source step refused) is never shipped.
+ await refused(
+ runDeployStage(ctx(fx, { ARCHILYZER_HOMEPAGE_OUT: dest }), req),
+ 1,
+ /homepage\/out has no \/source page/,
+ );
+ // A `--no-source` build: the page's empty state and nothing else.
+ mkdirSync(path.join(out, "source"));
+ writeFileSync(path.join(out, "source", "index.html"), "<!doctype html>");
+ // Only the SITE's directory set: the homepage is not copied beside it.
+ await refused(
+ runDeployStage(ctx(fx, { ARCHILYZER_SITE_OUT: path.join(fx.root, "site-out") }), req),
+ 1,
+ /a local homepage deploy needs ARCHILYZER_HOMEPAGE_OUT/,
+ );
+ assert.equal(existsSync(dest), false);
+ assert.equal(deployedBytes(fx, "_homepage"), null);
+ const res = await runDeployStage(ctx(fx, { ARCHILYZER_HOMEPAGE_OUT: dest }), req);
+ assert.equal(res.status, "ran");
+ assert.deepEqual(readdirSync(dest).sort(), ["index.html", "source"]);
+ assert.equal(readDeployedFile(path.join(fx.paths.exportBuildsDir, "_homepage"), "_homepage").local?.builtStampId, "home-1");
+ } finally {
+ fx.cleanup();
+ }
+});
+
test("the hub: its project, its bundle, and every tombstone probed plain and busted", async () => {
const fx = fixture();
try {
diff --git a/common/publish/deployStage.ts b/common/publish/deployStage.ts
@@ -24,10 +24,10 @@
// builtHomepageProblem + publishedSourceProblem
// 6. `--to local`: the bundle is copied into ARCHILYZER_SITE_OUT (the
// directory the compose `site` service serves; the homepage's is
-// ARCHILYZER_HOMEPAGE_OUT) and the stage records `local` — no credential,
-// no R2, no wrangler, no live check
-// 7. the credential preflight: no CLOUDFLARE_API_TOKEN (nor the global key
-// pair) and no wrangler OAuth login on disk → refused before wrangler
+// ARCHILYZER_HOMEPAGE_OUT, what the `homepage` service serves) and the
+// stage records `local` — no credential, no R2, no wrangler, no live check
+// 7. the credential preflight: no CLOUDFLARE_API_TOKEN and no wrangler OAuth
+// login on disk → refused before wrangler
// 8. a site's oversize archives to R2, from `<id>/.r2-staging`
// 9. the pinned wrangler (wranglerBin), `--branch main` or `--branch <b>`;
// Cloudflare refusing the credential reads as CLOUDFLARE_AUTH_REFUSED
@@ -143,7 +143,7 @@ export type DeployStageContext = {
onLog: (line: string) => void;
signal: AbortSignal;
// Default process.env: the credentials, WRANGLER_BIN, ARCHILYZER_SITE_OUT,
- // E2E_LIVE_CHECK.
+ // ARCHILYZER_HOMEPAGE_OUT, E2E_LIVE_CHECK.
env?: Record<string, string | undefined>;
// Where wrangler's OAuth login would be (default os.homedir()).
home?: string;
@@ -405,15 +405,12 @@ export async function runDeployStage(
// --- 6. --to local ---
if (toLocal) {
const dest =
- req.kind === "deploy-homepage"
- ? env.ARCHILYZER_HOMEPAGE_OUT?.trim() ||
- (env.ARCHILYZER_SITE_OUT?.trim() ? path.join(path.dirname(env.ARCHILYZER_SITE_OUT.trim()), "homepage") : "")
- : env.ARCHILYZER_SITE_OUT?.trim() ?? "";
+ (req.kind === "deploy-homepage" ? env.ARCHILYZER_HOMEPAGE_OUT : env.ARCHILYZER_SITE_OUT)?.trim() ?? "";
if (!dest) {
refuse(
req.kind === "deploy-homepage"
- ? "a local homepage deploy needs ARCHILYZER_HOMEPAGE_OUT (or ARCHILYZER_SITE_OUT) — the directory the local server serves."
- : "a local deploy needs ARCHILYZER_SITE_OUT — the directory the local site service serves.",
+ ? "a local homepage deploy needs ARCHILYZER_HOMEPAGE_OUT — the directory the local homepage service serves (the container sets it)."
+ : "a local deploy needs ARCHILYZER_SITE_OUT — the directory the local site service serves (the container sets it).",
);
}
log(`[deploy] ${target} → ${dest} (local)`);