Repository navigation
ci: check relative Markdown links and anchors - #571
Pedro Henrique Penna (ppenna) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Parsing defects can reject valid Markdown and miss broken links in the required quality gate.
5 open findings
Reference definitions in block containers are not recognized · New Indented code blocks are incorrectly scanned for broken links · New Fence closers are misdetected inside prefixed fenced content · New Heading link cleanup mishandles destinations with balanced parentheses · New Empty hyperlink destinations are incorrectly reported as broken · New
What changed in this PR
Adds a dependency-free link checker to NVX’s quality gate to catch broken documentation references.
Changes:
- Checks tracked Markdown links, anchors, and mentioned
doc/paths. - Adds parser and repository tests to Linux and Windows validation.
- Fixes the outdated network-proxy anchor and documents the check.
| File | Description |
|---|---|
scripts/test_markdown_links.py |
Tests parsing, resolution, and command behavior. |
scripts/check_markdown_links.py |
Implements link and documentation-path validation. |
doc/setup.md |
Documents the command and staging requirement. |
doc/project-structure.md |
Lists the new checker. |
doc/contribute.md |
Documents the new quality gate. |
aci_edge_sandboxes/README.md |
Corrects the network-proxy link. |
.github/actions/validate-nvx/action.yml |
Runs tests on Linux and Windows. |
.github/actions/check-quality/action.yml |
Adds the link-check step. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
3fd45f5 to
2407da7
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Parsing and README-resolution defects can reject valid documentation or let broken links pass the required quality gate.
6 open findings
Reject bare reference destinations with unbalanced parentheses · New Parse complete reference definitions including continuation titles · New Ignore escaped tags and comments when collecting HTML attributes · New Mask container prefixes on continuation lines · New Preserve unresolved reference labels in headings · New Resolve directory anchors using the tracked README filename · New
5 resolved since last review
Empty hyperlink destinations are incorrectly reported as broken Heading link cleanup mishandles destinations with balanced parentheses Fence closers are misdetected inside prefixed fenced content Indented code blocks are incorrectly scanned for broken links Reference definitions in block containers are not recognized
🧠 Review effort: Balanced
2407da7 to
399a1b7
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Confirmed parser errors can reject valid documentation and miss broken links in the new quality gate.
7 open findings
Avoid extracting links from HTML attribute values · New Exclude Markdown link titles from HTML attribute scanning · New Restrict reference definitions to their current block · New Parse footnote definitions as Markdown bodies · New Contain raw HTML blocks within active containers · New Do not split ordinary paragraphs as table rows · New Resolve shortcut reference images in heading anchors · New
9 resolved since last review
Resolve directory anchors using the tracked README filename Preserve unresolved reference labels in headings Mask container prefixes on continuation lines Ignore escaped tags and comments when collecting HTML attributes Parse complete reference definitions including continuation titles Reject bare reference destinations with unbalanced parentheses Empty hyperlink destinations are incorrectly reported as broken Heading link cleanup mishandles destinations with balanced parentheses Fence closers are misdetected inside prefixed fenced content
🧠 Review effort: Balanced
399a1b7 to
447cdcc
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Confirmed parsing errors can reject valid documentation and miss broken links in the required quality gate.
3 open findings
7 resolved since last review
Resolve shortcut reference images in heading anchors Do not split ordinary paragraphs as table rows Contain raw HTML blocks within active containers Parse footnote definitions as Markdown bodies Restrict reference definitions to their current block Exclude Markdown link titles from HTML attribute scanning Avoid extracting links from HTML attribute values
🧠 Review effort: Balanced
447cdcc to
a346d21
Compare
a346d21 to
0d3fb04
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Reproduced parser errors can reject valid documentation or allow broken links through the required quality gate.
5 open findings
Decode Markdown entities without corrupting destinations or slugs · New Track tab indentation consistently after list markers · New Preserve HTML outside comments during link and anchor extraction · New Recognize multiline CDATA, processing instruction, and declaration blocks · New Prevent ordered lists from interrupting quoted code-span paragraphs · New
3 resolved since last review
🧠 Review effort: Balanced
|
|
||
|
|
||
| def _unescape(text: str) -> str: | ||
| return html.unescape(_ESCAPED.sub(r"\1", text)) |
| spaces = _indent(line, item.end()) | ||
| used = 1 if empty or spaces > 4 else spaces | ||
| containers.append(item.end() - index + used) |
| leaf, fence = "fence", opener[1] | ||
| elif _HTML_COMMENT.match(rest): | ||
| close(number) | ||
| hidden[number] = True |
| elif _HTML_BLOCK.match(rest) or ( | ||
| leaf != "paragraph" and _HTML_TAG_LINE.fullmatch(rest) | ||
| ): | ||
| close(number) | ||
| leaf, start = "html", number |
| rather than continuing a paragraph lazily.""" | ||
| return _indent(rest, 0) < 4 and bool( | ||
| _QUOTE_MARKER.match(rest) | ||
| or _LIST_MARKER.match(rest) |
Nothing in CI checked the repository's relative Markdown links. #529 moved the design documentation, numbered its sections, and retargeted every inbound link by hand, while an older link had already broken: the MXC integration table in aci_edge_sandboxes/README.md linked the anchor native-proxy-and-forwarded-ports of a heading that #512 had renamed to "Native proxy and host loopback". GitHub opens a page at its top when an anchor is missing, so such links break silently, and so do comments that cite a document by path, as the citations of the removed time ABI specification did (#562). Add scripts/check_markdown_links.py, a standard-library check, and run it in check-quality, whose job runs for every pull request that is ready for review, including the documentation-only ones that are most likely to break a link: - It resolves every relative link and image target of the tracked Markdown files, whether an inline link, a reference definition, or an HTML href or src attribute, against the files that Git tracks, so a target that is untracked or differs in case, which GitHub cannot serve, fails too. A directory resolves, and its anchors are those of its README, whatever the README's case. An empty target, such as that of [Self](), links to its own document. - It resolves each anchor against the slugs that GitHub generates from the rendered text of the headings, including the numeric suffixes of repeated headings, and against HTML id and name attributes. Other files accept only line anchors, such as #L10, within their length. - It scans the blocks of each document as cmark nests them in block quotes, list items, and GitHub's footnote definitions, with lazy continuation lines and tabs that advance to the next multiple of four columns, and strips their markers before it parses inline content. As on GitHub, front matter, fenced and indented code, and the seven kinds of HTML blocks hold no Markdown links, while the definitions and headings inside those containers count. - It parses inline content left to right as CommonMark does: code spans, autolinks, and raw HTML are atomic, each destination is consumed with its title, and brackets match so that links do not nest and an image's description holds no links. A footnote renders, with its fn- and fnref- anchors, only where inline content cites it. Each cell of a table parses alone, and the cells past the header's count, which GitHub drops, hold no links. - It tokenizes the raw HTML, in the order that GitHub renders it, as an HTML parser does, to find the tags whose attributes hold targets and anchors. A comment hides everything that renders up to its end, even in later blocks, while a CDATA section, processing instruction, or declaration ends at its first >. - It parses each reference definition at the start of a paragraph whole, within that paragraph, with its title, before the inline content that follows, and accepts any destination without spaces, as GitHub does. In heading text, a reference link or image resolves only when a definition holds its label. - It decodes a destination as cmark does, its character references before its backslash escapes, and an HTML attribute as GitHub does, which keeps its backslashes, and its named references without semicolons, literal. - Links into the openvmm submodule resolve against its pinned commit when it is checked out and are skipped otherwise, as in the quality job, whose checkout does not fetch submodules. - It also resolves, from the repository root, every doc/ path, with any anchor, that a tracked text file mentions. DOC_PATH_FIXTURES lists the test fixtures that name missing documents, such as doc/notes.md in scripts/test_nvx_tools.py, and an entry that its file no longer needs fails the check. A mention followed by a character that could extend the path, such as the * of a pattern, is ambiguous and is skipped. - It reports each problem with its file and line, as an error annotation in GitHub Actions, and then exits with status 1. scripts/test_markdown_links.py covers the slugs, the parsing, the resolution in temporary repositories, including a submodule, the fixtures, and the command line, and validate-nvx runs it on Linux and Windows. Retarget the README link to native-proxy-and-host-loopback, the only broken link that the check found, and document the check in doc/contribute.md, doc/setup.md, and doc/project-structure.md. Fixes #563 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0d3fb04 to
6c90ad2
Compare

Fixes #563
Summary
Add a link check to the
Lint, typing, and formattingjob.scripts/check_markdown_links.pyresolves every relative link, image, and anchor in the tracked Markdown files as GitHub renders them, and everydoc/path that a tracked file mentions. Fix the one broken link that it found.Why
Nothing in CI checked the repository's relative Markdown links. #529 moved the design documentation, numbered its sections, and retargeted every inbound link by hand. Renumbering or renaming a section breaks links such as
3.2-clock-and-time.md#3210-guest-obligationssilently, because GitHub opens the page at its top. An older link had already broken that way:aci_edge_sandboxes/README.mdlinked#native-proxy-and-forwarded-ports, a heading that #512 renamed to "Native proxy and host loopback". Comments that cite a document by path break just as silently, as the citations of the removed time ABI specification did (#562). The quality job runs for every pull request that is ready for review, including the documentation-only ones that skip the workloads and are the most likely to break a link.What changed
scripts/check_markdown_links.py, standard library onlyopenvmm/, whether an inline link, a reference definition, or an HTMLhreforsrcattribute, against the files that Git tracks. A target that is untracked or differs in case fails too, because GitHub cannot serve it, and the check catches case mismatches on Windows and macOS as well. A directory resolves, and its anchors are those of its README, whatever the README's case. An empty target, such as that of[Self]()or<a href="">, links to its own document.-1,-2suffixes of repeated headings, and against HTMLidandnameattributes. Other files accept only line anchors, such as#L10, within their length.fn-andfnref-anchors, only where inline content cites it. Only a header and delimiter row start a table, each cell of which parses alone, and the cells past the header's count, which GitHub drops, hold no links.openvmm/against the pinned commit when the submodule is checked out, and skips them otherwise. The quality job's checkout does not fetch submodules, so CI skips them.doc/path, with any anchor, that a tracked text file mentions.DOC_PATH_FIXTURESlists the test fixtures that name missing documents,doc/notes.mdanddoc/input.patchinscripts/test_nvx_tools.pyand those of the new test module, and an entry that its file no longer needs fails the check. A mention followed by a character that could extend the path, such as the*of a glob, is ambiguous and is skipped.path:line: message, or::errorannotations in GitHub Actions, and exits with status 1 when a target does not resolve, or 2 when Git cannot list the tracked files.CI wiring
check-qualityruns the check in a newCheck Markdown linksstep.validate-nvxruns the newscripts/test_markdown_links.pyon Linux and Windows, astest_cli_validation_runs_every_script_test_modulerequires. It covers the slugs, the parsing, resolution in temporary repositories, including a submodule, the fixtures, and the command line.Fix and documentation
aci_edge_sandboxes/README.md: the MXC table'sruntimeConfig.networkProxyrow links#native-proxy-and-host-loopback.doc/contribute.mdlists the check among the quality gates,doc/setup.mdadds its command to the development checks and notes that it reads the files that Git tracks, so a new file must be staged first, anddoc/project-structure.mdlists the script.Reviewer notes
openvmm/, in.github/copilot-instructions.mdand.github/skills/nvx-openvmm-promote/SKILL.md, return 404 on github.com, which does not serve submodule content. They work in local checkouts, where agents read them, so this PR leaves them unchanged.Validation
0d3fb047a, rebased ondevat4d48dcd25, the check agrees with GitHub's own rendering of all 66 tracked Markdown files: its 425 anchors equal the heading ids that GitHub generates, and its 361 relative link targets equal the renderedhrefandsrcvalues.python scripts/check_markdown_links.pypasses at this head. Without the README fix, that link is the only problem that it reports. In a scratch copy, it caught a renumbered section, a renamed design document, and a code comment that cites the removeddoc/design/time-abi.md.scripts/test_markdown_links.py: 47 tests pass on Windows with Python 3.14 and 3.10.11.scripts/test_nvx_tools.py: the 52CiConfigurationTestsand the check-quality wiring test pass, andscripts/test_tool_versions.py's 17 tests pass, on Windows.ruff check,ruff format --check,pyrightfor Linux and Windows, andcompileall.Left to CI once this pull request is ready for review: the quality job itself, with its pinned ShellCheck and shfmt images, and the full
validate-nvxsuites, including the new tests on Linux.classify-ci-changesselects the NVX microVM tests and the workloads, becauseaci_edge_sandboxes/README.mdis under a test input path, and skips both OpenVMM suites.