summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-08-24docs: T9145: document WLB health script variablesJeleel Muibi
2026-08-21fix: IS-572: correct verified defects in the Cloudflare Workers docs ↵Yuriy Andamasov
pipeline (#2209) * fix: IS-572: correct verified defects in the Cloudflare Workers docs pipeline Fixes raised against the LTS port PR but landing on rolling first, so the backport runs in the right direction, plus one defect found while verifying the nightly canary sweep. - apex PDF: a 206 is now only answered to a request that actually carried a Range header, and every R2Range shape is normalized to concrete bounds. R2 reports a whole-object range on un-ranged gets, so keying the 206 off obj.range alone made every plain GET of the 1.3 PDF a 206. - apex PDF: a failed If-Match now maps to 412, not 304. - apex sitemap index: entries derive their origin from the request, so the canary index no longer points at production. - UA policy: drop the dead Google-Extended entry (a robots.txt token, never a UA) and log Applebot-Extended while still allowing Applebot, via a most-specific-match precedence between the allow and log lists. - docs-build: replace the check_head guard's exit 78 (neutral only in the retired Actions v1 runtime) with an output gating every deploy/promote step, so a superseded run ends green-and-skipped instead of red. - smoke/parity: read CF Access service-token credentials from the environment instead of argv, and pass them via env: in both workflows. - smoke: stop asserting X-Docs-Build on the PDF probe — apex's R2 fallback path legitimately never sets it, making the probe unpassable for 1.3. - gates/smoke: strip critical-pages lines before testing for comments. - parity: fetch each sitemap once, honouring the _SCHEME override. Advances: IS-572 * fix: IS-572: harden check_head guard against a failed ls-remote Phase-0 CodeRabbit findings on the new guard, both valid: - set -euo pipefail + an explicit emptiness check, so a failed git ls-remote cannot leave $remote empty and be misread as 'branch moved' (cut exits 0 on empty input) — that would silently skip a deploy that should have run. - pass github.ref_name and github.sha through env vars rather than interpolating them into the shell command. Advances: IS-572 * fix: IS-572: address adversarial + CodeRabbit review on the CF docs gates Answers the seven adversarial-review findings on vyos/vyos-documentation#2209, five of which GitHub CodeRabbit independently reproduced on review 4946100889. Range handling (apex Worker) — the reported defect was real but mis-diagnosed. Probing a real R2 binding under vitest-pool-workers shows R2 never returns the inverted/zero-length range the review predicted: for the Headers form this Worker uses, R2 answers every unsatisfiable, malformed, multi-range and unknown-unit Range by IGNORING it and returning the complete object with range={offset:0,length:size} — indistinguishable from a satisfied whole-object range, and it does not throw. (Only the object-literal form throws, code 10039, which this Worker cannot reach.) Trusting obj.range therefore answered `Range: bytes=99-` with 206 + `Content-Range: bytes 0-8/9` and the whole body, so a client resuming mid-download would silently corrupt its file. Range intent is now re-derived from the client's own header per RFC 9110 §14.1.2: unsatisfiable → 416 + `Content-Range: bytes */size`, ignored → 200 with the complete body, satisfiable → 206 as before. resolveRange() additionally clamps its length at zero so its documented contract holds for every R2Range input. Preconditions: reorder to RFC 9110 §13.2.2's MUST-ordered precedence (If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since) so `If-Match: "old"` + `If-None-Match: "new"` yields 412 rather than 304, and gate 304 on GET/HEAD per §13.1.2 so a conditional POST cannot be answered 304. UA gate: a log match now wins a contest against an allow match unless the allow entry strictly contains the log entry, so `GPTBot/1.0 DuckDuckBot` emits its ua-log event instead of being swallowed by the longer allow needle. Applebot / Applebot-Extended verdicts are unchanged and now pinned by tests. parity.py: restore the exact-200 sitemap check (_OPENER returns for any 2xx, so a 204 yielded an empty corpus and the gate passed having probed nothing) and route the sitemap request through a single credential-attaching constructor, so --sitemap-host against the Access-gated canary no longer 403s on every sitemap. Secrets: --access-id/--access-secret removed from both gates in favour of the paired env vars. An argv value is readable from the process table and captured by `set -x`; no call site used the flags. smoke.py reads --critical-list via Path.read_text() rather than leaking a file handle. Tests: 115 -> 138 worker tests, 46 -> 50 gate tests. Every new behavioural test was verified to fail against the pre-fix source. Advances: IS-572 * test: IS-572: compare the forbidden UA token case-insensitively bestMatch() lowercases every ua-policy.json entry before matching, so a "google-extended" entry would be functionally identical to the "Google-Extended" token this guard exists to keep out. toContain() compares primitives by strict equality, so the lowercase variant passed the assertion and would have quietly restored the entry. Lowercase both sides so the assertion matches the matcher's own case semantics. Verified by injecting a lowercase "google-extended" into the log list: the test now fails, and passed before this change. Advances: IS-572 * fix: IS-572: scope the CF Access token per host; correct range + precondition semantics Round-3 adversarial review findings. Each fix has a test that fails against 5b4f40f9. parity.py sent the CF Access service token to every host the run touched, which pre-cutover includes --sitemap-host docs.vyos.io — still served by ReadTheDocs. Credentials are now modelled as an Access record bound to --probe-host, and build_request() attaches them per destination rather than per run, so the token reaches only the host it belongs to while the post-cutover same-host configuration stays credentialed. The apex Worker's range handling assumed its own regex agreed with R2's parser. It does not: R2 accepts ASCII space only, this one accepts \s, so a tab-separated Range parsed here, was ignored there, and produced a 206 carrying the whole object in answer to a request for three bytes. classifyRangeHeader now returns the concrete bounds it derived and the Worker checks them against the bytes R2 actually returned before promising a 206 — any parser divergence degrades to a 200 instead of corrupting a resumed download. Range positions are compared as digit strings, so specs above 2^53 no longer collapse into each other, and Range is ignored outright on every method but GET (§14.2). If-Range was silently dropped: R2Conditional carries no such validator, so a stale range request was answered with bytes of the new representation under a 206. It is now evaluated against the object's own ETag, with a failed validator serving the complete representation. Preconditions were inferred from which headers were present, which cannot be right in both directions — If-Match satisfied plus If-None-Match satisfied owed a 304 and got a 412. The conditionals are filtered to those §13.2.2 applies to this request before R2 sees them, and a body-less result is resolved by evaluating the validators in §13.2.2 order. Also drops the UA gate's narrow-allow carve-out. It compared the two matched policy entries rather than the UA's token structure, so a request-controlled UA quoting both tokens bought itself an allow. No pair in ua-policy.json used it. 🤖 Generated by [robots](https://vyos.io) * fix: IS-572: cancel abandoned R2 body streams; drop stale --access-* comments CodeRabbit round-3 follow-up. R2 hands back a body on two paths whose response carries none, and dropping the reference leaves the stream open holding its connection until GC. CodeRabbit flagged the If-Range re-read; the larger instance is the 416, where R2 answers an unsatisfiable Range with the COMPLETE object — 29.2 MiB for the 1.3 PDF — and the response sends none of it. Both now go through discardBody(), which cancels so the transfer is aborted rather than drained, and swallows failures because the response on those paths is already decided. Keeping the get-then-re-read rather than switching to head()-then-get(): a head() would avoid opening the slice stream, but it would add a round-trip to the path where If-Range MATCHES — the normal resumed download — to tidy the rare path where it does not, and it would open a TOCTOU window that the current shape does not have, since here the object whose validator was checked is the very response served. The R2 test mock returned string bodies, which is why this class of bug was invisible to it. Bodies are now real ReadableStreams that record their own cancellation, so the three new tests observe the actual stream lifecycle: the 416 cancels the whole object, the stale If-Range cancels the discarded slice, and a served body is never cancelled. Also corrects two comments that still described --access-id/--access-secret as a manual fallback; the flags were removed in round 2 and argparse rejects them. A repo-wide sweep found no other prose describing the removed interface. The docs-build.yml change is one comment line inside an env: block; check_head is untouched. 🤖 Generated by [robots](https://vyos.io) * fix: IS-572: compare Access origins; ignore conditionals per method Round-4 adversarial review findings. parity.py: _authority() compared (hostname, port) verbatim, so `p.invalid`, `p.invalid:443` and the trailing-dot FQDN form were three different origins. Post-cutover both --sitemap-host and --probe-host name the same Access-gated host; spelling the default port out on either cost the sitemap fetch its token and 403'd it, reverting the same-host keeper case. Compare normalized origins instead: lowercased host, single trailing dot stripped, the scheme's default port folded to None (bare arguments read under _SCHEME). IDN/punycode equivalence stays out of scope -- both hosts are ASCII literals passed by CI. apex worker: applicablePreconditions() filtered conditionals by validator applicability but not by method, so RFC 9110 13.2.1 -- "a server MUST ignore the conditional request header fields ... such as CONNECT, OPTIONS, or TRACE" -- was unmet: an OPTIONS carrying a stale If-Match was refused 412 where the same request without it succeeded. Take the method and return an empty set for those three; R2 is then given nothing to evaluate. apex worker: the stale-If-Range re-read was a bare get(), dropping the other preconditions, so a key rewritten between the two reads was served under a 200 though the request's If-Match excluded it. The legacy snapshot deploy's force_pdf_refresh input does rewrite this key. Carry the same onlyIf into the re-read and resolve a body-less result through preconditionStatus() on the new object's validators. Advances: IS-572 * fix: IS-572: scope the Access token by origin, not authority; hide the secret from repr Round-5 CodeRabbit findings, both on the CF Access credential primitive in scripts/docs_gates/parity.py. _authority() returned (hostname, port) and dropped the scheme, so an https credential also applied to the plaintext http URL of the same name -- Access("p.invalid", ...).applies_to("http://p.invalid/") was True, and build_request() would then have attached CF-Access-Client-Id/-Secret to a cleartext request. `http://p.invalid:80/` is the sharp case: 80 folds to None under http, matching the https-scoped ("p.invalid", None) exactly. Not reachable in current callers: every URL in the module is built from _SCHEME and both workflow invocations pass bare hosts, so a single run is single-scheme and the http value exists only for the test monkeypatch. Fixed as a contract defect regardless -- applies_to() documents itself as comparing ORIGINs, an origin is scheme+host+port, and over-broad credential scope is the failure mode this primitive already produced once (round 2, token sent to docs.vyos.io). Return (scheme, host, port), resolving a bare argument's scheme under _SCHEME as before, and fold the default port against the origin's own scheme so http :80 and https :443 stay distinct rather than collapsing together. The four origin-normalization cases and the :8443 denial are unchanged. Access.client_secret is now dataclasses.field(repr=False). The default dataclass repr rendered every field, so a failed assertion, a debug print or any exception interpolating an Access would have put the service token into CI output, which is durable and world-readable for this repo. The client id stays in repr: it names which token without being the credential, and dropping it would make a scoping failure much harder to diagnose. The secret remains readable as a field, which is the only way it is ever consumed. Swept the rest of scripts/docs_gates/ for the same exposure: smoke.Probe holds no credential fields, smoke passes the id/secret as plain locals that reach no output path, and the str(e) texts recorded in parity-report.json and the SMOKE-FAIL lines carry urllib's code/reason/URL, never request headers. Two tests added, both verified failing against the pre-fix source. Advances: IS-572 🤖 Generated by [robots](https://vyos.io) * fix: IS-572: roll back to the newest deployment's 100%-traffic version id `wrangler deployments list --json` sorts ASCENDING by created_on (wrangler 4.123.0 versionsDeploymentsListHandler), so `.[0]` selected the OLDEST deployment, not the current one the stale comment claimed. It also took the deployment `id`, but `wrangler rollback` resolves its positional through fetchVersion() and needs a VERSION id. Select newest-first by created_on and take the version_id serving 100% of traffic, walking to the next-newest deployment when traffic is split across versions -- mirroring wrangler's own fetchDefaultRollbackVersionId(), minus its .shift() (it picks after deploying; we capture before). Non-array or unparseable output now fails safe to an empty id instead of a wrong one. Rewrite the stale comment to describe the actual JSON shape, note in the consumer that rollback_id is a version id, and add --message for rollback audit context (supported at 4.123.0; --yes supplies it non-interactively). Advances: IS-572 🤖 Generated by [robots](https://vyos.io) * fix: IS-572: re-check the branch tip after smoke; harden rollback capture Two production-safety fixes in docs-build.yml. 1. TOCTOU on the check_head guard. It resolved the branch tip ONCE, before the candidate deploy and before the smoke gate (480s deadline), then gated PROMOTE, the post-promote probe and the registry pointer publish on that minutes-old answer. A push landing in that window left the run promoting a superseded SHA to production and publishing a pointer naming it. `cancel-in-progress: false` does not cover this: it makes the SECOND run queue and skip, while the run already past the check sails on. Add a second guard after smoke, immediately before the first production-facing step, and move PROMOTE + probe + pointer publish onto its output. The probe must move with PROMOTE: left on the first check it would run after a skipped promote, see production serving the previous SHA, read that as a promote failure and roll back a production nobody touched. A branch that moved mid-run ends GREEN-and-skipped, matching the convention the exit-78 redesign established: production and the registry are untouched, and the queued run promotes the new tip, so there is nothing for an operator to act on. The residual check-to-deploy window is one step boundary; no compare-and-swap exists on the Cloudflare side to close it entirely. 2. The rollback-capture bootstrap branch swallowed every failure. `2>/dev/null` plus a bare `if` could not tell "Worker does not exist yet" from auth, network or malformed output, so a transient failure yielded an empty rollback_id, logged "nothing to roll back to" and promoted with the auto-rollback silently disarmed while the run looked healthy. Only a genuine not-found may now produce an empty id. wrangler 4.123.0 exits 1 on every error path and writes nothing machine-readable to stdout when the command fails, so the discriminator is the API error code it renders into stderr: 10007/10090, the pair its own isWorkerNotFoundError() uses. Auth carries 9106/10000 and connectivity failures carry no code, so neither can be mistaken for bootstrap. Anything unmatched fails the step. Non-array output now fails instead of being coerced to []; an empty list and a split-traffic list are reported distinctly (notice / warning) rather than sharing one ambiguous message with the failure paths. The code pair was read out of the shipped 4.123.0 bundle, not observed against a live missing Worker; the comment says so and asks for re-verification on wrangler bumps. Also corrects two comments the change falsifies: the concurrency note claiming queued runs are covered by "the check_head guard", and the exit-78 note claiming promote/registry gate on the first check. Advances: IS-572 🤖 Generated by [robots](https://vyos.io) * fix: IS-572: run the post-promote probe whenever the deploy succeeded PROMOTE ran `wrangler deploy` and then the hostname purge, the tarball and three `r2 object put` calls in one step, while the post-promote probe named no status function in its `if:` and so carried an implicit success(). A transient purge 5xx, a tar error or one failed upload therefore failed PROMOTE, skipped the probe entirely, and left the new version LIVE on the production Worker, never verified and never rolled back. Split the step at the deploy. Everything fallible after it moves to a new `promote_publish` step gated on `steps.promote.outcome == 'success'`, and the probe becomes the verification finalizer: `!cancelled() && steps.promote.outcome == 'success'`. `!cancelled()` counts as a status function, which is what suppresses the implicit success() and lets the probe run after a failed purge/upload; it is not `always()`, because firing an auto-rollback while an operator tears the run down is the wrong reflex. Gating on PROMOTE's outcome rather than re-reading check_head_2 is strictly stronger: 'skipped' on the moved-branch path, 'success' only once the production deploy completed. The registry-pointer publish keeps its bare condition, whose implicit success() is now load-bearing — it is what stops a pointer from naming a rolled-back generation. Rollback-target capture no longer reaches backwards. The old jq flattened `versions[]` across ALL deployments and took the first 100%-traffic id found scanning newest→oldest, so a current deployment deliberately serving A/B at 90/10 resolved to an older deployment's version — and a probe failure would "roll back" to it and destroy the split. It now inspects only the current deployment and refuses otherwise. The verdict is discriminated inside jq so a malformed record fails red instead of masquerading as split traffic: `select(.percentage == 100) | .version_id` on `{"percentage":100}` yielded null, `null // empty` yielded empty, and empty was read as "split" — an unparseable API response promoting with the auto-rollback silently disarmed. Non-numeric `percentage`, empty/absent `versions` and unsortable `created_on` are rejected for the same reason. The hostname purge is retried once. It is the one post-deploy failure with an asymmetric knock-on: the probe measures the edge, so a purge that never lands makes a healthy deploy look like a failed promote. Finally, the apex PDF range-divergence fallback no longer answers with a 206 carrying bounds the client never requested (RFC 9110 §14.4). It discards the slice and fails 503, keeping the r2-range-divergence log line that is the actual alarm. Unreachable under today's workerd, which ignores multi-range. Advances: IS-572 🤖 Generated by [robots](https://vyos.io) * ci: IS-572: bind DOCS_CF_LIVE via env: in the purge/registry step The purge branch expanded ${{ vars.DOCS_CF_LIVE }} directly into the shell body while the other three consumers of the same value bind it through env: (PDF carry-forward, candidate reset, post-promote probe). zizmor flags the inline form as template expansion; the value is repo-controlled so it is not exploitable, but the env form removes the class and matches the surrounding steps. * ci: IS-572: validate DOCS_CF_LIVE, pin wrangler, bound rollback percentages Three Major findings from CodeRabbit review 4971096766, all outside-diff. DOCS_CF_LIVE failed open. Four steps branch on it — the SKIP_PDF carry-forward staleness check, the candidate-reset staleness check, the production hostname purge and the post-promote probe — and every one compares against the literal string "true", so an unset or misspelled repository variable silently took the branch that skips the check, skips the purge and skips the probe, after which the registry pointer published an unpurged, never-verified deployment on a green run. Validate the value once at the top of the only job that reads it, so all four comparisons are safe by construction instead of each re-deriving the guard. `npx wrangler` in the previous-build metadata fetch ran before `npm ci`, so on the normal (non-SKIP_PDF) path it downloaded whatever version the registry served rather than the lockfile-pinned 4.123.0 every later step uses. Move the install ahead of the first wrangler invocation and make it unconditional; the inline `npm ci` in the SKIP_PDF branch and the conditional install step after the check_head guard are both redundant once it is. The rollback-target jq required `percentage` to be numeric but not in range, so a current deployment reporting both 100 and 101 yielded exactly one 100%-traffic match and classified as a determinate `ok` — arming a rollback with a version selected from data already known to be nonsense. Out-of-range now routes to the malformed path, never to `split:`. Advances: IS-572 * ci: IS-572: validate rollback_id at capture, bind it via env at the finalizer rollback_id originates from Cloudflare's API (`wrangler deployments list --json`), so it is untrusted text. It was written to $GITHUB_OUTPUT unvalidated and then reached shell source through a template expansion at three sites in the post-promote finalizer. The $GITHUB_OUTPUT write is the sharper of the two exposures: a value containing a newline injects arbitrary ADDITIONAL step outputs regardless of how carefully the consumer quotes, because no shell is involved. Validate at capture — the only place that can stop it. The character class is deliberately wider than the observed format. wrangler 4.123.0 performs no client-side validation of a version id (it interpolates the string straight into the API URL path), so the canonical UUID shape is what Cloudflare's bundled SDK documents rather than something the CLI enforces. Hard-failing on anything but a UUID would turn a Cloudflare format change into a blocked promote; hard-failing outside [A-Za-z0-9._-] cannot, and still certainly excludes newlines, quotes, whitespace and every shell metacharacter. A safe-but-non-UUID value warns instead of erroring. Empty stays legitimate — it is the no-target state of all three benign paths (first deploy, no deployments, split traffic) and the finalizer's -z branch depends on it. A malformed non-empty value fails loudly, matching how the surrounding capture already treats malformed API data. At the finalizer, ROLLBACK_ID is bound through the step's env: mapping and the three sites use "$ROLLBACK_ID", so the value is never substituted into the shell source. The -z semantics are unchanged. Advances: IS-572 * ci: IS-572: verify probe status, reconcile a failed pointer publish Two merge blockers from the pre-merge adversarial gate on d079a2c9. 1. The post-promote probe accepted an error response as verification. `withDocsHeaders` sets X-Docs-Build unconditionally, so error responses carry it too; the probe read only that header and never the status line, so a production answering 500 with the new SHA satisfied the match and the registry pointer published for a generation serving errors. The probe now parses the status line and requires an exact 200 alongside the built SHA. Parsing resets on every status line, so 1xx interim responses contribute nothing and only the final response block is read; duplicate X-Docs-Build headers with identical values are accepted while conflicting ones surface as "<conflicting>" and can never match. Requests are bounded with --max-time 20 and the pipeline runs under pipefail so a transport failure is distinguishable from a missing header. Early-attempt non-200s and transport failures stay retryable — only the final attempt's verdict decides. Every unverified outcome still rolls back, but the three causes now carry distinguishable diagnoses. --fail is deliberately not used: it collapses 4xx/5xx into curl exit 22 and discards the status line, which is the value needed to tell a broken new version apart from a transport failure. 2. Production and the registry could diverge permanently. The round-8 promote/publish split correctly decoupled a publish failure from the probe, but added no convergence action for the case where the probe then succeeds: production served the new SHA while latest.json still named the old one, with nothing to reconcile it. latest.json is the source of truth for the candidate-reset staleness check and the SKIP_PDF carry-forward, so both would later restore a generation production no longer serves. A reconciliation step now runs when the promote and probe both succeeded but the pointer did not publish. It converges FORWARD — re-uploading the sha-scoped generation and only then publishing the pointer, so the pointer can never name an incomplete generation — and on failure leaves the pointer untouched and reports the divergence by name with both SHAs and a copy-pasteable manual repair. Gating on the probe's success keeps it off the rollback paths entirely, where production and the pointer already agree. The pointer step's load-bearing implicit success() is unchanged. Refs: IS-572 * ci: IS-572: drop registry-pointer reconciliation, keep the probe fix Scope reduction. The `Reconcile registry pointer after a failed publish` step introduced in the previous commit is removed; the post-promote probe rewrite in that same commit stays. Removing the step returns registry-divergence behaviour to what `rolling` has today: when `promote_publish` fails after a successful deploy, the pointer step's load-bearing implicit success() skips it, and production can end up serving a SHA that `$slug/latest.json` does not name. That path is now documented at the `Publish registry pointer` step and tracked in https://vyos.dev/T9237 rather than repaired here. Also reverts the DOCS_CF_LIVE header comment to "four later steps", which restores its internal agreement with the "four comparisons downstream" sentence in the same block. Advances: IS-572 🤖 Generated by [robots](https://vyos.io)
2026-08-21Merge pull request #2153 from alexandr-san4ez/T8868-rollingViacheslav Hletenko
kdump: T8868: Add documentation for kernel crash dump feature
2026-08-19Merge pull request #2215 from opswill/currentViacheslav Hletenko
T9229: adds documentation for sys-rawio
2026-08-19T9229: adds documentation for sys-rawioopswill
2026-08-17T9220: ci: upload JUnit test results to Codecov Test Analytics (#2212)Yuriy Andamasov
* T9220: ci: upload JUnit test results to Codecov Test Analytics Emit vitest JUnit XML (console reporter preserved via --reporter=default) and add a pinned codecov-action@v5.5.5 test-results upload step (tokenless, informational, continue-on-error). Test job semantics for the deploy gate are unchanged. Codecov Test Analytics fleet rollout, T1 batch. 🤖 Generated by [robots](https://vyos.io) * T9220: chore: ignore vitest JUnit output dir in workers Also satisfies the workflow's workers/** path filter so this PR's CI run exercises the new test-results upload (acceptance evidence). 🤖 Generated by [robots](https://vyos.io) * T9220: ci: compare head repo identity, not fork flag (review finding) 🤖 Generated by [robots](https://vyos.io)
2026-08-16docs: T9166: document IPv6 for HA peer linkchariri
2026-08-14ci: T9208: bump AI-validation reviewer pin to v1.0.4 for scutum branch map ↵Yuriy Andamasov
(#2201) 🤖 Generated by [robots](https://vyos.io)
2026-08-14chore(deps): bump undici, @cloudflare/vitest-pool-workers and wrangler (#2205)dependabot[bot]
Bumps [undici](https://github.com/nodejs/undici) to 7.29.0 and updates ancestor dependencies [undici](https://github.com/nodejs/undici), [@cloudflare/vitest-pool-workers](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/vitest-pool-workers) and [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together. Updates `undici` from 7.28.0 to 7.29.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.28.0...v7.29.0) Updates `@cloudflare/vitest-pool-workers` from 0.18.8 to 0.21.3 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Changelog](https://github.com/cloudflare/workers-sdk/blob/main/packages/vitest-pool-workers/CHANGELOG.md) - [Commits](https://github.com/cloudflare/workers-sdk/commits/@cloudflare/vitest-pool-workers@0.21.3/packages/vitest-pool-workers) Updates `wrangler` from 4.114.0 to 4.123.0 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.123.0/packages/wrangler) --- updated-dependencies: - dependency-name: undici dependency-version: 7.29.0 dependency-type: indirect - dependency-name: "@cloudflare/vitest-pool-workers" dependency-version: 0.21.3 dependency-type: direct:development - dependency-name: wrangler dependency-version: 4.123.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14docs: igmp-proxy: document firewall requirements and operational commands ↵JR L.
(#2174) Co-authored-by: JR Lanteigne <dniminenn@users.noreply.github.com>
2026-08-14docs: update AGENTS.md bot-review section to current CodeRabbit reality (#2188)Yuriy Andamasov
The "Bot review workflow" section documented a manual-invocation flow that no longer matches how this repo works. Stale claims removed: - "Auto-reviews are disabled on this repo — both bots are triggered manually." CodeRabbit auto-review is enabled here; the per-repo disable override was lifted. - The 5-step workflow instructing contributors to comment `@copilot review` on a draft, iterate until Copilot is silent, then flip to ready and comment `@coderabbitai review`. - The two-row bot table pairing Copilot with drafts and CodeRabbit with ready PRs. Current reality documented instead: - CodeRabbit reviews automatically on the draft -> ready flip and on every subsequent push; drafts are always skipped. - CodeRabbit commonly edits its walkthrough comment in place rather than posting a new one, so the absence of a new comment is not the absence of a review. - A rate-limited CodeRabbit silently drops that review; commenting `@coderabbitai review` after the window resets is the only case where a manual trigger is appropriate. - Copilot is no longer part of the workflow and should not be invoked. Threads from a manual invocation by someone else are addressed like any other reviewer feedback. Also adds the missing AI Validation entry to the CI list: it cross-checks changed docs Markdown against the vyos-1x source tree for the corresponding branch and posts inline plus summary review comments, runs only when a PR touches docs Markdown, and skips when the required repository secrets are unavailable or the PR is a Mergify-authored backport. README.md's pointer to the contributor guide is updated in the same pass to drop Copilot from the named workflow. Note that .github/copilot-instructions.md is a symlink to AGENTS.md, so it picks up the change automatically. 🤖 Generated by [robots](https://vyos.io)
2026-08-14 docs: Update LLDP page to VyOS 1.5 standards (#2192)LiudmylaNad
* docs: Update LLDP page to VyOS 1.5 standards * Update lldp.md
2026-08-14docs: Update IPoE server page to VyOS 1.5 standards (#2191)LiudmylaNad
* docs: Update IPoE server page to VyOS 1.5 standards * Minor corrections * Update ipoe-server.md
2026-08-14docs: T9159: document service ntp source-address option (#2185)Ruben Herold
* docs: T9159: document service ntp source-address option Companion doc entry for the vyos-1x change adding "set service ntp source-address <address>" (chrony's bindacqaddress). * docs: T9159: document service ntp source-interface option Companion doc entry for the vyos-1x change adding "set service ntp source-interface <interface>" (chrony's bindacqdevice), addressing multi-VRF feedback on T9159. * docs: T9159: clarify source-interface is scoped to a single VRF The source-interface description implied it helps disambiguate across multiple VRFs at once, but service ntp vrf already binds the whole NTP client to a single VRF - source-interface only picks the egress device within that one VRF. Flagged by CodeRabbit review on PR #2185.
2026-08-14T9208: add scutum to docs CI branch enumerations (#2199)Yuriy Andamasov
* T9208: add scutum to docs CI branch enumerations The scutum docs branch (VyOS 1.6 train) was cut from circinus on 2026-08-13. Register it in the branch enumerations that gate CI on the default branch: - context7-refresh.yml: add scutum to the push-trigger branch list, the workflow_dispatch choice options and the defence-in-depth variant allow-list, so a docs push on scutum refreshes its own Context7 variant instead of being dropped. - context7.json: add a `scutum` entry to `previousVersions`. Context7 registers branch variants from this file on the default branch; a refresh with `branch: "scutum"` 404s until that registration exists. Enumerations only — no behaviour change for the existing rolling, circinus and sagitta variants. 🤖 Generated by [robots](https://vyos.io) * T9208: add scutum to the context7 branch-to-version mapping rule The previousVersions entry registers the scutum variant, but the branch-to-version mapping rule Context7 feeds to the model still jumped straight from rolling to circinus and claimed rolling covers "1.6+" — the version scutum now serves. Add the scutum row and drop the stale version claim from rolling so the rule matches the variant list. Caught by adversarial review on 2b10f78.
2026-08-14 docs: Update BGP page to VyOS 1.5 standards (#2111)LiudmylaNad
* docs: Update BGP page to VyOS 1.5 standards * Update bgp.md * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Daniil Baturin <daniil@baturin.org> * Apply suggestions from code review Co-authored-by: LiudmylaNad <l.nadolina@vyos.io> * Update bgp.md * Fix a few factual issues and improve the wording Co-authored-by: Daniil Baturin <daniil@baturin.org> * Apply style suggestions from code review Co-authored-by: Daniil Baturin <daniil@baturin.org> * Re-added the closing ``` at line 2289 that the CI bot flagged. No content change --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Daniil Baturin <daniil@baturin.org>
2026-08-11Merge pull request #2194 from nvollmar/T9184Viacheslav Hletenko
T9184: adds documentation for chown and other missing options
2026-08-11-fixes spellingNicolas Vollmar
2026-08-11T9184: adds documentation for chown and other missing optionsNicolas Vollmar
2026-08-06docs: clarify Proxmox Cloud-Init user setup (#2138)jeleel-muibi
* docs: clarify Proxmox Cloud-Init user setup * docs: address Proxmox example lint feedback --------- Co-authored-by: Jeleel Muibi <jeleel-muibi@users.noreply.github.com>
2026-08-06docs: Update mDNS Repeater page to VyOS 1.5 standards (#2177)LiudmylaNad
* docs: Update mDNS Repeater page to VyOS 1.5 standards * Minor corrections
2026-08-05Merge pull request #2189 from vyos/nos-task-idYuriy Andamasov
T9164: coderabbit: surface NOS Jira project alongside legacy VD
2026-08-05T9164: coderabbit: surface NOS Jira project alongside legacy VDnos-task-idYuriy Andamasov
VD tracker project renamed to NOS (2026-07). Pins NOS ahead of the retained legacy VD in knowledge_base.jira.project_keys so CodeRabbit resolves the new keys in review summaries. Companion to the mergify task-id regex sweep (canary vyos/vyos-1x#5379). 🤖 Generated by [robots](https://vyos.io)
2026-08-04docs: upgrade vendored DataTables 1.11.5 -> 2.3.8 (#2173)Yuriy Andamasov
* docs: upgrade vendored DataTables 1.11.5 -> 2.3.8 Swap the vendored single-component DataTables downloader build from 1.11.5 to 2.3.8 (docs/_static/js/datatables.js, docs/_static/css/datatables.css). The only init site, docs/_static/js/tables.js, uses the option-less jQuery-style $('#id').DataTable() form, which is unchanged in 2.x, so neither it nor docs/_templates/layout.html needed edits. jQuery 3.6.0 is still injected ahead of the bundle by sphinxcontrib-jquery and satisfies 2.x. Remove docs/_static/css/DataTables-1.11.5/ (10 sort-icon images). 2.x draws sort indicators in pure CSS: the new datatables.css contains no url() references at all, and the replaced 1.11.5 stylesheet was the only thing in the repo that referenced those files. Migrate custom selectors for the 2.x generated-class renames, each verified against the downloaded bundle rather than assumed: - .dataTables_info -> .dt-info (tables.css, text.css) - .paginate_button -> .dt-paging-button (tables.css, text.css) - #coverage a.paginate_button{,.current,.next,.previous} -> #coverage button.dt-paging-button{...} 2.x renders paging controls as <button> (was <a> in 1.x), so the old rules failed on both element type and class. The .current/.next/ .previous modifiers are still emitted and are retained. The #table-cfgcmd_wrapper / #table-opcmd_wrapper rules are deliberately untouched: only the wrapper *class* changed (dataTables_wrapper -> dt-container); the element id is still <tableId>_wrapper. Drop the CodeQL exclusion (.github/codeql/codeql-config.yml, plus the optional codeql-cfg-path input in .github/workflows/codeql.yml). That config existed solely to paths-ignore the 1.11.5 bundle, which tripped 7 alerts (5x js/incomplete-multi-character-sanitization, 2x js/incomplete-sanitization). Upstream 2.x hardened the implicated helpers - _stripHtml now runs a do/while fixpoint over the <script> strip, and _escapeHtml uses all-global regexes - so those patterns are not expected to recur. Removing the exclusion restores default CodeQL coverage of the file; this PR's own CodeQL run is the empirical test. Provenance (DataTables downloader builder, styling "DataTables default", component set dt only - the 2.x equivalent of the previous #dt/dt-1.11.5): https://cdn.datatables.net/v/dt/dt-2.3.8/datatables.js sha256 184fb4bd0b9a81a955acd608ba94d0643c74271e78c0fae30f1f40b824f88b1d https://cdn.datatables.net/v/dt/dt-2.3.8/datatables.css sha256 e37677437e0fbe4a463aafc83bc4ea8d60986b72050ba9f47f740b41148184ae Verified with a local make html: build succeeded, and coverage.html renders both tables (table-cfgcmd 9095 rows, table-opcmd 3036 rows) with the 2.3.8 assets injected in the correct order after jQuery. 🤖 Generated by [robots](https://vyos.io) * docs: simplify paging-button hover background to one declaration The hover rule carried 'background-color: #E1E4E5 !important' followed by 'background: none'. The important longhand outranks the later normal-importance shorthand, so the gray hover fill did apply — but the pair is confusing and CodeRabbit read it as a dead declaration. Collapse to a single 'background: #E1E4E5 !important', which produces the identical effective state (gray fill, upstream 2.x hover gradient suppressed) in one declaration. 🤖 Generated by [robots](https://vyos.io) * docs: re-target mobile search-input margin at the 2.x sibling DOM DataTables 1.x nested the search <input> inside its <label>; 2.x renders them as siblings inside div.dt-search, so the narrow-viewport descendant rule 'label input' no longer matched and the 10px top margin was silently lost below 576px. Target '.dt-search input' instead. Flagged independently by the implementation pass and both adversarial reviewers (Codex + agy). 🤖 Generated by [robots](https://vyos.io)
2026-08-04docs: Update Conntrack sync page to VyOS 1.5 standards (#2147)LiudmylaNad
* docs: Update Conntrack sync page to VyOS 1.5 standards * Minor corrections * Update conntrack-sync.md * Use "active/backup" terminology consistent with official conntrackd docs Co-authored-by: Daniil Baturin <daniil@baturin.org> --------- Co-authored-by: Daniil Baturin <daniil@baturin.org>
2026-08-04 docs: Update DNS forwarding page to VyOS 1.5 standards (#2163)LiudmylaNad
* docs: Update DNS forwarding page to VyOS 1.5 standards * Update dns.md * Update dns.md
2026-08-04 docs: Update Dynamic DNS page to VyOS 1.5 standards (#2164)LiudmylaNad
* docs: Update Dynamic DNS page to VyOS 1.5 standards
2026-08-04 docs: Update DHCP Relay page to VyOS 1.5 standards (#2146)LiudmylaNad
2026-08-04 docs: Update Router Advertisements page to VyOS 1.5 standards (#2132)LiudmylaNad
* docs: Update Router Advertisements page to VyOS 1.5 standards
2026-08-04 docs: Update HTTP API page to VyOS 1.5 standards (#2131)LiudmylaNad
* docs: Update HTTP API page to VyOS 1.5 standards * Clarify the wording about self-signed certificates * Fix unsual wording regarding DH parameters configuration * Update https.md --------- Co-authored-by: Daniil Baturin <daniil@baturin.org>
2026-08-03ci: bump REVIEWER_REF to reviewer-v1.0.3 (rolling branch-map fix) (#2182)Yuriy Andamasov
The reviewer-v1.0.2 tag ships a branches.json that maps docs branch 'rolling' to vyos-1x branch 'current'. The vyos-networks/vyos-1x mirror renamed 'current' to 'rolling', so the 'Checkout vyos-1x at mapped branch' step fails on every rolling-based PR (first observed run 30565684480, PR #2177). reviewer-v1.0.3 carries the corrected mapping (rolling -> rolling) plus CI/docs-only changes; no reviewer Python source changes between the two tags. 🤖 Generated by [robots](https://vyos.io)
2026-08-03vyos-api: clarify comment regarding commit failure and partial commits (#2178)John Estabrook
2026-07-31docs(Installation): Remove outdated portal link and Proxmox/KVM cloud… (#2176)LiudmylaNad
* docs(Installation): Remove outdated portal link and Proxmox/KVM cloud-init note * Update install.md
2026-07-28Merge pull request #2172 from ↵Yuriy Andamasov
vyos/dependabot/npm_and_yarn/workers/postcss-8.5.23 chore(deps): bump postcss from 8.5.17 to 8.5.23 in /workers
2026-07-28Merge pull request #2161 from ↵Yuriy Andamasov
vyos/dependabot/npm_and_yarn/workers/multi-1160cd7046 chore(deps): bump sharp, @cloudflare/vitest-pool-workers and wrangler in /workers
2026-07-28chore(deps): bump postcss from 8.5.17 to 8.5.23 in /workersdependabot/npm_and_yarn/workers/postcss-8.5.23dependabot[bot]
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.17 to 8.5.23. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.17...8.5.23) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.23 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28security: remediate CodeQL code-scanning alerts (#2171)Yuriy Andamasov
* security: remediate CodeQL code-scanning alerts (picker XSS sinks, test sanitization, vendored DataTables exclusion) Remediates all 11 open CodeQL alerts on the default branch: - version-picker.js (js/xss-through-dom, alerts 1-3): percent-encode every DOM-derived path component (select.value, parsed location segments) at URL construction time via encodePath()/langUrlFor(), and tighten the parseLocation slug charset to [A-Za-z0-9._-]. No-op on legitimate sphinx slugs — URLs stay byte-identical (asserted by tests). - workers/apex/test/manifest.test.ts (js/incomplete-multi-character- sanitization, alert 6): strip HTML comments from the root.html fixture repeatedly to a fixpoint instead of a single pass. - docs/_static/js/datatables.js (alerts 4,5,7-11): excluded from CodeQL analysis via .github/codeql/codeql-config.yml (new codeql-cfg-path input to the fleet reusable workflow). The file is vendored stock DataTables 1.11.5; the flagged helpers are display/sort normalization, not sanitization boundaries. Excluding keeps the vendored copy byte-identical to upstream instead of hand-patching it. Adds 9 picker tests (hostile-input encoding + slug-charset accept/reject); workers suite 103/103 green. 🤖 Generated by [robots](https://vyos.io) * security: normalize pre-existing percent escapes in encodePath Adversarial-review finding (Codex, medium): location.pathname returns well-formed escapes verbatim, so blind encodeURIComponent double-encoded them (%2E -> %252E), broke the HEAD probe on escaped deep links, and dumped the user at the version root. Each segment is now decoded first (malformed escapes keep the raw segment — no throw), then re-encoded to canonical single encoding. Decoding cannot resurrect dot-segments: the URL parser resolves '.'/'..' and their percent-encoded forms during navigation, so pathname never presents them (verified against the WHATWG parser in Node). workers suite 106/106 (+2 regression tests, mutation-verified). 🤖 Generated by [robots](https://vyos.io) * security: normalize percent escapes per run, not per segment Round-2 adversarial finding (Codex, medium): whole-segment decode meant one malformed escape (a%20b%zz) threw for the segment and double-encoded the valid escapes beside it. encodeSegment now decodes+re-encodes each well-formed %HH run independently; literal spans (including a bare '%') always pass through encodeURIComponent, so taint neutralization holds unconditionally; a run decoding to invalid UTF-8 stays verbatim (already pure %HH text). workers suite 108/108 (+2 discriminating regression tests). 🤖 Generated by [robots](https://vyos.io)
2026-07-28Merge pull request #2165 from vyos/claude/remove-superpowerdocs-ba80b9Yuriy Andamasov
docs: exclude superpowers/ from the Sphinx build
2026-07-28docs: exclude superpowers/ from the Sphinx buildYuriy Andamasov
`docs/superpowers/` holds Claude Code plugin-generated internal design specs and plans, not user documentation. Adding `superpowers` to `exclude_patterns` guards against Sphinx ever picking the directory up and publishing it should it reappear in the source tree. Guard only — the content itself was already removed in 3ab97e71 and abb8be5b.
2026-07-25chore(deps): bump sharp, @cloudflare/vitest-pool-workers and wranglerdependabot/npm_and_yarn/workers/multi-1160cd7046dependabot[bot]
Bumps [sharp](https://github.com/lovell/sharp) to 0.35.2 and updates ancestor dependencies [sharp](https://github.com/lovell/sharp), [@cloudflare/vitest-pool-workers](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/vitest-pool-workers) and [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together. Updates `sharp` from 0.34.5 to 0.35.2 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.2) Updates `@cloudflare/vitest-pool-workers` from 0.18.4 to 0.18.8 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Changelog](https://github.com/cloudflare/workers-sdk/blob/main/packages/vitest-pool-workers/CHANGELOG.md) - [Commits](https://github.com/cloudflare/workers-sdk/commits/@cloudflare/vitest-pool-workers@0.18.8/packages/vitest-pool-workers) Updates `wrangler` from 4.110.0 to 4.114.0 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.114.0/packages/wrangler) --- updated-dependencies: - dependency-name: sharp dependency-version: 0.35.2 dependency-type: indirect - dependency-name: "@cloudflare/vitest-pool-workers" dependency-version: 0.18.8 dependency-type: direct:development - dependency-name: wrangler dependency-version: 4.114.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23kdump: T8868: Add documentation for kernel crash dump featureOleksandr Kuchmystyi
Introduced the kdump section detailing its purpose and functionalities.
2026-07-22Merge pull request #2160 from vyos/claude/smoke-hardening-2Yevhen Bondarenko
docs-gates: re-land HTTPError close + detail logging; widen smoke retry envelope
2026-07-22docs-gates: hard-bound the smoke deadline (cap probe timeout + inter-round ↵Yuriy Andamasov
sleep to remaining budget) Codex adversarial finding: DEADLINE_SECONDS was only checked BEFORE each op, so a probe or sleep starting at 479s could overshoot to ~510s — 480 was a soft target, not a hard bound. Make it hard: run() now computes an absolute deadline = start + DEADLINE_SECONDS plus a _remaining() helper. The per-probe socket timeout is capped to min(PROBE_TIMEOUT_SECONDS, max(1, remaining)) — the previously hardcoded 30 is now the PROBE_TIMEOUT_SECONDS constant; a probe with < 1s of budget is skipped and counted unresolved. The inter-round sleep is capped to min(RETRY_SLEEP_SECONDS, remaining) and is skipped entirely when the budget is exhausted (falling into the existing deadline path). No body-read-level deadline is added — pages are small, so the socket-op timeout bounds reads adequately. Deadline-path failure accounting is unchanged. Tests: the probe timeout is capped to the remaining budget (fake opener records the timeout it was opened with); the inter-round sleep is capped to the remaining budget (sleep spy); the existing suite stays green. 🤖 Generated by [robots](https://vyos.io)
2026-07-22docs-gates: widen smoke retry envelope for worker-version propagation (5 ↵Yuriy Andamasov
rounds x 30s) The merge-triggered smoke run for the round-based retry work failed on a worker-version propagation race that outlasted the 3-round x 20s envelope: all probes were served the previous SHA through rounds 1-2 and one path (cli.html) was still stale at round 3 (2 sleeps x 20s = 40s insufficient). Widen to MAX_ROUNDS=5 / RETRY_SLEEP_SECONDS=30 -> 4 inter-round sleeps x 30s = 2 min, covering the observed 1-2+ min propagation waves. The green path is unaffected (no retries -> zero added time); DEADLINE_SECONDS=480 still bounds the worst case. Tests read the constants dynamically (monkeypatch), so none pin the old literals. 🤖 Generated by [robots](https://vyos.io)
2026-07-22docs-gates: close HTTPError response; name failed assertion in smoke logs ↵Yuriy Andamasov
(CR round 2) Two accepted GitHub-side CodeRabbit findings on the smoke gate: Close the HTTPError response stream: _probe_once read the non-2xx body via e.read() but never closed the HTTPError, which is file-like and owns the response socket — leaking it / raising ResourceWarning on the expected-404 path every run. The read is now wrapped in "with e:" INSIDE the crash-containment nesting, so the stream is closed even if the read raises (still yielding the retryable transport-error result, never a traceback). Name the failed assertion in retry/fail logs: _probe_once now returns a compact `detail` naming which check failed ("status" / "docs-build" / "apex-build" / "search-mount", multiple joined by "+", or the transport error text; None when ok) instead of a bare transport-only field. SMOKE-RETRY / SMOKE-FAIL lines gain `detail=<...>` alongside the existing status / docs-build fields, so an apex-build or search-mount failure no longer logs an opaque "status=200 docs-build=<sha>". ok-path behavior and the JSON / exit contract are unchanged. Tests: HTTPError stream is closed on the happy-404 path and when the read raises (RecordingBody close recorder); apex-build-only and search-mount-only failures name their detail in the logs; _probe_once joins multiple failed checks with "+". Removed the now-unused _http_error helper. 🤖 Generated by [robots](https://vyos.io)
2026-07-22Merge pull request #2159 from vyos/claude/smoke-hardeningYevhen Bondarenko
docs-gates: smoke per-probe retry + explicit UA; workers: broaden asset-ext classification
2026-07-22docs-gates: round-based smoke retries + deadline; contain HTTPError read ↵Yuriy Andamasov
crash; dedup index probe (review round 1) Adversarial round (Codex + agy, both REQUEST CHANGES) on the per-probe retry model shipped in the prior commit — reworked: Round-based retries (both providers' critical): probe the whole plan once, then re-probe ONLY the still-failing probes each round (up to MAX_ROUNDS=3, one RETRY_SLEEP_SECONDS=20 gap between rounds). A probe passing in any round passes. This keeps the full per-probe failure enumeration (diagnostic value) that a fail-fast retry would lose, while bounding added time to at most 2 sleeps. DEADLINE_SECONDS=480 (time.monotonic from run() start, checked before each probe AND before each inter-round sleep) caps total wall-clock; on breach a single SMOKE-DEADLINE line is logged and every unresolved probe counts as failed. Intermediate not-ok logs "SMOKE-RETRY <path>: round <n> ..."; the JSON {"failures": n} summary and exit contract are unchanged. Contain HTTPError read crash (agy critical): a transport error DURING e.read() inside the HTTPError branch previously escaped the outer catch and crashed the gate. _probe_once now nests the open/HTTPError handling so ANY exception on the open OR body-read path yields a retryable transport-error result, never a traceback. Dedup index probe (agy): critical-pages.txt lists index.html, so /en/<slug>/index.html was probed twice. probe_plan now filters index.html out of the critical list; plan[0] stays the single index (and sole search-mount) probe. ua-policy.json intentionally left unchanged (pushback recorded: fail-open plus block-precedence make an allow entry non-protective). Tests reworked for round semantics: transport-error recovery across rounds, HTTPError-read containment, one-sleep-per-inter-round-gap spy, round scoping (only the failed path re-probed), run() JSON + exit contract, zero-deadline path, and index-probe dedup. 🤖 Generated by [robots](https://vyos.io)
2026-07-22docs-gates: smoke per-probe retry + explicit UA; workers: broaden asset-ext ↵claude/smoke-hardeningYuriy Andamasov
classification smoke.py — BIC independence: probe requests now send an explicit User-Agent (vyos-docs-smoke/1.0) so the gate no longer depends on a Cloudflare Browser Integrity Check UA-skip rule surviving. The default Python-urllib UA was blocked by BIC until that exemption was added; a silent dependency on it is a latent gate failure the moment the rule is touched. smoke.py — propagation-race tolerance: each probe now retries up to 3 attempts (20s apart; MAX_ATTEMPTS + RETRY_SLEEP_SECONDS are module-level so tests can shrink them) and only fails after the final attempt. A freshly deployed worker version loses a brief propagation race in which a single probe is served by the PREVIOUS version (observed: status 307 + stale X-Docs-Build minutes after deploy), which previously failed the entire gate. Intermediate attempts log SMOKE-RETRY; only exhaustion logs SMOKE-FAIL and counts a failure. Retry fires only on a not-ok outcome (wrong status, wrong/missing build header, missing search mount, or a transport exception); a legitimately-expected 404 passes on the first attempt. workers/branch — broaden asset classification (CodeRabbit post-merge nit): fold .pdf into the case-insensitive ASSET_EXT_RE and add webp + otf, so uppercase .PDF and modern image/font assets get the longer asset cache class. /_static/ and /_images/ path checks unchanged. 🤖 Generated by [robots](https://vyos.io)
2026-07-22Merge pull request #2158 from vyos/claude/html-handling-parityYevhen Bondarenko
workers: html_handling none + worker index-mapping — RTD .html URL parity
2026-07-22workers: bare-directory 301 via asset probe + asset-class images + config ↵Yuriy Andamasov
congruence test (review round 1)