Link rot sweep #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Link rot sweep | |
| on: | |
| schedule: | |
| - cron: "17 6 * * 1" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: link-rot-sweep | |
| cancel-in-progress: false | |
| jobs: | |
| sweep: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check every link in README.md | |
| run: | | |
| # check-links exits 1 as soon as any URL is dead/suspect -- that's the | |
| # expected, reportable outcome of a link-rot sweep, not a script | |
| # failure, so `|| true` here (state commit + issue upsert below are | |
| # what actually need to keep running either way). | |
| python3 scripts/check_readme.py check-links --all --state .github/link-rot-state.json --json > /tmp/results.json || true | |
| python3 -c "import json; print('stats:', json.load(open('/tmp/results.json'))['stats'])" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Commit updated sweep state | |
| continue-on-error: true | |
| run: | | |
| if git diff --quiet -- .github/link-rot-state.json; then | |
| echo "No state changes to commit." | |
| else | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add .github/link-rot-state.json | |
| git commit -m "chore(link-rot): update sweep state [skip ci]" | |
| git pull --rebase origin "${{ github.ref_name }}" | |
| git push origin "HEAD:${{ github.ref_name }}" | |
| fi | |
| - name: Apply safe auto-fixes (remove confirmed-dead entries, update moved URLs) | |
| id: prune | |
| if: always() | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Only two categories are safe enough to fix without a human decision: | |
| # entries confirmed dead for 2+ consecutive weeks (with no known redirect), | |
| # and URLs that now redirect to a different working page. Everything else | |
| # (SUSPECT, secondary/prose links, ambiguous matches) is left in the | |
| # report for a human to judge -- see scripts/check_readme.py's cmd_prune | |
| # docstring for the exact rules. | |
| python3 scripts/check_readme.py prune \ | |
| --results /tmp/results.json --state .github/link-rot-state.json \ | |
| --readme README.md --pr-body-out /tmp/pr-body.md > /tmp/prune-summary.json | |
| changed=$(python3 -c "import json; print(json.load(open('/tmp/prune-summary.json'))['changed'])") | |
| if [ "$changed" != "True" ]; then | |
| echo "Nothing to auto-fix this run." | |
| exit 0 | |
| fi | |
| removed=$(python3 -c "import json; print(len(json.load(open('/tmp/prune-summary.json'))['removals']))") | |
| updated=$(python3 -c "import json; print(len(json.load(open('/tmp/prune-summary.json'))['updates']))") | |
| branch="automated/link-rot-fixes" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git checkout -B "$branch" | |
| git add README.md | |
| git commit -m "chore(link-rot): remove $removed confirmed-dead link(s), update $updated moved URL(s)" | |
| git push --force-with-lease origin "HEAD:$branch" | |
| if [ -z "$(gh pr list --repo "${{ github.repository }}" --head "$branch" --state open --json number --jq '.[0].number // empty')" ]; then | |
| gh pr create --repo "${{ github.repository }}" --base "${{ github.ref_name }}" --head "$branch" \ | |
| --title "chore(link-rot): remove $removed confirmed-dead link(s), update $updated moved URL(s)" \ | |
| --body-file /tmp/pr-body.md --label link-rot | |
| else | |
| gh pr edit "$branch" --repo "${{ github.repository }}" --body-file /tmp/pr-body.md | |
| fi | |
| pr_url=$(gh pr view "$branch" --repo "${{ github.repository }}" --json url --jq '.url') | |
| echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT" | |
| - name: Render report body | |
| if: always() | |
| env: | |
| FIX_PR_URL: ${{ steps.prune.outputs.pr_url }} | |
| run: | | |
| if [ -n "$FIX_PR_URL" ]; then | |
| python3 scripts/check_readme.py report --results /tmp/results.json --state .github/link-rot-state.json --fix-pr-url "$FIX_PR_URL" > /tmp/report-body.md | |
| else | |
| python3 scripts/check_readme.py report --results /tmp/results.json --state .github/link-rot-state.json > /tmp/report-body.md | |
| fi | |
| - name: Upsert consolidated link-rot report issue | |
| if: always() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| existing=$(gh issue list --repo "${{ github.repository }}" --search "🔗 Link rot report in:title" --label link-rot --state open --json number --jq '.[0].number // empty') | |
| if [ -n "$existing" ]; then | |
| gh issue edit "$existing" --repo "${{ github.repository }}" --body-file /tmp/report-body.md | |
| else | |
| gh issue create --repo "${{ github.repository }}" --title "🔗 Link rot report" --label link-rot --body-file /tmp/report-body.md | |
| fi |