Skip to content

Link Checker

Link Checker #789

Workflow file for this run

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