Skip to content

Markdown link check re-scans every markdown file on every pull request #2661

Description

@jkim323

Problem

markdown-link-check.yml validates the whole repository on every pull request. scripts/linting/Markdown-Link-Check.ps1 then spawns one markdown-link-check Node process per file, serially, across roughly 1,150 markdown files. Node cold-start cost dominates before a single link is checked.

Two independent inefficiencies compound:

  1. No changed-files scoping. The link check is the only lane in pr-validation.yml without it. frontmatter-validation, yaml-lint, ps-script-analyzer, skill-validation, pester-tests, pytest-tests, and eval-validation all already accept changed-files-only and are called with true.
  2. Serial, single-file CLI invocations. Each file is an independent process, so the loop is trivially parallelizable but runs one at a time.

Evidence

scripts/linting/Markdown-Link-Check.ps1 invokes the CLI once per file inside foreach ($file in $filesToCheck), with the plugins/ tree and test fixtures already excluded from discovery.

.github/workflows/pr-validation.yml calls the reusable workflow with only soft-fail: true and no scoping inputs.

Issue #2650 reports a full local run covering 4,089 links across 1,122 files, which gives a sense of the surface being re-walked on every pull request.

Suggested approach

Changed-files scoping. Add -ChangedFilesOnly and -BaseBranch to Markdown-Link-Check.ps1, using the existing Get-ChangedFilesFromGit helper in scripts/linting/Modules/LintingHelpers.psm1, and intersect the result with current discovery filters so the plugins/ and fixture exclusions still apply. Add matching changed-files-only and base-branch inputs to the reusable workflow, following the skill-validation.yml and asset-docs-validation.yml pattern, and set fetch-depth: 0 so merge-base resolves.

To avoid losing coverage, weekly-validation.yml should run the same reusable workflow with changed-files-only: false. It does not call the link check today.

Bounded parallelism. Convert the per-file loop to ForEach-Object -Parallel with a configurable throttle. Result aggregation, console output, and Write-CIAnnotation should stay serial so output ordering and annotations are unchanged.

Behavior that must be preserved

  • An empty changed set reports a clean run rather than failing; an unscoped scan that finds nothing still fails.
  • Malformed junit XML still marks a file failed regardless of the CLI exit code.
  • The results JSON schema, step summary, and broken-link annotations are unchanged.
  • npm run lint:md-links still performs a full local scan by default.

Interaction with #2647

#2647 argues that a green markdown link check carries no information today because the lane is soft-fail: true on pull requests. Scoping the lane to changed files narrows what it inspects per pull request, so the two should be considered together: the weekly full sweep is what preserves whole-repository coverage, and #2647's proposal to move relative-link enforcement into a non-soft-fail lane remains the stronger guarantee.

Further optimizations considered but not proposed here

  • Batch multiple files per CLI invocation, removing the remaining process spawns.
  • Deduplicate external URLs across files; hosts such as learn.microsoft.com repeat across hundreds of files and are re-fetched each time.
  • Tune timeout (currently 20s) and add aliveStatusCodes for bot-hostile hosts.

Acceptance

  • Pull request validation checks only markdown files changed against the base branch.
  • A full-repository sweep runs on a schedule.
  • Files are checked concurrently with a configurable, bounded throttle.
  • Existing behavior above is preserved and covered by tests.

Metadata

Metadata

Labels

agent-readyciContinuous integrationlintingLinting rules and validationmaintenanceMaintenance work, no version bumppriority-2High priority, address soonscriptsPowerShell, Bash, or Python scriptsworkflowsGitHub Actions workflows

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions