Skip to content

ci: replace hyperlink with lychee for docs link checking - #21246

Draft
uncoolclub wants to merge 2 commits into
eslint:mainfrom
uncoolclub:perf/docs-link-validator-lychee
Draft

uncoolclub wants to merge 2 commits into
eslint:mainfrom
uncoolclub:perf/docs-link-validator-lychee

Conversation

@uncoolclub

@uncoolclub uncoolclub commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Prerequisites checklist

AI acknowledgment

  • I did not use AI to generate this PR.
  • (If the above is not checked) I have reviewed the AI-generated content before submitting.

What is the purpose of this pull request? (put an "X" next to an item)

[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[X] Other, please explain: CI change. Replaces the docs link checker to fix #21124.

What changes did you make? (Give an overview)

Fixes #21124.

Draft as suggested, so this can be reviewed while an upstream lychee fix is pending.

This replaces the custom docs link checker with lychee. hyperlink crawls the built site with jsdom and assetgraph and builds a resource graph to answer two questions:

  • Does each internal link resolve?
  • Does each fragment exist?

lychee answers both directly, and supports the extensionless pretty URLs this site uses through fallback_extensions (since 0.16) and index_files (since 0.20).

Performance

Measured in CI, before and after:

Validate internal links whole Verify Docs Files job
Before (run 29733644481) 637s 735s
After (run on this branch) 2s 77s

The 2s includes downloading the lychee binary. lychee checked 80,268 links with 0 errors.

Memory drops too. Locally, on the same built site, peak RSS goes from 3.98GB (the measurement in #21124, and the reason the old script needed --max-old-space-size=4096) to 113MB.

Changes

All link-checking behaviour moves from docs/tools/validate-links.js to docs/lychee.toml. The script and its dependencies are removed: hyperlink, @munter/tap-render, tap-spot, cross-env, and the --max-old-space-size=4096 flag it needed.

docs/src/maintain/manage-releases.md told maintainers to add a pattern to skipPatterns in the deleted script. It now points at the exclude list in docs/lychee.toml.

docs/README.md gains an install note, because npm run lint:links now needs a lychee binary that npm does not install. Without the note a contributor gets command not found.

Coverage differences

Two behaviour changes, one in each direction.

<script src> is no longer checked. hyperlink checked script sources; lychee does not extract them. The site references 8 local scripts under assets/js/. I could not find a lychee option that restores this, so the limitation is documented in docs/lychee.toml rather than worked around. This is the one place the new setup checks less than the old one.

More HTML pages are checked. hyperlink only crawled pages reachable from index.html, leaving 50 orphans unchecked. lychee scans every HTML file in _site, which picks up 36 of them. The remaining 14 are component-library/ previews, excluded as link sources because they use placeholder hrefs.

Temporary exclusions

Two, both commented inline as workarounds rather than intentional skips.

Dotted extensionless URLs. lychee cannot resolve a path segment containing a dot, so /use/migrate-to-8.0.0, -9.0.0 and -10.0.0 are excluded. That is 3 unique URLs but 778 occurrences, since the migration banner appears on every rule page, so while the exclusion stands, deleting a migration page would go uncaught. Tracked upstream as lycheeverse/lychee#2274. The fix, #2276, merged on 2026-09-20, but it is not in a released binary yet: the newest release is lychee-v0.24.2 (2026-05-01), and the v0.25.0 release PR has been open since late May. I would rather not block this PR on that release. Once one carrying the fix ships, dropping the exclusion and bumping lycheeVersion is a two-line follow-up.

I also tested a remap workaround that keeps those URLs checked today. I preferred the exclusion because lychee validates the remapped path: if a remap accidentally points a broken URL at an existing file, the link passes validation. With the upstream fix already approved, the exclusion is the safer failure mode. Happy to switch if you would rather not carry it.

Protocol-relative URLs. lychee reads //host/path as a local path in offline mode. The site has two, both external favicons, which the previous checker also skipped.

Action and binary

The action is pinned to a commit SHA, consistent with the other third-party actions in this repository, so Dependabot keeps it current. lycheeVersion is pinned separately, since it fixes the binary rather than the action code that installs it.

lychee-action downloads the binary from GitHub Releases without verifying a checksum. The release does publish .sha256 files, but they are controlled by the same upstream release, while hardcoding one here would make every version bump require a second manual edit Dependabot cannot perform.

The job has contents: read, no repository secrets, no cache and no publish step, and link checking is its final step, so I left the action's installation behaviour unchanged.

Review questions

  1. <script src> coverage. The new checker does not validate these. Is that reduction a blocker? If so I can look at adding the extractor upstream.
  2. Dotted migration URLs. Should this PR carry the temporary exclusion until fix: append fallback extensions past a pre-existing extension lycheeverse/lychee#2276 is released, or would you prefer the remap workaround?
  3. Binary provisioning. lychee has no npm distribution, so the checker changes from an npm dependency to a GitHub Releases binary. It stays out of the published package (docs/ is not in files), and docs/ tooling already downloads binaries at install time through bin-wrapper (via gifsicle, jpegtran-bin, optipng-bin). Is that acceptable here?

Verification

Verified locally against a freshly built _site using lychee 0.24.2. The check completes in ~1s over 80,267 links with 0 errors, and still catches:

  • broken relative links
  • broken absolute internal links
  • broken <img src> references
  • broken <link rel="stylesheet"> references
  • broken fragments on pretty URLs
  • broken fragments on .html URLs
  • broken index.html directory links

Disclosure: I'm a participant of open source contribution program OSSCA

@eslint-github-bot eslint-github-bot Bot added the build This change relates to ESLint's build process label Aug 23, 2026
@github-project-automation github-project-automation Bot moved this to Needs Triage in Triage Aug 23, 2026
@netlify

netlify Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

❌ Deploy Preview for docs-eslint failed. Why did it fail? →

Name Link
🔨 Latest commit afaa354
🔍 Latest deploy log https://app.netlify.com/projects/docs-eslint/deploys/6ac200dbd982d90008552314

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions Bot added the Stale label Sep 2, 2026
@lumirlumir lumirlumir removed the Stale label Sep 7, 2026
@lumirlumir lumirlumir moved this from Needs Triage to Implementing in Triage Sep 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

Fixes eslint#21124.

`hyperlink` crawls the built site with jsdom and assetgraph, modelling every
asset relationship, to answer two questions: does each internal link resolve,
and does each fragment exist. It needs a 4GB heap and dominates Docs CI.

`lychee` answers both directly, and handles the extensionless pretty URLs this
site uses: `fallback_extensions` since 0.16, `index_files` since 0.20. All
behaviour now lives in `docs/lychee.toml` instead of a custom script, so
`hyperlink`, `@munter/tap-render`, `tap-spot` and `cross-env` all go, along
with the heap flag.

Two exclusions are workarounds rather than intentional skips, both noted
inline. Pretty-URL resolution cannot handle a path segment containing a dot,
which covers the `/use/migrate-to-<version>` links; that is
lycheeverse/lychee#2274, and the exclusion goes once the fix ships.
Protocol-relative URLs are read as local paths in offline mode; the site has
two, both external favicons that the old checker skipped as well.

One coverage gap this does not work around: `lychee` does not extract
`<script src>`, which `hyperlink` checked. No config option restores it.
@uncoolclub
uncoolclub force-pushed the perf/docs-link-validator-lychee branch from 8eb58bd to 1395770 Compare September 24, 2026 12:10
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DMartens

Copy link
Copy Markdown
Contributor

To your three questions:

  1. "<script src>" support: I do not think this is a blocker. After researching the problem seems to be that their parser just completely skips <script>.
    There is also the possibility to write an extractor / regular expression and directly pass the URLs to lychee.
  2. wait for release: I do not think we need to wait for a new release of lychee. As you mentioned the followup for this would be small
  3. binary distribution: I think in this case this would be fine, as the CI would still check the links. An alternative would be to use a GitHub binary downloader (e.g. gh-release-fetch), so users could get the binary easily.

@github-actions

Copy link
Copy Markdown
Contributor

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions Bot added the Stale label Oct 14, 2026
@DMartens DMartens removed the Stale label Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions Bot added the Stale label Oct 25, 2026
@DMartens DMartens removed the Stale label Oct 26, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions Bot added the Stale label Nov 6, 2026
@DMartens DMartens removed the Stale label Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build This change relates to ESLint's build process github actions

Projects

Status: Implementing

Development

Successfully merging this pull request may close these issues.

Change Request: Speed up Validate internal links step in Docs CI

3 participants