Skip to content

fix(lychee): accept 429, 5xx and timeouts in the online link check - #834

Merged
kyle-sexton merged 3 commits into
mainfrom
fix/lychee-transient-tolerance
Oct 21, 2026
Merged

kyle-sexton merged 3 commits into
mainfrom
fix/lychee-transient-tolerance

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Refs: melodic-software/claude-code-plugins#8730
Refs: melodic-software/ci-workflows#739

Summary

The online link check failed on transient responses: a 429, a 5xx such as 502, 503 or 522, or a timeout says nothing about whether a link is dead. The owner decided on 2026-10-21 that these responses must not fail the run, and must still appear in a read-only report. Dead links (404, 410 and every other 4xx) keep failing. lychee stays on v0.24.2 and lychee-action stays on v2.9.0, the latest stable releases.

Fix

  • lychee.toml:
    • Sets accept = ["100..=103", "200..=299", "429", "500..=599"] and accept_timeouts = true.
    • Replaces the comment that explained why 429 was not accepted. The new comment gives the reason for the change and its cost: an accepted 429 or 5xx is not retried, and its fragment is not checked. It names lycheeverse/lychee#2324 as the trigger to revisit 5xx.
  • components/lychee/lychee.test.sh: the status-code block now asserts the owner's rule.
    • 200, 429, 500, 503 and 599 pass the run, each counted as 1 OK.
    • An accepted 429 is requested once.
    • 404 and 410 fail the run with Rejected status code.
    • A timeout passes the run and is still reported as 1 Timeouts, while the same request with --accept-timeouts=false fails.
  • components/lychee-online/README.md: the prose now matches the new behavior.

Basis for each setting, all at lychee v0.24.2:

  • accept replaces the default, so 100..=103 and 200..=299 are restated. The --accept default is in the README. Status::new in status.rs#L90-L97 returns Ok only for a code in the accepted set. A local probe with --accept 429 alone rejected a 200.

  • A rejected 5xx is never retried, so accepting 5xx is the only way to tolerate it. The basis is retry.rs#L97-L100. In the probe, 502 and 503 each got one request with --max-retries 2.

  • An accepted 429 is a success whose fragment is not checked. The basis is the comment and the response.status.is_success() guard in website.rs#L137-L142. In the probe, an accepted 429 got one request.

  • accept_timeouts exists from v0.24.0. Basis: the lychee-v0.24.0 release notes (#2063). check.rs#L113-L117 uses is_success_ignoring_timeouts when it is set.

  • Repin, in the same merge. The claude-code-plugins caller (components/lychee-online/targets/claude-code-plugins.yml) moves from v0.55.0 (3a2c748) to v0.59.0 (4aff99320ef484a7beac35ee3a7b7b854ec1e33f). That release carries ci-workflows#844, the report step. The README "Pin" procedure moves the pin and the policy.json contract key together in one PR, so the config and the reusable that reports it land in one merge.

    • components/runner-policy/policy.json: the contract at 4aff993 is a verbatim copy of the v0.55.0 entry.
    • components/runner-policy/README.md: a v0.59.0 rollout line, signed off by hand on 2026-10-21. The owner approved the change on that date.
    • components/runner-policy/runner-policy.test.mjs: a test that the 4aff993 entry equals the v0.55.0 entry and is the only key at that SHA.
    • No other standards component pins maintenance-check-links-online.yml. The other 3a2c748 pins are pr-review, pr-review-security, maintenance-sync-standards and check-managed-files, which the repin cascade owns.

Contract review basis

  • The workflow blob changed: b2caebc3 at 3a2c748 and ed87540b at 4aff993. The diff is 83 lines added and 6 removed.
  • I parsed both versions with ci-workflows' workflow-yaml.cjs. The contract surface hashes identically at both SHAs (sha256 9d258d23...). That surface covers the on block (the workflow_call inputs, secrets and outputs), the top-level permissions: {}, and every job's permissions (contents: read), runs-on (${{ inputs.runner }}) and uses.
  • The changes are all in steps:
    • The lychee-action step, which holds token: ${{ github.token }} as before, gains id: lychee and moves to format: json, an output path, --verbose, and fail, failIfEmpty and jobSummary set to false.
    • A new Write the report step uses only env and jq and holds no credential.
  • Because the token-bearing step changed, the gate declines auto-approval: "credentialReferences changed since the previously reviewed ...@3a2c748".

Consumers. Every target in distribution/sync-manifest.yml takes the lychee component as managed; none holds it locally owned. The file is read by these lanes:

  • Offline lanes. These are the ci-workflows lychee-offline action and this repo's lychee-fixtures lane. Both run --offline, so they make no HTTP requests and neither key changes them. The offline run over this repo's Markdown gives the same result under the old and the new config: 959 Total, 402 OK, 0 Errors, 557 Excluded. The bad fixture fails with 2 errors under both.
  • The online check. This is the ci-workflows reusable maintenance-check-links-online.yml, which the claude-code-plugins caller runs. Under this config, lychee's markdown job summary lists timeouts but not accepted 429 or 5xx responses. ci-workflows#844 adds that list.

The api.github.com 403s are out of scope here. They are handled by exact-URL ignores in claude-code-plugins#8875 (standards#831).

Verification

  • npm run test:runner-policy: 325 pass, 0 fail, including the online link-check contract at the v0.59.0 tag copies its v0.55.0 entry forward verbatim. npm run lint:runner-policy: Runner policy passed.
  • End to end: I copied the repinned caller and claude-code-plugins' .github/runner-policy.json into a temp tree and ran runner-policy.mjs --repository-visibility public.
    • With this branch's policy: Runner policy passed.
    • With main's policy: one runner-target-contract failure for the caller (auto-approval declined, credentialReferences changed), so the contract is needed.
  • bash harness/shell/run-tests.sh components/lychee/lychee.test.sh still passes after merging main.
  • bash harness/shell/run-tests.sh components/lychee/lychee.test.sh: pass. I also mutated lychee.toml (added 404 to accept, set accept_timeouts = false), and the test failed on the 404 and timeout cases as expected. Then I restored the file.
  • bash harness/shell/run-tests.sh components/lychee/private-repo-inventory.test.sh: pass.
  • bash components/lychee/private-repo-inventory.sh check --config lychee.toml: OK.
  • shellcheck, typos, markdownlint-cli2, editorconfig and gitleaks pass through the lefthook pre-commit.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01Tje1j8KEYaYSxYe8RgJ3Lw

A throttled or erroring host, or a timeout, says nothing about whether a
link is dead, so it no longer fails the online run. 404, 410 and every
other 4xx still fail. accept restates the default 100..=103 and 200..=299
because setting it replaces the default. lychee 0.24.2 never retries a
rejected 5xx; revisit when lycheeverse/lychee#2324 ships.

The status-code test now asserts that rule: 429, 500, 503, 599 and a
timeout pass, an accepted 429 is requested once, 404 and 410 fail, and a
200 still passes.

Refs: melodic-software/claude-code-plugins#8730
Refs: melodic-software/ci-workflows#739

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tje1j8KEYaYSxYe8RgJ3Lw
kyle-sexton and others added 2 commits October 21, 2026 15:47
…lows v0.59.0

v0.59.0 (4aff993) carries ci-workflows#844, the report step that lists
timeouts and accepted 429 and 5xx links in the job summary, so the
caller moves in the same merge as the accept config it reports on.

The runner-policy contract at 4aff993 is a verbatim copy of the v0.55.0
entry: the reusable's workflow_call inputs, secrets, outputs, top-level
and job permissions and runs-on are structurally identical between
3a2c748 and 4aff993. The gate cannot auto-approve it because the
GITHUB_TOKEN-bearing lychee-action step changed (format, output, args,
an id), so the copy is registered by hand.

Refs: melodic-software/claude-code-plugins#8730
Refs: melodic-software/ci-workflows#739

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 21, 2026 19:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T19:52:30.816741Z 1a3af3c Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown

claude-review reviewed 1a3af3ceaee0ef027cce06cce28daf58b85c87a2: 0 finding(s), 0 as inline comments.

No findings on #834 at 1a3af3c. This review was read-only: I read the diff and the PR context, and ran no tests or builds myself.

  • Checked: the lychee.toml accept and accept_timeouts change, the rewritten status-code block in lychee.test.sh, the lychee-online README prose, the claude-code-plugins caller repin to v0.59.0, and the matching runner-policy entry (policy.json, README and test).
  • Config and tests: the accept list restates the default 100..=103 and 200..=299 ranges, as its comment says. The tests cover each outcome the PR states: pass for 200, 429, 500, 503, 599 and timeouts; fail for 404 and 410. They also include a control run that sets --accept-timeouts=false and fails. The 429-requested-once assertion counts requests recorded for the 429 path, so it holds. The server runs threaded so the hang path does not block the others.
  • Runner policy: the new policy.json entry keeps the commas and structure valid. The new test asserts the entry equals the v0.55.0 entry and is the only key at that SHA.
  • Docs: no other doc in the repo still says a 429 fails the online link check, so nothing is left to reconcile.
  • Closing issues: the PR only has Refs: lines, so there is no closing-issue criteria to check. The PR body follows the contract (Refs:, Summary, Fix, Verification, Related).
  • Author-claimed, not verified by me: the passing test runs, the dispatch probe and the contract hash comparison.
  • Paved paths: I did not resolve a standards index row.

@github-actions

Copy link
Copy Markdown

claude-security-review reviewed 1a3af3ceaee0ef027cce06cce28daf58b85c87a2: 0 finding(s), 0 as inline comments.

No security findings in PR #834 at head 1a3af3c.

I read the full diff. I did not read the ci-workflows v0.59.0 source, because the cross-repo gh api calls were not approved in this session.

  • Caller repin and runner-policy entry:
    • claude-code-plugins.yml moves the reusable maintenance-check-links-online.yml to a full 40-character commit SHA, 4aff9932…, labelled v0.59.0. The pin stays a SHA, so nothing is loosened.
    • The new runner-policy/policy.json contract entry copies the v0.55.0 entry unchanged. It has the same allowedInputs, an empty allowedSecrets, and contents: read as the minimum caller permission. Nothing widens, and no secret or permission is added. The new test asserts the entry equals the v0.55.0 one and is the only entry for that SHA.
    • Because I did not read the v0.59.0 source, I could not confirm that its workflow_call interface matches the "verbatim copy" claim in the README and policy. That claim is the PR author's hand sign-off. The structural test only compares the policy entries to each other.
  • lychee.toml change: it now accepts 429, 5xx and timeouts in the online link check. This lowers how strict the check is, but it is not an access-control boundary. It gates no secret or deployment, and the config comment documents the trade-off (no retry, no fragment check, a job-summary report). I did not count it as a security finding.
  • Test harness: the Python fixture server binds 127.0.0.1 on an ephemeral port. The path-to-status handler runs int(last) on a path the test itself generates. That is local test code with no external input, so there is no exploitable path.
  • Instruction surface: no CLAUDE.md, AGENTS.md, rules file, hook allowlist or other standing instruction is deleted or softened. The README wording changes only describe the new accept behavior, so I did not consult the instruction exception register.

@kyle-sexton
kyle-sexton merged commit 320cf83 into main Oct 21, 2026
57 of 58 checks passed
@kyle-sexton
kyle-sexton deleted the fix/lychee-transient-tolerance branch October 21, 2026 19:57
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