1+ # ============================================================
2+ # .github/workflows/links.yml (Lychee Link Checker)
3+ # ============================================================
4+ # SOURCE: https://github.com/denisecase/templates
5+ #
16# WHY-FILE: Automated link checking.
27# OBS: Behavior is configured in lychee.toml in this repository.
38# OBS: Runs on pull requests and monthly on schedule; manual trigger always available.
@@ -6,9 +11,7 @@ name: Check Links
611
712on :
813 workflow_dispatch : # WHY: Manual trigger - always available
9-
1014 pull_request : # WHY: Validates PR links before merge
11-
1215 schedule :
1316 - cron : " 0 6 1 * *" # WHY: Runs monthly (1st of month)
1417
@@ -17,22 +20,33 @@ concurrency:
1720 group : link-check-${{ github.ref }}
1821 cancel-in-progress : true
1922
23+ permissions :
24+ contents : read # WHY: Needed to checkout code.
25+ issues : write # WHY: Needed to create issue on scheduled failures.
26+ pull-requests : write # WHY: Needed to comment on PR if links broken.
27+
28+ env :
29+ PYTHONUNBUFFERED : " 1" # WHY: Real-time logging.
30+ PYTHONIOENCODING : " utf-8" # WHY: Ensure UTF-8 encoding for international characters.
31+ REPORT_FAILURES : " true" # WHY: Enable PR comments and scheduled issues for link failures.
32+
2033jobs :
2134 lychee :
22- runs-on : ubuntu-latest
23-
24- permissions :
25- contents : read
26- issues : write
27- pull-requests : write
35+ name : Link checks
36+ runs-on : ubuntu-latest # WHY: Linux environment matches most production deployments
37+ timeout-minutes : 20 # WHY: Prevent hanging jobs. If over time, likely stuck.
2838
2939 steps :
3040 - name : 1) Checkout repository code
31- uses : actions/checkout@v6 # OBS: v6 current as of Dec 2025
41+ uses : actions/checkout@v6
3242
3343 - name : 2) Check links with Lychee
34- uses : lycheeverse/lychee-action@v2
44+ id : lychee
45+ uses : lycheeverse/lychee-action@v2.7.0
3546 with :
47+ # WHY: Do not hard-fail this step; always run reporting steps.
48+ # Instead, fail the job explicitly at the end if exit_code != 0.
49+ fail : false
3650 args : >
3751 --config lychee.toml
3852 --user-agent "${{ github.repository }}/lychee"
@@ -42,12 +56,11 @@ jobs:
4256 './**/*.tex'
4357 './**/*.yml'
4458 './**/*.yaml'
45- lycheeVersion : latest # OBS: Always use latest lychee release
4659 env :
4760 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
4861
4962 - name : 3) Comment on PR if links broken
50- if : failure() && github.event_name == 'pull_request'
63+ if : steps.lychee.outputs.exit_code != 0 && github.event_name == 'pull_request' && env.REPORT_FAILURES == 'true '
5164 uses : actions/github-script@v8
5265 with :
5366 script : |
@@ -65,30 +78,40 @@ jobs:
6578 body: comment,
6679 });
6780
68- - name : 4) Create issue for scheduled failures # WHY: Track broken links found during scheduled checks
81+ - name : 4) Create issue for scheduled failures
82+ # WHY: Track broken links found during scheduled checks
6983 # OBS: Only creates issue if none already open with 'broken-links' label
70- if : failure() && github.event_name == 'schedule'
84+ if : steps.lychee.outputs.exit_code != 0 && github.event_name == 'schedule' && env.REPORT_FAILURES == 'true '
7185 uses : actions/github-script@v8
7286 with :
7387 script : |
88+ const owner = context.repo.owner;
89+ const repo = context.repo.repo;
7490 const date = new Date().toISOString().split("T")[0];
7591 const title = `Link Check Failed - ${date}`;
7692 const runUrl = `${context.payload.repository.html_url}/actions/runs/${context.runId}`;
7793 const body = `Monthly link check found broken links.\n\nWorkflow logs: ${runUrl}`;
7894
7995 const existing = await github.rest.issues.listForRepo({
80- owner: context.repo.owner ,
81- repo: context.repo.repo ,
96+ owner,
97+ repo,
8298 labels: "broken-links",
8399 state: "open",
100+ per_page: 1,
84101 });
85102
86103 if (existing.data.length === 0) {
87104 await github.rest.issues.create({
88- owner: context.repo.owner ,
89- repo: context.repo.repo ,
105+ owner,
106+ repo,
90107 title,
91108 body,
92109 labels: ["maintenance", "broken-links"],
93110 });
94111 }
112+
113+ - name : 5) Fail job if broken links were found
114+ if : steps.lychee.outputs.exit_code != 0
115+ run : |
116+ echo "Lychee found broken links (exit_code != 0). Failing workflow."
117+ exit 1
0 commit comments