Gate the markdown link checker on a fingerprint of the broken links - #16417
Merged
Azat Mukhametshin (azat-msft) merged 6 commits intoAug 31, 2026
Merged
Conversation
The md-link-checker agentic workflow ran every Friday and started Copilot unconditionally, even when the deterministic scan found nothing. The three most recent clean scheduled runs each reported "190 working, 0 broken" and together consumed 48.523 AI credits deciding there was nothing to do. Put a deterministic gate in front of it, mirroring the HTTP checker design in microsoft#16413. md-link-check-probe.yml takes over the Friday slot, runs check-md-links.py, and only dispatches the agent when the broken set differs from the checked-in fingerprint at known-broken-md-links.txt. Compare whole sets rather than gating on broken_count > 0: the workflow deliberately supports links that cannot be fixed, and counting alone would wake the agent again every week for links already reviewed and accepted. check-md-links.py gains broken-links.txt, the broken set as sorted, unique "source_file|link" lines. It carries only the source file and the link as written, with forward slashes and LF endings, so the same breakage produces the same bytes on a Windows laptop and on the runner, and rewording the report can never move the fingerprint. It is written on the early-exit paths too, so the probe never has to guess whether a missing file means "clean" or "did not run". md-link-checker.md becomes dispatch-only and now refreshes the fingerprint before opening its PR, so both fixes and reviewed-unfixable links are recorded. Its safe-output prefix changes from the shared "[link-checker]" to "[md-link-checker]" — with the shared prefix, an open HTTP checker PR would suppress the markdown checker's dispatch. The probe also skips dispatch when an agent run is already in flight but has not opened its PR yet. Verified: the script reports 190 working / 0 broken with an empty fingerprint; the compare step returns no-dispatch for a matching, reordered, or comments-only fingerprint and dispatch for added, removed, and missing ones, including paths with spaces and cross-file anchors; the dispatch guards skip on an open PR and on an in-flight run; and the recompiled lock has only workflow_dispatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
…cker-agent-gate # Conflicts: # .github/workflows/README.md
The open-PR check narrowed with `gh pr list --search '"[md-link-checker]" in:title'` and trusted the result count. GitHub matches a quoted phrase loosely and ignores the brackets, so that query also returns titles that merely contain the words "md link checker" — searching microsoft/vstest for it returns microsoft#16131, "Add local md-link-checker agent and dedupe link-checking rules", which does not carry the prefix at all. The equivalent HTTP query is looser still, matching three PRs with no prefix. A false match here fails quietly in the expensive direction: the probe decides a checker PR is already open and skips dispatch, so genuinely new broken links go unreviewed for as long as the unrelated PR stays open. Keep the search as a cheap server-side narrowing, but re-check the actual prefix with jq so only a real checker PR counts. Verified against microsoft/vstest: the loose search returns 1 PR and the filtered count is 0, while the same filter still finds all 6 genuinely prefixed "[link-checker] " PRs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
Azat Mukhametshin (azat-msft)
August 27, 2026 14:25
View session
…o the ref The regeneration commands in the fingerprint header named the file bare while telling the reader to run from the repo root, where that path does not resolve; they also redirected into a file called `new` in the root. Use a KNOWN variable holding the real path, matching what md-link-checker.md already does. The in-flight guard listed md-link-checker runs across every branch, so a manual run or backfill on an unrelated branch would hold up the scheduled main-branch dispatch for as long as it lasted. Scope the query to the ref being dispatched. Verified: the documented commands now run clean from the repo root and leave no stray file, `gh run list --branch` returns only that branch's runs, and every run block still passes bash -n. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
Azat Mukhametshin (azat-msft)
August 27, 2026 14:39
View session
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
.github/workflows/md-link-check-probe.yml:76
- The known-fingerprint parsing claims the file is “forgiving to edit by hand”, but the pipeline only drops full-line comments/blank lines and does not trim leading/trailing whitespace. If someone indents an entry (or a line gains trailing spaces), it will be treated as a distinct fingerprint entry and the probe will dispatch even when the broken set is unchanged. Trimming whitespace makes the comparison robust without changing the accepted format.
# Drop blank lines and '#' comments so the file stays forgiving to edit by hand.
# A real entry is "source_file|url", and a source path never starts with '#'.
# grep exits 1 when the file is nothing but comments, which is the normal state
# while the accepted set is empty, so its status must not fail the step.
{ grep -v '^[[:space:]]*#' "$KNOWN" || true; } | awk 'NF' | sort -u > "$OUT_DIR/known.sorted"
.github/workflows/md-link-checker.md:166
- This section says to “commit the fingerprint update” but then directs the agent to use
create-issue. Accepting/reviewing unfixable anchors requires updating the checked-in fingerprint, which is a code change that needs a PR; otherwise the probe will keep dispatching on every run. Align this branch with the HTTP checker pattern: when the working tree changes (including the fingerprint), create a PR and document the unfixable anchors in the PR body instead of creating an issue.
**If you could not fix anchors**
- Still commit the fingerprint update, so the reviewed-and-unfixable links stop waking you weekly
- Use the `create-issue` safe output to create an issue with broken links
- In the issue description, include:
- A summary of how many links could not be fixed
Mirrors the fix in the HTTP probe. Both guards are correctness checks, so a truncated window is a wrong answer rather than a shorter list: at --limit 100 a busy repo silently returns exactly 100 rows, and a prefixed PR pushed out of that window reads as 'none open' and dispatches a duplicate agent run. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
Azat Mukhametshin (azat-msft)
August 27, 2026 15:11
View session
Amaury Levé (Evangelink)
approved these changes
Aug 27, 2026
Follows microsoft#16429, which moves agentic workflow state out of .github because create-pull-request compiles with protect_top_level_dot_folders: true and gh-aw rejects any patch path whose first segment starts with a dot. This fingerprint would have landed in the location that has the problem, so the agent could never have committed the update this workflow instructs it to make. Only the path changes; the file and the comparison logic are untouched. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
Azat Mukhametshin (azat-msft)
August 31, 2026 09:54
View session
Azat Mukhametshin (azat-msft)
merged commit Aug 31, 2026
f49d6de
into
microsoft:main
20 checks passed
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.
Problem
md-link-checkerruns every Friday and starts Copilot unconditionally, even when the deterministic scan finds nothing. The three most recent clean scheduled runs each reported 190 working, 0 broken and together burned 48.523 AI credits deciding there was nothing to do:Approach
Mirrors the HTTP checker design in #16413 (now merged; this branch has been merged with
main).md-link-check-probe.ymltakes over the Friday slot, runscheck-md-links.py, and dispatches the agent only when the broken set differs from the checked-in fingerprint at.github/workflows/scripts/known-broken-md-links.txt.Comparing whole sets, rather than gating on
broken_count > 0, matters because the workflow deliberately supports links that cannot be fixed. Counting alone would wake the agent every week for links already reviewed and accepted.check-md-links.pygainsbroken-links.txt: the broken set as sorted, uniquesource_file|linklines. It carries only the source file and the link as written — no resolved paths, counts, or report wording — with forward slashes and LF endings, so the same breakage produces the same bytes on a Windows laptop and on the runner, and rewording the report can never move the fingerprint. It is written on the early-exit paths too, so the probe never has to guess whether a missing file means "clean" or "the scan did not run".md-link-checker.mdbecomes dispatch-only and refreshes the fingerprint before opening its PR, so both fixes and reviewed-unfixable links are recorded.Duplicate-run protection
The safe-output prefix changes from the shared
[link-checker]to[md-link-checker]. With the shared prefix, an open HTTP checker PR would suppress the markdown checker's dispatch, and vice versa. The probe also skips dispatch when an agent run is already in flight but has not opened its PR yet, and uses a concurrency group so two probes cannot race.The open-PR guard does not trust the search count. GitHub matches a quoted phrase loosely and ignores the brackets, so
'"[md-link-checker]" in:title'also returns titles that merely contain the words md link checker — in this repo it returns #16131, which carries no prefix at all. A false match fails quietly in the expensive direction: the probe would decide a checker PR is already open and skip dispatch, leaving genuinely new broken links unreviewed for as long as that unrelated PR stays open. The search is kept only as cheap server-side narrowing, andjqre-checks the realstartswith("[md-link-checker] ")prefix.Files
.github/workflows/md-link-check-probe.yml.github/workflows/md-link-checker.md[md-link-checker]prefix, fingerprint update.github/workflows/md-link-checker.lock.yml.github/workflows/scripts/check-md-links.py.github/workflows/scripts/known-broken-md-links.txt.github/workflows/README.md,.github/agents/md-link-checker.mdValidation
All of these were executed, not just reasoned about, and re-run after merging
main:broken-links.txtis sorted, forward-slashed, LF-only, and preserves paths with spaces and cross-file anchors.grep -vexits 1 when the file is all comments, whichset -ewould have turned into a step failure. Fixed with|| true.gh— skips on an open PR, skips on an in-flight run, dispatches when clear.[link-checker]PRs.gh aw compile md-link-checkersucceeds and the regenerated lock has onlyworkflow_dispatch.run:block passesbash -n.Confirmed that
workflow_dispatchis a documented exception to theGITHUB_TOKENrecursion-prevention rule, so the probe can dispatch the agent with the default token.Note: as in #16413, dropping the schedule makes gh-aw omit the agent's
gh-aw-copilot-${{ github.workflow }}concurrency group. The probe's in-flight check covers the automated path.