diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 69380b41e..1d324d3c9 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,7 +11,7 @@ permissions: id-token: write concurrency: - group: pages-${{ github.event_name }}-${{ github.ref }} + group: pages-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: @@ -99,9 +99,8 @@ jobs: retention-days: 30 deploy: - # Keep production deployment manual until the Astro cutover is complete. - # Pushes and pull requests still build and validate the full Pages artifact. - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' + # Publish validated main updates; pull requests only build and check. + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') concurrency: group: pages-production cancel-in-progress: false diff --git a/.github/workflows/workers.yml b/.github/workflows/workers.yml index 4075e25de..7461a84ae 100644 --- a/.github/workflows/workers.yml +++ b/.github/workflows/workers.yml @@ -154,6 +154,10 @@ jobs: if: inputs.operation == 'deploy' && inputs.environment == 'production' && (inputs.worker == 'both' || inputs.worker == 'redirects') run: npx wrangler deploy --env production --config workers/redirects/wrangler.jsonc + - name: Preserve origin fallback for production redirects + if: inputs.operation == 'deploy' && inputs.environment == 'production' && (inputs.worker == 'both' || inputs.worker == 'redirects') + run: node scripts/set-redirects-fail-open.mjs + - name: Deploy email Worker to production if: inputs.operation == 'deploy' && inputs.environment == 'production' && inputs.worker == 'email' run: npx wrangler deploy --config workers/email/wrangler.jsonc diff --git a/docs/deployment-runbook.md b/docs/deployment-runbook.md index 1cac46089..b6b4fc5ea 100644 --- a/docs/deployment-runbook.md +++ b/docs/deployment-runbook.md @@ -15,8 +15,9 @@ small Pages fallback files. As checked on 5 September 2026, Cloudflare is authoritative for `gecode.dev`. Cloudflare proxies the apex GitHub Pages A records and the `www` CNAME to `gecode.github.io` with Full (strict) TLS and Always Use HTTPS. GitHub Pages -still serves the active website. All nine historical documentation versions -and both latest aliases now use R2. Astro has not been deployed. +serves the Astro active website and mail archive. All nine historical +documentation versions and both latest aliases use R2. Classic active-site +URLs redirect to their canonical directory URLs through the redirect Worker. Cloudflare Email Routing is ready. Its managed MX, SPF, and DKIM records are authoritative, both forwarding destinations are verified, and the catch-all @@ -39,17 +40,29 @@ records the local fixes, rollback build, verification limits and remaining work. Phases 2–4 below describe the original migration sequence; DNS delegation and infrastructure setup are largely complete. Staging deployment through GitHub and clean-checkout website CI pass. The website migration is merged -in PR #7, with follow-up fixes in PRs #8 and #9; Pages remains manual. +in PR #7, with follow-up fixes in PRs #8 and #9. Production deployment and canary cleanup passed. The final checks completed -on 5 September at about 14:15 UTC. Continue with the one-day documentation -soak; do not deploy Astro before 6 September, 16:30 Europe/Stockholm. +on 5 September at about 14:15 UTC. The cutover gate is a fresh passing production smoke check, a review of +Worker errors, passing website CI and a verified rollback archive. A fixed +24-hour wait is not required; the owner approved proceeding on this basis. + +Astro deployment [33972798507](https://github.com/Gecode/gecode.github.io/actions/runs/33972798507) +and redirect deployment [33973076305](https://github.com/Gecode/gecode.github.io/actions/runs/33973076305) +succeeded on 5 September. All 23 canonical pages and classic redirects pass +live checks, including query preservation. Archive search and thread reading +work at mobile width; documentation smoke checks still pass. Redirect Worker version `1165ee03-782b-4f83-a687-0164a68d3087` is live. All twelve +redirect routes fail open to Pages fallbacks; both documentation routes remain +fail closed. The production redirect workflow sets and verifies this route +setting on every deployment with `scripts/set-redirects-fail-open.mjs`. +The pre-cutover Worker query reported 1,116 requests and zero +execution errors from 14:10 to 14:45:53 UTC. ## Cutover overview Keep each traffic change separate. Finish the preparation and bulk transfer without changing production, move DNS while GitHub Pages still serves the entire old site, move documentation, and publish Astro only after the -documentation path has soaked. +documentation health checks pass and rollback is ready. | Phase | When | Mostly unattended work | Attended checkpoint | Safe state afterward | | --- | --- | --- | --- | --- | @@ -57,8 +70,8 @@ documentation path has soaked. | 2. Prepare Cloudflare and GitHub | After phase 1; no maintenance window required | Create the pending zone, bucket, lifecycle rule, environments, and secrets. | Verify the Email Routing destinations and prepare the disabled routing Worker. | The public site is unchanged; Cloudflare has no traffic. | | 3. Copy documentation | After the R2 token exists; run overnight if useful | Upload and verify 6.4.0, then all historical immutable versions. | Review the manifests and representative files before DNS delegation. | GitHub Pages still serves production; R2 holds a verified copy. | | 4. Delegate DNS | In a short attended window after phases 2 and 3 | Wait for nameserver propagation and the Cloudflare zone to become active. | Test the website, TLS, apex redirect, and every mail alias. Restore the Namecheap nameservers if any of them fail. | GitHub Pages still serves the entire site through Cloudflare. No Worker production routes exist. | -| 5. Move documentation | After DNS is stable; use a separate attended window | Let the production documentation routes soak for at least one day. | Test one version through the canary route, deploy the production routes, retest, and remove the canary. | R2 serves `/doc/*` and `/doc-latest/*`; GitHub Pages still contains the old documentation for quick rollback. | -| 6. Publish Astro | After the documentation soak; use a separate attended window | Let the Pages deployment and monitoring run. | Deploy the reviewed Astro artifact and check the parity routes at desktop and mobile sizes. | Astro serves the website; R2 continues to serve documentation. Redeploy the previous Pages artifact to roll back Astro. | +| 5. Move documentation | After DNS is stable; use a separate attended window | Check production documentation responses and Worker errors before proceeding. | Test one version through the canary route, deploy the production routes, retest, and remove the canary. | R2 serves `/doc/*` and `/doc-latest/*`; GitHub Pages still contains the old documentation for quick rollback. | +| 6. Publish Astro | After documentation health checks pass; use a separate attended step | Let the Pages deployment and monitoring run. | Deploy the reviewed Astro artifact and check the parity routes at desktop and mobile sizes. | Astro serves the website; R2 continues to serve documentation. Redeploy the previous Pages artifact to roll back Astro. | | 7. Enable permanent active-site redirects | After Astro is confirmed healthy, either later that day or the next day | Monitor redirect and origin errors. | Deploy the redirect Worker and test every classic active-site `.html` URL. | Classic URLs return `308`; Pages fallback files remain available if the redirect Worker fails. | | 8. Add release automation and the web manual | After the cutover is stable | Build, validate, upload, and verify future documentation from release workflows. | Approve immutable publication and the separate `latest` change. | New Doxygen and modeling-manual releases follow the coordinated release pipeline. | @@ -66,14 +79,15 @@ Phases 2 and 3 contain most of the work and can run while unattended. Phases 4 through 7 are deliberately short and separated by stable, working states. At no point does a scheduled wait require `gecode.dev` to be unavailable. -Do not deploy the active-site redirect Worker before phase 6. The current -Jekyll origin serves `/download.html` and similar classic URLs but returns 404 +Do not deploy the active-site redirect Worker before phase 6. The pre-cutover +Jekyll origin served `/download.html` and similar classic URLs but returned 404 for their new directory forms. The Astro artifact includes fallback redirect files, so deploying the redirect Worker after Astro is safe. -Keep the Pages deploy job manual-only throughout the migration. Pushes and pull -requests still build and validate the artifact. Restore automatic default-branch -deployment in a separate change after the Astro deployment is stable. +Pages now deploys validated pushes to `main`; manual deployment from `main` +remains available. Pull requests only build and check. Push and manual runs +share concurrency for their branch so builds cannot publish out of order, +and production deployments are serialized. The first R2 migration does not depend on the rewritten *Modeling and Programming with Gecode* site. Reserve `modeling/` in each release tree now and @@ -358,7 +372,9 @@ bucket-wide deletion. During the first canary, remove the canary route to return that version to the existing GitHub Pages archive. Before the Astro cutover, remove the production documentation routes for the same rollback. If Astro itself must be rolled -back, redeploy a retained, verified Pages artifact. New Pages artifacts have 30-day +back, disable the Pages workflow and cancel pending/running push and manual +deployments before restoring a retained, verified Pages artifact. Rerunning +`pages.yml` rebuilds Astro; it does not restore the classic archive. New Pages artifacts have 30-day retention. The July 2026 artifact has expired; prepare the small classic-site rollback described below before the first cutover. Remove the redirect Worker while the old artifact is active because that artifact @@ -385,7 +401,10 @@ the existing Bundler dependencies installed. It creates an existing output directory. The 5 September rehearsal produced 91,168,795 uncompressed bytes. A verified copy is retained at `/Users/zayenz/gecode/website-rollback/classic-site-2026-07-15.tar.gz`; -restore its contents as the Pages artifact when needed. +restore its contents as the Pages artifact when needed. Executable restoration +instructions are retained beside it as `restore-classic-pages.md`. They use a +temporary payload branch and a manual restore workflow on `main`, preserving +the existing Pages environment restrictions. Before Astro, removing the documentation routes restores the existing origin archive. After Astro, restore the previous Worker deployment for a serving diff --git a/docs/migration-readiness-review-2026-09-05.md b/docs/migration-readiness-review-2026-09-05.md index c7cc8266d..c3b7a2970 100644 --- a/docs/migration-readiness-review-2026-09-05.md +++ b/docs/migration-readiness-review-2026-09-05.md @@ -9,9 +9,11 @@ trees were preserved. DNS migration, full historical R2 verification, staging/canary checks and the production documentation cutover are complete. The final production checks -passed on 5 September at about 14:15 UTC. Jekyll still serves the active website. -Allow at least one day of documentation soak before the separate Astro cutover -(no earlier than 6 September, 16:30 Europe/Stockholm). Future coordinated releases have a +passed on 5 September at about 14:15 UTC. Astro and active-site redirects +were subsequently deployed and verified on the same day. +The owner approved proceeding after fresh production health checks, passing +website CI and verification of the retained rollback archive; no fixed +24-hour wait is required. Future coordinated releases have a working local preparation slice; their publication coordinator is unfinished. The hosting arrangement is Cloudflare DNS/proxy, Workers and private R2, with @@ -24,12 +26,12 @@ or Workers is a separate project and is unnecessary for this cutover. | --- | --- | | DNS | Cloudflare nameservers `milan` and `tegan` are authoritative; the zone is active. MX and SPF use Cloudflare Email Routing. | | TLS | Cloudflare uses Full (strict) and Always Use HTTPS. Public HTTP redirects to HTTPS and the apex redirects to `www`. GitHub reports `https_enforced: false`; reconcile the origin setting separately. | -| Production website | `/download.html` returns 200; `/download/` returns 404. Jekyll remains live. | +| Production website | Astro is live. All 23 canonical pages return 200; their classic URLs return 308 with queries preserved. The twelve redirect routes fail open to Pages fallbacks. | | Production documentation | All nine historical versions and both aliases use R2. Only `/doc/latest/...` is indexable. The two production route patterns are fail-closed; the canary and superseded routes have been removed. | | Staging documentation | The reviewed Worker was deployed through GitHub. Its tests and live HTTP smoke checks pass for immutable 6.4.0, both aliases, canonical links, redirects, static assets and PDF range behavior. | | R2 | Public `r2.dev` access is disabled. The 14-day lifecycle applies only to staging. All nine archives were fully verified: 52,385 objects / 1,138,898,740 bytes, with no missing objects or hash failures. The 326 historical image-map MIME declarations are documented below. | | Email | Email Routing destinations and catch-all Worker configuration are present. Real delivery and delivery alerts were not exercised. | -| GitHub | The latest production Pages run remains the [15 July deployment](https://github.com/Gecode/gecode.github.io/actions/runs/29430643821). The [5 September staging Worker run](https://github.com/Gecode/gecode.github.io/actions/runs/33964652752) passed. The migration is merged on `main`; clean-checkout [CI passed](https://github.com/Gecode/gecode.github.io/actions/runs/33967188164), with Pages deployment skipped. | +| GitHub | [Astro deployment](https://github.com/Gecode/gecode.github.io/actions/runs/33972798507) and [redirect deployment](https://github.com/Gecode/gecode.github.io/actions/runs/33973076305) passed. Validated `main` pushes deploy automatically; PRs only build and check. | | Deployment credentials | All three Cloudflare environments now contain the account ID and dedicated `gecode-github-workers` token. Production accepts only `main` and `release/*-website` branches; the existing `zayenz` reviewer and approval settings were preserved. | ## Fixes completed @@ -82,11 +84,11 @@ After Astro, recover the previous Worker deployment for serving failures. Changing `LATEST_DOC_VERSION` repairs aliases only, not explicit version URLs. Do not delete immutable R2 objects during rollback. -## Remaining cutover sequence +## Cutover sequence and completion 1. **Website code landed.** The migration, archive and Worker changes are on - `main`, and clean-checkout CI passed. Pages deployment remains manual and - the classic rollback archive is retained outside the checkout. MPG and + `main`, and clean-checkout CI passed. The classic rollback archive and + executable restoration instructions are retained outside the checkout. MPG and release-support changes still need separate commits and producer rehearsal. 2. **Deployment access verified.** The dedicated Worker token is installed in `cloudflare-staging`, `cloudflare-canary` and `cloudflare-production`. @@ -97,20 +99,21 @@ Do not delete immutable R2 objects during rollback. 3. **Operational checks partly complete.** Historical uploads and fail-closed documentation routes are verified. Native documentation logs are enabled. Real mail delivery and useful failure alerts remain to be checked; redirect - routes will be checked after Astro. Keep the current DNS delegation. + routes now have their verified fail-open setting. Keep the current DNS delegation. 4. **Canary documentation verified.** The 6.4.0 deployment passed HTML, source folding, changelog fragment, CSS/JS/image, PDF ranges, missing paths, - canonical links and sitemap checks. Ordinary Jekyll pages remain unchanged. + canonical links and sitemap checks. Ordinary Jekyll pages stayed unchanged during this step. 5. **Documentation moved.** Production routes, every historical version and - both aliases pass verification. The canary is removed. Allow at least one - day with the Jekyll origin still available as fallback before Astro. -6. **Publish Astro.** Deploy `build:pages` from reviewed `main`. Verify ordinary - pages, downloads, publications, archive search/browsing and mobile layout, - plus R2 documentation. The Pages artifact must contain no documentation tree. -7. **Enable redirects and routine updates.** Deploy the classic URL redirect - Worker only after directory routes work. Check query and fragment behavior; - documentation and archive `.html` URLs remain content. Once stable, enable - automatic `main` Pages deployment and record the deployed Worker revision. + both aliases pass verification. The canary is removed. Before Astro, check + public documentation and Worker errors again and verify the rollback archive. +6. **Astro published.** The reviewed `main` artifact passed CI and deployed. + All 23 canonical pages, downloads, publications, archive search-to-thread + navigation and mobile layout pass. R2 documentation smoke checks pass. +7. **Redirects and routine updates enabled.** All classic active-site URLs + redirect after the directory routes were verified. Documentation and archive + `.html` URLs remain content. Validated `main` pushes deploy automatically. + Production redirect deployments also set and verify route fail-open behavior. + Live redirect Worker version: `1165ee03-782b-4f83-a687-0164a68d3087`. The installed Worker token has Account Settings Read, Workers Scripts Write and Workers R2 Storage Read, plus Zone Read and Workers Routes Write scoped to @@ -269,7 +272,7 @@ Completed locally: - The 91 MB classic rollback build from the exact July commit succeeds. Still required: a complete coordinated Gecode producer rehearsal, the -documentation soak, Astro cutover, real mail delivery, and failure-alert setup. +real mail delivery and failure-alert setup. Clean-checkout CI and internal lychee checks pass; nonblocking external checks -include new Astro URLs that are not live yet and broken historical external links. The archive browser samples are not +include broken historical external links. The archive browser samples are not an exhaustive accessibility or Lighthouse audit. diff --git a/scripts/set-redirects-fail-open.mjs b/scripts/set-redirects-fail-open.mjs new file mode 100644 index 000000000..3f4f8953e --- /dev/null +++ b/scripts/set-redirects-fail-open.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +const config = JSON.parse(await readFile("workers/redirects/wrangler.jsonc", "utf8")).env.production; +assert.equal(config.name, "gecode-classic-url-redirects", "Unexpected production redirect Worker"); +const patterns = config.routes.map((route) => { + assert.equal(route.zone_name, "gecode.dev", "Unexpected redirect zone"); + assert(/^www\.gecode\.dev\/(?:[a-z-]+\.html|publications)\*$/.test(route.pattern), + "Unexpected active-site redirect pattern"); + return route.pattern; +}); +assert(patterns.length > 0 && new Set(patterns).size === patterns.length, "Invalid redirect pattern set"); + +const account = process.env.CLOUDFLARE_ACCOUNT_ID; +const token = process.env.CLOUDFLARE_API_TOKEN; +assert(/^[a-f0-9]{32}$/i.test(account ?? ""), "CLOUDFLARE_ACCOUNT_ID is required"); +assert(token?.trim(), "CLOUDFLARE_API_TOKEN is required"); + +async function api(path, method = "GET", body) { + const response = await fetch(`https://api.cloudflare.com/client/v4${path}`, { + method, + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + signal: AbortSignal.timeout(30_000), + }); + assert(response.ok, `Cloudflare route request failed (HTTP ${response.status})`); + const result = await response.json(); + assert.equal(result.success, true, "Cloudflare did not confirm the route request"); + return result.result; +} + +const zones = await api(`/zones?name=gecode.dev&account.id=${account}`); +assert.equal(zones.length, 1, "Expected exactly one gecode.dev zone"); +assert.equal(zones[0].name, "gecode.dev", "Unexpected zone"); +assert.equal(zones[0].account.id, account, "Unexpected zone account"); +const routePath = `/zones/${zones[0].id}/workers/routes`; + +function redirectRoutes(routes) { + const owned = routes.filter((route) => route.script === config.name); + assert.deepEqual(owned.map((route) => route.pattern).sort(), [...patterns].sort(), + "Live redirect routes must match the production configuration"); + for (const pattern of patterns) { + const matches = routes.filter((route) => route.pattern === pattern); + assert.equal(matches.length, 1, "Expected exactly one route for each redirect pattern"); + assert.equal(matches[0].script, config.name, "Redirect route belongs to another Worker"); + } + return owned; +} + +// Wrangler does not configure this flag. Apply it only after checking the +// complete redirect route set, so documentation routes remain fail closed. +for (const route of redirectRoutes(await api(routePath))) { + if (route.request_limit_fail_open === true) continue; + await api(`${routePath}/${route.id}`, "PUT", { + pattern: route.pattern, + script: config.name, + request_limit_fail_open: true, + }); +} +assert(redirectRoutes(await api(routePath)).every((route) => route.request_limit_fail_open === true), + "Redirect routes did not retain fail-open behavior"); +console.log(`Verified fail-open behavior for ${patterns.length} production redirect routes.`);