cmc/cleberg.net
My personal web garden & blog.
clone: git clone https://gitbay.org/cmc/cleberg.net.git
main: .github/workflows/link-check.yml · raw
1name: Link check
2
3# Weekly rather than per-PR: most of these links are external, and the web is
4# flaky enough that a per-PR run would fail for reasons no author controls.
5# The paths filter still runs it on any PR that edits this workflow, so a change
6# here is validated by the thing it changes.
7on:
8 schedule:
9 - cron: "0 7 * * 1"
10 workflow_dispatch:
11 pull_request:
12 paths: [".github/workflows/link-check.yml"]
13
14permissions:
15 contents: read
16
17env:
18 ORGO_VERSION: "0.22.0"
19
20jobs:
21 links:
22 runs-on: ubuntu-latest
23 steps:
24 - uses: actions/checkout@v7
25
26 - name: Cache orgo
27 id: cache-orgo
28 uses: actions/cache@v6
29 with:
30 path: ~/.cargo/bin/orgo
31 key: orgo-${{ env.ORGO_VERSION }}-${{ runner.os }}
32
33 - name: Install orgo
34 if: steps.cache-orgo.outputs.cache-hit != 'true'
35 run: cargo install orgo --version "$ORGO_VERSION" --locked
36
37 # orgo directly, not build.py. A production build rewrites
38 # img.cleberg.net URLs to /img/, which is served from a different docroot
39 # and would look like 198 broken links here. The development output keeps
40 # them absolute, so the image host gets checked for real.
41 - name: Build the site
42 run: orgo build content -o .build-dev --strict
43
44 # Internal links are the half worth gating on: they are entirely within
45 # this repo, so a failure is always a real defect and always fixable here.
46 - name: Internal links
47 uses: lycheeverse/lychee-action@v2.9.0
48 with:
49 args: >-
50 --offline
51 --root-dir ${{ github.workspace }}/.build-dev
52 --include-verbatim
53 '.build-dev/**/*.html'
54 fail: true
55 output: internal.md
56
57 # External links are reported, not gated. A dead third-party link is worth
58 # knowing about, but it is not a reason to block the site.
59 - name: External links
60 uses: lycheeverse/lychee-action@v2.9.0
61 with:
62 args: >-
63 --root-dir ${{ github.workspace }}/.build-dev
64 --exclude '\.onion'
65 --exclude '^https?://(localhost|127\.0\.0\.1|0\.0\.0\.0)'
66 --exclude '(\{|%7B|client_id=$)'
67 --max-concurrency 8
68 --max-retries 2
69 --timeout 20
70 --accept 200,206,301,302,303,307,308,401,403,429
71 --include-verbatim
72 '.build-dev/**/*.html'
73 fail: false
74 output: external.md
75
76 # `fail: false` stops a dead third-party link failing the job — but it also
77 # swallows lychee refusing to start at all, which reports success while
78 # checking nothing. Assert the report has a summary table in it.
79 - name: External step actually ran
80 run: |
81 if ! grep -q "Total" external.md; then
82 echo "::error::lychee produced no summary — it did not run"
83 cat external.md
84 exit 1
85 fi
86
87 - name: Summary
88 if: always()
89 run: |
90 { echo "## Internal"; cat internal.md 2>/dev/null || echo "no report";
91 echo; echo "## External"; cat external.md 2>/dev/null || echo "no report";
92 } >> "$GITHUB_STEP_SUMMARY"
93
94 - uses: actions/upload-artifact@v7
95 if: always()
96 with:
97 name: link-reports
98 path: |
99 internal.md
100 external.md