Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Feb 12, 2026

Implements unassign-from-user safe output to remove assignees from issues, completing the symmetry with assign-to-user (similar to existing add-labels/remove-labels pattern). Enables workflows to replace assignees by unassigning current users before assigning new ones.

Changes

Backend (Go)

  • pkg/workflow/unassign_from_user.go: Parser and config struct with BaseSafeOutputConfig, SafeOutputTargetConfig, and Allowed filter
  • pkg/workflow/safe_outputs_config_generation.go: Config serialization, enabled tools tracking, and cross-repository permission logic
  • pkg/parser/schemas/main_workflow_schema.json: Schema validation for unassign-from-user property

Handler (JavaScript)

MCP Integration

  • actions/setup/js/safe_outputs_tools.json: Tool schema with assignee/assignees, issue_number, and repo fields

Testing

  • actions/setup/js/unassign_from_user.test.cjs: Comprehensive test suite with 11 tests covering all handler functionality
  • pkg/cli/workflows/test-unassign-from-user.md: Integration test workflow
  • .github/workflows/smoke-codex.md: Added unassign-from-user to smoke test workflow with fictitious user githubactionagent for ongoing validation

Configuration

safe-outputs:
  unassign-from-user:
    max: 3                          # Max operations per run
    allowed: [user1, user2]         # Optional allowlist
    target: "*"                     # Any issue, "triggering", or specific number
    target-repo: "owner/repo"       # Optional default target
    allowed-repos: ["owner/other"]  # Cross-repo operations

Usage

// Remove single assignee
unassign_from_user({ issue_number: 123, assignee: "olduser" })

// Remove multiple assignees
unassign_from_user({ 
  issue_number: 456, 
  assignees: ["user1", "user2"],
  repo: "owner/other-repo"  // Cross-repo support
})

Enables stale-issue re-triage: unassign current user, assign AI-suggested user based on area classification.

Original prompt

This section details on the original issue you should resolve

<issue_title>Feature request: unassign-from-user safe output handler for removing assignees</issue_title>
<issue_description>🤖: Bug report submitted by AI

Summary

There is currently no safe output handler for removing assignees from issues. The existing assign-to-user safe output can add assignees, and remove-labels can remove labels, but there is no counterpart for unassigning users.

Use Case

We're building a stale-issue re-triage workflow that:

  1. Runs area-label classification and assignee suggestion when the stale label is applied
  2. Compares the AI-suggested assignee against the issue's current assignee
  3. If they differ, should replace the current assignee with the suggested one

Step 3 is currently impossible via safe outputs. We can add the new assignee with assign-to-user, but we cannot remove the old one. This leads to issues accumulating multiple assignees instead of having the correct one.

Proposed Solution

Add an unassign-from-user (or remove-assignee) safe output, mirroring assign-to-user:

safe-outputs:
  unassign-from-user:
    allowed: [user1, user2]        # Optional: restrict which users can be unassigned
    max: 3                         # Optional: max unassignment operations (default: 1)
    target: "*"                    # Optional: "triggering" (default), "*", or number
    target-repo: "owner/repo"      # Optional: cross-repository

Handler Implementation

The handler would be straightforward — assign_to_user.cjs is ~110 lines, and the unassign version would be nearly identical but call github.rest.issues.removeAssignees instead of addAssignees:

await github.rest.issues.removeAssignees({
  owner: repoParts.owner,
  repo: repoParts.repo,
  issue_number: issueNumber,
  assignees: uniqueAssignees,
});

The handler should use resolveTargetRepoConfig / resolveAndValidateRepo from repo_helpers.cjs from the start (unlike assign_to_user which currently doesn't — see #15216).

MCP Tool Schema

{
  "name": "unassign_from_user",
  "description": "Remove one or more assignees from an issue.",
  "inputSchema": {
    "properties": {
      "assignee": { "type": "string", "description": "Single GitHub username to unassign." },
      "assignees": { "type": "array", "items": { "type": "string" }, "description": "GitHub usernames to unassign." },
      "issue_number": { "type": "number", "description": "Issue number. If omitted, uses the triggering issue." }
    }
  }
}

Compiler Changes

  • Add UnassignFromUserConfig struct embedding BaseSafeOutputConfig and SafeOutputTargetConfig (same pattern as AssignToUserConfig in assign_to_user.go)
  • Add parser, config builder, and handler registration following the existing assign-to-user pattern
  • Add to HANDLER_MAP in safe_output_handler_manager.cjs and safe_output_unified_handler_manager.cjs

Current Workarounds

  1. Accept multiple assignees: Add the correct assignee via assign-to-user, leave the old one. Issue ends up with 2+ assignees.
  2. Use non-read-only GitHub MCP tools: Grant issues: write permission to the agent job and use the GitHub MCP update_issue tool directly. This breaks the safe-outputs security pattern.
  3. Manual cleanup: Have a human remove the old assignee after the workflow runs.

None of these are satisfactory for automated re-triage at scale.

Symmetry with Existing Safe Outputs

The safe outputs system already follows an add/remove pattern for labels:

  • add-labelsremove-labels

The same pattern should exist for assignees:

  • assign-to-userunassign-from-user (missing)</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.


Changeset

  • Type: patch
  • Description: Document the new unassign-from-user safe-output handler and associated tooling

Generated by Changeset Generator

Copilot AI and others added 3 commits February 12, 2026 18:35
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI and others added 2 commits February 12, 2026 18:46
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add unassign-from-user safe output handler for removing assignees Add unassign-from-user safe output handler Feb 12, 2026
Copilot AI requested a review from pelikhan February 12, 2026 18:57
@pelikhan
Copy link
Contributor

@copilot update smoke_codex to add unassign from the the githubactionagent (fictitious user)

@github-actions
Copy link
Contributor

🧪 Smoke Test Results - Run 21960945262

PRs Tested:

Test Status:

  • ✅ GitHub MCP Testing
  • ✅ Safe Inputs GH CLI Testing
  • ⚠️ Serena MCP Testing (tool unavailable)
  • ✅ Playwright Testing
  • ✅ File Writing Testing
  • ✅ Bash Tool Testing
  • ✅ Discussion Interaction Testing
  • ✅ Build gh-aw
  • ✅ Workflow Dispatch Testing

Overall Status: PASS

@pelikhan @Copilot

AI generated by Smoke Copilot

@pelikhan pelikhan marked this pull request as ready for review February 12, 2026 19:27
Copilot AI review requested due to automatic review settings February 12, 2026 19:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new unassign-from-user safe output to remove assignees from GitHub issues (and PRs-as-issues), mirroring assign-to-user and label add/remove symmetry.

Changes:

  • Introduces Go compiler parsing/config and workflow schema/docs entries for unassign-from-user.
  • Adds a JS handler calling github.rest.issues.removeAssignees() with cross-repo support and registers it in handler managers.
  • Extends safe-outputs tool catalog and enables tool filtering/config generation for the new tool.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/workflow/unassign_from_user.go Adds Go config struct + parser for unassign-from-user safe output
pkg/workflow/safe_outputs_config_generation.go Emits tool config + enables tool + adds repo-parameter logic for unassign_from_user
pkg/workflow/safe_outputs_config.go Hooks the new parser into safe-outputs extraction
pkg/workflow/compiler_types.go Adds UnassignFromUser to SafeOutputsConfig
pkg/parser/schemas/main_workflow_schema.json Adds schema validation for safe-outputs.unassign-from-user
pkg/cli/workflows/test-unassign-from-user.md Adds a manual test workflow example for the new safe output
docs/src/content/docs/reference/frontmatter-full.md Documents unassign-from-user frontmatter configuration
actions/setup/js/unassign_from_user.cjs Implements the JS handler factory for removing assignees
actions/setup/js/unassign_from_user.test.cjs Adds Vitest coverage for handler behavior (allowed list, max, cross-repo, etc.)
actions/setup/js/safe_outputs_tools.json Adds MCP tool schema for unassign_from_user
actions/setup/js/safe_output_unified_handler_manager.cjs Registers the new handler in unified manager
actions/setup/js/safe_output_handler_manager.cjs Registers the new handler in non-unified manager

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +198 to +204
if data.SafeOutputs.UnassignFromUser != nil {
safeOutputsConfig["unassign_from_user"] = generateMaxWithAllowedConfig(
data.SafeOutputs.UnassignFromUser.Max,
1, // default max
data.SafeOutputs.UnassignFromUser.Allowed,
)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unassign_from_user is generated with generateMaxWithAllowedConfig(...), which appears to serialize only max and allowed. But the handler and repo-parameter injection logic rely on SafeOutputTargetConfig fields (e.g., target, target-repo/TargetRepoSlug, allowed-repos). As written, cross-repo config and targeting config may be dropped from the generated JS config, preventing repo support from working at runtime. Consider generating the config using the same helper/pattern used by other SafeOutputTargetConfig-based tools so target, target_repo, and allowed_repos are preserved.

Copilot uses AI. Check for mistakes.
async function main(config = {}) {
// Extract configuration
const allowedAssignees = config.allowed || [];
const maxCount = config.max || 10;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler defaults max to 10, but the PR description, schema, and Go-side defaulting indicate the default should be 1 operation. This mismatch can unintentionally allow more unassign operations when users enable the safe output with null/empty config. Align the default here with the documented default (and prefer nullish-coalescing so an explicitly provided value is respected as-is).

Suggested change
const maxCount = config.max || 10;
const maxCount = config.max ?? 1;

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +56
if (processedCount >= maxCount) {
core.warning(`Skipping unassign_from_user: max count of ${maxCount} reached`);
return {
success: false,
error: `Max count of ${maxCount} reached`,
};
}

processedCount++;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two related issues with maxCount: (1) processedCount++ happens before validating message/repo/assignees and before performing any API call, so invalid inputs (or no-op cases) still consume the run's operation budget; this can block subsequent valid operations. (2) Passing maxCount into processItems(...) couples "max operations per run" to "max assignees per single request", which can incorrectly truncate the assignee list when max is small (e.g., max: 1 would only ever remove a single assignee per call). Increment the counter only when you actually perform an unassign API call, and use a separate per-message limit (or no limit) for assignee list processing.

Copilot uses AI. Check for mistakes.
core.info(`Requested assignees to unassign: ${JSON.stringify(requestedAssignees)}`);

// Use shared helper to filter, sanitize, dedupe, and limit
const uniqueAssignees = processItems(requestedAssignees, allowedAssignees, maxCount);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two related issues with maxCount: (1) processedCount++ happens before validating message/repo/assignees and before performing any API call, so invalid inputs (or no-op cases) still consume the run's operation budget; this can block subsequent valid operations. (2) Passing maxCount into processItems(...) couples "max operations per run" to "max assignees per single request", which can incorrectly truncate the assignee list when max is small (e.g., max: 1 would only ever remove a single assignee per call). Increment the counter only when you actually perform an unassign API call, and use a separate per-message limit (or no limit) for assignee list processing.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +70
issueNumber = parseInt(String(unassignItem.issue_number), 10);
if (isNaN(issueNumber)) {
core.warning(`Invalid issue_number: ${unassignItem.issue_number}`);
return {
success: false,
error: `Invalid issue_number: ${unassignItem.issue_number}`,
};
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseInt will accept partially-numeric strings (e.g., '123abc') and non-integer numbers (e.g., 123.9) and silently coerce them to 123, which can target the wrong issue. Since issue_number must be an integer, validate that the parsed value is an integer and that the original input is strictly an integer representation (or use Number(...) + Number.isInteger(...) with appropriate string checks).

Suggested change
issueNumber = parseInt(String(unassignItem.issue_number), 10);
if (isNaN(issueNumber)) {
core.warning(`Invalid issue_number: ${unassignItem.issue_number}`);
return {
success: false,
error: `Invalid issue_number: ${unassignItem.issue_number}`,
};
}
const rawIssueNumber = String(unassignItem.issue_number).trim();
// Require a strict non-negative integer representation for issue numbers
if (!/^\d+$/.test(rawIssueNumber)) {
core.warning(`Invalid issue_number: ${unassignItem.issue_number}`);
return {
success: false,
error: `Invalid issue_number: ${unassignItem.issue_number}`,
};
}
issueNumber = Number(rawIssueNumber);
if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
core.warning(`Invalid issue_number: ${unassignItem.issue_number}`);
return {
success: false,
error: `Invalid issue_number: ${unassignItem.issue_number}`,
};
}

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,270 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file uses ESM import ... from syntax but is named .cjs (CommonJS). In Node, .cjs is treated as CommonJS and this will fail to parse unless the test runner applies special transforms. To avoid runtime/parser errors, either rename the test to an ESM extension (e.g., .test.js/.test.mjs) or convert the imports to require(...).

Suggested change
import { describe, it, expect, beforeEach, vi } from "vitest";
const { describe, it, expect, beforeEach, vi } = require("vitest");

Copilot uses AI. Check for mistakes.
- Add unassign-from-user to safe-outputs config with githubactionagent allowed
- Add test instruction to unassign fictitious user githubactionagent from PR
- Addresses PR review comment to test unassign functionality

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 12, 2026

@copilot update smoke_codex to add unassign from the the githubactionagent (fictitious user)

Done! Added unassign-from-user to the smoke-codex workflow:

  • Added to safe-outputs config with githubactionagent in the allowed list
  • Added test instruction to unassign the fictitious user from the PR
  • Workflow compiled successfully

Commit: 226fdf3

@github-actions
Copy link
Contributor

📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing...

@github-actions
Copy link
Contributor

🧪 Smoke Project is now testing project operations...

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

Changeset Generator completed successfully!

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

🎬 THE ENDSmoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟

@github-actions
Copy link
Contributor

Agent Container Tool Check ✅

Tool Status Version
bash 5.2.21
sh available
git 2.52.0
jq 1.7
yq 4.52.2
curl 8.5.0
gh 2.86.0
node 20.20.0
python3 3.12.3
go 1.24.13
java 10.0.102
dotnet 10.0.102

Result: 12/12 tools available ✅

All required development tools are present and functional in the agent container environment.

AI generated by Agent Container Smoke Test

@github-actions
Copy link
Contributor

Smoke Project completed successfully. All project operations validated.

@github-actions
Copy link
Contributor

PRs: Add custom-agent-for-aw to reference TOC; Pre-validate workflow names in MCP logs tool
GitHub MCP (2 merged PR titles): ✅
Serena MCP activate + find_symbol (>=3): ✅
Playwright title contains GitHub: ✅
File write + cat verify: ✅
Build (make build with temp caches): ✅
Overall: PASS

AI generated by Smoke Codex

@pelikhan pelikhan merged commit fd67f05 into main Feb 12, 2026
@pelikhan pelikhan deleted the copilot/add-unassign-from-user-handler branch February 12, 2026 19:48
@github-actions
Copy link
Contributor

Smoke Test Results

PRs: #15226, #15221
✅ GitHub MCP
✅ Safe Inputs GH CLI
⚠️ Serena MCP (skipped)
❌ Playwright (timeout)
✅ File Writing
✅ Bash Tool
✅ Discussion Interaction
✅ Build gh-aw
✅ Workflow Dispatch

Status: ⚠️ PARTIAL PASS (7/9)

@pelikhan @Copilot

AI generated by Smoke Copilot

@github-actions
Copy link
Contributor

📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤

Smoke test complete - 7/9 passed (Playwright timeout, Serena skipped)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: unassign-from-user safe output handler for removing assignees

2 participants