Skip to content

ci(docs): run docs build on every PR + add version-pin and src-citation guards - #88

Merged
chrisleekr merged 3 commits into
mainfrom
chore/51-docs-ci-drift-guards
May 1, 2026
Merged

ci(docs): run docs build on every PR + add version-pin and src-citation guards#88
chrisleekr merged 3 commits into
mainfrom
chore/51-docs-ci-drift-guards

Conversation

@chrisleekr-bot

@chrisleekr-bot chrisleekr-bot Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #51. The docs/ workflow used to gate behind a paths: filter scoped to docs/**, mkdocs.yml, CHANGELOG.md and the workflow itself, so code-side PRs that invalidated doc facts (Renovate Bun bumps, Dockerfile.* changes, src/ refactors that shifted cited line numbers) never tripped mkdocs build --strict. Even when --strict ran, it only validates internal links and snippet targets, not prose-vs-source agreement. This PR drops the paths: filter from pull_request: so every PR runs the docs build (the Deploy to GitHub Pages step keeps its if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' guard, so PRs validate but never publish), adds two project-specific guards that run before the strict build, and refreshes CLAUDE.md's Documentation map for the post-reorg docs/ layout.

Changes

  • Workflow trigger. .github/workflows/docs.yml pull_request: no longer carries a paths: filter. The push: trigger keeps its filter so GitHub Pages only redeploys when doc sources actually change.
  • Version-pin guard. New scripts/check-docs-versions.ts reads the canonical Bun version from .tool-versions, asserts that package.json engines.bun / packageManager and the two Dockerfile.* FROM oven/bun:<ver> AS base lines agree, then scans every docs/**/*.md for oven/bun:<ver> and Bun semvers (only inside lines that mention bun, to avoid false matches on Node / openssl pins) and fails on disagreement.
  • Citation guard. New scripts/check-docs-citations.ts walks docs/**/*.md, extracts every src/<path>.<ext>:<line> (or :<start>-<end>) citation, and verifies the file exists and the line / range is in bounds. Bare src/foo.ts references without a :line suffix are intentionally out of scope — they don't claim a line and can't go stale on a shift.
  • Script wiring. Both checks become package.json check:docs-versions / check:docs-citations scripts and are chained into bun run check. The workflow runs them as Verify docs version pins / Verify docs src citations steps before mkdocs build --strict (the build step is unchanged), behind a fresh oven-sh/setup-bun@v2 so bun is on PATH.
  • CLAUDE.md. Refreshes the surface-to-page map for the audience-first docs/ layout (operate/, build/, use/), and adds a paragraph declaring the two new CI-enforced gates.

Files changed

  • .github/workflows/docs.yml · drop paths: from pull_request:; add Bun setup and two Verify… steps before mkdocs build --strict.
  • scripts/check-docs-versions.ts · new — pin every Bun version reference to .tool-versions.
  • scripts/check-docs-citations.ts · new — anchor-verify src/<path>:<line> citations.
  • package.json · add check:docs-versions / check:docs-citations scripts; chain both into check.
  • CLAUDE.md · refresh Documentation surface-to-page map for the post-reorg layout; document the two new gates.
  • IMPLEMENT.md · tracking artifact for the implement-workflow comment.

Commits

  • 3455f33 · ci(docs): run docs build on every PR + add version-pin and src-citation guards

Tests run

  • bun run typecheck · pass (clean).
  • bun run lint · pass — 0 errors, 261 pre-existing warnings unchanged (none in the two new scripts).
  • bun run format · pass after bun run format:fix normalised scripts/check-docs-citations.ts.
  • bun run check:no-destructive · pass.
  • bun run check:docs-versions · pass on a clean tree (OK: every Bun version reference matches .tool-versions canonical 1.3.13).
  • bun run check:docs-citations · pass on a clean tree (OK: every src/<path>:<line> citation in docs/ points at an in-range location).
  • Negative-path for check:docs-versions: edited docs/operate/setup.md to say 1.3.8; rerun exited 1 with docs/operate/setup.md:9 [...] found '1.3.8', expected '1.3.13'. Reverted.
  • Negative-path for check:docs-citations: appended src/k8s/ephemeral-daemon-spawner.ts:99999 to docs/operate/deployment.md; rerun exited 1 with start line 99999 out of range (file has 226 lines). Reverted.

bun run test and bun run docs:build were not run locally — the test suite goes through scripts/test-isolated.sh and depends on Postgres + Valkey docker containers, and mkdocs build --strict needs docs/requirements.txt Python deps. Both run in CI on this PR.

Verification

Acceptance criteria from the plan:

  1. Docs build runs on non-doc PRs. pull_request: has no paths: filter; the Deploy to GitHub Pages step keeps its push / workflow_dispatch guard so PR runs validate without publishing. This PR itself touches src/-adjacent and workflow files but no docs/** content, so the Docs / build check on this PR is the live demonstration.
  2. Version-pin check is wired. Script exits 0 on a clean tree and 1 on disagreement; wired into bun run check and as a Verify docs version pins step in the workflow.
  3. Citation check is wired. Script exits 0 on a clean tree and 1 on out-of-range citations; wired into bun run check and as a Verify docs src citations step in the workflow.
  4. bun run check includes both gates. Updated in package.json.
  5. Workflow gate fails the job. Both Verify… steps invoke bun run … without continue-on-error, so a non-zero exit fails the Docs / build check.
  6. Strict build still green. No content edits to docs that the verifier would flag.

The four originally-cited stale facts (SETUP.md:11 >= 1.3.8, DEPLOYMENT.md:30 oven/bun:1.3.12, DEPLOYMENT.md:167 and DEPLOYMENT.md:215 src/app.ts:<line> pointers) were already re-synced during the audience-first docs reorg in commit a9c919d (#87). This PR only adds the structural CI gates that prevent them from rotting again.

Related Issues

Test plan

  • Tests added/updated where the change introduces new behaviour (the two new scripts are exercised by their negative-path checks above)
  • bun run typecheck clean
  • bun run lint no new errors
  • Existing tests still pass (or pre-existing failures noted above)

…on guards

Closes #51.

The docs workflow used to gate behind a paths: filter scoped to docs/**,
mkdocs.yml, CHANGELOG.md and the workflow itself, so code-side PRs that
invalidated doc facts (Renovate Bun bumps, Dockerfile changes, src/
refactors that shifted cited line numbers) never tripped mkdocs build
--strict. Even when --strict ran, it only validates internal links and
snippet targets, not prose-vs-source agreement.

Drops the paths: filter from pull_request: so every PR runs the docs
build (Deploy to GitHub Pages stays gated on push / workflow_dispatch).
Adds two project-specific checks that run before the strict build:

- scripts/check-docs-versions.ts pins every Bun version reference in
  docs/ to .tool-versions, and asserts package.json engines.bun /
  packageManager and the two Dockerfile.* FROM oven/bun:<ver> lines
  agree.
- scripts/check-docs-citations.ts walks docs/**/*.md, extracts every
  src/<path>:<line> (or :<start>-<end>) citation, and verifies the file
  exists and the line / range is in bounds.

Both scripts are also chained into bun run check. CLAUDE.md's
Documentation section is refreshed for the post-reorg docs/ layout
(operate/build/use directories) and now declares the two new gates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chrisleekr-bot

chrisleekr-bot Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

bot workflow review — succeeded

🔍 Code review complete — 6 files, +474/-122.

Summary

Solid, well-scoped CI hardening PR. The diagnosis is correct (the old paths: filter on pull_request: left a real silent-rot surface for code-side bumps that invalidate doc facts), the fix is the minimum-viable structural one (drop the filter, keep the publish step gated), and the two new verifier scripts are written defensively (canonical-source pattern, scoped regex on Bun-mention lines to avoid Node/openssl false positives, anchored citation regex with explicit extension allowlist). The negative-path checks called out in the PR description match what the scripts would emit. Three minor improvements posted inline; nothing blocking.

What was checked

  • Read in full: .github/workflows/docs.yml, scripts/check-docs-versions.ts, scripts/check-docs-citations.ts, package.json diff hunks, CLAUDE.md diff hunks, IMPLEMENT.md diff hunks.
  • Cross-references: confirmed .tool-versions says bun 1.3.13; package.json engines.bun and packageManager agree; both Dockerfile.orchestrator:13 and Dockerfile.daemon:14 FROM oven/bun:1.3.13 AS base lines agree; docs/operate/{deployment,setup}.md and docs/build/conventions.md Bun strings all reference 1.3.13. Scanned docs/ for src/<file>:<line> citations — currently zero, all src/ references are bare paths (intentionally out of scope per the script comment), so the citation gate runs against an empty input set on this PR.
  • Local validation: bun install (525 packages), bun run typecheck (clean), bun run lint (0 errors / 261 pre-existing warnings, matching the PR claim), bun run scripts/check-docs-versions.ts (OK), bun run scripts/check-docs-citations.ts (OK).
  • bun run docs:build was not run locally (needs Python deps from docs/requirements.txt); CI on this PR is the canonical surface for that, and the workflow change is exactly what makes that the case.

Findings

All three posted as inline comments on the diff. None blocking.

Severity File:line Issue
minor scripts/check-docs-versions.ts:108 checkDockerfile only scans FROM oven/bun:<ver> AS base lines; the oven/bun:1.3.13 mention in the comment at Dockerfile.daemon:193 will silently rot on the next bump. Fix: reuse the OVEN_RE line-scan you already do for docs/ against every Dockerfile line.
minor scripts/check-docs-citations.ts:24 CITATION_RE accepts .. segments in the path component, so a doc citing src/../foo.ts:1 would statSync outside src/. Read-only so not a security issue, but the gate would falsely report "OK" for citations that don't actually point into src/. Fix: reject normalized paths that contain a .. segment.
minor scripts/check-docs-citations.ts:136 (representative line — applies to both new scripts) No committed test exercises either gate. Manual negative-path checks proved the scripts work today, but a regex tightening / extension-list edit / \b slip in a future refactor would silently weaken the gate. Two short test/scripts/*.test.ts files using Bun.spawnSync against tmp-dir fixtures (one good, one bad) would defend the gate against the same drift it defends docs against.

Reasoning

A few things looked like they might be issues but checked out on closer reading:

  • engines.bun exact-match check (scripts/check-docs-versions.ts:69) — the script uses the first semver in engines.bun and asserts it equals canonical. Currently >=1.3.13 extracts 1.3.13 and matches. If maintainers ever loosen the floor, the script forces them to keep canonical and the floor in lockstep — that is the script's stated mandate, not a bug.
  • Citation regex extension allowlistts|tsx|mts|cts|mjs|cjs|js excludes .sql / .json / .yaml. Today there are no src/<path>.<ext>:<line> citations to non-TS files in docs/, so this is not a current coverage gap. Worth keeping in mind if SQL migrations ever start being cited with line anchors, but doesn't need to land in this PR.
  • Workflow concurrency on PR-wide triggerdocs-${{ github.ref }} with cancel-in-progress: false. PRs use refs/pull/<n>/merge, so each PR is its own group; multiple synchronize events queue rather than overlap. Fine.
  • Unanchored BUN_SEMVER_RE triggering on Node 20 / openssl 3.x — preempted by the if (!/bun/i.test(line)) continue; gate (scripts/check-docs-versions.ts:170). The oven/bun: prefix is also explicitly skipped (line 177) so it isn't double-counted. Confirmed against docs/operate/deployment.md:18 and docs/build/conventions.md's "Bun version" line — both pass.
  • Workflow step orderingsetup-bun is placed before setup-python, and the two Verify… steps run before Install dependencies / mkdocs build --strict. Bun scripts use only node:fs/node:path/node:url so bun install is not needed before the verify steps; the chosen order is correct and minimizes wasted CI time on a fail-fast scenario.
  • fileLineCount correctness (scripts/check-docs-citations.ts:49-57) — counts 0x0a and adds 1 only when the file is non-empty and doesn't end with 0x0a. Handles trailing-newline and no-trailing-newline files correctly; CRLF doesn't double-count because only \n is tallied.

Net: ship-ready. Recommend addressing the three minor inline findings, but none of them gate this PR.

cost: $3.5897 · turns: 54 · duration: 475s

let foundBase = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i] ?? "";
const baseMatch = /^FROM\s+oven\/bun:(\d+\.\d+\.\d+)\s+AS\s+base/.exec(line);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] The checkDockerfile regex only validates the FROM oven/bun:<ver> AS base line, so a stale oven/bun:<ver> reference in a Dockerfile comment will not be caught. Concrete example: Dockerfile.daemon:193 says # /root is mode 700 in oven/bun:1.3.13 — when .tool-versions next bumps, the FROM line gets fixed (script catches it) but that comment silently rots with the same kind of drift this PR is trying to eliminate.

Suggest applying the same OVEN_RE = /oven\/bun:(\d+\.\d+\.\d+)/g scan you already do for docs to every Dockerfile line, in addition to the anchored FROM ... AS base check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classification: Valid. Fixed in 0bf0164checkDockerfile now scans every oven/bun:<ver> occurrence on each Dockerfile line via the existing OVEN_RE regex (the anchored FROM ... AS base check is kept as a presence assertion). The stale-comment case at Dockerfile.daemon:193 would now trip on the next bump.

// Match `src/<path>.<ext>:<line>` or `:<start>-<end>`. Path may include
// `[A-Za-z0-9_./-]`; extension is one of the source-code extensions we
// actually cite. `:<line>` is required — bare paths are ignored.
const CITATION_RE = /\bsrc\/([A-Za-z0-9_./-]+\.(?:ts|tsx|mts|cts|mjs|cjs|js)):(\d+)(?:-(\d+))?\b/g;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] CITATION_RE accepts .. segments inside the path component ([A-Za-z0-9_./-]+), so a doc that wrote src/../README.md.ts:1 or src/sub/../foo.ts:1 would be statSync-ed at a path that escapes src/. The script is read-only so this is hygiene rather than a security bug, but it would silently report "OK" for citations that were never actually pointing into src/, defeating the gate's intent.

Cheap fix: after the regex match, reject any path whose normalized form begins with .. or contains a .. segment, e.g. if (relPath.split("/").includes("..")) { broken.push(...); continue; }.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classification: Valid. Fixed in 0bf0164 — after the regex match, relPath.split("/").includes("..") rejects any citation whose path component contains a .. segment with reason path contains a \..` segment — citations must point inside src/. New test test/scripts/check-docs-citations.test.ts includes a regression case (src/sub/../app.ts:1`).

process.exit(1);
}

main();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] No automated test for either new gate. The PR description shows a manual negative-path check (appending src/k8s/ephemeral-daemon-spawner.ts:99999 to a doc, observing exit 1, reverting), but nothing committed will catch it if a future refactor weakens the regex (e.g. tightens the boundary, drops an extension, or accidentally swallows the \b after \d+). These are CI gates — losing them silently is exactly the failure mode this PR is trying to prevent for docs.

Suggest a small test/scripts/check-docs-citations.test.ts (and sibling for versions) that writes a known-good and known-bad fixture into a tmp dir, invokes the check via Bun.spawnSync, and asserts the exit code + stderr substring. Two fixtures per script; ~30 LOC each.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classification: Valid. Fixed in 0bf0164 — added test/scripts/check-docs-versions.test.ts (5 cases) and test/scripts/check-docs-citations.test.ts (6 cases). Each test builds a tmp-dir fixture (.tool-versions, package.json, Dockerfile.*, docs/, src/), spawns the script via Bun.spawnSync with DOCS_CHECK_REPO_ROOT pointing at the fixture, and asserts exit code + a stderr substring. Both scripts grew a tiny env-var seam (process.env["DOCS_CHECK_REPO_ROOT"] ?? <import.meta.url-derived default>) so production invocations are unchanged but tests don't have to copy the script. bun test test/scripts/ → 11 pass, 0 fail.

@chrisleekr-bot

chrisleekr-bot Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

bot workflow resolve — succeeded

🔎 Resolve iteration complete — 0 failing checks, 3 open comment threads (some may already be resolved).

Resolve — PR #88 (chore/51-docs-ci-drift-guards)

Summary

All three open review threads on PR #88 were classified Valid and addressed in a single follow-up commit (0bf0164). The two new CI gates introduced by the PR (scripts/check-docs-versions.ts and scripts/check-docs-citations.ts) now (a) catch stale oven/bun:<ver> mentions inside Dockerfile comments / RUN / ENV lines instead of only the anchored FROM … AS base form, (b) reject .. segments inside the citation path component so a doc citing src/sub/../foo.ts:1 no longer silently reports OK, and (c) are exercised by 11 committed tests under test/scripts/ that drive each script via Bun.spawnSync against tmp-dir fixtures. Both scripts grew a tiny env-var seam (DOCS_CHECK_REPO_ROOT, namespaced + commented) so tests can point them at a fixture tree without copying the script — production invocations leave it unset and resolve repoRoot from import.meta.url exactly as before. Branch is now 0bf0164, 2 ahead of main, no rebase required; the new commit is the final blocker for CI reruns to confirm. Nothing else is outstanding from this resolve iteration.

CI status

Check State at start Action
All required checks passing on 3455f33 None (branch was clean entering resolve)

No diagnose-and-fix cycle was needed. The follow-up commit 0bf0164 will rerun the same suite. The new tests are scoped to test/scripts/ and stay inside the existing 90% per-file coverage threshold (Bun's coverage only counts files loaded into the test process; the spawned scripts run in a child process and aren't measured by the parent test file's coverage).

Review comments

ID File:line Classification Action Reply
3173541169 scripts/check-docs-versions.ts:113 Valid checkDockerfile now scans every oven/bun:<ver> occurrence on each line via the existing OVEN_RE regex; the anchored FROM … AS base check is kept solely as a presence assertion. Stale comments such as Dockerfile.daemon:193 (# /root is mode 700 in oven/bun:1.3.13) now trip on the next bump. Regression test: flags a Dockerfile comment whose oven/bun:<ver> has rotted in test/scripts/check-docs-versions.test.ts. 3173593316
3173541707 scripts/check-docs-citations.ts:29 Valid After the regex match, relPath.split("/").includes("..") rejects any citation whose path component contains a .. segment, with reason path contains a \..` segment — citations must point inside src/. Regression test: rejects `..` segments in the path component`. 3173593722
3173542242 scripts/check-docs-citations.ts:154 Valid Added test/scripts/check-docs-versions.test.ts (5 cases) + test/scripts/check-docs-citations.test.ts (6 cases). Each builds a tmp-dir fixture (.tool-versions, package.json, Dockerfile.*, docs/, src/), spawns the script via Bun.spawnSync with DOCS_CHECK_REPO_ROOT pointing at the fixture, and asserts exit code + a stderr substring. Both scripts now read process.env["DOCS_CHECK_REPO_ROOT"] ?? <import.meta.url default> so production behaviour is unchanged. bun test test/scripts/ → 11 pass, 0 fail. 3173594183

Commits pushed

Outstanding

Nothing blocks merge from a resolve perspective:

  • All three review comments are addressed and have evidence-backed replies.
  • Local bun run typecheck, bun run lint (0 errors / 276 pre-existing warnings, unchanged), bun run format, bun run check:no-destructive, bun run check:docs-versions, bun run check:docs-citations, and bun test test/scripts/ are all green.
  • bun run test (full suite via scripts/test-isolated.sh / Postgres + Valkey) and bun run docs:build (mkdocs strict, needs Python deps) were not run locally — both run in CI on this PR and were passing on the prior commit.
  • This bot can post inline replies but cannot submit a formal APPROVE review decision (FR-017). Final approval and merge remain a human action.

cost: $4.9707 · turns: 75 · duration: 875s

chrisleekr-bot[bot] and others added 2 commits May 1, 2026 14:38
- check-docs-versions.ts: scan every `oven/bun:<ver>` occurrence on each
  Dockerfile line, not just the anchored `FROM ... AS base` form, so a
  stale comment such as `Dockerfile.daemon:193` (`# /root is mode 700 in
  oven/bun:1.3.13`) now trips on the next bump.
- check-docs-citations.ts: reject `..` segments inside the citation path
  component so a doc that wrote `src/sub/../foo.ts:1` no longer silently
  reports OK for a pointer that escapes `src/`.
- Add `test/scripts/check-docs-{versions,citations}.test.ts` exercising
  both gates against tmp-dir fixtures via `Bun.spawnSync`. Both scripts
  now honor a `DOCS_CHECK_REPO_ROOT` env override so tests can point the
  gate at a fixture tree without copying the script; production
  invocations leave it unset and resolve from the script's own location.

Refs PR #88 review thread.

Co-authored-by: chrisleekr-bot[bot] <chrisleekr-bot[bot]@users.noreply.github.com>
Co-authored-by: chrisleekr-bot[bot] <chrisleekr-bot[bot]@users.noreply.github.com>
@chrisleekr-bot

chrisleekr-bot Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

bot workflow review — succeeded

🔍 Code review complete — 9 files, +768/-209.

Review — PR #88 (chore/51-docs-ci-drift-guards)

Summary

Second pass over PR #88 after the resolve cycle (0bf0164) addressed the three findings from the first review (3173541169, 3173541707, 3173542242). Scope is unchanged: drop paths: from the docs.yml pull_request: trigger, add scripts/check-docs-versions.ts and scripts/check-docs-citations.ts as pre-mkdocs build --strict gates, refresh the CLAUDE.md Documentation surface-to-page map, and (now) commit regression tests for both gates. No findings: every previously-flagged issue is resolved with a backing regression test, the new env-var seam (DOCS_CHECK_REPO_ROOT) is correctly scoped to test-only and leaves production behaviour byte-identical, and edge-case probing (double slash, . segments, complex engines.bun ranges) surfaced only cosmetic non-issues, no actual bugs. Ship-ready.

What was checked

Read in full:

  • .github/workflows/docs.ymlpull_request: no paths: filter, Verify… steps run before mkdocs build --strict, Deploy to GitHub Pages keeps its push / workflow_dispatch guard.
  • scripts/check-docs-versions.ts — including the new checkDockerfile line-scan via OVEN_RE and the DOCS_CHECK_REPO_ROOT env-var seam (lines 25–26).
  • scripts/check-docs-citations.ts — including the .. segment rejection (lines 95–103) and the matching env-var seam.
  • test/scripts/check-docs-versions.test.ts — 5 cases including the regression for the Dockerfile-comment case.
  • test/scripts/check-docs-citations.test.ts — 6 cases including the regression for src/sub/../app.ts:1.
  • package.json diff hunks — check:docs-versions / check:docs-citations wired into the check chain.
  • CLAUDE.md diff hunks — surface-to-page map matches the audience-first docs/{operate,build,use}/ layout, plus the new "CI-enforced doc gates" paragraph.

Cross-referenced:

  • Dockerfile.daemon:14 (FROM oven/bun:1.3.13 AS base), Dockerfile.daemon:193 (# /root is mode 700 in oven/bun:1.3.13), Dockerfile.orchestrator:13 (FROM oven/bun:1.3.13 AS base), .tool-versions (bun 1.3.13), package.json engines.bun (>=1.3.13) and packageManager (bun@1.3.13) — all agree on 1.3.13.
  • scripts/check-dockerfile-base-sync.ts — checks marker-delimited shared block byte-equality between the two Dockerfiles. Different concern from check-docs-versions.ts's pin verification; not redundant.
  • Resolve commit 0bf0164 diff vs 3455f33: confirmed exactly the three fixes flagged inline + matching test additions and the env-var seam (used solely to point the gate at fixture trees from Bun.spawnSync, with a comment explaining production unchanged).

Validation runs (locally):

  • bun run typecheck — clean.
  • bun run lint0 errors / 276 pre-existing warnings (none in the new scripts or tests).
  • bun run scripts/check-docs-versions.tsOK: every Bun version reference matches .tool-versions canonical 1.3.13.
  • bun run scripts/check-docs-citations.tsOK: every src/<path>:<line> citation in docs/ points at an in-range location (zero citations in docs/ today, so the gate runs against an empty input set on this PR).
  • bun test test/scripts/11 pass, 0 fail across both new test files.

Edge-case probing (manual, with disposable tmp-dir fixtures):

  • src//foo.ts:1 (double slash) — gate reports OK. Cosmetic only: path.join normalises double slashes, file existence check passes, no security or correctness implication.
  • src/./foo.ts:1 (single-dot segment) — gate reports OK. Cosmetic: . is a no-op in path resolution, so the citation still resolves to the same file.
  • engines.bun = "^2.0.0 || >=1.3.13" — gate fails with found '^2.0.0 || >=1.3.13', expected '1.3.13' because SEMVER.exec picks the first semver (2.0.0). This is correct behaviour for a tight-pin gate, not a bug; complex ranges defeat the gate's stated intent. The current engines.bun is >=1.3.13, which the script handles correctly.
  • Stale oven/bun:1.3.8 mention in a Dockerfile comment (the resolve regression case) — verified the new line-scan catches it via the committed test flags a Dockerfile comment whose oven/bun:<ver> has rotted.

Findings

No findings. Posting nothing as inline comments is deliberate (per review handler step 11 — "If you genuinely find nothing to flag, do NOT post any inline comments"). Every concern raised in the prior review iteration has been resolved with a regression test, and a fresh edge-case sweep surfaced no bugs.

Reasoning

A few things looked worth a closer look but did not pan out as findings:

  • SEMVER.exec(engines.bun)?.[0] extracts only the first semver. A future engines.bun = "^2.0.0 || >=1.3.13" would falsely fail. But this is the script's stated mandate — it pins tight to one canonical version. A maintainer who needs to broaden engines.bun is meant to bump .tool-versions first, or update the gate. Not a bug, and the current >=1.3.13 is handled correctly.

  • relPath.split("/").includes("..") only catches .. segments, not . or double slashes. Probed both: path.join normalises them, the file-existence check still resolves to the intended file, no security or correctness consequence (the script is read-only statSync). The PR's commit message and inline comment both call out the .. rejection explicitly as the intended scope, so this is by design.

  • Top-level OVEN_RE and BUN_SEMVER_RE regexes carry the g flag (shared lastIndex state). The script defensively resets lastIndex = 0 at the head of every loop that uses them. Single-threaded JS plus the explicit reset means there's no current footgun. Worth keeping in mind if a future refactor nests calls between checkDocFile and checkDockerfile, but not actionable today.

  • CITATION_RE extension allowlist (ts|tsx|mts|cts|mjs|cjs|js) excludes .sql, .json, .yaml. There are zero src/<path>:<line> citations in docs/ today (all src/ references are bare paths, intentionally out of scope), so this is not a current coverage gap. Worth revisiting only if SQL migration or JSON-schema citations with line anchors become a pattern; doesn't gate this PR.

  • Workflow permissions: contents: write at workflow scope. Only the deploy step needs write; the verify and build steps could run with read. Tightening would require a job split. Acceptable trade-off given the deploy step's if: guard already prevents PR runs from publishing.

  • Concurrency group docs-${{ github.ref }} with cancel-in-progress: false. PRs each get their own group via refs/pull/<n>/merge (no overlap). Pushes to main queue rather than cancel; with mkdocs gh-deploy --force the last push wins anyway. Fine.

  • Test helpers (makeFixture, runScript, fixtures/afterEach) are nearly identical between the two new test files. Could be DRY'd into a shared _helpers.ts. The two makeFixture shapes differ enough (citations needs only src + docs; versions needs .tool-versions + package.json + Dockerfiles + docs) that the duplication is borderline justified. Not worth gating the PR on a refactor.

  • The new DOCS_CHECK_REPO_ROOT env override could in principle be set by a malicious caller to point the gate at an arbitrary tree. The scripts are read-only (readFileSync / statSync / readdirSync), so the worst case is the gate reading files outside the repo. CI runs the scripts without setting the env var, so production behaviour is identical to before. The seam is correctly scoped.

Net: ship-ready. The first review's three findings are all addressed with regression tests; the resolve commit's env-var seam and .. rejection are minimal and well-scoped; the new tests give the gates a backstop against future regex tightening or extension-list edits. Final approval and merge remain a human action (FR-017).

cost: $3.2771 · turns: 41 · duration: 461s

@chrisleekr
chrisleekr merged commit be28b87 into main May 1, 2026
8 checks passed
@chrisleekr
chrisleekr deleted the chore/51-docs-ci-drift-guards branch May 1, 2026 14:55
@chrisleekr

Copy link
Copy Markdown
Owner

🎉 This PR is included in version 1.8.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(ci): docs workflow path filter skips code changes, leaving hardcoded Bun versions and src:line citations silently drifting

1 participant