Skip to content

Commit dd3b2ad

Browse files
committed
install-router: co-host staging + prod, fix branch model (main/staging, not master)
get.allow2.com co-hosts staging + prod (like steamdeck/stable + steamdeck/staging), so the router is now two envs of one worker: - prod (deploy from main): route get.allow2.com/install, CHANNEL=stable, manifests install/<p>.json, steamdeck/stable/ pages, indexable. - staging (deploy from staging): route get.allow2.com/staging, CHANNEL=staging, manifests staging/install/<p>.json, steamdeck/staging/ pages, and X-Robots-Tag: noindex,nofollow on EVERY response. Worker is env-aware via [env.*.vars] (CHANNEL/MANIFEST_BASE/DEFAULT_INSTALL/NOINDEX); config() defaults are prod-safe. Routes are EXACT so a worker never intercepts its own manifest sub-path fetches; chooser self-links track the actual route. Branch model corrected to the ecosystem convention: push main -> --env production, push staging -> --env staging, PR -> dry-run. ZERO master references. publish.sh writes the channel-appropriate manifest key (stable -> install/steamdeck.json, staging -> staging/install/steamdeck.json) and purges it. robots.txt gains Disallow: /*staging (matches staging anywhere in the path; the noindex header is the real not-indexed guarantee, robots.txt only controls crawling). Tests: node --check + harness all pass (incl. staging-noindex / prod-no-header / channel-correct manifest+fail-safe cases).
1 parent a157f8e commit dd3b2ad

7 files changed

Lines changed: 387 additions & 113 deletions

File tree

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
name: Deploy install-router (Cloudflare Worker)
22

3-
# Deploys the get.allow2.com/install Worker (workers/install-router) to
4-
# Cloudflare. The Worker is data-driven — per-platform install URLs live in R2
5-
# manifests, not in the Worker — so this pipeline only ships the routing logic.
3+
# Deploys the install-funnel Worker (workers/install-router) to Cloudflare. The
4+
# Worker is data-driven — per-platform install URLs live in R2 manifests, not in
5+
# the Worker — so this pipeline only ships the routing logic. It is CO-HOSTED as
6+
# a staging+prod pair on get.allow2.com (two envs of the same code).
67
#
7-
# push to the default branch → deploy --env production (binds the route)
8-
# pull_request → deploy --env production --dry-run (validate only)
8+
# Branch → env (ecosystem convention; the old default branch is being retired —
9+
# only `staging` and `main` drive deploys):
10+
# push to `main` → deploy --env production (route get.allow2.com/install, stable, indexable)
11+
# push to `staging` → deploy --env staging (route get.allow2.com/staging, staging pages, noindex)
12+
# pull_request → deploy --env production --dry-run (validate only, nothing published)
913
#
1014
# ── Secrets (repo/org settings → Actions → Secrets) ──────────────────────────
1115
# CF_WORKERS_API_TOKEN Cloudflare API token for Workers deploys. Create it
@@ -22,7 +26,7 @@ name: Deploy install-router (Cloudflare Worker)
2226

2327
on:
2428
push:
25-
branches: [master] # the repo default branch (origin/HEAD)
29+
branches: [staging, main] # staging → staging env, main → production env
2630
paths:
2731
- 'workers/install-router/**'
2832
pull_request:
@@ -35,18 +39,42 @@ jobs:
3539
steps:
3640
- uses: actions/checkout@v4
3741

38-
- name: Deploy (push) / validate (PR)
39-
# wrangler-action@v3 is PINNED deliberately: v4 defaults to Wrangler 4,
40-
# a major bump we are NOT adopting here. Staying on v3 + an explicit
41-
# Wrangler 3 version keeps the deploy reproducible.
42+
# Separate, if:-gated steps per target (clearer than one nested expression).
43+
# wrangler-action@v3 is PINNED deliberately: v4 defaults to Wrangler 4, a
44+
# major bump we are NOT adopting here. Staying on v3 + an explicit Wrangler
45+
# 3 version keeps the deploy reproducible.
46+
47+
- name: Deploy production (push to main)
48+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
49+
uses: cloudflare/wrangler-action@v3
50+
with:
51+
apiToken: ${{ secrets.CF_WORKERS_API_TOKEN }}
52+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
53+
workingDirectory: workers/install-router
54+
wranglerVersion: '3.90.0'
55+
# Real deploy: binds the production route get.allow2.com/install
56+
# (stable pages, indexable) per wrangler.toml [env.production].
57+
command: deploy --env production
58+
59+
- name: Deploy staging (push to staging)
60+
if: github.event_name == 'push' && github.ref == 'refs/heads/staging'
61+
uses: cloudflare/wrangler-action@v3
62+
with:
63+
apiToken: ${{ secrets.CF_WORKERS_API_TOKEN }}
64+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
65+
workingDirectory: workers/install-router
66+
wranglerVersion: '3.90.0'
67+
# Real deploy: binds the staging route get.allow2.com/staging
68+
# (staging pages, noindex) per wrangler.toml [env.staging].
69+
command: deploy --env staging
70+
71+
- name: Validate (pull_request, dry-run)
72+
if: github.event_name == 'pull_request'
4273
uses: cloudflare/wrangler-action@v3
4374
with:
4475
apiToken: ${{ secrets.CF_WORKERS_API_TOKEN }}
4576
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
4677
workingDirectory: workers/install-router
4778
wranglerVersion: '3.90.0'
48-
# On PRs, validate only (no route claimed, nothing published). On a push
49-
# to the default branch, do the real deploy which binds the production
50-
# route (get.allow2.com/install) per wrangler.toml [env.production].
51-
command: >-
52-
deploy --env production${{ github.event_name == 'pull_request' && ' --dry-run' || '' }}
79+
# Validate only — no route claimed, nothing published.
80+
command: deploy --env production --dry-run

‎robots.txt‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
# Keep the internal beta (staging channel) out of search engines.
2-
# /steamdeck/stable/ stays crawlable for SEO; only the staging prefix is excluded.
1+
# Keep anything "staging" out of search engines: the internal beta install pages
2+
# (/steamdeck/staging/) AND the staging install-router route (/staging + its
3+
# sub-paths, e.g. /staging/install/...). /steamdeck/stable/ and the prod
4+
# /install route stay crawlable for SEO.
5+
#
6+
# `Disallow: /*staging` matches "staging" ANYWHERE in the path — robots.txt `*`
7+
# spans path segments — so this ONE rule covers /staging, /steamdeck/staging/,
8+
# and any future /x/staging. The explicit /steamdeck/staging/ line below is kept
9+
# (harmless, and self-documenting).
10+
#
11+
# NOTE: robots.txt only controls CRAWLING. The real no-index guarantee for the
12+
# staging install-router is the X-Robots-Tag: noindex, nofollow header its Worker
13+
# adds to every response (see workers/install-router).
314
User-agent: *
15+
Disallow: /*staging
416
Disallow: /steamdeck/staging/

‎scripts/publish.sh‎

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -301,33 +301,36 @@ r2_put "${INSTALL_PAGE}" "${PREFIX}/${SUBDIR}/index.html" "text/html" "n
301301
r2_put "${HARDENING_PAGE}" "${PREFIX}/${SUBDIR}/hardening.html" "text/html" "no-cache"
302302
echo " uploaded ${SUBDIR}/${REF_FILE} + ${SUBDIR}/index.html + ${SUBDIR}/hardening.html"
303303

304-
# ── 4c. Write THIS platform's install-router manifest entry ──────────────────
305-
# The get.allow2.com/install Worker (workers/install-router) is data-driven: it
306-
# reads a per-platform manifest from R2 at request time and 302s to its `.url`.
307-
# Here we publish OUR entry — install/steamdeck.json — pointing at this channel's
308-
# real install page. Publishing a new page therefore needs NO Worker redeploy;
309-
# the Worker picks up the new URL within its 5-minute edge TTL (and the purge in
310-
# step 5 makes it instant).
304+
# ── 4c. Write THIS platform's install-router manifest entry (CHANNEL-AWARE) ───
305+
# The install-router Worker (workers/install-router) is data-driven: it reads a
306+
# per-platform manifest from R2 at request time and 302s to its `.url`. It is
307+
# CO-HOSTED as a staging+prod pair, and EACH env reads a DIFFERENT manifest base:
308+
# production Worker → get.allow2.com/install/<platform>.json
309+
# staging Worker → get.allow2.com/staging/install/<platform>.json
310+
# So this publish must write OUR entry to the manifest path for the channel it is
311+
# publishing — otherwise the staging Worker would never see the staging page.
311312
#
312-
# staging → https://get.allow2.com/steamdeck/staging/ (internal testers)
313-
# production → https://get.allow2.com/steamdeck/stable/ (public/prod)
313+
# production channel → key install/steamdeck.json → steamdeck/stable/
314+
# staging channel → key staging/install/steamdeck.json → steamdeck/staging/
314315
#
315-
# The manifest is a DOMAIN-LEVEL object at install/steamdeck.json — NOT under the
316-
# steamdeck/<channel>/ prefix — so the per-channel `--delete` sync never touches
317-
# it, and BOTH channels write the SAME key (whichever publishes last wins). That
318-
# is intended: the public entry point should point at whatever we most recently
319-
# published for the Deck. Content-Type application/json + no-cache so a re-publish
320-
# is never served stale.
316+
# Publishing a new page therefore needs NO Worker redeploy; the matching-env
317+
# Worker picks up the new URL within its 5-minute edge TTL (and the purge in step
318+
# 5 makes it instant). The two channels write DIFFERENT keys, so they never
319+
# clobber each other (unlike before, when both wrote the single install/... key).
321320
#
322-
# NOTE: other platform repos (windows / mac / android) write their OWN
323-
# install/<platform>.json the SAME way from their own release pipelines — this
324-
# repo owns ONLY the steamdeck entry.
321+
# Both keys are DOMAIN-ROOT objects — NOT under the steamdeck/<channel>/ prefix —
322+
# so the per-channel `--delete` sync never touches them. Content-Type
323+
# application/json + no-cache so a re-publish is never served stale.
325324
#
326-
# KEY: this is a DOMAIN-ROOT object served at get.allow2.com/install/steamdeck.json
327-
# (exactly what the Worker fetches), so it is written to the bucket-root key
328-
# `install/steamdeck.json` — the same bucket-root convention as robots.txt above,
329-
# NOT under the steamdeck/<channel>/ prefix.
325+
# NOTE: other platform repos (windows / mac / android) write their OWN
326+
# [staging/]install/<platform>.json the SAME way from their own release
327+
# pipelines — this repo owns ONLY the steamdeck entry.
330328
INSTALL_PAGE_URL="https://get.allow2.com/steamdeck/${SUBDIR}/"
329+
if [ "${CHANNEL}" = "staging" ]; then
330+
MANIFEST_KEY="staging/install/steamdeck.json"
331+
else
332+
MANIFEST_KEY="install/steamdeck.json"
333+
fi
331334
MANIFEST_JSON="$(mktemp "${TMPDIR:-/tmp}/steamdeck.json.XXXXXX")"
332335
# Chain into any existing EXIT trap (the GPG block may have set one for PUB_REF)
333336
# so both temp files are cleaned up — a bare `trap ... EXIT` would replace it.
@@ -341,16 +344,17 @@ cat > "${MANIFEST_JSON}" <<JSON
341344
"updatedAt": "$(date -u +%FT%TZ)"
342345
}
343346
JSON
344-
r2_put "${MANIFEST_JSON}" "install/steamdeck.json" "application/json" "no-cache"
345-
echo " wrote install/steamdeck.json -> ${INSTALL_PAGE_URL}"
347+
r2_put "${MANIFEST_JSON}" "${MANIFEST_KEY}" "application/json" "no-cache"
348+
echo " wrote ${MANIFEST_KEY} -> ${INSTALL_PAGE_URL}"
346349

347-
# ── 4a. Publish the domain-root robots.txt (excludes the staging beta) ────────
350+
# ── 4a. Publish the domain-root robots.txt (excludes everything "staging") ────
348351
# This is a DOMAIN-ROOT object (served at get.allow2.com/robots.txt), NOT under
349352
# steamdeck/<channel>/ — robots.txt is only honoured at the site root. It is
350-
# channel-agnostic (Disallow: /steamdeck/staging/, stable stays crawlable), so we
351-
# (re-)upload it on EVERY publish regardless of channel: idempotent + cheap, and
352-
# it can't be clobbered by the per-channel --delete sync (that only scopes the
353-
# steamdeck/<channel>/ prefix).
353+
# channel-agnostic (Disallow: /*staging covers the staging beta pages AND the
354+
# staging install-router route + its /staging/install/ manifests; stable +
355+
# the prod /install route stay crawlable), so we (re-)upload it on EVERY publish
356+
# regardless of channel: idempotent + cheap, and it can't be clobbered by the
357+
# per-channel --delete sync (that only scopes the steamdeck/<channel>/ prefix).
354358
ROBOTS_SRC="${PROJECT_ROOT}/robots.txt"
355359
[ -f "${ROBOTS_SRC}" ] || { echo "ERROR: ${ROBOTS_SRC} not found"; exit 1; }
356360
r2_put "${ROBOTS_SRC}" "robots.txt" "text/plain"
@@ -398,7 +402,7 @@ if [ -n "${CLOUDFLARE_API_TOKEN:-}" ] && [ -n "${CLOUDFLARE_ZONE_ID:-}" ]; then
398402
"${PUBLIC_URL}/hardening.html",
399403
"${PUBLIC_URL}/${REF_FILE}",
400404
"https://get.allow2.com/robots.txt",
401-
"https://get.allow2.com/install/steamdeck.json"
405+
"https://get.allow2.com/${MANIFEST_KEY}"
402406
]}
403407
JSON
404408
)
@@ -407,7 +411,7 @@ JSON
407411
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
408412
-H "Content-Type: application/json" \
409413
--data "${PURGE_FILES}" >/dev/null
410-
echo " purged ${SUBDIR}/summary{,.sig,.idx} + config + bare-dir URL (/ and no-slash) + index.html + hardening.html + ${REF_FILE} + root robots.txt + install/steamdeck.json"
414+
echo " purged ${SUBDIR}/summary{,.sig,.idx} + config + bare-dir URL (/ and no-slash) + index.html + hardening.html + ${REF_FILE} + root robots.txt + ${MANIFEST_KEY}"
411415
else
412416
echo " SKIPPED — set CLOUDFLARE_API_TOKEN + CLOUDFLARE_ZONE_ID to auto-purge."
413417
echo " Until then, testers may see stale metadata until the CDN TTL expires."

‎workers/install-router/README.md‎

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,42 @@
1-
# install-router — get.allow2.com/install
1+
# install-router — get.allow2.com/install (+ /staging)
22

3-
A dependency-free Cloudflare Worker that powers **`get.allow2.com/install`** as
4-
the single, universal entry point for **all** Allow2 installers, across every
5-
platform.
3+
A dependency-free Cloudflare Worker that powers the single, universal entry point
4+
for **all** Allow2 installers, across every platform.
65

76
This is the cross-platform funnel entry, **not** a Steam-Deck-specific page. It
87
lives in the allow2linux repo for now only because the `get.allow2.com` tooling
98
(`scripts/publish.sh`, `flatpak/gen-install-page.sh`) already lives here.
109

10+
## Co-hosted staging + production (same code, two envs)
11+
12+
The **same** Worker source runs as a **co-hosted staging+prod pair** on
13+
get.allow2.com — mirroring how get.allow2.com already co-hosts
14+
`steamdeck/stable` + `steamdeck/staging`. The two environments differ **only** by
15+
their wrangler `[env.*.vars]`, never by hardcoded logic:
16+
17+
| | **production** | **staging** |
18+
| --- | --- | --- |
19+
| Deployed from branch | `main` | `staging` |
20+
| Worker route (EXACT) | `get.allow2.com/install` | `get.allow2.com/staging` |
21+
| Wrangler env | `--env production` | `--env staging` |
22+
| Worker script name | `allow2-install-router` | `allow2-install-router-staging` |
23+
| `CHANNEL` | `stable` | `staging` |
24+
| `MANIFEST_BASE` (manifest reads) | `get.allow2.com/install/<p>.json` | `get.allow2.com/staging/install/<p>.json` |
25+
| `DEFAULT_INSTALL` (deck fail-safe) | `get.allow2.com/steamdeck/stable/` | `get.allow2.com/steamdeck/staging/` |
26+
| Sends visitors to | STABLE install pages | STAGING install pages |
27+
| `NOINDEX` | `""` (indexable) | `"1"` → `X-Robots-Tag: noindex, nofollow` on **every** response |
28+
29+
**Routes are EXACT paths, not wildcards** (`get.allow2.com/install`, not
30+
`/install*`). That is what lets each Worker read its own manifests without
31+
looping: the prod Worker fetches `get.allow2.com/install/<p>.json` and the
32+
staging Worker fetches `get.allow2.com/staging/install/<p>.json` — those are
33+
**sub-paths** of the route, and an exact route does not match sub-paths, so the
34+
manifest reads fall through to the R2 origin instead of re-entering the Worker.
35+
36+
Every default in the Worker's `config()` is **prod-safe** (stable pages, no
37+
noindex), so a missing or mis-set env fails toward production behaviour and can
38+
never accidentally deindex prod or point it at staging pages.
39+
1140
## What it does
1241

1342
On a `GET`, it reads the `User-Agent`, resolves the platform to a real install
@@ -26,10 +55,12 @@ Any non-`GET` method returns `405`.
2655
## Data-driven: per-platform manifests in R2
2756

2857
The per-platform install URLs are **not baked into the Worker**. At request time
29-
the Worker fetches a tiny JSON manifest from R2:
58+
the Worker fetches a tiny JSON manifest from R2, at the base for its env
59+
(`MANIFEST_BASE`):
3060

3161
```
32-
https://get.allow2.com/install/<platform>.json
62+
production: https://get.allow2.com/install/<platform>.json
63+
staging: https://get.allow2.com/staging/install/<platform>.json
3364
```
3465

3566
and `302`s to that manifest's `.url`. The fetch is **edge-cached for 5 minutes**
@@ -38,9 +69,12 @@ request, and a freshly-published install URL goes live within the TTL (the
3869
publisher also purges the manifest for instant pickup).
3970

4071
Each platform repo publishes its **own** manifest from its **own** release
41-
pipeline. In this repo, `scripts/publish.sh` writes `install/steamdeck.json`
42-
pointing at the channel it just published. Shipping a new install page therefore
43-
requires **no Worker redeploy** — only a manifest rewrite.
72+
pipeline. In this repo, `scripts/publish.sh` is **channel-aware**: publishing the
73+
`production` channel writes `install/steamdeck.json` (→ `steamdeck/stable/`), and
74+
publishing the `staging` channel writes `staging/install/steamdeck.json`
75+
(→ `steamdeck/staging/`) — the two never clobber each other, and each env's
76+
Worker reads the matching one. Shipping a new install page therefore requires
77+
**no Worker redeploy** — only a manifest rewrite.
4478

4579
The **only** things baked into the Worker are (a) the **list of known platforms**
4680
(`steamdeck`, `windows`, `mac`, `android`) and (b) the **UA→platform detection**
@@ -101,36 +135,44 @@ iPad that is off the target device.
101135

102136
## Deploy
103137

104-
The production route is bound **only** under the `production` env in
105-
`wrangler.toml` (so a bare `wrangler deploy` can't claim the path):
138+
Each route is bound **only** under its env in `wrangler.toml` (so a bare
139+
`wrangler deploy` can't claim a path):
106140

107141
```sh
108142
cd workers/install-router
109-
wrangler deploy --env production
143+
wrangler deploy --env production # binds get.allow2.com/install (stable, indexable)
144+
wrangler deploy --env staging # binds get.allow2.com/staging (staging, noindex)
110145
```
111146

112-
That command creates/updates the route `get.allow2.com/install` (zone
113-
`allow2.com`) and binds this Worker to it. CI does exactly this:
147+
Deploys are **branch-driven** (ecosystem convention; **`master` is being deleted
148+
— never referenced**):
114149

115-
- **`.github/workflows/deploy-install-router.yml`** runs `deploy --env production`
116-
on a push to the default branch (paths-filtered to `workers/install-router/**`),
117-
and `deploy --env production --dry-run` on PRs (validate only).
150+
- **`.github/workflows/deploy-install-router.yml`** (paths-filtered to
151+
`workers/install-router/**`):
152+
- push to **`main`** → `deploy --env production`
153+
- push to **`staging`** → `deploy --env staging`
154+
- **pull_request** → `deploy --env production --dry-run` (validate only)
118155
- It pins `cloudflare/wrangler-action@v3` with `wranglerVersion: '3.90.0'`
119156
(v4 defaults to Wrangler 4 — deliberately not adopted).
120157
- Secrets: `CF_WORKERS_API_TOKEN` (an "Edit Cloudflare Workers"-template token,
121158
zone-scoped to `allow2.com` — **distinct** from the cache-purge-only
122159
`CLOUDFLARE_API_TOKEN` used by `publish.yml`) and `CLOUDFLARE_ACCOUNT_ID`.
123160

124-
The Worker only handles `/install`; the rest of `get.allow2.com` is untouched.
161+
The Worker only handles the exact `/install` and `/staging` paths; the rest of
162+
`get.allow2.com` is untouched.
125163

126164
## Verify locally
127165

128166
```sh
129167
node --check src/index.js
130-
node test/harness.mjs # asserts UA -> target, manifest fetch, fail-safe, chooser, ?platform=
168+
node test/harness.mjs # UA -> target, manifest fetch, fail-safe, chooser, ?platform=, env-awareness
131169
wrangler deploy --env production --dry-run # optional, if wrangler is installed
170+
wrangler deploy --env staging --dry-run # optional
132171
```
133172

134173
The harness mocks `fetch` to return a manifest / 404 / malformed JSON and
135174
asserts the redirect target, the `DEFAULT_URL` fail-safe, the chooser render,
136-
and `?platform=` resolution.
175+
`?platform=` resolution, and the **env-awareness**: prod (stable pages, no
176+
noindex, prod manifest base) vs staging (staging pages, `X-Robots-Tag:
177+
noindex, nofollow` on every response, staging manifest base, `/staging`
178+
self-links).

0 commit comments

Comments
 (0)