From a72fcca1168bcea02b6e8833a5bd3bccddea0ee1 Mon Sep 17 00:00:00 2001 From: rivassec Date: Fri, 11 Sep 2026 16:18:12 -0700 Subject: [PATCH] ci: weekly external link-rot check (lychee) Scheduled, not per-PR: external checks are network-flaky and a third-party outage must never block merges. Internal links keep their per-PR guard (check_link_graph.py); this covers the external citations (DOJ, IC3, vendor docs, papers) that rot silently. - scans content/**/*.md + README, https/http schemes only - --base-url resolves root-relative links against the live site - .lycheeignore: Pelican {filename}/{static} syntax, code-example placeholders (localhost, host:port), bot-hostile hosts (LinkedIn/X) - failures open/update a 'link-rot' tracking issue and fail the run - validated locally with lychee 0.24.2: 103 OK, 0 errors, 32 excluded --- .github/workflows/link-rot.yml | 60 ++++++++++++++++++++++++++++++++++ .lycheeignore | 13 ++++++++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/link-rot.yml create mode 100644 .lycheeignore diff --git a/.github/workflows/link-rot.yml b/.github/workflows/link-rot.yml new file mode 100644 index 00000000..4d065ed5 --- /dev/null +++ b/.github/workflows/link-rot.yml @@ -0,0 +1,60 @@ +# External-link rot check. +# +# Scheduled (not per-PR) on purpose: external checks are network-flaky and a +# third-party outage must never block merges. The internal link graph is +# already guarded per-PR by check_link_graph.py; this covers the external +# citations (DOJ, IC3, vendor docs, papers) that rot silently. +# +# Failures open/update a single tracking issue rather than emailing a red run +# nobody reads. Exclusions live in .lycheeignore (placeholders from code +# examples, bot-hostile hosts). +name: Link rot check + +on: + schedule: + - cron: '0 14 * * 1' # Mondays 14:00 UTC + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + lychee: + name: External link check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Check external links in content + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + # Source markdown only; {filename} internal links are non-http and + # skipped via --scheme, and the built site's internal graph has its + # own per-PR guard. + args: >- + --scheme https --scheme http + --base-url 'https://rivassec.com' + --accept '200..=206,429' + --timeout 20 + --max-retries 2 + --max-concurrency 8 + --no-progress + 'content/**/*.md' 'README.md' + output: lychee-report.md + fail: false + + - name: Open or update tracking issue on failures + if: steps.lychee.outputs.exit_code != 0 + uses: peter-evans/create-issue-from-file@v5 + with: + title: 'Link rot: broken external links detected' + content-filepath: lychee-report.md + labels: link-rot + + - name: Fail the run if links are broken + if: steps.lychee.outputs.exit_code != 0 + run: | + echo "::error::lychee found broken external links (see lychee-report.md / tracking issue)" + exit 1 diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 00000000..4aee3c2a --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,13 @@ +# Pelican source-link syntax ({filename}/{static}); resolves at build time and +# the internal link graph has its own per-PR guard (check_link_graph.py). +%7B(filename|static)%7D +\{(filename|static)\} +# Placeholders and local addresses from code examples - never real links. +^https?://localhost +^https?://host(:|$|/) +^https?://127\.0\.0\.1 +# Hosts that block automated checkers while working fine in browsers +# (LinkedIn returns 999 to bots; X/Twitter gates aggressively). +^https?://(www\.)?linkedin\.com +^https?://(www\.)?x\.com +^https?://(www\.)?twitter\.com