fix: treat off-site links as external in the docs link checker - #772
Open
Ectsang wants to merge 1 commit into
Open
fix: treat off-site links as external in the docs link checker#772Ectsang wants to merge 1 commit into
Ectsang wants to merge 1 commit into
Conversation
check_links.py decides whether a link is external by matching its scheme
against ("http", "https") plus a short skip list. Anything else falls
through and its path is resolved against the local build, so an off-site
link is reported as a broken internal one:
//example.com/x -> "Not Found" (protocol-relative)
ftp://example.com/x -> "Not Found" (scheme we do not resolve)
vscode:extension/x -> "Not Found" (scheme carrying no host)
Classify by what the URL carries instead: a host, or a scheme we do not
resolve, means off-site. A relative path containing a colon still parses
with no scheme and no host, so it keeps being resolved.
Two adjacent gaps in the same block:
- The raw-Markdown guard added in Universal-Commerce-Protocol#759 compares a case-sensitive suffix,
so a link to page.MD is accepted while page.md is rejected.
- The path is percent-decoded but the fragment is not, so target/#re%61l
reports "Anchor not found" against an id of "real".
Also drops the reassignment of `link` after the site-prefix strip. That
value is never read again -- the last read is the startswith() test above
it -- so the removal is behavior-neutral. Called out here rather than left
silent, since it is not required by the fix.
Adds scripts/test_check_links.py, the first tests for this script, on the
same harness as test_validate_examples.py, plus a check-links-tests hook
in .pre-commit-config.yaml mirroring validate-examples-tests.
No CI step is added. Editing .github/workflows/docs.yml makes zizmor scan
it, which surfaces pre-existing unpinned-uses findings unrelated to this
change. Left for a maintainer to decide on separately.
No behavior change on the current docs. Built the site locally and diffed
findings between this and main: 0 findings either way, in both DOCS_MODE
values. `pre-commit run --all-files` passes.
Ectsang
force-pushed
the
fix/link-checker-off-site
branch
from
August 26, 2026 07:18
26aad8e to
b442f6b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nothing on the current docs is broken by this. It is hardening plus the first tests for
this script. I grepped for every shape below and found none of them in the docs today.
check_links.pydecides whether a link is external by matching its scheme against("http", "https"), plus a skip list formailto:/tel:/javascript:/data:. Anythingelse falls through and its path gets resolved against the local build, so a link pointing
somewhere else entirely is reported as a broken internal one:
//example.com/xTarget: local_preview/x (Not Found)ftp://example.com/xTarget: local_preview/x (Not Found)vscode:extension/ms-python.pythonTarget: local_preview/extension/… (Not Found)This matters because the docs job treats any finding as fatal. A false positive blocks a
deploy on a link that is not ours to fix, and the reverse case ships a broken link to
readers.
Classifying on what the URL carries fixes all three: a host, or a scheme we do not
resolve, means off-site. A relative path that happens to contain a colon still parses with
no scheme and no host, so it keeps being resolved as before.
Two adjacent gaps in the same block, both one-liners:
page.MDisaccepted while
page.mdis rejected.pathis percent-decoded butfragmentis not, sotarget/#re%61lreportsAnchor not foundagainst an id ofreal.Tests, and where they run
check_links.pyhad no tests. This addsscripts/test_check_links.pyon the same harnessas
test_validate_examples.py, covering off-site classification, resolution, theraw-Markdown guard, exit codes,
.linkignoreprecedence andDOCS_MODE=spec.These fail on unpatched
main:.pre-commit-config.yamlgains acheck-links-testshook mirroringvalidate-examples-tests, so the tests run rather than sit unused.I did not add a CI step. Editing
.github/workflows/docs.ymlmakes zizmor scan it, and itreports seven
unpinned-usesfindings onactions/checkout@v5andtj-actions/changed-files@v47that have nothing to do with this change. They only surfacewhen someone touches the file. Happy to send the CI step as a follow-up if you would like
it, but that decision looked like yours rather than mine to bundle in here.
The script reads
sys.argvand the environment at module level, so the tests drive it as asubprocess against a synthetic site rather than importing it. That exercises the same entry
point CI uses. Say the word if you would rather have an importable seam instead.
Verification
Built the site locally and diffed findings between this branch and
main: 0 findingseither way, in both
DOCS_MODE=rootandDOCS_MODE=spec, running both versions as realsubprocesses over the same build.
pre-commit run --all-filespasses.test_validate_examples.pystill passes.One removal to flag
The diff drops
link = link[len(SITE_URL) - 1 :]after the site-prefix strip. That valueis never read again: the last read is the
startswith(SITE_URL)test directly above it,and everything downstream uses
parsed.pathororiginal_link. Behavior-neutral, but notrequired by the fix, so I would rather name it than leave it for review to find.