Skip to content

Gate the markdown link checker on a fingerprint of the broken links - #16417

Merged
Azat Mukhametshin (azat-msft) merged 6 commits into
microsoft:mainfrom
azat-msft:azat-msft-md-link-checker-agent-gate
Aug 31, 2026
Merged

Gate the markdown link checker on a fingerprint of the broken links#16417
Azat Mukhametshin (azat-msft) merged 6 commits into
microsoft:mainfrom
azat-msft:azat-msft-md-link-checker-agent-gate

Conversation

@azat-msft

@azat-msft Azat Mukhametshin (azat-msft) commented Aug 26, 2026

Copy link
Copy Markdown
Member

Problem

md-link-checker runs 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:

Run Result AI credits
2026-08-07 190 working, 0 broken 15.205
2026-08-14 190 working, 0 broken 16.381
2026-08-21 190 working, 0 broken 16.937

Approach

Mirrors the HTTP checker design in #16413 (now merged; this branch has been merged with main). md-link-check-probe.yml takes over the Friday slot, runs check-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.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 — 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.md becomes 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, and jq re-checks the real startswith("[md-link-checker] ") prefix.

Worth a follow-up, not changed here: the merged HTTP probe's guard has the same looseness. Searching '"[link-checker]" in:title' matches #16417, #16413, and #16104, none of which carry the prefix. Left alone to keep this PR scoped.

Files

File Change
.github/workflows/md-link-check-probe.yml New scheduled deterministic gate
.github/workflows/md-link-checker.md Dispatch-only, [md-link-checker] prefix, fingerprint update
.github/workflows/md-link-checker.lock.yml Regenerated (gh-aw v0.81.6, the version the repo is compiled with)
.github/workflows/scripts/check-md-links.py Stable machine-readable broken set
.github/workflows/scripts/known-broken-md-links.txt Accepted fingerprint (currently empty)
.github/workflows/README.md, .github/agents/md-link-checker.md Document probe vs. agent

Validation

All of these were executed, not just reasoned about, and re-run after merging main:

  • Script reports 190 working, 0 broken against the repo; the fingerprint is legitimately empty.
  • Fixture run with broken links confirms broken-links.txt is sorted, forward-slashed, LF-only, and preserves paths with spaces and cross-file anchors.
  • Compare step run against fixtures — no dispatch for a matching, reordered, or comments-only fingerprint; dispatch for added, removed, and missing fingerprints. The comments-only case caught a real bug: grep -v exits 1 when the file is all comments, which set -e would have turned into a step failure. Fixed with || true.
  • Dispatch guards run against a stubbed gh — skips on an open PR, skips on an in-flight run, dispatches when clear.
  • Prefix filter verified against live data: 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.
  • gh aw compile md-link-checker succeeds and the regenerated lock has only workflow_dispatch.
  • Probe YAML parses; every run: block passes bash -n.

Confirmed that workflow_dispatch is a documented exception to the GITHUB_TOKEN recursion-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.

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>
Copilot AI lite review requested due to automatic review settings August 26, 2026 15: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.

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 AI review requested due to automatic review settings August 27, 2026 14:24

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/scripts/known-broken-md-links.txt Outdated
Comment thread .github/workflows/md-link-check-probe.yml Outdated
…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 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.

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 AI review requested due to automatic review settings August 27, 2026 15:11

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

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 AI review requested due to automatic review settings August 31, 2026 09:50

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@azat-msft
Azat Mukhametshin (azat-msft) merged commit f49d6de into microsoft:main Aug 31, 2026
20 checks passed
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.

3 participants