Skip to content

ci: check relative Markdown links and anchors - #571

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
agents/fix-issue-563-no-commit
Oct 16, 2026
Merged

Pedro Henrique Penna (ppenna) merged 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 the href of an HTML link or src of an image, 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, and an image's anchor, such as GitHub's #gh-dark-mode-only theme selector, goes unchecked.
  • Resolves each anchor as GitHub's page script looks a fragment up, in lowercase and with the user-content- prefix of the ids that GitHub renders, which the fragment may hold already, against the slugs that GitHub generates from the rendered text of the Markdown and HTML headings, including the -1, -2 suffixes of repeated headings, and against the id and name attributes of the 53 HTML elements whose ids GitHub's sanitizer keeps. Other files accept only line anchors, such as #L10, within their length.
  • Scans each document's blocks 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 parsing 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. A fence ends at a closing run that continues its opener's containers, or with those containers, and so does an HTML block that only its end condition, such as --> or ]]>, ends.
  • Parses inline content left to right as CommonMark does: code spans, autolinks, including GitHub's bare https:// and www. ones, 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, a code span its code, and a citation its footnote's number. A footnote renders, with its fn- and fnref- anchors, only where inline content cites it. A header and delimiter row start a table, a block of its own that ends where GitHub's table extension ends it, so no setext underline or lazy line follows it, and each of its cells parses alone, while the cells past the header's count, which GitHub drops, hold no links.
  • 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, while inline raw HTML must follow CommonMark's stricter tag rules to count at all. A comment hides everything that renders up to its end, even in later blocks, and the footnotes too when it never ends, while a CDATA section, processing instruction, or declaration ends at its first >. The tags that GitHub's tagfilter extension filters, such as <script> and <iframe>, render as text, and other HTML that GitHub's sanitizer strips, such as <video src> or <div href>, holds no targets.
  • Slugs the text that each heading holds in the tree that libxml2's HTML parser builds from the whole rendered document, where the paragraphs of tight lists, as cmark's last_line_blank bookkeeping finds them, have no <p> tags, as GitHub's anchors do. A start tag closes only the elements that libxml2's htmlStartClose table lets it close, so a paragraph's start tag closes a heading while a code block's or a list's does not, an end tag closes no element of a higher priority than its own, such as a <div>, and a heading closes the heading that it starts in, as an HTML5 parser rebuilds the tree. The text of a heading may thus run on across blocks, and the end tag of an element that an earlier block leaves open, such as a <div>, may close 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, even one whose parentheses do not balance. A definition counts where it renders, and wherever a rendered reference uses it, even in a footnote that does not render. In heading text, a full, collapsed, or shortcut reference link or image resolves only when a definition holds its label, and the delimiters of emphasis and strikethrough match as cmark-gfm's process_emphasis matches them, with its flanking rules and the rule of three, those of each link's text alone.
  • Decodes a destination as cmark does, trimmed and with its character references before its backslash escapes, so [a](c\&copy;.md) targets c\©.md on GitHub, and an HTML attribute as GitHub does, which keeps its backslashes, and its named references without semicolons, literal, and drops only the leading whitespace of a URL. As in HTML, the first attribute of each name, in any case, hides the others.
  • 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, 0c3db01de, rebased on dev at 140d367c8, 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 469 targeted snippets, including each construct that the 20 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: 90 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
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
Copilot AI balanced review requested due to automatic review settings October 15, 2026 20:19

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
Comment thread scripts/check_markdown_links.py
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 16, 2026 02: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.

🟡 Changes recommended

Unresolved parsing errors can reject valid documentation and miss real links in the mandatory 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 16, 2026 05:54

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.

🔵 Needs a closer look

The handwritten Markdown/HTML parser needs human compatibility review, with unresolved anchor-generation errors that can reject valid links.

2 open findings
3 resolved since last review

🧠 Review effort: Balanced

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

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

Confirmed heading-parsing errors can reject valid anchors and incorrectly fail the quality gate.

3 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment thread scripts/check_markdown_links.py
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 16, 2026 06:41

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.

🔵 Needs a closer look

The substantial handwritten parser has unresolved rendering-compatibility findings and needs final human verification.

2 open findings
3 resolved since last review

🧠 Review effort: Balanced

Comment thread scripts/check_markdown_links.py
Comment thread scripts/check_markdown_links.py Outdated
Copilot AI balanced review requested due to automatic review settings October 16, 2026 07:25

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.

🔵 Needs a closer look

The custom parser has confirmed anchor mismatches and needs fixes plus human validation of GitHub-rendering parity.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment thread scripts/check_markdown_links.py
Comment thread scripts/check_markdown_links.py Outdated
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
  the href of an HTML link or src of an image, 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, and an image's
  anchor, such as GitHub's #gh-dark-mode-only, goes unchecked.
- It resolves each anchor as GitHub's script looks it up, in lowercase
  and with the user-content- prefix of the ids that GitHub renders,
  which the anchor may hold already, against the slugs that GitHub
  generates from the rendered text of the Markdown and HTML headings,
  including the numeric suffixes of repeated headings, and against the
  id and name attributes of the HTML elements that GitHub keeps. 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, including GitHub's bare ones, 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. A table is a block of its own, which ends as
  GitHub's table extension ends it, and each of its cells parses alone,
  while 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 >. The tags that GitHub filters, such
  as <script> and <iframe>, render as text.
- As GitHub does, it slugs the text that each heading holds in the tree
  that libxml2's HTML parser builds from the whole rendered document,
  where the paragraphs of tight lists, as cmark finds them, have no
  tags. A start tag closes only the elements that libxml2 lets it close,
  so a paragraph's start tag closes a heading while a code block's does
  not, an end tag closes no element of a higher priority than its own,
  and a heading closes the heading that it starts in. The text of a
  heading may thus run on across blocks, and the end tag of an element
  that an earlier block leaves open, such as a div, may close it.
- 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. A
  definition counts where it renders, and wherever a rendered reference
  uses it, even in a footnote that does not render. In heading text, a
  reference link or image resolves only when a definition holds its
  label, a citation renders as its footnote's number, and the delimiters
  of emphasis and strikethrough match as cmark-gfm matches them, those
  of each link's text alone.
- It decodes a destination as cmark does, trimmed and with 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, and drops only the leading
  whitespace of a URL. As in HTML, the first attribute of each name
  hides the others.
- 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 16, 2026 07:48

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.

🔵 Needs a closer look

The custom parser needs human compatibility review before becoming a blocking gate, particularly given the confirmed false-positive case.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

Comment thread scripts/check_markdown_links.py
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