Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
MENVIRONMENT.md | 12+++++-------
Mcommon/bin/compose-site.postsVisibility.test.ts | 16++++++++++++++++
Mcommon/bin/compose-site.ts | 11++++++-----
Mcommon/lib/envVars.ts | 17++++++++++-------
Mcommon/lib/pagesDeploy.test.ts | 8++------
Mcommon/lib/pagesDeploy.ts | 8++++----
Mcommon/publish/deployStage.test.ts | 44++++++++++++++++++++++++++++++++++++++++++++
Mcommon/publish/deployStage.ts | 19++++++++-----------
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)`);