Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 32 additions & 28 deletions .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
- 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vitepress/cache/
.vitepress/dist/
node_modules/
node_modules/
lychee-report.md
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ CLAUDE.md
.claude
docs/plans
.vitepress/cache
lychee-report.md
4 changes: 2 additions & 2 deletions .vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]
Expand Down Expand Up @@ -58,7 +58,7 @@ export default defineConfig({
"/": [
{
text: "Documentation",
items: [{ text: "Specifications", link: "/specs" }]
items: [{ text: "Specifications", link: "/specs/" }]
},
{
text: "Projects",
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ hero:
link: /learn/why
- theme: alt
text: View Specs
link: /specs
link: /specs/

features:
- title: IETF Draft Spec Compliant
Expand Down
12 changes: 12 additions & 0 deletions lychee.toml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
68 changes: 59 additions & 9 deletions scripts/link-check.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading