Link Checker #789
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 Checker | |
| on: | |
| push: | |
| branches: [ "master" ] | |
| pull_request: | |
| branches: [ "master" ] | |
| schedule: | |
| - cron: "0 0 * * *" | |
| workflow_dispatch: | |
| permissions: { } | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| # Original source, at commit with id 444094f5: | |
| # https://github.com/kdeldycke/workflows/blob/main/.github/workflows/lint.yaml | |
| jobs: | |
| linkChecker: | |
| name: Link Checker | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write # required for peter-evans/create-issue-from-file | |
| contents: read | |
| # Manually manage the life-cycle of issues created in this job because the | |
| # `create-issue-from-file` action blindly creates issues ad-nauseam. | |
| # See also: https://github.com/peter-evans/create-issue-from-file/issues/298 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Decide whether to run lychee | |
| id: should_run_lychee | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| issues_json=$(gh issue list --state open --author 'github-actions[bot]' --json number,title,createdAt) | |
| num_open_issues=$(echo "$issues_json" | jq 'map(select(.title == "Broken links")) | length') | |
| if [ "$num_open_issues" -gt 0 ]; then | |
| broken_links_found=true | |
| else | |
| broken_links_found=false | |
| fi | |
| # if there are no links known to be broken, then run the link checker once a week only | |
| if [ "$broken_links_found" = "true" ] || [ "$(date -u +%A)" = "Monday" ]; then | |
| should_run_lychee=true | |
| else | |
| should_run_lychee=false | |
| fi | |
| printf 'should_run_lychee=%s\n' "$should_run_lychee" >> "$GITHUB_OUTPUT" | |
| issues_json_single_line=$(echo "$issues_json" | jq -r '@json') | |
| printf 'issues=%s\n' "$issues_json_single_line" >> "$GITHUB_OUTPUT" | |
| - name: Restore lychee cache | |
| id: restore_cache | |
| if: steps.should_run_lychee.outputs.should_run_lychee == 'true' | |
| uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: .lycheecache | |
| key: cache-lychee-${{ github.sha }} | |
| restore-keys: cache-lychee- | |
| - name: Link Checker | |
| id: lychee_run | |
| if: steps.should_run_lychee.outputs.should_run_lychee == 'true' | |
| uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 | |
| with: | |
| args: | | |
| --verbose | |
| --no-progress | |
| --cache | |
| --cache-exclude-status 400..=599 | |
| '.' | |
| continue-on-error: true | |
| # Delete the old cache on hit to emulate a cache update. | |
| # | |
| # See also: https://github.com/actions/cache/issues/342. | |
| # Do not make this step condition to `steps.restore_cache.outputs.cache-hit` since that is | |
| # only true if the _primary_ key was hit, not if _any_ key was hit. | |
| - name: Delete old lychee cache | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| MATCHED_KEY: ${{ steps.restore_cache.outputs.cache-matched-key }} | |
| if: steps.should_run_lychee.outputs.should_run_lychee == 'true' | |
| run: gh cache delete ${MATCHED_KEY} | |
| continue-on-error: true | |
| - name: Save lychee cache | |
| if: ${{ !cancelled() && steps.should_run_lychee.outputs.should_run_lychee == 'true' }} | |
| uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: .lycheecache | |
| key: cache-lychee-${{ github.sha }} | |
| - name: Filter issues | |
| if: github.ref == 'refs/heads/master' | |
| id: issue_groups | |
| shell: python | |
| env: | |
| LYCHEE_EXIT_CODE: ${{ steps.lychee_run.outputs.exit_code }} | |
| OPEN_ISSUES_RAW: ${{ steps.should_run_lychee.outputs.issues }} | |
| run: | | |
| import json | |
| import os | |
| from operator import itemgetter | |
| from pathlib import Path | |
| # If lychee was skipped, treat exit_code as 0 (no broken links found). | |
| exit_code = os.environ.get("LYCHEE_EXIT_CODE") | |
| if not exit_code: | |
| exit_code = "0" | |
| print(f"Lychee exit code: {exit_code!r}") | |
| broken_links_found = bool(int(exit_code)) | |
| if broken_links_found: | |
| print("Broken links found: create a new issue or update the existing one.") | |
| else: | |
| print("No broken link found: close all open issues.") | |
| open_issues_raw = os.environ.get("OPEN_ISSUES_RAW") | |
| if open_issues_raw: | |
| open_issues = json.loads(open_issues_raw) | |
| else: | |
| open_issues = [] | |
| issue_to_update: int | None = None | |
| issues_to_close: set[int] = set() | |
| for issue in sorted(open_issues, key=itemgetter("createdAt"), reverse=True): | |
| print(f"Processing {issue!r} ...") | |
| if issue["title"] != "Broken links": | |
| print(f"{issue!r} is not a broken links issue, skip it.") | |
| continue | |
| if broken_links_found and not issue_to_update: | |
| print(f"{issue!r} is the most recent open issue.") | |
| issue_to_update = issue["number"] | |
| else: | |
| print(f"{issue!r} is an old open issue we have to close.") | |
| issues_to_close.add(issue["number"]) | |
| output = f"broken_links_found={str(broken_links_found).lower()}\n" | |
| output += f"issue_to_update={issue_to_update}\n" | |
| output += f"issues_to_close={' '.join(map(str, issues_to_close))}\n" | |
| Path(os.getenv("GITHUB_OUTPUT")).write_text(output) | |
| - name: Print issue groups | |
| if: github.ref == 'refs/heads/master' | |
| env: | |
| BROKEN_LINKS: ${{ steps.issue_groups.outputs.broken_links_found }} | |
| ISSUE_TO_UPDATE: ${{ steps.issue_groups.outputs.issue_to_update }} | |
| ISSUES_TO_CLOSE: ${{ steps.issue_groups.outputs.issues_to_close }} | |
| run: | | |
| echo "Broken links found: ${BROKEN_LINKS}" | |
| echo "Issue to update: ${ISSUE_TO_UPDATE}" | |
| echo "Issues to close: ${ISSUES_TO_CLOSE}" | |
| - name: Create or update issue | |
| if: > | |
| github.ref == 'refs/heads/master' | |
| && fromJSON(steps.issue_groups.outputs.broken_links_found) | |
| uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0 | |
| with: | |
| title: "Broken links" | |
| issue-number: ${{ steps.issue_groups.outputs.issue_to_update }} | |
| content-filepath: ./lychee/out.md | |
| labels: 📚 documentation, 🤖 automated issue | |
| - name: Close old issues | |
| if: > | |
| github.ref == 'refs/heads/master' | |
| && steps.issue_groups.outputs.issues_to_close | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ISSUE_TO_UPDATE: ${{ steps.issue_groups.outputs.issue_to_update }} | |
| ISSUES_TO_CLOSE: ${{ steps.issue_groups.outputs.issues_to_close }} | |
| run: | | |
| for number in ${ISSUES_TO_CLOSE}; do | |
| if [[ "${ISSUE_TO_UPDATE}" =~ ^[0-9]+$ ]]; then | |
| comment="Superseded by #${ISSUE_TO_UPDATE}."; | |
| else | |
| comment="All broken links are fixed. 👏" | |
| fi | |
| gh issue close "$number" --comment "$comment"; | |
| done | |
| - name: Fail workflow on broken links | |
| env: | |
| LYCHEE_EXIT_CODE: ${{ steps.lychee_run.outputs.exit_code }} | |
| run: | | |
| exit_code="${LYCHEE_EXIT_CODE}" | |
| if [ -z "$exit_code" ]; then | |
| # lychee didn't run, treat as no broken links | |
| exit 0 | |
| else | |
| exit "$exit_code" | |
| fi |