diff --git a/.github/workflows/watch.yml b/.github/workflows/watch.yml index 91c61fd789..b9af8ed0a7 100644 --- a/.github/workflows/watch.yml +++ b/.github/workflows/watch.yml @@ -6,17 +6,18 @@ # parity script normalizes sitemap entries: URL pathname, trailing slash # stripped, "" → "/"). # -# On any failure the run files an issue on this repo (labeled -# production-watch) with the captured output, and the run itself fails. +# On any failure the run reports on this repo and the run itself fails. If an +# issue labeled production-watch is already open, the run comments on it +# instead of filing a duplicate (#118-#179 were one stale fixture entry +# re-filed every 6 hours); otherwise it opens a new one. # -# NOTE: downgrade the cron to weekly after 2026-08-29 (14 days -# post-cutover) — every 6 hours is only warranted while the fiestaboard.app -# cutover settles. +# Runs weekly: the 6-hourly cadence was only for the fiestaboard.app cutover, +# which settled after 2026-08-29. Use workflow_dispatch for an ad-hoc check. name: Production watch on: schedule: - - cron: "41 */6 * * *" # every 6 hours, off the top of the hour + - cron: "41 14 * * 1" # weekly, Monday 14:41 UTC, off the top of the hour workflow_dispatch: {} permissions: @@ -131,6 +132,17 @@ jobs: --description "Filed automatically by the production watch workflow" \ --color D93F0B 2>/dev/null || true + # De-duplicate: report on the oldest open production-watch issue + # if there is one, rather than filing a new issue every run. + EXISTING="$(gh issue list --repo "$GITHUB_REPOSITORY" \ + --label production-watch --state open --limit 100 \ + --json number --jq 'map(.number) | min // empty' 2>/dev/null || true)" + if [ -n "$EXISTING" ]; then + echo "Open production-watch issue #$EXISTING exists; commenting instead of filing a new one." + gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file "$BODY" + exit 0 + fi + if ! gh issue create --repo "$GITHUB_REPOSITORY" \ --title "$TITLE" --label production-watch --body-file "$BODY"; then echo "::warning::issue creation with label failed; retrying without label" diff --git a/tests/routes-fixture-versions.test.mjs b/tests/routes-fixture-versions.test.mjs new file mode 100644 index 0000000000..82c8dcb048 --- /dev/null +++ b/tests/routes-fixture-versions.test.mjs @@ -0,0 +1,48 @@ +// Keeps tests/routes.fixture.json consistent with which docs versions a +// deploy actually builds. Deploys only build versions.slice(0, +// DEPLOY_VERSION_CAP) (docusaurus.config.ts); every older snapshot is served +// as a redirect stub to the current docs. Each release snapshot pushes one +// version off the cap, so a fixture entry that pins an old version as a +// "page" silently goes stale — that is what filed #118–#179 (8.28 fell off +// the cap with the 9.10 snapshot on 2026-10-03). This test fails the +// snapshot PR instead of the production watch. + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; + +const read = (p) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8"); +const versions = JSON.parse(read("versions.json")); +const fixture = JSON.parse(read("tests/routes.fixture.json")); +const capMatch = read("docusaurus.config.ts").match(/const DEPLOY_VERSION_CAP = (\d+);/); + +test("DEPLOY_VERSION_CAP is readable from docusaurus.config.ts", () => { + assert.ok(capMatch, "could not find `const DEPLOY_VERSION_CAP = ;`"); +}); + +const built = new Set(versions.slice(0, Number(capMatch?.[1] ?? 0))); +const known = new Set(versions); +const versioned = fixture + .map((e) => ({ e, v: e.path.match(/^\/docs\/(\d+\.\d+)(?:\/|$)/)?.[1] })) + .filter(({ v }) => v && known.has(v)); + +for (const { e, v } of versioned) { + test(`fixture ${e.path} (${e.expect}) agrees with the version cap`, () => { + if (built.has(v)) { + // Built version: either a real page or the version root → its intro. + if (e.expect === "redirect") + assert.ok( + e.target.startsWith(`/docs/${v}/`), + `${v} is built, so ${e.path} should not redirect out of /docs/${v}/ (target ${e.target})`, + ); + } else { + assert.equal( + e.expect, + "redirect", + `${v} is past DEPLOY_VERSION_CAP and is no longer built — ${e.path} is a redirect stub, not a page`, + ); + const rest = e.path.slice(`/docs/${v}`.length) || "/intro"; + assert.equal(e.target, `/docs${rest}`, `${e.path} should redirect to the current docs at the same path`); + } + }); +} diff --git a/tests/routes.fixture.json b/tests/routes.fixture.json index 2eb73ce02e..1d56bf2e36 100644 --- a/tests/routes.fixture.json +++ b/tests/routes.fixture.json @@ -9,7 +9,7 @@ { "path": "/docs/troubleshooting", "expect": "page" }, { "path": "/docs/8.30/intro", "expect": "page", "fullBuildOnly": true }, - { "path": "/docs/8.28/setup/quick-start", "expect": "page", "fullBuildOnly": true }, + { "path": "/docs/8.28/setup/quick-start", "expect": "redirect", "target": "/docs/setup/quick-start" }, { "path": "/docs/8.15/intro", "expect": "redirect", "target": "/docs/intro", "fullBuildOnly": true }, { "path": "/docs/8.30", "expect": "redirect", "target": "/docs/8.30/intro", "fullBuildOnly": true },