Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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` automatically falls back to using `GH_AW_AGENT_TOKEN` (or the token 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.
Comment thread
pelikhan marked this conversation as resolved.
Resolved
:::

### 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."))
Comment thread
pelikhan marked this conversation as resolved.
Resolved
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.

Copilot AI Mar 6, 2026

Copy link

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