Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add CI guard for manual .xlf edits in human-authored PRs - #8902

Closed
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard
Closed

Add CI guard for manual .xlf edits in human-authored PRs#8902
Amaury Levé (Evangelink) wants to merge 6 commits into
mainfrom
dev/amauryleve/xlf-manual-edit-guard

Conversation

@Evangelink

@EvangelinkAmaury Levé (Evangelink) commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

The repository policy in copilot-instructions.md → Localization Guidelines is explicit:

NEVER manually modify *.xlf files. Instead, regenerate them by running dotnet msbuild <project>.csproj /t:UpdateXlf on the owning project.

Despite that, hand-edited .xlf files do occasionally land in PRs. They get silently reverted by the next OneLocBuild bulk update, so the fix is lost and any regression returns. This PR encodes the rule as a CI check so it's a property of the repository rather than reviewer vigilance.

What the guard does

  • Triggers on every pull_request_target whose diff touches **/*.xlf or **/*.resx (workflow definition is sourced from the base ref, making it tamper-resistant — see "Tamper-resistance" below).
  • Bot PRs are exempt (dotnet-bot, dotnet-maestro, any *[bot] account) — OneLocBuild owns the bulk update path.
  • For human PRs, every changed .xlf is mapped to its expected .resx path. The mapping uses the repo-standard <dir>/Resources/xlf/<Base>.<locale>.xlf<dir>/Resources/<Base>.resx layout convention for strict full-path matching, with a basename-only fallback for any future non-standard layout. If the matching .resx isn't in the diff, the check fails with a clear remediation message.

Why blocking rather than a soft notice

  • The policy uses "NEVER" — this is a hard rule, not a guideline.
  • The escape hatch is trivial (touch the .resx too, or get the OneLocBuild bot to do it), so the cost of a false positive is low.
  • Translation typo fixes that are genuinely one-off go via the OneLocBuild submission flow; hand-edits get overwritten anyway.

If reviewers prefer a soft warning (::notice annotation), I can switch the workflow — just say the word. I went with blocking because the policy wording justifies it.

Tamper-resistance

Both the workflow file and the script are sourced from the BASE commit so a PR cannot weaken its own gate:

  • Workflow: triggered by pull_request_target, which runs the workflow definition from the base ref. actions/checkout is pinned to ref: ${{ github.event.pull_request.head.sha }} so the git diff still evaluates the PR's tree.
  • Script: the run step does git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1" into $RUNNER_TEMP and invokes that copy. Falls back to the PR working copy only when the script doesn't yet exist at the base (bootstrap PRs).
  • Permissions: permissions: contents: read only, no secrets used, no execution of PR-controlled code — so the elevated default token associated with pull_request_target is inert here and the standard PWN risk doesn't apply.

Bootstrap (this PR only)

Because pull_request_target sources the workflow file from the base ref, the guard step does not run on this introducing PR — main doesn't yet contain xlf-manual-edit-guard.yml. The PR is verified instead by the local smoke-test scenarios listed under "Tested locally" below. Once merged, subsequent PRs touching **/*.xlf or **/*.resx will trigger the guard from the merged-in base workflow file.

Files added

  • .github/scripts/check-xlf-manual-edit.ps1 — PowerShell diff parser with tight ^[A-Za-z0-9._/+\-]+$ path-safety regex (rejects context-line decoys), repo-standard Resources/xlf layout-aware .xlf.resx mapping, exit-code reporting
  • .github/workflows/xlf-manual-edit-guard.yml — paths-scoped pull_request_target workflow, contents: read only, shell: pwsh (pre-installed on ubuntu-latest), 5-minute timeout

Tested locally

Seven synthetic diffs covering: bot exemption, [bot]-suffix wildcard, locale-bearing .xlf with matching .resx, no-locale .xlf, multi-segment locale (zh-Hans), the testfx Resources/xlf/ cross-directory layout, the cross-project basename-collision case (e.g. ExtensionResources.resx appears in 9 different projects — would have false-passed under basename-only matching), and a unified-diff with misleading context lines that mention .xlf paths in comments. All cases produced the expected exit code.


Part of a small batch of repo-maintenance improvements identified from a one-month review of PR review-comment patterns (companion PRs: #8898expert-reviewer extensions, #8899msbuild-reviewer Rule B-3, #8900 PublicAPI init-accessor guard, #8901 CLI help-text drift reminder).

The repository policy (.github/copilot-instructions.md, Localization
Guidelines section) is explicit:
NEVER manually modify *.xlf files. Instead, regenerate them by
running `dotnet msbuild <project>.csproj /t:UpdateXlf` on the
owning project.
The OneLocBuild bot (dotnet-bot) owns the bulk translation update
path. A human PR may legitimately touch *.xlf files only when those
changes are the output of /t:UpdateXlf after a *.resx edit -- in which
case the same PR will also include the *.resx change. A PR that
hand-edits *.xlf without any *.resx change is the defect this guard
catches.
What the guard does:
* On every pull_request whose diff touches *.xlf or *.resx, run
.github/scripts/check_xlf_manual_edit.py.
* Bot PRs (dotnet-bot, dotnet-maestro, any *[bot] account) are exempt
and the script exits 0 immediately.
* For human PRs the script extracts the basename of each changed *.xlf
(e.g. `Resources/xlf/Resource.cs.xlf` -> `Resource`) and checks
whether the diff also includes a *.resx with the same basename.
Basename matching is used (not strict path matching) because testfx
places *.xlf in a `xlf/` subdirectory while the matching *.resx
lives in the parent `Resources/` directory.
* If any *.xlf is unaccompanied, the workflow fails with a clear
remediation message (edit the *.resx, run /t:UpdateXlf, commit
both).
Why blocking rather than a soft notice:
* The policy uses "NEVER" -- this is a hard rule, not a guideline.
* The escape hatch is trivial (touch the *.resx too, or get the
OneLocBuild bot to do it), so the cost of a false positive is low.
* Translation typo fixes that are intentionally one-off can be done
via the OneLocBuild submission flow; manual *.xlf hand-edits would
be overwritten on the next bulk update anyway.
Why this matters now:
* Hand-edited *.xlf files get silently reverted by the next
OneLocBuild run, so the fix is lost and the bug regresses.
* The current PR-review process catches this inconsistently; encoding
the rule as a CI check makes it a property of the repository rather
than reviewer vigilance.
Tested locally with five synthetic diffs covering: bot exemption,
[bot]-suffix wildcard, locale-bearing *.xlf with matching *.resx,
no-locale *.xlf, multi-segment locale (zh-Hans), and the testfx
`Resources/xlf/` cross-directory layout. All cases produced the
expected exit code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:33
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed

CopilotAI 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

Adds a pull-request-time CI guard that enforces the repo’s localization policy: human-authored PRs must not include standalone .xlf changes (they must be accompanied by a corresponding .resx change), while bot-authored localization updates are exempt.

Changes:

  • Introduces a new pull_request workflow scoped to .xlf/.resx changes that runs a fast Python-based validation step.
  • Adds a Python script that detects whether changed .xlf files have a matching .resx basename also changed in the same PR, and emits a clear remediation report on failure.
  • Implements bot author exemptions (known bot list + *[bot] suffix) to allow OneLocBuild/dependency-bot localization updates.
Show a summary per file
FileDescription
.github/workflows/xlf-manual-edit-guard.ymlNew PR workflow to run the guard on .xlf/.resx diffs with minimal permissions and short timeout.
.github/scripts/check_xlf_manual_edit.pyImplements the diff path parsing, bot exemption logic, basename matching, and step-summary reporting used by the workflow.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR adds a new GitHub Actions workflow and a Python script to enforce the repository's localization policy: .xlf files must never be edited by hand — they must be regenerated via dotnet msbuild /t:UpdateXlf after editing the corresponding .resx.

Verdict table

DimensionResult
Logic / Correctness✅ LGTM
Security✅ LGTM
CI/CD Workflow Quality✅ LGTM
Error Handling💡 One suggestion (non-blocking, see inline comment)

Notable positives

  • Regex is correct.r"\.([a-zA-Z]{2,3}(?:-[A-Za-z0-9]+)*)\.xlf$" correctly strips the locale suffix (e.g. Strings.de.xlfStrings) and falls back gracefully to Path.stem for locale-less XLF files. Basename-only matching is intentionally permissive to accommodate xlf/ subdirectory layouts.
  • Bot exemption logic is clean: both the KNOWN_BOTS allowlist and the [bot]-suffix catch cover all expected automated actors (OneLocBuild, Maestro, GitHub Actions).
  • Minimal permissions (contents: read only) and timeout-minutes: 5 are appropriate.
  • pull_request (not pull_request_target) is the correct, safer trigger for a read-only guard that needs to run on fork PRs.
  • fetch-depth: 0 is required and present for the three-dot git diff origin/<base>...HEAD diff to resolve correctly.
  • write_step_summary is guarded against OSError so the script never crashes if GITHUB_STEP_SUMMARY is unavailable.

One non-blocking suggestion

See the inline comment on _run: subprocess.run itself can raise FileNotFoundError / OSError if git is not on PATH. In the ubuntu-latest CI environment this never fires in practice, but a small try/except OSError guard around the call would make the documented exit code 2 ("usage / IO error") accurate for that edge case and produce a friendlier error message than a raw Python traceback.

Generated by Expert Code Review (on open) for issue #8902 · sonnet46 3.4M

Comment thread.github/scripts/check_xlf_manual_edit.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at current implementation. But I think the only reliable way to detect this properly would be:

  1. Revert all XLF changes in the given PR.
  2. Touch the corresponding resx (so that it has higher timestamp and MSBuild can see that UpdateXlf is not update to date)
  3. Run dotnet build /t:UpdateXlf on the corresponding project, and ensure it's the same set of changes that were in the PR.

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
* Move bot-author exemption from Python to the workflow `if:`.
Per the reviewer, the Python script should focus on diff analysis
and not need to know about workflow context. Removed `KNOWN_BOTS`,
`is_bot()`, `--author`, and the `PR_AUTHOR` env var from the
script. The workflow now skips the whole job via:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dotnet-bot' &&
github.event.pull_request.user.login != 'dotnet-maestro'
`user.type == 'Bot'` covers GitHub App identities; the explicit
logins cover the dotnet-bot / dotnet-maestro user accounts.
* Pin actions to SHA per the repository's lock-file convention. The
workflow now uses:
actions/checkout@34e1148 # v4.3.1
actions/setup-python@a26af69 # v5.6.0
These are the current tip-of-v4 / tip-of-v5 commits as of
2026-06-07.
* Empty except: narrow `except Exception` to
`except (AttributeError, ValueError, OSError)` and add an
explanatory comment. The previous bare except swallowed any
programmer error from `sys.stdout.reconfigure`; the new tuple
matches the documented failure modes only.
* Drive-by while in the workflow: also switch the diff base from
`origin/<base.ref>` to the immutable `base.sha` from the event
payload, mirroring the fix applied to PR #8900. A base-branch
advance between the event and the checkout fetch could otherwise
pull in unrelated commits.
* Rename the env var to `BASE_REF` (with `BASE_SHA` fallback in the
script's `--base` default for backward compatibility) for
consistency with the rest of this PR series.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread.github/scripts/check_xlf_manual_edit.py Fixed
The previous fix added an explanatory comment ABOVE the try/except, but
github-code-quality's empty-except rule looks for a comment INSIDE the
except body. Move/extend the comment so it lives next to the `pass`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 11:24

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Comment thread.github/scripts/check_xlf_manual_edit.py Outdated
Match testfx's existing PowerShell tooling convention (eng/common/,
eng/scripts/, .github/scripts/scan-duplicates.ps1) instead of bringing
in a Python runtime dependency for a single guard script.
- Adds .github/scripts/check-xlf-manual-edit.ps1 with the same logic:
parse a diff (unified or name-only), strip the `.<locale>.xlf` suffix
to compute the resx basename, flag every .xlf with no matching .resx
change in the same PR.
- Removes .github/scripts/check_xlf_manual_edit.py.
- Drops actions/setup-python from the workflow and switches the
run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
- Preserves the BASE_REF / BASE_SHA env-var fallback so existing
workflow invocations keep working.
Smoke-tested locally with synthetic name-only diff inputs:
(a) .xlf alone → exit 1 with report, (b) .xlf + matching .resx → 0,
(c) no .xlf changes → 0, (d) .xlf + .zh-Hans.xlf + .resx → 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three concerns flagged on the previous Python version still applied to
the PowerShell port:
1. Guard script sourced from the PR checkout (untrusted)
A PR could weaken its own gate by editing the script in the same
commit. The workflow now sources the script from the base commit
via `git show "$BASE_REF:.github/scripts/check-xlf-manual-edit.ps1"`
into $RUNNER_TEMP and runs that copy, falling back to the PR copy
only when the script doesn't yet exist at base (bootstrap PRs).
2. Basename-only matching false-passes on cross-project basename
collisions
`ExtensionResources.resx` appears in 9 different projects in this
repo. A PR could hand-edit `proj-A/.../xlf/ExtensionResources.de.xlf`
while touching an unrelated `proj-B/.../ExtensionResources.resx`,
and the basename-only check would let it through.
Added `Get-XlfExpectedResx` that uses the repo-standard layout
mapping `<dir>/Resources/xlf/<Base>.*.xlf` → `<dir>/Resources/<Base>.resx`
for strict full-path matching. Basename fallback retained for any
future non-standard layout.
3. Unified-diff fallback parser too lax
Context lines were trimmed before classification, so a code-comment
line like ` /// see some/file.xlf` got promoted to a path. The
parser now (a) classifies hunk-header / context / +/- lines via the
RAW (untrimmed) prefix, and (b) rejects any candidate path that
doesn't match the `^[A-Za-z0-9._/+\-]+$` repo-safe alphabet.
Smoke-tested locally with:
- Cross-project basename collision → correctly FLAGS (was missed pre-fix).
- Matching same-project pair → PASS.
- Multi-locale .xlf with single matching .resx → PASS.
- Hand-edit with no .resx → FLAGS.
- Unified diff with misleading context lines → only flags the real .xlf
header, ignores the comment-line decoys.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:41

CopilotAI 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's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
Comment thread.github/workflows/xlf-manual-edit-guard.yml
…tance
Two related Copilot review comments on a09f2ed flagged that even with the
script sourced from the base commit, the workflow file itself was still
sourced from the PR head — so a PR could edit `.github/workflows/xlf-manual-edit-guard.yml`
(e.g. broaden the bot `if:`) and bypass the gate while still producing
a "successful" check.
Fix: switch the trigger from `on: pull_request` to `on: pull_request_target`
so the workflow definition is taken from the BASE ref (tamper-resistant).
This required two compensating changes:
1. actions/checkout: pin `ref: ${{ github.event.pull_request.head.sha }}`.
`pull_request_target` otherwise checks out the base ref, leaving HEAD
== base and the diff empty — silently turning the gate into a no-op.
2. Header comment: documents the tamper-resistance model end-to-end:
workflow from base, script from base, PR HEAD only used for the diff
subject. Also documents why the usual `pull_request_target` PWN risk
(elevated default GITHUB_TOKEN on fork PRs) does not apply here: the
workflow has `permissions: contents: read` only, uses no secrets,
and never executes PR-controlled code (it only runs our own
base-sourced script via pwsh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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

@Evangelink@Youssef1313