-
Notifications
You must be signed in to change notification settings - Fork 476
Disable checkout by default for pull_request_target workflows #46771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
82df40a
36427c9
ceaaf87
f534297
37bc639
0501f95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,3 +224,121 @@ Test workflow with pull_request triggers. | |
| } | ||
| } | ||
| } | ||
|
|
||
| // TestPullRequestTargetCheckoutDisabledByDefault verifies that pull_request_target workflows | ||
| // do not generate a "Checkout PR branch" step when checkout is not explicitly configured. | ||
| // This prevents the step from hard-failing when head branches are deleted (merged PRs) or | ||
| // inaccessible (fork PRs). | ||
| func TestPullRequestTargetCheckoutDisabledByDefault(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| workflowContent string | ||
| expectPRCheckout bool | ||
| description string | ||
| }{ | ||
| { | ||
| name: "pull_request_target with no checkout key - no PR checkout step", | ||
| workflowContent: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [closed] | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| engine: claude | ||
| strict: false | ||
| --- | ||
|
|
||
| # Thank you note workflow | ||
| Workflow triggered when a PR is closed. | ||
| `, | ||
| expectPRCheckout: false, | ||
| description: "pull_request_target without explicit checkout key should not generate 'Checkout PR branch' step", | ||
| }, | ||
| { | ||
| name: "pull_request_target with checkout: false - no PR checkout step", | ||
| workflowContent: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [closed] | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| engine: claude | ||
| strict: false | ||
| checkout: false | ||
| --- | ||
|
|
||
| # Thank you note workflow | ||
| Workflow triggered when a PR is closed. | ||
| `, | ||
| expectPRCheckout: false, | ||
| description: "pull_request_target with explicit checkout: false should not generate 'Checkout PR branch' step", | ||
| }, | ||
| { | ||
| name: "pull_request_target with trusted checkout mapping - PR checkout step present", | ||
| workflowContent: `--- | ||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| engine: claude | ||
| checkout: | ||
| repository: ${{ github.repository }} | ||
| ref: ${{ github.event.pull_request.base.sha }} | ||
| --- | ||
|
|
||
| # PR review workflow | ||
| Workflow triggered when a PR is opened with a trusted base checkout. | ||
| `, | ||
| expectPRCheckout: true, | ||
| description: "pull_request_target with explicit checkout mapping should generate 'Checkout PR branch' step", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The test asserts 💡 Suggested assertionAfter the existing hasPRCheckout check, add: if hasPRCheckout {
for _, insecure := range []string{"refs/pull", "head.ref", "head_ref"} {
if strings.Contains(lockStr, insecure) {
t.Errorf("%s: expected trusted base checkout but lock file contains insecure pattern %q", tt.description, insecure)
}
}
}This makes the integration test a genuine security regression guard, not just a presence check. @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in f534297 and 37bc639. The root cause was that |
||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| // Create temporary directory for test | ||
| tempDir, err := os.MkdirTemp("", "prt-checkout-test") | ||
| if err != nil { | ||
| t.Fatalf("Failed to create temp dir: %v", err) | ||
| } | ||
| defer os.RemoveAll(tempDir) | ||
|
|
||
| // Create workflows directory | ||
| workflowsDir := filepath.Join(tempDir, ".github", "workflows") | ||
| if err := os.MkdirAll(workflowsDir, 0755); err != nil { | ||
| t.Fatalf("Failed to create workflows directory: %v", err) | ||
| } | ||
|
|
||
| // Write test workflow file | ||
| workflowPath := filepath.Join(workflowsDir, "test-prt-workflow.md") | ||
| if err := os.WriteFile(workflowPath, []byte(tt.workflowContent), 0644); err != nil { | ||
| t.Fatalf("Failed to write workflow file: %v", err) | ||
| } | ||
|
|
||
| // Compile workflow | ||
| compiler := NewCompiler() | ||
| compiler.SetActionMode(ActionModeDev) | ||
| if err := compiler.CompileWorkflow(workflowPath); err != nil { | ||
| t.Fatalf("Failed to compile workflow: %v", err) | ||
| } | ||
|
|
||
| // Read generated lock file | ||
| lockPath := filepath.Join(workflowsDir, "test-prt-workflow.lock.yml") | ||
| lockContent, err := os.ReadFile(lockPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to read lock file: %v", err) | ||
| } | ||
| lockStr := string(lockContent) | ||
|
|
||
| // Check for PR checkout step | ||
| hasPRCheckout := strings.Contains(lockStr, "Checkout PR branch") | ||
| if hasPRCheckout != tt.expectPRCheckout { | ||
| t.Errorf("%s: expected PR checkout step: %v, got: %v", tt.description, tt.expectPRCheckout, hasPRCheckout) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,23 @@ func (c *Compiler) buildInitialWorkflowData( | |
| } | ||
| } | ||
|
|
||
| // Auto-disable checkout for pull_request_target workflows when not explicitly configured. | ||
| // For pull_request_target events, the head branch is often deleted (closed/merged PRs) | ||
| // or inaccessible (fork PRs), causing the "Checkout PR branch" step to fail. | ||
| // Users who need checkout can explicitly set a checkout configuration in frontmatter. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in commit 36427c9 and expanded further in f534297. |
||
| if len(workflowData.CheckoutConfigs) == 0 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Auto-disable guard fires before imported checkout configs are merged, so a workflow that imports a shared workflow providing checkout configs will have 💡 Suggested fixMove the auto-disable block to after the import-merge block (lines 125–144), or alternatively re-evaluate after merging: // ... merge imported checkout configs first ...
// Auto-disable for pull_request_target only when nothing was configured at all
if len(workflowData.CheckoutConfigs) == 0 && !workflowData.CheckoutDisabled {
if hasPullRequestTarget(result.Frontmatter) {
workflowData.CheckoutDisabled = true
}
}Placing it after the merge ensures that an explicitly imported checkout config prevents auto-disable.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f534297. The auto-disable block was moved to after the import-merge block. Imports are merged at lines 112–129; auto-disable runs at lines 131–152 (after). The guard checks
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] The outer guard 💡 SuggestionAdd an early exit for the already-explicitly-disabled case: if !workflowData.CheckoutDisabled && len(workflowData.CheckoutConfigs) == 0 {
if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet {
...
}
}This makes the intent explicit: only auto-disable when nothing has been configured. @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f534297. The outer guard now reads |
||
| if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet { | ||
| if onVal, ok := result.Frontmatter["on"]; ok { | ||
| if onMap, ok := onVal.(map[string]any); ok { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Suggested fixExtract a helper that handles all three YAML func hasTrigger(onVal any, trigger string) bool {
switch v := onVal.(type) {
case string:
return v == trigger
case []any:
for _, item := range v {
if s, ok := item.(string); ok && s == trigger {
return true
}
}
case map[string]any:
_, ok := v[trigger]
return ok
}
return false
}Then replace the nested map assertion with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f534297. Added
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] The 💡 Suggested fixAfter the existing map-path check, also handle the string form: if onStr, ok := onVal.(string); ok && onStr == "pull_request_target" {
workflowBuilderLog.Print("Auto-disabling checkout for pull_request_target workflow")
workflowData.CheckoutDisabled = true
}And for the list form ( @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f534297. Added |
||
| if _, hasPRT := onMap["pull_request_target"]; hasPRT { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mixed-trigger regression: if a workflow uses both _, hasPR := onMap["pull_request"]
if hasPRT && !hasPR {
workflowData.CheckoutDisabled = true
}@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f534297. The auto-disable block now guards with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multi-trigger workflows have checkout globally disabled — if a workflow uses both 💡 Suggested fixThis is inherently tricky because As a minimal safe approach, skip auto-disable when other triggers coexist: if onMap, ok := onVal.(map[string]any); ok {
_, hasPRT := onMap["pull_request_target"]
onlyPRT := hasPRT && len(onMap) == 1
if onlyPRT {
workflowData.CheckoutDisabled = true
}
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f534297. Auto-disable only fires when |
||
| workflowBuilderLog.Print("Auto-disabling checkout for pull_request_target workflow") | ||
| workflowData.CheckoutDisabled = true | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Merge checkout configs from imported shared workflows. | ||
| // Imported configs are appended after the main workflow's configs so that the main | ||
| // workflow's entries take precedence when CheckoutManager deduplicates by (repository, path). | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in f534297 and 37bc639. The original code only checked
CheckoutDisabled, so an explicitcheckout:mapping on apull_request_targetworkflow still emittedcheckout_pr_branch.cjs. The fix adds anIsPullRequestTargetfield (set inworkflow_builder.gofor allpull_request_target-only workflows) and checks it first inShouldGeneratePRCheckoutStep— so the PR-head checkout step is always suppressed forpull_request_target, regardless of whether an explicitactions/checkoutconfig was provided. The test expectation was corrected toexpectPRCheckout: false.