Skip to content

Commit 74480df

Browse files
authored
Fix assign-to-agent failing with GitHub App tokens — auto-fallback to GH_AW_AGENT_TOKEN (#19796)
1 parent 5ffb364 commit 74480df

File tree

4 files changed

+127
-8
lines changed

4 files changed

+127
-8
lines changed

docs/src/content/docs/reference/assign-to-copilot.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ The required token type and permissions depend on whether you own the repository
9797

9898
### Using a GitHub App
9999

100-
Alternatively, you can use a GitHub App with appropriate permissions instead of a PAT for enhanced security.
100+
:::caution[GitHub App tokens are not supported for Copilot assignment]
101+
The Copilot assignment API only accepts fine-grained PATs. GitHub App installation tokens are rejected with "not available as an assignee" regardless of the permissions granted to the App.
102+
103+
When `github-app:` is configured in `safe-outputs`, `assign-to-agent` will not use the GitHub App installation token. It first looks for an explicit `github-token:` in the `assign-to-agent` config, then for `github-token:` configured at the `safe-outputs` level, and only then falls back to the magic secret chain (`GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN`). Make sure `GH_AW_AGENT_TOKEN` is set as a repository secret with the required PAT permissions, or specify an explicit `github-token:` in your `assign-to-agent` config.
104+
:::
101105

102106
### Using a magic secret
103107

pkg/workflow/compiler.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
218218
return formatCompilerError(markdownPath, "error", "threat detection requires sandbox.agent to be enabled. Threat detection runs inside the agent sandbox (AWF) with fully blocked network. Either enable sandbox.agent or use 'threat-detection: false' to disable the threat-detection configuration in safe-outputs.", errors.New("threat detection requires sandbox.agent"))
219219
}
220220

221+
// Emit warning when assign-to-agent is used with github-app: but no explicit github-token:.
222+
// GitHub App tokens are rejected by the Copilot assignment API — a PAT is required.
223+
// The token fallback chain (GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN) is used automatically.
224+
if workflowData.SafeOutputs != nil &&
225+
workflowData.SafeOutputs.AssignToAgent != nil &&
226+
workflowData.SafeOutputs.GitHubApp != nil &&
227+
workflowData.SafeOutputs.AssignToAgent.GitHubToken == "" {
228+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
229+
"assign-to-agent does not support GitHub App tokens. "+
230+
"The Copilot assignment API requires a fine-grained PAT. "+
231+
"The token fallback chain (GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN) will be used automatically. "+
232+
"Add github-token: to your assign-to-agent config to specify a different token."))
233+
c.IncrementWarningCount()
234+
}
235+
221236
// Emit experimental warning for safe-inputs feature
222237
if IsSafeInputsEnabled(workflowData.SafeInputs, workflowData) {
223238
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Using experimental feature: safe-inputs"))

pkg/workflow/compiler_safe_outputs_job_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,106 @@ func TestJobWithGitHubApp(t *testing.T) {
427427
assert.Contains(t, stepsContent, "Invalidate GitHub App token")
428428
}
429429

430+
// TestAssignToAgentWithGitHubAppUsesAgentToken tests that when github-app: is configured,
431+
// assign-to-agent uses GH_AW_AGENT_TOKEN rather than the App installation token.
432+
// The Copilot assignment API only accepts PATs, not GitHub App tokens.
433+
func TestAssignToAgentWithGitHubAppUsesAgentToken(t *testing.T) {
434+
compiler := NewCompiler()
435+
compiler.jobManager = NewJobManager()
436+
437+
workflowData := &WorkflowData{
438+
Name: "Test Workflow",
439+
SafeOutputs: &SafeOutputsConfig{
440+
GitHubApp: &GitHubAppConfig{
441+
AppID: "12345",
442+
PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}",
443+
},
444+
AssignToAgent: &AssignToAgentConfig{
445+
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")},
446+
},
447+
},
448+
}
449+
450+
job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md")
451+
452+
require.NoError(t, err)
453+
require.NotNil(t, job)
454+
455+
stepsContent := strings.Join(job.Steps, "")
456+
457+
// App token minting step should be present (github-app: is configured)
458+
assert.Contains(t, stepsContent, "Generate GitHub App token", "App token minting step should be present")
459+
460+
// Find the assign_to_agent step section
461+
assignToAgentStart := strings.Index(stepsContent, "id: assign_to_agent")
462+
require.Greater(t, assignToAgentStart, -1, "assign_to_agent step should exist")
463+
464+
// Find the end of the assign_to_agent step (next step starts with " - ")
465+
nextStepOffset := strings.Index(stepsContent[assignToAgentStart:], "\n - ")
466+
var assignToAgentSection string
467+
if nextStepOffset == -1 {
468+
assignToAgentSection = stepsContent[assignToAgentStart:]
469+
} else {
470+
assignToAgentSection = stepsContent[assignToAgentStart : assignToAgentStart+nextStepOffset]
471+
}
472+
473+
// The assign_to_agent step should use GH_AW_AGENT_TOKEN, NOT the App token
474+
assert.Contains(t, assignToAgentSection, "GH_AW_AGENT_TOKEN",
475+
"assign_to_agent step should use GH_AW_AGENT_TOKEN, not the App token")
476+
assert.NotContains(t, assignToAgentSection, "safe-outputs-app-token.outputs.token",
477+
"assign_to_agent step should not use the GitHub App token")
478+
}
479+
480+
// TestAssignToAgentWithGitHubAppAndExplicitToken tests that an explicit github-token
481+
// on assign-to-agent takes precedence over both the App token and GH_AW_AGENT_TOKEN.
482+
func TestAssignToAgentWithGitHubAppAndExplicitToken(t *testing.T) {
483+
compiler := NewCompiler()
484+
compiler.jobManager = NewJobManager()
485+
486+
workflowData := &WorkflowData{
487+
Name: "Test Workflow",
488+
SafeOutputs: &SafeOutputsConfig{
489+
GitHubApp: &GitHubAppConfig{
490+
AppID: "12345",
491+
PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}",
492+
},
493+
AssignToAgent: &AssignToAgentConfig{
494+
BaseSafeOutputConfig: BaseSafeOutputConfig{
495+
Max: strPtr("1"),
496+
GitHubToken: "${{ secrets.MY_CUSTOM_TOKEN }}",
497+
},
498+
},
499+
},
500+
}
501+
502+
job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md")
503+
504+
require.NoError(t, err)
505+
require.NotNil(t, job)
506+
507+
stepsContent := strings.Join(job.Steps, "")
508+
509+
// Find the assign_to_agent step section
510+
assignToAgentStart := strings.Index(stepsContent, "id: assign_to_agent")
511+
require.Greater(t, assignToAgentStart, -1, "assign_to_agent step should exist")
512+
513+
nextStepOffset := strings.Index(stepsContent[assignToAgentStart:], "\n - ")
514+
var assignToAgentSection string
515+
if nextStepOffset == -1 {
516+
assignToAgentSection = stepsContent[assignToAgentStart:]
517+
} else {
518+
assignToAgentSection = stepsContent[assignToAgentStart : assignToAgentStart+nextStepOffset]
519+
}
520+
521+
// The explicit token should take precedence
522+
assert.Contains(t, assignToAgentSection, "secrets.MY_CUSTOM_TOKEN",
523+
"assign_to_agent step should use the explicitly configured github-token")
524+
assert.NotContains(t, assignToAgentSection, "safe-outputs-app-token.outputs.token",
525+
"assign_to_agent step should not use the GitHub App token even with explicit token")
526+
assert.NotContains(t, assignToAgentSection, "GH_AW_AGENT_TOKEN",
527+
"assign_to_agent step should not use GH_AW_AGENT_TOKEN when explicit token is set")
528+
}
529+
430530
// TestJobOutputs tests that job outputs are correctly configured
431531
func TestJobOutputs(t *testing.T) {
432532
compiler := NewCompiler()

pkg/workflow/safe_outputs_env.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -281,13 +281,11 @@ func (c *Compiler) addSafeOutputCopilotGitHubTokenForConfig(steps *[]string, dat
281281
// addSafeOutputAgentGitHubTokenForConfig adds github-token to the with section for agent assignment operations
282282
// Uses precedence: config token > safe-outputs token > GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN
283283
// This is specifically for assign-to-agent operations which require elevated permissions.
284+
//
285+
// Note: GitHub App tokens are intentionally NOT used here, even when github-app: is configured.
286+
// The Copilot assignment API only accepts PATs (fine-grained or classic), not GitHub App
287+
// installation tokens. Callers must provide an explicit github-token or rely on GH_AW_AGENT_TOKEN.
284288
func (c *Compiler) addSafeOutputAgentGitHubTokenForConfig(steps *[]string, data *WorkflowData, configToken string) {
285-
// If app is configured, use app token
286-
if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil {
287-
*steps = append(*steps, " github-token: ${{ steps.safe-outputs-app-token.outputs.token }}\n")
288-
return
289-
}
290-
291289
// Get safe-outputs level token
292290
var safeOutputsToken string
293291
if data.SafeOutputs != nil {
@@ -300,7 +298,9 @@ func (c *Compiler) addSafeOutputAgentGitHubTokenForConfig(steps *[]string, data
300298
effectiveCustomToken = safeOutputsToken
301299
}
302300

303-
// Get effective token
301+
// Get effective token - falls back to ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
302+
// when no explicit token is provided. GitHub App tokens are never used here because the
303+
// Copilot assignment API rejects them.
304304
effectiveToken := getEffectiveCopilotCodingAgentGitHubToken(effectiveCustomToken)
305305
*steps = append(*steps, fmt.Sprintf(" github-token: %s\n", effectiveToken))
306306
}

0 commit comments

Comments
 (0)