diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 6d3081f..c8ed1ac 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -1,19 +1,30 @@ name: Link Check +# Link checks fail the workflow; they never open issues. A checker that fails +# to run must not look like a pass, and one issue per run buries the signal. +# +# GitHub disables scheduled workflows after 60 days without activity, so a +# quiet period can switch this off. Re-enable it with `gh workflow enable`. + on: push: branches: [main] pull_request: branches: [main] schedule: - - cron: '0 0 * * *' # Daily at midnight + - cron: "0 0 * * *" # Daily at midnight + workflow_dispatch: + +permissions: + contents: read + +env: + LYCHEE_VERSION: 0.24.1 jobs: link-checker: name: Link Checker runs-on: ubuntu-latest - permissions: - issues: write # Required for creating issues from file steps: - uses: actions/checkout@v4 @@ -28,28 +39,21 @@ jobs: - name: Build site run: npm run build - - name: Link Checker - id: lychee - uses: lycheeverse/lychee-action@v2 - with: - # Check all files in the built site - args: | - .vitepress/dist - --root-dir .vitepress/dist - --include-mail=false - --exclude "^file://" - --accept '200..=299,403' - # Don't fail action on broken links (we'll create an issue instead) - fail: false - # Output format - format: markdown - # Output file path - output: lychee/out.md - - - name: Create Issue From File - if: steps.lychee.outputs.exit_code != 0 - uses: peter-evans/create-issue-from-file@v5 - with: - title: Link Checker Report - content-filepath: ./lychee/out.md - labels: report, automated issue \ No newline at end of file + - name: Install lychee + run: | + set -euo pipefail + curl -fsSL --connect-timeout 15 --max-time 300 \ + -o "$RUNNER_TEMP/lychee.tar.gz" \ + "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz" + mkdir -p "$RUNNER_TEMP/lychee-extract" + tar -xzf "$RUNNER_TEMP/lychee.tar.gz" -C "$RUNNER_TEMP/lychee-extract" + install -m 0755 "$(find "$RUNNER_TEMP/lychee-extract" -type f -name lychee -print -quit)" "$RUNNER_TEMP/lychee" + echo "$RUNNER_TEMP" >> "$GITHUB_PATH" + "$RUNNER_TEMP/lychee" --version + + - name: Check internal links + run: scripts/link-check.sh internal + + - name: Check external links + if: ${{ !cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + run: scripts/link-check.sh external diff --git a/.gitignore b/.gitignore index 9a99e21..1d2104f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vitepress/cache/ .vitepress/dist/ -node_modules/ \ No newline at end of file +node_modules/ +lychee-report.md diff --git a/.prettierignore b/.prettierignore index f1d0d62..a7d6746 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ CLAUDE.md .claude docs/plans .vitepress/cache +lychee-report.md diff --git a/.vitepress/config.mjs b/.vitepress/config.mjs index ee0e9d1..4558c66 100644 --- a/.vitepress/config.mjs +++ b/.vitepress/config.mjs @@ -30,7 +30,7 @@ export default defineConfig({ nav: [ { text: "Home", link: "/" }, { text: "Learn", link: "/learn/" }, - { text: "Specs", link: "/specs" }, + { text: "Specs", link: "/specs/" }, { text: "Projects", items: [{ text: "idempot-js", link: "https://js.idempot.dev" }] @@ -58,7 +58,7 @@ export default defineConfig({ "/": [ { text: "Documentation", - items: [{ text: "Specifications", link: "/specs" }] + items: [{ text: "Specifications", link: "/specs/" }] }, { text: "Projects", diff --git a/docs/index.md b/docs/index.md index cbf3bf9..4f5001f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ hero: link: /learn/why - theme: alt text: View Specs - link: /specs + link: /specs/ features: - title: IETF Draft Spec Compliant diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..c820427 --- /dev/null +++ b/lychee.toml @@ -0,0 +1,12 @@ +# Shared settings for the link checker. scripts/link-check.sh and the Link +# Check workflow both read this file. Per-run options (the scheme to check) +# stay on the command line. + +accept = ["200..=299", "403"] +include_mail = false + +# Third-party hosts answer slowly or intermittently. Retry before reporting a +# broken link. +max_retries = 3 +retry_wait_time = 2 +timeout = 20 diff --git a/package.json b/package.json index 97c24f7..ec29f4b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "format:check": "prettier --check \"**/*.{js,jsx,json,md}\"", "lint": "eslint .vitepress/config.mjs", "lint:fix": "eslint .vitepress/config.mjs --fix", - "lint:links": "./scripts/link-check.sh" + "lint:links": "./scripts/link-check.sh", + "lint:links:external": "./scripts/link-check.sh external" }, "lint-staged": { "*.js": [ diff --git a/scripts/link-check.sh b/scripts/link-check.sh index 5286eb4..b5ac9b6 100755 --- a/scripts/link-check.sh +++ b/scripts/link-check.sh @@ -1,9 +1,59 @@ -#!/bin/bash -set -e - -echo "Running lychee link check on dist folder..." -lychee .vitepress/dist \ - --root-dir .vitepress/dist \ - --include-mail=false \ - --exclude "^file://" \ - --accept '200..=299,403' +#!/usr/bin/env bash +# Check links in the built site. +# +# ./scripts/link-check.sh internal # links between pages and assets +# ./scripts/link-check.sh external # links to other hosts +# +# Exits non-zero when a link is broken or when the report is missing, so a +# checker that failed to run cannot look like a pass. Requires lychee 0.24.1 or +# later on PATH and a build in .vitepress/dist. +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +mode=${1:-internal} +case "$mode" in + internal) schemes=(--scheme file) ;; + external) schemes=(--scheme http --scheme https) ;; + *) echo "usage: $0 [internal|external]" >&2; exit 2 ;; +esac + +if [ ! -d .vitepress/dist ]; then + echo "no build found in .vitepress/dist; run npm run build first" >&2 + exit 1 +fi + +report=${LYCHEE_REPORT:-lychee-report.md} +rm -f "$report" + +if lychee .vitepress/dist --root-dir .vitepress/dist "${schemes[@]}" \ + --format markdown --output "$report"; then + status=0 +else + status=$? +fi + +# The summary table is the only proof that lychee ran. A missing report, or one +# without the table, means the tool failed, not that the links are fine. +if ! grep -q 'Total' "$report" 2>/dev/null; then + echo "lychee wrote no report for $mode links; the check did not run" >&2 + cat "$report" >&2 2>/dev/null || true + exit 1 +fi + +# A zero total means lychee ran but checked nothing: a wrong scheme, a wrong +# root, or an empty build. That is not a pass. +total=$(grep -oE 'Total[[:space:]]*\|[[:space:]]*[0-9]+' "$report" | head -1 | grep -oE '[0-9]+$') +if [ -z "$total" ] || [ "$total" -eq 0 ]; then + echo "lychee checked no links for $mode; the check did not run" >&2 + cat "$report" >&2 + exit 1 +fi + +cat "$report" +if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + cat "$report" >> "$GITHUB_STEP_SUMMARY" +fi + +exit "$status"