diff --git a/docs/src/content/docs/reference/assign-to-copilot.mdx b/docs/src/content/docs/reference/assign-to-copilot.mdx index 69858093b3e..1ac5a189092 100644 --- a/docs/src/content/docs/reference/assign-to-copilot.mdx +++ b/docs/src/content/docs/reference/assign-to-copilot.mdx @@ -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 diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index e64db99dd09..f6c5108ee22 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -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")) diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index cd243683f1b..15304e133ab 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -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() diff --git a/pkg/workflow/safe_outputs_env.go b/pkg/workflow/safe_outputs_env.go index 34395b7292b..11f8d7c3844 100644 --- a/pkg/workflow/safe_outputs_env.go +++ b/pkg/workflow/safe_outputs_env.go @@ -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. 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 { @@ -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)) }