diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index bddef017d9d..aed1508f14c 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -47,10 +47,23 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { consolidatedSafeOutputsStepsLog.Print("Building shared PR checkout steps") var steps []string fetchDepth := 1 - - if defaultCheckout := NewCheckoutManager(data.CheckoutConfigs).GetDefaultCheckoutOverride(); defaultCheckout != nil && defaultCheckout.fetchDepth != nil { - fetchDepth = *defaultCheckout.fetchDepth - consolidatedSafeOutputsStepsLog.Printf("Using custom checkout fetch-depth for safe_outputs: %d", fetchDepth) + var sparsePatterns []string + + // Build a single CheckoutManager so we can query both the default and cross-repo entries. + checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) + + // Same-repo fallback: use the default (workspace-root) checkout's fetch-depth and + // sparse-checkout patterns. Both are overridden below for cross-repo targets once + // targetRepoSlug is known. + if defaultCheckout := checkoutMgr.GetDefaultCheckoutOverride(); defaultCheckout != nil { + if defaultCheckout.fetchDepth != nil { + fetchDepth = *defaultCheckout.fetchDepth + consolidatedSafeOutputsStepsLog.Printf("Using custom checkout fetch-depth for safe_outputs: %d", fetchDepth) + } + if len(defaultCheckout.sparsePatterns) > 0 { + sparsePatterns = defaultCheckout.sparsePatterns + consolidatedSafeOutputsStepsLog.Printf("Using %d sparse-checkout pattern(s) from default checkout for safe_outputs", len(sparsePatterns)) + } } // Determine which token to use for checkout @@ -90,6 +103,24 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { consolidatedSafeOutputsStepsLog.Printf("Using trialLogicalRepoSlug: %s", targetRepoSlug) } + // For cross-repo targets, override fetch-depth and sparse-checkout patterns + // from the checkout: config entry that targets the same repository. The agent + // job already uses these values; the safe_outputs job must mirror them so that + // (a) large repos are not checked out in full unnecessarily and (b) the working + // tree is consistent with what the agent operated on. + if targetRepoSlug != "" { + if targetEntry := checkoutMgr.GetCheckoutForRepository(targetRepoSlug); targetEntry != nil { + if targetEntry.fetchDepth != nil { + fetchDepth = *targetEntry.fetchDepth + consolidatedSafeOutputsStepsLog.Printf("Using checkout fetch-depth for cross-repo target %s: %d", targetRepoSlug, fetchDepth) + } + if len(targetEntry.sparsePatterns) > 0 { + sparsePatterns = targetEntry.sparsePatterns + consolidatedSafeOutputsStepsLog.Printf("Using %d sparse-checkout pattern(s) for cross-repo target %s", len(sparsePatterns), targetRepoSlug) + } + } + } + // Determine the ref (branch) to checkout // Priority: create-pull-request base-branch > extracted base-branch from agent output > fallback expression // This is critical: we must checkout the base branch, not github.sha (the triggering commit), @@ -149,6 +180,12 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { steps = append(steps, fmt.Sprintf(" token: %s\n", checkoutToken)) steps = append(steps, " persist-credentials: false\n") steps = append(steps, fmt.Sprintf(" fetch-depth: %d\n", fetchDepth)) + if len(sparsePatterns) > 0 { + steps = append(steps, " sparse-checkout: |\n") + for _, pattern := range sparsePatterns { + steps = append(steps, fmt.Sprintf(" %s\n", strings.TrimSpace(pattern))) + } + } } // Step 1b: Checkout repository with conditional execution @@ -172,6 +209,12 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { steps = append(steps, fmt.Sprintf(" token: %s\n", checkoutToken)) steps = append(steps, " persist-credentials: false\n") steps = append(steps, fmt.Sprintf(" fetch-depth: %d\n", fetchDepth)) + if len(sparsePatterns) > 0 { + steps = append(steps, " sparse-checkout: |\n") + for _, pattern := range sparsePatterns { + steps = append(steps, fmt.Sprintf(" %s\n", strings.TrimSpace(pattern))) + } + } // Step 2: Configure Git credentials with conditional execution // Security: Pass GitHub token through environment variable to prevent template injection @@ -207,7 +250,6 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { // Without this, applyBundleToBranch must fall back to per-SHA git fetch (prerequisite // recovery), which requires uploadpack.allowReachableSHA1InWant on the server. if targetRepoSlug != "" { - checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) if matchedEntry := checkoutMgr.GetCheckoutForRepository(targetRepoSlug); matchedEntry != nil && len(matchedEntry.fetchRefs) > 0 { consolidatedSafeOutputsStepsLog.Printf("Adding fetch refs step for cross-repo target %s (%d refs)", targetRepoSlug, len(matchedEntry.fetchRefs)) if fetchStep := buildSafeOutputsFetchRefsStep(targetRepoSlug, checkoutToken, matchedEntry.fetchRefs, RenderCondition(condition)); fetchStep != "" { diff --git a/pkg/workflow/compiler_safe_outputs_steps_test.go b/pkg/workflow/compiler_safe_outputs_steps_test.go index c5d9f8bfbc6..e20e8b61f35 100644 --- a/pkg/workflow/compiler_safe_outputs_steps_test.go +++ b/pkg/workflow/compiler_safe_outputs_steps_test.go @@ -406,6 +406,94 @@ func TestBuildSharedPRCheckoutSteps(t *testing.T) { "contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')", }, }, + { + name: "cross-repo with sparse-checkout patterns propagates them to safe_outputs checkout", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + GitHubToken: "${{ secrets.CROSS_PAT }}", + }, + TargetRepoSlug: "org/monorepo", + BaseBranch: "main", + }, + }, + checkoutConfigs: []*CheckoutConfig{ + { + Repository: "org/monorepo", + SparseCheckout: ".github\nscripts\ntest", + }, + }, + checkContains: []string{ + "sparse-checkout: |", + " .github", + " scripts", + " test", + }, + }, + { + name: "cross-repo without sparse-checkout does not emit sparse-checkout block", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + TargetRepoSlug: "org/full-repo", + }, + }, + checkoutConfigs: []*CheckoutConfig{ + { + Repository: "org/full-repo", + }, + }, + checkNotContains: []string{ + "sparse-checkout:", + }, + }, + { + name: "cross-repo fetch-depth is read from checkout config for target repo, not default override", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + TargetRepoSlug: "org/target", + }, + }, + checkoutConfigs: []*CheckoutConfig{ + { + // The "default" checkout (empty repo/path) has a different fetch-depth. + // The cross-repo entry's fetch-depth should win for the safe_outputs checkout. + FetchDepth: func() *int { d := 0; return &d }(), + }, + { + Repository: "org/target", + FetchDepth: func() *int { d := 10; return &d }(), + }, + }, + checkContains: []string{ + "fetch-depth: 10", + }, + checkNotContains: []string{ + "fetch-depth: 0", + }, + }, + { + name: "same-repo sparse-checkout from default checkout override propagates to safe_outputs", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + GitHubToken: "${{ secrets.GITHUB_TOKEN }}", + }, + // No TargetRepoSlug: same-repo operation + }, + }, + checkoutConfigs: []*CheckoutConfig{ + { + // Default workspace-root checkout with sparse patterns + SparseCheckout: ".github\napp\nlib", + }, + }, + checkContains: []string{ + "sparse-checkout: |", + " .github", + " app", + " lib", + }, + }, } for _, tt := range tests {