|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Canonical check: internal doc links must be relative (../path/file.mdx), not |
| 4 | +# absolute /docs/... routes. Relative links are validated at build time by |
| 5 | +# Docusaurus' onBrokenMarkdownLinks; absolute route links are not. |
| 6 | +# |
| 7 | +# This is the single source of truth shared by CI (.github/workflows/main.yml) |
| 8 | +# and the local pre-commit hook (.husky/pre-commit) — like `pnpm format:mdx`, |
| 9 | +# there is one definition of the rule, not two driftable copies. |
| 10 | +# |
| 11 | +# It only flags absolute /docs links in NEWLY PROPOSED content: it reads a |
| 12 | +# word-level diff and inspects added tokens only. Editing a typo on a line that |
| 13 | +# already contains an absolute link does not flag that link (the link stays a |
| 14 | +# context token); introducing or editing a /docs link does. Pre-existing debt |
| 15 | +# in untouched lines is never the contributor's problem. |
| 16 | +# |
| 17 | +# CONTRIBUTING.md is excluded: it is not part of the docs build and its /docs |
| 18 | +# link is an intentional, illustrative example. |
| 19 | +# |
| 20 | +# Usage: |
| 21 | +# scripts/check-relative-links.sh --staged # staged changes (pre-commit) |
| 22 | +# scripts/check-relative-links.sh --range [BASE...HEAD] # PR range (CI); default origin/main...HEAD |
| 23 | +# |
| 24 | +# Exit 0 = clean, 1 = absolute /docs link(s) introduced. |
| 25 | + |
| 26 | +set -euo pipefail |
| 27 | + |
| 28 | +mode="${1:---range}" |
| 29 | +DOCS_PATHSPEC=('docs/*.md' 'docs/*.mdx') |
| 30 | + |
| 31 | +case "$mode" in |
| 32 | + --staged) |
| 33 | + diff_cmd=(git diff --cached --word-diff=porcelain -- "${DOCS_PATHSPEC[@]}") |
| 34 | + ;; |
| 35 | + --range) |
| 36 | + range="${2:-origin/main...HEAD}" |
| 37 | + diff_cmd=(git diff --word-diff=porcelain "$range" -- "${DOCS_PATHSPEC[@]}") |
| 38 | + ;; |
| 39 | + *) |
| 40 | + echo "usage: $0 [--staged | --range [BASE...HEAD]]" >&2 |
| 41 | + exit 2 |
| 42 | + ;; |
| 43 | +esac |
| 44 | + |
| 45 | +# Walk the word-diff porcelain stream. For each new-file line, remember whether |
| 46 | +# any *added* token on it contains an absolute '](/docs' link; report that line |
| 47 | +# if so. New-file line numbers are tracked from each hunk's @@ header, advancing |
| 48 | +# on every completed line except pure deletions (old-file-only). |
| 49 | +findings=$( |
| 50 | + "${diff_cmd[@]}" | awk ' |
| 51 | + function reset() { hasrem=0; haskept=0; flag=0 } |
| 52 | + /^diff --git / { infile=0; reset(); next } |
| 53 | + /^--- / { next } |
| 54 | + /^\+\+\+ / { path=substr($0,7); sub(/\t.*/,"",path); infile=1; next } |
| 55 | + /^@@ / { match($0,/\+[0-9]+/); newno=substr($0,RSTART+1,RLENGTH-1)+0; reset(); next } |
| 56 | + !infile { next } |
| 57 | + /^~/ { |
| 58 | + is_removal_only = (hasrem && !haskept) |
| 59 | + if (!is_removal_only) { |
| 60 | + if (flag && path !~ /CONTRIBUTING\.md$/) |
| 61 | + print path ":" newno ": absolute /docs link introduced in added content — use a relative ../path/file.mdx link" |
| 62 | + newno++ |
| 63 | + } |
| 64 | + reset(); next |
| 65 | + } |
| 66 | + { |
| 67 | + pfx = substr($0,1,1); tok = substr($0,2) |
| 68 | + if (pfx == "-") { hasrem = 1 } |
| 69 | + else if (pfx == "+") { haskept = 1; if (tok ~ /\]\(\/docs/) flag = 1 } |
| 70 | + else { haskept = 1 } # context token (leading space) |
| 71 | + } |
| 72 | + ' |
| 73 | +) |
| 74 | + |
| 75 | +if [ -n "$findings" ]; then |
| 76 | + echo "$findings" |
| 77 | + echo "" |
| 78 | + echo "Fix: replace the absolute /docs/... link with a relative link ending in .mdx" |
| 79 | + echo "(e.g. ../getting-started/setup.mdx). See docs/platforms/anchor-platform/CONTRIBUTING.md." |
| 80 | + exit 1 |
| 81 | +fi |
| 82 | + |
| 83 | +echo "All links check out" |
| 84 | +exit 0 |
0 commit comments