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
6 changes: 5 additions & 1 deletion docs/src/content/docs/reference/assign-to-copilot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ The required token type and permissions depend on whether you own the repository

### Using a GitHub App

Alternatively, you can use a GitHub App with appropriate permissions instead of a PAT for enhanced security.
:::caution[GitHub App tokens are not supported for Copilot assignment]
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.

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.
:::

### Using a magic secret

Expand Down
15 changes: 15 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
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"))
}

// Emit warning when assign-to-agent is used with github-app: but no explicit github-token:.
// GitHub App tokens are rejected by the Copilot assignment API — a PAT is required.
// The token fallback chain (GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN) is used automatically.
if workflowData.SafeOutputs != nil &&
workflowData.SafeOutputs.AssignToAgent != nil &&
workflowData.SafeOutputs.GitHubApp != nil &&
workflowData.SafeOutputs.AssignToAgent.GitHubToken == "" {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
"assign-to-agent does not support GitHub App tokens. "+
"The Copilot assignment API requires a fine-grained PAT. "+
"The token fallback chain (GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN) will be used automatically. "+
"Add github-token: to your assign-to-agent config to specify a different token."))
c.IncrementWarningCount()
}

// Emit experimental warning for safe-inputs feature
if IsSafeInputsEnabled(workflowData.SafeInputs, workflowData) {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Using experimental feature: safe-inputs"))
Expand Down
100 changes: 100 additions & 0 deletions pkg/workflow/compiler_safe_outputs_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,106 @@ func TestJobWithGitHubApp(t *testing.T) {
assert.Contains(t, stepsContent, "Invalidate GitHub App token")
}

// TestAssignToAgentWithGitHubAppUsesAgentToken tests that when github-app: is configured,
// assign-to-agent uses GH_AW_AGENT_TOKEN rather than the App installation token.
// The Copilot assignment API only accepts PATs, not GitHub App tokens.
func TestAssignToAgentWithGitHubAppUsesAgentToken(t *testing.T) {
compiler := NewCompiler()
compiler.jobManager = NewJobManager()

workflowData := &WorkflowData{
Name: "Test Workflow",
SafeOutputs: &SafeOutputsConfig{
GitHubApp: &GitHubAppConfig{
AppID: "12345",
PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}",
},
AssignToAgent: &AssignToAgentConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")},
},
},
}

job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md")

require.NoError(t, err)
require.NotNil(t, job)

stepsContent := strings.Join(job.Steps, "")

// App token minting step should be present (github-app: is configured)
assert.Contains(t, stepsContent, "Generate GitHub App token", "App token minting step should be present")

// Find the assign_to_agent step section
assignToAgentStart := strings.Index(stepsContent, "id: assign_to_agent")
require.Greater(t, assignToAgentStart, -1, "assign_to_agent step should exist")

// Find the end of the assign_to_agent step (next step starts with " - ")
nextStepOffset := strings.Index(stepsContent[assignToAgentStart:], "\n - ")
var assignToAgentSection string
if nextStepOffset == -1 {
assignToAgentSection = stepsContent[assignToAgentStart:]
} else {
assignToAgentSection = stepsContent[assignToAgentStart : assignToAgentStart+nextStepOffset]
}

// The assign_to_agent step should use GH_AW_AGENT_TOKEN, NOT the App token
assert.Contains(t, assignToAgentSection, "GH_AW_AGENT_TOKEN",
"assign_to_agent step should use GH_AW_AGENT_TOKEN, not the App token")
assert.NotContains(t, assignToAgentSection, "safe-outputs-app-token.outputs.token",
"assign_to_agent step should not use the GitHub App token")
}

// TestAssignToAgentWithGitHubAppAndExplicitToken tests that an explicit github-token
// on assign-to-agent takes precedence over both the App token and GH_AW_AGENT_TOKEN.
func TestAssignToAgentWithGitHubAppAndExplicitToken(t *testing.T) {
compiler := NewCompiler()
compiler.jobManager = NewJobManager()

workflowData := &WorkflowData{
Name: "Test Workflow",
SafeOutputs: &SafeOutputsConfig{
GitHubApp: &GitHubAppConfig{
AppID: "12345",
PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}",
},
AssignToAgent: &AssignToAgentConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: strPtr("1"),
GitHubToken: "${{ secrets.MY_CUSTOM_TOKEN }}",
},
},
},
}

job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md")

require.NoError(t, err)
require.NotNil(t, job)

stepsContent := strings.Join(job.Steps, "")

// Find the assign_to_agent step section
assignToAgentStart := strings.Index(stepsContent, "id: assign_to_agent")
require.Greater(t, assignToAgentStart, -1, "assign_to_agent step should exist")

nextStepOffset := strings.Index(stepsContent[assignToAgentStart:], "\n - ")
var assignToAgentSection string
if nextStepOffset == -1 {
assignToAgentSection = stepsContent[assignToAgentStart:]
} else {
assignToAgentSection = stepsContent[assignToAgentStart : assignToAgentStart+nextStepOffset]
}

// The explicit token should take precedence
assert.Contains(t, assignToAgentSection, "secrets.MY_CUSTOM_TOKEN",
"assign_to_agent step should use the explicitly configured github-token")
assert.NotContains(t, assignToAgentSection, "safe-outputs-app-token.outputs.token",
"assign_to_agent step should not use the GitHub App token even with explicit token")
assert.NotContains(t, assignToAgentSection, "GH_AW_AGENT_TOKEN",
"assign_to_agent step should not use GH_AW_AGENT_TOKEN when explicit token is set")
}

// TestJobOutputs tests that job outputs are correctly configured
func TestJobOutputs(t *testing.T) {
compiler := NewCompiler()
Expand Down
14 changes: 7 additions & 7 deletions pkg/workflow/safe_outputs_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,11 @@ func (c *Compiler) addSafeOutputCopilotGitHubTokenForConfig(steps *[]string, dat
// addSafeOutputAgentGitHubTokenForConfig adds github-token to the with section for agent assignment operations
// Uses precedence: config token > safe-outputs token > GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN
// This is specifically for assign-to-agent operations which require elevated permissions.
//
// Note: GitHub App tokens are intentionally NOT used here, even when github-app: is configured.
// The Copilot assignment API only accepts PATs (fine-grained or classic), not GitHub App
// installation tokens. Callers must provide an explicit github-token or rely on GH_AW_AGENT_TOKEN.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says callers must provide an explicit github-token or rely on GH_AW_AGENT_TOKEN, but the actual precedence is config token → safe-outputs.github-token → (GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN). Update the note to reflect that safe-outputs.github-token is also an explicit override and that the full fallback chain may be used.

Suggested change
// installation tokens. Callers must provide an explicit github-token or rely on GH_AW_AGENT_TOKEN.
// installation tokens. Callers must provide an explicit github-token (either via config or
// safe-outputs.github-token) or rely on the fallback of ${{ secrets.GH_AW_AGENT_TOKEN ||
// secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}.

Copilot uses AI. Check for mistakes.
func (c *Compiler) addSafeOutputAgentGitHubTokenForConfig(steps *[]string, data *WorkflowData, configToken string) {
// If app is configured, use app token
if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil {
*steps = append(*steps, " github-token: ${{ steps.safe-outputs-app-token.outputs.token }}\n")
return
}

// Get safe-outputs level token
var safeOutputsToken string
if data.SafeOutputs != nil {
Expand All @@ -300,7 +298,9 @@ func (c *Compiler) addSafeOutputAgentGitHubTokenForConfig(steps *[]string, data
effectiveCustomToken = safeOutputsToken
}

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