Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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" + '
Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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('^' + ".*" + ' Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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('^' + ".*" + ' Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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" + ' Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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('^' + ".*" + ' Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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('^' + ".*" + ' Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink
, '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); } })(); })(); Add PR-time guard: forbid new \init\ accessors on public API by Evangelink · Pull Request #8900 · microsoft/testfx · GitHub
Skip to content

Add PR-time guard: forbid new \init\ accessors on public API - #8900

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard
Jun 8, 2026
Merged

Add PR-time guard: forbid new \init\ accessors on public API#8900
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/public-api-init-guard

Conversation

@Evangelink

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

Copy link
Copy Markdown
Member

Summary

Adds a fast PR-time guard that fails when a PR adds a new init accessor to any PublicAPI.Unshipped.txt file. The check is a PowerShell script + plain GitHub Actions workflow — no LLM, no agentic workflow, no gh aw compile — so it runs in under 10s on every applicable PR.

Why

The no-init policy is already documented in two places:

…but enforcement today depends on a reviewer noticing the .init -> void line in a PublicAPI.Unshipped.txt diff. expert-reviewer.agent only runs on opt-in / on a schedule, so a fast-moving PR can land before it weighs in. This guard runs unconditionally on every PR that touches a PublicAPI.Unshipped.txt.

Grandfathered entries already in PublicAPI.Shipped.txt are intentionally not flagged — there are ~50 of them in src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Shipped.txt and the policy explicitly exempts them.

What changed

FilePurpose
.github/scripts/check-public-api-init.ps1The check itself. Parses a unified diff (from git diff or -DiffFile), finds additions to any PublicAPI.Unshipped.txt, and flags lines matching .init -> void. Writes a markdown report to $GITHUB_STEP_SUMMARY on failure.
.github/workflows/public-api-init-guard.ymlpull_request workflow, paths-scoped to **/PublicAPI.Unshipped.txt and the script/workflow themselves. contents: read only. Concurrency-cancelling per-PR. Runs via shell: pwsh (pre-installed on ubuntu-latest), no extra setup step.

Validation

Locally tested with two synthetic diffs:

  1. Violation diff (two .init -> void additions to an Unshipped.txt, one .init -> void addition to a Shipped.txt):

    ❌ Public-API policy violation: new `init` accessors detected.
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Bar.init -> void`
    - `src/Foo/PublicAPI/PublicAPI.Unshipped.txt` → `Microsoft.Testing.Platform.Foo.Quux.init -> void`
    Exit: 1
    

    Correctly ignored the Shipped.txt addition and the regular .set -> void line.

  2. Clean diff (regular getter/setter pair):

    ✅ No new `init` accessors detected in PublicAPI.Unshipped.txt additions.
    Exit: 0
    

Risk

  • New required check on a narrow set of PRs (only those touching **/PublicAPI.Unshipped.txt). Existing PRs with valid init removals or unrelated edits won't see it.
  • The PowerShell regex (\.init\s*->\s*void\s*$) is tight enough to not flag explanatory comments or the *REMOVED* / *NULLABILITY* sentinel lines used by the public-API analyzer (the script skips lines starting with # or *).
  • contents: read only — cannot mutate the PR.
  • If this guard is later made a required check via branch protection, the renumbering of rules in PR [msbuild-reviewer] Add Rule B-3: catch bare-token property typos in MSBuild conditions #8899 (msbuild-reviewer Rule B-3) provides a related precedent for catching grandfather/new boundary cases.

Follow-ups (not in this PR)

  • Could be extended to also flag new public API in src/**/*.cs that doesn't appear in any PublicAPI.Unshipped.txt diff, but that needs Roslyn-quality parsing to be reliable — out of scope here.

🤖 Authored with GitHub Copilot CLI based on a one-month review-comment audit of microsoft/testfx.

Adds a lightweight (Python + workflow, no LLM) PR check that fails the build
whenever a PR adds a line matching '.init -> void' to any PublicAPI.Unshipped.txt
file. Grandfathered entries already in PublicAPI.Shipped.txt are intentionally
ignored.
Background: copilot-instructions.md and expert-reviewer.agent dimension #4
both already document the no-init policy, but the rule is enforced only by
reviewer attention today. Surfaces of this policy:
- .github/copilot-instructions.md (Public API guidelines section)
- .github/agents/expert-reviewer.agent.md (Overarching Principle #2)
This guard runs in <10s, has no LLM cost, and is paths-scoped to PRs that
actually touch a PublicAPI.Unshipped.txt file or the guard itself.
Files:
- .github/scripts/check_public_api_init.py - script with --diff-file support
for offline testing, follows the .github/scripts/check_vendored_files.py
style already in the repo.
- .github/workflows/public-api-init-guard.yml - pull_request workflow,
paths-scoped, concurrency-cancelling, contents:read only.
Tested locally with a synthetic diff containing two .init additions in
Unshipped.txt and one in Shipped.txt: the script correctly reports the two
Unshipped violations and ignores the Shipped addition, exits 1. Clean diff
exits 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 7, 2026 08:28
Comment thread.github/scripts/check_public_api_init.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 lightweight, PR-scoped GitHub Actions guard to enforce the repo policy that new public APIs must not introduce init accessors, by scanning PR diffs for additions of .init -> void in PublicAPI.Unshipped.txt.

Changes:

  • Adds a new pull_request workflow that runs only when **/PublicAPI.Unshipped.txt (or the guard itself) changes.
  • Adds a Python script that parses a unified diff and flags newly-added .init -> void lines in PublicAPI.Unshipped.txt.
  • Emits a human-readable report to both console output and GITHUB_STEP_SUMMARY when violations are found.
Show a summary per file
FileDescription
.github/workflows/public-api-init-guard.ymlNew PR workflow wiring: checkout, base computation, and running the Python guard (paths-scoped and least-privilege).
.github/scripts/check_public_api_init.pyDiff parser + policy enforcement: identifies new .init -> void additions in PublicAPI.Unshipped.txt and prints a markdown report.

Copilot's findings

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

Comment thread.github/workflows/public-api-init-guard.yml Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/scripts/check_public_api_init.py Outdated

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.

#DimensionVerdict
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT
20Build Infrastructure🔵 1 NIT

✅ 18/21 dimensions clean (15 N/A for Python/YAML-only PR, 3 confirmed LGTM).

  • Test Completeness — parse_diff() has no automated tests

Dimension notes

Dimensions 2, 4–12, 14–16, 18–19: N/A — No C# source, no test files, no IPC protocol changes, no analyzer code. All skipped per the reviewer spec.

Dimension 1 (Correctness): LGTM — The three-dot git diff semantics are correct for PR merge-base comparison. The INIT_LINE regex (\.init\s*->\s*void\s*$) is anchored correctly and matches the exact format the public-API analyzer emits. The startswith("+") / not startswith("+++") guard correctly excludes file headers. New-file hunks (where the entire file is an addition) are handled correctly. The Shipped.txt vs Unshipped.txt distinction is implemented correctly via basename comparison.

Dimension 3 (Security): LGTMpull_request (not pull_request_target) is the correct trigger; the workflow cannot be hijacked by PR authors to gain elevated permissions. permissions: contents: read is minimal. The subprocess.run(cmd, ...) call passes base as a list element, not in a shell string, so there is no injection surface. ${{ github.event.pull_request.base.ref }} is written to an env: variable and only echo'd into $GITHUB_OUTPUT wrapped in double-quotes — safe for branch names (which cannot contain newlines in GitHub).

Dimension 13 (Test Completeness): MODERATE — The parse_diff() function contains several non-trivial branches and is the core of the entire guardrail, but has no automated tests. The PR description notes "locally tested with two synthetic diffs", which is not reproducible in CI. Regressions in the parsing logic (e.g., a future edit flipping the startswith('+') guard) would silently pass the self-test since the diff of the script itself contains no PublicAPI.Unshipped.txt changes. Worth adding a small pytest suite under .github/scripts/tests/ covering: (a) a diff that creates a new PublicAPI.Unshipped.txt entirely, (b) a diff containing both Unshipped.txt and Shipped.txt hunks, (c) the sentinel-line skip (#/* prefixes), and (d) a clean diff with no violations. Note: check_vendored_files.py similarly has no unit tests, so this is consistent with current repo practice — but for a new policy guardrail this is worth addressing as a follow-up.

Dimension 17 (Documentation): NIT — Inline comment on line 24 filed above (/tmp/pr.diff reference in docstring).

Dimension 20 (Build Infrastructure): NIT — Inline comment on line 53 filed above (unnecessary setup-python step). Actions are pinned to tag aliases (@v4, @v5) rather than commit SHAs; this is consistent with the repo's existing hand-written workflows (check-vendored-files.yml, markdownlint.yml, dedup-analysis.yml), so not a regression, but SHA pinning would provide stronger supply-chain assurance.

Dimension 21 (Scope & PR Discipline): LGTM — Clean single-concern PR. Follow-up work (checking new public API in .cs that doesn't appear in any PublicAPI.Unshipped.txt) is explicitly called out as out of scope with a rationale.

Generated by Expert Code Review (on open) for issue #8900 · sonnet46 2.5M

Comment thread.github/scripts/check_public_api_init.py Outdated
Comment thread.github/workflows/public-api-init-guard.yml Outdated
* Docstring: fix two-dot vs three-dot mismatch. The implementation
uses `git diff <base>...HEAD` (three-dot) but the usage text
said `..HEAD` (two-dot). Updated the docstring to `...HEAD`
and added a sentence explaining the three-dot semantics (changes
on HEAD since the merge-base with BASE_SHA).
* Docstring: drop the misleading `/tmp/pr.diff` example -- CI
runs the script directly without writing a diff file. Replaced
with a local `git diff -U0 main...HEAD > pr.diff` example so
the `--diff-file` flag still has a concrete use case shown.
* Workflow: switch the diff base from the named base ref to the
exact base SHA captured in the event payload. The named ref
can advance between the PR event firing and the checkout fetch,
which would pull in unrelated commits and false-flag
`.init -> void` lines the PR never touched. `base.sha` is
immutable for the duration of the PR event and is the merge-base
candidate, so three-dot diff `<base.sha>...HEAD` reliably yields
the PR's net public-API change. This also lets us drop the
separate "Compute base ref" step.
* Empty except: add explanatory comment + `# noqa: BLE001`. The
bare except around `sys.stdout.reconfigure` was intentional
(output-encoding setup is best-effort and must not fail the
script), but the silent `pass` left readers wondering. The
comment now documents the intent.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 7, 2026
* 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>
testfx's existing tooling is PowerShell (eng/common/, eng/scripts/,
.github/scripts/scan-duplicates.ps1) — only Arcade's vendored
install-debs.py is Python. Introducing a new Python script + an
actions/setup-python step for a 200-line guard rail added a runtime
dependency the repo otherwise doesn't need.
This rewrite:
- Adds .github/scripts/check-public-api-init.ps1 with the same logic:
parse unified diff, filter PublicAPI.Unshipped.txt additions, flag
any `.init -> void` line, write a Markdown step summary.
- Removes .github/scripts/check_public_api_init.py.
- Drops the actions/setup-python step from the workflow and switches
the run step to `shell: pwsh` (cross-platform on ubuntu-latest).
- Updates `paths:` to track the new .ps1 filename.
Smoke-tested locally with synthetic diff inputs covering: (a) added
.init line in Unshipped.txt → exit 1 with report, (b) shipped .init
line ignored, (c) clean Unshipped.txt → exit 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 8, 2026 08:05

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: 1

Comment thread.github/workflows/public-api-init-guard.yml
@Evangelink
Amaury Levé (Evangelink) merged commit 1873c65 into mainJun 8, 2026
29 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/public-api-init-guard branch June 8, 2026 11:29
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.

2 participants

@Evangelink