fix(lychee): accept 429 and exclude four checker-blocked hosts - #303
Conversation
Five of the eight errors on claude-code-plugins' rolling link-check report were not dead links. Each was verified alive before being handled here, and each fix is the narrowest one that matches the actual cause. A 429 is the SERVER rate-limiting the checker, never evidence a link is dead, and it lands on whichever host the shared CI runner IP happens to be throttled against on a given run — so it names a different healthy URL each time. The accepted-status range now carries 429. The range restates lychee's own documented default (`--accept` help: `[default: 100..=103,200..=299]`, read from the pinned binary rather than recalled) because `accept` REPLACES the default rather than extending it, so spelling it out is what keeps the change from silently narrowing what counts as success. Excluding each rate-limited host was rejected: it stops checking a link that is fine, and the next run throttles a different one. Four hosts are excluded, all of them checker-side limitations on links proven to be alive: - dl.acm.org and queue.acm.org answer 403 even with a full browser User-Agent, so no header tuning reaches them. A DOI is also the most stable citation form there is. - docs.genius.com answers 403 to the checker and 200 with a browser User-Agent — the documented bot-block case this list already exists for. - www.ntia.gov is reported as "SSL certificate not trusted", but the chain verifies locally: `openssl s_client` returns `Verify return code: 0 (ok)` against a Cloudflare TLS Issuing ECC CA intermediate, and curl fetches it 200 under strict verification. That is a trust store failing to complete an ECC chain, not an untrustworthy host. Every entry records the verification that earned it so a future reader can re-test instead of trusting the list, and the header now says plainly that this list only ever holds live links the checker cannot reach. The Genius assertion is a line-anchored row count rather than a substring absence. The excluded URL is the host root, so every sibling path that must stay checked contains it as a prefix — assert_not_contains could only pass by deleting the sibling control that proves the boundary. No linked issue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C
|
Claude finished @kyle-sexton's task in 3m 2s —— View job Claude finished the code review
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e418063612
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
The rolling link-check report listed **8 errors**. Only **3 were dead
links** — the other 5 are live
URLs the checker cannot reach, and their fix lands upstream (see below).
Every replacement here was
verified against the live target, not inferred from the URL shape.
## The three dead links
**`docs/adr/0002-…md`** — GitHub retired the
`repositories/configuring-branches-and-merges…/managing-protected-branches/`
path for the required-status-checks troubleshooting page. It now lives
under
`pull-requests/how-tos/merge-and-close-pull-requests/`. Verified 200
with **no redirect**, H1 reads
"Troubleshooting required status checks", and it still covers the
check-never-reports case the ADR
cites it for ("Associated checks stay in a 'Pending' state and block
merging").
An independent fresh-context lookup caught that my first replacement
here was itself a 301 — the
`collaborating-with-pull-requests/…` path redirects to the `how-tos/`
one. Both resolve today, but a
redirect is a second thing that can be retired, and lychee already hints
to prefer resolved URLs, so
the second commit swaps in the canonical target. Verified both
directions: the old path returns 301
with that Location, the new one returns 200 with none.
**`plugins/dometrain/README.md`** — Dometrain moved its plans page from
`/pro/` to `/dometrain-pro/`.
Verified 200, `<title>Dometrain Plans - Dometrain</title>`. The link
text stays "Dometrain Pro"
because the slug and the product name both still are.
**`plugins/source-control/skills/babysit-prs/reference/freshness.md`** —
the most interesting of the
three. `graphql/reference/enums` did not 404; it became a **navigation
index** and no longer carries
any enum definitions at all, which is why the failure was `Cannot find
fragment` rather than a dead
page. GitHub split the GraphQL reference by domain, so
`MergeStateStatus` now lives on the `pulls`
page. The replacement was verified structurally, not just by status
code: `id="enum-mergestatestatus"`
is present in the **served HTML** (so lychee's fragment check resolves
it, rather than the anchor
being JS-injected), and the page carries both descriptions this doc
quotes verbatim — "The head ref
is out of date" and "The merge is blocked".
## The other five are not content defects, and are fixed upstream
`lychee.toml` is a **`managed` component** for this repo per
`standards/distribution/sync-manifest.yml`,
so editing it here would be silently overwritten by the next sync. The
config half of this report is
therefore **melodic-software/standards#303**:
- **`www.gnu.org/software/coreutils/…` (429)** — verified 200. A 429 is
the server rate-limiting the
checker and lands on whichever host the shared runner IP is throttled
against that run, so the fix
is `accept`-ing 429 rather than excluding a healthy host that would just
be replaced by a
different one next run.
- **`dl.acm.org` and `queue.acm.org` (403)** — 403 even with a full
browser User-Agent; no header
tuning reaches them.
- **`docs.genius.com` (403)** — 200 with a browser User-Agent; the
documented bot-block case.
- **`www.ntia.gov` (SSL not trusted)** — the chain verifies locally
(`openssl s_client` →
`Verify return code: 0 (ok)`, curl 200 under strict verification). A
trust store failing an ECC
chain, not an untrustworthy host.
This PR merging alone will not clear the report; #303 has to land and
sync. Flagging that plainly
rather than letting a half-clear look like a regression.
## Verification
Run with the real `lychee.toml` plus the proposed upstream config, over
all seven files the report
named:
```text
🔍 118 Total 🔗 116 Unique ✅ 114 OK 🚫 0 Errors 👻 4 Excluded
```
All 8 reported errors resolved. Also run against this repo's gates:
- `scripts/check-changed-skills.sh origin/main` — 1 skill checked, 0
failed
- `scripts/check-contract-slice-prune.sh --check-diff origin/main` —
pass (no `docs/topics/` path is
touched; that file's two ACM URLs are handled by exclusion, not by
editing it)
- `scripts/check-changelog-parity.sh --check-bump origin/main` — pass.
**No plugin version bump**: a
corrected external URL in a reference doc changes no behavior contract,
no gate requires one, and
bumping `source-control` would collide with the in-flight bump on #1782
— the collision class
tracked as #1746.
- `markdownlint-cli2`, `typos` — clean
## Related
- Fixes #640
- melodic-software/standards#303 — the upstream half; owns `lychee.toml`
for this repo
- #1746 — the concurrent version-bump collision class, the reason this
change deliberately bumps
nothing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The exclusion assertions only inspect `--dump` output, so nothing exercised `accept`'s effect on a real response and the 429 behavior could have regressed silently. A dependency-free Python responder binds an ephemeral loopback port and answers 429 on one path and 404 on a control path. The real lychee binary runs against scratch markdown for each, using the repository's own `lychee.toml`: the 429 case must exit 0 and report an OK link, the 404 control must exit non-zero and name the rejected status. The config excludes loopback, so `--include` re-admits the fixture URL and a `--dump` assertion proves it is genuinely checked rather than passing vacuously through the exclusion. Mutation-verified: with `"429"` removed from `accept` in `lychee.toml`, the suite fails with `accepted 429 exits 0 — exit expected 0 got 2` and the OK-count assertion; restoring it returns the suite to green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STASxNiqEm7doy2Z65un4H

melodic-software/claude-code-plugins' rolling link-check report(ccp#640) listed 8 errors.
Three were genuinely dead links and are fixed in that repo. Five were not link defects at all —
every one is a live URL the checker cannot reach — and
lycheeis amanagedcomponent for thatrepo, so their fix belongs here rather than in a materialization that the next sync would overwrite.
Every URL below was verified alive before being handled, and each fix is the narrowest one that
matches the actual cause.
429 is accepted, and no rate-limited host is excluded
A 429 means the server is rate-limiting the checker — never that the link is dead. It also lands
on whichever host the shared CI runner IP happens to be throttled against on a given run, so it
names a different healthy URL each time. Excluding the host that happened to lose that lottery
(
www.gnu.org/software/coreutils/..., verified 200) would stop checking a link that is fine and letthe next run throttle a different one, so
acceptcarries 429 instead.The range spells out lychee's own documented default rather than only listing 429:
acceptreplaces the default set, it does not extend it — so writing the default back is whatkeeps this from silently narrowing what counts as success. The default was read from the pinned
binary's own
--accepthelp ([default: 100..=103,200..=299]), not recalled.Four hosts excluded, all checker-side limitations
dl.acm.org(DOI)queue.acm.orgdocs.genius.comwww.ntia.gov(SBOM PDF)openssl s_clientreturnsVerify return code: 0 (ok)against a Cloudflare TLS Issuing ECC CA intermediate, and curl fetches it 200 under strict verificationThe NTIA entry is the one worth a second look on review: the report's wording ("SSL certificate not
trusted") reads like a security signal, but the chain is fine off the runner, so this is a trust
store failing to complete an ECC chain rather than an untrustworthy host. It is recorded that way in
the file, with a note to re-test before removing — a genuinely bad chain would fail locally too.
The list header now says plainly what the list is: checker-side limitations on links verified to
be alive, never a dead link silenced, and each entry records the verification that earned it so a
future reader can re-test rather than trust the list.
One test-shape correction
The Genius assertion is a line-anchored
assert_row_count, notassert_not_contains. The excludedURL is the host root, so every sibling path that must stay checked (
docs.genius.com/example)contains it as a prefix —
assert_not_containscould only have passed by deleting the siblingcontrol that proves the boundary, which is the assertion doing the opposite of its job. The other
three new exclusions have no such collision and use the existing idiom unchanged.
Verification
bash components/lychee/lychee.test.sh— 53 assertions, 0 failures, exit 0 (was 45; 8 added:one exclusion proof and one still-checked sibling per new host)
this list is tested
shellcheck components/lychee/lychee.test.sh— cleanmarkdownlint-cli2,typos,gitleaks,editorconfig— all clean via the pre-commit hooklychee.tomlparses as valid TOML;acceptand the 20 exclude entries read back as intendedRelated
report that surfaced these five; its three genuine dead links are fixed separately in that repo
distribution/sync-manifest.yml—lycheeismanagedformelodic-software/claude-code-plugins(and for
.github,ci-runner,ci-workflows,dotfiles), so this change reaches them throughthe normal sync
🤖 Generated with Claude Code
https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C