diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index dc7d4070ef3b..bc014bffece4 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -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 }} diff --git a/.github/workflows/real-behavior-proof.yml b/.github/workflows/real-behavior-proof.yml index 55d6b2a9ea03..7ae8e8935f32 100644 --- a/.github/workflows/real-behavior-proof.yml +++ b/.github/workflows/real-behavior-proof.yml @@ -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 diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index d0d29486db82..3f06af790167 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -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"], diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 2707c491a44d..ef18dda9fdbf 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -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; diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 64d20df201e3..de67dbd95a3b 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -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"]),