Skip to content

ci: check relative Markdown links and anchors - #571

Draft
Pedro Henrique Penna (ppenna) wants to merge 1 commit into
devfrom
agents/fix-issue-563-no-commit
Draft

Pedro Henrique Penna (ppenna) wants to merge 1 commit into
devfrom
agents/fix-issue-563-no-commit

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #563

Summary

Add a link check to the Lint, typing, and formatting job. scripts/check_markdown_links.py resolves every relative link, image, and anchor in the tracked Markdown files as GitHub renders them, and every doc/ 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-obligations silently, because GitHub opens the page at its top. An older link had already broken that way: aci_edge_sandboxes/README.md linked #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 only

  • Resolves every relative link and image target of the tracked Markdown files outside openvmm/, whether an inline link, a reference definition, or an HTML href or src attribute, 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.
  • Resolves each anchor against the slugs that GitHub generates from the rendered text of the headings, including the -1, -2 suffixes of repeated headings, and against HTML id and name attributes. Other files accept only line anchors, such as #L10, within their length.
  • Scans each document's blocks as CommonMark nests them in block quotes, list items, and GitHub's footnote definitions, with lazy continuation lines, and strips their markers before parsing inline content. As on GitHub, front matter, fenced and indented code, and HTML comments and blocks hold no links, while the definitions and headings inside those containers count. A fence ends at a closing run that continues its opener's containers, or with those containers, and so does a raw HTML block.
  • 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. Heading anchors come from the same parse, so a link keeps its text, an autolink its address, and a code span its code. A footnote renders, with its fn- and fnref- 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.
  • 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, even one whose parentheses do not balance. In heading text, a full, collapsed, or shortcut reference link or image resolves only when a definition holds its label.
  • Resolves links into 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.
  • 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, doc/notes.md and doc/input.patch in scripts/test_nvx_tools.py and 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.
  • Prints path:line: message, or ::error annotations 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-quality runs the check in a new Check Markdown links step.
  • validate-nvx runs the new scripts/test_markdown_links.py on Linux and Windows, as test_cli_validation_runs_every_script_test_module requires. 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's runtimeConfig.networkProxy row links #native-proxy-and-host-loopback.
  • doc/contribute.md lists the check among the quality gates, doc/setup.md adds its command to the development checks and notes that it reads the files that Git tracks, so a new file must be staged first, and doc/project-structure.md lists the script.

Reviewer notes

  • The check implements the part of CommonMark that decides where links can occur, the block quotes, list items, and leaf blocks, and the inline syntax of links and code spans, rather than depending on a Markdown library. A false failure blocks CI, so it skips constructs that it cannot resolve unambiguously rather than reporting them.
  • The two existing links into openvmm/, in .github/copilot-instructions.md and .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

  • At this head, 0d3fb047a, rebased on dev at 4d48dcd25, 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 rendered href and src values.
  • For 107 targeted snippets, including each construct that the five reviews found and its variants in block quotes, list items, footnotes, tables, and HTML blocks, the targets and anchors that the check extracts equal those of GitHub's Markdown API.
  • python scripts/check_markdown_links.py passes 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 removed doc/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 52 CiConfigurationTests and the check-quality wiring test pass, and scripts/test_tool_versions.py's 17 tests pass, on Windows.
  • ruff check, ruff format --check, pyright for Linux and Windows, and compileall.

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-nvx suites, including the new tests on Linux. classify-ci-changes selects the NVX microVM tests and the workloads, because aci_edge_sandboxes/README.md is under a test input path, and skips both OpenVMM suites.

Copilot AI balanced review requested due to automatic review settings October 15, 2026 16:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Parsing defects can reject valid Markdown and miss broken links in the required quality gate.

5 open findings
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.

Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 18:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 18:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Remaining parsing errors can reject valid documentation or miss real links in the required quality gate.

3 open findings
3 resolved since last review

🧠 Review effort: Balanced

Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Comment thread scripts/check_markdown_links.py Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 19:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/check_markdown_links.py Outdated


def _unescape(text: str) -> str:
return html.unescape(_ESCAPED.sub(r"\1", text))
Comment thread scripts/check_markdown_links.py Outdated
Comment on lines +500 to +502
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
Comment thread scripts/check_markdown_links.py Outdated
Comment on lines +528 to +532
elif _HTML_BLOCK.match(rest) or (
leaf != "paragraph" and _HTML_TAG_LINE.fullmatch(rest)
):
close(number)
leaf, start = "html", number
Comment thread scripts/check_markdown_links.py Outdated
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>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: check relative Markdown links and anchors

2 participants