Skip to content

fix(ci): stop the link check reporting the site against itself, and gate the heavy workflows - #326

Open
YuanYuYuan wants to merge 7 commits into
mainfrom
fix/lychee-self-links
Open

fix(ci): stop the link check reporting the site against itself, and gate the heavy workflows#326
YuanYuYuan wants to merge 7 commits into
mainfrom
fix/lychee-self-links

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three CI defects, all visible on this PR's own first run: the weekly link check reports the site's own URL as broken, it opens a new issue every time it does, and a one-line edit to its config started the full ROS matrix.

Key Changes

commit change
fix(ci) .lychee.toml excludes zettascalelabs.github.io/hiroz
ci ci.yml, test.yml, rmw-zenoh-rs.yml trigger from an allow-list; Check Formatting and hu-docs-repro move to their own workflows
ci(docs-links) the link check reuses and closes one report issue
ci allow-list .config/nextest.toml and .cargo/config.toml; record why a required check must not sit on top of a path filter
ci a guard that fails when a changed file reaches no workflow
ci clang-format and vale run as pre-commit hooks

1. The link check asked GitHub Pages about its own pages

mkdocs.yml sets site_url: https://zettascalelabs.github.io/hiroz/. MkDocs writes that URL into site/sitemap.xml and into the rel="canonical" tag of every page, and the check runs over the freshly built site/. Every run therefore asked GitHub Pages about several hundred of its own URLs.

today with the patch
links to third-party hosts checked checked — unchanged
hand-written self-links in docs/ none exist none exist — unchanged
site/sitemap.xml entries several hundred requests to GitHub Pages excluded
rel="canonical" of every page one request per page excluded
what those requests test the previous deployment nothing
a page added in this build 404 — reported as broken not reported
a live page under the request burst 503 — reported as broken not reported

Grepping docs/ and mkdocs.yml finds that URL only on the site_url line, so every occurrence in the built site is machine-generated. The exclusion removes no hand-written link from coverage.

2. paths-ignore is a deny-list, so it ran everything it did not name

A deny-list has to name every file that must not build. Anything it misses runs the full matrix. .lychee.toml was one such file — this PR's first commit started 4 interop distros, 4 Python distros, ROS tests, WASM, SHM, coverage and both macOS jobs, for a link-checker comment.

test.yml and rmw-zenoh-rs.yml also still ignored book/**, a directory the MkDocs migration removed, so every docs/ edit ran both of them.

Each of the three now lists what it actually builds from. Two jobs had a wider interest than the build inputs, so they move out rather than widen the list back:

job moved to why
Check Formatting lint.yml, no path filter the pre-commit check covers .rs, .toml, .yaml, .md, .py and .nix across the tree, so a config-only change must still reach it. It is also the cheapest job in CI (~1 min)
hu-docs-repro hu-docs.yml, which adds docs/tools/** it replays every documented hu command. Under paths-ignore a docs/ edit could change those commands without ever running it

docs.yml gains .lychee.toml, crates/*/examples/** and check-example-coverage.nu — its own steps read all three, and it was not triggered by a change to any of them.

Resulting triggers, simulated over the GitHub path-filter semantics (* stops at /, ** does not, a later ! overrides an earlier match):

file changed ci test rmw-zenoh-rs hu-docs docs mkdocs-preview lint
.lychee.toml · · · · RUN · RUN
.github/workflows/docs-links.yml · · · · · · RUN
crates/hiroz/src/lib.rs RUN RUN RUN RUN · · RUN
crates/hiroz/README.md · · · · RUN RUN RUN
docs/tools/hu.md · · · RUN RUN RUN RUN
docs/index.md · · · · RUN RUN RUN
Cargo.toml RUN RUN RUN RUN · · RUN
.config/nextest.toml RUN RUN RUN RUN · · RUN
.cargo/config.toml RUN RUN RUN RUN · · RUN
scripts/test-go.nu RUN RUN RUN RUN · · RUN
codecov.yml RUN · · · · · RUN
mkdocs.yml · · · · RUN RUN RUN

Writing the allow-list surfaced two files that the deny-list had also missed, in the opposite direction. .config/nextest.toml sets slow-timeout, test-threads and fail-fast for every nextest leg in ci.yml and test.yml; .cargo/config.toml carries the alias the WASM plugin build uses. Neither ran a single test it changes. Both are now listed.

This shape is what GitHub documents, with one condition attached

Use the paths filter when you want to include file path patterns or when you want to both include and exclude file path patterns. Use the paths-ignore filter when you only want to exclude file path patterns.
Workflow syntax for GitHub Actions

These three workflows both include and exclude (crates/** then !crates/**/*.md), which paths-ignore cannot express — the two filters cannot be combined for one event. The negation also follows the documented last-match-wins rule, and the docs require at least one positive pattern alongside any !, which every list here has.

Important

The condition is branch protection. A workflow a path filter skips reports nothing, and a required check that never reports leaves the PR pending forever — the single most-reported problem with path filters (community#54877). Gating with a job-level if: avoids it, because a job skipped that way reports success.

main requires no checks today — required_status_checks.contexts and .checks are both empty — so nothing here can wedge a PR. Each filtered workflow now carries that condition as a comment, so whoever adds a required check reads it at the point they would break it.

The allow-list's own cost, and the guard that removes it

An allow-list has to be extended when work lands somewhere new, and a missed entry is a silent skip rather than a wasted run — a green board and an empty board look identical. The layout bounds the exposure (401 of 431 tracked source files are under crates/, 29 under scripts/, 1 under nix/, all allow-listed prefixes), but bounding is not closing.

lint.yml has no path filter, so it sees every change. scripts/check-ci-path-coverage.py now diffs the PR against its merge base and requires each changed file either to match some workflow's paths, or to appear in an EXEMPT list with a reason. Both outcomes are fine; silence is not.

The parse is strict in two ways, each closing a way the guard itself could rot quietly:

condition why it is an error rather than a default
a workflow has a paths: key but no pattern parses an empty filter would silently cover nothing, and the guard would pass by covering nothing
a workflow's push and pull_request lists differ Actions rejects YAML anchors, so each event carries its own copy. Two copies drift, and both look maintained

Running it over the whole tree found six files that reach nothing: .clang-format, .vale.ini and four .vale/styles/ files. Both tools shipped in the devshell and no job ran either. Rather than exempt them, the last commit wires them in — see below.

Controls, all four measured:

case result
all 830 tracked files rc=0, "every one triggers a workflow or is exempt"
a file in a new top-level bindings/ rc=1, names the file
ci.yml's paths list emptied rc=1, "has a paths: key but no patterns parsed"
one entry deleted from ci.yml's pull_request copy only rc=1, "its paths: lists disagree between events"

It is written in Python rather than Nushell like the rest of scripts/ because it runs on every pull request and python3 is already on the runner; installing nushell would cost more than the check.

3. One report issue, not one per run

Every failing run opened another issue with the identical title, and closing one left the rest open saying the same thing. The run now looks for an open issue with that title first: it updates that one when links fail, opens a new one only when none is open, and closes the open one with a comment when every link resolves.

Wiring the two tools the guard found

They join the pre-commit hook set that lint.yml already runs on every change, so they also run locally on commit rather than only in CI.

vale could not run at all

.vale.ini named Vocab = ros-z — a name the rename to hiroz left behind — and the vocabulary sat in <styles>/Vocabularies/, the Vale 2 location that Vale 3 replaced with <styles>/config/vocabularies/. Either alone makes vale exit 2 before linting anything, which is why it was never wired.

With both fixed it reported 412 alerts:

rule count disposition
Vale.Spelling 333, over 50 distinct words nearly all terms this project uses — hiroz, hu, hiroz_msgs, hiroz_py among them, never added when ros-z was renamed. 50 terms go into the vocabulary and clear all 333
write-good.Passive 62 removed. Passive voice is frequently the correct choice in reference prose, so this is a style opinion rather than a defect
write-good.Weasel 16 removed, same reason
write-good.TooWordy 1 removed, same reason

.vale.ini records why only spelling is enforced: it is the rule with an objective answer.

Two of the 50 "words" were not words. docs/tools/why-hu.md wrote **H**iroz **U**nion, and mid-word bold splits both words in two, so vale saw iroz and nion. Adding those fragments to the vocabulary would mask real typos, so the line reads **Hiroz Union** instead.

clang-format

688 violations, of which 464 are in crates/hiroz-go/hiroz/hiroz_ffi.h — generated by cbindgen from crates/hiroz/build.rs. Formatting it would be undone by the next cargo build --features ffi and show up as a spurious diff, so the hook excludes it and the exclusion carries that reason. The remaining 224 are in five hand-written files, reformatted here:

crates/hiroz-go/hiroz/callback_bridge.c    |  16 +---
crates/rmw-zenoh-rs/binding.hpp            |   4 +-
crates/rmw-zenoh-rs/include/rmw_bridge.h   | 142 ++++++++++-------------------
crates/rmw-zenoh-rs/include/serde_bridge.h |  23 ++---
crates/rmw-zenoh-rs/src/serde_bridge.cc    |  43 ++++-----

Note

Those headers are consumed by bindgen in rmw-zenoh-rs's build.rs, which needs ROS headers and cannot be built outside CI. Reformatting changes no token, but the claim that it builds rests on this PR's rmw_zenoh_rs Tests job, not on a local run.

What fails without this

  • #323: [503] https://zettascalelabs.github.io/hiroz/experimental/shm/ (at site/sitemap.xml:64:15). That page returns 200 on request, so the report is a false positive from rate limiting.
  • This PR's first commit, one line in .lychee.toml: gh pr checks listed 27 checks including all four interop distros, all four Python distros, ROS tests, WASM, SHM and coverage.

Verification

claim how it was checked
the reported page is not broken curl -o /dev/null -w '%{http_code}'200
the exclusion regex survives TOML parsing and matches only the intended host parsed .lychee.toml with tomllib, matched the #323 URL and the site root; docs.ros.org and github.com/ZettaScaleLabs/hiroz/... stay checked, and a lookalike with the dots replaced does not match
the new triggers behave as claimed the table above, from a simulation of GitHub's filter semantics over the committed paths lists
every workflow still parses, and lints yaml.safe_load on all 12 files, then pre-commit run yamllintPassed
the allow-list covers where source actually lives git ls-files over .rs/.nu/.sh/.py/.go/.toml/.nix: 401 under crates/, 29 under scripts/, 1 under nix/, the rest root-level configs — which is how .config/nextest.toml and .cargo/config.toml were found
the filter shape matches GitHub's guidance the two quotes above, from the workflow-syntax reference and community#54877
the coverage guard detects, and does not just pass the four controls above, including two that break the guard's own parser
vale and clang-format pass, and would fail pre-commit run --all-filesrc=0, all hooks pass. Then a deliberate typo in docs/index.md → vale rc=1 naming Tpyo, teh, wrod; a deliberately misformatted function in serde_bridge.cc → clang-format rc=1
the generated header is not reformatted git status on crates/hiroz-go/hiroz/hiroz_ffi.h after a full hook run → unchanged

The negative case cannot be observed on this PR: the allow-list includes .github/workflows/ci.yml, which this PR edits, so the heavy matrix correctly runs here. Only a later config-only change shows the skip in production.

Breaking Changes

None.

MkDocs writes site_url into site/sitemap.xml and into the rel=canonical tag
of every page, so each run asks GitHub Pages about several hundred of its own
URLs. That check queries the previous deployment, so a page added in the
current build always 404s, and the request burst gets rate-limited.

Fixes #323: a 503 reported on a page that is live and returns 200.
ci.yml, test.yml and rmw-zenoh-rs.yml selected work with paths-ignore. A
deny-list has to name every file that must not build, so every file it does
not name runs the full ROS matrix. .lychee.toml was one: a one-line comment
in it started 4 interop distros, 4 Python distros, ROS tests, WASM, SHM,
coverage and both macOS jobs. test.yml and rmw-zenoh-rs.yml also still
ignored book/**, a directory the MkDocs migration removed, so every docs/
edit ran both.

Each of the three now lists what it actually builds from.

Two jobs had a wider interest than the build inputs, so they move out rather
than widen the list back:

- Check Formatting -> lint.yml, no path filter. The pre-commit check covers
  .rs, .toml, .yaml, .md, .py and .nix across the tree, so a config-only
  change must still reach it. It is also the cheapest job in CI.
- hu-docs-repro -> hu-docs.yml, which adds docs/tools/**. It replays every
  documented hu command, and under paths-ignore a docs/** edit could change
  those commands without ever running it.

docs.yml gains .lychee.toml, crates/*/examples/** and
check-example-coverage.nu, which its own steps read.
The weekly check opened a new issue on every failing run, each with the
identical title. Closing one left the others open saying the same thing.

The run now looks for an open issue with that title first. It updates that
one when links fail, opens a new one only when none is open, and closes the
open one with a comment when every link resolves.
@YuanYuYuan YuanYuYuan changed the title fix(ci): stop the link check querying the site's own published URL fix(ci): stop the link check reporting the site against itself, and gate the heavy workflows Aug 25, 2026
.config/nextest.toml sets slow-timeout, test-threads and fail-fast for
every nextest leg in ci.yml and test.yml, and .cargo/config.toml carries
the alias the WASM plugin build uses. Neither was listed, so a change to
either ran no test that it changes.

Also record why a required status check must not be added on top of these
filters: a workflow the filter skips reports nothing, and a required check
that never reports blocks the pull request. main requires no checks today.
An allow-list only skips what it does not name, so work landing somewhere
new is silently never built. That is the one cost the allow-list trades
for, and it is invisible: a green run and a skipped run look the same.

lint.yml has no path filter, so it sees every change. It now diffs the pull
request against its merge base and requires each file to match some
workflow's paths, or to appear in the script's EXEMPT list with a reason.
Both outcomes are fine; silence is not.

The parse is strict in two ways, each covering a way this could rot
quietly. A workflow with a paths: key that yields no patterns is an error,
not an empty filter that silently covers nothing. And because Actions
rejects YAML anchors, push and pull_request each carry their own copy of
the list -- the script requires the copies to stay identical.

Running it over the tree found six files that reach nothing: .clang-format,
.vale.ini and four .vale/styles files. Both tools ship in the devshell and
no job runs either, so they are exempt with that stated as the reason.
The coverage guard reported .clang-format and .vale.ini as files no job
reads. Both are now read, by the hook set that lint.yml already runs on
every change -- so they also run locally on commit rather than only in CI.

vale could not run at all. .vale.ini named Vocab = ros-z, a name the rename
to hiroz left behind, and the vocabulary sat in the Vale 2 location
(<styles>/Vocabularies/) that Vale 3 replaced with <styles>/config/
vocabularies/. Either alone makes vale exit 2 before it lints anything.

With both fixed it reported 412 alerts. 333 were spelling, over 50 distinct
words, and nearly all were terms this project uses -- hiroz, hu, hiroz_msgs
and hiroz_py among them, never added when ros-z was renamed. 50 terms go
into the vocabulary and clear all 333.

The other 79 were write-good Passive, Weasel and TooWordy. Passive voice is
frequently the correct choice in reference prose, so those three rules are
a style opinion rather than a defect; they are removed, and .vale.ini
records why. Spelling is the rule with an objective answer.

'iroz' and 'nion' were not words: docs/tools/why-hu.md wrote
'**H**iroz **U**nion', and mid-word bold splits both words in two. Adding
those fragments to the vocabulary would mask real typos, so the line reads
'**Hiroz Union**' instead.

clang-format found 688 violations. 464 are in
crates/hiroz-go/hiroz/hiroz_ffi.h, which cbindgen generates from
crates/hiroz/build.rs -- formatting it would be undone by the next build,
so the hook excludes it. The remaining 224 are in five hand-written files
and this reformats them.
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ZettaScaleLabs.github.io/hiroz/pr-preview/pr-326/

Built to branch gh-pages at 2026-08-25 10:06 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Refines CI triggering, link-report handling, and repository-wide lint enforcement.

Changes:

  • Replaces heavy-workflow deny-lists with guarded allow-lists.
  • Reuses and closes recurring broken-link issues.
  • Adds Vale/clang-format hooks and applies formatting fixes.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
.lychee.toml Excludes generated self-links.
.vale.ini Enables project spelling checks.
.vale/styles/config/vocabularies/hiroz/accept.txt Adds project vocabulary.
.vale/styles/write-good/Passive.yml Removes passive-voice rule.
.vale/styles/write-good/TooWordy.yml Removes wordiness rule.
.vale/styles/write-good/Weasel.yml Removes weasel-word rule.
.github/workflows/ci.yml Adds allow-list and moves jobs.
.github/workflows/docs-links.yml Reuses recurring report issues.
.github/workflows/docs.yml Expands documentation triggers.
.github/workflows/hu-docs.yml Extracts documentation reproduction job.
.github/workflows/lint.yml Adds lint and path-coverage jobs.
.github/workflows/rmw-zenoh-rs.yml Adds workflow allow-list.
.github/workflows/test.yml Adds workflow allow-list.
scripts/check-ci-path-coverage.py Guards workflow path coverage.
nix/pre-commit.nix Adds Vale and clang-format hooks.
docs/tools/why-hu.md Fixes emphasized product name.
crates/hiroz-go/hiroz/callback_bridge.c Applies C formatting.
crates/rmw-zenoh-rs/binding.hpp Reorders includes.
crates/rmw-zenoh-rs/include/rmw_bridge.h Applies C++ formatting.
crates/rmw-zenoh-rs/include/serde_bridge.h Applies C++ formatting.
crates/rmw-zenoh-rs/src/serde_bridge.cc Applies C++ formatting.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/hu-docs.yml
scripts/test-release-version-semantics.sh reads .github/workflows/
release.yml and asserts against it. release.yml itself runs only on a v*
tag, so no pull request exercises it and that script is its one check
before a tag is pushed.

Under the old deny-list an edit to release.yml ran ci.yml, which owned
that script. Moving the job to hu-docs.yml dropped it: neither event
listed release.yml, so the check silently stopped running.

hu-docs.yml now lists it.

The coverage guard should have caught this and did not. Its EXEMPT list
carried a blanket .github/**, which swallowed every file under that
directory -- including the one whose only check had just disappeared. The
blanket is gone; a workflow that no path filter reaches now needs its own
line and its own reason.

Removing it immediately found a second instance: mkdocs-preview.yml has a
paths: filter that omitted its own filename, so editing that workflow
triggered nothing and it could not test itself. It now lists itself, as
every other filtered workflow does.
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.

2 participants