diff options
| author | Yuriy Andamasov <yuriy@vyos.io> | 2026-07-10 17:14:13 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-10 16:14:13 +0200 |
| commit | 21689ef59b0eb34b1a29eda739dc10d33d25b44a (patch) | |
| tree | 95883c2ee70d4c09562ae3e0289e18f34999240c /docs/_static | |
| parent | 6d2d28d4411ff52ad0f9314cf3696336748ab89d (diff) | |
| download | vyos-documentation-21689ef59b0eb34b1a29eda739dc10d33d25b44a.tar.gz vyos-documentation-21689ef59b0eb34b1a29eda739dc10d33d25b44a.zip | |
docs: Cloudflare Workers hosting pipeline (apex, content workers, CI, previews) (#2140)
* 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)
Diffstat (limited to 'docs/_static')
| -rw-r--r-- | docs/_static/css/version-picker.css | 11 | ||||
| -rw-r--r-- | docs/_static/js/pagefind-wrapper.js | 69 | ||||
| -rw-r--r-- | docs/_static/js/version-picker.js | 153 |
3 files changed, 233 insertions, 0 deletions
diff --git a/docs/_static/css/version-picker.css b/docs/_static/css/version-picker.css new file mode 100644 index 00000000..16ce16c3 --- /dev/null +++ b/docs/_static/css/version-picker.css @@ -0,0 +1,11 @@ +.vyos-version-picker-item { list-style: none; } +#vyos-version-picker { display: flex; align-items: center; gap: .5em; padding: .4em 0; font-size: .9em; } +#vyos-version-picker select { max-width: 14em; padding: .15em .3em; } +#vyos-version-picker select:focus { outline: 2px solid #ffae12; outline-offset: 1px; } +.vyos-pdf-link { font-weight: 600; } +.vyos-version-banner { position: relative; padding: .6em 2.2em .6em 1em; font-size: .95em; line-height: 1.4; } +.vyos-banner-dev { background: #e7f2fa; color: #2a6496; } +.vyos-banner-newer-lts { background: #fff6e5; color: #6b5900; } +.vyos-banner-eol { background: #fdecea; color: #8a1f11; } +.vyos-version-banner a { text-decoration: underline; color: inherit; font-weight: 600; } +.vyos-banner-dismiss { position: absolute; right: .5em; top: .35em; background: none; border: 0; font-size: 1.2em; cursor: pointer; color: inherit; } diff --git a/docs/_static/js/pagefind-wrapper.js b/docs/_static/js/pagefind-wrapper.js new file mode 100644 index 00000000..a4acadf8 --- /dev/null +++ b/docs/_static/js/pagefind-wrapper.js @@ -0,0 +1,69 @@ +/* Version-scoped Pagefind loader. Derives its base from the current URL so the + * same file works on production, canary, and /pr-<n>/ previews (ยง9). */ +(function (window) { + 'use strict'; + + function basePathFor(pathname) { + var m = pathname.match(/^((\/pr-\d+)?\/[a-z]{2}(?:_[A-Z]{2})?\/[^/]+\/)/); + if (!m) return null; + return { base: m[1], prefix: m[2] || '' }; + } + + function prefixResultUrl(url, prefix) { + return prefix ? prefix + url : url; + } + + function assetUrlsFor(base) { + return { + css: base + 'pagefind/pagefind-ui.css', + js: base + 'pagefind/pagefind-ui.js', + }; + } + + function init() { + var mount = document.getElementById('vyos-search'); + if (!mount) return; + var ctx = basePathFor(window.location.pathname); + if (!ctx) return; + var assets = assetUrlsFor(ctx.base); + /* Stylesheet first so the UI never renders unstyled. CSS load failure is + * cosmetic only โ no error handler on the link. */ + var l = document.createElement('link'); + l.rel = 'stylesheet'; + l.href = assets.css; + document.head.appendChild(l); + var s = document.createElement('script'); + s.src = assets.js; + s.onerror = function () { + /* Per-version Pagefind assets missing (index not built yet, preview + * namespace, transient failure) โ show a visible notice instead of + * leaving #vyos-search a silent empty div. */ + var p = document.createElement('p'); + p.className = 'vyos-search-unavailable'; + p.textContent = 'Search is temporarily unavailable for this version.'; + mount.appendChild(p); + }; + s.onload = function () { + /* global PagefindUI */ + new window.PagefindUI({ + element: '#vyos-search', + baseUrl: ctx.base, + bundlePath: ctx.base + 'pagefind/', + processResult: function (result) { + result.url = prefixResultUrl(result.url, ctx.prefix); + return result; + }, + }); + }; + document.head.appendChild(s); + } + + window.VyOSSearch = { + basePathFor: basePathFor, + prefixResultUrl: prefixResultUrl, + assetUrlsFor: assetUrlsFor, + init: init, + }; + if (typeof document !== 'undefined' && document.addEventListener) + document.addEventListener('DOMContentLoaded', init); +})(window); diff --git a/docs/_static/js/version-picker.js b/docs/_static/js/version-picker.js new file mode 100644 index 00000000..f13ad60f --- /dev/null +++ b/docs/_static/js/version-picker.js @@ -0,0 +1,153 @@ +/* VyOS docs version picker + status banner + language scaffold. + * Vanilla JS, no dependencies, no build step. Degrades silently when + * /versions.json is unreachable (docs stay fully readable). */ +(function (window) { + 'use strict'; + + // Deliberately does not match /pr-<n>/ preview prefixes โ previews are single-version, + // so the picker has nothing to switch between and stays hidden there by design. + function parseLocation(pathname) { + var m = pathname.match(/^\/([a-z]{2}(?:_[A-Z]{2})?)\/([^/]+)\/(.*)$/); + if (!m) return null; + return { lang: m[1], slug: m[2], rest: m[3] }; + } + + // Contract: versions.json lists versions newest-first, so the first 'lts' entry found + // here is the newest LTS โ callers rely on that ordering rather than comparing versions. + function newestLts(manifest) { + for (var i = 0; i < manifest.versions.length; i++) + if (manifest.versions[i].status === 'lts') return manifest.versions[i].slug; + return null; + } + + function bannerFor(slug, manifest) { + var entry = null, i; + for (i = 0; i < manifest.versions.length; i++) + if (manifest.versions[i].slug === slug) entry = manifest.versions[i]; + if (!entry) return null; + var newest = newestLts(manifest); + if (entry.status === 'dev') return { kind: 'dev', newest: newest }; + if (entry.status === 'eol') return { kind: 'eol', newest: newest }; + if (entry.status === 'lts' && newest && newest !== slug) + return { kind: 'newer-lts', newest: newest }; + return null; + } + + function targetUrlFor(loc, targetSlug) { + return '/' + loc.lang + '/' + targetSlug + '/' + loc.rest; + } + + /* Full navigation URL for a version switch: same path on the target version + * with the current query string + fragment re-attached, so deep links + * (?highlight=โฆ, #section) survive the switch (ยง4 URL-stability contract). */ + function navUrlFor(loc, targetSlug, search, hash) { + return targetUrlFor(loc, targetSlug) + (search || '') + (hash || ''); + } + + /* ---- DOM layer (no execution at import time) ---- */ + function bannerText(b, manifest) { + if (b.kind === 'dev') return 'You are reading the development (rolling) docs.'; + if (b.kind === 'eol') return 'This VyOS version is end-of-life; these docs are frozen. See the ' + b.newest + ' (LTS) docs.'; + return 'A newer LTS (' + b.newest + ') is available.'; + } + + function init() { + var anchor = document.getElementById('vyos-version-picker'); + if (!anchor) return; + var loc = parseLocation(window.location.pathname); + if (!loc) return; + + fetch('/versions.json', { headers: { accept: 'application/json' } }) + .then(function (r) { if (!r.ok) throw new Error('versions.json ' + r.status); return r.json(); }) + .then(function (manifest) { + renderPicker(anchor, loc, manifest); + renderLang(anchor, loc, manifest); + renderBanner(loc, manifest); + }) + .catch(function () { /* silent degradation (ยง4) */ }); + } + + function renderPicker(anchor, loc, manifest) { + var label = document.createElement('label'); + label.setAttribute('for', 'vyos-version-select'); + label.textContent = 'Version: '; + var sel = document.createElement('select'); + sel.id = 'vyos-version-select'; + manifest.versions.forEach(function (v) { + var o = document.createElement('option'); + o.value = v.slug; o.textContent = v.label; o.selected = v.slug === loc.slug; + sel.appendChild(o); + }); + sel.addEventListener('change', function () { + var search = window.location.search, hash = window.location.hash; + var target = navUrlFor(loc, sel.value, search, hash); + var fallback = '/' + loc.lang + '/' + sel.value + '/' + (search || '') + (hash || ''); + fetch(targetUrlFor(loc, sel.value), { method: 'HEAD' }) + .then(function (r) { + window.location.href = (r.status === 404) ? fallback : target; + }) + .catch(function () { window.location.href = fallback; }); + }); + anchor.appendChild(label); + anchor.appendChild(sel); + + var entry = manifest.versions.filter(function (v) { return v.slug === loc.slug; })[0]; + if (entry && entry.pdf) { + var a = document.createElement('a'); + a.href = entry.pdf; a.className = 'vyos-pdf-link'; a.textContent = 'PDF'; + anchor.appendChild(a); + } + } + + function renderLang(anchor, loc, manifest) { + if (!manifest.languages || manifest.languages.length <= 1) return; // scaffold: hidden while en-only (ยง4) + var sel = document.createElement('select'); + sel.id = 'vyos-lang-select'; + sel.setAttribute('aria-label', 'Language'); + manifest.languages.forEach(function (l) { + var o = document.createElement('option'); + o.value = l.code; o.textContent = l.label; o.selected = l.code === loc.lang; + sel.appendChild(o); + }); + sel.addEventListener('change', function () { + window.location.href = '/' + sel.value + '/' + loc.slug + '/' + loc.rest + + window.location.search + window.location.hash; + }); + anchor.appendChild(sel); + } + + function renderBanner(loc, manifest) { + var b = bannerFor(loc.slug, manifest); + if (!b) return; + var key = 'vyos-banner-dismissed-' + loc.slug; + try { if (window.localStorage.getItem(key)) return; } catch (e) { /* private mode */ } + var div = document.createElement('div'); + div.className = 'vyos-version-banner vyos-banner-' + b.kind; + div.setAttribute('role', b.kind === 'eol' ? 'alert' : 'note'); + var span = document.createElement('span'); + span.textContent = bannerText(b, manifest); + div.appendChild(span); + if (b.newest && b.kind !== 'dev') { + var link = document.createElement('a'); + link.href = '/' + loc.lang + '/' + b.newest + '/'; + link.textContent = ' Switch to ' + b.newest + '.'; + div.appendChild(link); + } + var x = document.createElement('button'); + x.textContent = 'ร'; x.className = 'vyos-banner-dismiss'; + x.setAttribute('aria-label', 'Dismiss'); + x.addEventListener('click', function () { + try { window.localStorage.setItem(key, '1'); } catch (e) { /* ignore */ } + div.remove(); + }); + div.appendChild(x); + document.body.insertBefore(div, document.body.firstChild); + } + + window.VyOSVersionPicker = { + parseLocation: parseLocation, bannerFor: bannerFor, + targetUrlFor: targetUrlFor, navUrlFor: navUrlFor, init: init, + }; + if (typeof document !== 'undefined' && document.addEventListener) + document.addEventListener('DOMContentLoaded', init); +})(window); |
