| Age | Commit message (Collapse) | Author |
|
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)
|
|
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)
|
|
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)
|
|
(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)
|
|
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)
|
|
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)
|
|
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)
|
|
Addresses Copilot review on PR #2023:
1. .. code-block:: tracking was triggered by a plain substring check, which
matched mid-line occurrences too. In MD prose like ``.. code-block::``
(docs/documentation.md:222) this set in_rst_codeblock=True spuriously and
could suppress line-length checks downstream. Replace with a leading-whitespace-
anchored regex and gate on file_ext in ('.rst', '.txt') or an open {eval-rst}
MyST fence so the directive opener is only recognized where it can actually
occur.
2. print('start') in main() was leftover debug noise — remove it.
|
|
removal with word-boundary prefix
Agent-Logs-Url: https://github.com/vyos/vyos-documentation/sessions/cdefcaf2-e89e-4090-b39a-15b385b774df
Co-authored-by: andamasov <12631358+andamasov@users.noreply.github.com>
|
|
Previous workflow:
env:
FILES_MODIFIED: ${{ steps.file_changes.outputs.files_modified }}
run: python scripts/doc-linter.py "$FILES_MODIFIED"
`trilom/file-changes-action`'s `files_modified` output is
modifications-only. A PR adding a new `.md`/`.rst` doc page passed
`files_added`, never `files_modified`, so a brand-new page with long
lines or real public IPs slipped past the linter entirely.
Workflow: also pass `files_added` and `files_renamed` as separate
positional args. Each output is a JSON array (action v1.2.4) and is
passed via env to avoid shell-quoting issues.
Linter: `main()` now accepts one OR multiple positional argv entries,
each a JSON array of paths. Arrays are merged and deduplicated before
linting. Single-arg invocations remain backward-compatible. Switched
from `ast.literal_eval` to `json.loads` — the action's outputs are
JSON, and `json.loads` is the right tool (and dodges
literal_eval-via-`eval`-substring linter warnings).
Test coverage:
- Two JSON arrays merge -> single linter run on union.
- Empty-string argv entry skipped (no `files_renamed` in many PRs).
- Malformed JSON -> falls back to walking DOCS_ROOT.
- No argv -> walks DOCS_ROOT.
- Single-arg invocation -> backward-compat preserved.
Tracked as item 7 of the rolling-side cleanup backlog from PR #2014 /
#2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
`is_docs_path()` returned True for any path under `docs/`, including
the archived RST shadows under `docs/_rst_legacy/` and the build
output under `docs/_build/`. Sphinx excludes both from the build
(per `docs/conf.py`'s exclude_patterns) and AGENTS marks
`_rst_legacy` as reference-only. The linter shouldn't process either.
Add a `DOCS_EXCLUDED_SUBDIRS = ('_build', '_rst_legacy')` constant.
After confirming a path is under `docs/`, walk each excluded subtree
and reject the path if it's contained.
Also unify the auto-discover walk fallback to call `is_docs_path()`
for the filter — previously it had its own hand-rolled `"_build"
not in path` check that didn't handle `_rst_legacy` at all and would
have walked the entire legacy archive. Prune `dirs[:]` in-place at
each walk level so we don't descend into the excluded subtrees in
the first place — optimization on top of correctness. Reverted the
`_dirs` -> `dirs` rename here because we now mutate it.
Test coverage: 10 hand-coded `is_docs_path()` cases — all pass:
- `docs/configuration/foo.md` -> True
- `docs/_rst_legacy/foo.rst` -> False (was True)
- `docs/_rst_legacy/subdir/rst-foo.rst` -> False (was True)
- `docs/_build/html/index.html` -> False (was True)
- `docs/_include/foo.txt` -> True (live snippets stay in scope)
- `docs` -> True
- `AGENTS.md`, `README.md`, `.github/copilot-instructions.md`,
`scripts/doc-linter.py` -> False (already correct)
Tracked as item 4 of the rolling-side cleanup backlog from PR #2014
/ #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
The line-length skip was
test_line_length = not (in_md_fence or in_rst_codeblock)
`in_md_fence` was True for every MyST/Markdown fence regardless of
content type. That includes admonition directives like `:::{note}`,
`:::{warning}`, `:::{tip}` whose content is normal prose, not
preformatted code. Long lines in admonitions were silently skipped,
contradicting the documented 80-char rule which exempts code blocks
only.
Track an `is_code` property on each fence-stack entry. A fence is
code-bearing when:
- info string is empty (plain ``` per CommonMark), OR
- info string doesn't start with `{` (bare language tag like
`python`, `bash`, `yaml`), OR
- info string is `{<directive>}` and `<directive>` is in the
CODE_BEARING_DIRECTIVES set (`code-block`, `code`, `sourcecode`,
`cfgcmd`, `opcmd`, `cmdinclude`, `cmdincludemd`, `literalinclude`,
`parsed-literal`, `raw`, `command-output`, `eval-rst`).
Anything else is prose-bearing (`{note}`, `{warning}`, `{tip}`,
`{deprecated}`, `{seealso}`, …) and its content gets line-length
checked.
`in_md_code_fence` checks the topmost stack entry — the innermost
fence wins, so a `{note}` containing an inner `{code-block}` lints
the outer prose lines and skips the inner code-block body. The
classic `is_suppression_marker()` call still uses `in_md_fence`
because suppression markers are about "any fence depth" not
"code-bearing depth".
`{eval-rst}` is kept in CODE_BEARING_DIRECTIVES to preserve current
behavior — its body is RST and any line-length on nested
`.. code-block::` is handled by the separate RST tracker. Tightening
eval-rst is a separate change if wanted.
Test coverage:
- `_fence_is_code` classifier: 16 cases (code-like vs prose-like)
all pass.
- Integration: long line in `{note}` flagged ✓; long line in
```python``` not flagged ✓; long line in `{cfgcmd}` not flagged ✓;
nested `{note}` > ```text``` — inner skipped ✓; nested `{note}` >
prose — flagged ✓.
Sweep over current `docs/` tree: 28 new warnings surface across the
existing pages (long prose inside admonition directives that the
previous logic had been silently hiding). CI on PR scope is changed
files only, so the new findings appear only when contributors touch
those pages — they won't break this PR or future infra PRs.
Tracked as item 5 of the rolling-side cleanup backlog from PR #2014
/ #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
The dedent check inside `handle_file_action()` was
if in_rst_codeblock:
if len(line) > rst_codeblock_indent and not line[rst_codeblock_indent].isspace():
in_rst_codeblock = False
This worked only when the next line was at least
`rst_codeblock_indent + 1` chars long — the indexing
`line[rst_codeblock_indent]` requires that. A short dedented
line (e.g., a single character at column 0 under a directive
indented at column 4) failed the length guard and `in_rst_codeblock`
stayed True. The block remained open longer than it should,
suppressing line-length checks on subsequent prose until either
EOF or the next `.. code-block::` reset the state.
Replace with a leading-whitespace-count check: on any non-blank
line, exit the block when leading-ws is <= the directive's
column. Blank lines don't reset the block context.
Test: a 3-line file with `.. code-block:: text` directive at col
0, one body line, a single `a` at col 0, then a 113-char line
at col 0. With the old logic the long line is still treated as
inside the code block and not flagged. With the new logic the
single-`a` dedent exits the block and the long line is flagged
as expected.
Tracked as items 6 and 12 of the rolling-side cleanup backlog
from PR #2014 / #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
The leading-compression group in `IPV6GROUPS` was
r'(?:\s' + IPV6SEG + r':){1,7}:'
The `\s` required whitespace before each hextet in the repeated
group. In practice this meant compressed forms with leading
hextets — `2001:db8::`, `64:ff9b::`, `fe80::1` — only matched
when preceded by whitespace inside the line. The linter calls
`lint_ipv6(line.strip())`, so at start-of-stripped-line there's
no whitespace, and the address fell through to no match.
Real-world impact: a documentation page mentioning
`2001:4860:4860::8888` (Google DNS) or `64:ff9b::1` (NAT64
well-known prefix) at the start of a line silently passed the
IPv6 documentation-address check.
None of the other groups in `IPV6GROUPS` use a `\s` prefix.
This one was inconsistent. Drop the `\s` so the branch matches
compressed forms directly, like its peers.
Verified with 6 hand-coded cases (RFC 3849 doc range, Google
DNS, NAT64 prefix, mid-line and start-of-line positions). All
pass.
Tracked as item 3 of the rolling-side cleanup backlog from PR
#2014 / #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
`lint_ipv4()` and `lint_ipv6()` used `re.search`, which returns
only the first match. A line like
Set DNS forwarder 192.0.2.1 then fall back to 8.8.8.8
flagged nothing because `192.0.2.1` (RFC 5737 documentation
range) is allowed and the search stopped there. The real
public IP `8.8.8.8` slipped through despite being exactly
the case the linter was meant to catch.
Switch both functions to `re.finditer` and walk every match:
return on the first disallowed address; only return None when
all matches on the line are allowed (private / multicast /
non-global).
Also fix the casing of "private space" in both error
messages — was "private Space" with a stray capital.
Verified with 7 hand-coded cases (allowed + public mixes,
boundary cases, IPv6 RFC 3849 / Google DNS). All pass.
Tracked as items 1, 2, and 10 of the rolling-side cleanup
backlog from PR #2014 / #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
`lint_AS()` and its `NUMBER` regex were a placeholder for a
future AS-number documentation-range check (RFC 5398).
`lint_AS()` was never called from anywhere — it'd merely
`pass` on `re.search` hit. Pure dead code that made it look
like AS-number linting existed when it didn't.
Delete:
- the `NUMBER` regex constant
- the `lint_AS()` function
If/when AS-number linting is actually desired, implement it
properly: hook into the lint loop in `handle_file_action()`,
return the standard `(message, line, severity)` tuple on
violations, and define the allowed AS ranges from
`/^AGENTS.md/` (currently 64496–64511 and 65536–65551).
Tracked as item 8 of the rolling-side cleanup backlog from
PR #2014 / #2019 / #2020 reviews.
🤖 Generated by [robots](https://vyos.io)
|
|
`lint_mac()` was called and its return value immediately overwritten
with `None`:
err_mac = lint_mac(cnt, line.strip())
# disable mac detection for the moment, too many false positives
err_mac = None
The comment is correct — MAC linting produced too many false
positives — but the cleanup never landed. The dead call ran on every
line for every linted file, and the function/MAC-regex/MAC-error-text
all sat in the source as a misleading hint that MAC linting was a
live feature.
Delete:
- the `MAC` regex constant
- the `lint_mac()` function
- the `err_mac = lint_mac(...)`, `err_mac = None`, and the `err_mac`
entry in the tuple iterated by the error-collection loop in
`handle_file_action()`
Tracked as item 9 of the rolling-side cleanup backlog flagged across
the PR #2014 / #2019 / #2020 reviews. When MAC linting is genuinely
wanted again, recover the regex/function from git history and wire it
in cleanly.
🤖 Generated by [robots](https://vyos.io)
|
|
ValueError)
CodeRabbit / Ruff BLE001: the previous 'except Exception as e:' on the
explicit-file-list path caught any error, masking runtime failures
from handle_file_action() as silent fallback behavior. Only input
validation errors from ast.literal_eval(sys.argv[1]) should trigger
the fallback walk.
Refactor:
- Wrap only the parse step in try/except, catching just IndexError
(missing argv[1]), SyntaxError (malformed literal), and ValueError
(non-literal input).
- On parse failure, set files = None and dispatch to the DOCS_ROOT
walk via an explicit 'else' branch.
- On parse success, run the file loop outside the try so any errors
from handle_file_action() propagate normally and CI fails loudly.
Also drops the unused 'as e' (Ruff BLE001 noise) and the implicit
catch of TypeError (e.g. ast.literal_eval('42') returns an int and
'for file in 42:' would have been silently swallowed -> fallback
walk; now it raises clearly).
Verified scenarios:
- explicit file list (CI normal path) -> exit 0.
- no argv -> IndexError caught -> walks DOCS_ROOT.
- malformed argv ('not-a-list') -> SyntaxError caught -> walks DOCS_ROOT.
- explicit list with a non-existent file -> FileNotFoundError
propagates (previously silently triggered a fallback walk).
- explicit list with a non-list literal ('42') -> TypeError
propagates (programming error stays visible).
|
|
CodeRabbit nit (Ruff B007): the dirs variable from os.walk(DOCS_ROOT)
in the auto-discover fallback is unused. Renaming to _dirs makes the
intent explicit and silences the warning.
|
|
Three Copilot findings on ab497bf:
1. is_docs_path() docstring claimed paths 'resolve' under docs/, but
the implementation only normalized via abspath() — a symlink under
docs/ that points outside the tree would be treated as in-scope.
Switch both inputs to os.path.realpath() so symlinks are followed
to their real targets. The reverse case is also handled: if docs/
is itself a symlink (some CI checkouts), realpath() resolves it
consistently for both sides of the commonpath comparison.
Verified with a synthetic case: docs/poison.md -> /etc/hosts now
returns False (with abspath() it returned True).
2. The auto-discover fallback in main() still hardcoded
os.walk('docs') instead of using the new DOCS_ROOT constant.
Use DOCS_ROOT in both paths so the docs root is configured in
exactly one place.
3. Indentation inside 'for file in files:' was double-indented (8
spaces under the for, instead of 4) — pre-existing oddity from
before 65a8e9f, preserved through the is_docs_path() addition.
Normalize to a single indent level under the loop.
CI behavior unchanged: tj-actions/changed-files passes repo-relative
paths with no symlinks under docs/, which were already handled. The
realpath() switch only changes behavior in the symlink-escape case,
which was a bug.
|
|
CodeRabbit Pre-merge Docstring Coverage check reported 50% on
scripts/doc-linter.py (threshold 80%). Add minimal one-line docstrings
to each public function; no behavior change.
🤖 Generated by [robots](https://vyos.io)
|
|
CodeRabbit review on 28224f3 flagged that is_docs_path() introduced
in 65a8e9f only matched repo-relative path strings. An absolute path
to docs/... (e.g., from a local invocation that pre-resolves paths,
or from tooling that uses git ls-files --full-path) would silently
fail the docs/ check and the file would be skipped.
Rewrite the helper to use os.path.commonpath against an absolute
docs/ root computed on each call. Both inputs are normalized to
absolute form, so repo-relative and absolute callers produce the
same result. ValueError from commonpath (mixed Windows drives or
empty input) is caught and treated as 'not a docs path'.
abs_docs is recomputed per call rather than captured at import time
so the helper picks up the actual cwd at invocation, matching the
existing assumption that CI / local runs invoke the linter from the
repo root.
Verified against 12 edge cases:
- repo-relative docs paths (docs, docs/foo.md, docs/sub/dir/foo.md,
./docs/foo.md) -> True.
- repo-relative meta paths (AGENTS.md, README.md,
.github/copilot-instructions.md, docs_other/foo.md) -> False.
- absolute paths inside docs/ -> True; inside repo root but outside
docs/ -> False.
- traversal attempts (../other/foo.md, docs/../AGENTS.md) -> False.
CI behavior unchanged: tj-actions/changed-files passes repo-relative
paths, which were already handled by the previous logic.
|
|
The linter targets published documentation sources; the auto-discover
fallback already walks `docs/` only. CI was passing root-level meta
files (README.md, AGENTS.md, .github/copilot-instructions.md — the
last is a symlink to AGENTS.md) which forced docs-publication
conventions (80-char wrap, RFC IP rules, suppression markers) onto
project meta that has no business obeying them.
Add an `is_docs_path()` guard in `main()` so the explicit-file-list
path matches the auto-discover behavior — only files under `docs/`
are linted. AGENTS.md and the Copilot-instruction symlink are now
out of scope.
Verified:
- `python3 scripts/doc-linter.py "['AGENTS.md', 'README.md', '.github/copilot-instructions.md']"` → exit 0 (all skipped).
- `python3 scripts/doc-linter.py "['docs/_test_lint.md']"` with a real public IP → still errors as expected.
🤖 Generated by [robots](https://vyos.io)
|
|
Two issues from PR review:
1. MD/MyST fence tracking treated any longer same-char fence as a
closer, which would close `:::{note}` (3 cols) when seeing a
nested `::::{code-block}` (4 cols) opener inside it. Real bug
in `docs/configuration/interfaces/wireless.md:198–209` (currently
unobservable because inner code lines are <80 chars).
The "opener has info string / closer has none" heuristic is not
sufficient on its own: there are 2,826 bare-fence opens in the
tree, so info-string presence cannot distinguish opener from
closer.
Fix: stack-based tracking. A fence is treated as a closer only
when (a) the stack is non-empty, (b) char and length match the
top, AND (c) no info string follows. Anything else opens a new
(possibly nested) fence. The outermost fence's info string still
determines the `md_fence_is_eval_rst` flag.
2. `is_suppression_marker()` accepted `% stop_vyoslinter` in any
file outside an MD fence. Per AGENTS.md and the doc-linter
instructions, MyST `% ...` markers are only valid in `.md`
files; a stray `% stop_vyoslinter` in `.rst`/`.txt` should not
silently disable linting. Pass `file_ext` and gate the marker
forms accordingly: `% ...` only in `.md` outside fences;
`.. ...` in `.rst`/`.txt` outside RST code-blocks, or in `.md`
inside an `{eval-rst}` fence.
3. Drop the `not in_rst_codeblock` guard on `.. code-block::`
detection. Each occurrence resets the tracked indent (matches
`origin/rolling` baseline). Without this, code-block-inside-
code-block kept the outer indent and broke dedent detection
(verified regression: `_rst_legacy/configuration/system/
rst-syslog.rst:216` long-line warning was lost; restored).
Verified:
- All 7 original synthetic fixtures pass.
- New fixture `nested.md` (3-col outer wraps 4-col inner with long
line in between fences) produces exactly one warning at the line
outside both fences.
- New fixture `wrongmarker.rst` (`%` in `.rst`) — IP error fires
(marker correctly ignored).
- Full-tree run vs origin/rolling baseline: zero regressions on
pre-existing `.rst`/`.txt` warnings; all new output is `.md`.
🤖 Generated by [robots](https://vyos.io)
|
|
Agent-Logs-Url: https://github.com/vyos/vyos-documentation/sessions/5d679560-8a77-4735-b585-74c09293eea5
Co-authored-by: andamasov <12631358+andamasov@users.noreply.github.com>
|
|
Active docs are now MyST `.md`; the linter previously only inspected
`.rst` and `.txt`, so ~250 active pages were unchecked for IP usage and
line length on every PR.
scripts/doc-linter.py:
- Add `.md` to the extension filter (use `endswith` for correctness;
the prior 4-char slice silently skipped `.md` files).
- Track MyST/Markdown fenced code blocks (```` ``` ```` and `:::`) for
line-length exemption — same semantics as `.. code-block::` for RST.
- Recognize both suppression marker forms: `.. stop_vyoslinter` /
`.. start_vyoslinter` (RST and `.txt` includes) and `% stop_vyoslinter`
/ `% start_vyoslinter` (MyST). Both work in either context; pick the
form that matches the surrounding parser.
- Replace the brittle `try/finally: fp.close()` with a `with` block —
the previous form raised `UnboundLocalError` if `open()` itself
failed.
- Fix typo `forgett` → `forget`.
.github/instructions/rst-linter.instructions.md → doc-linter.instructions.md:
- Broaden `applyTo` from `**/*.rst` to `**/*.md,**/*.rst,**/*.txt`.
- Document MyST suppression syntax and fenced-code line-length
exemption.
- Note the parser-form rule for `{eval-rst}` blocks.
No regression on `.txt` includes: identical lint output verified
against the origin/rolling baseline on a sample of files.
Pre-existing IP violations exist in 14 `.md` files (e.g.
`configexamples/lac-lns.md` line 95 — a `8.8.8.8` already wrapped in
`% stop_vyoslinter`/`% start_vyoslinter`, correctly suppressed). PRs
touching unsuppressed violations will start failing CI; this is the
intent of enabling the check.
🤖 Generated by [robots](https://vyos.io)
|
|
The swap mechanism (RST-as-fallback for migrated MD pages) is dormant —
docs/_rst_overrides.txt has been empty since the MyST flip trio
(#1899/#1900/#1901) landed in May 2026. The mechanism's surface area
(scripts/swap_sources.py, its 245-line test, RTD pre/post hooks,
Makefile glue, conf.py dynamic loader) is dead weight, and the
rst-*.rst shadows scattered across the source tree cause Context7's
parser to misclassify the project as RST.
Changes:
- Move 253 rst-*.rst shadow files into docs/_rst_legacy/ preserving
subdirectory structure. They remain in the repo for reference; Sphinx
excludes the folder via exclude_patterns; Context7 excludes it via
excludeFolders.
- Strip swap_sources.py invocation from docs/Makefile (swap/restore
targets, : swap deps, trap chains).
- Strip jobs: pre_build/post_build block from .readthedocs.yml.
- Strip rst-*.rst exclude entry and the _md_exclude.txt loader from
docs/conf.py; replace with a single _rst_legacy exclude.
- Delete scripts/swap_sources.py, tests/test_swap_sources.py,
docs/_rst_overrides.txt.
- Update context7.json: add docs/_rst_legacy to excludeFolders;
fix stale "Branch current tracks…" rule to "Branch rolling tracks…"
(default branch was renamed 2026-05-10).
- Update AGENTS.md: drop the "RST override mechanism" section and the
test-runner snippet for the deleted test; describe _rst_legacy as
archive only.
Verified: sphinx-build -b html with --keep-going produces identical
warning set (68 unique), identical sitemap entry count (257), identical
llms.txt entry count (22), zero rst-* URLs in any artifact.
🤖 Generated by [robots](https://vyos.io)
|
|
The reusable lint-doc workflow at vyos/.github checks out vyos/.github
on the consumer's PR base.ref to source doc-linter.py — designed for
per-release-train linter rules. With this repo's default renamed
current → rolling and vyos/.github still on current, the checkout
errors with "fetch +refs/heads/rolling*: exit code 1".
Rather than chase branch parity across repos, move the linter where it
belongs: doc-linter.py is doc-specific and only consumed here. Inlining
removes the cross-repo coupling permanently and unblocks any future
branch renames in this repo without touching vyos/.github.
- scripts/doc-linter.py: copied byte-for-byte from
vyos/.github@current:.github/doc-linter.py (sha
3dc7c2fc16242e62b0ea7107f767577e999ca417 — identical across all four
release-train branches in vyos/.github, so no behavioral change).
- .github/workflows/lint-doc.yml: replaces `uses:
vyos/.github/.github/workflows/lint-doc.yml@current` with the inlined
steps. Same actions (bullfrogsec/bullfrog, trilom/file-changes-action,
setup-python) and the same final invocation, just sourcing the script
from this repo. Adds explicit minimal permissions (contents/pull-requests
read) and passes the file list via env var to follow the workflow-
injection guidance.
Follow-up: vyos/.github still hosts the now-orphaned doc-linter.py and
its reusable workflow — separate cleanup PR can delete them once any
other consumers migrate (none observed today; this repo was the only
caller).
🤖 Generated by [robots](https://vyos.io)
|
|
Phase 2 of the MD-as-primary flip. Inverts swap_sources.py so it activates
RST overrides (rst-<stem>.rst → <stem>.rst, with the matching <stem>.md
excluded via _md_exclude.txt) for stems listed in docs/_rst_overrides.txt.
Changes:
- scripts/swap_sources.py: rewritten with inverted rename direction and
renamed runtime artifacts (_rst_override_state.json, _md_exclude.txt).
CLI flags --swap/--restore/--dry-run/--status kept for compatibility with
the Makefile and Read the Docs config.
- docs/conf.py: clean up the runtime-artifact references that Phase 1 left
pointing at the old _swap_state.json and _swap_exclude.txt names.
- scripts/import_myst.py and tests/test_import_myst.py deleted; obsolete
after the flip (MD is canonical, no separate import workflow needed).
- tests/test_swap_sources.py: rewritten for the new semantics. All 10 tests
pass under pytest.
Smoke-tested end-to-end on a real worktree page (quick-start): adding the
stem to _rst_overrides.txt, --dry-run, --swap, --status, --restore, all
behave correctly. State JSON has version 2 (bumped from 1 to surface the
incompatibility on rollback if old state lingers).
Phase 3 will verify Makefile, .readthedocs.yml, docs/_ext/vyos.py don't
reference any of the old names, then mark the PR ready-for-review.
Generated by robots https://vyos.io
|
|
Category D — drop obsolete canary mechanism settings:
- conf.py: remove '**/md-*.md' from exclude_patterns (no canaries left)
- Makefile: replace malformed '*/_build/*' with '$(BUILDDIR)/**' and drop
the '*/md-*' ignore (canary files no longer exist)
Category C — script robustness:
- import_myst.py:
* list_myst_files() now raises SystemExit on git ls-tree failure instead
of silently returning [] (would have masked typo'd --source refs)
* list_rst_files() skips _build/ when scanning for .rst stems
* import_page() rejects stems containing '..' or absolute paths and
re-checks that the resolved destination stays under docs_dir
* --dry-run uses a separate "would_import" counter; summary line now
distinguishes dry-run from actual imports
- swap_sources.py:
* parse_swap_list() reads with explicit encoding='utf-8'
* do_restore() validates state file version + entry shape before
renaming files; raises with actionable message on corruption
* State file reads/writes use explicit encoding='utf-8' throughout
_swap.txt:
- Wrap long comment line to satisfy 80-character doc-linter limit
🤖 Generated by [robots](https://vyos.io)
|
|
The atexit handler in --swap mode caused immediate restore on process
exit, breaking standalone usage. Makefile trap and RTD post_build
handle restore reliably.
🤖 Generated by [robots](https://vyos.io)
|
|
Adds scripts/import_myst.py with import_page, git_show, list_myst_files,
list_rst_files, and do_import. Imported files are written as md-{name}.md
alongside existing RST files; importing is decoupled from swap activation.
Adds tests/test_import_myst.py covering single-page write, identical-skip,
warn-on-different-without-force, force-overwrite, and nested-path creation.
All 5 tests pass on Python 3.9.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Pre-build swap/restore script that renames md-{name}.md → {name}.md
before Sphinx builds and restores after. Includes state tracking,
exclude file generation, collision detection, and partial-failure
rollback. 10 tests cover all specified behaviors plus rollback path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
This reverts commit 4b36114e053ee11d0cb264a1e4cfe4692d78f194.
|
|
* feat: add swap_sources.py for incremental RST-to-MyST migration
Pre-build swap/restore script that renames md-{name}.md → {name}.md
before Sphinx builds and restores after. Includes state tracking,
exclude file generation, collision detection, and partial-failure
rollback. 10 tests cover all specified behaviors plus rollback path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add import_myst.py for importing MyST files from myst/* branches
Adds scripts/import_myst.py with import_page, git_show, list_myst_files,
list_rst_files, and do_import. Imported files are written as md-{name}.md
alongside existing RST files; importing is decoupled from swap activation.
Adds tests/test_import_myst.py covering single-page write, identical-skip,
warn-on-different-without-force, force-overwrite, and nested-path creation.
All 5 tests pass on Python 3.9.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add MyST swap exclude patterns and directive config to conf.py
🤖 Generated by [robots](https://vyos.io)
* feat: add swap-wrapped rendering targets to Makefile
🤖 Generated by [robots](https://vyos.io)
* feat: add swap pre/post build hooks for ReadTheDocs
🤖 Generated by [robots](https://vyos.io)
* feat: add empty _swap.txt, remove atexit from swap script
The atexit handler in --swap mode caused immediate restore on process
exit, breaking standalone usage. Makefile trap and RTD post_build
handle restore reliably.
🤖 Generated by [robots](https://vyos.io)
* feat: activate quick-start as MyST canary via swap mechanism
Imports docs/md-quick-start.md from origin/myst/current and adds
quick-start to docs/_swap.txt. Validates the swap pipeline end-to-end
on one page: import_myst pulls the MD via git show, swap_sources
renames md-quick-start.md to quick-start.md, sphinx-build renders
quick-start.html with zero MD-specific warnings, and restore reverses
the rename cleanly.
🤖 Generated by [robots](https://vyos.io)
* feat: activate 106 visual-validated canaries via swap
Imports 105 MD files (plus quick-start already present) from
origin/myst/current and adds them to docs/_swap.txt. The selection
is the BackstopJS visual-passers cohort: pages with <5% rendered
diff vs the live RST docs at docs.vyos.io/en/latest/, filtered to
those with an RST counterpart on current and no cmdincludemd usage
(template-format reconciliation pending).
Local sphinx-build with all 106 swapped: succeeded with 100
warnings (vs 95 baseline). The 5 new warnings are all undefined
cross-reference labels, not build failures:
- contributing/development.md (missing 'coding-guidelines')
- operation/upgrade-recovery.md (3 missing 'how_it_works' /
'cancelling_recovery')
- vpp/configuration/dataplane/{buffers,memory,unix}.md (missing
'vpp_config_dataplane_*' labels)
Source list: ~/.claude/projects/-Users-vybot-GitHub-vyos-documentation/docs/2026-04-29-myst-conversion-audit/visual-passers-under-5pct.txt
BackstopJS report: claude/gifted-hertz-74b9f9 worktree
(visual-compare/), 2026-04-23 vs vyos--1838.org.readthedocs.build.
🤖 Generated by [robots](https://vyos.io)
* fix: re-import 4 canary md-*.md files with xref label fixes
Re-imports the dash-form-corrected versions of:
- contributing/md-development.md (added (coding-guidelines)= anchor)
- operation/md-upgrade-recovery.md (3 ref renames: how_it_works /
cancelling_recovery -> dash form)
- vpp/configuration/dataplane/md-buffers.md (vpp_config_dataplane_physmem
-> vpp-config-dataplane-physmem)
- vpp/configuration/dataplane/md-unix.md
(vpp_config_dataplane_interface_rx_mode
-> vpp-config-dataplane-interface-rx-mode)
Source: origin/myst/current commit 59fbe3ea. Verified locally: clean
swap-build no longer reports any of the 5 target labels (1 of 6 —
vpp-config-hugepages — remains because system.md isn't in the canary
swap list; that anchor lives there).
🤖 Generated by [robots](https://vyos.io)
* fix: re-add 4 canary md-*.md files deleted by 242b334a
Commit 242b334a accidentally staged deletions instead of modifications
because the working tree had unprefixed *.md files left over from an
incomplete swap-restore cycle. Re-imports the same 4 files from
origin/myst/current with the xref label fixes applied:
- contributing/md-development.md — (coding-guidelines)= anchor
- operation/md-upgrade-recovery.md — how_it_works → how-it-works,
cancelling_recovery → cancelling-recovery
- vpp/configuration/dataplane/md-buffers.md — vpp_config_dataplane_physmem
→ vpp-config-dataplane-physmem
- vpp/configuration/dataplane/md-unix.md — vpp_config_dataplane_interface_rx_mode
→ vpp-config-dataplane-interface-rx-mode
Source: origin/myst/current commit 59fbe3ea.
🤖 Generated by [robots](https://vyos.io)
* fix: resolve remaining xref label gaps in swap-active build
Three small additions clear the cross-reference warnings tied to
underscore-vs-dash label form mismatches and the vpp-config-hugepages
reference that previously needed system.md in the canary set.
- system.rst: add .. _vpp-config-hugepages: alongside the existing
underscore label so memory.md references resolve regardless of
whether system.md is swap-active.
- md-lcp.md: add (vpp_config_dataplane_lcp_ignore-kernel-routes)=
alongside dash form (carries upstream from myst/current 079fa786).
- md-memory.md: add (vpp_config_dataplane_memory)= alongside dash
form (also from myst/current 079fa786).
Local clean swap-build with 106 canaries:
before: 305 warnings, 8 undefined-label entries in our scope
after: 300 warnings, 0 undefined-label entries in our scope
Remaining undefined-label warnings (release-notes, prepare_commit)
are in documentation.rst and unrelated to the canary swap mechanism.
🤖 Generated by [robots](https://vyos.io)
* fix: re-add md-lcp.md and md-memory.md (deleted by 870c9e7e)
Same disaster pattern as 242b334a: a swap-restore cycle left
unprefixed *.md files in the working tree, and the subsequent
git add staged deletions instead of modifications. Restoring the
two affected md-*.md files from origin/myst/current 079fa786
(which has the dual underscore+dash anchors needed for the
swap-active build).
🤖 Generated by [robots](https://vyos.io)
* feat: expand canaries to 114; refresh 3 with cfgcmd body fix
Adds 8 new visual-validated canaries from the post-cfgcmd-fix
BackstopJS run (2026-04-29):
- configuration/policy/as-path-list
- configuration/policy/community-list
- configuration/policy/extcommunity-list
- configuration/policy/large-community-list
- configuration/policy/local-route
- configuration/policy/prefix-list
- configuration/service/salt-minion
- configuration/system/updates
Refreshes 3 existing canaries whose MD content changed via the
cfgcmd/opcmd single-line body fix on myst/current fc19ab5c:
- configuration/firewall/global-options
- configuration/firewall/groups
- configuration/policy/route
All 11 sourced from origin/myst/current. Net: 106 -> 114 canaries.
🤖 Generated by [robots](https://vyos.io)
* fix: re-import md-cloud-init.md (block 3 fix from myst/current)
🤖 Generated by [robots](https://vyos.io)
* feat(swap): import .md files and webp transition from myst/current
Selective import from origin/myst/current (cf9c9b34):
- Add/update 255 .md files (full MyST conversion plus webp ref updates)
- Delete 175 PNG/JPG from docs/_static/images (webp twins already present)
- Delete 5 autotest topology.png (webp twins already present)
Preserved on swap (untouched):
- All .rst files (incremental swap pattern)
- conf.py, _ext/, _include/*.txt, .gitignore
- 115 canary md-*.md files
- 7 superpowers/specs/*.md design docs
- Logos vyos-logo.png / vyos-logo-icon.png (referenced by conf.py)
🤖 Generated by [robots](https://vyos.io)
* chore(swap): remove canary md-*.md files and docs/superpowers
- Remove 115 canary md-*.md files (incremental swap helpers no longer needed)
- Remove 8 files under docs/superpowers (project planning/design docs that
shouldn't ship in the documentation tree)
🤖 Generated by [robots](https://vyos.io)
* docs: address Copilot review feedback on imported MyST pages
Fix issues flagged by Copilot review on PR #1857 (the same content lives
in myst/current as the canonical source):
Real bugs:
- site-2-site-cisco.md: replace curly quote (U+2019) with ASCII apostrophe
- rsa-keys.md: fix typo "key-pair nam>>" → "key-pair name>"
- vmware.md: lowercase admonition directive (:::{NOTE} → :::{note})
- vpp/configuration/nat/index.md: remove blank line inside {include} fence
Grammar:
- vpp/configuration/interfaces/loopback.md: "bounded" → "bound"
- vpp/configuration/sflow.md: "VyOS support" → "VyOS supports"
- vpp/requirements.md: "bypass" → "bypasses"
- vpp/configuration/dataplane/interface.md: "configures" → "configure"
CI linter (IP addresses):
- nmp.md: wrap 8.8.8.8 example with stop/start_vyoslinter
- lac-lns.md: wrap LNS config block (contains 8.8.8.8)
- wan-load-balancing.md: wrap whole file (illustrative non-RFC IPs)
- policy/examples.md: replace 192.0.1.1 with RFC 5737 192.0.2.1
🤖 Generated by [robots](https://vyos.io)
* fix(swap): address Copilot review feedback on swap infrastructure
Category D — drop obsolete canary mechanism settings:
- conf.py: remove '**/md-*.md' from exclude_patterns (no canaries left)
- Makefile: replace malformed '*/_build/*' with '$(BUILDDIR)/**' and drop
the '*/md-*' ignore (canary files no longer exist)
Category C — script robustness:
- import_myst.py:
* list_myst_files() now raises SystemExit on git ls-tree failure instead
of silently returning [] (would have masked typo'd --source refs)
* list_rst_files() skips _build/ when scanning for .rst stems
* import_page() rejects stems containing '..' or absolute paths and
re-checks that the resolved destination stays under docs_dir
* --dry-run uses a separate "would_import" counter; summary line now
distinguishes dry-run from actual imports
- swap_sources.py:
* parse_swap_list() reads with explicit encoding='utf-8'
* do_restore() validates state file version + entry shape before
renaming files; raises with actionable message on corruption
* State file reads/writes use explicit encoding='utf-8' throughout
_swap.txt:
- Wrap long comment line to satisfy 80-character doc-linter limit
🤖 Generated by [robots](https://vyos.io)
* refactor(swap): rename imported .md files to md- prefix for swap mechanism
Restore the canary file naming convention that swap_sources.py expects:
the imported MyST pages now live as docs/<dir>/md-<name>.md alongside
the existing docs/<dir>/<name>.rst, so swap_sources.py --swap can rename
them into place at build time.
- 254 .md files renamed (every page with a matching .rst counterpart)
- 2 MyST-only pages left at their final names (no .rst exists, no swap
needed): docs/copyright.md, docs/automation/terraform/terraformvyos.md
All 114 stems listed in docs/_swap.txt now have a corresponding
md-<name>.md source file ready to swap in.
🤖 Generated by [robots](https://vyos.io)
* docs: address CodeRabbit review feedback on imported MyST pages
Fix issues flagged by CodeRabbit on PR #1857. All issues are pre-existing
in the upstream RST docs and inherited by the MyST conversion.
Real bugs:
- inter-vrf-routing-vrf-lite.md: invalid IPv6 next-hop "2001:db8::*" →
"2001:db8::1"
- ipsec-pa-route-based.md: vendor mislabel "Cisco" → "Palo Alto"
(header on line 39 and "Monitoring on Cisco side" section heading)
- bgp-ipv6-unnumbered.md: AS number mismatch between configuration and
verification output for both routers (Router A: 65020 → 64496;
Router B: 65021 → 64499)
- qos.md: class 30 used "match ADDRESS20" instead of ADDRESS30 — broke
the documented pattern (classes 10/20/30 → ADDRESS10/20/30)
Security:
- OpenVPN_with_LDAP.md: redact full PEM private key material from the
three "set pki ... private key '...'" lines and from the embedded
OpenVPN client <key> block; replace with <REDACTED> / ...REDACTED...
placeholders. Public certificates retained.
🤖 Generated by [robots](https://vyos.io)
* feat(swap): default to serving MyST for all swapped pages
Replace the previously-curated 114-stem _swap.txt with the full set of
254 imported md-prefixed pages, so MD is served by default at build
time. To revert any specific page back to RST, remove its stem from
_swap.txt (or comment it out).
🤖 Generated by [robots](https://vyos.io)
* fix(ext): handle RST fallback in CmdInclude when _renderer absent
`cmdincludemd` is in `myst_fence_as_directive`, so MyST routes
fence blocks through `render_fence → render_restructuredtext →
MockRSTParser`. In that path `self.state` is a plain docutils Body
with no `_renderer`, crashing the build.
Fall back to `nested_parse` when `_renderer` is unavailable so the
directive works in both MyST and RST/MockRSTParser contexts.
🤖 Generated by [robots](https://vyos.io)
* feat(conf): copy .md sources into HTML output for plain-text serving
Adds a build-finished hook that mirrors every .md file from the Sphinx
source tree into the HTML output directory verbatim, making unrendered
MyST sources accessible alongside HTML renders at the same URL path.
🤖 Generated by [robots](https://vyos.io)
* docs: address review feedback from PR #1857
Fix conversion artifacts, typos, grammar errors, and technical
inaccuracies flagged by automated code review (Copilot + CodeRabbit).
Infrastructure: add root-level md-*.md exclusion to conf.py,
fix sphinx-autobuild ignore globs in Makefile.
Content: fix curly quotes, invalid Go panic() calls, shell quoting
in cURL examples, incorrect firewall command paths, typos across
22 documentation files, remove duplicate sections.
🤖 Generated by [robots](https://vyos.io)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|