📖 [Docs]: The Markdown standard's cross-repository links are now checked, not just described - #145
📖 [Docs]: The Markdown standard's cross-repository links are now checked, not just described#145Marius Storhaug (MariusStorhaug) wants to merge 12 commits into
Conversation
The check does not exist yet, so every case is red. That is the point: the negative proof comes before the implementation, per the Testing standard's test-first rule. The suite runs entirely offline. An in-process HttpListener stub answers the two GitHub endpoints the check will call, so the cases for a missing file, a missing anchor, an exhausted rate limit, a failing request, and a target repository a reader cannot read are deterministic and locally runnable. The slug expectations are a recorded fixture from github-slugger 2.0.0 - the library GitHub's own anchors come from - not a second derivation of the rule under test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The check reads the target's content rather than sending a HEAD request: a URL fragment never reaches the server, so an anchor into a file answers 200 whether or not the heading exists - which is exactly the case that broke seven links from Storhaug-ting/S62 into Storhaug-ting/Kilden. Anchors are slugged with GitHub's rules, taken from Kilden's Test-MarkdownLink.ps1 rather than from this repository's ConvertTo-Slug. That one mirrors python-markdown for the published site, and a cross-repository link resolves against GitHub's rendering, not ours. Three outcomes rather than two. A 404 alone cannot tell a deleted file from a repository no reader can open, so the repository is probed before a link is called broken, and both a network failure and an unreadable target are reported under their own heading. Proven negatively as well as positively: disabling the anchor comparison turns exactly the two anchor cases red, and treating a 404 as success turns exactly the missing-file and unreadable-repository cases red. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every one of these was a live 404 in published documentation, found by the check rather than by a reader - which is the whole point of it. Process-PSModule moved its framework tests from 'scripts/tests' into '.github/actions/Test-PSModule/src/tests', and renamed 'Publish-Docs.yml' to 'Publish-Site.yml'. Test-PSModule's README no longer carries the 'SourceCode tests' and 'Module tests' sections the two anchors pointed at, so those links now point at the test settings themselves, which is where the behaviour they describe actually lives. 'DEPENDENCIES.md' no longer exists in Process-PSModule and has no successor anywhere in the organization, so the sentence promising it now points at the workflows and actions that make up the composition. The Checkout-GitHubRepo link is left in place and marked private: the file is there, the repository is not public, so a reader who gets a 404 now knows why. Personal accounts are outside the check's scope for the same reason - no check can speak for a repository it cannot read. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Its own workflow rather than a job in Docs.yml for two reasons. The name of a red check then says the network check failed rather than the documentation being wrong. And Docs.yml's publish job needs build, lint, links, and test, so a job there would let a GitHub outage block a Pages deploy. It also runs weekly. A target repository moves content on its own schedule, long after a pull request here has merged, and nothing in the pull-request trigger will ever notice. GITHUB_TOKEN is passed for rate-limit headroom only - 60 requests an hour anonymously against 1000 authenticated. It unlocks no private repository elsewhere, so the check still measures what an anonymous reader can reach. Docs.yml's comment claiming to be the only workflow is corrected, and its Links job now says which half of the link checking it owns. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Running the check locally without a token exhausted the anonymous limit of 60 requests an hour part way through, and it then spent another two dozen requests being told the same thing. The quota is a run-level fact, not a per-request one: the first 403 with no remaining quota now stops the rest. The message also says when no token was set, so the reader is told the one thing that would have changed the outcome rather than being left to infer it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The standard asked authors to write a link form nothing verified. It now says which links are checked, by what, and against which slug rules - including that GitHub's anchors differ from the published site's, so an author writing a cross-repository anchor knows which of the two to copy. Two consequences are spelled out because they change what an author writes: a public page must not link into a repository a reader cannot open, and a run that resolved no cross-repository link fails rather than passing on an empty set. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pushed to make the new check go red in CI on purpose, so the run linked from the pull request shows it failing and shows what it says when it does. Reverted in the next commit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The GitHub slug rules are expressed as '\p{Nd}' and friends, and codespell
reads the category name as a misspelling of 'And'. An ignore-regex for the
whole '\p{...}' form fixes the class rather than the one word, and leaves
ordinary spell checking intact - a probe file with 'mispelled' and
'sentance' still produces two errors.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ing anchor" This reverts commit 45c6ec5.
There was a problem hiding this comment.
Pull request overview
This PR adds CI validation for cross-repository links (into MSX-controlled GitHub orgs) so that moved files/anchors in other repositories are detected automatically, and it repairs several existing dead cross-repo links in the documentation.
Changes:
- Introduces a new PowerShell validator (
Test-CrossRepositoryLink.ps1) plus an offline Pester suite with an in-process stub GitHub API. - Adds a dedicated GitHub Actions workflow (
Cross-Repository-Links.yml) to run the cross-repository link check on PRs/pushes and weekly. - Updates documentation/standards and repairs broken links into PSModule repositories.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/Test-CrossRepositoryLink.Tests.ps1 |
Adds offline Pester coverage for cross-repository link parsing, resolution, anchoring, caching, and failure classification. |
./.github/scripts/Test-CrossRepositoryLink.ps1 |
Implements the cross-repository link checker (GitHub API resolution + GitHub-style anchor validation). |
.github/workflows/Cross-Repository-Links.yml |
New workflow to run the cross-repo link check (PR/push + weekly schedule). |
.github/workflows/Docs.yml |
Clarifies that the existing Links job is intentionally network-free and that cross-repo checking lives elsewhere. |
CONTRIBUTING.md |
Documents running the new cross-repository link check locally before opening a PR. |
src/docs/Coding-Standards/Markdown.md |
Documents that links are checked and describes the new cross-repo check’s behavior/scope. |
src/docs/Ways-of-Working/Git-Worktrees.md |
Adds context that a referenced helper script link points to a private repository. |
src/docs/Capabilities/process-psmodule/pipeline-stages.md |
Repairs multiple PSModule cross-repo links to moved/renamed targets. |
src/docs/Capabilities/process-psmodule/index.md |
Replaces an obsolete dependency reference with links to current workflow/action structure. |
.github/linters/.codespellrc |
Avoids false positives from \p{...} Unicode-category escapes used in regexes. |
Suppressed comments (2)
src/docs/Coding-Standards/Markdown.md:67
Test-CrossRepositoryLink.ps1is described here as resolving links "as published URLs", but the very next bullet scopes it togithub.comandraw.githubusercontent.com. Rewording this to explicitly say “GitHub URLs” avoids confusion with published-site (Pages) URLs.
**Into another repository** — `Test-CrossRepositoryLink.ps1` resolves the links this standard asks you to write as published URLs. It runs as its own job, so a red check says the network check failed rather than the documentation being wrong, and again weekly, because a target repository moves content long after a pull request here has merged.
src/docs/Capabilities/process-psmodule/pipeline-stages.md:244
- Grammatically, “tests” is plural, so the verb should be “verify” (or rephrase to a singular subject like “test suite”).
The [PSModule - Module tests](https://github.com/PSModule/Process-PSModule/blob/main/.github/actions/Test-PSModule/src/tests/Module/PSModule/PSModule.Tests.ps1) verifies the following coding practices that the framework enforces:
| - **Use sentence-style headings.** | ||
| - **Surround headings, lists, and fenced blocks with a blank line** for readability, even though the linter no longer enforces it. | ||
| - **Prefer relative links** within a repository; use the canonical published URL for cross-repository references. | ||
| - **Prefer relative links** within a repository; use the canonical published URL for cross-repository references. Both forms are checked in CI — see [Links are checked](#links-are-checked). |
There was a problem hiding this comment.
Right, and that sentence was doing exactly what this pull request exists to stop — a standard claiming a gate it does not have.
Fixed in d00db90. The bullet now says relative links, and cross-repository links on \github.com, are checked in CI, the section says the check resolves \github.com\ links rather than 'the links this standard asks you to write as published URLs', and there is a paragraph naming what is not covered: a \msxorg.github.io\ URL, which is the canonical form for a repository publishing to Pages. That gap is #150, which also records why it was not simply added here — a pull request adding a page and linking its published URL would fail until the page is published.
A link of the form 'github.com/OWNER/REPO/tree/REF' carries a reference and no path, and the check only asked whether the repository existed. A renamed or deleted branch passed as resolved - the one thing that link form actually asserts went unverified. It now asks the contents endpoint for the repository root at that reference, so an unknown branch, tag, or commit answers 404 and is reported by name. Proven the same way as the rest: reverting the routing condition turns exactly the new case red. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The standard claimed both link forms it recommends are checked. Only one is. The canonical published URL for a repository that publishes to Pages is a 'msxorg.github.io' URL, and nothing verifies those - the gap is now named in the standard and carried by #150 rather than being papered over by the sentence that was supposed to close it. The Process-PSModule tables are also introduced by 'tests ... verify' rather than 'verifies', on the two lines whose links moved. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/docs/Capabilities/process-psmodule/pipeline-stages.md:66
- Grammar: this sentence is missing “to” (“…is used by … evaluate…”). As written it’s ungrammatical and slightly harder to read in the rendered docs.
- [PSModule framework settings for style and standards for source code](https://github.com/PSModule/Process-PSModule/tree/main/.github/actions/Test-PSModule/src/tests/SourceCode)
- This produces a JSON-based report that is used by [Get-PesterTestResults](#get-test-results) evaluate the results of the tests.
Links from these docs into other MSX repositories are now checked in CI. Until now the Markdown standard asked authors to write cross-repository links and nothing verified them, so a target that moved was found by a reader following a dead link rather than by a red check. That has already cost seven links from Storhaug-ting/S62 into Storhaug-ting/Kilden — and, it turns out, six here.
New: Cross-repository links are verified
A link into
MSXOrg,PSModule, orStorhaug-tingongithub.comorraw.githubusercontent.comis resolved against the target repository. Both halves are checked: the file has to exist, and a#fragmenthas to match a heading that actually renders there. A URL fragment is never sent to the server, so a HEAD request would answer 200 for an anchor that does not exist — the content is fetched and its headings slugged with GitHub's rules, which are not the rules this site uses.## Hello — worldis#hello--worldon GitHub and#hello-worldon the published site; write the one GitHub gives you.Two things follow for authors, and the standard now says both. A public page must not link into a repository a reader cannot open — the check reads targets as an anonymous reader does, so a private target is reported. And a run that resolved no cross-repository link at all fails, because every link resolves is trivially true over an empty set.
A link the check could not answer — a network failure, an exhausted API rate limit, or a target nobody outside can reach — is reported under its own heading, separate from a broken link, so a red run says which of the two happened.
It runs as its own check,
Cross-repository links, on every pull request and push tomain, and again weekly: a target repository moves content long after a pull request here has merged, and nothing in the pull-request trigger would ever notice.Fixed: Six dead links into PSModule repositories
Every one of these was a live 404 in published documentation:
scripts/teststo.github/actions/Test-PSModule/src/testsPublish-Docs.ymlis nowPublish-Site.ymlDEPENDENCIES.mdno longer exists, and has no successor anywhere in the organizationThe
Checkout-GitHubRepolink in Git Worktrees is kept and marked private: the file is there, the repository is not public, so a reader who gets a 404 now knows why.Technical details
Proven red before green. The suite was written and committed before the script existed. Beyond that, two mutations were run against the finished check: disabling the anchor comparison turned exactly the two anchor cases red, and treating a 404 as success turned exactly the missing-file and unreadable-repository cases red — so the cases measure what their names claim. Then the live proof: two links in
pipeline-stages.mdwere pointed deliberately wrong, one at a missing file and one at a missing anchor, and pushed. The run went red naming both links with the right classification on each; the commit was reverted and the run went green.Files.
.github/scripts/Test-CrossRepositoryLink.ps1(new),tests/Test-CrossRepositoryLink.Tests.ps1(new, 31 cases),.github/workflows/Cross-Repository-Links.yml(new),.github/workflows/Docs.yml,.github/linters/.codespellrc,src/docs/Coding-Standards/Markdown.md,CONTRIBUTING.md, and the four documentation pages carrying the repaired links.Decisions recorded on #142, together with five corrections to the issue body found while implementing it:
github.comandraw.githubusercontent.com; ownersMSXOrg,PSModule,Storhaug-ting— the organizations MSX controls, which is where a target moves because we moved it. Configured as the-Ownerdefault. Shapes resolved: repository root,?tab=readme-ov-file#anchor,/blob/{ref}/{path},/tree/{ref}/{path},raw…./issues/,/pull/,/discussions/,/releases/,/actions/,/wiki/,/compare/,/commit/are ignored: API objects, not paths, and they do not move when a repository is restructured.GITHUB_TOKENis used only for rate-limit headroom (60 → 1000 requests/hour) and unlocks no private repository elsewhere, so the effective visibility is public — the right bar for a public documentation site.mainover the API. Resolved remotely, a pull request that moves a file would pass here and break as it merged. Not in the issue body; added to it.ConvertTo-GitHubSlugandGet-RenderedHeadingText, taken fromStorhaug-ting/Kilden, not from this repository'sConvertTo-Slug, which mirrors python-markdown for the published site and would check the wrong algorithm. A one-character error in Kilden's own example was found in the process — 📖 [Docs]: Slug example now shows the anchor the function actually produces Storhaug-ting/Kilden#16.LICENSEandREADME.mdand no managed-file mechanism, so there is nothing to ship through. The script is written to be portable instead: single file, no repository-specific constants, everything that varies is a parameter, and the repository it runs in comes fromGITHUB_REPOSITORYor the git remote. Adoption is carried by Distribute the Markdown link check instead of copying it a fourth time Custo#6, and the answer is reported to Storhaug-ting/S62#10.Testing. The suite runs fully offline: an in-process
System.Net.HttpListenerstub answers the two GitHub endpoints the check calls, so the missing-file, missing-anchor, rate-limit, request-failure, unreadable-repository, deduplication, and short-circuit cases are deterministic and locally runnable. The seam is-ApiBaseUri, which followsGITHUB_API_URLand is a real feature rather than a test hook — it is what makes the script work against GitHub Enterprise Server. The fourteen slug expectations are a recorded fixture fromgithub-slugger2.0.0 rather than a second derivation of the rule under test; the PowerShell implementation matches it on all fourteen.Verified.
Invoke-PesterSuite.ps1: 85 passed, 0 failed across 5 suites.PSScriptAnalyzerwith.github/linters/.powershell-psscriptanalyzer.psd1: clean on both new files.Test-DocumentationLink.ps1: 114 files, all resolve.Test-CrossRepositoryLink.ps1: 34 links in 121 files, all resolve.Update-DocumentationIndex.ps1 -Check: clean.markdownlint-cli2andcodespellwith the repository's configs: clean — and the codespell change was negative-tested against a probe containingmispelledandsentance, which still produces two errors, so the newignore-regexdid not blanket-disable spell checking.Implementation plan progress: all eight steps of #142 complete. Nothing deferred except the published-URL form, which was never in that issue's scope and is now #150.
Standards and framework alignment pass
.github/scripts/**(PowerShell)tests/**(PowerShell).github/workflows/**.github/linters/**src/docs/**,CONTRIBUTING.mdIssue convergence sweep. Scoped to open issues about link checking, documentation gates, and the repositories this touches: #142 (closed by this), #150 (opened by this, not satisfied by it), MSXOrg/Custo#6 (opened by this), Storhaug-ting/S62#10 and Storhaug-ting/Kilden#12 (both answered, neither delivered — the check they need lives in their own repositories). #134's rule is honoured here but that pull request is already merged.
Relevant issues (or links)