From 21689ef59b0eb34b1a29eda739dc10d33d25b44a Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 10 Jul 2026 17:14:13 +0300 Subject: docs: Cloudflare Workers hosting pipeline (apex, content workers, CI, previews) (#2140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs-infra: scaffold Cloudflare workers workspace (versions.json v2, matrix, toolchain) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: record full Phase-0 plan decision in workers/PLAN.md ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: shared content worker โ€” asset serving, cache classes, X-Docs-Build, canary no-store ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: run worker script before assets; test fetch entrypoint ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex manifest loader + dispatch map + runtime binding guard (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex redirects (aliases, PDF, trailing-slash) + special paths (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: PDF redirect honors pdf:null and preserves query ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex UA gate โ€” allowlist-wins, log-only AI crawlers, empty block list at launch ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex router (pipeline ยง3.2), themed 404/503, /kb seam, env configs + congruence test ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: add missing-User-Agent regression test for apex UA gate ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: R2-streaming preview worker โ€” MIME map, noindex, no-store (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: preview 404 no-store + fetch handler tests ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: bootstrap script โ€” binding-target workers must exist before apex deploys ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex run_worker_first, lockfile for npm ci, PDF Location from manifest ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: derive html_baseurl from DOCS_VERSION_SLUG with RTD fallback (canonical gate prereq) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: version picker + status banner + language scaffold (vanilla JS, TDD pure core) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: picker preserves query+hash across switch; valid breadcrumb markup ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: Pagefind search wrapper with runtime base-path + preview prefix handling (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: pagefind wrapper โ€” asset-failure notice + UI stylesheet load ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: gate Pagefind searchbox to CF builds (RTD keeps stock search until cutover) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: deploy sanity gates โ€” limits, critical pages, count-delta, canonical (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: hermetic gate tests via fixture versions.json ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: docs-build workflow โ€” candidate/smoke/promote two-stage deploy + registry + rollback Two-stage CF Workers pipeline: build in pinned container, assemble artifact, sanity gates, deploy candidate, scoped pre-traffic smoke via canary apex, promote (rollback-id capture, hostname purge, registry upload), post-promote probe + auto-rollback. DOCS_CF_LIVE repo variable gates every docs.vyos.io production interaction pre-cutover. scripts/docs_gates/smoke.py adds one authorized check beyond the spec: the version's index.html probe asserts the #vyos-search mount div is present in the response body, guarding CI silently forgetting DOCS_VERSION_SLUG (which would otherwise ship stock RTD search without the Pagefind gate noticing). ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: build docs image in-workflow with buildx cache (v4.1 โ€” digest pin dropped) Plan v4.1 amendment: the ghcr.io digest-pinned image does not exist (workflow would hard-fail at the first docker step on every push). Replace the BUILD_IMAGE env placeholder with an in-workflow docker build from docker/Dockerfile via docker/setup-buildx-action@v3 + docker/build-push-action@v6 (context: docker/, load: true, tags: docs-build:local, GHA cache from/to). The checked-out commit is the pin; buildx GHA cache keeps repeat builds cheap. Sphinx-build step swaps to docs-build:local; inner script unchanged. ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex/preview deploy workflow โ€” canary auto, production behind environment approval * docs-infra: apex-deploy concurrency guard (per-ref, cancel-in-progress) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: fork-safe PR preview pipeline โ€” approval record, R2 prefixes, label consumption, cleanup * docs-infra: nightly preview sweep โ€” pipefail + per-prefix failure isolation ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: nightly canary QA โ€” per-entry sweep + URL-parity corpus vs RTD ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: parity sweep scoped to CF-built versions; transport-error resilience ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: one-off bootstrap workflow (binding targets โ€” runs once on this push) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: remove one-off bootstrap workflow (bootstrap complete) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: one-off canary apex + preview deploy (route targets for Task 3.6 step 2c) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: remove one-off canary deploy workflow (targets live) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: address Phase-0 CodeRabbit findings (canonical gate, error caching, registry pointer, validation) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: strengthen manifest tests (full dispatch iteration, mutation-free validate) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: address GitHub CodeRabbit review (pointer-after-probe, fail-closed sweeps, block-precedence UA gate, preview hardening) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: adversarial review fixes โ€” error no-store, probe retry, PR-list membership, preview dotted-segment ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: serve oversized legacy PDF from R2 via apex (spec ยง5 fallback) The 1.3 PDF (29.2 MiB) exceeds the 25 MiB static-asset cap and is absent from the legacy content Worker's build, so /_/downloads/en/1.3/pdf/ (and the picker's PDF link) 301'd into a dead-end 404 post-cutover. Add the R2 object fallback spec ยง5 already documented but never implemented: a DOCS_PDFS R2 bucket binding on the apex Worker, a manifest pdf_r2_key field (1.3 only), and a router step ahead of version dispatch that streams the object with its own cache class (canary/error still force no-store). ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: PDF R2 fallback honors Range + If-None-Match, preserves ETag ๐Ÿค– Generated by [robots](https://vyos.io) --- workers/picker-test/pagefind-wrapper.test.ts | 42 ++++++++++++++++++ workers/picker-test/picker.test.ts | 64 ++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 workers/picker-test/pagefind-wrapper.test.ts create mode 100644 workers/picker-test/picker.test.ts (limited to 'workers/picker-test') diff --git a/workers/picker-test/pagefind-wrapper.test.ts b/workers/picker-test/pagefind-wrapper.test.ts new file mode 100644 index 00000000..75e8be50 --- /dev/null +++ b/workers/picker-test/pagefind-wrapper.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from "vitest"; +// The workers pool has no real filesystem (node:fs readFileSync is an unimplemented +// stub โ€” see @cloudflare/vitest-pool-workers/dist/worker/lib/node/fs.mjs, and confirmed +// empirically here: "readFileSync() is not yet implemented in Workers"). Same constraint +// documented in picker.test.ts and apex/test/manifest.test.ts; import as Vite `?raw` asset +// instead so content is inlined at bundle time โ€” no runtime filesystem access needed. +// eslint-disable-next-line import/no-unresolved +import src from "../../docs/_static/js/pagefind-wrapper.js?raw"; + +const ns: Record = {}; +new Function("window", src)(ns as never); +const W = (ns as never as { VyOSSearch: Record }).VyOSSearch; + +describe("basePathFor (ยง9)", () => { + it("production/canary version path", () => { + expect(W.basePathFor("/en/rolling/search.html")) + .toEqual({ base: "/en/rolling/", prefix: "" }); + }); + it("PR preview path keeps /pr-/ prefix and reports it", () => { + expect(W.basePathFor("/pr-42/en/1.5/search.html")) + .toEqual({ base: "/pr-42/en/1.5/", prefix: "/pr-42" }); + }); + it("prefixes result URLs in previews", () => { + expect(W.prefixResultUrl("/en/1.5/cli/index.html", "/pr-42")).toBe("/pr-42/en/1.5/cli/index.html"); + expect(W.prefixResultUrl("/en/1.5/cli/index.html", "")).toBe("/en/1.5/cli/index.html"); + }); +}); + +describe("assetUrlsFor (css + js pair under the version base)", () => { + it("production/canary base", () => { + expect(W.assetUrlsFor("/en/rolling/")).toEqual({ + css: "/en/rolling/pagefind/pagefind-ui.css", + js: "/en/rolling/pagefind/pagefind-ui.js", + }); + }); + it("PR preview base keeps the /pr-/ prefix", () => { + expect(W.assetUrlsFor("/pr-42/en/1.5/")).toEqual({ + css: "/pr-42/en/1.5/pagefind/pagefind-ui.css", + js: "/pr-42/en/1.5/pagefind/pagefind-ui.js", + }); + }); +}); diff --git a/workers/picker-test/picker.test.ts b/workers/picker-test/picker.test.ts new file mode 100644 index 00000000..33ae2264 --- /dev/null +++ b/workers/picker-test/picker.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +// The workers pool has no real filesystem (node:fs readFileSync is an unimplemented +// stub โ€” see @cloudflare/vitest-pool-workers/dist/worker/lib/node/fs.mjs, and confirmed +// empirically here: "readFileSync() is not yet implemented in Workers"). Same constraint +// documented in apex/test/manifest.test.ts; import as Vite `?raw` / native JSON assets +// instead so content is inlined at bundle time โ€” no runtime filesystem access needed. +// eslint-disable-next-line import/no-unresolved +import src from "../../docs/_static/js/version-picker.js?raw"; +// eslint-disable-next-line import/no-unresolved +import manifest from "../versions.json"; + +// Evaluate the plain script and grab its namespace (no DOM access at module scope allowed). +const ns: Record = {}; +new Function("window", src)(ns as never); +const P = (ns as never as { VyOSVersionPicker: Record }).VyOSVersionPicker; + +describe("parseLocation", () => { + it("extracts lang/slug/rest from a docs path", () => { + expect(P.parseLocation("/en/1.5/cli/index.html")) + .toEqual({ lang: "en", slug: "1.5", rest: "cli/index.html" }); + }); + it("returns null off the version tree (e.g. previews without prefix knowledge)", () => { + expect(P.parseLocation("/kb/x")).toBeNull(); + }); +}); + +describe("bannerFor (ยง4)", () => { + it("dev โ†’ info banner", () => { + expect(P.bannerFor("rolling", manifest)).toMatchObject({ kind: "dev" }); + }); + it("newest lts โ†’ no banner; older lts โ†’ newer-lts notice naming 1.5", () => { + expect(P.bannerFor("1.5", manifest)).toBeNull(); + expect(P.bannerFor("1.4", manifest)).toMatchObject({ kind: "newer-lts", newest: "1.5" }); + }); + it("eol โ†’ warning linking newest LTS", () => { + expect(P.bannerFor("1.3", manifest)).toMatchObject({ kind: "eol", newest: "1.5" }); + }); +}); + +describe("targetUrlFor", () => { + it("same path on target version", () => { + expect(P.targetUrlFor({ lang: "en", slug: "1.5", rest: "cli/index.html" }, "1.4")) + .toBe("/en/1.4/cli/index.html"); + }); +}); + +describe("navUrlFor (query + fragment preserved across version switch)", () => { + const loc = { lang: "en", slug: "1.4", rest: "quick-start.html" }; + it("neither โ†’ bare target path", () => { + expect(P.navUrlFor(loc, "1.5", "", "")).toBe("/en/1.5/quick-start.html"); + }); + it("query-only", () => { + expect(P.navUrlFor(loc, "1.5", "?ref=x", "")) + .toBe("/en/1.5/quick-start.html?ref=x"); + }); + it("hash-only", () => { + expect(P.navUrlFor(loc, "1.5", "", "#section-3")) + .toBe("/en/1.5/quick-start.html#section-3"); + }); + it("both, in query-then-hash order", () => { + expect(P.navUrlFor(loc, "1.5", "?ref=x", "#section-3")) + .toBe("/en/1.5/quick-start.html?ref=x#section-3"); + }); +}); -- cgit v1.2.3