Skip to content

Commit 681a533

Browse files
committed
Add markdown link sanity workflow
1 parent 8c67181 commit 681a533

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Markdown Link Sanity
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
markdown-links:
14+
name: Check relative markdown links resolve
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 5
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v6
21+
22+
- name: Check relative links in markdown files
23+
shell: bash
24+
run: |
25+
set -euo pipefail
26+
python3 - <<'PYEOF'
27+
import re, sys, os, glob
28+
29+
link_re = re.compile(r'\[[^\]]*\]\(([^)]+)\)')
30+
broken = []
31+
checkout_root = os.path.abspath(os.getcwd())
32+
33+
for md_file in glob.glob('**/*.md', recursive=True):
34+
# Skip vendored plugin docs: their links target the upstream
35+
# repo layout, not ours.
36+
if md_file.startswith(('.git/', 'Plugins/')):
37+
continue
38+
base_dir = os.path.dirname(md_file)
39+
with open(md_file, encoding='utf-8') as f:
40+
content = f.read()
41+
for match in link_re.finditer(content):
42+
target = match.group(1).strip()
43+
if target.startswith(('http://', 'https://', 'mailto:', '#')):
44+
continue
45+
target_path = target.split('#')[0]
46+
if not target_path:
47+
continue
48+
resolved = os.path.abspath(os.path.join(base_dir, target_path))
49+
if os.path.commonpath([checkout_root, resolved]) != checkout_root:
50+
continue
51+
if not os.path.exists(resolved):
52+
broken.append(f'{md_file}: {target}')
53+
54+
if broken:
55+
print('::error::Broken relative markdown links found:')
56+
for b in broken:
57+
print(f' {b}')
58+
sys.exit(1)
59+
60+
print('All relative markdown links resolve.')
61+
PYEOF

0 commit comments

Comments
 (0)