Repository navigation
fix(ci): scope the spell check to the branch, not the whole divergence - #3530
Conversation
`changed_lines.py` ran `git diff <base>`, which compares the tip of the base branch against the working tree. Every commit landing on main after a branch was cut therefore showed up, and the pre-existing side of each of those appeared as an *added* line -- the working tree still holds it while main no longer does. So a branch a few dozen commits behind main reported most of the repository as newly added. The spell-check job scopes cspell to those lines, so it was spell-checking repo-wide vocabulary and failing on words the branch never wrote. Measured on a branch 34 commits behind main: 160,102 reported added lines against 142 real ones, and every word cspell flagged appeared zero times in the branch's actual diff. The base is now resolved to its merge base with HEAD, which excludes the base-side commits. Resolving the base rather than switching to `git diff base...HEAD` keeps the comparison against the working tree, since this script is also run locally before committing -- there is a test for that. An unresolvable merge base falls back to the base tip, the previous behaviour, with a warning; over-reporting is the safe direction for checks built on this. The `--depth=1` in the workflow's base fetch had to go with it. It writes .git/shallow into the full clone from the checkout above and truncates the history, after which `git merge-base` fails outright -- verified in an isolated repo, where the depth-limited fetch turned a working merge-base into exit code 1. Tests: 4 new ones over real git repositories with a diverged base. Reverting only the script fails exactly those 4 and leaves the 3 existing tests passing. Signed-off-by: LHMQ878 <230791102+LHMQ878@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
🤖 AI Agent: contributor-guide — View details
Welcome to the microsoft/agent-governance-toolkit! Thank you for your contribution. You've done a great job providing a detailed explanation of the issue and the solution in your pull request description. Before we can merge, please ensure that your code passes all tests and adheres to the project's coding standards. Specifically, make sure to run For more guidance, please refer to our CONTRIBUTING.md. |
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 0 warnings. The fix is robust and improves CI correctness.
No action items needed. Clean change. |
🤖 AI Agent: test-generator — `scripts/ci/changed_lines.py`
|
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
🤖 AI Agent: security-scanner — View details
No security issues found. |
|
🔴 Contributor Check: HIGH
Automated check by AGT Contributor Check. |
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
`injective` is the precise term for the property the fix rests on -- the length-prefixed encoding maps distinct field tuples to distinct strings -- so it goes in the dictionary rather than being paraphrased away. `neighbours` becomes `neighbors`, matching the spelling already used elsewhere in the tree. The job's other reported words come from files this branch does not touch; that is a defect in how the changed-line set is computed, fixed separately in microsoft#3530. Signed-off-by: LHMQ878 <230791102+LHMQ878@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
TL;DR: 0 blockers, 1 warning. This ships; warning is fine as a follow-up.
| # | Sev | Issue | Where |
|---|---|---|---|
| 1 | Warn | Warning message is printed to stdout (can pollute piped output); prefer stderr | resolve_merge_base() |
Changes:
- Resolve the diff base to the
git merge-basewithHEADto avoid attributing base-branch changes to the PR branch. - Add regression tests covering diverged base branches, uncommitted work,
--mode changed-files, and merge-base failure fallback. - Update the spell-check workflow to avoid
--depth=1fetching that would break merge-base resolution.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
scripts/ci/changed_lines.py |
Resolve base ref to merge-base before diffing to scope “added lines/changed files” to the PR branch’s actual changes. |
tests/ci/test_changed_lines.py |
Adds git-backed regression tests for diverged branches, uncommitted work inclusion, and merge-base fallback behavior. |
.github/workflows/spell-check.yml |
Removes depth-limited fetch to preserve history needed for merge-base and prevent repo-wide spell-checking on stale branches. |
|
This is a really good change, I've been seeing this on a variety of PRs. |
|
Move the Approving the workflow runs now so CI can run. Once that change is in and CI is green, we'll merge. |
Adds the `### Fixed` entry for the `changed_lines.py` change. The numbers are the measured ones from the PR description (160,102 reported added lines versus 142 real ones on a branch 34 commits behind main), so the entry says what went wrong rather than only that something was fixed. Also notes the removal of the depth-limited base fetch, because that is a workflow behaviour change in its own right: a shallow fetch into the full clone writes .git/shallow and truncates the history the merge base needs, which would have made the script change a silent no-op in CI. Signed-off-by: LHMQ878 <LHMQ878@users.noreply.github.com>
|
Thanks — one of the two findings was right and is addressed in CHANGELOG entry — valid, addedFair catch: how the base is resolved is user-visible for anyone running
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
scripts/ci/changed_lines.py:75
- The warning in
resolve_merge_base()is printed to stdout. When this script is used without--output(or its output is piped/consumed), this warning will be mixed into the functional output and can break downstream consumers. Print warnings to stderr instead.
message = result.stderr.strip() or f"git merge-base exited {result.returncode}"
print(f"warning: cannot resolve merge base with {base} ({message}); diffing against its tip instead")
return base
tests/ci/test_changed_lines.py:172
- This test asserts the merge-base warning appears on stdout, but warnings should go to stderr to avoid corrupting the script's normal stdout output. Update the assertion to check
capsys.err.
assert changed_lines.resolve_merge_base(repo, "refs/heads/no-such-branch") == "refs/heads/no-such-branch"
assert "cannot resolve merge base" in capsys.readouterr().out
`injective` is the precise term for the property the fix rests on -- the length-prefixed encoding maps distinct field tuples to distinct strings -- so it goes in the dictionary rather than being paraphrased away. `neighbours` becomes `neighbors`, matching the spelling already used elsewhere in the tree. The job's other reported words come from files this branch does not touch; that is a defect in how the changed-line set is computed, fixed separately in microsoft#3530. Signed-off-by: LHMQ878 <230791102+LHMQ878@users.noreply.github.com>
Without `--output` this script writes its result to stdout, so the
fallback warning was interleaved with the data: a caller reading
`--mode changed-files` from stdout gets the words of the warning as
file names. Reproduced -- with an unresolvable base and stderr
discarded, the sole line on stdout is `warning: cannot resolve merge
base ...`.
The spell-check workflow passes `--output` and so was never affected,
but `--output` is optional and the fallback exists for exactly the
shallow clone a CI job runs in, so any other stdout consumer would hit
it.
Asserted at both levels: the existing unit test now checks `capsys`
`.err` and additionally that `.out` is empty, and a new CLI test runs
the script as a subprocess over two unrelated root commits -- a ref that
does not exist would fail the fallback `git diff` as well and never
reach the case -- then requires every stdout line to be a path the diff
produced.
Restoring the `print` to stdout fails both:
AssertionError: assert 'cannot resolve merge base' in '' (x2)
Signed-off-by: LHMQ878 <LHMQ878@users.noreply.github.com>
|
Done in Worth flagging that this is a bit more than a tidiness fix, which I only saw when writing the test. Without A caller doing So I asserted it at both levels rather than only where you suggested:
One thing the second test cost me a pass on: my first version used a nonexistent ref, which fails the fallback Discrimination: restoring the
Ready for CI whenever you approve the runs. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
tests/ci/test_changed_lines.py:172
- If the merge-base warning is emitted on stderr (so stdout remains clean for script output), this assertion should check
capsysstderr rather than stdout.
assert changed_lines.resolve_merge_base(repo, "refs/heads/no-such-branch") == "refs/heads/no-such-branch"
scripts/ci/changed_lines.py:75
- The fallback warning is printed to stdout, which will corrupt the script’s normal stdout output when
--outputis not used (e.g., piping the added-lines output into another tool). Warnings/logs should go to stderr so the data stream stays parseable.
# checks built on this -- they flag too much rather than miss something.
message = result.stderr.strip() or f"git merge-base exited {result.returncode}"
# stderr, not stdout: without `--output` this script emits its result on
|
A live instance of this on another of my open PRs, in case it is useful for prioritising: #3512 is failing Same measurement, exact job command ( Every flagged word is in a file #3512 does not touch. That is the second branch where I have measured it, and the failure mode is identical: the count is three orders of magnitude off, and the contributor has no way to make it pass except rebasing. No change requested here — this PR already fixes it. Just recording a second data point on a real red check. |
|
Worth prioritising this one: Note #3568 targets the same gate by scoping the diff to the PR merge-base, so the two overlap. Worth a maintainer picking one and closing the other rather than both landing. |
|
liamcrumm both of your conditions are met — the stderr change went in as print(
f"warning: cannot resolve merge base with {base} ({message}); diffing against its tip instead",
file=sys.stderr,
)and the test now asserts on captured = capsys.readouterr()
assert "cannot resolve merge base" in captured.err
assert captured.out == ""CI on Imran Siddique (@imran-siddique) I checked the list you posted. Of the 14, 10 are currently red on The logs confirm it's this root cause rather than real typos. On #3120 — a 2-file, +72/-6 TypeScript change: A 2-file PR produced an added-lines file 396 lines long, and The inverse is worth noting as well: #3440 has exactly one hit, Ready to merge whenever you are; happy to rebase if anything has moved. |
…ines Rewrites the spell-check.yml comment to present the explicit refspec as hardening rather than as the fix for an observed over-scan. The stale-ref causal claim did not hold: #3530 had already corrected the scoping, and runs after it scope correctly. Also fixes a real defect in extract_added_lines. The `+++ b/path` header was skipped by prefix, which silently drops an added line whose own content starts with `++` (rendered `+++...` in the diff). Skipping by hunk position separates the two exactly. Adds a regression test that fails without the fix, plus coverage for the GITHUB_ACTIONS annotation branch.
…ines Rewrites the spell-check.yml comment to present the explicit refspec as hardening rather than as the fix for an observed over-scan. The stale-ref causal claim did not hold: #3530 had already corrected the scoping, and runs after it scope correctly. Also fixes a real defect in extract_added_lines. The `+++ b/path` header was skipped by prefix, which silently drops an added line whose own content starts with `++` (rendered `+++...` in the diff). Skipping by hunk position separates the two exactly. Adds a regression test that fails without the fix, plus coverage for the GITHUB_ACTIONS annotation branch. Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
…#3640) * fix(ci): scope spell-check to the PR's own diff, not a stale base ref `git fetch origin <branch>` only writes FETCH_HEAD unless the configured refspec covers the branch. actions/checkout narrows remote.origin.fetch to the ref it checked out, so on a pull request `refs/remotes/origin/<base>` keeps whatever value it had at checkout time. `--base origin/<base>` then resolves a merge base against that stale ref. It succeeds, so no warning fires, but it points far enough back that every commit landing on the base since is reported as added. PR #3567 changes two TypeScript files totalling 83 added lines and was spell-checked against several hundred lines it never touched, failing on words absent from its diff (AEDT, anthonyonazure, Clendenen, langgenius, ringbreachdetector). 22 of 68 open PRs currently have a red spell-check. - Fetch the base with an explicit refspec so the remote-tracking ref is actually updated. - Emit the merge-base fallback as a ::warning:: annotation as well as on stderr. On stderr alone an over-reporting run is indistinguishable from a correctly scoped one at the point where the check result is read, which is how this went unnoticed while failing unrelated PRs. - Add the two words genuinely introduced by open PRs: `linkbase` (#3620) and `unpermitted` (#3200). The rest of the currently-flagged words are artifacts of the over-report and go away with the scoping fix. Refs #3514. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * fix(ci): correct the scoping rationale and stop dropping ++ content lines Rewrites the spell-check.yml comment to present the explicit refspec as hardening rather than as the fix for an observed over-scan. The stale-ref causal claim did not hold: #3530 had already corrected the scoping, and runs after it scope correctly. Also fixes a real defect in extract_added_lines. The `+++ b/path` header was skipped by prefix, which silently drops an added line whose own content starts with `++` (rendered `+++...` in the diff). Skipping by hunk position separates the two exactly. Adds a regression test that fails without the fix, plus coverage for the GITHUB_ACTIONS annotation branch. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * test(ci): drop deliberate misspellings from new fixtures, allow refspec The new tests exercise extract_added_lines, which only extracts, so the fixtures never needed misspelled words. Using them meant the added lines reintroduced typoo and tokenn into the spell-check scan, which the job correctly flagged. Adds refspec to the repo terms: a genuine git term now used in the spell-check.yml comment. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * ci(cspell): drop linkbase, #3620 now carries it #3620 added linkbase to .cspell-repo-terms.txt itself and is green on all 96 checks, so this copy no longer unblocks anything. Both PRs adding the same line to the same file would conflict, or leave a duplicate entry, depending on merge order. refspec is used by this PR's own workflow comment. unpermitted stays: #3200 uses the word in a test name, adds no dictionary entry, and its Spell-check changed files is still red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BiraRPG9NcLDZsNSmSXxE7 Signed-off-by: Imran Siddique <imran.siddique@opaque.co> --------- Signed-off-by: Imran Siddique <imran.siddique@opaque.co> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…microsoft#3640) * fix(ci): scope spell-check to the PR's own diff, not a stale base ref `git fetch origin <branch>` only writes FETCH_HEAD unless the configured refspec covers the branch. actions/checkout narrows remote.origin.fetch to the ref it checked out, so on a pull request `refs/remotes/origin/<base>` keeps whatever value it had at checkout time. `--base origin/<base>` then resolves a merge base against that stale ref. It succeeds, so no warning fires, but it points far enough back that every commit landing on the base since is reported as added. PR microsoft#3567 changes two TypeScript files totalling 83 added lines and was spell-checked against several hundred lines it never touched, failing on words absent from its diff (AEDT, anthonyonazure, Clendenen, langgenius, ringbreachdetector). 22 of 68 open PRs currently have a red spell-check. - Fetch the base with an explicit refspec so the remote-tracking ref is actually updated. - Emit the merge-base fallback as a ::warning:: annotation as well as on stderr. On stderr alone an over-reporting run is indistinguishable from a correctly scoped one at the point where the check result is read, which is how this went unnoticed while failing unrelated PRs. - Add the two words genuinely introduced by open PRs: `linkbase` (microsoft#3620) and `unpermitted` (microsoft#3200). The rest of the currently-flagged words are artifacts of the over-report and go away with the scoping fix. Refs microsoft#3514. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * fix(ci): correct the scoping rationale and stop dropping ++ content lines Rewrites the spell-check.yml comment to present the explicit refspec as hardening rather than as the fix for an observed over-scan. The stale-ref causal claim did not hold: microsoft#3530 had already corrected the scoping, and runs after it scope correctly. Also fixes a real defect in extract_added_lines. The `+++ b/path` header was skipped by prefix, which silently drops an added line whose own content starts with `++` (rendered `+++...` in the diff). Skipping by hunk position separates the two exactly. Adds a regression test that fails without the fix, plus coverage for the GITHUB_ACTIONS annotation branch. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * test(ci): drop deliberate misspellings from new fixtures, allow refspec The new tests exercise extract_added_lines, which only extracts, so the fixtures never needed misspelled words. Using them meant the added lines reintroduced typoo and tokenn into the spell-check scan, which the job correctly flagged. Adds refspec to the repo terms: a genuine git term now used in the spell-check.yml comment. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * ci(cspell): drop linkbase, microsoft#3620 now carries it microsoft#3620 added linkbase to .cspell-repo-terms.txt itself and is green on all 96 checks, so this copy no longer unblocks anything. Both PRs adding the same line to the same file would conflict, or leave a duplicate entry, depending on merge order. refspec is used by this PR's own workflow comment. unpermitted stays: microsoft#3200 uses the word in a test name, adds no dictionary entry, and its Spell-check changed files is still red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BiraRPG9NcLDZsNSmSXxE7 Signed-off-by: Imran Siddique <imran.siddique@opaque.co> --------- Signed-off-by: Imran Siddique <imran.siddique@opaque.co> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…microsoft#3640) * fix(ci): scope spell-check to the PR's own diff, not a stale base ref `git fetch origin <branch>` only writes FETCH_HEAD unless the configured refspec covers the branch. actions/checkout narrows remote.origin.fetch to the ref it checked out, so on a pull request `refs/remotes/origin/<base>` keeps whatever value it had at checkout time. `--base origin/<base>` then resolves a merge base against that stale ref. It succeeds, so no warning fires, but it points far enough back that every commit landing on the base since is reported as added. PR microsoft#3567 changes two TypeScript files totalling 83 added lines and was spell-checked against several hundred lines it never touched, failing on words absent from its diff (AEDT, anthonyonazure, Clendenen, langgenius, ringbreachdetector). 22 of 68 open PRs currently have a red spell-check. - Fetch the base with an explicit refspec so the remote-tracking ref is actually updated. - Emit the merge-base fallback as a ::warning:: annotation as well as on stderr. On stderr alone an over-reporting run is indistinguishable from a correctly scoped one at the point where the check result is read, which is how this went unnoticed while failing unrelated PRs. - Add the two words genuinely introduced by open PRs: `linkbase` (microsoft#3620) and `unpermitted` (microsoft#3200). The rest of the currently-flagged words are artifacts of the over-report and go away with the scoping fix. Refs microsoft#3514. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * fix(ci): correct the scoping rationale and stop dropping ++ content lines Rewrites the spell-check.yml comment to present the explicit refspec as hardening rather than as the fix for an observed over-scan. The stale-ref causal claim did not hold: microsoft#3530 had already corrected the scoping, and runs after it scope correctly. Also fixes a real defect in extract_added_lines. The `+++ b/path` header was skipped by prefix, which silently drops an added line whose own content starts with `++` (rendered `+++...` in the diff). Skipping by hunk position separates the two exactly. Adds a regression test that fails without the fix, plus coverage for the GITHUB_ACTIONS annotation branch. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * test(ci): drop deliberate misspellings from new fixtures, allow refspec The new tests exercise extract_added_lines, which only extracts, so the fixtures never needed misspelled words. Using them meant the added lines reintroduced typoo and tokenn into the spell-check scan, which the job correctly flagged. Adds refspec to the repo terms: a genuine git term now used in the spell-check.yml comment. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * ci(cspell): drop linkbase, microsoft#3620 now carries it microsoft#3620 added linkbase to .cspell-repo-terms.txt itself and is green on all 96 checks, so this copy no longer unblocks anything. Both PRs adding the same line to the same file would conflict, or leave a duplicate entry, depending on merge order. refspec is used by this PR's own workflow comment. unpermitted stays: microsoft#3200 uses the word in a test name, adds no dictionary entry, and its Spell-check changed files is still red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BiraRPG9NcLDZsNSmSXxE7 Signed-off-by: Imran Siddique <imran.siddique@opaque.co> --------- Signed-off-by: Imran Siddique <imran.siddique@opaque.co> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: yuvrajsingh2428 <offcyuvi2428@gmail.com>
The bug
scripts/ci/changed_lines.pyrunsgit diff <base>, a two-dot diff: base tip vs working tree. Every commit that lands onmainafter a branch is cut therefore appears in it — and the pre-existing side of each of those appears as an added line, because the working tree still holds it whilemainno longer does.The spell-check job scopes cspell to exactly those lines, so on any branch that has fallen behind, cspell is handed repo-wide vocabulary and fails on words the branch never wrote.
Measured on a branch 34 commits behind
main(one commit, 2 files, +142):Every word the job flagged appears zero times in that branch's actual diff:
The reported line numbers went up to 159873, in a file the branch does not touch. So the failure is not "a contributor used an unknown word" — it is the check reading the base branch's own history as the contributor's work. Currently there is no way for a contributor to make it pass other than rebasing.
--mode changed-fileshas the same defect; nothing in-tree uses that mode today, but it answers the same question, so it is fixed by the same change and has a test.The change
The base is resolved to its merge base with
HEADbefore diffing, which excludes the base-side commits.Resolving the base rather than switching the diff to
git diff base...HEADis deliberate: the three-dot form compares two commits and would ignore the working tree, and this script is also usable locally before committing. There's a test asserting uncommitted work is still reported.An unresolvable merge base falls back to the base tip — the previous behaviour — with a warning to the log rather than an exception. Over-reporting is the safe direction for a check built on this: it flags too much rather than missing something.
The
--depth=1in the workflow had to go with itgit fetch origin <base> --depth=1writes.git/shallowinto the full clone thatfetch-depth: 0just made, truncating the history somerge-basecannot reach the divergence point. Verified in an isolated repo — the depth-limited fetch turns a working merge-base into exit code 1:and the diff then behaves as the bug describes:
Note the fetch is why the bug survived: with the graft in place
merge-basewould fail on every run, so the fix would silently do nothing. That is also what the fallback test pins down, so a future reintroduction of--depthdegrades visibly instead of quietly.Verification
tests/ci/test_changed_lines.py, over real git repositories with a base that has moved on. Reverting only the script fails exactly those 4 — 3 assertion failures and theresolve_merge_baseattribute error — and leaves the 3 existing tests passing.pytest tests/ci— 72 passed, 6 skipped.ruff check ... --select E,F,W --ignore E501clean, matching whatci.ymlruns. I did not runruff format: the repo has no ruff config at the root and the committed file does not satisfy the default formatter, so running it would have reformatted lines this PR does not touch. The test file diff is +117/-0.generate_workflows.py --checkreports no drift —spell-check.ymlis hand-authored, not generated.cspell@8.17.3 --config .cspell.jsonfinds none of the words above.I ran into this because it is failing my own open PRs, so the "34 commits behind" branch above is mine. The fix is not specific to it — every PR that sits for a while hits this.
I'm happy to split the
--depth=1removal into its own commit if you'd prefer to review them separately, and to add.cspell.jsonentries instead if you consider the current base-tip behaviour intentional.