Skip to content

ci: check relative Markdown links and anchors - #575

Closed
Jay (jay23606) wants to merge 5 commits into
microsoft:devfrom
jay23606:fix/md-link-check
Closed

Jay (jay23606) wants to merge 5 commits into
microsoft:devfrom
jay23606:fix/md-link-check

Conversation

@jay23606

Copy link
Copy Markdown
Contributor

Fixes #563

Summary

  • scripts/check_markdown_links.py resolves every relative link and image target in tracked Markdown outside openvmm/, and every anchor against GitHub's heading slugs, ignoring fenced and inline code.
  • Runs in .github/actions/check-quality with its unit tests (scripts/test_check_markdown_links.py).
  • Fixes the one broken anchor it finds: aci_edge_sandboxes/README.md linked #native-proxy-and-forwarded-ports, a heading renamed to "Native proxy and host loopback".

Test

  • python scripts/check_markdown_links.py passes on this branch.
  • python -m unittest discover -s scripts -p test_check_markdown_links.py passes (6 tests).
  • ruff check/format and pyright (strict, Linux and Windows) are clean.
  • Links into openvmm/ are skipped: it is vendored and was not checked out in my clone, so two links into it could not be verified here.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 15, 2026 19:03

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 silently miss broken links or fail CI on valid documentation.

6 open findings
What changed in this PR

Adds Markdown link validation to the quality gate to catch broken documentation paths and anchors, addressing #563.

Changes:

  • Adds a dependency-free checker and unit tests.
  • Runs both in the quality action.
  • Corrects the sandbox README’s native-proxy anchor.
File Description
scripts/​test_check_markdown_links.py Tests slug generation, anchors, and target extraction.
scripts/​check_markdown_links.py Checks relative Markdown targets and anchors outside vendored files.
aci_edge_sandboxes/​README.md Fixes the native-proxy anchor destination.
.github/​actions/​check-quality/​action.yml Runs the checker and its tests.

🧠 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
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:12

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

Verified parsing and resolution errors reject valid documentation links and allow broken links to pass.

1 open finding
6 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Parse Markdown destinations with titles and balanced parentheses

scripts/​check_markdown_links.py:33

This expression misses or truncates valid Markdown destinations. [guide](missing.md 'Guide') and [guide](<missing guide.md>) produce no targets, so broken links pass CI. [guide](guide(v2).md) produces guide(v2, so an existing file is reported missing. Parse angle-delimited destinations, balanced/escaped parentheses, and Markdown title delimiters, and add regression cases for these forms.

Medium severity Decode HTML entities before generating heading slugs

scripts/​check_markdown_links.py:93

HTML entities are not decoded before generating heading slugs. For example, # Fish &amp; Chips renders with anchor fish--chips, but this checker generates fish-amp-chips. It therefore rejects the valid anchor and accepts an incorrect one. Decode entities after processing heading markup but before restoring stashed code spans, so entity text inside code remains literal, and add regression cases for both forms.

Medium severity Preserve inline-code-only Setext heading anchors

scripts/​check_markdown_links.py:104

A Setext heading containing only inline code, such as a line containing Setup in backticks followed by =====, loses its anchor: masking turns the heading text into whitespace, so the blank-line check discards it. GitHub generates setup, but this checker rejects links to it. Use nonblank placeholders for code spans during heading detection, while still masking fences, and add a regression case.

Medium severity Strip query strings before resolving link paths

scripts/​check_markdown_links.py:165

Query strings are treated as part of the filesystem path. Valid links such as README.md?plain=1 or image.png?raw=true are reported missing even when the file exists, causing the quality job to reject valid documentation. Strip the query before resolving the path, while retaining the fragment for anchor validation.

🧠 Review effort: Balanced

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

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

Parsing and URL-resolution defects can reject valid documentation or miss broken links.

0 open findings

1 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Markdown link destinations with parentheses and titles are misparsed

scripts/​check_markdown_links.py:35

The destination regex misparses valid Markdown: [guide](guide(v2).md) yields guide(v2, while [guide](missing.md 'Guide title') and [guide](<missing guide.md>) are missed entirely. This can reject existing files or silently accept broken links. Parse balanced and escaped parentheses, angle-delimited destinations, and Markdown title delimiters, and add regression cases for these forms.

Medium severity Fenced code blocks inside Markdown containers are not ignored

scripts/​check_markdown_links.py:61

Fence detection ignores Markdown containers. A block quote containing > ~~~, > [example](missing.md), and > ~~~ leaves the code example visible to link extraction, causing a false CI failure. List-item fences indented four spaces have the same problem; blank lines also prevent the code-span masker from hiding backtick examples. Track block-quote prefixes and list-relative indentation when opening and closing fences, and add regression cases for both containers.

Medium severity HTML entities are not decoded when generating heading slugs

scripts/​check_markdown_links.py:95

HTML entities are not decoded when generating heading slugs. The Markdown heading # Salt &amp; Pepper renders as “Salt & Pepper”, whose anchor is salt--pepper, but this code generates salt-amp-pepper and rejects the valid link. Decode entities after removing markup and before restoring stashed code spans, so entities written inside code remain literal. Add regression tests for both cases.

Medium severity Query strings are incorrectly treated as part of relative filenames

scripts/​check_markdown_links.py:167

A relative URL such as README.md?plain=1 is checked as a literal filename containing ?plain=1, so it fails the quality gate even when README.md exists. Remove the query before resolving the path, but before percent-decoding so encoded question marks in filenames remain intact.

🧠 Review effort: Balanced

Jay (jay23606) and others added 3 commits October 15, 2026 17:49
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
A backtick in one paragraph must not pair with one in another, since Markdown
ends a paragraph at a blank line. Add a regression case.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 21:49

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 parsing errors reject valid Markdown links and anchors while allowing some broken image links to pass.

5 open findings

🧠 Review effort: Balanced

Comment thread scripts/check_markdown_links.py
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
Destinations keep balanced parentheses and drop titles and angle brackets.
Images nested in link labels are checked, and each target reports the line of
its opening bracket. Fences may be indented inside list items. Heading slugs
decode HTML entities outside code spans, and query strings are not part of
the path. Each form has a regression test.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 00: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.

🟡 Changes recommended

Verified parsing errors reject valid Markdown links and anchors while allowing some broken links through.

1 open finding
5 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Reference links with multiline or angle-bracket destinations are misparsed

scripts/​check_markdown_links.py:40

Reference destinations can begin on the following line or use angle brackets around paths containing spaces. For [g]:\n missing.md, this regex finds no target, so a broken reference link passes CI. For [g]: <dir with space/g.md>, it checks the truncated target dir instead. Allow the optional line break and parse bracketed destinations separately from bare destinations.

Medium severity Fenced code blocks inside blockquotes are not recognized

scripts/​check_markdown_links.py:61

Fences inside blockquotes are not recognized because their lines start with >. A block containing > ~~~, > [example](missing.md), and > ~~~ therefore reports a broken link even though GitHub renders it as code. Track blockquote containers when opening and closing fences so quoted code examples do not fail the quality job.

Medium severity Heading links with parentheses are normalized incorrectly

scripts/​check_markdown_links.py:92

Heading normalization still uses a link regex that stops at the first closing parenthesis. For # [Guide](guide(v2).md), it produces guidemd instead of GitHub's guide, rejecting valid #guide links and accepting nonexistent #guidemd anchors. Extract heading labels with balanced destination parsing, including the title forms supported by the link checker.

Medium severity Inline-code Setext headings are incorrectly treated as blank

scripts/​check_markdown_links.py:127

Masking inline code makes a code-only Setext heading appear blank. A line containing nvx_tools in backticks followed by =========== defines #nvx_tools on GitHub, but anchors_in() returns no anchor and valid links fail CI. Use fence-masked text with inline code preserved when checking whether the heading paragraph contains content.

🧠 Review effort: Balanced

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

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

Verified parsing errors can reject valid documentation and allow broken links through CI.

0 open findings

1 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Escaped backticks incorrectly mask live links

scripts/​check_markdown_links.py:32

The code-span regex treats escaped opening backticks as delimiters. For example, \`[guide](missing.md)\` is masked completely, although the escaped backticks leave a live Markdown link. Broken links can therefore pass CI. Respect backslash parity when recognizing opening delimiters, without treating backslashes inside an actual code span as escapes, and add regression coverage.

Medium severity Reference destinations fail with newlines or spaces

scripts/​check_markdown_links.py:39

Reference destinations can start on the next line or contain spaces inside angle brackets. Here, [g]:\n missing.md produces no target, while [g]: <dir with space/g.md> produces dir. This lets broken reference links pass CI and can reject valid ones. Parse the complete destination in both forms and add regression tests alongside the inline-destination tests.

Medium severity Fenced blocks with container prefixes are misdetected

scripts/​check_markdown_links.py:62

Fence detection does not recognize blockquote or list-marker prefixes. A quoted fence such as > ~~~\n> [example](missing.md)\n> ~~~ exposes the example as a link. With `- ```\n example\n ````, the closing fence becomes an opener, so subsequent live links are also hidden through the end of the document. Track container prefixes and boundaries when masking fences, and test both forms so CI neither rejects code examples nor skips real links.

Medium severity Headings in blockquotes and lists are missed

scripts/​check_markdown_links.py:114

Headings inside blockquotes and list items are missed: both > ## Setup and - ## Setup produce an empty anchor set, although they render as headings with a setup anchor on GitHub. Valid links therefore fail CI, and a later duplicate heading receives the wrong suffix. Recognize container prefixes before extracting heading text, preserve code-block masking, and test standalone and duplicate headings inside containers.

🧠 Review effort: Balanced

A backslash escape before ASCII punctuation is part of the Markdown syntax,
so guide\(v2\).md names guide(v2).md in inline and reference links. HTML
attribute values keep their backslashes. Regression tests cover both.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23462 <4602056+jay23606@users.noreply.github.com>
@ppenna

Copy link
Copy Markdown
Contributor

Thank you for working on this, Jay (@jay23606).

I'm closing this pull request as superseded by #571, which fixed #563 as well and has merged into dev. It adds its checker to the same Lint, typing, and formatting job, fixes the same anchor in aci_edge_sandboxes/README.md, and also checks the doc/ paths that tracked files mention. On the current dev, this branch's checker finds no broken link that the merged one misses.

If you find a Markdown link that the merged checker gets wrong, please open an issue.

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

3 participants