Summary
The Internal links check in .github/workflows/links.yml (the internal job) intermittently fails with exit code 13. When it does, linkinator crawls only the homepage and then aborts — it never reports a broken link. This is a tooling crash, not a real broken-link finding, so it produces false-red runs that block PRs.
Seen on PR #56: failed twice on re-run with identical output, while the same content scans clean locally.
Evidence
Failing CI log (job tail):
🏊♂️ crawling http://localhost:4321
[200] http://localhost:4321/
Warning: Detected unsettled top-level await at .../linkinator/build/src/cli.js:318
await main();
^
##[error]Process completed with exit code 13.
The crawl dies ~5s after fetching /, before recursing into any other page.
Running the exact same command locally against the built site passes cleanly:
🤖 Successfully scanned 210 links in 0.155 seconds.
linkinator exit: 0
So there are zero broken internal links — the failure is the crawler crashing, not the content.
Root cause
Node exit code 13 = "unsettled top-level await": the event loop emptied while a top-level await was still pending. This is a known interaction between linkinator@6's CLI (await main() at top level) and newer Node runtimes. CI pins node-version: 22, but the job invokes npx --yes linkinator@6, which resolves to whatever latest 6.x is published at run time — so the behaviour can change underneath us without any repo change.
The same pattern is used in both jobs in links.yml:
internal job — .github/workflows/links.yml:48
external job — .github/workflows/links.yml:103
Possible fixes (pick one)
- Pin to a known-good exact version — replace
linkinator@6 with a pinned linkinator@<x.y.z> (and bump deliberately), so an upstream patch can't silently reintroduce the crash. Lowest-risk.
- Retry the crawl step — wrap the linkinator invocation in a small retry loop (it's a hard crash, not a slow link), so a transient abort doesn't fail the job. Mirrors the existing external-link hardening.
- Switch checker — evaluate
lychee (Rust, no Node top-level-await foot-gun) as a drop-in for the internal crawl.
Option 1 is the smallest change and most in keeping with the existing linkinator.config.json setup.
Impact
- False-red Internal links check blocks PR merges until manually re-run (and re-runs don't reliably help, since it's deterministic per resolved linkinator version).
- No correctness risk to the site — broken links are still caught when the crawler doesn't crash, and the daily external sweep is unaffected by this specific symptom.
🤖 Generated with Claude Code
Summary
The Internal links check in
.github/workflows/links.yml(theinternaljob) intermittently fails with exit code 13. When it does, linkinator crawls only the homepage and then aborts — it never reports a broken link. This is a tooling crash, not a real broken-link finding, so it produces false-red runs that block PRs.Seen on PR #56: failed twice on re-run with identical output, while the same content scans clean locally.
Evidence
Failing CI log (job tail):
The crawl dies ~5s after fetching
/, before recursing into any other page.Running the exact same command locally against the built site passes cleanly:
So there are zero broken internal links — the failure is the crawler crashing, not the content.
Root cause
Node exit code 13 = "unsettled top-level await": the event loop emptied while a top-level
awaitwas still pending. This is a known interaction betweenlinkinator@6's CLI (await main()at top level) and newer Node runtimes. CI pinsnode-version: 22, but the job invokesnpx --yes linkinator@6, which resolves to whatever latest 6.x is published at run time — so the behaviour can change underneath us without any repo change.The same pattern is used in both jobs in
links.yml:internaljob —.github/workflows/links.yml:48externaljob —.github/workflows/links.yml:103Possible fixes (pick one)
linkinator@6with a pinnedlinkinator@<x.y.z>(and bump deliberately), so an upstream patch can't silently reintroduce the crash. Lowest-risk.lychee(Rust, no Node top-level-await foot-gun) as a drop-in for the internal crawl.Option 1 is the smallest change and most in keeping with the existing
linkinator.config.jsonsetup.Impact
🤖 Generated with Claude Code