diff --git a/.changeset/patch-fix-cross-repo-checkout-ref.md b/.changeset/patch-fix-cross-repo-checkout-ref.md new file mode 100644 index 0000000000..98462f60f6 --- /dev/null +++ b/.changeset/patch-fix-cross-repo-checkout-ref.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fixed `create-pull-request` with `target-repo`: the `safe_outputs` checkout step now uses a cross-repo-safe ref expression (omitting `github.ref_name`) to avoid failures when the triggering branch does not exist in the target repository. diff --git a/.github/workflows/smoke-create-cross-repo-pr.lock.yml b/.github/workflows/smoke-create-cross-repo-pr.lock.yml index 2fed407c03..b3af8e0654 100644 --- a/.github/workflows/smoke-create-cross-repo-pr.lock.yml +++ b/.github/workflows/smoke-create-cross-repo-pr.lock.yml @@ -1455,7 +1455,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: github/gh-aw-side-repo - ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.event.repository.default_branch }} token: ${{ secrets.GH_AW_SIDE_REPO_PAT }} persist-credentials: false fetch-depth: 1 diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index f219bf0d65..d6fc874c55 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -72,10 +72,18 @@ func (c *Compiler) buildSharedPRCheckoutSteps(data *WorkflowData) []string { // targeting.  Then a lot of this gnarly event code will be only on the "front end" (prepping the // coding agent) not the "backend" (applying the safe outputs) const baseBranchFallbackExpr = "${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}" + // Cross-repo fallback omits github.ref_name because it refers to the branch in the triggering repository, + // which may not exist in the target repository (e.g., when triggered via workflow_dispatch from a feature branch). + const crossRepoFallbackExpr = "${{ github.base_ref || github.event.pull_request.base.ref || github.event.repository.default_branch }}" var checkoutRef string if data.SafeOutputs.CreatePullRequests != nil && data.SafeOutputs.CreatePullRequests.BaseBranch != "" { checkoutRef = data.SafeOutputs.CreatePullRequests.BaseBranch consolidatedSafeOutputsStepsLog.Printf("Using custom base-branch from create-pull-request for checkout ref: %s", checkoutRef) + } else if targetRepoSlug != "" { + // Cross-repo checkout: avoid github.ref_name which refers to the triggering branch, + // not a branch in the target repository. + checkoutRef = crossRepoFallbackExpr + consolidatedSafeOutputsStepsLog.Printf("Using cross-repo fallback base branch expression for checkout ref (no github.ref_name)") } else { checkoutRef = baseBranchFallbackExpr consolidatedSafeOutputsStepsLog.Printf("Using fallback base branch expression for checkout ref") diff --git a/pkg/workflow/compiler_safe_outputs_steps_test.go b/pkg/workflow/compiler_safe_outputs_steps_test.go index 9b280aecbc..ac03a2e58b 100644 --- a/pkg/workflow/compiler_safe_outputs_steps_test.go +++ b/pkg/workflow/compiler_safe_outputs_steps_test.go @@ -13,11 +13,12 @@ import ( // TestBuildSharedPRCheckoutSteps tests shared PR checkout step generation func TestBuildSharedPRCheckoutSteps(t *testing.T) { tests := []struct { - name string - safeOutputs *SafeOutputsConfig - trialMode bool - trialRepo string - checkContains []string + name string + safeOutputs *SafeOutputsConfig + trialMode bool + trialRepo string + checkContains []string + checkNotContains []string }{ { name: "create pull request only", @@ -120,6 +121,46 @@ func TestBuildSharedPRCheckoutSteps(t *testing.T) { "token: ${{ secrets.GH_AW_CROSS_REPO_PAT }}", "GIT_TOKEN: ${{ secrets.GH_AW_CROSS_REPO_PAT }}", `REPO_NAME: "org/target-repo"`, + // Cross-repo checkout must not use github.ref_name + "ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.event.repository.default_branch }}", + }, + }, + { + name: "cross-repo without base-branch uses safe ref omitting github.ref_name", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + TargetRepoSlug: "org/other-repo", + }, + }, + checkContains: []string{ + "ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.event.repository.default_branch }}", + }, + checkNotContains: []string{ + "github.ref_name", + }, + }, + { + name: "trial mode cross-repo omits github.ref_name from checkout ref", + trialMode: true, + trialRepo: "org/trial-repo", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{}, + }, + checkContains: []string{ + "repository: org/trial-repo", + "ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.event.repository.default_branch }}", + }, + }, + { + name: "cross-repo with explicit base-branch uses base-branch not cross-repo fallback", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + TargetRepoSlug: "org/other-repo", + BaseBranch: "develop", + }, + }, + checkContains: []string{ + "ref: develop", }, }, { @@ -212,6 +253,10 @@ func TestBuildSharedPRCheckoutSteps(t *testing.T) { for _, expected := range tt.checkContains { assert.Contains(t, stepsContent, expected, "Expected to find: "+expected) } + + for _, notExpected := range tt.checkNotContains { + assert.NotContains(t, stepsContent, notExpected, "Expected NOT to find: "+notExpected) + } }) } }