From 69f60af49be028469d0796dba9805b9b59706bf9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 06:22:01 +0000 Subject: [PATCH 1/5] Add dismiss pull request review safe output Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/dismiss_pull_request_review.cjs | 163 ++++++++++++++++++ .../js/dismiss_pull_request_review.test.cjs | 114 ++++++++++++ .../setup/js/safe_output_handler_manager.cjs | 2 + actions/setup/js/safe_outputs_handlers.cjs | 26 +++ .../setup/js/safe_outputs_handlers.test.cjs | 50 ++++++ actions/setup/js/safe_outputs_tools.json | 48 ++++++ .../setup/js/safe_outputs_tools_loader.cjs | 1 + actions/setup/js/types/safe-outputs.d.ts | 17 ++ pkg/parser/schemas/main_workflow_schema.json | 85 +++++++++ pkg/workflow/dismiss_pull_request_review.go | 48 ++++++ pkg/workflow/js/safe_outputs_tools.json | 48 ++++++ pkg/workflow/safe_output_handlers.go | 13 ++ pkg/workflow/safe_outputs_config.go | 7 + pkg/workflow/safe_outputs_handler_registry.go | 16 ++ pkg/workflow/safe_outputs_max_validation.go | 5 + pkg/workflow/safe_outputs_state.go | 2 + .../safe_outputs_tools_computation.go | 4 + .../safe_outputs_tools_repo_params.go | 5 + pkg/workflow/safe_outputs_validation.go | 3 + .../safe_outputs_validation_config.go | 10 ++ pkg/workflow/tool_description_enhancer.go | 14 ++ pkg/workflow/unified_prompt_step.go | 3 + schemas/agent-output.json | 34 ++++ 23 files changed, 718 insertions(+) create mode 100644 actions/setup/js/dismiss_pull_request_review.cjs create mode 100644 actions/setup/js/dismiss_pull_request_review.test.cjs create mode 100644 pkg/workflow/dismiss_pull_request_review.go diff --git a/actions/setup/js/dismiss_pull_request_review.cjs b/actions/setup/js/dismiss_pull_request_review.cjs new file mode 100644 index 00000000000..d5170b5e13f --- /dev/null +++ b/actions/setup/js/dismiss_pull_request_review.cjs @@ -0,0 +1,163 @@ +// @ts-check +/// + +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + */ + +const { getErrorMessage } = require("./error_helpers.cjs"); +const { resolveTarget, isStagedMode, logStagedPreviewInfo, checkRequiredFilter } = require("./safe_output_helpers.cjs"); +const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); +const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); + +/** @type {string} Safe output type handled by this module */ +const HANDLER_TYPE = "dismiss_pull_request_review"; + +/** + * Resolve the effective actor used as both the dismisser and default expected author. + * @returns {string} + */ +function getEffectiveActor() { + const actor = (process.env.GITHUB_ACTOR || context?.actor || "github-actions[bot]").trim(); + return actor || "github-actions[bot]"; +} + +/** + * Main handler factory for dismiss_pull_request_review. + * @type {HandlerFactoryFunction} + */ +async function main(config = {}) { + const maxCount = config.max || 10; + const targetConfig = config.target || "triggering"; + const isStaged = isStagedMode(config); + const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); + const githubClient = await createAuthenticatedGitHubClient(config); + const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : []; + const requiredTitlePrefix = config.required_title_prefix || ""; + const dismisser = getEffectiveActor(); + + let processedCount = 0; + + return async function handleDismissPullRequestReview(message) { + if (processedCount >= maxCount) { + return { + success: false, + error: `Max count of ${maxCount} reached`, + }; + } + + const reviewId = Number.parseInt(String(message.review_id || ""), 10); + if (!Number.isInteger(reviewId) || reviewId <= 0) { + return { + success: false, + error: "review_id must be a positive integer", + }; + } + + const justification = typeof message.justification === "string" ? message.justification.trim() : ""; + if (justification.length < 20) { + return { + success: false, + error: "justification must be at least 20 characters", + }; + } + + const expectedAuthor = typeof message.author === "string" && message.author.trim().length > 0 ? message.author.trim() : dismisser; + if (expectedAuthor !== dismisser) { + return { + success: false, + error: `author must match the current workflow actor (${dismisser})`, + }; + } + + const targetResult = resolveTarget({ + targetConfig, + item: message, + context, + itemType: "pull request review dismissal", + // In resolveTarget conventions, supportsPR=false means PR-only handlers. + supportsPR: false, + }); + if (!targetResult.success) { + return { + success: false, + error: targetResult.error, + }; + } + const pullRequestNumber = targetResult.number; + + const repoResult = resolveAndValidateRepo(message, defaultTargetRepo, allowedRepos, "pull request review dismissal"); + if (!repoResult.success) { + return { + success: false, + error: repoResult.error, + }; + } + const { owner, repo } = repoResult.repoParts; + + const filterResult = await checkRequiredFilter(githubClient, repoResult.repoParts, pullRequestNumber, requiredLabels, requiredTitlePrefix, HANDLER_TYPE); + if (filterResult) return filterResult; + + if (isStaged) { + logStagedPreviewInfo(`Would dismiss review #${reviewId} on PR #${pullRequestNumber} (${owner}/${repo}) as ${dismisser}`); + processedCount++; + return { + success: true, + staged: true, + review_id: reviewId, + pull_request_number: pullRequestNumber, + repo: `${owner}/${repo}`, + author: expectedAuthor, + }; + } + + try { + const { data: review } = await githubClient.rest.pulls.getReview({ + owner, + repo, + pull_number: pullRequestNumber, + review_id: reviewId, + }); + + const reviewAuthorLogin = review?.user?.login; + if (typeof reviewAuthorLogin !== "string" || reviewAuthorLogin.trim() === "") { + return { + success: false, + error: "review author is unavailable for dismissal validation", + }; + } + const reviewAuthor = reviewAuthorLogin.trim(); + if (reviewAuthor !== expectedAuthor) { + return { + success: false, + error: `review author (${reviewAuthor || "unknown"}) must match dismisser (${dismisser})`, + }; + } + + const { data: dismissed } = await githubClient.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pullRequestNumber, + review_id: reviewId, + message: justification, + }); + + processedCount++; + return { + success: true, + review_id: reviewId, + pull_request_number: pullRequestNumber, + repo: `${owner}/${repo}`, + author: reviewAuthor, + review_url: dismissed?.html_url || review?.html_url, + }; + } catch (error) { + return { + success: false, + error: getErrorMessage(error), + }; + } + }; +} + +module.exports = { main, HANDLER_TYPE }; diff --git a/actions/setup/js/dismiss_pull_request_review.test.cjs b/actions/setup/js/dismiss_pull_request_review.test.cjs new file mode 100644 index 00000000000..b5ea9941f2e --- /dev/null +++ b/actions/setup/js/dismiss_pull_request_review.test.cjs @@ -0,0 +1,114 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +const mockCore = { + debug: vi.fn(), + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }, +}; + +global.core = mockCore; + +const mockGetReview = vi.fn(); +const mockDismissReview = vi.fn(); + +const mockGithub = { + rest: { + pulls: { + getReview: mockGetReview, + dismissReview: mockDismissReview, + }, + }, +}; + +global.github = mockGithub; +global.context = { + actor: "github-actions[bot]", + eventName: "pull_request", + repo: { owner: "test-owner", repo: "test-repo" }, + payload: { pull_request: { number: 42 } }, +}; + +describe("dismiss_pull_request_review", () => { + let handler; + + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + process.env.GITHUB_ACTOR = "github-actions[bot]"; + + mockGetReview.mockResolvedValue({ + data: { + html_url: "https://github.com/test-owner/test-repo/pull/42#pullrequestreview-123", + user: { login: "github-actions[bot]" }, + }, + }); + mockDismissReview.mockResolvedValue({ + data: { + html_url: "https://github.com/test-owner/test-repo/pull/42#pullrequestreview-123", + }, + }); + + const { main } = require("./dismiss_pull_request_review.cjs"); + handler = await main({ max: 10 }); + }); + + it("dismisses a review when author matches current actor", async () => { + const result = await handler({ + type: "dismiss_pull_request_review", + review_id: 123, + justification: "This stale review no longer reflects the updated implementation.", + }); + + expect(result.success).toBe(true); + expect(mockGetReview).toHaveBeenCalledWith( + expect.objectContaining({ + pull_number: 42, + review_id: 123, + }) + ); + expect(mockDismissReview).toHaveBeenCalledWith( + expect.objectContaining({ + pull_number: 42, + review_id: 123, + }) + ); + }); + + it("rejects when provided author differs from current actor", async () => { + const result = await handler({ + type: "dismiss_pull_request_review", + review_id: 123, + author: "octocat", + justification: "This stale review no longer reflects the updated implementation.", + }); + + expect(result.success).toBe(false); + expect(result.error).toContain("author must match the current workflow actor"); + expect(mockDismissReview).not.toHaveBeenCalled(); + }); + + it("rejects when fetched review author differs from current actor", async () => { + mockGetReview.mockResolvedValueOnce({ + data: { + user: { login: "octocat" }, + }, + }); + + const result = await handler({ + type: "dismiss_pull_request_review", + review_id: 123, + justification: "This stale review no longer reflects the updated implementation.", + }); + + expect(result.success).toBe(false); + expect(result.error).toContain("review author"); + expect(mockDismissReview).not.toHaveBeenCalled(); + }); +}); diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index a1a43f6fd68..81927104c16 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -49,6 +49,7 @@ const HANDLER_MAP = { update_release: "./update_release.cjs", create_pull_request_review_comment: "./create_pr_review_comment.cjs", submit_pull_request_review: "./submit_pr_review.cjs", + dismiss_pull_request_review: "./dismiss_pull_request_review.cjs", reply_to_pull_request_review_comment: "./reply_to_pr_review_comment.cjs", resolve_pull_request_review_thread: "./resolve_pr_review_thread.cjs", create_pull_request: "./create_pull_request.cjs", @@ -164,6 +165,7 @@ const THREAT_WARNING_ABORT_TYPES = new Set([ "merge_pull_request", "mark_pull_request_as_ready_for_review", "resolve_pull_request_review_thread", + "dismiss_pull_request_review", "add_labels", "remove_labels", "add_reviewer", diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index fe864bfba5a..974fc07d7ce 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1978,6 +1978,31 @@ function createHandlers(server, appendSafeOutput, config = {}) { return defaultHandler("submit_pull_request_review")(args); }; + /** + * Handler for dismiss_pull_request_review tool (MCP server phase). + * Enforces justification minimum length and actor-author consistency before recording. + */ + const dismissPullRequestReviewHandler = args => { + const justification = (args && typeof args.justification === "string" ? args.justification : "").trim(); + if (justification.length < 20) { + throw { + code: -32602, + message: `${ERR_VALIDATION}: dismiss_pull_request_review: 'justification' must be at least 20 characters`, + }; + } + + const actor = (process.env.GITHUB_ACTOR || context?.actor || "github-actions[bot]").trim() || "github-actions[bot]"; + const author = args && typeof args.author === "string" ? args.author.trim() : ""; + if (author && author !== actor) { + throw { + code: -32602, + message: `${ERR_VALIDATION}: dismiss_pull_request_review: 'author' must match current workflow actor (${actor})`, + }; + } + + return defaultHandler("dismiss_pull_request_review")(args); + }; + /** * Recursively copy all regular files from srcDir into destDir, preserving the relative * path structure under srcDir. Non-regular entries (sockets, devices, pipes, symlinks) @@ -2187,6 +2212,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { addCommentHandler, createPullRequestReviewCommentHandler, submitPullRequestReviewHandler, + dismissPullRequestReviewHandler, updateIssueHandler, updatePullRequestHandler, }; diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 39bcd7e62f8..a30d63a6ab5 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -87,6 +87,7 @@ describe("safe_outputs_handlers", () => { delete process.env.GH_AW_ASSETS_BRANCH; delete process.env.GH_AW_ASSETS_MAX_SIZE_KB; delete process.env.GH_AW_ASSETS_ALLOWED_EXTS; + delete process.env.GITHUB_ACTOR; }); describe("probe intent helpers", () => { @@ -2707,6 +2708,55 @@ describe("safe_outputs_handlers", () => { }); }); + describe("dismissPullRequestReviewHandler", () => { + it("should write entry and return success with valid justification", () => { + const result = handlers.dismissPullRequestReviewHandler({ + review_id: 123, + justification: "This stale review no longer matches the latest patch.", + }); + + const data = JSON.parse(result.content[0].text); + expect(data.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "dismiss_pull_request_review", + review_id: 123, + }) + ); + }); + + it("should throw MCP error when justification is shorter than 20 characters", () => { + expect(() => + handlers.dismissPullRequestReviewHandler({ + review_id: 123, + justification: "too short", + }) + ).toThrow( + expect.objectContaining({ + code: -32602, + message: expect.stringContaining("at least 20 characters"), + }) + ); + }); + + it("should throw MCP error when author does not match current workflow actor", () => { + process.env.GITHUB_ACTOR = "github-actions[bot]"; + + expect(() => + handlers.dismissPullRequestReviewHandler({ + review_id: 123, + justification: "This stale review no longer matches the latest patch.", + author: "octocat", + }) + ).toThrow( + expect.objectContaining({ + code: -32602, + message: expect.stringContaining("must match current workflow actor"), + }) + ); + }); + }); + describe("createPullRequestReviewCommentHandler", () => { it("should write entry and return success", () => { const result = handlers.createPullRequestReviewCommentHandler({ path: "src/foo.js", line: 5, body: "Consider renaming." }); diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index bd789c3fff2..db3e770565a 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -510,6 +510,54 @@ "additionalProperties": false } }, + { + "name": "dismiss_pull_request_review", + "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (typically github-actions[bot]). You must provide a justification of at least 20 characters.", + "inputSchema": { + "type": "object", + "required": ["review_id", "justification"], + "properties": { + "review_id": { + "type": ["number", "string"], + "description": "The numeric review ID to dismiss.", + "x-synonyms": ["reviewId"] + }, + "justification": { + "type": "string", + "minLength": 20, + "description": "Reason for dismissing the review. Must be at least 20 characters." + }, + "author": { + "type": "string", + "description": "Optional expected review author login. If provided, it must match the current workflow actor." + }, + "pull_request_number": { + "type": ["number", "string"], + "description": "Pull request number containing the review. If omitted, uses the triggering PR context.", + "x-synonyms": ["pullRequestNumber"] + }, + "repo": { + "type": "string", + "description": "Target repository in 'owner/repo' format. If omitted, uses the configured target repository. Must be in the allowed-repos list if specified." + }, + "secrecy": { + "type": "string", + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." + }, + "integrity": { + "type": "string", + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\")." + } + }, + "additionalProperties": false + }, + "x-safe-outputs-target-requirements": { + "*": { + "primary": "pull_request_number", + "anyOf": ["pull_request_number"] + } + } + }, { "name": "resolve_pull_request_review_thread", "description": "Resolve a review thread on a pull request. Use this to mark a review conversation as resolved after addressing the feedback. The thread_id must be the node ID of the review thread (e.g., PRRT_kwDO...).", diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index 852c01f0b9e..cb4d7669c75 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -139,6 +139,7 @@ function attachHandlers(tools, handlers, logger) { add_comment: handlers.addCommentHandler, create_pull_request_review_comment: handlers.createPullRequestReviewCommentHandler, submit_pull_request_review: handlers.submitPullRequestReviewHandler, + dismiss_pull_request_review: handlers.dismissPullRequestReviewHandler, update_issue: handlers.updateIssueHandler, update_pull_request: handlers.updatePullRequestHandler, }; diff --git a/actions/setup/js/types/safe-outputs.d.ts b/actions/setup/js/types/safe-outputs.d.ts index 6aba9c713e0..c9915760770 100644 --- a/actions/setup/js/types/safe-outputs.d.ts +++ b/actions/setup/js/types/safe-outputs.d.ts @@ -425,6 +425,21 @@ interface ReplyToPullRequestReviewCommentItem extends BaseSafeOutputItem { pull_request_number?: number | string; } +/** + * JSONL item for dismissing a pull request review + */ +interface DismissPullRequestReviewItem extends BaseSafeOutputItem { + type: "dismiss_pull_request_review"; + /** Numeric review ID to dismiss */ + review_id: number | string; + /** Dismissal justification (minimum 20 characters) */ + justification: string; + /** Optional review author login, must match workflow actor */ + author?: string; + /** Optional PR number (required when target is "*") */ + pull_request_number?: number | string; +} + /** * JSONL item for creating a GitHub Project V2 */ @@ -495,6 +510,7 @@ type SafeOutputItem = | LinkSubIssueItem | HideCommentItem | ReplyToPullRequestReviewCommentItem + | DismissPullRequestReviewItem | CreateProjectItem | AutofixCodeScanningAlertItem | ResolvePullRequestReviewThreadItem; @@ -539,6 +555,7 @@ export { LinkSubIssueItem, HideCommentItem, ReplyToPullRequestReviewCommentItem, + DismissPullRequestReviewItem, AutofixCodeScanningAlertItem, ResolvePullRequestReviewThreadItem, SafeOutputItem, diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ea4dd301ed6..9ec67375f77 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -7201,6 +7201,91 @@ ], "description": "Enable AI agents to submit consolidated pull request reviews with a status decision. Works with create-pull-request-review-comment to batch inline comments into a single review." }, + "dismiss-pull-request-review": { + "oneOf": [ + { + "type": "object", + "description": "Configuration for dismissing pull request reviews previously submitted by the current workflow actor.", + "properties": { + "max": { + "description": "Maximum number of review dismissals to perform (default: 10) Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", + "oneOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + { + "type": "string", + "pattern": "^\\$\\{\\{.*\\}\\}$", + "description": "GitHub Actions expression that resolves to an integer at runtime" + } + ] + }, + "target": { + "type": "string", + "description": "Target PR for review dismissal: 'triggering' (default, current PR), '*' (any PR, requires pull_request_number in agent output), or explicit PR number." + }, + "target-repo": { + "type": "string", + "description": "Target repository in format 'owner/repo' for cross-repository review dismissal. Takes precedence over trial target repo settings." + }, + "allowed-repos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of additional repositories in format 'owner/repo' where review dismissals are allowed. The target repository (current or target-repo) is always implicitly allowed." + }, + "github-token": { + "$ref": "#/$defs/github_token", + "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." + }, + "staged": { + "$ref": "#/$defs/templatable_boolean", + "description": "When true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", + "examples": [true, false] + }, + "samples": { + "description": "Internal hidden feature. Optional list of declarative sample payloads that exercise this safe-output handler. Used by the hidden `gh aw compile --use-samples` flag to replace the agentic step with a deterministic replay through the safe-outputs MCP server. Each entry should conform to the corresponding MCP tool inputSchema.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + { + "type": "object", + "additionalProperties": true + } + ] + }, + "required-labels": { + "type": "array", + "items": { + "type": "string" + }, + "description": "All of these labels must be present on the target item for this operation to proceed" + }, + "required-title-prefix": { + "type": "string", + "description": "The target item's title must start with this prefix for this operation to proceed" + } + }, + "additionalProperties": false + }, + { + "type": "null", + "description": "Enable pull request review dismissal with default configuration" + } + ], + "description": "Enable AI agents to dismiss pull request reviews authored by the current workflow actor." + }, + "dismiss-review": { + "$ref": "#/properties/safe-outputs/properties/dismiss-pull-request-review" + }, "reply-to-pull-request-review-comment": { "oneOf": [ { diff --git a/pkg/workflow/dismiss_pull_request_review.go b/pkg/workflow/dismiss_pull_request_review.go new file mode 100644 index 00000000000..30fb026e397 --- /dev/null +++ b/pkg/workflow/dismiss_pull_request_review.go @@ -0,0 +1,48 @@ +package workflow + +import "github.com/github/gh-aw/pkg/logger" + +var dismissPullRequestReviewLog = logger.New("workflow:dismiss_pull_request_review") + +// DismissPullRequestReviewConfig holds configuration for dismissing pull request reviews. +type DismissPullRequestReviewConfig struct { + BaseSafeOutputConfig `yaml:",inline"` + SafeOutputTargetConfig `yaml:",inline"` + SafeOutputFilterConfig `yaml:",inline"` +} + +// parseDismissPullRequestReviewConfig handles dismiss-pull-request-review configuration. +func (c *Compiler) parseDismissPullRequestReviewConfig(outputMap map[string]any) *DismissPullRequestReviewConfig { + var configData any + if value, exists := outputMap["dismiss-pull-request-review"]; exists { + configData = value + } else if value, exists := outputMap["dismiss-review"]; exists { + // Backward-compatible alias. + configData = value + } else { + return nil + } + + dismissPullRequestReviewLog.Print("Parsing dismiss-pull-request-review configuration") + config := &DismissPullRequestReviewConfig{} + + if configMap, ok := configData.(map[string]any); ok { + // Parse common base fields with default max of 10. + c.parseBaseSafeOutputConfig(configMap, &config.BaseSafeOutputConfig, 10) + + // Parse target config (target, target-repo, allowed-repos). + targetConfig, isInvalid := ParseTargetConfig(configMap) + if isInvalid { + return nil + } + config.SafeOutputTargetConfig = targetConfig + + // Parse filter config (required-labels, required-title-prefix). + config.SafeOutputFilterConfig = ParseFilterConfig(configMap) + } else { + // If configData is nil or not a map, still set the default max. + config.Max = defaultIntStr(10) + } + + return config +} diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index ce02572644e..52826437dc0 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -654,6 +654,54 @@ "additionalProperties": false } }, + { + "name": "dismiss_pull_request_review", + "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (typically github-actions[bot]). You must provide a justification of at least 20 characters.", + "inputSchema": { + "type": "object", + "required": ["review_id", "justification"], + "properties": { + "review_id": { + "type": ["number", "string"], + "description": "The numeric review ID to dismiss.", + "x-synonyms": ["reviewId"] + }, + "justification": { + "type": "string", + "minLength": 20, + "description": "Reason for dismissing the review. Must be at least 20 characters." + }, + "author": { + "type": "string", + "description": "Optional expected review author login. If provided, it must match the current workflow actor." + }, + "pull_request_number": { + "type": ["number", "string"], + "description": "Pull request number containing the review. If omitted, uses the triggering PR context.", + "x-synonyms": ["pullRequestNumber"] + }, + "repo": { + "type": "string", + "description": "Target repository in 'owner/repo' format. If omitted, uses the configured target repository. Must be in the allowed-repos list if specified." + }, + "secrecy": { + "type": "string", + "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." + }, + "integrity": { + "type": "string", + "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\")." + } + }, + "additionalProperties": false + }, + "x-safe-outputs-target-requirements": { + "*": { + "primary": "pull_request_number", + "anyOf": ["pull_request_number"] + } + } + }, { "name": "resolve_pull_request_review_thread", "description": "Resolve a review thread on a pull request. Use this to mark a review conversation as resolved after addressing the feedback. The thread_id must be the node ID of the review thread (e.g., PRRT_kwDO...).", diff --git a/pkg/workflow/safe_output_handlers.go b/pkg/workflow/safe_output_handlers.go index 30116acea69..577e256bdd9 100644 --- a/pkg/workflow/safe_output_handlers.go +++ b/pkg/workflow/safe_output_handlers.go @@ -116,6 +116,19 @@ var safeOutputHandlers = []safeOutputHandlerDescriptor{ return NewPermissionsContentsReadPRWrite() }, }, + { + Key: "dismiss-pull-request-review", + Aliases: []string{"dismiss-review"}, + StructField: "DismissPullRequestReview", + ToolName: "dismiss_pull_request_review", + NewConfig: func() any { return &DismissPullRequestReviewConfig{} }, + PermissionBuilder: func(safeOutputs *SafeOutputsConfig) *Permissions { + if !isSafeOutputHandlerEnabledAndUnstaged(safeOutputs, "DismissPullRequestReview") { + return nil + } + return NewPermissionsContentsReadPRWrite() + }, + }, { Key: "add-comment", StructField: "AddComments", diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index c94fb6cd899..8ac20f226a7 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -37,6 +37,7 @@ type SafeOutputsConfig struct { CloseIssues *CloseIssuesConfig `yaml:"close-issue,omitempty"` ClosePullRequests *ClosePullRequestsConfig `yaml:"close-pull-request,omitempty"` MarkPullRequestAsReadyForReview *MarkPullRequestAsReadyForReviewConfig `yaml:"mark-pull-request-as-ready-for-review,omitempty"` + DismissPullRequestReview *DismissPullRequestReviewConfig `yaml:"dismiss-pull-request-review,omitempty"` // Dismiss a pull request review authored by the workflow actor AddComments *AddCommentsConfig `yaml:"add-comment,omitempty"` CommentMemory *CommentMemoryConfig `yaml:"comment-memory,omitempty"` // Persist and update managed memory comments on issues/PRs CreatePullRequests *CreatePullRequestsConfig `yaml:"create-pull-request,omitempty"` @@ -278,6 +279,12 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut config.MarkPullRequestAsReadyForReview = markPRReadyConfig } + // Handle dismiss-pull-request-review (and dismiss-review alias) + dismissPRReviewConfig := c.parseDismissPullRequestReviewConfig(outputMap) + if dismissPRReviewConfig != nil { + config.DismissPullRequestReview = dismissPRReviewConfig + } + // Handle add-comment commentsConfig := c.parseCommentsConfig(outputMap) if commentsConfig != nil { diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index 86474a154c6..23d639fbaf9 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -248,6 +248,22 @@ var handlerRegistry = map[string]handlerBuilder{ AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)). Build() }, + "dismiss_pull_request_review": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.DismissPullRequestReview == nil { + return nil + } + c := cfg.DismissPullRequestReview + return newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddIfNotEmpty("target", c.Target). + AddStringSlice("required_labels", c.RequiredLabels). + AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("github-token", c.GitHubToken). + AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)). + Build() + }, "create_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { if cfg.CreateCodeScanningAlerts == nil { return nil diff --git a/pkg/workflow/safe_outputs_max_validation.go b/pkg/workflow/safe_outputs_max_validation.go index d3bca7247a6..cb3db60d7ea 100644 --- a/pkg/workflow/safe_outputs_max_validation.go +++ b/pkg/workflow/safe_outputs_max_validation.go @@ -170,6 +170,11 @@ func validateSafeOutputsMax(config *SafeOutputsConfig) error { return err } } + if config.DismissPullRequestReview != nil { + if err := checkMaxField("dismiss_pull_request_review", config.DismissPullRequestReview.Max); err != nil { + return err + } + } if config.HideComment != nil { if err := checkMaxField("hide_comment", config.HideComment.Max); err != nil { return err diff --git a/pkg/workflow/safe_outputs_state.go b/pkg/workflow/safe_outputs_state.go index 7962bf13fb9..7420295d37c 100644 --- a/pkg/workflow/safe_outputs_state.go +++ b/pkg/workflow/safe_outputs_state.go @@ -49,6 +49,7 @@ func hasAnySafeOutputEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.CloseIssues != nil || safeOutputs.ClosePullRequests != nil || safeOutputs.MarkPullRequestAsReadyForReview != nil || + safeOutputs.DismissPullRequestReview != nil || safeOutputs.AddComments != nil || safeOutputs.CommentMemory != nil || safeOutputs.CreatePullRequests != nil || @@ -115,6 +116,7 @@ func hasNonBuiltinSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.CloseIssues != nil || safeOutputs.ClosePullRequests != nil || safeOutputs.MarkPullRequestAsReadyForReview != nil || + safeOutputs.DismissPullRequestReview != nil || safeOutputs.AddComments != nil || safeOutputs.CommentMemory != nil || safeOutputs.CreatePullRequests != nil || diff --git a/pkg/workflow/safe_outputs_tools_computation.go b/pkg/workflow/safe_outputs_tools_computation.go index 4d640cec2a1..420c62fac25 100644 --- a/pkg/workflow/safe_outputs_tools_computation.go +++ b/pkg/workflow/safe_outputs_tools_computation.go @@ -48,6 +48,10 @@ func computeEnabledToolNames(data *WorkflowData) map[string]struct { enabledTools["mark_pull_request_as_ready_for_review"] = struct { }{} } + if data.SafeOutputs.DismissPullRequestReview != nil { + enabledTools["dismiss_pull_request_review"] = struct { + }{} + } if data.SafeOutputs.AddComments != nil { enabledTools["add_comment"] = struct { }{} diff --git a/pkg/workflow/safe_outputs_tools_repo_params.go b/pkg/workflow/safe_outputs_tools_repo_params.go index e41254da6ff..57883598ff2 100644 --- a/pkg/workflow/safe_outputs_tools_repo_params.go +++ b/pkg/workflow/safe_outputs_tools_repo_params.go @@ -45,6 +45,11 @@ func addRepoParameterIfNeeded(tool map[string]any, toolName string, safeOutputs hasAllowedRepos = len(config.AllowedRepos) > 0 targetRepoSlug = config.TargetRepoSlug } + case "dismiss_pull_request_review": + if config := safeOutputs.DismissPullRequestReview; config != nil { + hasAllowedRepos = len(config.AllowedRepos) > 0 + targetRepoSlug = config.TargetRepoSlug + } case "create_agent_session": if config := safeOutputs.CreateAgentSessions; config != nil { hasAllowedRepos = len(config.AllowedRepos) > 0 diff --git a/pkg/workflow/safe_outputs_validation.go b/pkg/workflow/safe_outputs_validation.go index dbf02a50b72..f429ce0b504 100644 --- a/pkg/workflow/safe_outputs_validation.go +++ b/pkg/workflow/safe_outputs_validation.go @@ -141,6 +141,9 @@ func validateSafeOutputsTarget(config *SafeOutputsConfig) error { if config.MarkPullRequestAsReadyForReview != nil { configs = append(configs, targetConfig{"mark-pull-request-as-ready-for-review", config.MarkPullRequestAsReadyForReview.Target}) } + if config.DismissPullRequestReview != nil { + configs = append(configs, targetConfig{"dismiss-pull-request-review", config.DismissPullRequestReview.Target}) + } if config.AddComments != nil { configs = append(configs, targetConfig{"add-comment", config.AddComments.Target}) } diff --git a/pkg/workflow/safe_outputs_validation_config.go b/pkg/workflow/safe_outputs_validation_config.go index 9257e5f1409..ccb4a377874 100644 --- a/pkg/workflow/safe_outputs_validation_config.go +++ b/pkg/workflow/safe_outputs_validation_config.go @@ -456,6 +456,16 @@ var ValidationConfig = map[string]TypeValidationConfig{ "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" }, }, + "dismiss_pull_request_review": { + DefaultMax: 10, + Fields: map[string]FieldValidation{ + "review_id": {Required: true, PositiveInteger: true}, + "justification": {Required: true, Type: "string", Sanitize: true, MinLength: 20, MaxLength: MaxBodyLength}, + "author": {Type: "string", Sanitize: true, MaxLength: 128}, + "pull_request_number": {IssueOrPRNumber: true}, + "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" + }, + }, } // validationConfigJSONCache caches GetValidationConfigJSON results keyed by the sorted, diff --git a/pkg/workflow/tool_description_enhancer.go b/pkg/workflow/tool_description_enhancer.go index 73416174139..dc394d133ec 100644 --- a/pkg/workflow/tool_description_enhancer.go +++ b/pkg/workflow/tool_description_enhancer.go @@ -291,6 +291,20 @@ func enhanceToolDescription(toolName, baseDescription string, safeOutputs *SafeO } } + case "dismiss_pull_request_review": + if config := safeOutputs.DismissPullRequestReview; config != nil { + if templatableIntValue(config.Max) > 0 { + constraints = append(constraints, fmt.Sprintf("Maximum %d review dismissal(s) can be performed.", templatableIntValue(config.Max))) + } + if config.Target != "" { + constraints = append(constraints, fmt.Sprintf("Target: %s.", config.Target)) + } + if config.TargetRepoSlug != "" { + constraints = append(constraints, fmt.Sprintf("Review dismissals will be performed in repository %q.", config.TargetRepoSlug)) + } + constraints = append(constraints, "justification must contain at least 20 characters.") + } + case "resolve_pull_request_review_thread": if config := safeOutputs.ResolvePullRequestReviewThread; config != nil { if templatableIntValue(config.Max) > 0 { diff --git a/pkg/workflow/unified_prompt_step.go b/pkg/workflow/unified_prompt_step.go index 0529bc34067..0e9b79cf695 100644 --- a/pkg/workflow/unified_prompt_step.go +++ b/pkg/workflow/unified_prompt_step.go @@ -574,6 +574,9 @@ func buildSafeOutputsSections(safeOutputs *SafeOutputsConfig) []PromptSection { if safeOutputs.MarkPullRequestAsReadyForReview != nil { tools = append(tools, toolWithMaxBudget("mark_pull_request_as_ready_for_review", safeOutputs.MarkPullRequestAsReadyForReview.Max)) } + if safeOutputs.DismissPullRequestReview != nil { + tools = append(tools, toolWithMaxBudget("dismiss_pull_request_review", safeOutputs.DismissPullRequestReview.Max)) + } if safeOutputs.CreatePullRequestReviewComments != nil { tools = append(tools, toolWithMaxBudget("create_pull_request_review_comment", safeOutputs.CreatePullRequestReviewComments.Max)) } diff --git a/schemas/agent-output.json b/schemas/agent-output.json index 38d1049c161..82d7368c745 100644 --- a/schemas/agent-output.json +++ b/schemas/agent-output.json @@ -54,6 +54,7 @@ { "$ref": "#/$defs/DispatchWorkflowOutput" }, { "$ref": "#/$defs/AutofixCodeScanningAlertOutput" }, { "$ref": "#/$defs/SubmitPullRequestReviewOutput" }, + { "$ref": "#/$defs/DismissPullRequestReviewOutput" }, { "$ref": "#/$defs/ReplyToPullRequestReviewCommentOutput" }, { "$ref": "#/$defs/ResolvePullRequestReviewThreadOutput" } ] @@ -402,6 +403,39 @@ "required": ["type", "comment_id", "body"], "additionalProperties": false }, + "DismissPullRequestReviewOutput": { + "title": "Dismiss Pull Request Review Output", + "description": "Output for dismissing an existing pull request review authored by the current workflow actor.", + "type": "object", + "properties": { + "type": { + "const": "dismiss_pull_request_review" + }, + "review_id": { + "type": ["number", "string"], + "description": "The numeric review ID to dismiss." + }, + "justification": { + "type": "string", + "description": "Dismissal justification message (minimum 20 characters).", + "minLength": 20 + }, + "author": { + "type": "string", + "description": "Optional review author login. If provided, it must match the current workflow actor." + }, + "pull_request_number": { + "type": ["number", "string"], + "description": "Pull request number (optional - uses triggering PR if not provided)" + }, + "repo": { + "type": "string", + "description": "Optional target repository in owner/repo format." + } + }, + "required": ["type", "review_id", "justification"], + "additionalProperties": false + }, "ResolvePullRequestReviewThreadOutput": { "title": "Resolve Pull Request Review Thread Output", "description": "Output for resolving a review thread on a pull request. Marks a review conversation as resolved after the feedback has been addressed.", From fea663ce2320ee3c2a87fd3219accd1bc42d09fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 07:07:22 +0000 Subject: [PATCH 2/5] docs(adr): add draft ADR-43125 for actor-bound dismiss-review safe output --- ...-actor-bound-dismiss-review-safe-output.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/adr/43125-add-actor-bound-dismiss-review-safe-output.md diff --git a/docs/adr/43125-add-actor-bound-dismiss-review-safe-output.md b/docs/adr/43125-add-actor-bound-dismiss-review-safe-output.md new file mode 100644 index 00000000000..aaa7cc5e6eb --- /dev/null +++ b/docs/adr/43125-add-actor-bound-dismiss-review-safe-output.md @@ -0,0 +1,43 @@ +# ADR-43125: Add Actor-Bound Dismiss-Review Safe Output + +**Date**: 2026-07-03 +**Status**: Draft +**Deciders**: pelikhan, copilot-swe-agent + +--- + +### Context + +The safe-outputs system controls which GitHub API mutations AI agents can perform during workflow execution. Before this change, agents could submit PR reviews but had no way to dismiss them — requiring human intervention to clear stale reviews. Agents need to dismiss their own previously-submitted reviews when subsequent PR changes make those reviews outdated. The core constraint is that agents must only be permitted to dismiss reviews they themselves authored; allowing dismissal of arbitrary reviews would enable one actor to silence another's feedback, which is a material security and trust boundary. + +### Decision + +We will add `dismiss_pull_request_review` (aliased as `dismiss-review`) as a new safe-output type. The handler enforces three invariants before calling `pulls.dismissReview`: the `justification` must be at least 20 characters, the caller-supplied `author` field (if provided) must match the inferred workflow actor (`GITHUB_ACTOR`), and the review fetched from the API must also have been authored by that same actor. These checks are enforced at both the MCP pre-validation layer and the runtime execution layer. + +### Alternatives Considered + +#### Alternative 1: Allow dismissal of any review (no actor-bound constraint) + +Agents could dismiss any review regardless of author, simplifying the implementation by removing identity checks. This was rejected because it would allow an agent to silently clear human reviewer feedback without authorization, undermining the review process and creating a significant privilege-escalation vector within the safe-outputs trust boundary. + +#### Alternative 2: Omit review dismissal from safe-outputs entirely + +Review dismissal could be left as a human-only operation. This preserves the narrowest possible safe-outputs surface area. This was rejected because it forces manual intervention in automated PR workflows where agents legitimately need to dismiss their own stale reviews after code updates, reducing the practical utility of the agent review system. + +### Consequences + +#### Positive +- Agents can automate the full review lifecycle (submit → update → dismiss) without requiring human intervention for stale-review cleanup. +- The actor-bound invariant (checked at both MCP and runtime layers) establishes a clear, auditable security boundary: an agent can only dismiss what it authored. + +#### Negative +- The actor identity is resolved from `GITHUB_ACTOR` at runtime; workflows that run under different actors across re-runs may find the invariant unexpectedly restrictive if the actor changes between the review submission and the dismissal attempt. +- Adding another safe-output type increases the attack surface of the safe-outputs system and requires coordinated updates across config parsing, schema, type declarations, handler registry, validation, and prompt construction — a high-touch pattern that is easy to implement inconsistently. + +#### Neutral +- The `dismiss-review` alias mirrors the naming convention used by `submit-pull-request-review` / `submit-review`, keeping the YAML surface consistent for workflow authors. +- The 20-character minimum for `justification` is enforced identically in both the MCP validation layer and the runtime handler, which adds redundancy but also defense-in-depth. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From 6958b0140e105bd4c86e9414864a11bd7ffa5367 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 07:44:43 +0000 Subject: [PATCH 3/5] Clarify dismiss-review actor inference text Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/safe_outputs_tools.json | 2 +- pkg/workflow/js/safe_outputs_tools.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index db3e770565a..cf17416397b 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -512,7 +512,7 @@ }, { "name": "dismiss_pull_request_review", - "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (typically github-actions[bot]). You must provide a justification of at least 20 characters.", + "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (github.actor / GITHUB_ACTOR). You must provide a justification of at least 20 characters.", "inputSchema": { "type": "object", "required": ["review_id", "justification"], diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 52826437dc0..845cbc9ee05 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -656,7 +656,7 @@ }, { "name": "dismiss_pull_request_review", - "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (typically github-actions[bot]). You must provide a justification of at least 20 characters.", + "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (github.actor / GITHUB_ACTOR). You must provide a justification of at least 20 characters.", "inputSchema": { "type": "object", "required": ["review_id", "justification"], From b344f46d01e17a1a5dcc243cd67535263c5f5e35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 04:51:57 +0000 Subject: [PATCH 4/5] Stabilize GH token env test and satisfy workflow lint gate Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/arc_dind_artifacts.go | 4 ++-- pkg/workflow/github_cli_test.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/arc_dind_artifacts.go b/pkg/workflow/arc_dind_artifacts.go index b679240dbc1..ee566fc208e 100644 --- a/pkg/workflow/arc_dind_artifacts.go +++ b/pkg/workflow/arc_dind_artifacts.go @@ -15,9 +15,9 @@ import ( func rewriteTmpGhAwPathsForArcDind(paths []string) []string { result := make([]string, len(paths)) for i, p := range paths { - if strings.HasPrefix(p, constants.TmpGhAwDirSlash) { + if suffix, ok := strings.CutPrefix(p, constants.TmpGhAwDirSlash); ok { // /tmp/gh-aw/foo → ${{ runner.temp }}/gh-aw/foo - result[i] = constants.GhAwRootDir + "/" + strings.TrimPrefix(p, constants.TmpGhAwDirSlash) + result[i] = constants.GhAwRootDir + "/" + suffix } else if p == constants.TmpGhAwDir { result[i] = constants.GhAwRootDir } else { diff --git a/pkg/workflow/github_cli_test.go b/pkg/workflow/github_cli_test.go index ab113d41c57..c7c172f02c7 100644 --- a/pkg/workflow/github_cli_test.go +++ b/pkg/workflow/github_cli_test.go @@ -142,6 +142,16 @@ func TestExecGHUsesConfiguredProcessEnvLookup(t *testing.T) { }) t.Run("does not inject gh token when both tokens are absent", func(t *testing.T) { + originalGHToken, ghTokenWasSet := os.LookupEnv("GH_TOKEN") + if ghTokenWasSet { + require.NoError(t, os.Unsetenv("GH_TOKEN")) + } + t.Cleanup(func() { + if ghTokenWasSet { + require.NoError(t, os.Setenv("GH_TOKEN", originalGHToken)) + } + }) + SetProcessEnvLookup(func(key string) (string, bool) { return "", false }) From 41a302d2f79fac860b8ee266472f7890037f9391 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 07:04:01 +0000 Subject: [PATCH 5/5] Fix dismiss review schema descriptions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_tools.json | 4 ++-- pkg/workflow/js/safe_outputs_tools.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index cf17416397b..954e8c85fc0 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -512,14 +512,14 @@ }, { "name": "dismiss_pull_request_review", - "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (github.actor / GITHUB_ACTOR). You must provide a justification of at least 20 characters.", + "description": "Use this tool to dismiss a pull request review when the review is stale after automated updates. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (github.actor / GITHUB_ACTOR). You must provide a justification of at least 20 characters.", "inputSchema": { "type": "object", "required": ["review_id", "justification"], "properties": { "review_id": { "type": ["number", "string"], - "description": "The numeric review ID to dismiss.", + "description": "Numeric pull request review ID to dismiss (for example, 123456789).", "x-synonyms": ["reviewId"] }, "justification": { diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 845cbc9ee05..822b43277f6 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -656,14 +656,14 @@ }, { "name": "dismiss_pull_request_review", - "description": "Dismiss a pull request review. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (github.actor / GITHUB_ACTOR). You must provide a justification of at least 20 characters.", + "description": "Use this tool to dismiss a pull request review when the review is stale after automated updates. The review must be authored by the same workflow actor who is dismissing it. By default, the actor is inferred from the current GitHub Actions context (github.actor / GITHUB_ACTOR). You must provide a justification of at least 20 characters.", "inputSchema": { "type": "object", "required": ["review_id", "justification"], "properties": { "review_id": { "type": ["number", "string"], - "description": "The numeric review ID to dismiss.", + "description": "Numeric pull request review ID to dismiss (for example, 123456789).", "x-synonyms": ["reviewId"] }, "justification": {