ci: gate PR edit automation by changed field (#95010)

* ci: gate PR edit automation by changed field

* ci: include title edits in ClawSweeper dispatch gate

* ci: refine PR edit automation gates

Co-authored-by: Sash Zats <sash@zats.io>

* test(ci): require boolean workflow matcher

Co-authored-by: Sash Zats <sash@zats.io>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Sash Zats
2026-07-16 03:42:14 -04:00
committed by GitHub
parent 48b11d1c66
commit 0960d07d8e
5 changed files with 97 additions and 0 deletions
+14
View File
@@ -27,6 +27,15 @@ permissions: {}
jobs:
label:
if: >-
${{
github.event_name == 'pull_request_target' &&
(
github.event.action != 'edited' ||
github.event.changes.title ||
github.event.changes.base
)
}}
permissions:
contents: read
pull-requests: write
@@ -45,11 +54,13 @@ jobs:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6
if: ${{ github.event.action != 'edited' || github.event.changes.base }}
with:
configuration-path: .github/labeler.yml
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
sync-labels: true
- name: Apply PR size label
if: ${{ github.event.action != 'edited' || github.event.changes.base }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
@@ -139,6 +150,7 @@ jobs:
labels: [targetSizeLabel],
});
- name: Apply maintainer or trusted-contributor label
if: ${{ github.event.action != 'edited' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
@@ -210,6 +222,7 @@ jobs:
// });
// }
- name: Apply beta-blocker title label
if: ${{ github.event.action != 'edited' || github.event.changes.title }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
@@ -263,6 +276,7 @@ jobs:
});
}
- name: Apply too-many-prs label
if: ${{ github.event.action != 'edited' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
@@ -16,6 +16,12 @@ permissions: {}
jobs:
real-behavior-proof:
name: Real behavior proof
if: >-
${{
github.event.action != 'edited' ||
github.event.changes.body ||
github.event.changes.base
}}
permissions:
contents: read
issues: read
+4
View File
@@ -701,7 +701,11 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
["test/scripts/setup-pnpm-store-cache-ensure-node.test.ts"],
],
[".github/images/live-media-runner/Dockerfile", LIVE_MEDIA_RUNNER_IMAGE_TEST_TARGETS],
[".github/workflows/auto-response.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
[".github/workflows/ci.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
[".github/workflows/clawsweeper-dispatch.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
[".github/workflows/labeler.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
[".github/workflows/real-behavior-proof.yml", ["test/scripts/ci-workflow-guards.test.ts"]],
[
".github/workflows/security-sensitive-guard.yml",
["test/scripts/security-sensitive-guard-workflow.test.ts"],
+59
View File
@@ -376,6 +376,19 @@ function readCriticalQualityWorkflow() {
return readFileSync(".github/workflows/codeql-critical-quality.yml", "utf8");
}
function readWorkflow(path: string) {
return parse(readFileSync(path, "utf8"));
}
const PULL_REQUEST_EDIT_FIELDS = ["title", "body", "base"] as const;
function readPullRequestEditFields(condition: unknown) {
const expression = typeof condition === "string" ? condition : "";
return PULL_REQUEST_EDIT_FIELDS.filter((field) =>
expression.includes(`github.event.changes.${field}`),
);
}
function readTrackedText(relativePath: string): string {
if (existsSync(relativePath)) {
return readFileSync(relativePath, "utf8");
@@ -665,6 +678,52 @@ function runGeneratedPublisherScenario(
}
describe("ci workflow guards", () => {
it("routes PR edited metadata only to interested automation", () => {
const autoResponse = readWorkflow(".github/workflows/auto-response.yml");
const clawsweeperDispatch = readWorkflow(".github/workflows/clawsweeper-dispatch.yml");
const labeler = readWorkflow(".github/workflows/labeler.yml");
const realBehaviorProof = readWorkflow(".github/workflows/real-behavior-proof.yml");
for (const workflow of [autoResponse, clawsweeperDispatch, labeler, realBehaviorProof]) {
expect(workflow.on.pull_request_target.types).toContain("edited");
}
expect({
autoResponse: readPullRequestEditFields(autoResponse.jobs["auto-response"].if),
clawsweeperDispatch: readPullRequestEditFields(clawsweeperDispatch.jobs.dispatch.if),
labeler: readPullRequestEditFields(labeler.jobs.label.if),
realBehaviorProof: readPullRequestEditFields(
realBehaviorProof.jobs["real-behavior-proof"].if,
),
}).toEqual({
autoResponse: [],
clawsweeperDispatch: [],
labeler: ["title", "base"],
realBehaviorProof: ["body", "base"],
});
const labelerSteps = labeler.jobs.label.steps;
const changedFieldsForStep = (matcher: (step: WorkflowStep) => boolean) =>
readPullRequestEditFields(labelerSteps.find(matcher)?.if);
expect({
pathLabels: changedFieldsForStep(
(step) => step.uses?.startsWith("actions/labeler@") === true,
),
size: changedFieldsForStep((step) => step.name === "Apply PR size label"),
contributor: changedFieldsForStep(
(step) => step.name === "Apply maintainer or trusted-contributor label",
),
betaBlocker: changedFieldsForStep((step) => step.name === "Apply beta-blocker title label"),
activePrLimit: changedFieldsForStep((step) => step.name === "Apply too-many-prs label"),
}).toEqual({
pathLabels: ["base"],
size: ["base"],
contributor: [],
betaBlocker: ["title"],
activePrLimit: [],
});
});
it("makes the hosted release-gate fallback explicit and exact-SHA only", () => {
const workflow = readCiWorkflow();
const releaseGate = workflow.on.workflow_dispatch.inputs.release_gate;
+14
View File
@@ -1292,6 +1292,20 @@ describe("scripts/test-projects changed-target routing", () => {
});
});
it("keeps PR automation workflow edits on workflow guard tests", () => {
for (const workflowPath of [
".github/workflows/auto-response.yml",
".github/workflows/clawsweeper-dispatch.yml",
".github/workflows/labeler.yml",
".github/workflows/real-behavior-proof.yml",
]) {
expect(resolveChangedTestTargetPlan([workflowPath])).toEqual({
mode: "targets",
targets: ["test/scripts/ci-workflow-guards.test.ts"],
});
}
});
it("keeps security-sensitive guard workflow edits on guard workflow tests", () => {
expect(
resolveChangedTestTargetPlan([".github/workflows/security-sensitive-guard.yml"]),