From 5e7e60b114f10e4a192f4a27fb078f26c1e0f8e8 Mon Sep 17 00:00:00 2001 From: Morgan Roderick <20321+mroderick@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:53:05 +0100 Subject: [PATCH 1/2] fix(site): link the specs page with its directory URL VitePress renders /specs as /specs.html while the build writes specs/index.html. The link works in production through a 308 redirect, but a local check of the built site reports it as a dead file. Point the nav, sidebar and hero action at /specs/ so the link resolves in both. --- .vitepress/config.mjs | 4 ++-- docs/index.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 From 7b889aeff1d8bfb2f007707bbad6beeb4f4a911f Mon Sep 17 00:00:00 2001 From: Morgan Roderick <20321+mroderick@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:53:13 +0100 Subject: [PATCH 2/2] fix(link-check): fail on broken links and never open issues The workflow ran the checker with fail: false and gated the issue step on a non-zero exit code. When lychee could not start, the step exited 127 and the action still wrote a report, so every scheduled run opened an issue with no content. 63 runs produced 63 issues. Run lychee directly, fail the job when the tool is missing or the report is absent or empty, and never open an issue. Internal links run on every push and PR; external links run on the schedule. Harden the install and the run: bound the download with connect and total timeouts, locate the binary after extraction instead of hardcoding the tarball layout, and run the external check after a failure but not after cancellation. --- .github/workflows/link-check.yml | 60 +++++++++++++++------------- .gitignore | 3 +- .prettierignore | 1 + lychee.toml | 12 ++++++ package.json | 3 +- scripts/link-check.sh | 68 +++++++++++++++++++++++++++----- 6 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 lychee.toml 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/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"