Skip to content

Commit 67d213e

Browse files
authored
ci: fail on unresolved relative markdown links (#40)
markdownlint checks link syntax, not whether the target exists, so a moved file or a bad ../ path only surfaced at release time when release.sh --bundle walks the archive. Add a step to the markdownlint job that resolves every relative link in a tracked .md and fails on any that does not exist, reporting all offenders. Root-relative links resolve from the repo root as GitHub renders them; external, mailto, and anchor-only links are skipped.
1 parent 752c0a3 commit 67d213e

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

.github/workflows/main.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,36 @@ jobs:
284284
node-version: 22
285285
- name: Run markdownlint
286286
run: npx markdownlint-cli2 "**/*.md"
287+
- name: Check relative markdown links resolve
288+
# markdownlint checks link SYNTAX, not whether the target exists. This walks
289+
# every relative link in a tracked .md and fails on any that does not resolve,
290+
# so a moved file or a bad ../ path is caught here instead of only by the
291+
# release-time bundle build. Root-relative (/foo) links resolve from the repo
292+
# root, as GitHub renders them; external, mailto, and pure-anchor links are skipped.
293+
run: |
294+
python3 - <<'PY'
295+
import os, re, subprocess, sys
296+
files = subprocess.check_output(["git", "ls-files", "*.md"], text=True).split()
297+
link_re = re.compile(r'\]\(\s*<?([^)>\s]+)')
298+
bad = []
299+
for f in files:
300+
d = os.path.dirname(f)
301+
with open(f, encoding="utf-8") as fh:
302+
text = fh.read()
303+
for m in link_re.finditer(text):
304+
target = m.group(1)
305+
if target.startswith(("http://", "https://", "mailto:", "tel:", "#")):
306+
continue
307+
path = target.split("#")[0].split("?")[0]
308+
if not path:
309+
continue
310+
resolved = path.lstrip("/") if path.startswith("/") else os.path.join(d, path)
311+
if not os.path.exists(os.path.normpath(resolved)):
312+
bad.append(f"{f}: [{target}] -> {os.path.normpath(resolved)}")
313+
if bad:
314+
print("Broken relative markdown links:")
315+
for b in bad:
316+
print(" " + b)
317+
sys.exit(1)
318+
print(f"OK: all relative markdown links resolve ({len(files)} files checked)")
319+
PY

0 commit comments

Comments
 (0)