Skip to content

Add a link checker for links added or changed in a PR - #1875

Merged
bedroesb merged 3 commits into
elixir-europe:masterfrom
eastagiletracker:agile-board/pr-link-checker
Oct 13, 2026
Merged

bedroesb merged 3 commits into
elixir-europe:masterfrom
eastagiletracker:agile-board/pr-link-checker

Conversation

@eastagiletracker

@eastagiletracker eastagiletracker commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

This PR proposes a pull-request link checker that runs Lychee on only the links a PR adds or changes (Fixes #1754). We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/669. You can sign in with your GitHub ID to claim ownership of the project.

Today the only link check is the nightly Link Checker workflow, which builds the whole site and reports into #1570, so a broken link reaches master first and is found a day later. On current master (6bbfc3d1) none of the workflows that run on pull_request invoke Lychee (grep -l lychee .github/workflows/* returns only link-check.yml, whose triggers are schedule and workflow_dispatch).

The new .github/workflows/pr-link-check.yml runs on pull requests that touch .md, .yml or .yaml content (not .github/). It takes git diff --unified=0 base...head, keeps only the added lines, and passes them to lycheeverse/lychee-action@v2.9.0 with the same Lychee version (v0.24.2) and the same github.com exclusion as the nightly job. Only http/https links are checked, since relative and root-relative links and {% tool %} tags only resolve in the built site, which the nightly job still covers. The job fails when an added link is broken and writes the Lychee report to the job summary. It needs only contents: read, so it works for PRs from forks and never checks out or runs PR code with a writable token. Two details that are easy to trip over: the diff is written to $RUNNER_TEMP because lychee-action deletes ./lychee before it runs, and failIfEmpty: false is set because a PR whose new lines contain no links should pass (the action fails such runs by default). Note that a Markdown paragraph is a single line in this repo, so editing one word re-checks every link in that paragraph. I read that as covering the "changed" part of the request.

Verification. I ran the workflow on GitHub Actions in my fork, using three throwaway PRs that each append one line to pages/national_resources/gr_resources.md:

  • a line with a 404 page (https://rdmkit.elixir-europe.org/this-page-does-not-exist-lc-test), a working link and a relative link: the job failed with [404] https://rdmkit.elixir-europe.org/this-page-does-not-exist-lc-test | Rejected status code ... Not Found (4 links: 1 OK, 1 broken, 2 excluded, namely the relative link and a reserved .invalid test domain that Lychee skips by design)
  • a line with a working link, a DOI, a {% tool "fega" %} tag and a relative link: passed (3 links checked, none broken)
  • a line with no links: passed (0 links, no empty-input failure)

I also ran the same extraction and Lychee arguments locally against four merged commits (1140c83c fega links, 0600b296, acf037b8 Spatial Transcriptomics Portal entry, 8dc81626). Each picked up exactly the links that commit introduced, including the url: field of a new tool_and_resource_list.yml entry, and all passed. A non-existent domain is reported as a connection error. actionlint shows no new findings: its output on the workflows directory is the same before and after the change (two pre-existing notes in all-registry-links-update.yml and jekyll.yml).

How this was managed

This work was tracked as story pcj on a board imported from this repo's issues and pull requests, which we used to manage it.

board

If you'd rather not receive contributions like this, reply no-more-prs on this pull request and we won't open any further ones on your repositories.


Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com

@bedroesb

Copy link
Copy Markdown
Member

@eastagiletracker This can be a good start. Could you align the versions used with the one of our current link checker? https://github.com/elixir-europe/rdmkit/blob/master/.github/workflows/link-check.yml

@eastagiletracker

Copy link
Copy Markdown
Contributor Author

Thanks @bedroesb, done in 6470447: the PR workflow now pins lycheeverse/lychee-action@v2.9.0 and lycheeVersion: v0.24.2, the same as link-check.yml after #1897. I re-ran the same Lychee arguments with v0.24.2: a 404 still fails the check, a file of good links passes, and an empty diff passes.

@bedroesb bedroesb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, some extra comments

Comment thread .github/workflows/link-check-pr.yml Outdated
branches: [master, main]
paths:
- '**.md'
- '**.html'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have html as content files so this can be removed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 59e2b49, the trigger paths are now .md, .yml and .yaml.

Comment thread .github/workflows/link-check-pr.yml Outdated
# extracts the links from them. Written outside the workspace because
# lychee-action removes ./lychee before it runs.
git diff --unified=0 --no-color --diff-filter=AMR "$BASE_SHA...$HEAD_SHA" \
-- '*.md' '*.html' '*.yml' '*.yaml' ':(exclude).github/**' \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed from the git diff pathspec as well (59e2b49).

Comment thread .github/workflows/link-check-pr.yml Outdated
with:
# External links only: relative links and {% tool %} tags are resolved
# at build time and covered by the nightly check of the built site.
args: --no-progress --scheme https --scheme http --exclude '^https://github\.com' --accept '100..=103,200..=299,429' ${{ runner.temp }}/pr-added-lines.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligned in 59e2b49 where it applies. Same as link-check.yml: lychee-action v2.9.0, Lychee v0.24.2, the ^https://github\.com exclusion, a --root-dir, id: lychee and the github.repository_owner == 'elixir-europe' guard. Kept different on purpose:

  • fail: true, because failing the PR is the point of this check (the nightly job reports instead).
  • --scheme https --scheme http, because the PR job checks raw diff lines, not the built _site, so relative links and {% tool %} tags cannot resolve here. The nightly job still checks those.
  • --accept ...,429, so a rate-limited site does not block a PR. Happy to drop it if you prefer to match the nightly job exactly.
  • --fallback-extensions html and the --exclude-path entries are not needed, since they only apply to the built _site.

One correction to my earlier comment: with v0.24.2, Lychee reports root-relative links such as [..](/github_way) as errors unless a root dir is given, so the version bump alone would have failed PRs that touch paragraphs with those links. --root-dir now resolves them to file:// URLs, which the scheme filter skips. I checked this on the existing pages/ paragraphs that contain root-relative links: 4 errors without the flag, 0 with it. A real 404 still fails, and actionlint is clean.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change filename to pr-link-check.yml to align with https://github.com/elixir-europe/rdmkit/blob/master/.github/workflows/pr-checklist.yml naming

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to .github/workflows/pr-link-check.yml (59e2b49).

@bedroesb

Copy link
Copy Markdown
Member

What happens with false positives? Does that mean the page can not be brought live?

@eastagiletracker

Copy link
Copy Markdown
Contributor Author

@bedroesb No, it does not block anything by itself. master has no required status checks, so a failing link check shows a red X on the PR, but you can still merge and the page goes live as usual. It only becomes a hard gate if you later add it as a required check in branch protection.

For a known false positive (a site that blocks bots with a 403, a flaky host and so on) there are two options:

  • Merge anyway. The report in the job summary shows which link failed, so you can decide case by case.
  • Add a regex for the URL to a .lycheeignore file in the repo root. Lychee reads it automatically, so it also applies to the nightly link-check.yml. I tested that an ignored URL then passes. I can add an empty .lycheeignore with a comment to this PR if that is useful.

Rate-limited responses (429) are already accepted, which removes the most common false positive. If you would rather never see a red X, I can switch it to fail: false so it only reports, like the nightly job.

One more note: this PR only changes .github/, which the workflow excludes, so the check does not run on this PR itself. It runs on the first content PR after merge.

@bedroesb

Copy link
Copy Markdown
Member

Thanks, lets try it out after merge!

@bedroesb
bedroesb merged commit a63c6d0 into elixir-europe:master Oct 13, 2026
3 checks passed
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.

Add a link checker that checks with every PR the links that are changed/added in the PR

2 participants