Skip to content

ci: validate every internal anchor in the built site - #38

Merged
mairas merged 1 commit into
mainfrom
feat/check-anchors
Aug 13, 2026
Merged

ci: validate every internal anchor in the built site#38
mairas merged 1 commit into
mainfrom
feat/check-anchors

Conversation

@mairas

@mairas mairas commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Anchors are generated from heading text, so editing a heading silently breaks
every link pointing at it — including links on pages nobody touched. This adds
check-anchors to the existing build job, from
halos-org/docs-tools pinned to
v0.1.0 in pyproject.toml, so the same command runs here and on a laptop.

print_page/ is excluded. It is the single-page export from
mkdocs-print-site-plugin, which rewrites every fragment into ids of its own.
Measured on this branch: all 690 links the exclusion drops are page-local
fragments of that one generated page, so no authored link is silenced, and the
36 content pages are clean either way.

What this catches, and what it does not

mkdocs 1.6 has validation.links.anchors, which this repo does not enable. It
is worth knowing where the two differ before deciding this step earns its place.

mkdocs validation.links.anchors check-anchors
reads source markdown built HTML
markdown links with a fragment yes yes
raw HTML <a href> no yes
root-absolute links no yes
message names source file and link built HTML paths

Both were measured on this branch. Renaming ## Certificate warning in
getting-started/first-boot.md is caught by both, and mkdocs gives the better
message — it names user-guide/troubleshooting.md and the source link, where
the checker names user-guide/troubleshooting/index.html. But a raw
<a href="user-guide/dashboard/#no-such-anchor"> and a root-absolute
[abs](/user-guide/dashboard/#no-such-anchor) both pass mkdocs with --strict
and anchors: warn, and both fail here.

This repo currently has zero of either form, so today the extra coverage is real
but unexercised. Tracked as
halos-org/docs-tools#13,
which also covers the checker's docstring still claiming mkdocs validates
nothing.

On the numbers

The step reports "Checked 1905 anchor links across 36 pages". That is not a
coverage figure: 1890 of the 1905 are same-page fragments — Material's heading
permalinks, page table of contents and sidebar navigation, generated from the
headings they point at and therefore unable to break independently. 15 are
cross-page, and those are what the step protects.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@mairas, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 773714e7-31ad-4839-ad63-395da8573837

📥 Commits

Reviewing files that changed from the base of the PR and between f0644eb and ca5a0c3.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • pyproject.toml

Comment @coderabbitai help to get the list of available commands.

@mairas
mairas force-pushed the feat/check-anchors branch 2 times, most recently from 2195201 to 9d59b2b Compare August 13, 2026 12:31
@mairas

mairas commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mairas

mairas commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Code review — 4 personas

Scope: feat/check-anchors against main — one CI step, one dependency pin, the
lock. Reviewers: correctness, testing, maintainability, project-standards.

The review changed what this PR claims more than what it does. Three
measurements, each reproduced before acting on it.

The premise was wrong, then wrong in the other direction

The original comment said mkdocs build --strict "does not validate them". That
was true before mkdocs 1.6; this repo is on 1.6.1. Setting
validation: links: anchors: warn makes the strict build fail on the same
deliberate break, with no print_page exclusion needed, because mkdocs reads
source markdown rather than generated output. It also gives the better message:
renaming ## Certificate warning in getting-started/first-boot.md gets

WARNING - Doc file 'user-guide/troubleshooting.md' contains a link
'../getting-started/first-boot.md#certificate-warning', but the doc
'getting-started/first-boot.md' does not contain an anchor '#certificate-warning'.

against check-anchors' user-guide/troubleshooting/index.html -> ../../getting-started/first-boot/#certificate-warning.
The source path is the one you can edit.

That looked like grounds to drop this step, and I filed
halos-org/docs-tools#13
saying so. The testing reviewer found the counter-evidence and I reproduced it.
Adding to docs/index.md:

<a href="user-guide/dashboard/#raw-html-no-such-anchor">raw</a>
[abs](/user-guide/dashboard/#abs-no-such-anchor)

mkdocs build --strict with anchors: warn exits 0 — the absolute link
draws only an INFO, the raw HTML nothing, because neither is a markdown link.
check-anchors reports both and exits 1. The tools are complementary:
mkdocs reads source, this reads built HTML.

Both repos have zero links of either form today, so the extra coverage is real
and currently unexercised. That makes it a documentation problem, and the CI
comment now records the boundary so nobody deletes the step on discovering the
config option. #13 is
updated with the correction.

"1905 anchor links" is not a coverage figure

Classifying what the checker counts: 1890 same-page fragments, 15 cross-page.
The 1890 are Material's heading permalinks, page table of contents and sidebar
navigation — generated from the headings they point at, so they cannot break
independently. The named failure mode applies to the 15. The PR body quoted 1905
as if it were protection; corrected.

Fixed

# Issue Reviewer Conf
1 The anchor step sat before the cmp loop over docs/migrate/*.sh. A job stops at its first failing step, and this is now the step most likely to be red, so a broken anchor suppressed an unrelated check. Moved last. correctness 0.85
2 The comment carried 690 and 36 — measurements taken while making the change, wrong after the next page is added, with nothing to recompute them. Replaced with the durable property the testing reviewer measured: all 690 dropped links are page-local fragments of the generated page, so no authored link is silenced. project-standards, maintainability 0.80
3 Commit subject was 52 characters against the 50 cap. project-standards 0.85

Not fixed, and why

The dependency sits in [project] dependencies, so deploy.yml installs it
too
(correctness, 0.65). A rename or a deleted tag on halos-org/docs-tools
would break publishing, not just the check. A dependency group would contain it.
Left alone because it interacts with the bigger question above — if the step
stays, the containment is worth doing, and that is one decision not two.

Nothing makes this block a merge (testing, 0.90, pre-existing). gh api repos/halos-org/docs/branches/main/protection returns no required checks and no
rulesets, and deploy.yml builds and publishes on push to main without
depending on CI. So a broken anchor is advisory here and the site publishes
regardless. That is true of the existing mkdocs build --strict and shellcheck
steps too, so it is not this PR's regression — but it bounds what "catch broken
anchors in CI" delivers. Adding build to required status checks fixes it for
every step at once.

uv sync has no --locked in both ci.yml and deploy.yml (testing,
project-standards). The lock pins 7f09d05, which is what v0.1.0 points at
today; without --locked a re-pointed tag plus a refreshed lock would run
different code silently. One word, but it belongs in a change that touches both
workflows.

Coverage

The failing path has never run in CI — only locally, reproduced independently by
two reviewers. The checker's own behaviour is covered by 13 tests in
halos-org/docs-tools at the pinned SHA, including both exit-2 guards. The
exclusion cannot widen silently: fnmatch is prefix-anchored, so only a page
created under a top-level print_page/ would match, and that is visible in the
diff that adds it.

Pre-existing, unrelated, worth knowing: docs/overrides/ sits inside the docs
tree, so mkdocs copies the raw Jinja template to site/overrides/main.html and
serves it with an unrendered href="{{ page.url_to_print_page }}".

Verdict

Ready with a decision. Nothing here is broken — CI is green, the step catches
what it claims, and the three findings are fixed. The open question is whether
this repo wants the git dependency for coverage it does not currently exercise,
or validation.links.anchors: warn for free, or both.

@mairas
mairas merged commit de76a65 into main Aug 13, 2026
2 checks passed
@mairas
mairas deleted the feat/check-anchors branch August 13, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant