diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a2549..5e408d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,7 @@ jobs: # above does NOT validate tag existence, so without this guard the # failure would surface at deploy time rather than PR time. # - # Pin alignment between env blocks is workflow-aware: + # Pin alignment between the two configs is workflow-aware: # dev-targeting PRs: pins may diverge (staging-leads-prod # during the soak-then-promote workflow; # see RELEASES.md § Sandbox image releases). @@ -108,8 +108,9 @@ jobs: # the registry. # main-targeting PRs: pins MUST be equal (released state). # The release PR is expected to promote the - # top-level pin to match env.staging before - # merging. + # production pin in wrangler.jsonc to match + # the staging pin in wrangler.staging.jsonc + # before merging. # # Skipped on forks (secrets are unavailable to PRs from forks by # default). Same-repo branches always hit it. @@ -122,9 +123,9 @@ jobs: run: | set -euo pipefail - prod_img=$(bun x wrangler deploy --dry-run 2>&1 \ + prod_img=$(bun x wrangler deploy --dry-run --config wrangler.jsonc 2>&1 \ | grep -oE 'registry\.cloudflare\.com/[^ )]+' | head -1 || true) - stg_img=$(bun x wrangler deploy --dry-run --env staging 2>&1 \ + stg_img=$(bun x wrangler deploy --dry-run --config wrangler.staging.jsonc 2>&1 \ | grep -oE 'registry\.cloudflare\.com/[^ )]+' | head -1 || true) if [ -z "$prod_img" ] || [ -z "$stg_img" ]; then @@ -141,19 +142,19 @@ jobs: fail=0 if echo "$images" | grep -qE "anc-sandbox[[:space:]]+${prod_tag}\b"; then - echo "verified: top-level pin anc-sandbox:${prod_tag} is in the CF managed registry" + echo "verified: production pin anc-sandbox:${prod_tag} (wrangler.jsonc) is in the CF managed registry" else - echo "::error::top-level pin anc-sandbox:${prod_tag} not found in CF managed registry" + echo "::error::production pin anc-sandbox:${prod_tag} (wrangler.jsonc) not found in CF managed registry" echo "did you forget: bun x wrangler containers build -p -t anc-sandbox:${prod_tag} docker/sandbox/" fail=1 fi if [ "$prod_tag" = "$stg_tag" ]; then - echo "verified: env.staging pin equals top-level pin (lockstep)" + echo "verified: staging pin equals production pin (lockstep)" elif echo "$images" | grep -qE "anc-sandbox[[:space:]]+${stg_tag}\b"; then - echo "verified: env.staging pin anc-sandbox:${stg_tag} is in the CF managed registry" + echo "verified: staging pin anc-sandbox:${stg_tag} (wrangler.staging.jsonc) is in the CF managed registry" else - echo "::error::env.staging pin anc-sandbox:${stg_tag} not found in CF managed registry" + echo "::error::staging pin anc-sandbox:${stg_tag} (wrangler.staging.jsonc) not found in CF managed registry" echo "did you forget: bun x wrangler containers build -p -t anc-sandbox:${stg_tag} docker/sandbox/" fail=1 fi @@ -163,9 +164,9 @@ jobs: # leads prod during development). if [ "${BASE_REF}" = "main" ] && [ "$prod_tag" != "$stg_tag" ]; then echo "::error::release PR to main has divergent image pins" - echo " top-level: anc-sandbox:${prod_tag}" - echo " env.staging: anc-sandbox:${stg_tag}" - echo "Promote the top-level pin to match env.staging before merging." + echo " wrangler.jsonc (prod): anc-sandbox:${prod_tag}" + echo " wrangler.staging.jsonc (stg): anc-sandbox:${stg_tag}" + echo "Promote the wrangler.jsonc pin to match wrangler.staging.jsonc before merging." echo "See RELEASES.md § Sandbox image releases for the soak-and-promote procedure." fail=1 fi diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index af403b1..0642c7a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -96,7 +96,10 @@ jobs: with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} - command: deploy --env staging + # Two separate wrangler configs (production vs staging) — see + # the header comment in wrangler.staging.jsonc for the rationale + # (workaround for cloudflare/workers-sdk#13925). + command: deploy --config wrangler.staging.jsonc production: name: build + deploy production @@ -134,8 +137,9 @@ jobs: with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} - # --env="" tells wrangler explicitly to use the top-level config - # (the production environment in this repo's multi-env shape). - # Without it, wrangler 4.x warns about ambiguity even though - # bare `deploy` does the right thing. - command: deploy --env="" + # wrangler.jsonc is the default config so no --config flag is + # required. The split-config layout (wrangler.jsonc for prod, + # wrangler.staging.jsonc for staging) replaces the previous + # multi-env block; see wrangler.jsonc's header comment for the + # rationale (workaround for cloudflare/workers-sdk#13925). + command: deploy diff --git a/RELEASES.md b/RELEASES.md index 340cbf1..b2d2738 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -242,16 +242,42 @@ and regenerate. Hand-editing the generated artifact directly produces drift the ## Deploy -`.github/workflows/deploy.yml` runs on pushes to `dev` or `main`, targeting separate Workers via wrangler environments: +`.github/workflows/deploy.yml` runs on pushes to `dev` or `main`, targeting separate Workers via two independent +wrangler configs: -| Branch | Worker | Domain | Wrangler command | -| ------ | -------------------------- | -------------------------------------------------- | ------------------------------- | -| `dev` | `agentnative-site-staging` | `agentnative-site-staging..workers.dev` | `wrangler deploy --env staging` | -| `main` | `agentnative-site` | `anc.dev` (custom domain, `workers_dev: false`) | `wrangler deploy` | +| Branch | Worker | Domain | Wrangler command | +| ------ | -------------------------- | -------------------------------------------------- | ------------------------------------------------- | +| `dev` | `agentnative-site-staging` | `agentnative-site-staging..workers.dev` | `wrangler deploy --config wrangler.staging.jsonc` | +| `main` | `agentnative-site` | `anc.dev` (custom domain, `workers_dev: false`) | `wrangler deploy` | The staging-host guard in `src/worker/headers.ts` adds `X-Robots-Tag: noindex` on any response served from a `.workers.dev` host. Production at `anc.dev` gets full indexing. +Every response also carries an `X-Build-Env` header (`production`, `staging`, or `development` when running tests), +which doubles as a quick "which Worker is this?" diagnostic and as the source-level mechanism that keeps the two +compiled Worker scripts at distinct etags — see the next section. + +### Why two separate wrangler configs (not a single multi-env file) + +Standard Workers Assets is keyed in a way that lets one Worker's asset uploads shadow another Worker's served content +when the two compiled Worker scripts have identical bytes. Reproduction and discussion at +[cloudflare/workers-sdk#13925](https://github.com/cloudflare/workers-sdk/issues/13925). + +To preserve real isolation between staging and production assets, this repo deploys from **two top-level wrangler +configs** rather than the more common single-config multi-env shape: + +- `wrangler.jsonc` deploys the `agentnative-site` (production) Worker. +- `wrangler.staging.jsonc` deploys the `agentnative-site-staging` Worker. + +Each config carries its own `define` block that substitutes `__BUILD_ENV__` (referenced in `src/worker/index.ts`) with a +literal string at deploy time (`"production"` vs `"staging"`). That single-character divergence is enough to give the +two compiled scripts distinct etags, which keeps their asset namespaces physically separate per CF's deduplication +keying. Without the substitution, staging deploys could silently override production-served asset content. + +Keep both configs in sync for anything that isn't a deliberate per-env divergence (compatibility flags, observability, +asset settings, etc.). The sandbox image pin is the one canonical exception: it's deliberately split between +soak-then-promote envs (see "Sandbox image releases" below). + Manual deploys use `workflow_dispatch` with an explicit environment picker: ```bash @@ -285,15 +311,15 @@ The live-scoring path uses a Cloudflare Containers binding that pins an Alpine + `registry.cloudflare.com//anc-sandbox:`. Build is decoupled from deploy: a Worker code-only deploy never rebuilds the image, and an image-only release never reships Worker code unintentionally. -`wrangler.jsonc` holds TWO independent image pins: +The two pins live in separate wrangler configs: -- `containers[0].image` (top-level) is the PRODUCTION pin. The `agentnative-site` Worker on `anc.dev` deploys from this - tag. Advances only at release time. -- `env.staging.containers[0].image` is the STAGING pin. The `agentnative-site-staging` Worker on +- `wrangler.jsonc` → `containers[0].image` is the PRODUCTION pin. The `agentnative-site` Worker on `anc.dev` deploys + from this tag. Advances only at release time. +- `wrangler.staging.jsonc` → `containers[0].image` is the STAGING pin. The `agentnative-site-staging` Worker on `agentnative-site-staging.brettdavies.workers.dev` deploys from this tag. Advances independently during development. -The two pins may legitimately differ. Each env block owns its own container application with its own version history. -The pins describe what staging and prod each run, not a shared constraint. +The two pins may legitimately differ. Each config owns its own container application with its own version history. The +pins describe what staging and prod each run, not a shared constraint. #### Default workflow: staging soak then promote @@ -311,8 +337,8 @@ bun x wrangler containers build -p -t "anc-sandbox:$GIT_SHA" docker/sandbox/ The command runs `docker build` locally and pushes to the CF registry, authenticated via `CLOUDFLARE_API_TOKEN`. Output ends with a `: digest: sha256:... size: ...` line confirming the push. -Update **only `env.staging.containers[0].image`** in `wrangler.jsonc` with the new tag. Leave the top-level (prod) pin -alone. Commit the Dockerfile change + the staging-pin update together. PR to `dev`. +Update **only `containers[0].image` in `wrangler.staging.jsonc`** with the new tag. Leave the production pin (in +`wrangler.jsonc`) alone. Commit the Dockerfile change + the staging-pin update together. PR to `dev`. CI on a dev-targeting PR verifies the new staging tag exists in the registry; the prod pin keeps pointing at the last-released tag, which also still exists (image-retention discipline). The CI guard accepts the divergence. @@ -323,8 +349,8 @@ traffic on the staging.workers.dev URL. **Promotion (release PR to main):** When the image is ready to ship, cut a release branch from `main` and cherry-pick the dev commits as usual. Add one -promotion commit that bumps the top-level `containers[0].image` to match `env.staging.containers[0].image`. Open the PR -to `main`. CI on a main-targeting PR enforces TWO invariants: +promotion commit that bumps `containers[0].image` in `wrangler.jsonc` to match `wrangler.staging.jsonc`'s pin. Open the +PR to `main`. CI on a main-targeting PR enforces TWO invariants: - both pins exist in the CF managed registry, AND - both pins point at the same tag (released state) @@ -345,8 +371,8 @@ FROM line. #### Deploy never rebuilds -`wrangler deploy --env staging` (and `wrangler deploy` on main) against the fully-qualified registry URI does NOT -trigger a rebuild. The image was already published during the local `wrangler containers build -p` step. +`wrangler deploy --config wrangler.staging.jsonc` (and `wrangler deploy` on main) against the fully-qualified registry +URI does NOT trigger a rebuild. The image was already published during the local `wrangler containers build -p` step. #### Image-retention discipline diff --git a/docker/sandbox/README.md b/docker/sandbox/README.md index 3b887e5..e7dfa68 100644 --- a/docker/sandbox/README.md +++ b/docker/sandbox/README.md @@ -28,12 +28,12 @@ resolves the account ID from auth at push time. Push output ends with a line lik : digest: sha256:... size: ... ``` -Pin the resulting tag in `wrangler.jsonc`. The file holds two independent pins, and the choice of which one(s) to update -depends on the change: +Pin the resulting tag in one of the two wrangler configs. Each config holds its own `containers[0].image`, and the +choice of which one(s) to update depends on the change: -- For a normal sandbox change (any commit past the base-image FROM line): update only `env.staging.containers[0].image`. - The image soaks on staging, then a separate release PR to main promotes the top-level (prod) pin to match. This is the - default and what the CI guard expects. +- For a normal sandbox change (any commit past the base-image FROM line): update only `containers[0].image` in + `wrangler.staging.jsonc`. The image soaks on staging, then a separate release PR to main promotes the production pin + in `wrangler.jsonc` to match. This is the default and what the CI guard expects. - For a low-risk bump (base-image security patch, dependency-only update with no behavior delta): update both pins in lockstep. The CI guard accepts equal pins too. @@ -43,7 +43,7 @@ soak-then-promote flow and the release-time invariant the main-targeting CI chec After pinning, deploy without rebuilding: ```sh -bun x wrangler deploy --env staging +bun x wrangler deploy --config wrangler.staging.jsonc # verify staging is healthy, then promote via the dev → main release flow ``` diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push index aebd449..ad3ad75 100755 --- a/scripts/hooks/pre-push +++ b/scripts/hooks/pre-push @@ -58,11 +58,11 @@ bun run build bold '==> bun test' bun test -bold '==> bun x wrangler deploy --dry-run (production env)' -bun x wrangler deploy --dry-run --env="" +bold '==> bun x wrangler deploy --dry-run (production config)' +bun x wrangler deploy --dry-run --config wrangler.jsonc -bold '==> bun x wrangler deploy --dry-run (staging env)' -bun x wrangler deploy --dry-run --env staging +bold '==> bun x wrangler deploy --dry-run (staging config)' +bun x wrangler deploy --dry-run --config wrangler.staging.jsonc bold '==> Pack-README drift check' bun scripts/generate-pack-readme.mjs site --check ; diff --git a/styles/config/vocabularies/site/accept.txt b/styles/config/vocabularies/site/accept.txt index 9c4ff6a..393908c 100644 --- a/styles/config/vocabularies/site/accept.txt +++ b/styles/config/vocabularies/site/accept.txt @@ -227,3 +227,10 @@ yazi yq zoomable zoxide +configs +etag +etags +deduplication +namespaces +envs +CF's diff --git a/tests/worker.test.ts b/tests/worker.test.ts index 15420c0..01030e1 100644 --- a/tests/worker.test.ts +++ b/tests/worker.test.ts @@ -107,6 +107,7 @@ describe('applyHeaders — HTML branch', () => { request: req('https://anc.dev/p3'), servedMarkdown: false, pathname: '/p3', + buildEnv: 'development', }); expect(res.headers.get('Link')).toBe('; rel="alternate"; type="text/markdown"'); expect(res.headers.get('X-Llms-Txt')).toBe('/llms.txt'); @@ -119,6 +120,7 @@ describe('applyHeaders — HTML branch', () => { request: req('https://anc.dev/'), servedMarkdown: false, pathname: '/', + buildEnv: 'development', }); expect(res.headers.get('Link')).toBe('; rel="alternate"; type="text/markdown"'); }); @@ -130,6 +132,7 @@ describe('applyHeaders — markdown branch', () => { request: req('https://anc.dev/p3.md'), servedMarkdown: true, pathname: '/p3.md', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).toBe('text/markdown; charset=utf-8'); expect(res.headers.get('X-Robots-Tag')).toBe('noindex'); @@ -143,6 +146,7 @@ describe('applyHeaders — JSON branch (skill-distribution)', () => { request: req('https://anc.dev/skill.json'), servedMarkdown: false, pathname: '/skill.json', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).toBe('application/json; charset=utf-8'); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); @@ -158,6 +162,7 @@ describe('applyHeaders — JSON branch (skill-distribution)', () => { request: req('https://anc.dev/foo.json'), servedMarkdown: false, pathname: '/foo.json', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).toBe('application/json; charset=utf-8'); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); @@ -168,6 +173,7 @@ describe('applyHeaders — JSON branch (skill-distribution)', () => { request: req('https://anc.dev/installer'), servedMarkdown: false, pathname: '/installer', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).not.toBe('application/json; charset=utf-8'); expect(res.headers.get('Link')).toContain('rel="alternate"'); @@ -180,6 +186,7 @@ describe('applyHeaders — SVG branch (badge surface)', () => { request: req('https://anc.dev/badge/rg.svg'), servedMarkdown: false, pathname: '/badge/rg.svg', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).toBe('image/svg+xml; charset=utf-8'); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); @@ -197,6 +204,7 @@ describe('applyHeaders — SVG branch (badge surface)', () => { request: req('https://agentnative-site-staging.workers.dev/badge/rg.svg'), servedMarkdown: false, pathname: '/badge/rg.svg', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).toBe('image/svg+xml; charset=utf-8'); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); @@ -208,6 +216,7 @@ describe('applyHeaders — SVG branch (badge surface)', () => { request: req('https://anc.dev/badge'), servedMarkdown: false, pathname: '/badge', + buildEnv: 'development', }); expect(res.headers.get('Content-Type')).not.toBe('image/svg+xml; charset=utf-8'); expect(res.headers.get('Link')).toContain('rel="alternate"'); @@ -220,6 +229,7 @@ describe('applyHeaders — hashed assets', () => { request: req('https://anc.dev/fonts/uncut-sans-variable.woff2'), servedMarkdown: false, pathname: '/fonts/uncut-sans-variable.woff2', + buildEnv: 'development', }); expect(res.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable'); }); @@ -229,6 +239,7 @@ describe('applyHeaders — hashed assets', () => { request: req('https://anc.dev/og-image.png'), servedMarkdown: false, pathname: '/og-image.png', + buildEnv: 'development', }); expect(res.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable'); }); @@ -240,6 +251,7 @@ describe('applyHeaders — staging-host guard (locked decision #4)', () => { request: req('https://agentnative-site.brett.workers.dev/p3'), servedMarkdown: false, pathname: '/p3', + buildEnv: 'development', }); expect(res.headers.get('X-Robots-Tag')).toBe('noindex'); // Link + X-Llms-Txt still present on HTML. @@ -251,6 +263,7 @@ describe('applyHeaders — staging-host guard (locked decision #4)', () => { request: req('https://agentnative-site.brett.workers.dev/fonts/uncut-sans-variable.woff2'), servedMarkdown: false, pathname: '/fonts/uncut-sans-variable.woff2', + buildEnv: 'development', }); expect(res.headers.get('X-Robots-Tag')).toBe('noindex'); expect(res.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable'); @@ -261,11 +274,44 @@ describe('applyHeaders — staging-host guard (locked decision #4)', () => { request: req('https://anc.dev/p3'), servedMarkdown: false, pathname: '/p3', + buildEnv: 'development', }); expect(res.headers.get('X-Robots-Tag')).toBeNull(); }); }); +describe('applyHeaders — X-Build-Env header (cloudflare/workers-sdk#13925 workaround)', () => { + test('production buildEnv shows up in the response', () => { + const res = applyHeaders(new Response('html'), { + request: req('https://anc.dev/p3'), + servedMarkdown: false, + pathname: '/p3', + buildEnv: 'production', + }); + expect(res.headers.get('X-Build-Env')).toBe('production'); + }); + + test('staging buildEnv shows up in the response', () => { + const res = applyHeaders(new Response('html'), { + request: req('https://agentnative-site-staging.brett.workers.dev/p3'), + servedMarkdown: false, + pathname: '/p3', + buildEnv: 'staging', + }); + expect(res.headers.get('X-Build-Env')).toBe('staging'); + }); + + test('development buildEnv (tests + local) surfaces too', () => { + const res = applyHeaders(new Response('html'), { + request: req('https://anc.dev/p3'), + servedMarkdown: false, + pathname: '/p3', + buildEnv: 'development', + }); + expect(res.headers.get('X-Build-Env')).toBe('development'); + }); +}); + // --------------------------------------------------------------------------- // End-to-end handler: asset-lookup rewrite for the markdown branch. // --------------------------------------------------------------------------- diff --git a/wrangler.jsonc b/wrangler.jsonc index 9cd6c7f..c5ba28f 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,5 +1,20 @@ { "$schema": "node_modules/wrangler/config-schema.json", + // --------------------------------------------------------------------------- + // PRODUCTION wrangler config — deploys the `agentnative-site` Worker on + // anc.dev. Sibling: `wrangler.staging.jsonc` deploys the + // `agentnative-site-staging` Worker on the workers.dev subdomain. + // + // The two configs are intentionally separate top-level files (no + // `env.staging` block here) to work around the Workers Assets cross-env + // asset-sharing bug filed at + // https://github.com/cloudflare/workers-sdk/issues/13925. With a single + // multi-env config, asset uploads from staging deploys silently override + // what production serves at the same URL paths whenever the two compiled + // Worker scripts have identical bytes. The `define` block below pairs + // with `src/worker/index.ts`'s `__BUILD_ENV__` reference to guarantee + // the two scripts produce distinct etags. + // --------------------------------------------------------------------------- "name": "agentnative-site", "main": "src/worker/index.ts", // account_id is intentionally NOT in the repo. Wrangler picks it up from @@ -16,6 +31,11 @@ // contributor's local wrangler invocations stay opted out regardless of // their shell environment. "send_metrics": false, + // Build-time substitution. `__BUILD_ENV__` in src/worker/index.ts is + // replaced with the literal string "production" at deploy time. + // Staging's config substitutes "staging" instead. Same source file, + // distinct compiled output. See header above for the rationale. + "define": { "__BUILD_ENV__": "\"production\"" }, "assets": { "directory": "./dist", "binding": "ASSETS", @@ -36,15 +56,14 @@ "containers": [ { "class_name": "Sandbox", - // PRODUCTION pin — this is the image deployed to anc.dev. - // Advances only at release time. The staging pin - // (env.staging.containers[0].image) advances independently - // during development; release PRs to main promote this pin to - // match staging after the soak. + // PRODUCTION pin. Advances only at release time. The staging pin + // (in wrangler.staging.jsonc) advances independently during + // development; release PRs to main promote this pin to match + // staging after the soak. // // Promotion procedure (release PR, main-targeting): - // 1. Confirm the env.staging tag is the one to ship to prod - // 2. Replace this tag with env.staging's tag + // 1. Confirm the staging tag is the one to ship to prod + // 2. Replace this tag with staging's tag // 3. CI on the main-targeting PR asserts both pins are aligned // AND that the tag exists in the CF managed registry // 4. Merge; CI deploys prod @@ -100,93 +119,11 @@ "simple": { "limit": 10, "period": 60 } } ], - // Production (top-level): anc.dev custom domain, no .workers.dev URL. - // Deployed via `wrangler deploy` (no --env flag) on push to main. + // Production: anc.dev custom domain, no .workers.dev URL. + // Deployed via `wrangler deploy` on push to main. // Cloudflare auto-provisions SSL cert + DNS CNAME for the custom domain. "routes": [{ "pattern": "anc.dev", "custom_domain": true }], - "workers_dev": false, - // Staging: separate Worker (agentnative-site-staging) deployed from dev. - // workers.dev only — no custom domain. The staging-noindex guard in - // src/worker/headers.ts adds X-Robots-Tag: noindex on *.workers.dev. - // Deploy with: wrangler deploy --env staging - // - // Wrangler env semantics: SOME keys inherit from top-level, others - // (durable_objects, containers, migrations, ratelimits, r2_buckets) - // do NOT — wrangler warns when they're missing under an env. Mirror - // every live-scoring binding under env.staging explicitly. The - // staging Worker name (agentnative-site-staging) namespaces the DO - // instances away from prod automatically; the R2 bucket name + the - // rate-limit namespace_id need explicit staging suffixes. - "env": { - "staging": { - // Explicit name needed under env.staging because the containers - // app-name derivation (worker-name + class-name) doesn't follow - // wrangler's automatic `-` env-suffix convention for - // containers. With this set, the staging container is named - // `agentnative-site-staging-sandbox` and is distinct from prod's - // `agentnative-site-sandbox`. - "name": "agentnative-site-staging", - "workers_dev": true, - "containers": [ - { - "class_name": "Sandbox", - // STAGING pin — advances independently from the top-level - // (prod) pin during development. This is the "leading" side - // of the soak-then-promote workflow: new images land here - // first, soak on the staging Worker, then get promoted to - // the top-level pin via a release PR to main. - // - // Image bump procedure (feat PR, dev-targeting): - // 1. From clean tree on dev: GIT_SHA=$(git rev-parse --short HEAD) - // 2. wrangler containers build -p -t anc-sandbox:$GIT_SHA docker/sandbox/ - // 3. Update THIS pin only (leave top-level alone). For low- - // risk bumps, update both pins; CI accepts lockstep on - // dev-targeting PRs. - // 4. PR to dev; CI verifies the tag exists in the registry - // and (for divergent pins) allows the lead. - // 5. Merge; CI deploys the staging Worker - // (agentnative-site-staging) to the new image. Soak. - // - // `containers` is non-inheritable per-env, so each env block - // needs its own copy of the image URI. The two pins are - // independent CF resources (separate container apps, separate - // version histories) and may legitimately diverge. - // - // See RELEASES.md § Sandbox image releases for the full flow. - "image": "registry.cloudflare.com/6c1bafea907fecbd4ad665b8d0a78e53/anc-sandbox:30f61f1", - "instance_type": "basic", - "max_instances": 3 - } - ], - "durable_objects": { - "bindings": [ - { - "class_name": "Sandbox", - "name": "SCORE" - } - ] - }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": ["Sandbox"] - } - ], - "r2_buckets": [ - { - "binding": "SCORE_CACHE", - "bucket_name": "anc-score-cache-staging" - } - ], - "ratelimits": [ - { - "name": "SCORE_LIMITER", - "namespace_id": "1002", - "simple": { "limit": 10, "period": 60 } - } - ] - } - } + "workers_dev": false // Smart Placement intentionally NOT set: this is a static-asset + CN // site with no data source. `placement: { mode: "smart" }` + the // `assets.run_worker_first: true` we need for the CN branch would let diff --git a/wrangler.staging.jsonc b/wrangler.staging.jsonc new file mode 100644 index 0000000..de4d0df --- /dev/null +++ b/wrangler.staging.jsonc @@ -0,0 +1,98 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + // --------------------------------------------------------------------------- + // STAGING wrangler config — deploys the `agentnative-site-staging` Worker + // on its workers.dev subdomain. Sibling: `wrangler.jsonc` deploys the + // `agentnative-site` production Worker on anc.dev. + // + // The two configs are intentionally separate top-level files (no + // `env.staging` block) to work around the Workers Assets cross-env + // asset-sharing bug filed at + // https://github.com/cloudflare/workers-sdk/issues/13925. The `define` + // block below pairs with `src/worker/index.ts`'s `__BUILD_ENV__` + // reference so the staging-built script has different bytes than the + // production-built script, which keeps their asset namespaces isolated. + // + // Deploy with: `wrangler deploy --config wrangler.staging.jsonc` + // (the `deploy` job in `.github/workflows/deploy.yml` does this on + // every push to dev). + // --------------------------------------------------------------------------- + "name": "agentnative-site-staging", + "main": "src/worker/index.ts", + "compatibility_date": "2026-04-01", + "compatibility_flags": ["nodejs_compat"], + "send_metrics": false, + // Build-time substitution mirrors wrangler.jsonc but with the literal + // string "staging". The deliberate divergence between this value and + // production's "production" is what keeps the two compiled Worker + // scripts at distinct etags. See `src/worker/index.ts`. + "define": { "__BUILD_ENV__": "\"staging\"" }, + "assets": { + "directory": "./dist", + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page", + "run_worker_first": true + }, + "observability": { + "enabled": true, + "head_sampling_rate": 1.0 + }, + "containers": [ + { + "class_name": "Sandbox", + // STAGING pin. Advances independently from production during + // development. This is the "leading" side of the soak-then-promote + // workflow: new images land here first, soak on the staging + // Worker, then get promoted to production's pin via a release PR + // to main. + // + // Image bump procedure (feat PR, dev-targeting): + // 1. From clean tree on dev: GIT_SHA=$(git rev-parse --short HEAD) + // 2. wrangler containers build -p -t anc-sandbox:$GIT_SHA docker/sandbox/ + // 3. Update THIS pin only (leave wrangler.jsonc alone). For + // low-risk bumps, update both pins; CI accepts lockstep on + // dev-targeting PRs. + // 4. PR to dev; CI verifies the tag exists in the registry + // and (for divergent pins) allows the lead. + // 5. Merge; CI deploys the staging Worker + // (agentnative-site-staging) to the new image. Soak. + // + // See RELEASES.md § Sandbox image releases for the full flow. + "image": "registry.cloudflare.com/6c1bafea907fecbd4ad665b8d0a78e53/anc-sandbox:30f61f1", + "instance_type": "basic", + "max_instances": 3 + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "SCORE" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["Sandbox"] + } + ], + "r2_buckets": [ + { + "binding": "SCORE_CACHE", + "bucket_name": "anc-score-cache-staging" + } + ], + "ratelimits": [ + { + "name": "SCORE_LIMITER", + "namespace_id": "1002", + "simple": { "limit": 10, "period": 60 } + } + ], + // Staging: workers.dev only, no custom domain. The staging-noindex + // guard in src/worker/headers.ts adds X-Robots-Tag: noindex on every + // response served from a *.workers.dev host. + "workers_dev": true +}