Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/daily-assign-issue-to-user.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 33 additions & 1 deletion .github/workflows/smoke-codex.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions pkg/workflow/compiler_safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,32 @@ var handlerRegistry = map[string]handlerBuilder{
}
return builder.Build()
},
"assign_to_user": func(cfg *SafeOutputsConfig) map[string]any {
if cfg.AssignToUser == nil {
return nil
}
c := cfg.AssignToUser
return newHandlerConfigBuilder().
AddIfPositive("max", c.Max).
AddStringSlice("allowed", c.Allowed).
AddIfNotEmpty("target", c.Target).
AddIfNotEmpty("target-repo", c.TargetRepoSlug).
AddStringSlice("allowed_repos", c.AllowedRepos).
Build()
},
"unassign_from_user": func(cfg *SafeOutputsConfig) map[string]any {
if cfg.UnassignFromUser == nil {
return nil
}
c := cfg.UnassignFromUser
return newHandlerConfigBuilder().
AddIfPositive("max", c.Max).
AddStringSlice("allowed", c.Allowed).
AddIfNotEmpty("target", c.Target).
AddIfNotEmpty("target-repo", c.TargetRepoSlug).
AddStringSlice("allowed_repos", c.AllowedRepos).
Build()
},
"create_project_status_update": func(cfg *SafeOutputsConfig) map[string]any {
if cfg.CreateProjectStatusUpdates == nil {
return nil
Expand Down
151 changes: 151 additions & 0 deletions pkg/workflow/compiler_safe_outputs_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -870,3 +870,154 @@ func TestCreatePullRequestBaseBranch(t *testing.T) {
})
}
}

// TestHandlerConfigAssignToUser tests assign_to_user configuration
func TestHandlerConfigAssignToUser(t *testing.T) {
compiler := NewCompiler()

workflowData := &WorkflowData{
Name: "Test Workflow",
SafeOutputs: &SafeOutputsConfig{
AssignToUser: &AssignToUserConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: 5,
},
SafeOutputTargetConfig: SafeOutputTargetConfig{
Target: "issues",
TargetRepoSlug: "org/target-repo",
AllowedRepos: []string{"org/repo1", "org/repo2"},
},
Allowed: []string{"user1", "user2", "copilot"},
},
},
}

var steps []string
compiler.addHandlerManagerConfigEnvVar(&steps, workflowData)

// Extract and validate JSON
for _, step := range steps {
if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") {
parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ")
if len(parts) == 2 {
jsonStr := strings.TrimSpace(parts[1])
jsonStr = strings.Trim(jsonStr, "\"")
jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"")

var config map[string]map[string]any
err := json.Unmarshal([]byte(jsonStr), &config)
require.NoError(t, err, "Handler config JSON should be valid")

assignConfig, ok := config["assign_to_user"]
require.True(t, ok, "Should have assign_to_user handler")

// Check max
max, ok := assignConfig["max"]
require.True(t, ok, "Should have max field")
assert.InDelta(t, 5.0, max, 0.001, "Max should be 5")

// Check allowed users
allowed, ok := assignConfig["allowed"]
require.True(t, ok, "Should have allowed field")
allowedSlice, ok := allowed.([]any)
require.True(t, ok, "Allowed should be an array")
assert.Len(t, allowedSlice, 3, "Should have 3 allowed users")
assert.Equal(t, "user1", allowedSlice[0])
assert.Equal(t, "user2", allowedSlice[1])
assert.Equal(t, "copilot", allowedSlice[2])

// Check target
target, ok := assignConfig["target"]
require.True(t, ok, "Should have target field")
assert.Equal(t, "issues", target)

// Check target-repo
targetRepo, ok := assignConfig["target-repo"]
require.True(t, ok, "Should have target-repo field")
assert.Equal(t, "org/target-repo", targetRepo)

// Check allowed_repos
allowedRepos, ok := assignConfig["allowed_repos"]
require.True(t, ok, "Should have allowed_repos field")
allowedReposSlice, ok := allowedRepos.([]any)
require.True(t, ok, "Allowed repos should be an array")
assert.Len(t, allowedReposSlice, 2, "Should have 2 allowed repos")
}
}
}
}

// TestHandlerConfigUnassignFromUser tests unassign_from_user configuration
func TestHandlerConfigUnassignFromUser(t *testing.T) {
compiler := NewCompiler()

workflowData := &WorkflowData{
Name: "Test Workflow",
SafeOutputs: &SafeOutputsConfig{
UnassignFromUser: &UnassignFromUserConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{
Max: 10,
},
SafeOutputTargetConfig: SafeOutputTargetConfig{
Target: "issues",
TargetRepoSlug: "org/target-repo",
AllowedRepos: []string{"org/repo1"},
},
Allowed: []string{"githubactionagent", "bot-user"},
},
},
}

var steps []string
compiler.addHandlerManagerConfigEnvVar(&steps, workflowData)

// Extract and validate JSON
for _, step := range steps {
if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") {
parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ")
if len(parts) == 2 {
jsonStr := strings.TrimSpace(parts[1])
jsonStr = strings.Trim(jsonStr, "\"")
jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"")

var config map[string]map[string]any
err := json.Unmarshal([]byte(jsonStr), &config)
require.NoError(t, err, "Handler config JSON should be valid")

unassignConfig, ok := config["unassign_from_user"]
require.True(t, ok, "Should have unassign_from_user handler")

// Check max
max, ok := unassignConfig["max"]
require.True(t, ok, "Should have max field")
assert.InDelta(t, 10.0, max, 0.001, "Max should be 10")

// Check allowed users
allowed, ok := unassignConfig["allowed"]
require.True(t, ok, "Should have allowed field")
allowedSlice, ok := allowed.([]any)
require.True(t, ok, "Allowed should be an array")
assert.Len(t, allowedSlice, 2, "Should have 2 allowed users")
assert.Equal(t, "githubactionagent", allowedSlice[0])
assert.Equal(t, "bot-user", allowedSlice[1])

// Check target
target, ok := unassignConfig["target"]
require.True(t, ok, "Should have target field")
assert.Equal(t, "issues", target)

// Check target-repo
targetRepo, ok := unassignConfig["target-repo"]
require.True(t, ok, "Should have target-repo field")
assert.Equal(t, "org/target-repo", targetRepo)

// Check allowed_repos
allowedRepos, ok := unassignConfig["allowed_repos"]
require.True(t, ok, "Should have allowed_repos field")
allowedReposSlice, ok := allowedRepos.([]any)
require.True(t, ok, "Allowed repos should be an array")
assert.Len(t, allowedReposSlice, 1, "Should have 1 allowed repo")
}
}
}
}
32 changes: 32 additions & 0 deletions pkg/workflow/js/safe_outputs_tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,38 @@
"additionalProperties": false
}
},
{
"name": "unassign_from_user",
"description": "Remove one or more assignees from an issue. Use this to unassign users when work is being reassigned or removed from their queue.",
"inputSchema": {
"type": "object",
"properties": {
"issue_number": {
"type": [
"number",
"string"
],
"description": "Issue number to unassign users from. This is the numeric ID from the GitHub URL (e.g., 543 in github.com/owner/repo/issues/543). If omitted, uses the issue that triggered this workflow."
},
"assignees": {
"type": "array",
"items": {
"type": "string"
},
"description": "GitHub usernames to unassign from the issue (e.g., ['octocat', 'mona'])."
},
"assignee": {
"type": "string",
"description": "Single GitHub username to unassign. Use 'assignees' array for multiple users."
},
"repo": {
"type": "string",
"description": "Target repository in 'owner/repo' format. If omitted, uses the current repository. Must be in allowed-repos list if specified."
}
},
"additionalProperties": false
}
},
{
"name": "update_issue",
"description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section.",
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/safe_outputs_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ func TestGetSafeOutputsToolsJSON(t *testing.T) {
"assign_milestone",
"assign_to_agent",
"assign_to_user",
"unassign_from_user",
"update_issue",
"update_pull_request",
"push_to_pull_request_branch",
Expand Down
Loading