Skip to content

Commit c69880d

Browse files
authored
fix: unique step names for checkout GitHub App token minting steps (#24609)
1 parent 021d1a9 commit c69880d

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

pkg/workflow/checkout_step_generator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func (cm *CheckoutManager) GenerateCheckoutAppTokenSteps(c *Compiler, permission
2525
stepID := fmt.Sprintf("checkout-app-token-%d", i)
2626
for _, step := range appSteps {
2727
modified := strings.ReplaceAll(step, "id: safe-outputs-app-token", "id: "+stepID)
28+
// Rename the step to make it unique when multiple checkouts use app auth.
29+
// This prevents duplicate step name errors when more than one checkout entry
30+
// falls back to the top-level github-app (or has its own github-app configured).
31+
modified = strings.ReplaceAll(modified, "name: Generate GitHub App token", fmt.Sprintf("name: Generate GitHub App token for checkout (%d)", i))
2832
steps = append(steps, modified)
2933
}
3034
}

pkg/workflow/duplicate_step_validation_integration_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,97 @@ This workflow tests that duplicate checkout steps are properly deduplicated.
8989

9090
t.Logf("✓ Duplicate step validation working correctly: found %d checkout step(s) in safe_outputs job (deduplicated)", checkoutCount)
9191
}
92+
93+
// TestDuplicateStepValidation_CheckoutPlusGitHubApp_Integration tests that combining
94+
// a top-level github-app with multiple cross-repo checkouts and tools.github does not
95+
// produce duplicate 'Generate GitHub App token' steps in the activation job.
96+
//
97+
// When multiple checkout entries all fall back to the top-level github-app,
98+
// each minting step previously received the same name, triggering the duplicate
99+
// step validation error ("compiler bug: duplicate step 'Generate GitHub App token'").
100+
func TestDuplicateStepValidation_CheckoutPlusGitHubApp_Integration(t *testing.T) {
101+
tmpDir := testutil.TempDir(t, "duplicate-checkout-token-test")
102+
103+
// Workflow that combines all three conditions that triggered the bug:
104+
// 1. Top-level github-app: (used as fallback for all token-minting operations)
105+
// 2. Two cross-repo checkout: entries (both fall back to the top-level github-app)
106+
// 3. tools.github: with mode: remote
107+
mdContent := `---
108+
on:
109+
issues:
110+
types: [opened]
111+
engine: claude
112+
strict: false
113+
permissions:
114+
contents: read
115+
issues: read
116+
pull-requests: read
117+
118+
github-app:
119+
app-id: ${{ secrets.APP_ID }}
120+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
121+
repositories: ["side-repo", "target-repo"]
122+
123+
checkout:
124+
- repository: myorg/target-repo
125+
ref: main
126+
- repository: myorg/side-repo
127+
ref: main
128+
129+
tools:
130+
github:
131+
mode: remote
132+
toolsets: [default]
133+
---
134+
135+
# Test Workflow
136+
137+
This workflow tests that multiple checkouts + top-level github-app + tools.github
138+
compile without duplicate 'Generate GitHub App token' step errors in the activation job.
139+
`
140+
141+
mdFile := filepath.Join(tmpDir, "test-checkout-github-app.md")
142+
err := os.WriteFile(mdFile, []byte(mdContent), 0644)
143+
if err != nil {
144+
t.Fatalf("Failed to create test file: %v", err)
145+
}
146+
147+
// Compile workflow — must succeed so the generated lock file can be validated.
148+
compiler := NewCompiler()
149+
err = compiler.CompileWorkflow(mdFile)
150+
if err != nil {
151+
if strings.Contains(err.Error(), "duplicate step") {
152+
t.Fatalf("Regression: duplicate step error when combining multiple checkouts + top-level github-app: %v", err)
153+
}
154+
t.Fatalf("Compilation failed unexpectedly before lock-file assertions could run: %v", err)
155+
}
156+
157+
// Read the generated lock file and verify the activation job has unique step names
158+
lockFile := stringutil.MarkdownToLockFile(mdFile)
159+
lockContent, err := os.ReadFile(lockFile)
160+
if err != nil {
161+
t.Fatalf("Failed to read lock file: %v", err)
162+
}
163+
lockContentStr := string(lockContent)
164+
165+
// Both checkout token minting steps should be present with unique names.
166+
// The step names are "Generate GitHub App token for checkout (N)" — one per checkout entry.
167+
count0 := strings.Count(lockContentStr, "name: Generate GitHub App token for checkout (0)")
168+
count1 := strings.Count(lockContentStr, "name: Generate GitHub App token for checkout (1)")
169+
if count0 != 1 {
170+
t.Errorf("Expected exactly 1 'Generate GitHub App token for checkout (0)' step, got %d", count0)
171+
}
172+
if count1 != 1 {
173+
t.Errorf("Expected exactly 1 'Generate GitHub App token for checkout (1)' step, got %d", count1)
174+
}
175+
176+
// Exactly one generic "Generate GitHub App token" step is expected — for the GitHub MCP server
177+
// in the agent job (id: github-mcp-app-token). If more than one appears, that means a
178+
// checkout minting step was not renamed, which would cause a duplicate-name error.
179+
genericCount := strings.Count(lockContentStr, "name: Generate GitHub App token\n")
180+
if genericCount > 1 {
181+
t.Errorf("Found %d generic 'Generate GitHub App token' steps; checkout steps must use unique names to avoid duplicates", genericCount)
182+
}
183+
184+
t.Logf("✓ No duplicate token steps: checkout (0) count=%d, checkout (1) count=%d, generic=%d", count0, count1, genericCount)
185+
}

0 commit comments

Comments
 (0)