Skip to content

Commit a9cb82d

Browse files
ElliotFriendclaude
andauthored
ci: enforce relative internal doc links + pre-commit hint (#2550)
* fix: use relative .mdx links on Tier 1 Orgs page Replace absolute /docs/... links with relative ../path/file.mdx links per Docusaurus convention (docs/platforms/anchor-platform/CONTRIBUTING.md). Relative links with the file extension are validated at build time by onBrokenMarkdownLinks, unlike absolute route links. Fixes #2546 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: convert absolute /docs links to relative .mdx repo-wide Sweeps the remaining absolute /docs/... links (7 occurrences across 5 files) to relative ../path/file.mdx links per the repo convention. Relative links with the file extension are validated at build time by onBrokenMarkdownLinks; absolute route links are not. Tier 1 Orgs page is handled separately in #2546. CONTRIBUTING.md is left untouched: it is excluded from the docs build and its /docs link is an intentional illustrative example. Part of #2547 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: enforce relative internal doc links; add pre-commit hint Sub-task 2 of #2547. Adds two layers preventing absolute /docs/... links from regressing: - CI gate (main.yml): repo-wide grep in the mdx-format job, fails the PR if any docs/ .md/.mdx file contains a `](/docs` link. Unbypassable, setup-independent — the real guarantee. - Pre-commit hint (.husky/pre-commit): staged-files-only grep for fast local feedback. Scoped to staged files so pre-existing links in untouched files never block unrelated commits. Both exclude CONTRIBUTING.md, which is not part of the docs build and whose /docs link is an intentional illustrative example. Part of #2547 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: check for new absolute /docs links in diff only, via shared script Replace the repo-wide grep with a single canonical script (scripts/check-relative-links.sh) shared by CI and the pre-commit hook, mirroring how `pnpm format:mdx` avoids duplicated prettier invocations. The check now inspects a word-level diff (git diff --word-diff=porcelain) and flags absolute /docs links only in newly proposed content — added tokens. Fixing a typo on a line that already contains an absolute link no longer blocks the commit; only introducing or editing a /docs link does. Contributors are never punished for pre-existing debt in untouched lines. - scripts/check-relative-links.sh: --staged (pre-commit) and --range (CI) - main.yml: fetch-depth 0 so the PR base is available to diff against - package.json: add `check:links` alias Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * add a small status update that links check out okay * fix weird space in script comment --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 46b9c9f commit a9cb82d

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

.github/workflows/main.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ jobs:
2222
steps:
2323
- name: Checkout App Repo
2424
uses: actions/checkout@v7
25+
with:
26+
# Full history so we can diff the PR against its base branch and only
27+
# flag links introduced by this PR, not pre-existing repo content.
28+
fetch-depth: 0
2529

2630
- name: Install pnpm
2731
uses: pnpm/action-setup@v6
@@ -46,6 +50,15 @@ jobs:
4650
if: ${{ failure() }}
4751
run: pnpm diff:mdx && echo "::error::Prettier static analysis failed"
4852

53+
- name: No new absolute internal /docs links
54+
# Canonical rule lives in scripts/check-relative-links.sh (shared with
55+
# the pre-commit hook). Only flags absolute /docs links introduced by
56+
# this PR's diff, not pre-existing content in untouched lines.
57+
run: |
58+
base="${{ github.base_ref }}"
59+
scripts/check-relative-links.sh --range "origin/${base:-main}...HEAD" \
60+
|| { echo "::error::New absolute /docs link(s) found above."; exit 1; }
61+
4962
5063
5164
build:

.husky/pre-commit

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
G='\033[0;32m'
22
P='\033[0;35m'
3+
R='\033[0;31m'
34
CLEAN='\033[0;0m'
45

56
pnpm check:mdx || (echo -e "${G}Hint:${CLEAN} execute ${P}pnpm format:mdx${CLEAN} to format files" && exit 1)
7+
8+
# No new absolute internal /docs links in staged docs. Canonical rule lives in
9+
# scripts/check-relative-links.sh (shared with CI) — only flags links introduced
10+
# in staged changes, never pre-existing content in untouched lines.
11+
scripts/check-relative-links.sh --staged || {
12+
echo -e "${R}Error:${CLEAN} absolute /docs link(s) found above. Use a relative ${P}../path/file.mdx${CLEAN} link with the .mdx extension instead."
13+
exit 1
14+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"format:mdx": "prettier --config .prettierrc.js --write \"{docs,src/pages,meeting-notes}/**/*.{md,mdx}\"",
2424
"ci-format:mdx": "prettier --config .prettierrc.js --write --log-level silent \"{docs,src/pages,meeting-notes}/**/*.{md,mdx}\"",
2525
"check:mdx": "prettier --config .prettierrc.js -c \"{docs,src/pages,meeting-notes}/**/*.{md,mdx}\"",
26+
"check:links": "scripts/check-relative-links.sh --range",
2627
"ci:mdx": "prettier --config .prettierrc.js \"{docs,src/pages,meeting-notes}/**/*.{md,mdx}\" -l --no-editorconfig",
2728
"diff:mdx": "pnpm ci-format:mdx && git diff -- . ':(exclude)package-lock.json' ':(exclude)package.json' ':(exclude)yarn.lock' ':(exclude)pnpm-lock.yaml' | awk \"/diff --git/ {found=1} found {print}\"",
2829
"lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix",

scripts/check-relative-links.sh

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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

Comments
 (0)