From a783e56b774795c1f4bd8a6088dfcce96a307c5f Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 25 Sep 2026 17:11:19 +0200 Subject: ci: port the Cloudflare Workers docs pipeline to circinus (slug 1.5) (#2208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: port Cloudflare Workers docs pipeline files to circinus (verbatim) Copies the branch-agnostic half of the docs.vyos.io Cloudflare Workers pipeline from `rolling` at 8cb568bf, byte-identical: - .github/workflows/docs-build.yml - workers/ (entire tree) - scripts/docs_gates/ - docker/im-convert.sh - docs/_static/js/version-picker.js, js/pagefind-wrapper.js, css/version-picker.css (new files, no circinus counterpart) - docs/_templates/breadcrumbs.html, searchbox.html (new files) docs-build.yml already triggers on push to [rolling, circinus, sagitta] and resolves `circinus` -> worker vyos-docs-v15-en / slug 1.5 from workers/matrix.json; the files simply did not exist on this branch, so slug 1.5 still serves the bootstrap placeholder. workers/versions.json + workers/matrix.json are deliberately identical across all three branches and must be kept in sync. Advances: IS-572 * ci: wire circinus docs build into the Cloudflare Workers pipeline Hand-merges the CF-specific hunks onto circinus's own docker/Dockerfile and docs/conf.py rather than clobbering them with rolling's versions — circinus keeps its own content-driven history in both files. docker/Dockerfile: - imagemagick + librsvg2-bin (sphinx.ext.imgconverter backend) and poppler-utils (pdfinfo, used by docs-build.yml's PDF page-count completeness check), installed --no-install-recommends - install docker/im-convert.sh as /usr/local/bin/im-convert docs/conf.py: - enable sphinx.ext.imgconverter + image_converter = 'im-convert' so the LaTeX/PDF builder stops silently dropping .webp/.svg images - register js/version-picker.js + css/version-picker.css unconditionally (degrades silently on ReadTheDocs) - _vyos_cf_build gate off the raw DOCS_VERSION_SLUG env var; only CF builds load js/pagefind-wrapper.js, and html_context['vyos_cf_build'] lets _templates/searchbox.html fall back to the stock Sphinx searchbox via the "!" bang-include on RTD The RTD path stays the default in both files: circinus continues building on ReadTheDocs until RTD sunset, and every CF feature activates only when DOCS_VERSION_SLUG is present. .readthedocs.yml is untouched. circinus keeps its own version/release/html_title/source_suffix and its hardcoded html_baseurl (its CF slug is also `1.5`, so rolling's DOCS_VERSION_SLUG/READTHEDOCS_VERSION resolution block is a no-op here and was deliberately not ported). Advances: IS-572 * ci: IS-572: re-sync ported Cloudflare Workers pipeline files with rolling The category-1 files in this port are byte-identical copies from `rolling`. `rolling` has since moved: [vyos-documentation#2209](https://github.com/vyos/vyos-documentation/pull/2209) merged as `3a1c6c30`, thirteen rounds of hardening on exactly these files. Re-take all 14 category-1 paths from `origin/rolling` via `git checkout origin/rolling -- `, so byte-identity holds by construction rather than by hand-editing: .github/workflows/docs-build.yml scripts/docs_gates/{gates,parity,smoke,test_gates,test_parity,test_smoke}.py workers/.gitignore workers/apex/src/{index,special,uagate}.ts workers/apex/test/{router,uagate}.test.ts workers/apex/ua-policy.json Thirteen of the fourteen carry [vyos-documentation#2209](https://github.com/vyos/vyos-documentation/pull/2209) exactly — the pre-change tree was byte-identical to `3a1c6c30^` for those paths. `workers/.gitignore` additionally picks up the one-line `test-results/` entry from [vyos-documentation#2212](https://github.com/vyos/vyos-documentation/pull/2212); inert on circinus, since only the deliberately-unported `apex-deploy.yml` writes that directory. Deliberate exclusions are unchanged: `docs-canary-qa.yml` (cron runs on the default branch only, so it is not ported even though [vyos-documentation#2209](https://github.com/vyos/vyos-documentation/pull/2209) touched it on `rolling`), `apex-deploy.yml`, and the `docs-preview-*` workflows. `docs/conf.py` stays hand-merged and circinus-specific, with its ReadTheDocs fallback intact. 🤖 Generated by [robots](https://vyos.io) --- workers/branch/src/index.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 workers/branch/src/index.ts (limited to 'workers/branch/src') diff --git a/workers/branch/src/index.ts b/workers/branch/src/index.ts new file mode 100644 index 00000000..47ef3ad6 --- /dev/null +++ b/workers/branch/src/index.ts @@ -0,0 +1,83 @@ +export interface Env { + ASSETS: Fetcher; // static assets binding + DOCS_BUILD_SHA: string; // injected at deploy + DOCS_ENV: "production" | "canary"; +} + +export type CacheClass = "page" | "asset"; + +// Binary/media asset extensions (case-insensitive, so ".PDF" also matches) get the longer +// asset cache class, alongside the Sphinx /_static/ (theme) + /_images/ (figure) trees. +const ASSET_EXT_RE = /\.(pdf|png|jpe?g|webp|svg|gif|ico|woff2?|ttf|otf|eot)$/i; + +export function classifyPath(path: string): CacheClass { + if ( + path.includes("/_static/") || + path.includes("/_images/") || + ASSET_EXT_RE.test(path) + ) + return "asset"; + return "page"; // HTML, versions.json, sitemaps, robots/llms, pagefind index +} + +export function cacheHeaderFor(cls: CacheClass): string { + return cls === "asset" + ? "public, max-age=300, s-maxage=600, must-revalidate" + : "public, max-age=0, s-maxage=300, must-revalidate"; +} + +export function withDocsHeaders( + resp: Response, + path: string, + env: Pick, +): Response { + const out = new Response(resp.body, resp); + out.headers.set("X-Docs-Build", env.DOCS_BUILD_SHA); + out.headers.set( + "Cache-Control", + // Error responses (4xx/5xx) must never carry the page/asset cache class — a + // cached 404 would poison the edge for the full s-maxage window. + env.DOCS_ENV === "canary" || out.status >= 400 + ? "no-store" + : cacheHeaderFor(classifyPath(path)), + ); + return out; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + // With assets html_handling "none", the runtime serves explicit .html URLs directly + // but does NOT map a directory URL ("/foo/") to its index.html — the worker must map + // trailing-slash URLs to index.html itself to preserve ReadTheDocs URL parity. + if (url.pathname.endsWith("/")) { + const mapped = new URL(url); + mapped.pathname = url.pathname + "index.html"; + const resp = await env.ASSETS.fetch(new Request(mapped, request)); + return withDocsHeaders(resp, url.pathname, env); + } + // Bare extensionless path (no trailing slash, no "." in the last segment): it may be a + // real directory whose slashed form RTD 301-redirects to ("/foo" → "/foo/"), or a + // file-like path with no matching asset (e.g. "/cli", whose real asset is "cli.html") + // that RTD 404s. A dot heuristic can't separate them, so probe the assets binding for + // "/index.html": 200 → 301 to the slashed form; anything else → fall through to + // the exact-path fetch (404 for "/cli", matching live RTD). + const lastSegment = url.pathname.slice(url.pathname.lastIndexOf("/") + 1); + if (lastSegment !== "" && !lastSegment.includes(".")) { + const probe = new URL(url); + probe.pathname = url.pathname + "/index.html"; + const probeResp = await env.ASSETS.fetch(new Request(probe, { method: "GET" })); + probeResp.body?.cancel(); // existence check only — release the probe body stream + if (probeResp.status === 200) { + const location = url.pathname + "/" + url.search; // preserve query; no fragment + return withDocsHeaders( + new Response(null, { status: 301, headers: { Location: location } }), + url.pathname, + env, + ); + } + } + const resp = await env.ASSETS.fetch(request); + return withDocsHeaders(resp, url.pathname, env); + }, +} satisfies ExportedHandler; -- cgit v1.2.3