fix(devx): retry the lychee binary download and say when the link check did not run - #8253
Merged
Merged
Conversation
…ck did not run `Check Documentation Links` went red three times on 2026-08-12 without examining a single link. lychee-action@v2's `lychee-setup` step fetches the release tarball with a bare `curl -sfLO` -- no retry -- and when that one request loses, curl exits 22, the action's `Install lychee` and `Run Lychee` steps both report `skipped`, and the job fails in ~9 seconds. All three job logs read directly, byte-identical in shape: #8128 17:23:49Z run 31622391000 job 94200196323 exit 22, 497ms #8205 20:08:47Z run 31636151516 job 94246838620 exit 22, 177ms #8225 21:21:28Z run 31642129140 job 94266966755 exit 22 Transient, not systemic: each cleared on the next attempt, and a sibling PR went green six minutes after the last failure. Three changes, none of them a weakening -- `fail: true` stays on every path and a retry that exhausts still fails the job. Retry. The action is invoked twice: attempt 1 defers its verdict via `continue-on-error`, a 15s wait follows, then an identical retry that carries no such escape. A back-to-back retry would retry inside the same blip; every observed recovery was tens of seconds to tens of minutes later. Zero cost on a green run -- all three added steps are `skipped` when attempt 1 passes. Legibility. A new step distinguishes "links are broken" from "the link check never ran" and says so in the job summary and as an error annotation. The discriminator is exact, not heuristic: the action's entrypoint.sh writes `exit_code` to $GITHUB_OUTPUT *before* it exits, so a genuine broken-link failure carries a value while a setup failure skips `Run Lychee` and leaves it unset. It exits 1 on its own so the case stays red even if someone later makes the retry lenient. Version pin. Three comments in this file reasoned about "the pinned lychee 0.24.2" -- the `--offline` argument turns on which version runs -- but nothing here pinned it; the version came from the action's own default, which moves when the `v2` tag moves. `lycheeVersion: v0.24.2` asserts at the invocation site what `--offline` already depends on. The argv moves to a job-level `env` so the two invocations cannot drift. Verified byte-identical to origin/main's inline args by parsing both. Caching (the card's shape 1) is NOT included, on measurement: the action `rm -rf`s its download directory and re-downloads unconditionally, so `actions/cache` cannot reach it -- a cache step here would read as coverage while doing nothing. Part of #8238 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BytmXbyC9R2Wvpg2uW14fc
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 13, 2026 00:40
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8238.
Part of, not a closing keyword, on purpose: the triage comment scoped this card as shapes 2 + 1 + 4, and shape 1 (cache the binary) is not implemented here — it was measured unreachable rather than deferred. Whether that measurement closes the card is the PM's call, not this PR's. Everything else in scope is delivered.The measured failure
Check Documentation Linksgoes red having examined zero links. The job dies inlycheeverse/lychee-action@v2'slychee-setupstep, which fetches the release tarball with a barecurl -sfLO— no--retry, one attempt. When that request loses, curl exits 22, the action'sInstall lycheeandRun Lycheesteps both reportskipped, and the job fails in about nine seconds.All three job logs read directly (the card carried one first-hand reading and two reported ones; the other two are now confirmed first-hand), byte-identical in shape — same URL, same exit code, same two skipped steps:
31622391000/94200196323exit code 22, setupduration_ms=497,Install lychee+Run Lycheeoutcome=skipped31636151516/94246838620exit code 22, setupduration_ms=177, same two skipped31642129140/94266966755exit code 22, lychee step 0s of a 9s jobNo divergent failure mode. The card flagged that a different failure wearing the same check name would be the more valuable finding — it is not the case here; all three are one mode.
One correction to the card's framing: for #8205 the red was cleared by a new push, not a re-run. #8128 and #8225 were re-run.
What changed
Three things, none of them a weakening.
fail: truestays on every path, and a retry that exhausts still fails the job.1. Retry the setup (card shape 2). The action is invoked twice: attempt 1 defers its verdict via
continue-on-error, then an identical retry runs that carries no such escape — so its failure fails the job. A 15s wait sits between them: a back-to-back retry would be retrying inside the same blip, and every observed recovery was tens of seconds to tens of minutes later. The 15s is a judgement and is labelled as one in the file; the retry is the part the evidence supports.2. Say when the link check did not run (card shape 4). A new step distinguishes "links are broken" from "lychee never executed" and writes it to the job summary plus an
::error::annotation. The discriminator is exact, not heuristic: the action'sentrypoint.shwritesexit_code=$LYCHEE_EXIT_CODEto$GITHUB_OUTPUTbefore it exits, so a genuine broken-link failure carries a value (2) while a setup failure skipsRun Lycheeand leaves the output unset. Empty means lychee never ran. The step exits 1 on its own so the case stays red even if a later change makes the retry lenient — the #4690 shape, guarded rather than assumed.3. Pin the lychee version. Three comments in this file already reasoned about "the pinned lychee 0.24.2" — the
--offlineargument is only honoured from 0.24.x, so it is load-bearing — but nothing here pinned it. The version came from the action's ownlycheeVersiondefault, which moves whenever thev2tag moves; the determinism claim was true by coincidence.lycheeVersion: v0.24.2now asserts at the invocation site the thing--offlinedepends on, which is the rule that comment already applies to--offlineitself.The argv moves to a job-level
envso two invocations cannot drift apart. Verified byte-identical toorigin/main's inline args by parsing both files, not by reading the diff.Why caching is not here
actions/cachecannot reach the action's binary.lychee-setupdoesrm -rf "${TEMP_DIR}" && mkdir -p "${TEMP_DIR}"and then downloads unconditionally — there is no "already present, skip" branch anywhere in the composite. A cache step around it would restore a directory the action immediately deletes: no download avoided, and a cache entry in the workflow that reads as coverage while doing nothing. Reaching a cache would mean dropping the action's downloader entirely and owning the install, which is a much larger surface than a transient CDN blip justifies.Vendoring / mirroring (shape 3) is likewise not here: the card scoped it to "only if 1 and 2 prove insufficient", and 2 has not yet had a chance to prove anything.
Hypotheses the dispatch asked to measure
@v2was used with nolycheeVersion, so the pin lived in the action's default. Fixed here.Check Documentation Linksin the required set? No. It is absent fromREQUIRED_CONTEXTSinscripts/check-required-contexts.mjs, andpnpm check:required-contextsenumerates nine contexts without it. Corroborated twice more: the workflow header records the maintainer ruling of 2026-08-07 to land it advisory-first, and the file has nomerge_grouptrigger, which its own ⛔ note says is a precondition for ever being required. So this flake does not block auto-merge; the cost is a misleading red plus a re-run on an unrelated lane. No prose correction is owed tocheck-required-contexts.mjs— its ⛔ exclusion list names contexts a specific audit ruled out, and silence about this one is absence, not error. That file is untouched.skippedon a green run; the version pin names the value CI already used. Baseline for reference, from run31642129140attempt 2: 13s job, of which the lychee step (download plus check) is 1s.Verification
lychee.tomlis untouched — what the checker checks is a separate concern from whether it starts.Derived gates for the changed path via
node scripts/pm/dispatch-gates.mjs .github/workflows/check-links.yml, all run and green:check:workflow-status-functionsis the one that matters most for this diff and it parses every workflow as real YAML, so it also proves the file is well-formed. Its rule is job-levelif:only; these are step-level, which that gate documents as deliberately out of scope.Beyond the gates, the resolved semantics were asserted rather than eyeballed — a throwaway script parsed both this file and
origin/main's and checked 22 properties, all passing:The four-scenario table in [5] is a symbolic evaluation of the step's
if:over the reachable states; the fact it rests on — thatexit_codeis written before the exit — was read out of the action'sentrypoint.sh, not assumed. The one thing this PR cannot demonstrate locally is a live setup failure, since it needs the CDN to actually fail; the discriminator is argued from the action's source instead, and that limit is stated here rather than papered over.Changeset
None. Workflow-only, nothing released —
skip-changesetapplied.Generated by Claude Code