name: Link check # Weekly rather than per-PR: most of these links are external, and the web is # flaky enough that a per-PR run would fail for reasons no author controls. # The paths filter still runs it on any PR that edits this workflow, so a change # here is validated by the thing it changes. on: schedule: - cron: "0 7 * * 1" workflow_dispatch: pull_request: paths: [".github/workflows/link-check.yml"] permissions: contents: read env: ORGO_VERSION: "0.22.0" jobs: links: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Cache orgo id: cache-orgo uses: actions/cache@v6 with: path: ~/.cargo/bin/orgo key: orgo-${{ env.ORGO_VERSION }}-${{ runner.os }} - name: Install orgo if: steps.cache-orgo.outputs.cache-hit != 'true' run: cargo install orgo --version "$ORGO_VERSION" --locked # orgo directly, not build.py. A production build rewrites # img.cleberg.net URLs to /img/, which is served from a different docroot # and would look like 198 broken links here. The development output keeps # them absolute, so the image host gets checked for real. - name: Build the site run: orgo build content -o .build-dev --strict # Internal links are the half worth gating on: they are entirely within # this repo, so a failure is always a real defect and always fixable here. - name: Internal links uses: lycheeverse/lychee-action@v2.9.0 with: args: >- --offline --root-dir ${{ github.workspace }}/.build-dev --include-verbatim '.build-dev/**/*.html' fail: true output: internal.md # External links are reported, not gated. A dead third-party link is worth # knowing about, but it is not a reason to block the site. - name: External links uses: lycheeverse/lychee-action@v2.9.0 with: args: >- --root-dir ${{ github.workspace }}/.build-dev --exclude '\.onion' --exclude '^https?://(localhost|127\.0\.0\.1|0\.0\.0\.0)' --exclude '(\{|%7B|client_id=$)' --max-concurrency 8 --max-retries 2 --timeout 20 --accept 200,206,301,302,303,307,308,401,403,429 --include-verbatim '.build-dev/**/*.html' fail: false output: external.md # `fail: false` stops a dead third-party link failing the job — but it also # swallows lychee refusing to start at all, which reports success while # checking nothing. Assert the report has a summary table in it. - name: External step actually ran run: | if ! grep -q "Total" external.md; then echo "::error::lychee produced no summary — it did not run" cat external.md exit 1 fi - name: Summary if: always() run: | { echo "## Internal"; cat internal.md 2>/dev/null || echo "no report"; echo; echo "## External"; cat external.md 2>/dev/null || echo "no report"; } >> "$GITHUB_STEP_SUMMARY" - uses: actions/upload-artifact@v7 if: always() with: name: link-reports path: | internal.md external.md