Skip to content

Commit e449f3f

Browse files
Copilotpelikhan
andcommitted
Propagate target_repo to downstream jobs via CheckoutManager
Per review feedback: add crossRepoTargetRepo to CheckoutManager so the platform (host) repo is accessible wherever checkout is needed. - Add crossRepoTargetRepo field + SetCrossRepoTargetRepo/GetCrossRepoTargetRepo to CheckoutManager - Expose target_repo as activation job output (needs.activation.outputs.target_repo) so agent/safe_outputs jobs can reference it - Use SetCrossRepoTargetRepo in generateCheckoutGitHubFolderForActivation instead of passing raw string - Set crossRepoTargetRepo on agent job's CheckoutManager using needs.activation.outputs.target_repo - Add TestCrossRepoTargetRepo and TestActivationJobTargetRepoOutput tests - Recompile 166 lock files + update golden test files Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 0ad9c33 commit e449f3f

File tree

6 files changed

+145
-1
lines changed

6 files changed

+145
-1
lines changed

.github/workflows/smoke-workflow-call.lock.yml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/workflow/checkout_manager.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ type CheckoutManager struct {
131131
ordered []*resolvedCheckout
132132
// index maps checkoutKey to the position in ordered
133133
index map[checkoutKey]int
134+
// crossRepoTargetRepo holds the platform (host) repository to use when performing
135+
// .github/.agents sparse checkout steps for cross-repo workflow_call invocations.
136+
//
137+
// In the activation job this is set to "${{ steps.resolve-host-repo.outputs.target_repo }}".
138+
// In the agent and safe_outputs jobs it is set to "${{ needs.activation.outputs.target_repo }}".
139+
// An empty string means the checkout targets the current repository (github.repository).
140+
crossRepoTargetRepo string
134141
}
135142

136143
// NewCheckoutManager creates a new CheckoutManager pre-loaded with user-supplied
@@ -146,6 +153,24 @@ func NewCheckoutManager(userCheckouts []*CheckoutConfig) *CheckoutManager {
146153
return cm
147154
}
148155

156+
// SetCrossRepoTargetRepo stores the platform (host) repository expression used for
157+
// .github/.agents sparse checkout steps. Call this when the workflow has a workflow_call
158+
// trigger and the checkout should target the platform repo rather than github.repository.
159+
//
160+
// In the activation job pass "${{ steps.resolve-host-repo.outputs.target_repo }}".
161+
// In downstream jobs (agent, safe_outputs) pass "${{ needs.activation.outputs.target_repo }}".
162+
func (cm *CheckoutManager) SetCrossRepoTargetRepo(repo string) {
163+
checkoutManagerLog.Printf("Setting cross-repo target: %q", repo)
164+
cm.crossRepoTargetRepo = repo
165+
}
166+
167+
// GetCrossRepoTargetRepo returns the platform repo expression previously set by
168+
// SetCrossRepoTargetRepo, or an empty string if no cross-repo target was set
169+
// (same-repo invocation or inlined imports).
170+
func (cm *CheckoutManager) GetCrossRepoTargetRepo() string {
171+
return cm.crossRepoTargetRepo
172+
}
173+
149174
// add processes a single CheckoutConfig and either creates a new entry or merges
150175
// it into an existing entry with the same key.
151176
func (cm *CheckoutManager) add(cfg *CheckoutConfig) {

pkg/workflow/checkout_manager_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,3 +910,34 @@ func TestAdditionalCheckoutWithAppAuth(t *testing.T) {
910910
assert.Contains(t, combined, "other/repo", "should reference the additional repo")
911911
})
912912
}
913+
914+
// TestCrossRepoTargetRepo verifies the SetCrossRepoTargetRepo/GetCrossRepoTargetRepo lifecycle.
915+
func TestCrossRepoTargetRepo(t *testing.T) {
916+
t.Run("default is empty string (same-repo)", func(t *testing.T) {
917+
cm := NewCheckoutManager(nil)
918+
assert.Equal(t, "", cm.GetCrossRepoTargetRepo(), "new checkout manager should have no cross-repo target")
919+
})
920+
921+
t.Run("activation job expression is stored and retrievable", func(t *testing.T) {
922+
cm := NewCheckoutManager(nil)
923+
cm.SetCrossRepoTargetRepo("${{ steps.resolve-host-repo.outputs.target_repo }}")
924+
assert.Equal(t, "${{ steps.resolve-host-repo.outputs.target_repo }}", cm.GetCrossRepoTargetRepo())
925+
})
926+
927+
t.Run("downstream job expression (needs.activation.outputs) is stored and retrievable", func(t *testing.T) {
928+
cm := NewCheckoutManager(nil)
929+
cm.SetCrossRepoTargetRepo("${{ needs.activation.outputs.target_repo }}")
930+
assert.Equal(t, "${{ needs.activation.outputs.target_repo }}", cm.GetCrossRepoTargetRepo())
931+
})
932+
933+
t.Run("GenerateGitHubFolderCheckoutStep uses stored value", func(t *testing.T) {
934+
cm := NewCheckoutManager(nil)
935+
cm.SetCrossRepoTargetRepo("${{ needs.activation.outputs.target_repo }}")
936+
937+
lines := cm.GenerateGitHubFolderCheckoutStep(cm.GetCrossRepoTargetRepo(), GetActionPin)
938+
combined := strings.Join(lines, "")
939+
940+
assert.Contains(t, combined, "repository: ${{ needs.activation.outputs.target_repo }}",
941+
"checkout step should use the cross-repo target")
942+
})
943+
}

pkg/workflow/compiler_activation_job.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate
5959
// Expose the model output from the activation job so downstream jobs can reference it
6060
outputs["model"] = "${{ steps.generate_aw_info.outputs.model }}"
6161

62+
// Expose the resolved platform (host) repository so agent and safe_outputs jobs can use
63+
// needs.activation.outputs.target_repo for any checkout that must target the platform repo
64+
// rather than github.repository (the caller's repo in cross-repo workflow_call scenarios).
65+
if hasWorkflowCallTrigger(data.On) && !data.InlinedImports {
66+
outputs["target_repo"] = "${{ steps.resolve-host-repo.outputs.target_repo }}"
67+
}
68+
6269
// Add secret validation step before context variable validation.
6370
// This validates that the required engine secrets are available before any other checks.
6471
secretValidationStep := engine.GetSecretValidationStep(data)
@@ -506,8 +513,9 @@ func (c *Compiler) generateCheckoutGitHubFolderForActivation(data *WorkflowData)
506513
cm := NewCheckoutManager(nil)
507514
if data != nil && hasWorkflowCallTrigger(data.On) && !data.InlinedImports {
508515
compilerActivationJobLog.Print("Adding cross-repo-aware .github checkout for workflow_call trigger")
516+
cm.SetCrossRepoTargetRepo("${{ steps.resolve-host-repo.outputs.target_repo }}")
509517
return cm.GenerateGitHubFolderCheckoutStep(
510-
"${{ steps.resolve-host-repo.outputs.target_repo }}",
518+
cm.GetCrossRepoTargetRepo(),
511519
GetActionPin,
512520
)
513521
}

pkg/workflow/compiler_activation_job_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,74 @@ func TestCheckoutDoesNotUseEventNameExpression(t *testing.T) {
253253
assert.NotContains(t, combined, "github.action_repository",
254254
"checkout must not use github.action_repository")
255255
}
256+
257+
// TestActivationJobTargetRepoOutput verifies that the activation job exposes target_repo as an
258+
// output when a workflow_call trigger is present (without inlined imports), so that agent and
259+
// safe_outputs jobs can reference needs.activation.outputs.target_repo.
260+
func TestActivationJobTargetRepoOutput(t *testing.T) {
261+
tests := []struct {
262+
name string
263+
onSection string
264+
inlinedImports bool
265+
expectTargetRepo bool
266+
}{
267+
{
268+
name: "workflow_call trigger - target_repo output added",
269+
onSection: `"on":
270+
workflow_call:`,
271+
expectTargetRepo: true,
272+
},
273+
{
274+
name: "mixed triggers with workflow_call - target_repo output added",
275+
onSection: `"on":
276+
issue_comment:
277+
types: [created]
278+
workflow_call:`,
279+
expectTargetRepo: true,
280+
},
281+
{
282+
name: "workflow_call with inlined-imports - no target_repo output",
283+
onSection: `"on":
284+
workflow_call:`,
285+
inlinedImports: true,
286+
expectTargetRepo: false,
287+
},
288+
{
289+
name: "no workflow_call - no target_repo output",
290+
onSection: `"on":
291+
issues:
292+
types: [opened]`,
293+
expectTargetRepo: false,
294+
},
295+
}
296+
297+
for _, tt := range tests {
298+
t.Run(tt.name, func(t *testing.T) {
299+
compiler := NewCompilerWithVersion("dev")
300+
compiler.SetActionMode(ActionModeDev)
301+
302+
data := &WorkflowData{
303+
Name: "test-workflow",
304+
On: tt.onSection,
305+
InlinedImports: tt.inlinedImports,
306+
AI: "copilot",
307+
}
308+
309+
job, err := compiler.buildActivationJob(data, false, "", "test.lock.yml")
310+
require.NoError(t, err, "buildActivationJob should succeed")
311+
require.NotNil(t, job, "activation job should not be nil")
312+
313+
if tt.expectTargetRepo {
314+
assert.Contains(t, job.Outputs, "target_repo",
315+
"activation job should expose target_repo output for downstream jobs")
316+
assert.Equal(t,
317+
"${{ steps.resolve-host-repo.outputs.target_repo }}",
318+
job.Outputs["target_repo"],
319+
"target_repo output should reference resolve-host-repo step")
320+
} else {
321+
assert.NotContains(t, job.Outputs, "target_repo",
322+
"activation job should not expose target_repo when workflow_call is absent or inlined-imports enabled")
323+
}
324+
})
325+
}
326+
}

pkg/workflow/compiler_yaml_main_job.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
2020
// Build a CheckoutManager with any user-configured checkouts
2121
checkoutMgr := NewCheckoutManager(data.CheckoutConfigs)
2222

23+
// Propagate the platform (host) repo resolved by the activation job so that
24+
// checkout steps in this job and in safe_outputs can use the correct repository
25+
// for .github/.agents sparse checkouts when called cross-repo.
26+
// The activation job exposes this as needs.activation.outputs.target_repo.
27+
if hasWorkflowCallTrigger(data.On) && !data.InlinedImports {
28+
checkoutMgr.SetCrossRepoTargetRepo("${{ needs.activation.outputs.target_repo }}")
29+
}
30+
2331
// Generate GitHub App token minting steps for checkouts with app auth
2432
// These must be emitted BEFORE the checkout steps that reference them
2533
if checkoutMgr.HasAppAuth() {

0 commit comments

Comments
 (0)