From 98a55e67a8f8345ece39f131262211b00b43b8c0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 2 Dec 2025 23:47:13 +0000
Subject: [PATCH 01/11] Initial plan
From 834aef659bf744fa9dd28a0f567b9ec21f4e6f29 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 00:15:17 +0000
Subject: [PATCH 02/11] Add assign-to-user safe output type for assigning users
to issues
- Add JSON schema definition for assign-to-user in main_workflow_schema.json
- Add AssignToUserConfig struct and parsing logic
- Create pkg/workflow/assign_to_user.go for job builder
- Create pkg/workflow/js/assign_to_user.cjs JavaScript implementation
- Add assign_to_user tool to safe_outputs_tools.json
- Register script and embed directive in scripts.go and js.go
- Add to generateFilteredToolsJSON(), generateSafeOutputsConfig(), HasSafeOutputsEnabled()
- Add to compiler_jobs.go for job building
- Add to safe_output_validation_config.go for validation
- Update tests in safe_outputs_tools_test.go and safe_output_validation_config_test.go
- Update documentation in docs/src/content/docs/reference/safe-outputs.md
Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
---
.../content/docs/reference/safe-outputs.md | 34 +++++
pkg/parser/schemas/main_workflow_schema.json | 39 ++++++
pkg/workflow/assign_to_user.go | 108 +++++++++++++++
pkg/workflow/compiler.go | 1 +
pkg/workflow/compiler_jobs.go | 18 +++
pkg/workflow/js/assign_to_user.cjs | 131 ++++++++++++++++++
pkg/workflow/js/safe_outputs_tools.json | 26 ++++
pkg/workflow/safe_output_validation_config.go | 8 ++
.../safe_output_validation_config_test.go | 1 +
pkg/workflow/safe_outputs.go | 23 +++
pkg/workflow/safe_outputs_tools_test.go | 1 +
pkg/workflow/scripts.go | 9 ++
12 files changed, 399 insertions(+)
create mode 100644 pkg/workflow/assign_to_user.go
create mode 100644 pkg/workflow/js/assign_to_user.cjs
diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md
index 5b40714aa7a..f50c3c45631 100644
--- a/docs/src/content/docs/reference/safe-outputs.md
+++ b/docs/src/content/docs/reference/safe-outputs.md
@@ -42,6 +42,7 @@ This declares that the workflow should create at most one new issue.
| [**Close Discussion**](#close-discussion-close-discussion) | `close-discussion:` | Close discussions with comment and resolution | 1 | ✅ |
| [**Create Agent Task**](#agent-task-creation-create-agent-task) | `create-agent-task:` | Create Copilot agent tasks | 1 | ✅ |
| [**Assign to Agent**](#assign-to-agent-assign-to-agent) | `assign-to-agent:` | Assign Copilot agents to issues | 1 | ✅ |
+| [**Assign to User**](#assign-to-user-assign-to-user) | `assign-to-user:` | Assign users to issues | 1 | ✅ |
| [**Push to PR Branch**](#push-to-pr-branch-push-to-pull-request-branch) | `push-to-pull-request-branch:` | Push changes to PR branch | 1 | ❌ |
| [**Update Release**](#release-updates-update-release) | `update-release:` | Update GitHub release descriptions | 1 | ✅ |
| [**Code Scanning Alerts**](#code-scanning-alerts-create-code-scanning-alert) | `create-code-scanning-alert:` | Generate SARIF security advisories | unlimited | ❌ |
@@ -462,6 +463,39 @@ Ensure Copilot is enabled for your repository. Check organization settings if bo
Reference: [GitHub Copilot agent documentation](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/)
+### Assign to User (`assign-to-user:`)
+
+Assigns GitHub users to issues. Specify `allowed` to restrict which users can be assigned.
+
+```yaml wrap
+safe-outputs:
+ assign-to-user:
+ allowed: [user1, user2] # restrict to specific users
+ max: 3 # max assignments (default: 1)
+ target: "*" # "triggering" (default), "*", or number
+ target-repo: "owner/repo" # cross-repository
+```
+
+**Target**: `"triggering"` (requires issue event), `"*"` (any issue), or number (specific issue).
+
+**Agent Output Format:**
+```json
+{
+ "type": "assign_to_user",
+ "issue_number": 123,
+ "assignees": ["octocat", "mona"]
+}
+```
+
+Single user assignment is also supported:
+```json
+{
+ "type": "assign_to_user",
+ "issue_number": 123,
+ "assignee": "octocat"
+}
+```
+
## Cross-Repository Operations
Many safe outputs support `target-repo` for cross-repository operations. Requires a PAT (via `github-token` or `GH_AW_GITHUB_TOKEN`) with access to target repositories. The default `GITHUB_TOKEN` only has permissions for the current repository.
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 3143d98a147..88d1b9dbfed 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -3472,6 +3472,45 @@
}
]
},
+ "assign-to-user": {
+ "oneOf": [
+ {
+ "type": "null",
+ "description": "Enable user assignment with default configuration"
+ },
+ {
+ "type": "object",
+ "description": "Configuration for assigning users to issues from agentic workflow output",
+ "properties": {
+ "allowed": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional list of allowed usernames. If specified, only these users can be assigned."
+ },
+ "max": {
+ "type": "integer",
+ "description": "Optional maximum number of user assignments (default: 1)",
+ "minimum": 1
+ },
+ "target": {
+ "type": ["string", "number"],
+ "description": "Target issue to assign users to. Use 'triggering' (default) for the triggering issue, '*' to allow any issue, or a specific issue number."
+ },
+ "target-repo": {
+ "type": "string",
+ "description": "Target repository in format 'owner/repo' for cross-repository user assignment. Takes precedence over trial target repo settings."
+ },
+ "github-token": {
+ "$ref": "#/$defs/github_token",
+ "description": "GitHub token to use for this specific output type. Overrides global github-token if specified."
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
"link-sub-issue": {
"oneOf": [
{
diff --git a/pkg/workflow/assign_to_user.go b/pkg/workflow/assign_to_user.go
new file mode 100644
index 00000000000..adbecda51fd
--- /dev/null
+++ b/pkg/workflow/assign_to_user.go
@@ -0,0 +1,108 @@
+package workflow
+
+import (
+ "fmt"
+)
+
+// AssignToUserConfig holds configuration for assigning users to issues from agent output
+type AssignToUserConfig struct {
+ BaseSafeOutputConfig `yaml:",inline"`
+ SafeOutputTargetConfig `yaml:",inline"`
+ Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed usernames. If omitted, any users are allowed.
+}
+
+// buildAssignToUserJob creates the assign_to_user job
+func (c *Compiler) buildAssignToUserJob(data *WorkflowData, mainJobName string) (*Job, error) {
+ if data.SafeOutputs == nil || data.SafeOutputs.AssignToUser == nil {
+ return nil, fmt.Errorf("safe-outputs.assign-to-user configuration is required")
+ }
+
+ cfg := data.SafeOutputs.AssignToUser
+
+ // Handle max count with default of 1
+ maxCount := 1
+ if cfg.Max > 0 {
+ maxCount = cfg.Max
+ }
+
+ // Build custom environment variables using shared helpers
+ listJobConfig := ListJobConfig{
+ SafeOutputTargetConfig: cfg.SafeOutputTargetConfig,
+ Allowed: cfg.Allowed,
+ }
+ customEnvVars := BuildListJobEnvVars("GH_AW_ASSIGNEES", listJobConfig, maxCount)
+
+ // Add standard environment variables (metadata + staged/target repo)
+ customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, cfg.TargetRepoSlug)...)
+
+ // Create outputs for the job
+ outputs := map[string]string{
+ "assigned_users": "${{ steps.assign_to_user.outputs.assigned_users }}",
+ }
+
+ var jobCondition = BuildSafeOutputType("assign_to_user")
+ if cfg.Target == "" {
+ // Only run if in issue context when target is not specified
+ issueCondition := BuildPropertyAccess("github.event.issue.number")
+ jobCondition = buildAnd(jobCondition, issueCondition)
+ }
+
+ // Use the shared builder function to create the job
+ return c.buildSafeOutputJob(data, SafeOutputJobConfig{
+ JobName: "assign_to_user",
+ StepName: "Assign to User",
+ StepID: "assign_to_user",
+ MainJobName: mainJobName,
+ CustomEnvVars: customEnvVars,
+ Script: getAssignToUserScript(),
+ Permissions: NewPermissionsContentsReadIssuesWrite(),
+ Outputs: outputs,
+ Condition: jobCondition,
+ Token: cfg.GitHubToken,
+ TargetRepoSlug: cfg.TargetRepoSlug,
+ })
+}
+
+// parseAssignToUserConfig handles assign-to-user configuration
+func (c *Compiler) parseAssignToUserConfig(outputMap map[string]any) *AssignToUserConfig {
+ if configData, exists := outputMap["assign-to-user"]; exists {
+ assignToUserConfig := &AssignToUserConfig{}
+
+ if configMap, ok := configData.(map[string]any); ok {
+ // Parse allowed users (supports both string and array)
+ if allowed, exists := configMap["allowed"]; exists {
+ if allowedStr, ok := allowed.(string); ok {
+ // Single string format
+ assignToUserConfig.Allowed = []string{allowedStr}
+ } else if allowedArray, ok := allowed.([]any); ok {
+ // Array format
+ var allowedStrings []string
+ for _, user := range allowedArray {
+ if userStr, ok := user.(string); ok {
+ allowedStrings = append(allowedStrings, userStr)
+ }
+ }
+ assignToUserConfig.Allowed = allowedStrings
+ }
+ }
+
+ // Parse target config (target, target-repo)
+ targetConfig, isInvalid := ParseTargetConfig(configMap)
+ if isInvalid {
+ return nil // Invalid configuration, return nil to cause validation error
+ }
+ assignToUserConfig.SafeOutputTargetConfig = targetConfig
+
+ // Parse common base fields (github-token, max) with default max of 1
+ c.parseBaseSafeOutputConfig(configMap, &assignToUserConfig.BaseSafeOutputConfig, 0)
+ } else {
+ // If configData is nil or not a map (e.g., "assign-to-user:" with no value),
+ // use defaults
+ assignToUserConfig.Max = 1
+ }
+
+ return assignToUserConfig
+ }
+
+ return nil
+}
diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go
index 1408596b3cd..824508ba3c9 100644
--- a/pkg/workflow/compiler.go
+++ b/pkg/workflow/compiler.go
@@ -275,6 +275,7 @@ type SafeOutputsConfig struct {
AddReviewer *AddReviewerConfig `yaml:"add-reviewer,omitempty"`
AssignMilestone *AssignMilestoneConfig `yaml:"assign-milestone,omitempty"`
AssignToAgent *AssignToAgentConfig `yaml:"assign-to-agent,omitempty"`
+ AssignToUser *AssignToUserConfig `yaml:"assign-to-user,omitempty"` // Assign users to issues
UpdateIssues *UpdateIssuesConfig `yaml:"update-issues,omitempty"`
UpdatePullRequests *UpdatePullRequestsConfig `yaml:"update-pull-request,omitempty"` // Update GitHub pull request title/body
PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pull-request-branch,omitempty"`
diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go
index 29e925a95f4..647852c07f2 100644
--- a/pkg/workflow/compiler_jobs.go
+++ b/pkg/workflow/compiler_jobs.go
@@ -474,6 +474,24 @@ func (c *Compiler) buildSafeOutputsJobs(data *WorkflowData, jobName, markdownPat
safeOutputJobNames = append(safeOutputJobNames, assignToAgentJob.Name)
}
+ // Build assign_to_user job if output.assign-to-user is configured
+ if data.SafeOutputs.AssignToUser != nil {
+ assignToUserJob, err := c.buildAssignToUserJob(data, jobName)
+ if err != nil {
+ return fmt.Errorf("failed to build assign_to_user job: %w", err)
+ }
+ // Safe-output jobs should depend on agent job (always) AND detection job (if enabled)
+ if threatDetectionEnabled {
+ assignToUserJob.Needs = append(assignToUserJob.Needs, constants.DetectionJobName)
+ // Add detection success check to the job condition
+ assignToUserJob.If = AddDetectionSuccessCheck(assignToUserJob.If)
+ }
+ if err := c.jobManager.AddJob(assignToUserJob); err != nil {
+ return fmt.Errorf("failed to add assign_to_user job: %w", err)
+ }
+ safeOutputJobNames = append(safeOutputJobNames, assignToUserJob.Name)
+ }
+
// Build update_issue job if output.update-issue is configured
if data.SafeOutputs.UpdateIssues != nil {
updateIssueJob, err := c.buildCreateOutputUpdateIssueJob(data, jobName)
diff --git a/pkg/workflow/js/assign_to_user.cjs b/pkg/workflow/js/assign_to_user.cjs
new file mode 100644
index 00000000000..62f15205389
--- /dev/null
+++ b/pkg/workflow/js/assign_to_user.cjs
@@ -0,0 +1,131 @@
+// @ts-check
+///
+
+const { processSafeOutput, processItems } = require("./safe_output_processor.cjs");
+
+async function main() {
+ // Use shared processor for common steps
+ const result = await processSafeOutput(
+ {
+ itemType: "assign_to_user",
+ configKey: "assign_to_user",
+ displayName: "Assignees",
+ itemTypeName: "user assignment",
+ supportsPR: false, // Issue-only: not relevant for PRs
+ supportsIssue: true,
+ envVars: {
+ allowed: "GH_AW_ASSIGNEES_ALLOWED",
+ maxCount: "GH_AW_ASSIGNEES_MAX_COUNT",
+ target: "GH_AW_ASSIGNEES_TARGET",
+ },
+ },
+ {
+ title: "Assign to User",
+ description: "The following user assignments would be made if staged mode was disabled:",
+ renderItem: item => {
+ let content = "";
+ if (item.issue_number) {
+ content += `**Target Issue:** #${item.issue_number}\n\n`;
+ } else {
+ content += `**Target:** Current issue\n\n`;
+ }
+ if (item.assignees && item.assignees.length > 0) {
+ content += `**Users to assign:** ${item.assignees.join(", ")}\n\n`;
+ } else if (item.assignee) {
+ content += `**User to assign:** ${item.assignee}\n\n`;
+ }
+ return content;
+ },
+ }
+ );
+
+ if (!result.success) {
+ return;
+ }
+
+ // @ts-ignore - TypeScript doesn't narrow properly after success check
+ const { item: assignItem, config, targetResult } = result;
+ if (!config || !targetResult || targetResult.number === undefined) {
+ core.setFailed("Internal error: config, targetResult, or targetResult.number is undefined");
+ return;
+ }
+ const { allowed: allowedAssignees, maxCount } = config;
+ const issueNumber = targetResult.number;
+
+ // Support both singular "assignee" and plural "assignees" for flexibility
+ let requestedAssignees = [];
+ if (assignItem.assignees && Array.isArray(assignItem.assignees)) {
+ requestedAssignees = assignItem.assignees;
+ } else if (assignItem.assignee) {
+ requestedAssignees = [assignItem.assignee];
+ }
+
+ core.info(`Requested assignees: ${JSON.stringify(requestedAssignees)}`);
+
+ // Use shared helper to filter, sanitize, dedupe, and limit
+ const uniqueAssignees = processItems(requestedAssignees, allowedAssignees, maxCount);
+
+ if (uniqueAssignees.length === 0) {
+ core.info("No assignees to add");
+ core.setOutput("assigned_users", "");
+ await core.summary
+ .addRaw(
+ `
+## User Assignment
+
+No users were assigned (no valid assignees found in agent output).
+`
+ )
+ .write();
+ return;
+ }
+
+ core.info(`Assigning ${uniqueAssignees.length} users to issue #${issueNumber}: ${JSON.stringify(uniqueAssignees)}`);
+
+ try {
+ // Get target repository from environment or use current
+ const targetRepoEnv = process.env.GH_AW_TARGET_REPO_SLUG?.trim();
+ let targetOwner = context.repo.owner;
+ let targetRepo = context.repo.repo;
+
+ if (targetRepoEnv) {
+ const parts = targetRepoEnv.split("/");
+ if (parts.length === 2) {
+ targetOwner = parts[0];
+ targetRepo = parts[1];
+ core.info(`Using target repository: ${targetOwner}/${targetRepo}`);
+ }
+ }
+
+ // Add assignees to the issue
+ await github.rest.issues.addAssignees({
+ owner: targetOwner,
+ repo: targetRepo,
+ issue_number: issueNumber,
+ assignees: uniqueAssignees,
+ });
+
+ core.info(`Successfully assigned ${uniqueAssignees.length} user(s) to issue #${issueNumber}`);
+
+ core.setOutput("assigned_users", uniqueAssignees.join("\n"));
+
+ const assigneesListMarkdown = uniqueAssignees.map(assignee => `- \`${assignee}\``).join("\n");
+ await core.summary
+ .addRaw(
+ `
+## User Assignment
+
+Successfully assigned ${uniqueAssignees.length} user(s) to issue #${issueNumber}:
+
+${assigneesListMarkdown}
+`
+ )
+ .write();
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ core.error(`Failed to assign users: ${errorMessage}`);
+ core.setFailed(`Failed to assign users: ${errorMessage}`);
+ }
+}
+
+await main();
diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json
index 4d5ced058f2..89069a558a8 100644
--- a/pkg/workflow/js/safe_outputs_tools.json
+++ b/pkg/workflow/js/safe_outputs_tools.json
@@ -332,6 +332,32 @@
"additionalProperties": false
}
},
+ {
+ "name": "assign_to_user",
+ "description": "Assign one or more GitHub users to an issue. Use this to delegate work to specific team members. Users must have access to the repository.",
+ "inputSchema": {
+ "type": "object",
+ "required": ["issue_number"],
+ "properties": {
+ "issue_number": {
+ "type": ["number", "string"],
+ "description": "Issue number to assign users to. If omitted, assigns to the issue that triggered this workflow."
+ },
+ "assignees": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "GitHub usernames to assign to the issue (e.g., ['octocat', 'mona']). Users must have access to the repository."
+ },
+ "assignee": {
+ "type": "string",
+ "description": "Single GitHub username to assign. Use 'assignees' array for multiple users."
+ }
+ },
+ "additionalProperties": false
+ }
+ },
{
"name": "update_issue",
"description": "Update an existing GitHub issue's status, title, or body. Use this to modify issue properties after creation. Only the fields you specify will be updated; other fields remain unchanged.",
diff --git a/pkg/workflow/safe_output_validation_config.go b/pkg/workflow/safe_output_validation_config.go
index 98969ac4708..3cf4d7d688f 100644
--- a/pkg/workflow/safe_output_validation_config.go
+++ b/pkg/workflow/safe_output_validation_config.go
@@ -100,6 +100,14 @@ var ValidationConfig = map[string]TypeValidationConfig{
"agent": {Type: "string", Sanitize: true, MaxLength: 128},
},
},
+ "assign_to_user": {
+ DefaultMax: 1,
+ Fields: map[string]FieldValidation{
+ "issue_number": {IssueOrPRNumber: true},
+ "assignees": {Type: "[]string", Sanitize: true, MaxLength: 39}, // GitHub username max length is 39
+ "assignee": {Type: "string", Sanitize: true, MaxLength: 39}, // Single assignee alternative
+ },
+ },
"update_issue": {
DefaultMax: 1,
CustomValidation: "requiresOneOf:status,title,body",
diff --git a/pkg/workflow/safe_output_validation_config_test.go b/pkg/workflow/safe_output_validation_config_test.go
index f0939a6fef9..4335116eb68 100644
--- a/pkg/workflow/safe_output_validation_config_test.go
+++ b/pkg/workflow/safe_output_validation_config_test.go
@@ -29,6 +29,7 @@ func TestGetValidationConfigJSON(t *testing.T) {
"add_reviewer",
"assign_milestone",
"assign_to_agent",
+ "assign_to_user",
"update_issue",
"update_pull_request",
"push_to_pull_request_branch",
diff --git a/pkg/workflow/safe_outputs.go b/pkg/workflow/safe_outputs.go
index f0faacef7a3..ffe8056a98d 100644
--- a/pkg/workflow/safe_outputs.go
+++ b/pkg/workflow/safe_outputs.go
@@ -42,6 +42,7 @@ func HasSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool {
safeOutputs.AddReviewer != nil ||
safeOutputs.AssignMilestone != nil ||
safeOutputs.AssignToAgent != nil ||
+ safeOutputs.AssignToUser != nil ||
safeOutputs.UpdateIssues != nil ||
safeOutputs.UpdatePullRequests != nil ||
safeOutputs.PushToPullRequestBranch != nil ||
@@ -223,6 +224,12 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut
}
}
+ // Handle assign-to-user
+ assignToUserConfig := c.parseAssignToUserConfig(outputMap)
+ if assignToUserConfig != nil {
+ config.AssignToUser = assignToUserConfig
+ }
+
// Handle update-issue
updateIssuesConfig := c.parseUpdateIssuesConfig(outputMap)
if updateIssuesConfig != nil {
@@ -895,6 +902,19 @@ func generateSafeOutputsConfig(data *WorkflowData) string {
}
safeOutputsConfig["assign_to_agent"] = assignToAgentConfig
}
+ if data.SafeOutputs.AssignToUser != nil {
+ assignToUserConfig := map[string]any{}
+ // Always include max (use configured value or default)
+ maxValue := 1 // default
+ if data.SafeOutputs.AssignToUser.Max > 0 {
+ maxValue = data.SafeOutputs.AssignToUser.Max
+ }
+ assignToUserConfig["max"] = maxValue
+ if len(data.SafeOutputs.AssignToUser.Allowed) > 0 {
+ assignToUserConfig["allowed"] = data.SafeOutputs.AssignToUser.Allowed
+ }
+ safeOutputsConfig["assign_to_user"] = assignToUserConfig
+ }
if data.SafeOutputs.UpdateIssues != nil {
updateConfig := map[string]any{}
// Always include max (use configured value or default)
@@ -1097,6 +1117,9 @@ func generateFilteredToolsJSON(data *WorkflowData) (string, error) {
if data.SafeOutputs.AssignToAgent != nil {
enabledTools["assign_to_agent"] = true
}
+ if data.SafeOutputs.AssignToUser != nil {
+ enabledTools["assign_to_user"] = true
+ }
if data.SafeOutputs.UpdateIssues != nil {
enabledTools["update_issue"] = true
}
diff --git a/pkg/workflow/safe_outputs_tools_test.go b/pkg/workflow/safe_outputs_tools_test.go
index ab21391822c..08f5358ab09 100644
--- a/pkg/workflow/safe_outputs_tools_test.go
+++ b/pkg/workflow/safe_outputs_tools_test.go
@@ -282,6 +282,7 @@ func TestGetSafeOutputsToolsJSON(t *testing.T) {
"add_reviewer",
"assign_milestone",
"assign_to_agent",
+ "assign_to_user",
"update_issue",
"update_pull_request",
"push_to_pull_request_branch",
diff --git a/pkg/workflow/scripts.go b/pkg/workflow/scripts.go
index 7cb53dbdb2f..3f3ca39df48 100644
--- a/pkg/workflow/scripts.go
+++ b/pkg/workflow/scripts.go
@@ -39,6 +39,9 @@ var assignMilestoneScriptSource string
//go:embed js/assign_to_agent.cjs
var assignToAgentScriptSource string
+//go:embed js/assign_to_user.cjs
+var assignToUserScriptSource string
+
//go:embed js/assign_copilot_to_created_issues.cjs
var assignCopilotToCreatedIssuesScriptSource string
@@ -116,6 +119,7 @@ func init() {
DefaultScriptRegistry.Register("add_reviewer", addReviewerScriptSource)
DefaultScriptRegistry.Register("assign_milestone", assignMilestoneScriptSource)
DefaultScriptRegistry.Register("assign_to_agent", assignToAgentScriptSource)
+ DefaultScriptRegistry.Register("assign_to_user", assignToUserScriptSource)
DefaultScriptRegistry.Register("assign_copilot_to_created_issues", assignCopilotToCreatedIssuesScriptSource)
DefaultScriptRegistry.Register("link_sub_issue", linkSubIssueScriptSource)
DefaultScriptRegistry.Register("create_discussion", createDiscussionScriptSource)
@@ -184,6 +188,11 @@ func getAssignToAgentScript() string {
return DefaultScriptRegistry.Get("assign_to_agent")
}
+// getAssignToUserScript returns the bundled assign_to_user script
+func getAssignToUserScript() string {
+ return DefaultScriptRegistry.Get("assign_to_user")
+}
+
// getAssignCopilotToCreatedIssuesScript returns the bundled assign_copilot_to_created_issues script
func getAssignCopilotToCreatedIssuesScript() string {
return DefaultScriptRegistry.Get("assign_copilot_to_created_issues")
From 5f70a5bc7048d3b34ec2ed7dd380b1c6660e82fc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 00:19:26 +0000
Subject: [PATCH 03/11] Fix default max value in parseBaseSafeOutputConfig call
Change from 0 to 1 to match documented default behavior.
Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
---
pkg/workflow/assign_to_user.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/workflow/assign_to_user.go b/pkg/workflow/assign_to_user.go
index adbecda51fd..baa5ed96db6 100644
--- a/pkg/workflow/assign_to_user.go
+++ b/pkg/workflow/assign_to_user.go
@@ -94,7 +94,7 @@ func (c *Compiler) parseAssignToUserConfig(outputMap map[string]any) *AssignToUs
assignToUserConfig.SafeOutputTargetConfig = targetConfig
// Parse common base fields (github-token, max) with default max of 1
- c.parseBaseSafeOutputConfig(configMap, &assignToUserConfig.BaseSafeOutputConfig, 0)
+ c.parseBaseSafeOutputConfig(configMap, &assignToUserConfig.BaseSafeOutputConfig, 1)
} else {
// If configData is nil or not a map (e.g., "assign-to-user:" with no value),
// use defaults
From 3d7565e48fdd199a50166b4a7d2270c8d733bcd1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 00:23:30 +0000
Subject: [PATCH 04/11] Refactor parseAssignToUserConfig to use
ParseListJobConfig
Reduce code duplication and improve consistency with other similar
parsing functions.
Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
---
pkg/workflow/assign_to_user.go | 27 ++++-----------------------
1 file changed, 4 insertions(+), 23 deletions(-)
diff --git a/pkg/workflow/assign_to_user.go b/pkg/workflow/assign_to_user.go
index baa5ed96db6..990c5baee9e 100644
--- a/pkg/workflow/assign_to_user.go
+++ b/pkg/workflow/assign_to_user.go
@@ -69,29 +69,10 @@ func (c *Compiler) parseAssignToUserConfig(outputMap map[string]any) *AssignToUs
assignToUserConfig := &AssignToUserConfig{}
if configMap, ok := configData.(map[string]any); ok {
- // Parse allowed users (supports both string and array)
- if allowed, exists := configMap["allowed"]; exists {
- if allowedStr, ok := allowed.(string); ok {
- // Single string format
- assignToUserConfig.Allowed = []string{allowedStr}
- } else if allowedArray, ok := allowed.([]any); ok {
- // Array format
- var allowedStrings []string
- for _, user := range allowedArray {
- if userStr, ok := user.(string); ok {
- allowedStrings = append(allowedStrings, userStr)
- }
- }
- assignToUserConfig.Allowed = allowedStrings
- }
- }
-
- // Parse target config (target, target-repo)
- targetConfig, isInvalid := ParseTargetConfig(configMap)
- if isInvalid {
- return nil // Invalid configuration, return nil to cause validation error
- }
- assignToUserConfig.SafeOutputTargetConfig = targetConfig
+ // Parse list job config (target, target-repo, allowed)
+ listJobConfig, _ := ParseListJobConfig(configMap, "allowed")
+ assignToUserConfig.SafeOutputTargetConfig = listJobConfig.SafeOutputTargetConfig
+ assignToUserConfig.Allowed = listJobConfig.Allowed
// Parse common base fields (github-token, max) with default max of 1
c.parseBaseSafeOutputConfig(configMap, &assignToUserConfig.BaseSafeOutputConfig, 1)
From 996a78afa9d0fbeea2356fc32ec8be2c52ef016a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 00:51:44 +0000
Subject: [PATCH 05/11] Add test-assign-to-user.md workflow for assign_to_user
safe output
This adds a test agentic workflow that exercises the new assign-to-user
safe output type.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
pkg/cli/workflows/test-assign-to-user.md | 41 ++++++++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 pkg/cli/workflows/test-assign-to-user.md
diff --git a/pkg/cli/workflows/test-assign-to-user.md b/pkg/cli/workflows/test-assign-to-user.md
new file mode 100644
index 00000000000..2773cca9afb
--- /dev/null
+++ b/pkg/cli/workflows/test-assign-to-user.md
@@ -0,0 +1,41 @@
+---
+name: Test Assign to User
+description: Test workflow for assign_to_user safe output feature
+on:
+ issues:
+ types: [labeled]
+ workflow_dispatch:
+ inputs:
+ issue_number:
+ description: 'Issue number to test with'
+ required: true
+ type: string
+ assignee:
+ description: 'GitHub username to assign'
+ required: true
+ type: string
+
+permissions:
+ actions: write
+ contents: write
+ issues: write
+
+engine: copilot
+timeout-minutes: 5
+
+safe-outputs:
+ assign-to-user:
+ max: 5
+strict: false
+---
+
+# Assign to User Test Workflow
+
+This workflow tests the `assign_to_user` safe output feature, which allows AI agents to assign GitHub users to issues.
+
+## Task
+
+**For workflow_dispatch:**
+Assign user `${{ github.event.inputs.assignee }}` to issue #${{ github.event.inputs.issue_number }} using the `assign_to_user` tool from the `safeoutputs` MCP server.
+
+Do not use GitHub tools. The assign_to_user tool will handle the actual assignment.
From 1ef0994be3f170aa065aa3dfa8a75f79bc602d83 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 3 Dec 2025 00:57:29 +0000
Subject: [PATCH 06/11] Add changeset [skip-ci]
---
.changeset/patch-add-assign-to-user-safe-output.md | 8 ++++++++
1 file changed, 8 insertions(+)
create mode 100644 .changeset/patch-add-assign-to-user-safe-output.md
diff --git a/.changeset/patch-add-assign-to-user-safe-output.md b/.changeset/patch-add-assign-to-user-safe-output.md
new file mode 100644
index 00000000000..070bd51b120
--- /dev/null
+++ b/.changeset/patch-add-assign-to-user-safe-output.md
@@ -0,0 +1,8 @@
+---
+"gh-aw": patch
+---
+
+Add `assign-to-user` safe output type and supporting files (schemas, Go structs, JS implementation, tests, and docs).
+
+This change adds a new safe output `assign-to-user` analogous to `assign-to-agent`, including parser schema, job builder, JavaScript runner script, and tests. It is an internal addition and does not change public CLI APIs.
+
From 4e070e14c7c0ccc262db8ee63d45e93e92d74c82 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 01:09:25 +0000
Subject: [PATCH 07/11] Update dev.md to use assign-to-user for mrjf on
workflow dispatch
Changes the dev workflow from using assign-to-agent (Copilot) to
assign-to-user (mrjf) for issue assignment.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/dev.lock.yml | 929 +++++++++++++++++----------------
.github/workflows/dev.md | 18 +-
2 files changed, 478 insertions(+), 469 deletions(-)
diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index 13ae2eec40b..951e16694e8 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -18,26 +18,26 @@
# gh aw compile
# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
#
-# Find issues with "[deps]" in title and assign to Copilot agent
+# Find issues with "[deps]" in title and assign to mrjf
#
# Original Frontmatter:
# ```yaml
# on:
# workflow_dispatch:
# name: Dev
-# description: Find issues with "[deps]" in title and assign to Copilot agent
+# description: Find issues with "[deps]" in title and assign to mrjf
# timeout-minutes: 5
# strict: false
# engine: claude
# permissions:
# contents: read
-# issues: read
+# issues: write
# tools:
# github:
# toolsets: [repos, issues]
# safe-outputs:
-# assign-to-agent:
-# name: copilot
+# assign-to-user:
+# allowed: [mrjf]
# ```
#
# Job Dependency Graph:
@@ -45,15 +45,15 @@
# graph LR
# activation["activation"]
# agent["agent"]
-# assign_to_agent["assign_to_agent"]
+# assign_to_user["assign_to_user"]
# conclusion["conclusion"]
# detection["detection"]
# activation --> agent
-# agent --> assign_to_agent
-# detection --> assign_to_agent
+# agent --> assign_to_user
+# detection --> assign_to_user
# agent --> conclusion
# activation --> conclusion
-# assign_to_agent --> conclusion
+# assign_to_user --> conclusion
# agent --> detection
# ```
#
@@ -61,7 +61,7 @@
# ```markdown
# # Dependency Issue Assignment
#
-# Find an open issue in this repository with "[deps]" in the title and assign it to the Copilot agent for resolution.
+# Find an open issue in this repository with "[deps]" in the title and assign it to mrjf for resolution.
#
# ## Task
#
@@ -70,16 +70,16 @@
# is:issue is:open "[deps]" in:title repo:${{ github.repository }}
# ```
#
-# 2. **Filter out assigned issues**: Skip any issues that already have Copilot as an assignee.
+# 2. **Filter out assigned issues**: Skip any issues that already have mrjf as an assignee.
#
-# 3. **Assign to Copilot**: For the first suitable issue found, use the `assign_to_agent` tool to assign it to the Copilot agent.
+# 3. **Assign to mrjf**: For the first suitable issue found, use the `assign_to_user` tool to assign it to mrjf.
#
# **Agent Output Format:**
# ```json
# {
-# "type": "assign_to_agent",
+# "type": "assign_to_user",
# "issue_number": ,
-# "agent": "copilot"
+# "assignee": "mrjf"
# }
# ```
#
@@ -104,7 +104,7 @@ name: "Dev"
permissions:
contents: read
- issues: read
+ issues: write
concurrency:
group: "gh-aw-${{ github.workflow }}"
@@ -213,7 +213,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
- issues: read
+ issues: write
concurrency:
group: "gh-aw-claude-${{ github.workflow }}"
env:
@@ -422,21 +422,28 @@ jobs:
run: |
mkdir -p /tmp/gh-aw/safeoutputs
cat > /tmp/gh-aw/safeoutputs/config.json << 'EOF'
- {"assign_to_agent":{"default_agent":"copilot"},"missing_tool":{"max":0},"noop":{"max":1}}
+ {"assign_to_user":{"allowed":["mrjf"],"max":1},"missing_tool":{"max":0},"noop":{"max":1}}
EOF
cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF'
[
{
- "description": "Assign the GitHub Copilot coding agent to work on an issue. The agent will analyze the issue and attempt to implement a solution, creating a pull request when complete. Use this to delegate coding tasks to Copilot.",
+ "description": "Assign one or more GitHub users to an issue. Use this to delegate work to specific team members. Users must have access to the repository.",
"inputSchema": {
"additionalProperties": false,
"properties": {
- "agent": {
- "description": "Agent identifier to assign. Defaults to 'copilot' (the Copilot coding agent) if not specified.",
+ "assignee": {
+ "description": "Single GitHub username to assign. Use 'assignees' array for multiple users.",
"type": "string"
},
+ "assignees": {
+ "description": "GitHub usernames to assign to the issue (e.g., ['octocat', 'mona']). Users must have access to the repository.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
"issue_number": {
- "description": "Issue number to assign the Copilot agent to. The issue should contain clear, actionable requirements.",
+ "description": "Issue number to assign users to. If omitted, assigns to the issue that triggered this workflow.",
"type": [
"number",
"string"
@@ -448,7 +455,7 @@ jobs:
],
"type": "object"
},
- "name": "assign_to_agent"
+ "name": "assign_to_user"
},
{
"description": "Report that a tool or capability needed to complete the task is not available. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.",
@@ -497,17 +504,21 @@ jobs:
EOF
cat > /tmp/gh-aw/safeoutputs/validation.json << 'EOF'
{
- "assign_to_agent": {
+ "assign_to_user": {
"defaultMax": 1,
"fields": {
- "agent": {
+ "assignee": {
"type": "string",
"sanitize": true,
- "maxLength": 128
+ "maxLength": 39
+ },
+ "assignees": {
+ "type": "[]string",
+ "sanitize": true,
+ "maxLength": 39
},
"issue_number": {
- "required": true,
- "positiveInteger": true
+ "issueOrPRNumber": true
}
}
},
@@ -1640,7 +1651,7 @@ jobs:
cat << 'PROMPT_EOF' | envsubst > "$GH_AW_PROMPT"
# Dependency Issue Assignment
- Find an open issue in this repository with "[deps]" in the title and assign it to the Copilot agent for resolution.
+ Find an open issue in this repository with "[deps]" in the title and assign it to mrjf for resolution.
## Task
@@ -1649,16 +1660,16 @@ jobs:
is:issue is:open "[deps]" in:title repo:${GH_AW_GITHUB_REPOSITORY}
```
- 2. **Filter out assigned issues**: Skip any issues that already have Copilot as an assignee.
+ 2. **Filter out assigned issues**: Skip any issues that already have mrjf as an assignee.
- 3. **Assign to Copilot**: For the first suitable issue found, use the `assign_to_agent` tool to assign it to the Copilot agent.
+ 3. **Assign to mrjf**: For the first suitable issue found, use the `assign_to_user` tool to assign it to mrjf.
**Agent Output Format:**
```json
{
- "type": "assign_to_agent",
+ "type": "assign_to_user",
"issue_number": ,
- "agent": "copilot"
+ "assignee": "mrjf"
}
```
@@ -4079,22 +4090,20 @@ jobs:
main();
}
- assign_to_agent:
+ assign_to_user:
needs:
- agent
- detection
if: >
- (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_agent'))) &&
- (needs.detection.outputs.success == 'true')
+ ((((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_user'))) &&
+ (github.event.issue.number)) && (needs.detection.outputs.success == 'true')
runs-on: ubuntu-slim
permissions:
- actions: write
- contents: write
+ contents: read
issues: write
- pull-requests: write
timeout-minutes: 10
outputs:
- assigned_agents: ${{ steps.assign_to_agent.outputs.assigned_agents }}
+ assigned_users: ${{ steps.assign_to_user.outputs.assigned_users }}
steps:
- name: Download agent output artifact
continue-on-error: true
@@ -4107,17 +4116,17 @@ jobs:
mkdir -p /tmp/gh-aw/safeoutputs/
find "/tmp/gh-aw/safeoutputs/" -type f -print
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
- - name: Assign to Agent
- id: assign_to_agent
+ - name: Assign to User
+ id: assign_to_user
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
- GH_AW_AGENT_DEFAULT: "copilot"
- GH_AW_AGENT_MAX_COUNT: 1
+ GH_AW_ASSIGNEES_ALLOWED: "mrjf"
+ GH_AW_ASSIGNEES_MAX_COUNT: 1
GH_AW_WORKFLOW_NAME: "Dev"
GH_AW_ENGINE_ID: "claude"
with:
- github-token: ${{ secrets.GH_AW_AGENT_TOKEN }}
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs");
const MAX_LOG_CONTENT_LENGTH = 10000;
@@ -4179,472 +4188,472 @@ jobs:
core.setFailed(error instanceof Error ? error : String(error));
}
}
- const AGENT_LOGIN_NAMES = {
- copilot: "copilot-swe-agent",
- };
- function getAgentName(assignee) {
- const normalized = assignee.startsWith("@") ? assignee.slice(1) : assignee;
- if (AGENT_LOGIN_NAMES[normalized]) {
- return normalized;
+ function parseAllowedItems(envValue) {
+ const trimmed = envValue?.trim();
+ if (!trimmed) {
+ return undefined;
}
- return null;
+ return trimmed
+ .split(",")
+ .map(item => item.trim())
+ .filter(item => item);
}
- async function getAvailableAgentLogins(owner, repo) {
- const query = `
- query($owner: String!, $repo: String!) {
- repository(owner: $owner, name: $repo) {
- suggestedActors(first: 100, capabilities: CAN_BE_ASSIGNED) {
- nodes { ... on Bot { login __typename } }
- }
+ function parseMaxCount(envValue, defaultValue = 3) {
+ if (!envValue) {
+ return { valid: true, value: defaultValue };
+ }
+ const parsed = parseInt(envValue, 10);
+ if (isNaN(parsed) || parsed < 1) {
+ return {
+ valid: false,
+ error: `Invalid max value: ${envValue}. Must be a positive integer`,
+ };
+ }
+ return { valid: true, value: parsed };
+ }
+ function resolveTarget(params) {
+ const { targetConfig, item, context, itemType, supportsPR = false } = params;
+ const isIssueContext = context.eventName === "issues" || context.eventName === "issue_comment";
+ const isPRContext =
+ context.eventName === "pull_request" ||
+ context.eventName === "pull_request_review" ||
+ context.eventName === "pull_request_review_comment";
+ const target = targetConfig || "triggering";
+ if (target === "triggering") {
+ if (supportsPR) {
+ if (!isIssueContext && !isPRContext) {
+ return {
+ success: false,
+ error: `Target is "triggering" but not running in issue or pull request context, skipping ${itemType}`,
+ shouldFail: false,
+ };
}
- }
- `;
- try {
- const response = await github.graphql(query, { owner, repo });
- const actors = response.repository?.suggestedActors?.nodes || [];
- const knownValues = Object.values(AGENT_LOGIN_NAMES);
- const available = [];
- for (const actor of actors) {
- if (actor && actor.login && knownValues.includes(actor.login)) {
- available.push(actor.login);
+ } else {
+ if (!isPRContext) {
+ return {
+ success: false,
+ error: `Target is "triggering" but not running in pull request context, skipping ${itemType}`,
+ shouldFail: false,
+ };
}
}
- return available.sort();
- } catch (e) {
- const msg = e instanceof Error ? e.message : String(e);
- core.debug(`Failed to list available agent logins: ${msg}`);
- return [];
}
- }
- async function findAgent(owner, repo, agentName) {
- const query = `
- query($owner: String!, $repo: String!) {
- repository(owner: $owner, name: $repo) {
- suggestedActors(first: 100, capabilities: CAN_BE_ASSIGNED) {
- nodes {
- ... on Bot {
- id
- login
- __typename
- }
- }
- }
- }
- }
- `;
- try {
- const response = await github.graphql(query, { owner, repo });
- const actors = response.repository.suggestedActors.nodes;
- const loginName = AGENT_LOGIN_NAMES[agentName];
- if (!loginName) {
- core.error(`Unknown agent: ${agentName}. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`);
- return null;
- }
- for (const actor of actors) {
- if (actor.login === loginName) {
- return actor.id;
+ let itemNumber;
+ let contextType;
+ if (target === "*") {
+ const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ if (numberField) {
+ itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
+ if (isNaN(itemNumber) || itemNumber <= 0) {
+ return {
+ success: false,
+ error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ shouldFail: true,
+ };
}
- }
- const available = actors.filter(a => a && a.login && Object.values(AGENT_LOGIN_NAMES).includes(a.login)).map(a => a.login);
- core.warning(`${agentName} coding agent (${loginName}) is not available as an assignee for this repository`);
- if (available.length > 0) {
- core.info(`Available assignable coding agents: ${available.join(", ")}`);
+ contextType = supportsPR && item.item_number ? "issue" : "pull request";
} else {
- core.info("No coding agents are currently assignable in this repository.");
+ return {
+ success: false,
+ error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ shouldFail: true,
+ };
}
- if (agentName === "copilot") {
- core.info(
- "Please visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot"
- );
+ } else if (target !== "triggering") {
+ itemNumber = parseInt(target, 10);
+ if (isNaN(itemNumber) || itemNumber <= 0) {
+ return {
+ success: false,
+ error: `Invalid ${supportsPR ? "issue" : "pull request"} number in target configuration: ${target}`,
+ shouldFail: true,
+ };
}
- return null;
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- core.error(`Failed to find ${agentName} agent: ${errorMessage}`);
- return null;
- }
- }
- async function getIssueDetails(owner, repo, issueNumber) {
- const query = `
- query($owner: String!, $repo: String!, $issueNumber: Int!) {
- repository(owner: $owner, name: $repo) {
- issue(number: $issueNumber) {
- id
- assignees(first: 100) {
- nodes {
- id
- }
- }
- }
+ contextType = supportsPR ? "issue" : "pull request";
+ } else {
+ if (isIssueContext) {
+ if (context.payload.issue) {
+ itemNumber = context.payload.issue.number;
+ contextType = "issue";
+ } else {
+ return {
+ success: false,
+ error: "Issue context detected but no issue found in payload",
+ shouldFail: true,
+ };
+ }
+ } else if (isPRContext) {
+ if (context.payload.pull_request) {
+ itemNumber = context.payload.pull_request.number;
+ contextType = "pull request";
+ } else {
+ return {
+ success: false,
+ error: "Pull request context detected but no pull request found in payload",
+ shouldFail: true,
+ };
}
}
- `;
- try {
- const response = await github.graphql(query, { owner, repo, issueNumber });
- const issue = response.repository.issue;
- if (!issue || !issue.id) {
- core.error("Could not get issue data");
- return null;
- }
- const currentAssignees = issue.assignees.nodes.map(assignee => assignee.id);
+ }
+ if (!itemNumber) {
return {
- issueId: issue.id,
- currentAssignees: currentAssignees,
+ success: false,
+ error: `Could not determine ${supportsPR ? "issue or pull request" : "pull request"} number`,
+ shouldFail: true,
};
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- core.error(`Failed to get issue details: ${errorMessage}`);
- return null;
}
+ return {
+ success: true,
+ number: itemNumber,
+ contextType: contextType || (supportsPR ? "issue" : "pull request"),
+ };
}
- async function assignAgentToIssue(issueId, agentId, currentAssignees, agentName) {
- const actorIds = [agentId];
- for (const assigneeId of currentAssignees) {
- if (assigneeId !== agentId) {
- actorIds.push(assigneeId);
- }
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
}
- const mutation = `
- mutation($assignableId: ID!, $actorIds: [ID!]!) {
- replaceActorsForAssignable(input: {
- assignableId: $assignableId,
- actorIds: $actorIds
- }) {
- __typename
- }
- }
- `;
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ function loadSafeOutputsConfig() {
+ const configPath = "/tmp/gh-aw/safeoutputs/config.json";
try {
- core.info("Using built-in github object for mutation");
- core.debug(`GraphQL mutation with variables: assignableId=${issueId}, actorIds=${JSON.stringify(actorIds)}`);
- const response = await github.graphql(mutation, {
- assignableId: issueId,
- actorIds: actorIds,
- });
- if (response && response.replaceActorsForAssignable && response.replaceActorsForAssignable.__typename) {
- return true;
- } else {
- core.error("Unexpected response from GitHub API");
- return false;
+ if (!fs.existsSync(configPath)) {
+ core.warning(`Config file not found at ${configPath}, using defaults`);
+ return {};
}
+ const configContent = fs.readFileSync(configPath, "utf8");
+ return JSON.parse(configContent);
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- try {
- core.debug(`Raw GraphQL error message: ${errorMessage}`);
- if (error && typeof error === "object") {
- const details = {};
- if (error.errors) details.errors = error.errors;
- if (error.response) details.response = error.response;
- if (error.data) details.data = error.data;
- if (Array.isArray(error.errors)) {
- details.compactMessages = error.errors.map(e => e.message).filter(Boolean);
- }
- const serialized = JSON.stringify(details, (_k, v) => v, 2);
- if (serialized && serialized !== "{}") {
- core.debug(`Raw GraphQL error details: ${serialized}`);
- core.error("Raw GraphQL error details (for troubleshooting):");
- for (const line of serialized.split(/\n/)) {
- if (line.trim()) core.error(line);
- }
- }
- }
- } catch (loggingErr) {
- core.debug(`Failed to serialize GraphQL error details: ${loggingErr instanceof Error ? loggingErr.message : String(loggingErr)}`);
- }
- if (
- errorMessage.includes("Resource not accessible by personal access token") ||
- errorMessage.includes("Resource not accessible by integration") ||
- errorMessage.includes("Insufficient permissions to assign")
- ) {
- core.info("Primary mutation replaceActorsForAssignable forbidden. Attempting fallback addAssigneesToAssignable...");
- try {
- const fallbackMutation = `
- mutation($assignableId: ID!, $assigneeIds: [ID!]!) {
- addAssigneesToAssignable(input: {
- assignableId: $assignableId,
- assigneeIds: $assigneeIds
- }) {
- clientMutationId
- }
- }
- `;
- core.info("Using built-in github object for fallback mutation");
- core.debug(`Fallback GraphQL mutation with variables: assignableId=${issueId}, assigneeIds=[${agentId}]`);
- const fallbackResp = await github.graphql(fallbackMutation, {
- assignableId: issueId,
- assigneeIds: [agentId],
- });
- if (fallbackResp && fallbackResp.addAssigneesToAssignable) {
- core.info(`Fallback succeeded: agent '${agentName}' added via addAssigneesToAssignable.`);
- return true;
- } else {
- core.warning("Fallback mutation returned unexpected response; proceeding with permission guidance.");
- }
- } catch (fallbackError) {
- const fbMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
- core.error(`Fallback addAssigneesToAssignable failed: ${fbMsg}`);
- }
- logPermissionError(agentName);
- } else {
- core.error(`Failed to assign ${agentName}: ${errorMessage}`);
+ core.warning(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
+ return {};
+ }
+ }
+ function getSafeOutputConfig(outputType) {
+ const config = loadSafeOutputsConfig();
+ return config[outputType] || {};
+ }
+ function validateTitle(title, fieldName = "title") {
+ if (title === undefined || title === null) {
+ return { valid: false, error: `${fieldName} is required` };
+ }
+ if (typeof title !== "string") {
+ return { valid: false, error: `${fieldName} must be a string` };
+ }
+ const trimmed = title.trim();
+ if (trimmed.length === 0) {
+ return { valid: false, error: `${fieldName} cannot be empty` };
+ }
+ return { valid: true, value: trimmed };
+ }
+ function validateBody(body, fieldName = "body", required = false) {
+ if (body === undefined || body === null) {
+ if (required) {
+ return { valid: false, error: `${fieldName} is required` };
}
- return false;
+ return { valid: true, value: "" };
+ }
+ if (typeof body !== "string") {
+ return { valid: false, error: `${fieldName} must be a string` };
}
+ return { valid: true, value: body };
}
- function logPermissionError(agentName) {
- core.error(`Failed to assign ${agentName}: Insufficient permissions`);
- core.error("");
- core.error("Assigning Copilot agents requires:");
- core.error(" 1. All four workflow permissions:");
- core.error(" - actions: write");
- core.error(" - contents: write");
- core.error(" - issues: write");
- core.error(" - pull-requests: write");
- core.error("");
- core.error(" 2. A classic PAT with 'repo' scope OR fine-grained PAT with explicit Write permissions above:");
- core.error(" (Fine-grained PATs must grant repository access + write for Issues, Pull requests, Contents, Actions)");
- core.error("");
- core.error(" 3. Repository settings:");
- core.error(" - Actions must have write permissions");
- core.error(" - Go to: Settings > Actions > General > Workflow permissions");
- core.error(" - Select: 'Read and write permissions'");
- core.error("");
- core.error(" 4. Organization/Enterprise settings:");
- core.error(" - Check if your org restricts bot assignments");
- core.error(" - Verify Copilot is enabled for your repository");
- core.error("");
- core.info("For more information, see: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr");
- }
- function generatePermissionErrorSummary() {
- let content = "\n### ⚠️ Permission Requirements\n\n";
- content += "Assigning Copilot agents requires **ALL** of these permissions:\n\n";
- content += "```yaml\n";
- content += "permissions:\n";
- content += " actions: write\n";
- content += " contents: write\n";
- content += " issues: write\n";
- content += " pull-requests: write\n";
- content += "```\n\n";
- content += "**Token capability note:**\n";
- content += "- Current token (PAT or GITHUB_TOKEN) lacks assignee mutation capability for this repository.\n";
- content += "- Both `replaceActorsForAssignable` and fallback `addAssigneesToAssignable` returned FORBIDDEN/Resource not accessible.\n";
- content += "- This typically means bot/user assignment requires an elevated OAuth or GitHub App installation token.\n\n";
- content += "**Recommended remediation paths:**\n";
- content += "1. Create & install a GitHub App with: Issues/Pull requests/Contents/Actions (write) → use installation token in job.\n";
- content += "2. Manual assignment: add the agent through the UI until broader token support is available.\n";
- content += "3. Open a support ticket referencing failing mutation `replaceActorsForAssignable` and repository slug.\n\n";
- content +=
- "**Why this failed:** Fine-grained and classic PATs can update issue title (verified) but not modify assignees in this environment.\n\n";
- content += "📖 Reference: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr (general agent docs)\n";
- return content;
- }
- async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) {
- if (!AGENT_LOGIN_NAMES[agentName]) {
- const error = `Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`;
- core.warning(error);
- return { success: false, error };
+ function validateLabels(labels, allowedLabels = undefined, maxCount = 3) {
+ if (!labels || !Array.isArray(labels)) {
+ return { valid: false, error: "labels must be an array" };
}
- try {
- core.info(`Looking for ${agentName} coding agent...`);
- const agentId = await findAgent(owner, repo, agentName);
- if (!agentId) {
- const error = `${agentName} coding agent is not available for this repository`;
- const available = await getAvailableAgentLogins(owner, repo);
- const enrichedError = available.length > 0 ? `${error} (available agents: ${available.join(", ")})` : error;
- return { success: false, error: enrichedError };
- }
- core.info(`Found ${agentName} coding agent (ID: ${agentId})`);
- core.info("Getting issue details...");
- const issueDetails = await getIssueDetails(owner, repo, issueNumber);
- if (!issueDetails) {
- return { success: false, error: "Failed to get issue details" };
- }
- core.info(`Issue ID: ${issueDetails.issueId}`);
- if (issueDetails.currentAssignees.includes(agentId)) {
- core.info(`${agentName} is already assigned to issue #${issueNumber}`);
- return { success: true };
- }
- core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`);
- const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName);
- if (!success) {
- return { success: false, error: `Failed to assign ${agentName} via GraphQL` };
- }
- core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`);
- return { success: true };
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- return { success: false, error: errorMessage };
+ for (const label of labels) {
+ if (label && typeof label === "string" && label.startsWith("-")) {
+ return { valid: false, error: `Label removal is not permitted. Found line starting with '-': ${label}` };
+ }
+ }
+ let validLabels = labels;
+ if (allowedLabels && allowedLabels.length > 0) {
+ validLabels = labels.filter(label => allowedLabels.includes(label));
}
+ const uniqueLabels = validLabels
+ .filter(label => label != null && label !== false && label !== 0)
+ .map(label => String(label).trim())
+ .filter(label => label)
+ .map(label => sanitizeLabelContent(label))
+ .filter(label => label)
+ .map(label => (label.length > 64 ? label.substring(0, 64) : label))
+ .filter((label, index, arr) => arr.indexOf(label) === index);
+ if (uniqueLabels.length > maxCount) {
+ core.info(`Too many labels (${uniqueLabels.length}), limiting to ${maxCount}`);
+ return { valid: true, value: uniqueLabels.slice(0, maxCount) };
+ }
+ if (uniqueLabels.length === 0) {
+ return { valid: false, error: "No valid labels found after sanitization" };
+ }
+ return { valid: true, value: uniqueLabels };
}
- async function main() {
+ function validateMaxCount(envValue, configDefault, fallbackDefault = 1) {
+ const defaultValue = configDefault !== undefined ? configDefault : fallbackDefault;
+ if (!envValue) {
+ return { valid: true, value: defaultValue };
+ }
+ const parsed = parseInt(envValue, 10);
+ if (isNaN(parsed) || parsed < 1) {
+ return {
+ valid: false,
+ error: `Invalid max value: ${envValue}. Must be a positive integer`,
+ };
+ }
+ return { valid: true, value: parsed };
+ }
+ async function processSafeOutput(config, stagedPreviewOptions) {
+ const {
+ itemType,
+ configKey,
+ displayName,
+ itemTypeName,
+ supportsPR = false,
+ supportsIssue = false,
+ findMultiple = false,
+ envVars,
+ } = config;
const result = loadAgentOutput();
if (!result.success) {
- return;
+ return { success: false, reason: "Agent output not available" };
}
- const assignItems = result.items.filter(item => item.type === "assign_to_agent");
- if (assignItems.length === 0) {
- core.info("No assign_to_agent items found in agent output");
- return;
+ let items;
+ if (findMultiple) {
+ items = result.items.filter(item => item.type === itemType);
+ if (items.length === 0) {
+ core.info(`No ${itemType} items found in agent output`);
+ return { success: false, reason: `No ${itemType} items found` };
+ }
+ core.info(`Found ${items.length} ${itemType} item(s)`);
+ } else {
+ const item = result.items.find(item => item.type === itemType);
+ if (!item) {
+ core.warning(`No ${itemType.replace(/_/g, "-")} item found in agent output`);
+ return { success: false, reason: `No ${itemType} item found` };
+ }
+ items = [item];
+ const itemDetails = getItemDetails(item);
+ if (itemDetails) {
+ core.info(`Found ${itemType.replace(/_/g, "-")} item with ${itemDetails}`);
+ }
}
- core.info(`Found ${assignItems.length} assign_to_agent item(s)`);
if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") {
await generateStagedPreview({
- title: "Assign to Agent",
- description: "The following agent assignments would be made if staged mode was disabled:",
- items: assignItems,
- renderItem: item => {
- let content = `**Issue:** #${item.issue_number}\n`;
- content += `**Agent:** ${item.agent || "copilot"}\n`;
- content += "\n";
- return content;
- },
+ title: stagedPreviewOptions.title,
+ description: stagedPreviewOptions.description,
+ items: items,
+ renderItem: stagedPreviewOptions.renderItem,
});
- return;
+ return { success: false, reason: "Staged mode - preview generated" };
}
- const defaultAgent = process.env.GH_AW_AGENT_DEFAULT?.trim() || "copilot";
- core.info(`Default agent: ${defaultAgent}`);
- const maxCountEnv = process.env.GH_AW_AGENT_MAX_COUNT;
- const maxCount = maxCountEnv ? parseInt(maxCountEnv, 10) : 1;
- if (isNaN(maxCount) || maxCount < 1) {
- core.setFailed(`Invalid max value: ${maxCountEnv}. Must be a positive integer`);
- return;
+ const safeOutputConfig = getSafeOutputConfig(configKey);
+ const allowedEnvValue = envVars.allowed ? process.env[envVars.allowed] : undefined;
+ const allowed = parseAllowedItems(allowedEnvValue) || safeOutputConfig.allowed;
+ if (allowed) {
+ core.info(`Allowed ${itemTypeName}s: ${JSON.stringify(allowed)}`);
+ } else {
+ core.info(`No ${itemTypeName} restrictions - any ${itemTypeName}s are allowed`);
+ }
+ const maxCountEnvValue = envVars.maxCount ? process.env[envVars.maxCount] : undefined;
+ const maxCountResult = validateMaxCount(maxCountEnvValue, safeOutputConfig.max);
+ if (!maxCountResult.valid) {
+ core.setFailed(maxCountResult.error);
+ return { success: false, reason: "Invalid max count configuration" };
}
+ const maxCount = maxCountResult.value;
core.info(`Max count: ${maxCount}`);
- const itemsToProcess = assignItems.slice(0, maxCount);
- if (assignItems.length > maxCount) {
- core.warning(`Found ${assignItems.length} agent assignments, but max is ${maxCount}. Processing first ${maxCount}.`);
- }
- const targetRepoEnv = process.env.GH_AW_TARGET_REPO?.trim();
- let targetOwner = context.repo.owner;
- let targetRepo = context.repo.repo;
- if (targetRepoEnv) {
- const parts = targetRepoEnv.split("/");
- if (parts.length === 2) {
- targetOwner = parts[0];
- targetRepo = parts[1];
- core.info(`Using target repository: ${targetOwner}/${targetRepo}`);
+ const target = envVars.target ? process.env[envVars.target] || "triggering" : "triggering";
+ core.info(`${displayName} target configuration: ${target}`);
+ if (findMultiple) {
+ return {
+ success: true,
+ items: items,
+ config: {
+ allowed,
+ maxCount,
+ target,
+ },
+ };
+ }
+ const item = items[0];
+ const targetResult = resolveTarget({
+ targetConfig: target,
+ item: item,
+ context,
+ itemType: itemTypeName,
+ supportsPR: supportsPR || supportsIssue,
+ });
+ if (!targetResult.success) {
+ if (targetResult.shouldFail) {
+ core.setFailed(targetResult.error);
} else {
- core.warning(`Invalid target-repo format: ${targetRepoEnv}. Expected owner/repo. Using current repository.`);
+ core.info(targetResult.error);
}
+ return { success: false, reason: targetResult.error };
}
- const agentCache = {};
- const results = [];
- for (const item of itemsToProcess) {
- const issueNumber = typeof item.issue_number === "number" ? item.issue_number : parseInt(String(item.issue_number), 10);
- const agentName = item.agent || defaultAgent;
- if (isNaN(issueNumber) || issueNumber <= 0) {
- core.error(`Invalid issue_number: ${item.issue_number}`);
- continue;
- }
- if (!AGENT_LOGIN_NAMES[agentName]) {
- core.warning(`Agent "${agentName}" is not supported. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`);
- results.push({
- issue_number: issueNumber,
- agent: agentName,
- success: false,
- error: `Unsupported agent: ${agentName}`,
- });
- continue;
- }
- try {
- let agentId = agentCache[agentName];
- if (!agentId) {
- core.info(`Looking for ${agentName} coding agent...`);
- agentId = await findAgent(targetOwner, targetRepo, agentName);
- if (!agentId) {
- throw new Error(`${agentName} coding agent is not available for this repository`);
+ return {
+ success: true,
+ item: item,
+ config: {
+ allowed,
+ maxCount,
+ target,
+ },
+ targetResult: {
+ number: targetResult.number,
+ contextType: targetResult.contextType,
+ },
+ };
+ }
+ function getItemDetails(item) {
+ if (item.labels && Array.isArray(item.labels)) {
+ return `${item.labels.length} labels`;
+ }
+ if (item.reviewers && Array.isArray(item.reviewers)) {
+ return `${item.reviewers.length} reviewers`;
+ }
+ return null;
+ }
+ function sanitizeItems(items) {
+ return items
+ .filter(item => item != null && item !== false && item !== 0)
+ .map(item => String(item).trim())
+ .filter(item => item)
+ .filter((item, index, arr) => arr.indexOf(item) === index);
+ }
+ function filterByAllowed(items, allowed) {
+ if (!allowed || allowed.length === 0) {
+ return items;
+ }
+ return items.filter(item => allowed.includes(item));
+ }
+ function limitToMaxCount(items, maxCount) {
+ if (items.length > maxCount) {
+ core.info(`Too many items (${items.length}), limiting to ${maxCount}`);
+ return items.slice(0, maxCount);
+ }
+ return items;
+ }
+ function processItems(rawItems, allowed, maxCount) {
+ const filtered = filterByAllowed(rawItems, allowed);
+ const sanitized = sanitizeItems(filtered);
+ return limitToMaxCount(sanitized, maxCount);
+ }
+ async function main() {
+ const result = await processSafeOutput(
+ {
+ itemType: "assign_to_user",
+ configKey: "assign_to_user",
+ displayName: "Assignees",
+ itemTypeName: "user assignment",
+ supportsPR: false,
+ supportsIssue: true,
+ envVars: {
+ allowed: "GH_AW_ASSIGNEES_ALLOWED",
+ maxCount: "GH_AW_ASSIGNEES_MAX_COUNT",
+ target: "GH_AW_ASSIGNEES_TARGET",
+ },
+ },
+ {
+ title: "Assign to User",
+ description: "The following user assignments would be made if staged mode was disabled:",
+ renderItem: item => {
+ let content = "";
+ if (item.issue_number) {
+ content += `**Target Issue:** #${item.issue_number}\n\n`;
+ } else {
+ content += `**Target:** Current issue\n\n`;
}
- agentCache[agentName] = agentId;
- core.info(`Found ${agentName} coding agent (ID: ${agentId})`);
- }
- core.info("Getting issue details...");
- const issueDetails = await getIssueDetails(targetOwner, targetRepo, issueNumber);
- if (!issueDetails) {
- throw new Error("Failed to get issue details");
- }
- core.info(`Issue ID: ${issueDetails.issueId}`);
- if (issueDetails.currentAssignees.includes(agentId)) {
- core.info(`${agentName} is already assigned to issue #${issueNumber}`);
- results.push({
- issue_number: issueNumber,
- agent: agentName,
- success: true,
- });
- continue;
- }
- core.info(`Assigning ${agentName} coding agent to issue #${issueNumber}...`);
- const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName);
- if (!success) {
- throw new Error(`Failed to assign ${agentName} via GraphQL`);
- }
- core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`);
- results.push({
- issue_number: issueNumber,
- agent: agentName,
- success: true,
- });
- } catch (error) {
- let errorMessage = error instanceof Error ? error.message : String(error);
- if (errorMessage.includes("coding agent is not available for this repository")) {
- try {
- const available = await getAvailableAgentLogins(targetOwner, targetRepo);
- if (available.length > 0) {
- errorMessage += ` (available agents: ${available.join(", ")})`;
- }
- } catch (e) {
- core.debug("Failed to enrich unavailable agent message with available list");
+ if (item.assignees && item.assignees.length > 0) {
+ content += `**Users to assign:** ${item.assignees.join(", ")}\n\n`;
+ } else if (item.assignee) {
+ content += `**User to assign:** ${item.assignee}\n\n`;
}
- }
- core.error(`Failed to assign agent "${agentName}" to issue #${issueNumber}: ${errorMessage}`);
- results.push({
- issue_number: issueNumber,
- agent: agentName,
- success: false,
- error: errorMessage,
- });
+ return content;
+ },
}
+ );
+ if (!result.success) {
+ return;
}
- const successCount = results.filter(r => r.success).length;
- const failureCount = results.filter(r => !r.success).length;
- let summaryContent = "## Agent Assignment\n\n";
- if (successCount > 0) {
- summaryContent += `✅ Successfully assigned ${successCount} agent(s):\n\n`;
- for (const result of results.filter(r => r.success)) {
- summaryContent += `- Issue #${result.issue_number} → Agent: ${result.agent}\n`;
- }
- summaryContent += "\n";
+ const { item: assignItem, config, targetResult } = result;
+ if (!config || !targetResult || targetResult.number === undefined) {
+ core.setFailed("Internal error: config, targetResult, or targetResult.number is undefined");
+ return;
}
- if (failureCount > 0) {
- summaryContent += `❌ Failed to assign ${failureCount} agent(s):\n\n`;
- for (const result of results.filter(r => !r.success)) {
- summaryContent += `- Issue #${result.issue_number} → Agent: ${result.agent}: ${result.error}\n`;
- }
- const hasPermissionError = results.some(
- r => !r.success && r.error && (r.error.includes("Resource not accessible") || r.error.includes("Insufficient permissions"))
- );
- if (hasPermissionError) {
- summaryContent += generatePermissionErrorSummary();
+ const { allowed: allowedAssignees, maxCount } = config;
+ const issueNumber = targetResult.number;
+ let requestedAssignees = [];
+ if (assignItem.assignees && Array.isArray(assignItem.assignees)) {
+ requestedAssignees = assignItem.assignees;
+ } else if (assignItem.assignee) {
+ requestedAssignees = [assignItem.assignee];
+ }
+ core.info(`Requested assignees: ${JSON.stringify(requestedAssignees)}`);
+ const uniqueAssignees = processItems(requestedAssignees, allowedAssignees, maxCount);
+ if (uniqueAssignees.length === 0) {
+ core.info("No assignees to add");
+ core.setOutput("assigned_users", "");
+ await core.summary
+ .addRaw(
+ `
+ ## User Assignment
+ No users were assigned (no valid assignees found in agent output).
+ `
+ )
+ .write();
+ return;
+ }
+ core.info(`Assigning ${uniqueAssignees.length} users to issue #${issueNumber}: ${JSON.stringify(uniqueAssignees)}`);
+ try {
+ const targetRepoEnv = process.env.GH_AW_TARGET_REPO_SLUG?.trim();
+ let targetOwner = context.repo.owner;
+ let targetRepo = context.repo.repo;
+ if (targetRepoEnv) {
+ const parts = targetRepoEnv.split("/");
+ if (parts.length === 2) {
+ targetOwner = parts[0];
+ targetRepo = parts[1];
+ core.info(`Using target repository: ${targetOwner}/${targetRepo}`);
+ }
}
+ await github.rest.issues.addAssignees({
+ owner: targetOwner,
+ repo: targetRepo,
+ issue_number: issueNumber,
+ assignees: uniqueAssignees,
+ });
+ core.info(`Successfully assigned ${uniqueAssignees.length} user(s) to issue #${issueNumber}`);
+ core.setOutput("assigned_users", uniqueAssignees.join("\n"));
+ const assigneesListMarkdown = uniqueAssignees.map(assignee => `- \`${assignee}\``).join("\n");
+ await core.summary
+ .addRaw(
+ `
+ ## User Assignment
+ Successfully assigned ${uniqueAssignees.length} user(s) to issue #${issueNumber}:
+ ${assigneesListMarkdown}
+ `
+ )
+ .write();
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ core.error(`Failed to assign users: ${errorMessage}`);
+ core.setFailed(`Failed to assign users: ${errorMessage}`);
}
- await core.summary.addRaw(summaryContent).write();
- const assignedAgents = results
- .filter(r => r.success)
- .map(r => `${r.issue_number}:${r.agent}`)
- .join("\n");
- core.setOutput("assigned_agents", assignedAgents);
- if (failureCount > 0) {
- core.setFailed(`Failed to assign ${failureCount} agent(s)`);
- }
- }
- (async () => {
- await main();
- })();
+ }
+ await main();
conclusion:
needs:
- agent
- activation
- - assign_to_agent
+ - assign_to_user
if: (always()) && (needs.agent.result != 'skipped')
runs-on: ubuntu-slim
permissions:
@@ -5147,7 +5156,7 @@ jobs:
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_NAME: "Dev"
- WORKFLOW_DESCRIPTION: "Find issues with \"[deps]\" in title and assign to Copilot agent"
+ WORKFLOW_DESCRIPTION: "Find issues with \"[deps]\" in title and assign to mrjf"
with:
script: |
const fs = require('fs');
diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md
index 1abbcb42699..f1d27d2c76c 100644
--- a/.github/workflows/dev.md
+++ b/.github/workflows/dev.md
@@ -2,23 +2,23 @@
on:
workflow_dispatch:
name: Dev
-description: Find issues with "[deps]" in title and assign to Copilot agent
+description: Find issues with "[deps]" in title and assign to mrjf
timeout-minutes: 5
strict: false
engine: claude
permissions:
contents: read
- issues: read
+ issues: write
tools:
github:
toolsets: [repos, issues]
safe-outputs:
- assign-to-agent:
- name: copilot
+ assign-to-user:
+ allowed: [mrjf]
---
# Dependency Issue Assignment
-Find an open issue in this repository with "[deps]" in the title and assign it to the Copilot agent for resolution.
+Find an open issue in this repository with "[deps]" in the title and assign it to mrjf for resolution.
## Task
@@ -27,16 +27,16 @@ Find an open issue in this repository with "[deps]" in the title and assign it t
is:issue is:open "[deps]" in:title repo:${{ github.repository }}
```
-2. **Filter out assigned issues**: Skip any issues that already have Copilot as an assignee.
+2. **Filter out assigned issues**: Skip any issues that already have mrjf as an assignee.
-3. **Assign to Copilot**: For the first suitable issue found, use the `assign_to_agent` tool to assign it to the Copilot agent.
+3. **Assign to mrjf**: For the first suitable issue found, use the `assign_to_user` tool to assign it to mrjf.
**Agent Output Format:**
```json
{
- "type": "assign_to_agent",
+ "type": "assign_to_user",
"issue_number": ,
- "agent": "copilot"
+ "assignee": "mrjf"
}
```
From c28b4055599ac858a3c76f084b09acd6e589db85 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 04:36:21 +0000
Subject: [PATCH 08/11] Fix assign-to-user job not being invoked on
workflow_dispatch
The assign_to_user job requires `target: "*"` when triggered via
workflow_dispatch since there's no issue context. Without this, the
job condition included `(github.event.issue.number)` which evaluates
to falsy on workflow_dispatch events.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/dev.lock.yml | 6 ++++--
.github/workflows/dev.md | 1 +
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index 951e16694e8..8fccfa0904c 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -38,6 +38,7 @@
# safe-outputs:
# assign-to-user:
# allowed: [mrjf]
+# target: "*"
# ```
#
# Job Dependency Graph:
@@ -4095,8 +4096,8 @@ jobs:
- agent
- detection
if: >
- ((((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_user'))) &&
- (github.event.issue.number)) && (needs.detection.outputs.success == 'true')
+ (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'assign_to_user'))) &&
+ (needs.detection.outputs.success == 'true')
runs-on: ubuntu-slim
permissions:
contents: read
@@ -4123,6 +4124,7 @@ jobs:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_ASSIGNEES_ALLOWED: "mrjf"
GH_AW_ASSIGNEES_MAX_COUNT: 1
+ GH_AW_ASSIGNEES_TARGET: "*"
GH_AW_WORKFLOW_NAME: "Dev"
GH_AW_ENGINE_ID: "claude"
with:
diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md
index f1d27d2c76c..6bd3931e04b 100644
--- a/.github/workflows/dev.md
+++ b/.github/workflows/dev.md
@@ -15,6 +15,7 @@ tools:
safe-outputs:
assign-to-user:
allowed: [mrjf]
+ target: "*"
---
# Dependency Issue Assignment
From b90b51a67e54f37725175be10dcf3e5630f21b11 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 04:50:15 +0000
Subject: [PATCH 09/11] Fix resolveTarget to handle issue_number field for
target="*"
When target="*", the resolveTarget function now checks for
item.issue_number in addition to item.item_number and
item.pull_request_number. This fixes assign_to_user failing when
the agent output uses the standard issue_number field.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/dev.lock.yml | 10 ++++++----
.github/workflows/issue-classifier.lock.yml | 10 ++++++----
.github/workflows/issue-triage-agent.lock.yml | 10 ++++++----
.github/workflows/poem-bot.lock.yml | 10 ++++++----
.github/workflows/smoke-claude.lock.yml | 10 ++++++----
.github/workflows/smoke-codex.lock.yml | 10 ++++++----
.github/workflows/smoke-copilot-no-firewall.lock.yml | 10 ++++++----
.github/workflows/smoke-copilot-playwright.lock.yml | 10 ++++++----
.github/workflows/smoke-copilot.lock.yml | 10 ++++++----
pkg/workflow/js/safe_output_helpers.cjs | 12 +++++++-----
10 files changed, 61 insertions(+), 41 deletions(-)
diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index 8fccfa0904c..1e21511903f 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -4243,21 +4243,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml
index f8055825073..a6f19a3183e 100644
--- a/.github/workflows/issue-classifier.lock.yml
+++ b/.github/workflows/issue-classifier.lock.yml
@@ -1145,21 +1145,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml
index e8d2137cbe2..f9ff503d8b0 100644
--- a/.github/workflows/issue-triage-agent.lock.yml
+++ b/.github/workflows/issue-triage-agent.lock.yml
@@ -333,21 +333,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml
index 779ffeb5e05..7860ae2942c 100644
--- a/.github/workflows/poem-bot.lock.yml
+++ b/.github/workflows/poem-bot.lock.yml
@@ -1949,21 +1949,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml
index f12c9549edd..0d38b036ad3 100644
--- a/.github/workflows/smoke-claude.lock.yml
+++ b/.github/workflows/smoke-claude.lock.yml
@@ -1536,21 +1536,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml
index b648814d044..1e83c4fe528 100644
--- a/.github/workflows/smoke-codex.lock.yml
+++ b/.github/workflows/smoke-codex.lock.yml
@@ -1415,21 +1415,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml
index fdf1881c9df..92c1230a871 100644
--- a/.github/workflows/smoke-copilot-no-firewall.lock.yml
+++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml
@@ -1427,21 +1427,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml
index cd5f7a2da24..449c658f57f 100644
--- a/.github/workflows/smoke-copilot-playwright.lock.yml
+++ b/.github/workflows/smoke-copilot-playwright.lock.yml
@@ -1475,21 +1475,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml
index 1d8267f1b44..1be2fdb002e 100644
--- a/.github/workflows/smoke-copilot.lock.yml
+++ b/.github/workflows/smoke-copilot.lock.yml
@@ -1406,21 +1406,23 @@ jobs:
let itemNumber;
let contextType;
if (target === "*") {
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
diff --git a/pkg/workflow/js/safe_output_helpers.cjs b/pkg/workflow/js/safe_output_helpers.cjs
index 7da6cc7d599..a234ec893ff 100644
--- a/pkg/workflow/js/safe_output_helpers.cjs
+++ b/pkg/workflow/js/safe_output_helpers.cjs
@@ -93,23 +93,25 @@ function resolveTarget(params) {
let contextType;
if (target === "*") {
- // Use item_number or pull_request_number from item
- const numberField = supportsPR ? item.item_number || item.pull_request_number : item.pull_request_number;
+ // Use item_number, issue_number, or pull_request_number from item
+ const numberField = supportsPR
+ ? item.item_number || item.issue_number || item.pull_request_number
+ : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);
if (isNaN(itemNumber) || itemNumber <= 0) {
return {
success: false,
- error: `Invalid ${supportsPR ? "item_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
+ error: `Invalid ${supportsPR ? "item_number/issue_number/pull_request_number" : "pull_request_number"} specified: ${numberField}`,
shouldFail: true,
};
}
- contextType = supportsPR && item.item_number ? "issue" : "pull request";
+ contextType = supportsPR && (item.item_number || item.issue_number) ? "issue" : "pull request";
} else {
return {
success: false,
- error: `Target is "*" but no ${supportsPR ? "item_number" : "pull_request_number"} specified in ${itemType} item`,
+ error: `Target is "*" but no ${supportsPR ? "item_number/issue_number" : "pull_request_number"} specified in ${itemType} item`,
shouldFail: true,
};
}
From 5835830b09b5e177bddb0d35fb798ebc31c5c5ca Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 3 Dec 2025 04:56:14 +0000
Subject: [PATCH 10/11] Update dev.md to pick any open unassigned issue
Changed from searching for specific "[deps]" issues to searching for
any open issue that isn't already assigned to mrjf. This makes the
workflow more general and useful for testing assign-to-user.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/dev.lock.yml | 34 +++++++++++++++++++---------------
.github/workflows/dev.md | 16 +++++++++-------
2 files changed, 28 insertions(+), 22 deletions(-)
diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml
index 1e21511903f..9d03d9b2312 100644
--- a/.github/workflows/dev.lock.yml
+++ b/.github/workflows/dev.lock.yml
@@ -18,14 +18,14 @@
# gh aw compile
# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
#
-# Find issues with "[deps]" in title and assign to mrjf
+# Find an open issue and assign it to mrjf
#
# Original Frontmatter:
# ```yaml
# on:
# workflow_dispatch:
# name: Dev
-# description: Find issues with "[deps]" in title and assign to mrjf
+# description: Find an open issue and assign it to mrjf
# timeout-minutes: 5
# strict: false
# engine: claude
@@ -60,20 +60,22 @@
#
# Original Prompt:
# ```markdown
-# # Dependency Issue Assignment
+# # Issue Assignment
#
-# Find an open issue in this repository with "[deps]" in the title and assign it to mrjf for resolution.
+# Find an open issue in this repository and assign it to mrjf for resolution.
#
# ## Task
#
-# 1. **Search for issues**: Use GitHub search to find open issues with "[deps]" in the title:
+# 1. **Search for issues**: Use GitHub search to find open issues in this repository:
# ```
-# is:issue is:open "[deps]" in:title repo:${{ github.repository }}
+# is:issue is:open repo:${{ github.repository }}
# ```
#
# 2. **Filter out assigned issues**: Skip any issues that already have mrjf as an assignee.
#
-# 3. **Assign to mrjf**: For the first suitable issue found, use the `assign_to_user` tool to assign it to mrjf.
+# 3. **Pick an issue**: Select the first suitable unassigned issue found.
+#
+# 4. **Assign to mrjf**: Use the `assign_to_user` tool to assign the selected issue to mrjf.
#
# **Agent Output Format:**
# ```json
@@ -84,7 +86,7 @@
# }
# ```
#
-# If no suitable issues are found, output a message indicating that no "[deps]" issues are available for assignment.
+# If no suitable issues are found, output a noop message indicating that no unassigned issues are available.
# ```
#
# Pinned GitHub Actions:
@@ -1650,20 +1652,22 @@ jobs:
PROMPT_DIR="$(dirname "$GH_AW_PROMPT")"
mkdir -p "$PROMPT_DIR"
cat << 'PROMPT_EOF' | envsubst > "$GH_AW_PROMPT"
- # Dependency Issue Assignment
+ # Issue Assignment
- Find an open issue in this repository with "[deps]" in the title and assign it to mrjf for resolution.
+ Find an open issue in this repository and assign it to mrjf for resolution.
## Task
- 1. **Search for issues**: Use GitHub search to find open issues with "[deps]" in the title:
+ 1. **Search for issues**: Use GitHub search to find open issues in this repository:
```
- is:issue is:open "[deps]" in:title repo:${GH_AW_GITHUB_REPOSITORY}
+ is:issue is:open repo:${GH_AW_GITHUB_REPOSITORY}
```
2. **Filter out assigned issues**: Skip any issues that already have mrjf as an assignee.
- 3. **Assign to mrjf**: For the first suitable issue found, use the `assign_to_user` tool to assign it to mrjf.
+ 3. **Pick an issue**: Select the first suitable unassigned issue found.
+
+ 4. **Assign to mrjf**: Use the `assign_to_user` tool to assign the selected issue to mrjf.
**Agent Output Format:**
```json
@@ -1674,7 +1678,7 @@ jobs:
}
```
- If no suitable issues are found, output a message indicating that no "[deps]" issues are available for assignment.
+ If no suitable issues are found, output a noop message indicating that no unassigned issues are available.
PROMPT_EOF
- name: Append XPIA security instructions to prompt
@@ -5160,7 +5164,7 @@ jobs:
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_NAME: "Dev"
- WORKFLOW_DESCRIPTION: "Find issues with \"[deps]\" in title and assign to mrjf"
+ WORKFLOW_DESCRIPTION: "Find an open issue and assign it to mrjf"
with:
script: |
const fs = require('fs');
diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md
index 6bd3931e04b..33c95998c93 100644
--- a/.github/workflows/dev.md
+++ b/.github/workflows/dev.md
@@ -2,7 +2,7 @@
on:
workflow_dispatch:
name: Dev
-description: Find issues with "[deps]" in title and assign to mrjf
+description: Find an open issue and assign it to mrjf
timeout-minutes: 5
strict: false
engine: claude
@@ -17,20 +17,22 @@ safe-outputs:
allowed: [mrjf]
target: "*"
---
-# Dependency Issue Assignment
+# Issue Assignment
-Find an open issue in this repository with "[deps]" in the title and assign it to mrjf for resolution.
+Find an open issue in this repository and assign it to mrjf for resolution.
## Task
-1. **Search for issues**: Use GitHub search to find open issues with "[deps]" in the title:
+1. **Search for issues**: Use GitHub search to find open issues in this repository:
```
- is:issue is:open "[deps]" in:title repo:${{ github.repository }}
+ is:issue is:open repo:${{ github.repository }}
```
2. **Filter out assigned issues**: Skip any issues that already have mrjf as an assignee.
-3. **Assign to mrjf**: For the first suitable issue found, use the `assign_to_user` tool to assign it to mrjf.
+3. **Pick an issue**: Select the first suitable unassigned issue found.
+
+4. **Assign to mrjf**: Use the `assign_to_user` tool to assign the selected issue to mrjf.
**Agent Output Format:**
```json
@@ -41,4 +43,4 @@ Find an open issue in this repository with "[deps]" in the title and assign it t
}
```
-If no suitable issues are found, output a message indicating that no "[deps]" issues are available for assignment.
\ No newline at end of file
+If no suitable issues are found, output a noop message indicating that no unassigned issues are available.
\ No newline at end of file
From f1161df25a9dce3fa6920e7b4d69df0a1a10eb66 Mon Sep 17 00:00:00 2001
From: Peli de Halleux
Date: Wed, 3 Dec 2025 05:24:24 +0000
Subject: [PATCH 11/11] Fix: Update setup-node action version and streamline
numberField assignment in safe_output_helpers
---
.github/workflows/daily-team-status.lock.yml | 8 ++++----
pkg/workflow/js/safe_output_helpers.cjs | 4 +---
2 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml
index 3e357edc72f..9438843340e 100644
--- a/.github/workflows/daily-team-status.lock.yml
+++ b/.github/workflows/daily-team-status.lock.yml
@@ -193,8 +193,8 @@
# https://github.com/actions/download-artifact/commit/018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
# - actions/github-script@v8 (ed597411d8f924073f98dfc5c65a23a2325f34cd)
# https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd
-# - actions/setup-node@v6 (2028fbc5c25fe9cf00d9f06a71cc4710d4507903)
-# https://github.com/actions/setup-node/commit/2028fbc5c25fe9cf00d9f06a71cc4710d4507903
+# - actions/setup-node@v6 (395ad3262231945c25e8478fd5baf05154b1d79f)
+# https://github.com/actions/setup-node/commit/395ad3262231945c25e8478fd5baf05154b1d79f
# - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4)
# https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4
@@ -405,7 +405,7 @@ jobs:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
- name: Setup Node.js
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
node-version: '24'
package-manager-cache: false
@@ -5873,7 +5873,7 @@ jobs:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
- name: Setup Node.js
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
node-version: '24'
package-manager-cache: false
diff --git a/pkg/workflow/js/safe_output_helpers.cjs b/pkg/workflow/js/safe_output_helpers.cjs
index a234ec893ff..6d3fbc94c44 100644
--- a/pkg/workflow/js/safe_output_helpers.cjs
+++ b/pkg/workflow/js/safe_output_helpers.cjs
@@ -94,9 +94,7 @@ function resolveTarget(params) {
if (target === "*") {
// Use item_number, issue_number, or pull_request_number from item
- const numberField = supportsPR
- ? item.item_number || item.issue_number || item.pull_request_number
- : item.pull_request_number;
+ const numberField = supportsPR ? item.item_number || item.issue_number || item.pull_request_number : item.pull_request_number;
if (numberField) {
itemNumber = typeof numberField === "number" ? numberField : parseInt(String(numberField), 10);