From 3acd54b1cf3b5915d4cd879d22aa8f75a64d9413 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 05:48:11 +0000 Subject: [PATCH 01/12] Initial plan From be9d7da3933e4abd51b98a0c7ec8a105d2e1c5c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:06:46 +0000 Subject: [PATCH 02/12] Add assignees support to create_issue job Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 7 + pkg/workflow/create_issue.go | 33 ++++ ...create_issue_assignees_integration_test.go | 174 ++++++++++++++++++ pkg/workflow/create_issue_assignees_test.go | 168 +++++++++++++++++ 4 files changed, 382 insertions(+) create mode 100644 pkg/workflow/create_issue_assignees_integration_test.go create mode 100644 pkg/workflow/create_issue_assignees_test.go diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 9a384d1d980..3f1595ea999 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2055,6 +2055,13 @@ "type": "string" } }, + "assignees": { + "type": "array", + "description": "Optional list of GitHub usernames to assign the created issue to (e.g., ['user1', 'user2', 'bot-name'])", + "items": { + "type": "string" + } + }, "max": { "type": "integer", "description": "Maximum number of issues to create (default: 1)", diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 5f39aeb2d99..d8da1353bb3 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -10,6 +10,7 @@ type CreateIssuesConfig struct { BaseSafeOutputConfig `yaml:",inline"` TitlePrefix string `yaml:"title-prefix,omitempty"` Labels []string `yaml:"labels,omitempty"` + Assignees []string `yaml:"assignees,omitempty"` // List of users/bots to assign the issue to TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository issues } @@ -40,6 +41,19 @@ func (c *Compiler) parseIssuesConfig(outputMap map[string]any) *CreateIssuesConf } } + // Parse assignees + if assignees, exists := configMap["assignees"]; exists { + if assigneesArray, ok := assignees.([]any); ok { + var assigneeStrings []string + for _, assignee := range assigneesArray { + if assigneeStr, ok := assignee.(string); ok { + assigneeStrings = append(assigneeStrings, assigneeStr) + } + } + issuesConfig.Assignees = assigneeStrings + } + } + // Parse target-repo if targetRepoSlug, exists := configMap["target-repo"]; exists { if targetRepoStr, ok := targetRepoSlug.(string); ok { @@ -116,6 +130,25 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str }) steps = append(steps, scriptSteps...) + // Add assignee steps if assignees are configured + if len(data.SafeOutputs.CreateIssues.Assignees) > 0 { + for i, assignee := range data.SafeOutputs.CreateIssues.Assignees { + steps = append(steps, fmt.Sprintf(" - name: Assign issue to %s\n", assignee)) + steps = append(steps, " if: steps.create_issue.outputs.issue_number != ''\n") + steps = append(steps, " env:\n") + steps = append(steps, " GH_TOKEN: ${{ github.token }}\n") + steps = append(steps, fmt.Sprintf(" ASSIGNEE: %q\n", assignee)) + steps = append(steps, " ISSUE_NUMBER: ${{ steps.create_issue.outputs.issue_number }}\n") + steps = append(steps, " run: |\n") + steps = append(steps, " gh issue edit \"$ISSUE_NUMBER\" --add-assignee \"$ASSIGNEE\"\n") + + // Add a comment after each assignee step except the last + if i < len(data.SafeOutputs.CreateIssues.Assignees)-1 { + steps = append(steps, "\n") + } + } + } + // Create outputs for the job outputs := map[string]string{ "issue_number": "${{ steps.create_issue.outputs.issue_number }}", diff --git a/pkg/workflow/create_issue_assignees_integration_test.go b/pkg/workflow/create_issue_assignees_integration_test.go new file mode 100644 index 00000000000..aace4cf8e89 --- /dev/null +++ b/pkg/workflow/create_issue_assignees_integration_test.go @@ -0,0 +1,174 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestCreateIssueWorkflowCompilationWithAssignees tests end-to-end workflow compilation with assignees +func TestCreateIssueWorkflowCompilationWithAssignees(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "assignees-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + testContent := `--- +name: Test Assignees Feature +on: + issues: + types: [opened] +permissions: + contents: read +engine: claude +safe-outputs: + create-issue: + title-prefix: "[ai] " + labels: [automation, ai-generated] + assignees: [user1, user2, bot-helper] +--- + +# Test Workflow with Assignees + +This is a test workflow that should create an issue and assign it to multiple users. +` + + testFile := filepath.Join(tmpDir, "test-assignees.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-assignees.lock.yml") + compiledContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledStr := string(compiledContent) + + // Verify that create_issue job exists + if !strings.Contains(compiledStr, "create_issue:") { + t.Error("Expected create_issue job in compiled workflow") + } + + // Verify that assignee steps are present + if !strings.Contains(compiledStr, "Assign issue to user1") { + t.Error("Expected assignee step for user1 in compiled workflow") + } + if !strings.Contains(compiledStr, "Assign issue to user2") { + t.Error("Expected assignee step for user2 in compiled workflow") + } + if !strings.Contains(compiledStr, "Assign issue to bot-helper") { + t.Error("Expected assignee step for bot-helper in compiled workflow") + } + + // Verify gh issue edit command + if !strings.Contains(compiledStr, "gh issue edit") { + t.Error("Expected gh issue edit command in compiled workflow") + } + + // Verify --add-assignee flag + if !strings.Contains(compiledStr, "--add-assignee") { + t.Error("Expected --add-assignee flag in compiled workflow") + } + + // Verify ISSUE_NUMBER from step output + if !strings.Contains(compiledStr, "${{ steps.create_issue.outputs.issue_number }}") { + t.Error("Expected ISSUE_NUMBER to reference create_issue step output") + } + + // Verify conditional execution + if !strings.Contains(compiledStr, "if: steps.create_issue.outputs.issue_number != ''") { + t.Error("Expected conditional if statement for assignee steps") + } + + // Verify GH_TOKEN is set + if !strings.Contains(compiledStr, "GH_TOKEN: ${{ github.token }}") { + t.Error("Expected GH_TOKEN environment variable in compiled workflow") + } + + // Verify environment variables for assignees are properly quoted + if !strings.Contains(compiledStr, `ASSIGNEE: "user1"`) { + t.Error("Expected quoted ASSIGNEE environment variable for user1") + } + if !strings.Contains(compiledStr, `ASSIGNEE: "user2"`) { + t.Error("Expected quoted ASSIGNEE environment variable for user2") + } + if !strings.Contains(compiledStr, `ASSIGNEE: "bot-helper"`) { + t.Error("Expected quoted ASSIGNEE environment variable for bot-helper") + } +} + +// TestCreateIssueWorkflowCompilationWithoutAssignees tests that workflows without assignees still work +func TestCreateIssueWorkflowCompilationWithoutAssignees(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "no-assignees-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + testContent := `--- +name: Test Without Assignees +on: + issues: + types: [opened] +permissions: + contents: read +engine: claude +safe-outputs: + create-issue: + title-prefix: "[ai] " + labels: [automation] +--- + +# Test Workflow without Assignees + +This workflow should compile successfully without assignees configuration. +` + + testFile := filepath.Join(tmpDir, "test-no-assignees.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-no-assignees.lock.yml") + compiledContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledStr := string(compiledContent) + + // Verify that create_issue job exists + if !strings.Contains(compiledStr, "create_issue:") { + t.Error("Expected create_issue job in compiled workflow") + } + + // Verify that no assignee steps are present + if strings.Contains(compiledStr, "Assign issue to") { + t.Error("Did not expect assignee steps in workflow without assignees") + } + if strings.Contains(compiledStr, "gh issue edit") { + t.Error("Did not expect gh issue edit command in workflow without assignees") + } +} diff --git a/pkg/workflow/create_issue_assignees_test.go b/pkg/workflow/create_issue_assignees_test.go new file mode 100644 index 00000000000..bc2797ba723 --- /dev/null +++ b/pkg/workflow/create_issue_assignees_test.go @@ -0,0 +1,168 @@ +package workflow + +import ( + "strings" + "testing" +) + +func TestCreateIssueJobWithAssignees(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test with assignees configured + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + Assignees: []string{"user1", "user2", "bot-user"}, + }, + }, + } + + job, err := c.buildCreateOutputIssueJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create issue job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that assignee steps are included + if !strings.Contains(stepsContent, "Assign issue to user1") { + t.Error("Expected assignee step for user1") + } + if !strings.Contains(stepsContent, "Assign issue to user2") { + t.Error("Expected assignee step for user2") + } + if !strings.Contains(stepsContent, "Assign issue to bot-user") { + t.Error("Expected assignee step for bot-user") + } + + // Check that gh issue edit command is present + if !strings.Contains(stepsContent, "gh issue edit") { + t.Error("Expected gh issue edit command in steps") + } + + // Check that --add-assignee flag is present + if !strings.Contains(stepsContent, "--add-assignee") { + t.Error("Expected --add-assignee flag in gh issue edit command") + } + + // Check that ISSUE_NUMBER environment variable is set from step output + if !strings.Contains(stepsContent, "ISSUE_NUMBER: ${{ steps.create_issue.outputs.issue_number }}") { + t.Error("Expected ISSUE_NUMBER to be set from create_issue step output") + } + + // Check that condition is set to only run if issue_number is not empty + if !strings.Contains(stepsContent, "if: steps.create_issue.outputs.issue_number != ''") { + t.Error("Expected conditional if statement for assignee steps") + } + + // Verify that GH_TOKEN is set + if !strings.Contains(stepsContent, "GH_TOKEN: ${{ github.token }}") { + t.Error("Expected GH_TOKEN environment variable to be set") + } +} + +func TestCreateIssueJobWithoutAssignees(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test without assignees + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + // No assignees configured + }, + }, + } + + job, err := c.buildCreateOutputIssueJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create issue job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that no assignee steps are included + if strings.Contains(stepsContent, "Assign issue to") { + t.Error("Did not expect assignee steps when no assignees configured") + } + if strings.Contains(stepsContent, "gh issue edit") { + t.Error("Did not expect gh issue edit command when no assignees configured") + } +} + +func TestCreateIssueJobWithSingleAssignee(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test with a single assignee + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + Assignees: []string{"single-user"}, + }, + }, + } + + job, err := c.buildCreateOutputIssueJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create issue job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that single assignee step is included + if !strings.Contains(stepsContent, "Assign issue to single-user") { + t.Error("Expected assignee step for single-user") + } + + // Check that gh issue edit command is present + if !strings.Contains(stepsContent, "gh issue edit") { + t.Error("Expected gh issue edit command in steps") + } + + // Verify environment variable for assignee + if !strings.Contains(stepsContent, `ASSIGNEE: "single-user"`) { + t.Error("Expected ASSIGNEE environment variable to be set") + } +} + +func TestParseIssuesConfigWithAssignees(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test parsing assignees from config + outputMap := map[string]any{ + "create-issue": map[string]any{ + "title-prefix": "[test] ", + "labels": []any{"bug", "enhancement"}, + "assignees": []any{"user1", "user2", "github-bot"}, + }, + } + + config := c.parseIssuesConfig(outputMap) + if config == nil { + t.Fatal("Expected parseIssuesConfig to return non-nil config") + } + + if len(config.Assignees) != 3 { + t.Errorf("Expected 3 assignees, got %d", len(config.Assignees)) + } + + expectedAssignees := []string{"user1", "user2", "github-bot"} + for i, expected := range expectedAssignees { + if i >= len(config.Assignees) { + t.Errorf("Missing assignee at index %d, expected %s", i, expected) + continue + } + if config.Assignees[i] != expected { + t.Errorf("Assignee at index %d: expected %s, got %s", i, expected, config.Assignees[i]) + } + } +} From c42fcc660c6aef070b007ebd2c58cc31d1745ad7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:10:20 +0000 Subject: [PATCH 03/12] Add documentation for assignees field in create_issue Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/safe-outputs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index ac1f0fcfb15..258840af63e 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -58,12 +58,16 @@ safe-outputs: create-issue: title-prefix: "[ai] " # Optional: prefix for issue titles labels: [automation, agentic] # Optional: labels to attach to issues + assignees: [user1, user2, bot] # Optional: users/bots to assign the issue to max: 5 # Optional: maximum number of issues (default: 1) target-repo: "owner/target-repo" # Optional: create issues in a different repository (requires github-token with appropriate permissions) ``` The agentic part of your workflow should describe the issue(s) it wants created. +**Configuration Options:** +- **`assignees:`** - List of GitHub usernames (users or bots) to automatically assign to created issues. The workflow automatically adds steps that call `gh issue edit --add-assignee` for each assignee after the issue is created. Only runs if the issue was successfully created. + **Example markdown to generate the output:** ```yaml From 621f2f38287d0afadb28cf5be6ff6448d95d2303 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:12:50 +0000 Subject: [PATCH 04/12] Add backward compatibility tests for create_issue assignees feature Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../create_issue_backward_compat_test.go | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 pkg/workflow/create_issue_backward_compat_test.go diff --git a/pkg/workflow/create_issue_backward_compat_test.go b/pkg/workflow/create_issue_backward_compat_test.go new file mode 100644 index 00000000000..6f40626a1ad --- /dev/null +++ b/pkg/workflow/create_issue_backward_compat_test.go @@ -0,0 +1,149 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestCreateIssueBackwardCompatibility ensures existing workflows without assignees still compile correctly +func TestCreateIssueBackwardCompatibility(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "backward-compat-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test with an existing workflow format (no assignees) + testContent := `--- +name: Legacy Workflow Format +on: + issues: + types: [opened] +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: + title-prefix: "[legacy] " + labels: [automation] + max: 2 +--- + +# Legacy Workflow + +This workflow uses the old format without assignees and should continue to work. +` + + testFile := filepath.Join(tmpDir, "legacy-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Legacy workflow should compile without errors: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "legacy-workflow.lock.yml") + compiledContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledStr := string(compiledContent) + + // Verify that create_issue job exists + if !strings.Contains(compiledStr, "create_issue:") { + t.Error("Expected create_issue job in compiled workflow") + } + + // Verify that JavaScript step is present + if !strings.Contains(compiledStr, "Create Output Issue") { + t.Error("Expected Create Output Issue step in compiled workflow") + } + + // Verify that no assignee steps are present + if strings.Contains(compiledStr, "Assign issue to") { + t.Error("Did not expect assignee steps in legacy workflow") + } + + // Verify that outputs are still set correctly + if !strings.Contains(compiledStr, "issue_number: ${{ steps.create_issue.outputs.issue_number }}") { + t.Error("Expected issue_number output in compiled workflow") + } + if !strings.Contains(compiledStr, "issue_url: ${{ steps.create_issue.outputs.issue_url }}") { + t.Error("Expected issue_url output in compiled workflow") + } +} + +// TestCreateIssueMinimalConfiguration ensures minimal configuration still works +func TestCreateIssueMinimalConfiguration(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "minimal-config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test with minimal configuration (just enabling create-issue) + testContent := `--- +name: Minimal Workflow +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: +--- + +# Minimal Workflow + +Create an issue with minimal configuration. +` + + testFile := filepath.Join(tmpDir, "minimal-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Minimal workflow should compile without errors: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "minimal-workflow.lock.yml") + compiledContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledStr := string(compiledContent) + + // Verify that create_issue job exists + if !strings.Contains(compiledStr, "create_issue:") { + t.Error("Expected create_issue job in compiled workflow") + } + + // Verify that no assignee steps are present + if strings.Contains(compiledStr, "Assign issue to") { + t.Error("Did not expect assignee steps in minimal workflow") + } + + // Verify basic job structure + if !strings.Contains(compiledStr, "permissions:") { + t.Error("Expected permissions section in create_issue job") + } + if !strings.Contains(compiledStr, "issues: write") { + t.Error("Expected issues: write permission in create_issue job") + } +} From 85f06ebbf2e92501c00b2335a4c2a4f0efb3b56e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:27:07 +0000 Subject: [PATCH 05/12] Address PR feedback: use proper GitHub token, support string/array assignees, handle copilot username Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../content/docs/reference/safe-outputs.md | 6 +- pkg/parser/schemas/main_workflow_schema.json | 18 ++- pkg/workflow/create_issue.go | 25 +++- ...create_issue_assignees_integration_test.go | 127 +++++++++++++++++- pkg/workflow/create_issue_assignees_test.go | 110 ++++++++++++++- 5 files changed, 268 insertions(+), 18 deletions(-) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 258840af63e..2fff83eb502 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -58,7 +58,7 @@ safe-outputs: create-issue: title-prefix: "[ai] " # Optional: prefix for issue titles labels: [automation, agentic] # Optional: labels to attach to issues - assignees: [user1, user2, bot] # Optional: users/bots to assign the issue to + assignees: [user1, user2, copilot] # Optional: users/bots to assign the issue to max: 5 # Optional: maximum number of issues (default: 1) target-repo: "owner/target-repo" # Optional: create issues in a different repository (requires github-token with appropriate permissions) ``` @@ -66,7 +66,9 @@ safe-outputs: The agentic part of your workflow should describe the issue(s) it wants created. **Configuration Options:** -- **`assignees:`** - List of GitHub usernames (users or bots) to automatically assign to created issues. The workflow automatically adds steps that call `gh issue edit --add-assignee` for each assignee after the issue is created. Only runs if the issue was successfully created. +- **`assignees:`** - GitHub username(s) to automatically assign to created issues. Accepts either a single string (`assignees: user1`) or an array of strings (`assignees: [user1, user2]`). The workflow automatically adds steps that call `gh issue edit --add-assignee` for each assignee after the issue is created. Only runs if the issue was successfully created. + - **Special value**: Use `copilot` to assign to the `copilot-swe-agent` bot + - Uses the configured GitHub token (respects `github-token` precedence: create-issue config > safe-outputs config > top-level config > default) **Example markdown to generate the output:** diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 3f1595ea999..d6cbca9fbf9 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2056,11 +2056,19 @@ } }, "assignees": { - "type": "array", - "description": "Optional list of GitHub usernames to assign the created issue to (e.g., ['user1', 'user2', 'bot-name'])", - "items": { - "type": "string" - } + "oneOf": [ + { + "type": "string", + "description": "Single GitHub username to assign the created issue to (e.g., 'user1' or 'copilot'). Use 'copilot' to assign to copilot-swe-agent." + }, + { + "type": "array", + "description": "List of GitHub usernames to assign the created issue to (e.g., ['user1', 'user2', 'copilot']). Use 'copilot' to assign to copilot-swe-agent.", + "items": { + "type": "string" + } + } + ] }, "max": { "type": "integer", diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index d8da1353bb3..7b5c769f50f 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -41,9 +41,13 @@ func (c *Compiler) parseIssuesConfig(outputMap map[string]any) *CreateIssuesConf } } - // Parse assignees + // Parse assignees (supports both string and array) if assignees, exists := configMap["assignees"]; exists { - if assigneesArray, ok := assignees.([]any); ok { + if assigneeStr, ok := assignees.(string); ok { + // Single string format + issuesConfig.Assignees = []string{assigneeStr} + } else if assigneesArray, ok := assignees.([]any); ok { + // Array format var assigneeStrings []string for _, assignee := range assigneesArray { if assigneeStr, ok := assignee.(string); ok { @@ -132,12 +136,25 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str // Add assignee steps if assignees are configured if len(data.SafeOutputs.CreateIssues.Assignees) > 0 { + // Get the effective GitHub token to use for gh CLI + var safeOutputsToken string + if data.SafeOutputs != nil { + safeOutputsToken = data.SafeOutputs.GitHubToken + } + effectiveToken := getEffectiveGitHubToken(token, getEffectiveGitHubToken(safeOutputsToken, data.GitHubToken)) + for i, assignee := range data.SafeOutputs.CreateIssues.Assignees { + // Special handling: "copilot" is the username for "copilot-swe-agent" + actualAssignee := assignee + if assignee == "copilot" { + actualAssignee = "copilot-swe-agent" + } + steps = append(steps, fmt.Sprintf(" - name: Assign issue to %s\n", assignee)) steps = append(steps, " if: steps.create_issue.outputs.issue_number != ''\n") steps = append(steps, " env:\n") - steps = append(steps, " GH_TOKEN: ${{ github.token }}\n") - steps = append(steps, fmt.Sprintf(" ASSIGNEE: %q\n", assignee)) + steps = append(steps, fmt.Sprintf(" GH_TOKEN: %s\n", effectiveToken)) + steps = append(steps, fmt.Sprintf(" ASSIGNEE: %q\n", actualAssignee)) steps = append(steps, " ISSUE_NUMBER: ${{ steps.create_issue.outputs.issue_number }}\n") steps = append(steps, " run: |\n") steps = append(steps, " gh issue edit \"$ISSUE_NUMBER\" --add-assignee \"$ASSIGNEE\"\n") diff --git a/pkg/workflow/create_issue_assignees_integration_test.go b/pkg/workflow/create_issue_assignees_integration_test.go index aace4cf8e89..e594283cbe0 100644 --- a/pkg/workflow/create_issue_assignees_integration_test.go +++ b/pkg/workflow/create_issue_assignees_integration_test.go @@ -93,9 +93,9 @@ This is a test workflow that should create an issue and assign it to multiple us t.Error("Expected conditional if statement for assignee steps") } - // Verify GH_TOKEN is set - if !strings.Contains(compiledStr, "GH_TOKEN: ${{ github.token }}") { - t.Error("Expected GH_TOKEN environment variable in compiled workflow") + // Verify GH_TOKEN is set with proper token expression + if !strings.Contains(compiledStr, "GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") { + t.Error("Expected GH_TOKEN environment variable with proper token expression in compiled workflow") } // Verify environment variables for assignees are properly quoted @@ -172,3 +172,124 @@ This workflow should compile successfully without assignees configuration. t.Error("Did not expect gh issue edit command in workflow without assignees") } } + +// TestCreateIssueWorkflowWithCopilotAssignee tests that "copilot" is mapped to "copilot-swe-agent" +func TestCreateIssueWorkflowWithCopilotAssignee(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "copilot-assignee-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + testContent := `--- +name: Test Copilot Assignee +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: + assignees: copilot +--- + +# Test Workflow + +Create an issue and assign to copilot. +` + + testFile := filepath.Join(tmpDir, "test-copilot.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-copilot.lock.yml") + compiledContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledStr := string(compiledContent) + + // Verify that step name shows "copilot" + if !strings.Contains(compiledStr, "Assign issue to copilot") { + t.Error("Expected assignee step name to show 'copilot'") + } + + // Verify that actual assignee is "copilot-swe-agent" + if !strings.Contains(compiledStr, `ASSIGNEE: "copilot-swe-agent"`) { + t.Error("Expected ASSIGNEE to be mapped to 'copilot-swe-agent'") + } + + // Verify that "copilot" is NOT used as the actual assignee value + if strings.Contains(compiledStr, `ASSIGNEE: "copilot"`) { + t.Error("Did not expect 'copilot' to be used directly as assignee value") + } +} + +// TestCreateIssueWorkflowWithStringAssignee tests that single string assignee works +func TestCreateIssueWorkflowWithStringAssignee(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "string-assignee-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + testContent := `--- +name: Test String Assignee +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: + assignees: single-user +--- + +# Test Workflow + +Create an issue with a single assignee. +` + + testFile := filepath.Join(tmpDir, "test-string.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the compiled output + outputFile := filepath.Join(tmpDir, "test-string.lock.yml") + compiledContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Failed to read compiled output: %v", err) + } + + compiledStr := string(compiledContent) + + // Verify that assignee step is created + if !strings.Contains(compiledStr, "Assign issue to single-user") { + t.Error("Expected assignee step for single-user") + } + + // Verify the assignee environment variable + if !strings.Contains(compiledStr, `ASSIGNEE: "single-user"`) { + t.Error("Expected ASSIGNEE environment variable for single-user") + } +} diff --git a/pkg/workflow/create_issue_assignees_test.go b/pkg/workflow/create_issue_assignees_test.go index bc2797ba723..644824b14fb 100644 --- a/pkg/workflow/create_issue_assignees_test.go +++ b/pkg/workflow/create_issue_assignees_test.go @@ -58,9 +58,9 @@ func TestCreateIssueJobWithAssignees(t *testing.T) { t.Error("Expected conditional if statement for assignee steps") } - // Verify that GH_TOKEN is set - if !strings.Contains(stepsContent, "GH_TOKEN: ${{ github.token }}") { - t.Error("Expected GH_TOKEN environment variable to be set") + // Verify that GH_TOKEN is set with proper token expression + if !strings.Contains(stepsContent, "GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") { + t.Error("Expected GH_TOKEN environment variable to be set with proper token expression") } } @@ -137,7 +137,7 @@ func TestParseIssuesConfigWithAssignees(t *testing.T) { // Create a compiler instance c := NewCompiler(false, "", "test") - // Test parsing assignees from config + // Test parsing assignees from config (array format) outputMap := map[string]any{ "create-issue": map[string]any{ "title-prefix": "[test] ", @@ -166,3 +166,105 @@ func TestParseIssuesConfigWithAssignees(t *testing.T) { } } } + +func TestParseIssuesConfigWithSingleStringAssignee(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test parsing assignees from config (string format) + outputMap := map[string]any{ + "create-issue": map[string]any{ + "title-prefix": "[test] ", + "labels": []any{"bug"}, + "assignees": "single-user", + }, + } + + config := c.parseIssuesConfig(outputMap) + if config == nil { + t.Fatal("Expected parseIssuesConfig to return non-nil config") + } + + if len(config.Assignees) != 1 { + t.Errorf("Expected 1 assignee, got %d", len(config.Assignees)) + } + + if config.Assignees[0] != "single-user" { + t.Errorf("Expected assignee 'single-user', got %s", config.Assignees[0]) + } +} + +func TestCreateIssueJobWithCopilotAssignee(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test with "copilot" as assignee (should be mapped to "copilot-swe-agent") + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + Assignees: []string{"copilot"}, + }, + }, + } + + job, err := c.buildCreateOutputIssueJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create issue job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that the step name shows "copilot" + if !strings.Contains(stepsContent, "Assign issue to copilot") { + t.Error("Expected assignee step name to show 'copilot'") + } + + // Check that the actual assignee is "copilot-swe-agent" + if !strings.Contains(stepsContent, `ASSIGNEE: "copilot-swe-agent"`) { + t.Error("Expected ASSIGNEE environment variable to be set to 'copilot-swe-agent'") + } + + // Verify that the original "copilot" is NOT used as assignee + if strings.Contains(stepsContent, `ASSIGNEE: "copilot"`) && !strings.Contains(stepsContent, `ASSIGNEE: "copilot-swe-agent"`) { + t.Error("Expected 'copilot' to be mapped to 'copilot-swe-agent', not used directly") + } +} + +func TestCreateIssueJobWithCustomGitHubToken(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test with custom GitHub token configuration + workflowData := &WorkflowData{ + Name: "test-workflow", + GitHubToken: "${{ secrets.CUSTOM_PAT }}", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + GitHubToken: "${{ secrets.ISSUE_SPECIFIC_PAT }}", + }, + Assignees: []string{"user1"}, + }, + }, + } + + job, err := c.buildCreateOutputIssueJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create issue job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that the issue-specific token is used (highest precedence) + if !strings.Contains(stepsContent, "GH_TOKEN: ${{ secrets.ISSUE_SPECIFIC_PAT }}") { + t.Error("Expected issue-specific GitHub token to be used in assignee steps") + } + + // Verify default token is NOT used + if strings.Contains(stepsContent, "GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN") { + t.Error("Did not expect default token when custom token is configured") + } +} From 9efd720fadf03af84d96b9ff166cd5975850f52e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:35:06 +0000 Subject: [PATCH 06/12] Update dev.md to create poem issue assigned to copilot Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 12 ++++++++++-- .github/workflows/dev.md | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 3863c4dfcf3..e0e3f202922 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -955,7 +955,7 @@ jobs: cat > $GITHUB_AW_PROMPT << 'EOF' - Search the latest trends about javascript frameworks using tavily tools, and the last 3 pull requests using github tools and print a summary. + Write a creative poem about GitHub Agentic Workflows and create an issue with the poem. Assign the issue to copilot. EOF - name: Append XPIA security instructions to prompt @@ -3527,6 +3527,14 @@ jobs: (async () => { await main(); })(); + - name: Assign issue to copilot + if: steps.create_issue.outputs.issue_number != '' + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + ASSIGNEE: "copilot-swe-agent" + ISSUE_NUMBER: ${{ steps.create_issue.outputs.issue_number }} + run: | + gh issue edit "$ISSUE_NUMBER" --add-assignee "$ASSIGNEE" detection: needs: agent @@ -3556,7 +3564,7 @@ jobs: env: WORKFLOW_NAME: "Dev" WORKFLOW_DESCRIPTION: "No description provided" - WORKFLOW_MARKDOWN: "\n\nSearch the latest trends about javascript frameworks using tavily tools, and the last 3 pull requests using github tools and print a summary.\n" + WORKFLOW_MARKDOWN: "\n\nWrite a creative poem about GitHub Agentic Workflows and create an issue with the poem. Assign the issue to copilot.\n" with: script: | const fs = require('fs'); diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index 2e7ee963b16..bb3d949ea57 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -17,8 +17,9 @@ tools: github: safe-outputs: create-issue: + assignees: copilot imports: - shared/mcp/tavily.md --- -Search the latest trends about javascript frameworks using tavily tools, and the last 3 pull requests using github tools and print a summary. +Write a creative poem about GitHub Agentic Workflows and create an issue with the poem. Assign the issue to copilot. From b938ade5b22ffc07013ff03d1b6e301c48196bad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:54:04 +0000 Subject: [PATCH 07/12] Remove push trigger from dev.md to prevent self-triggering Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 7 ++++--- .github/workflows/dev.md | 3 --- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index e0e3f202922..50db2a96de0 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -27,9 +27,6 @@ name: "Dev" "on": - push: - paths: - - .github/workflows/dev.md workflow_dispatch: null permissions: {} @@ -68,6 +65,8 @@ jobs: permissions: actions: read contents: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" env: GITHUB_AW_SAFE_OUTPUTS: /tmp/gh-aw/safe-outputs/outputs.jsonl GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create_issue\":{\"max\":1},\"missing_tool\":{}}" @@ -3540,6 +3539,8 @@ jobs: needs: agent runs-on: ubuntu-latest permissions: read-all + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" timeout-minutes: 10 steps: - name: Download agent output artifact diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index bb3d949ea57..f4e0687f0ef 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -1,9 +1,6 @@ --- on: workflow_dispatch: - push: - paths: - - '.github/workflows/dev.md' concurrency: group: dev-workflow-${{ github.ref }} cancel-in-progress: true From 77e522fdacc83e452d45ac8f67e74a6a8c2d22e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:35:30 +0000 Subject: [PATCH 08/12] Add checkout step for gh CLI when assignees are configured Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 2 ++ pkg/workflow/create_issue.go | 4 ++++ .../create_issue_assignees_integration_test.go | 9 +++++++++ pkg/workflow/create_issue_assignees_test.go | 10 ++++++++++ 4 files changed, 25 insertions(+) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 50db2a96de0..fc8ebd660ab 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -3526,6 +3526,8 @@ jobs: (async () => { await main(); })(); + - name: Checkout repository for gh CLI + uses: actions/checkout@v5 - name: Assign issue to copilot if: steps.create_issue.outputs.issue_number != '' env: diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 7b5c769f50f..8868ecb2799 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -136,6 +136,10 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str // Add assignee steps if assignees are configured if len(data.SafeOutputs.CreateIssues.Assignees) > 0 { + // Add checkout step for gh CLI to work + steps = append(steps, " - name: Checkout repository for gh CLI\n") + steps = append(steps, " uses: actions/checkout@v5\n") + // Get the effective GitHub token to use for gh CLI var safeOutputsToken string if data.SafeOutputs != nil { diff --git a/pkg/workflow/create_issue_assignees_integration_test.go b/pkg/workflow/create_issue_assignees_integration_test.go index e594283cbe0..1c79d34f00c 100644 --- a/pkg/workflow/create_issue_assignees_integration_test.go +++ b/pkg/workflow/create_issue_assignees_integration_test.go @@ -98,6 +98,15 @@ This is a test workflow that should create an issue and assign it to multiple us t.Error("Expected GH_TOKEN environment variable with proper token expression in compiled workflow") } + // Verify checkout step is present + if !strings.Contains(compiledStr, "Checkout repository for gh CLI") { + t.Error("Expected checkout step for gh CLI in compiled workflow") + } + + if !strings.Contains(compiledStr, "uses: actions/checkout@v5") { + t.Error("Expected checkout to use actions/checkout@v5 in compiled workflow") + } + // Verify environment variables for assignees are properly quoted if !strings.Contains(compiledStr, `ASSIGNEE: "user1"`) { t.Error("Expected quoted ASSIGNEE environment variable for user1") diff --git a/pkg/workflow/create_issue_assignees_test.go b/pkg/workflow/create_issue_assignees_test.go index 644824b14fb..82392b8932e 100644 --- a/pkg/workflow/create_issue_assignees_test.go +++ b/pkg/workflow/create_issue_assignees_test.go @@ -27,6 +27,11 @@ func TestCreateIssueJobWithAssignees(t *testing.T) { // Convert steps to a single string for testing stepsContent := strings.Join(job.Steps, "") + // Check that checkout step is included before assignee steps + if !strings.Contains(stepsContent, "Checkout repository for gh CLI") { + t.Error("Expected checkout step for gh CLI") + } + // Check that assignee steps are included if !strings.Contains(stepsContent, "Assign issue to user1") { t.Error("Expected assignee step for user1") @@ -62,6 +67,11 @@ func TestCreateIssueJobWithAssignees(t *testing.T) { if !strings.Contains(stepsContent, "GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") { t.Error("Expected GH_TOKEN environment variable to be set with proper token expression") } + + // Verify that checkout uses actions/checkout@v5 + if !strings.Contains(stepsContent, "uses: actions/checkout@v5") { + t.Error("Expected checkout to use actions/checkout@v5") + } } func TestCreateIssueJobWithoutAssignees(t *testing.T) { From d0063bc3547fbea00fcfd475567b8428c248a896 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:57:24 +0000 Subject: [PATCH 09/12] Make checkout step conditional and add Copilot Agent documentation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 1 + .../content/docs/reference/safe-outputs.md | 41 +++++++++++++++++++ pkg/workflow/create_issue.go | 1 + ...create_issue_assignees_integration_test.go | 11 +++++ pkg/workflow/create_issue_assignees_test.go | 13 ++++++ 5 files changed, 67 insertions(+) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index fc8ebd660ab..04756a97b4f 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -3527,6 +3527,7 @@ jobs: await main(); })(); - name: Checkout repository for gh CLI + if: steps.create_issue.outputs.issue_number != '' uses: actions/checkout@v5 - name: Assign issue to copilot if: steps.create_issue.outputs.issue_number != '' diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 2fff83eb502..91f00506f8c 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -70,6 +70,47 @@ The agentic part of your workflow should describe the issue(s) it wants created. - **Special value**: Use `copilot` to assign to the `copilot-swe-agent` bot - Uses the configured GitHub token (respects `github-token` precedence: create-issue config > safe-outputs config > top-level config > default) +#### Assign Issue to GitHub Copilot Agent + +You can automatically assign created issues to the GitHub Copilot Agent by using the special `copilot` value in the `assignees` field. This is useful for workflows that generate issues for the Copilot Agent to work on. + +**Example:** +```yaml +safe-outputs: + create-issue: + title-prefix: "[ai] " + assignees: copilot # Automatically assigns to copilot-swe-agent +``` + +When you specify `copilot` as an assignee, the compiler automatically: +1. Maps `copilot` to the actual GitHub username `copilot-swe-agent` +2. Adds a checkout step to enable the GitHub CLI +3. Creates an assignee step that runs after the issue is successfully created + +The generated workflow includes: +```yaml +- name: Checkout repository for gh CLI + if: steps.create_issue.outputs.issue_number != '' + uses: actions/checkout@v5 +- name: Assign issue to copilot + if: steps.create_issue.outputs.issue_number != '' + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + ASSIGNEE: "copilot-swe-agent" + ISSUE_NUMBER: ${{ steps.create_issue.outputs.issue_number }} + run: | + gh issue edit "$ISSUE_NUMBER" --add-assignee "$ASSIGNEE" +``` + +**Multiple assignees including Copilot:** +```yaml +safe-outputs: + create-issue: + assignees: [user1, copilot, user2] # Mix human users and the Copilot Agent +``` + +This enables workflows where the Copilot Agent collaborates with human team members on generated issues. + **Example markdown to generate the output:** ```yaml diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 8868ecb2799..ea6704c4a49 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -138,6 +138,7 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str if len(data.SafeOutputs.CreateIssues.Assignees) > 0 { // Add checkout step for gh CLI to work steps = append(steps, " - name: Checkout repository for gh CLI\n") + steps = append(steps, " if: steps.create_issue.outputs.issue_number != ''\n") steps = append(steps, " uses: actions/checkout@v5\n") // Get the effective GitHub token to use for gh CLI diff --git a/pkg/workflow/create_issue_assignees_integration_test.go b/pkg/workflow/create_issue_assignees_integration_test.go index 1c79d34f00c..29bd774c007 100644 --- a/pkg/workflow/create_issue_assignees_integration_test.go +++ b/pkg/workflow/create_issue_assignees_integration_test.go @@ -107,6 +107,17 @@ This is a test workflow that should create an issue and assign it to multiple us t.Error("Expected checkout to use actions/checkout@v5 in compiled workflow") } + // Verify checkout step is conditional on issue creation + checkoutPattern := "Checkout repository for gh CLI" + checkoutIndex := strings.Index(compiledStr, checkoutPattern) + if checkoutIndex != -1 { + // Check that conditional appears after the checkout step name + afterCheckout := compiledStr[checkoutIndex:] + if !strings.Contains(afterCheckout[:200], "if: steps.create_issue.outputs.issue_number != ''") { + t.Error("Expected checkout step to be conditional on issue creation") + } + } + // Verify environment variables for assignees are properly quoted if !strings.Contains(compiledStr, `ASSIGNEE: "user1"`) { t.Error("Expected quoted ASSIGNEE environment variable for user1") diff --git a/pkg/workflow/create_issue_assignees_test.go b/pkg/workflow/create_issue_assignees_test.go index 82392b8932e..3cf80f05426 100644 --- a/pkg/workflow/create_issue_assignees_test.go +++ b/pkg/workflow/create_issue_assignees_test.go @@ -32,6 +32,19 @@ func TestCreateIssueJobWithAssignees(t *testing.T) { t.Error("Expected checkout step for gh CLI") } + // Verify that checkout step is conditional on issue creation + checkoutPattern := "Checkout repository for gh CLI" + checkoutIndex := strings.Index(stepsContent, checkoutPattern) + if checkoutIndex == -1 { + t.Error("Expected checkout step") + } else { + // Check that conditional appears after the checkout step name + afterCheckout := stepsContent[checkoutIndex:] + if !strings.Contains(afterCheckout, "if: steps.create_issue.outputs.issue_number != ''") { + t.Error("Expected checkout step to be conditional on issue creation") + } + } + // Check that assignee steps are included if !strings.Contains(stepsContent, "Assign issue to user1") { t.Error("Expected assignee step for user1") From 4e3356b91f269b5416c78141b3f2f40eeec5bc84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 12:08:48 +0000 Subject: [PATCH 10/12] Remove detailed Copilot section and add PAT requirement note Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../content/docs/reference/safe-outputs.md | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 91f00506f8c..7016d5ffe93 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -69,47 +69,7 @@ The agentic part of your workflow should describe the issue(s) it wants created. - **`assignees:`** - GitHub username(s) to automatically assign to created issues. Accepts either a single string (`assignees: user1`) or an array of strings (`assignees: [user1, user2]`). The workflow automatically adds steps that call `gh issue edit --add-assignee` for each assignee after the issue is created. Only runs if the issue was successfully created. - **Special value**: Use `copilot` to assign to the `copilot-swe-agent` bot - Uses the configured GitHub token (respects `github-token` precedence: create-issue config > safe-outputs config > top-level config > default) - -#### Assign Issue to GitHub Copilot Agent - -You can automatically assign created issues to the GitHub Copilot Agent by using the special `copilot` value in the `assignees` field. This is useful for workflows that generate issues for the Copilot Agent to work on. - -**Example:** -```yaml -safe-outputs: - create-issue: - title-prefix: "[ai] " - assignees: copilot # Automatically assigns to copilot-swe-agent -``` - -When you specify `copilot` as an assignee, the compiler automatically: -1. Maps `copilot` to the actual GitHub username `copilot-swe-agent` -2. Adds a checkout step to enable the GitHub CLI -3. Creates an assignee step that runs after the issue is successfully created - -The generated workflow includes: -```yaml -- name: Checkout repository for gh CLI - if: steps.create_issue.outputs.issue_number != '' - uses: actions/checkout@v5 -- name: Assign issue to copilot - if: steps.create_issue.outputs.issue_number != '' - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - ASSIGNEE: "copilot-swe-agent" - ISSUE_NUMBER: ${{ steps.create_issue.outputs.issue_number }} - run: | - gh issue edit "$ISSUE_NUMBER" --add-assignee "$ASSIGNEE" -``` - -**Multiple assignees including Copilot:** -```yaml -safe-outputs: - create-issue: - assignees: [user1, copilot, user2] # Mix human users and the Copilot Agent -``` - -This enables workflows where the Copilot Agent collaborates with human team members on generated issues. + - **Note**: To assign issues to bots (including `copilot`), you must use a Personal Access Token (PAT) with appropriate permissions. The default `GITHUB_TOKEN` does not have permission to assign issues to bots. Configure a PAT using the `github-token` field at the workflow, safe-outputs, or create-issue level. **Example markdown to generate the output:** From 104888c8e9875d27c851c783587d81015d9d96b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 18 Oct 2025 12:13:38 +0000 Subject: [PATCH 11/12] Add changeset for assignees feature --- .changeset/patch-add-assignees-to-create-issue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-add-assignees-to-create-issue.md diff --git a/.changeset/patch-add-assignees-to-create-issue.md b/.changeset/patch-add-assignees-to-create-issue.md new file mode 100644 index 00000000000..a9a103584d9 --- /dev/null +++ b/.changeset/patch-add-assignees-to-create-issue.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add assignees support to create_issue safe-output job From 44dd7d70c9ced09fccbcd9907b6e62ac63211823 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 12:19:15 +0000 Subject: [PATCH 12/12] Convert PAT note to Astro Starlight adornment format Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/safe-outputs.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 7016d5ffe93..2bec85f3583 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -69,7 +69,10 @@ The agentic part of your workflow should describe the issue(s) it wants created. - **`assignees:`** - GitHub username(s) to automatically assign to created issues. Accepts either a single string (`assignees: user1`) or an array of strings (`assignees: [user1, user2]`). The workflow automatically adds steps that call `gh issue edit --add-assignee` for each assignee after the issue is created. Only runs if the issue was successfully created. - **Special value**: Use `copilot` to assign to the `copilot-swe-agent` bot - Uses the configured GitHub token (respects `github-token` precedence: create-issue config > safe-outputs config > top-level config > default) - - **Note**: To assign issues to bots (including `copilot`), you must use a Personal Access Token (PAT) with appropriate permissions. The default `GITHUB_TOKEN` does not have permission to assign issues to bots. Configure a PAT using the `github-token` field at the workflow, safe-outputs, or create-issue level. + +:::note +To assign issues to bots (including `copilot`), you must use a Personal Access Token (PAT) with appropriate permissions. The default `GITHUB_TOKEN` does not have permission to assign issues to bots. Configure a PAT using the `github-token` field at the workflow, safe-outputs, or create-issue level. +::: **Example markdown to generate the output:**