From b58d4c729c4ba923c062534968454bdf261fe39e Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 29 May 2026 00:18:33 +0100 Subject: [PATCH 1/2] fix: propagate sparse-checkout patterns and fetch-depth to safe_outputs checkout When a cross-repo checkout is configured with sparse-checkout patterns, the safe_outputs job's checkout step was always doing a full checkout of the target repo, ignoring the sparse patterns. Similarly, the fetch-depth for the safe_outputs checkout was read from the default (workspace-root) checkout override instead of the target repo's checkout config entry. This meant workflows with large cross-repo checkouts (e.g. github/github with ~1.4 GB of excluded assets) would download the full repo in safe_outputs even though the agent job used a sparse checkout. Fixes: - Read sparsePatterns from the target repo's checkout entry and emit sparse-checkout in both safe_outputs checkout steps (step 1a trusted branch and step 1b main checkout) - Read fetchDepth from the target repo's checkout entry for cross-repo targets rather than from GetDefaultCheckoutOverride() - Eliminate duplicate NewCheckoutManager() call in step 3 (reuse the instance created at the top of the function) Tests: 3 new table-driven test cases added to TestBuildSharedPRCheckoutSteps: - cross-repo with sparse-checkout patterns propagates them to safe_outputs - cross-repo without sparse-checkout does not emit sparse-checkout block - cross-repo fetch-depth is read from checkout config for target repo --- pkg/workflow/compiler_safe_outputs_steps.go | 39 ++++++++++- .../compiler_safe_outputs_steps_test.go | 65 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index bddef017d9d..9db7cb0e60e 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -47,8 +47,14 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { consolidatedSafeOutputsStepsLog.Print("Building shared PR checkout steps") var steps []string fetchDepth := 1 + var sparsePatterns []string - if defaultCheckout := NewCheckoutManager(data.CheckoutConfigs).GetDefaultCheckoutOverride(); defaultCheckout != nil && defaultCheckout.fetchDepth != nil { + // 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. + // Overridden below for cross-repo targets once targetRepoSlug is known. + if defaultCheckout := checkoutMgr.GetDefaultCheckoutOverride(); defaultCheckout != nil && defaultCheckout.fetchDepth != nil { fetchDepth = *defaultCheckout.fetchDepth consolidatedSafeOutputsStepsLog.Printf("Using custom checkout fetch-depth for safe_outputs: %d", fetchDepth) } @@ -90,6 +96,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 +173,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 +202,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 +243,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..ab203495726 100644 --- a/pkg/workflow/compiler_safe_outputs_steps_test.go +++ b/pkg/workflow/compiler_safe_outputs_steps_test.go @@ -406,6 +406,71 @@ 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", + }, + }, } for _, tt := range tests { From afc9d4a340195e4eebd839df52521ce3649d47b7 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 29 May 2026 00:27:04 +0100 Subject: [PATCH 2/2] fix: also propagate sparse-checkout from default checkout override for same-repo safe_outputs Address Copilot review: sparsePatterns was only read from the cross-repo checkout entry (when targetRepoSlug != ""). Same-repo safe_outputs runs using the default/workspace-root checkout override now also get their sparse-checkout patterns propagated. Add test case: same-repo sparse-checkout from default checkout override propagates to safe_outputs. --- pkg/workflow/compiler_safe_outputs_steps.go | 17 ++++++++++---- .../compiler_safe_outputs_steps_test.go | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index 9db7cb0e60e..aed1508f14c 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -52,11 +52,18 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []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. - // Overridden below for cross-repo targets once targetRepoSlug is known. - if defaultCheckout := checkoutMgr.GetDefaultCheckoutOverride(); defaultCheckout != nil && defaultCheckout.fetchDepth != nil { - fetchDepth = *defaultCheckout.fetchDepth - consolidatedSafeOutputsStepsLog.Printf("Using custom checkout fetch-depth for safe_outputs: %d", fetchDepth) + // 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 diff --git a/pkg/workflow/compiler_safe_outputs_steps_test.go b/pkg/workflow/compiler_safe_outputs_steps_test.go index ab203495726..e20e8b61f35 100644 --- a/pkg/workflow/compiler_safe_outputs_steps_test.go +++ b/pkg/workflow/compiler_safe_outputs_steps_test.go @@ -471,6 +471,29 @@ func TestBuildSharedPRCheckoutSteps(t *testing.T) { "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 {