diff options
| author | Yuriy Andamasov <yuriy@vyos.io> | 2026-09-25 17:11:19 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-25 16:11:19 +0100 |
| commit | a783e56b774795c1f4bd8a6088dfcce96a307c5f (patch) | |
| tree | dcd0c88ff02969351840c8222ea97c72d9258e18 /scripts/docs_gates/gates.py | |
| parent | 4c8d1d1a0f23e2106a97f4b672607121ff03a091 (diff) | |
| download | vyos-documentation-a783e56b774795c1f4bd8a6088dfcce96a307c5f.tar.gz vyos-documentation-a783e56b774795c1f4bd8a6088dfcce96a307c5f.zip | |
ci: port the Cloudflare Workers docs pipeline to circinus (slug 1.5) (#2208)
* 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 -- <paths>`, 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)
Diffstat (limited to 'scripts/docs_gates/gates.py')
| -rw-r--r-- | scripts/docs_gates/gates.py | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/scripts/docs_gates/gates.py b/scripts/docs_gates/gates.py new file mode 100644 index 00000000..7b5cbf1d --- /dev/null +++ b/scripts/docs_gates/gates.py @@ -0,0 +1,96 @@ +"""Deploy-blocking sanity gates (spec ยง7.1). + +Gates: file-count vs plan cap (<= 80% of 100k), per-file < 25 MiB, index.html + +critical-page presence, page-count delta vs previous deploy, canonical URLs must +start with the https://docs.vyos.io/en/<slug>/ prefix, declared PDF present, +Pagefind non-empty. +Exit 0 = deployable; exit 1 = blocked (one line per failed gate on stderr). +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +FILE_CAP = 100_000 +CAP_FRACTION = 0.8 +MAX_FILE = 25 * 1024 * 1024 +COUNT_DELTA_MIN_RATIO = 0.5 # new build must have >= 50% of previous page count +CANONICAL_RE = re.compile(r'<link\s+rel="canonical"\s+href="([^"]+)"') + + +def _fail(msgs: list[str], msg: str) -> None: + msgs.append(msg) + print(f"GATE-FAIL: {msg}", file=sys.stderr) + + +def run(artifact: Path, slug: str, versions: Path, previous_meta: Path | None, + critical: list[str]) -> int: + fails: list[str] = [] + root = artifact / "en" / slug + manifest = json.loads(versions.read_text()) + entry = next((v for v in manifest["versions"] if v["slug"] == slug), None) + if entry is None: + _fail(fails, f"slug {slug} not in versions.json") + return 1 + + files = [p for p in artifact.rglob("*") if p.is_file()] + if len(files) > FILE_CAP * CAP_FRACTION: + _fail(fails, f"file count {len(files)} > 80% of {FILE_CAP} cap") + for p in files: + if p.stat().st_size > MAX_FILE: + _fail(fails, f"{p.relative_to(artifact)} exceeds 25 MiB") + + for rel in ["index.html", *critical]: + if not (root / rel).is_file(): + _fail(fails, f"critical page missing: en/{slug}/{rel}") + + pagefind = root / "pagefind" + if not pagefind.is_dir() or not any(pagefind.iterdir()): + _fail(fails, "Pagefind index missing or empty") + + if entry.get("pdf"): + expected = artifact / entry["pdf"].lstrip("/") + if not expected.is_file(): + _fail(fails, f"declared PDF missing: {entry['pdf']}") + + pages = [p for p in root.rglob("*.html")] + if previous_meta is not None and previous_meta.is_file(): + prev = json.loads(previous_meta.read_text()) + if prev.get("page_count") and len(pages) < prev["page_count"] * COUNT_DELTA_MIN_RATIO: + _fail(fails, f"page count collapsed: {len(pages)} vs previous {prev['page_count']}") + + want = f"https://docs.vyos.io/en/{slug}/" + for p in pages: + m = CANONICAL_RE.search(p.read_text(errors="ignore")) + if m is None: + _fail(fails, f"missing canonical link in en/{slug}/{p.relative_to(root)}") + break # one example is enough to block + if not m.group(1).startswith(want): + _fail(fails, f"bad canonical in en/{slug}/{p.relative_to(root)}: {m.group(1)}") + break # one example is enough to block + + return 1 if fails else 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--artifact", type=Path, required=True) + ap.add_argument("--slug", required=True) + ap.add_argument("--versions", type=Path, required=True) + ap.add_argument("--previous-meta", type=Path, default=None) + ap.add_argument("--critical-list", type=Path, + default=Path("scripts/docs_gates/critical-pages.txt")) + a = ap.parse_args() + # Strip BEFORE the comment test: an indented " # note" line is a comment, not a page + # every deployable build must contain โ the unstripped test turned it into a live entry, + # and a comment can never exist as a file, so it would block the deploy as a missing page. + lines = (line.strip() for line in a.critical_list.read_text().splitlines()) + critical = [line for line in lines if line and not line.startswith("#")] + return run(a.artifact, a.slug, a.versions, a.previous_meta, critical) + + +if __name__ == "__main__": + raise SystemExit(main()) |
