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