Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions pkg/workflow/compiler_safe_outputs_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 != "" {
Expand Down
88 changes: 88 additions & 0 deletions pkg/workflow/compiler_safe_outputs_steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
},
},
Comment thread
dsyme marked this conversation as resolved.
{
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 {
Expand Down
Loading