diff --git a/.github/workflows/ci_dependencies.yml b/.github/workflows/ci_dependencies.yml index 82b2822..7eeaadc 100644 --- a/.github/workflows/ci_dependencies.yml +++ b/.github/workflows/ci_dependencies.yml @@ -8,7 +8,7 @@ name: Dependencies on: push: branches: [main] - pull_request_target: + pull_request: branches: [main] permissions: @@ -44,7 +44,7 @@ jobs: name: Dependabot Comment if: >- always() - && github.event_name == 'pull_request_target' + && github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' runs-on: ubuntu-latest needs: [call_deps_reviewer, call_dependabot_reviewer] @@ -152,7 +152,7 @@ jobs: # The job-level literal must match MIN_RELEASE_AGE_HOURS; env.* is unavailable here. if: >- always() - && github.event_name == 'pull_request_target' + && github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && needs.call_deps_reviewer.result == 'success' && needs.call_dependabot_reviewer.result == 'success' @@ -205,7 +205,7 @@ jobs: const validDependency = dependency !== 'unavailable'; const validVersion = version !== 'unavailable'; - const staticSignalsAreSafe = eventName === 'pull_request_target' + const staticSignalsAreSafe = eventName === 'pull_request' && author === 'dependabot[bot]' && /^[0-9a-f]{40}$/.test(expectedHeadSHA) && depsReviewResult === 'success' diff --git a/internal/workflowtest/ci_dependencies_test.go b/internal/workflowtest/ci_dependencies_test.go index 79cd3ed..e0f39b1 100644 --- a/internal/workflowtest/ci_dependencies_test.go +++ b/internal/workflowtest/ci_dependencies_test.go @@ -92,7 +92,10 @@ func TestCIDependenciesWorkflow_StructureEnforcesGuardedApproval(t *testing.T) { assertMatches(t, workflow, `(?m)^# .+\n# --\n# .+`, "purpose header") assertMatches(t, workflow, `(?ms)^on:\s*\n\s+push:\s*\n\s+branches:\s*\[main\]`, "main push trigger") - assertMatches(t, workflow, `(?ms)^on:.*?\n\s+pull_request_target:\s*\n\s+branches:\s*\[main\]`, "trusted main pull-request trigger") + assertMatches(t, workflow, `(?ms)^on:.*?\n\s+pull_request:\s*\n\s+branches:\s*\[main\]`, "main pull-request trigger") + if strings.Contains(workflow, "pull_request_target") { + t.Fatal("workflow must not use the insecure pull_request_target event") + } assertMatches(t, workflow, `(?ms)^permissions:\s*\n\s+contents:\s+read\s*\n\s+issues:\s+none\s*\n\s+pull-requests:\s+none`, "read-only workflow permissions") assertMatches(t, workflow, `(?ms)^concurrency:\s*\n\s+group:\s+.*github\.workflow.*github\.event\.pull_request\.number.*github\.ref.*\n\s+cancel-in-progress:\s+true`, "PR- or ref-scoped concurrency") @@ -118,7 +121,7 @@ func TestCIDependenciesWorkflow_StructureEnforcesGuardedApproval(t *testing.T) { assertFullSHAPins(t, workflow) assertContains(t, commentJob, "always()", "failure-tolerant reporting condition") - assertMatches(t, commentJob, `(?m)^\s+&& github\.event_name == 'pull_request_target'$`, "trusted pull-request reporting guard") + assertMatches(t, commentJob, `(?m)^\s+&& github\.event_name == 'pull_request'$`, "pull-request reporting guard") assertMatches(t, commentJob, `(?m)^\s+&& github\.event\.pull_request\.user\.login == 'dependabot\[bot\]'$`, "Dependabot-only reporting guard") assertContains(t, commentJob, "needs.call_deps_reviewer.result", "general review result wiring") for _, output := range []string{"risk_level", "dep_name", "dep_version", "release_age_hours"} { @@ -147,7 +150,7 @@ func TestCIDependenciesWorkflow_StructureEnforcesGuardedApproval(t *testing.T) { t.Fatal("review report must describe eligibility without claiming approval") } - assertMatches(t, approvalJob, `(?m)^\s+&& github\.event_name == 'pull_request_target'$`, "trusted pull-request approval guard") + assertMatches(t, approvalJob, `(?m)^\s+&& github\.event_name == 'pull_request'$`, "pull-request approval guard") assertMatches(t, approvalJob, `(?m)^\s+&& github\.event\.pull_request\.user\.login == 'dependabot\[bot\]'$`, "Dependabot-only approval guard") assertContains(t, approvalJob, "needs.call_deps_reviewer.result", "approval review result predicate") assertContains(t, approvalJob, "needs.call_dependabot_reviewer.outputs.risk", "approval risk predicate") @@ -285,8 +288,8 @@ func TestCIDependenciesWorkflow_ApprovalScriptEvaluatesPolicyFixtures(t *testing risk: "low", releaseAge: "48", }, { - name: "untrusted pull request event", - eventName: "pull_request", author: dependabotAuthor, depsReviewResult: "success", + name: "untrusted pull request target event", + eventName: "pull_request_target", author: dependabotAuthor, depsReviewResult: "success", dependabotReviewResult: "success", risk: "low", releaseAge: "48", }, { @@ -816,7 +819,7 @@ func requireNode(t *testing.T) string { func policyEnvironment(fixture policyFixture) []string { eventName := fixture.eventName if eventName == "" { - eventName = "pull_request_target" + eventName = "pull_request" } reviewConclusion := fixture.reviewConclusion if reviewConclusion == "" && !fixture.omitReviewConclusion { diff --git a/openspec/changes/automate-dependency-update/design.md b/openspec/changes/automate-dependency-update/design.md index dcdd760..3d968c0 100644 --- a/openspec/changes/automate-dependency-update/design.md +++ b/openspec/changes/automate-dependency-update/design.md @@ -46,7 +46,7 @@ The implementation will follow the conservative `unbound-force/unbound-force` be ### 2. Match repository event and concurrency conventions -The workflow will run on pushes to `main` and use `pull_request_target` for pull requests targeting `main`. The trusted event runs the default-branch workflow definition so a dependency update cannot modify the actions that receive write permission; no job checks out or executes pull-request code. A workflow/ref concurrency group with `cancel-in-progress: true` will prevent redundant runs. Pull-request mutation jobs will have an explicit Dependabot actor condition, so push events and human-authored pull requests can run analysis without receiving Dependabot comments or approvals. +The workflow will run on pushes to `main` and use `pull_request` for pull requests targeting `main`, matching the proven ComplyTime reference workflow without introducing the privileged `pull_request_target` event. Pull-request mutation jobs will have an explicit Dependabot actor condition and will not check out or execute pull-request code while holding write permission. Review jobs remain read-only, so push events and human-authored pull requests can run analysis without receiving Dependabot comments or approvals. A pull-request/ref concurrency group with `cancel-in-progress: true` will prevent redundant runs. ### 3. Use full-SHA pins and job-scoped least privilege