From f12eaff112a306d04239dab3f7200f1575cc7b83 Mon Sep 17 00:00:00 2001 From: jay23606 <4602056+jay23606@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:02:54 -0400 Subject: [PATCH 1/5] ci: check relative Markdown links and anchors Nothing in CI checked the repository's relative links, so a renamed heading broke its anchors silently. scripts/check_markdown_links.py resolves every relative link and image target, and every anchor against GitHub's heading slugs, ignoring code. Fix the one anchor it finds broken. Co-Authored-By: Claude Haiku 5.5 --- .github/actions/check-quality/action.yml | 2 + aci_edge_sandboxes/README.md | 2 +- scripts/check_markdown_links.py | 145 +++++++++++++++++++++++ scripts/test_check_markdown_links.py | 46 +++++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 scripts/check_markdown_links.py create mode 100644 scripts/test_check_markdown_links.py diff --git a/.github/actions/check-quality/action.yml b/.github/actions/check-quality/action.yml index 9f735e8eb..623631a62 100644 --- a/.github/actions/check-quality/action.yml +++ b/.github/actions/check-quality/action.yml @@ -31,6 +31,8 @@ runs: set -euo pipefail python -m ruff check scripts .github/specula python -m compileall -q .github/specula + python scripts/check_markdown_links.py + python -m unittest discover -s scripts -p "test_check_markdown_links.py" ${SHELLCHECK} --shell=sh \ guest/common/init guest/alpine/nvx-container-enter \ guest/alpine/nvx-container-launch \ diff --git a/aci_edge_sandboxes/README.md b/aci_edge_sandboxes/README.md index 616aaadc6..121ed7b47 100644 --- a/aci_edge_sandboxes/README.md +++ b/aci_edge_sandboxes/README.md @@ -1030,7 +1030,7 @@ An MXC `StatefulSandboxBackend` adapter maps onto this crate as follows: | `policy.network_egress` rules | `EgressPolicy` rules, field for field | | `working_directory` | `ExecRequest::with_cwd(nvx::guest_path(...))`; a directory that the workload cannot enter ends with `Failed(WorkingDirectory)` | | `process.env`, `process.inheritDefaultEnv` | `ProcessSpec::env`, `None` when omitted, and `inherit_default_env`, or `ExecRequest::with_envs` and `with_inherit_default_env`; see [Environment](#environment) | -| `runtimeConfig.networkProxy` | `ProvisionRequest::with_network_proxy` with the `agent` backend; see [Native proxy and forwarded ports](#native-proxy-and-forwarded-ports) | +| `runtimeConfig.networkProxy` | `ProvisionRequest::with_network_proxy` with the `agent` backend; see [Native proxy and forwarded ports](#native-proxy-and-host-loopback) | A proof-of-concept MXC adapter, `nvx_backend`, implements this mapping. It runs each phase in its own process against a real VM and consumes exec pipes diff --git a/scripts/check_markdown_links.py b/scripts/check_markdown_links.py new file mode 100644 index 000000000..7d5363c28 --- /dev/null +++ b/scripts/check_markdown_links.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Check relative links, images, and anchors in tracked Markdown files. + +Every relative link or image target must exist in the working tree, and every +anchor must match a heading slug, using GitHub's rules, in the target file. Code +spans and fenced code blocks are ignored. Files under ``openvmm/`` are vendored +and skipped. +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path +from urllib.parse import unquote + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKIPPED_PREFIXES = ("openvmm/",) + +FENCE = re.compile(r"^\s*(```|~~~)") +HEADING = re.compile(r"^\s{0,3}#{1,6}\s+(?P.+?)\s*#*\s*$") +INLINE_CODE = re.compile(r"`[^`]*`") +LINK = re.compile(r"!?\[[^\]]*\]\(\s*[^)\s>]+)>?(?:\s+\"[^\"]*\")?\s*\)") +HTML_TARGET = re.compile(r"""\b(?:href|src)\s*=\s*["'](?P[^"']+)["']""") +HTML_ID = re.compile(r"""\b(?:id|name)\s*=\s*["'](?P[^"']+)["']""") +SCHEME = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:") + + +def github_slug(heading: str) -> str: + """Return the anchor GitHub generates for a heading's text.""" + text = INLINE_CODE.sub(lambda match: match.group(0).strip("`"), heading) + text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text) + kept = [ + "-" if char == " " else char + for char in text.strip().lower() + if char in " -_" or char.isalnum() + ] + return "".join(kept) + + +def anchors_in(markdown: str) -> set[str]: + """Return every anchor a Markdown document defines, including duplicate suffixes.""" + anchors: set[str] = set() + seen: dict[str, int] = {} + in_fence = False + for line in markdown.splitlines(): + if FENCE.match(line): + in_fence = not in_fence + continue + if in_fence: + continue + heading = HEADING.match(line) + if heading: + slug = github_slug(heading.group("text")) + count = seen.get(slug, 0) + seen[slug] = count + 1 + anchors.add(slug if count == 0 else f"{slug}-{count}") + anchors.update(HTML_ID.findall(line)) + return anchors + + +def targets_in(markdown: str) -> list[tuple[int, str]]: + """Return (line number, target) for each link outside code.""" + found: list[tuple[int, str]] = [] + in_fence = False + for number, line in enumerate(markdown.splitlines(), start=1): + if FENCE.match(line): + in_fence = not in_fence + continue + if in_fence: + continue + prose = INLINE_CODE.sub("", line) + for match in LINK.finditer(prose): + found.append((number, match.group("target"))) + for match in HTML_TARGET.finditer(prose): + found.append((number, match.group("target"))) + return found + + +def _in_skipped_tree(path: Path) -> bool: + """Return whether a path lies in a vendored tree this check does not cover.""" + try: + relative = path.resolve().relative_to(REPO_ROOT).as_posix() + except ValueError: + return False + return relative.startswith(SKIPPED_PREFIXES) + + +def check_target(source: Path, number: int, target: str) -> str | None: + """Return a problem description for one link, or None if it resolves.""" + if target.startswith("//") or SCHEME.match(target): + return None + path_part, _, fragment = target.partition("#") + if path_part: + base = REPO_ROOT if path_part.startswith("/") else source.parent + destination = base / unquote(path_part.lstrip("/")) + if _in_skipped_tree(destination): + return None + if not destination.exists(): + return f"{source}:{number}: missing target '{target}'" + else: + destination = source + if fragment and destination.is_file() and destination.suffix == ".md": + markdown = destination.read_text(encoding="utf-8") + if unquote(fragment) not in anchors_in(markdown): + return f"{source}:{number}: no heading for anchor '#{fragment}' in {destination.name}" + return None + + +def tracked_markdown() -> list[Path]: + """Return the tracked Markdown files this check covers.""" + listing = subprocess.run( + ["git", "ls-files", "--", "*.md"], + cwd=REPO_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.splitlines() + return [ + REPO_ROOT / name for name in listing if not name.startswith(SKIPPED_PREFIXES) + ] + + +def main() -> int: + problems: list[str] = [] + for source in tracked_markdown(): + text = source.read_text(encoding="utf-8") + for number, target in targets_in(text): + problem = check_target(source, number, target) + if problem: + problems.append(problem) + for problem in problems: + print(problem, file=sys.stderr) + if problems: + print(f"{len(problems)} broken Markdown link(s)", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_check_markdown_links.py b/scripts/test_check_markdown_links.py new file mode 100644 index 000000000..508d403fb --- /dev/null +++ b/scripts/test_check_markdown_links.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Tests for the Markdown link and anchor checker.""" + +from __future__ import annotations + +import unittest + +from check_markdown_links import anchors_in, github_slug, targets_in + + +class GithubSlugTests(unittest.TestCase): + def test_lowercases_drops_punctuation_and_joins_words_with_hyphens(self) -> None: + self.assertEqual( + github_slug("Native proxy and host loopback"), + "native-proxy-and-host-loopback", + ) + self.assertEqual(github_slug("3.2 Clock & time"), "32-clock--time") + + def test_keeps_underscores_and_code_text(self) -> None: + self.assertEqual(github_slug("The `nvx_tools` module"), "the-nvx_tools-module") + + +class AnchorTests(unittest.TestCase): + def test_duplicate_headings_get_numbered_suffixes(self) -> None: + anchors = anchors_in("# Setup\n\n## Setup\n\n## Setup\n") + self.assertEqual(anchors, {"setup", "setup-1", "setup-2"}) + + def test_headings_inside_fences_do_not_count(self) -> None: + anchors = anchors_in("```\n# Not a heading\n```\n\n# Real\n") + self.assertEqual(anchors, {"real"}) + + +class TargetTests(unittest.TestCase): + def test_links_in_code_are_ignored(self) -> None: + text = "`[inline](missing.md)`\n\n```\n[fenced](missing.md)\n```\n\n[live](real.md)\n" + self.assertEqual(targets_in(text), [(7, "real.md")]) + + def test_images_and_html_targets_are_found(self) -> None: + text = '![diagram](img/a.png)\nx\n' + self.assertEqual(targets_in(text), [(1, "img/a.png"), (2, "other.md#x")]) + + +if __name__ == "__main__": + unittest.main() From d523175e49949516f2daa368001bac83a2454660 Mon Sep 17 00:00:00 2001 From: jay23606 <4602056+jay23606@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:12:37 -0400 Subject: [PATCH 2/5] ci: match GitHub's Markdown rules in the link checker Fences close only on their own character and length. Setext headings, code spans of any backtick length, multiline labels, and reference-style definitions are handled. Slugs come from rendered heading text, and duplicate suffixes skip slugs already taken. Each rule has a regression test. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com> --- scripts/check_markdown_links.py | 148 +++++++++++++++++++-------- scripts/test_check_markdown_links.py | 50 ++++++++- 2 files changed, 157 insertions(+), 41 deletions(-) diff --git a/scripts/check_markdown_links.py b/scripts/check_markdown_links.py index 7d5363c28..d826a1138 100644 --- a/scripts/check_markdown_links.py +++ b/scripts/check_markdown_links.py @@ -5,9 +5,10 @@ """Check relative links, images, and anchors in tracked Markdown files. Every relative link or image target must exist in the working tree, and every -anchor must match a heading slug, using GitHub's rules, in the target file. Code -spans and fenced code blocks are ignored. Files under ``openvmm/`` are vendored -and skipped. +anchor must match a heading slug in the target file, using GitHub's rules. +Inline links, reference-style definitions, and HTML ``href`` and ``src`` +attributes are checked. Fenced code blocks and code spans are ignored. Files +under ``openvmm/`` are vendored and skipped. """ from __future__ import annotations @@ -21,19 +22,75 @@ REPO_ROOT = Path(__file__).resolve().parent.parent SKIPPED_PREFIXES = ("openvmm/",) -FENCE = re.compile(r"^\s*(```|~~~)") -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+(?P.+?)\s*#*\s*$") -INLINE_CODE = re.compile(r"`[^`]*`") -LINK = re.compile(r"!?\[[^\]]*\]\(\s*[^)\s>]+)>?(?:\s+\"[^\"]*\")?\s*\)") +FENCE_OPEN = re.compile(r"^ {0,3}(?P`{3,}|~{3,})(?P.*)$") +FENCE_CLOSE = re.compile(r"^ {0,3}(?P`{3,}|~{3,})[ \t]*$") +HEADING = re.compile(r"^ {0,3}#{1,6}[ \t]+(?P.+?)[ \t]*#*[ \t]*$") +SETEXT_UNDERLINE = re.compile(r"^ {0,3}(?:=+|-+)[ \t]*$") +# A code span opens and closes with the same run length and may span lines. +CODE_SPAN = re.compile(r"(?`+)(?!`)(?P.*?)(?(?:[^\[\]]|\[[^\]]*\])*)\]" + r"\([ \t\n]*[^)\s>]+)>?(?:[ \t\n]+\"[^\"]*\")?[ \t\n]*\)" +) +REFERENCE_DEFINITION = re.compile( + r"^ {0,3}\[(?P