Skip to content

fix(devx): retry the lychee binary download and say when the link check did not run - #8253

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-8238-lychee-setup-flake
Aug 13, 2026
Merged

fix(devx): retry the lychee binary download and say when the link check did not run#8253
hotlong merged 1 commit into
mainfrom
claude/issue-8238-lychee-setup-flake

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

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 Links goes red having examined zero links. The job dies in lycheeverse/lychee-action@v2's lychee-setup step, which fetches the release tarball with a bare curl -sfLO — no --retry, one attempt. When that request loses, curl exits 22, the action's Install lychee and Run Lychee steps both report skipped, 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:

PRwhenrun / jobevidence
#812817:23:49Z31622391000 / 94200196323exit code 22, setup duration_ms=497, Install lychee + Run Lycheeoutcome=skipped
#820520:08:47Z31636151516 / 94246838620exit code 22, setup duration_ms=177, same two skipped
#822521:21:28Z31642129140 / 94266966755exit code 22, lychee step 0s of a 9s job

No 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: true stays 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's entrypoint.sh writes exit_code=$LYCHEE_EXIT_CODE to $GITHUB_OUTPUTbefore it exits, so a genuine broken-link failure carries a value (2) while a setup failure skips Run Lychee and 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 --offline argument is only honoured from 0.24.x, so it is load-bearing — but nothing here pinned it. The version came from the action's own lycheeVersion default, which moves whenever the v2 tag moves; the determinism claim was true by coincidence. lycheeVersion: v0.24.2 now asserts at the invocation site the thing --offline depends on, which is the rule that comment already applies to --offline itself.

The argv moves to a job-level env so two invocations cannot drift apart. Verified byte-identical to origin/main's inline args by parsing both files, not by reading the diff.

Why caching is not here

actions/cache cannot reach the action's binary. lychee-setup does rm -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

  • A — are all instances the same setup failure? Yes. Three logs, one mode, quoted above.
  • B — is the lychee version pinned?No, it was not. @v2 was used with no lycheeVersion, so the pin lived in the action's default. Fixed here.
  • C — is Check Documentation Links in the required set?No. It is absent from REQUIRED_CONTEXTS in scripts/check-required-contexts.mjs, and pnpm check:required-contexts enumerates 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 no merge_group trigger, 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 to check-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.
  • D — retry support and cache reachability? No retry input exists in the action, so the retry is a wrapper. Cache is unreachable, argued above from the action source.
  • E — happy-path cost?Zero. All three added steps are gated on attempt 1 having failed and report skipped on a green run; the version pin names the value CI already used. Baseline for reference, from run 31642129140 attempt 2: 13s job, of which the lychee step (download plus check) is 1s.

Verification

lychee.toml is 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-functions OK (24 workflow files, 43 jobs, 24 job-level if:)
check:required-contexts OK
check:node-version OK (27 setup-node steps, all Node 22)
check:shard-attestation OK (92 self-test assertions)
check:changeset-gate-self-tests OK
check-adr-links OK (525 destinations resolve) + --self-test
check-changeset-no-major OK
check-nul-bytes OK (7482 files)

check:workflow-status-functions is 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-level if: 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:

[1] the argv lychee receives is UNCHANGED from origin/main
ok job-level env.LYCHEE_ARGS === the old inline args, byte for byte — identical
[3] both attempts are the same check, and both fail-closed
ok lychee / lychee-retry: fail: true, same args, same version
ok attempt 1 defers its verdict to the retry
ok the retry has NO continue-on-error (its failure fails the job)
[4] the happy path costs nothing: every added step is conditional
ok 3 steps added, all gated on a prior failure
[5] the "did not run" explainer fires on exactly one scenario
ok silent: attempt 1 green, retry skipped
ok silent: setup flaked once, retry ran clean
ok FIRES : setup failed BOTH times (the #8238 shape)
ok silent: lychee ran and found broken links
ok the explainer is fail-closed on its own (exit 1)

The four-scenario table in [5] is a symbolic evaluation of the step's if: over the reachable states; the fact it rests on — that exit_code is written before the exit — was read out of the action's entrypoint.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-changeset applied.


Generated by Claude Code

…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
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 12:28am

Request Review

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hotlong@claude