diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 4d966720d04..b4139002238 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -1016,7 +1016,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write outputs: @@ -1275,7 +1274,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: read - discussions: write issues: write pull-requests: write timeout-minutes: 15 diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 9409c952d50..1e498f6d26b 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4817,6 +4817,10 @@ "type": "string", "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] } + }, + "discussions": { + "type": "boolean", + "description": "Controls whether the workflow requests discussions:write permission for add-comment. Default: true (includes discussions:write). Set to false if your GitHub App lacks Discussions permission to prevent 422 errors during token generation." } }, "additionalProperties": false, @@ -5736,6 +5740,10 @@ "type": "string", "enum": ["spam", "abuse", "off_topic", "outdated", "resolved"] } + }, + "discussions": { + "type": "boolean", + "description": "Controls whether the workflow requests discussions:write permission for hide-comment. Default: true (includes discussions:write). Set to false if your GitHub App lacks Discussions permission to prevent 422 errors during token generation." } }, "additionalProperties": false diff --git a/pkg/workflow/add_comment.go b/pkg/workflow/add_comment.go index ffd52ea953c..05630953bb5 100644 --- a/pkg/workflow/add_comment.go +++ b/pkg/workflow/add_comment.go @@ -23,6 +23,7 @@ type AddCommentsConfig struct { Discussion *bool `yaml:"discussion,omitempty"` // Target discussion comments instead of issue/PR comments. Must be true if present. HideOlderComments bool `yaml:"hide-older-comments,omitempty"` // When true, minimizes/hides all previous comments from the same workflow before creating the new comment AllowedReasons []string `yaml:"allowed-reasons,omitempty"` // List of allowed reasons for hiding older comments (default: all reasons allowed) + Discussions *bool `yaml:"discussions,omitempty"` // When false, excludes discussions:write permission. Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission. } // buildCreateOutputAddCommentJob creates the add_comment job @@ -113,6 +114,15 @@ func (c *Compiler) buildCreateOutputAddCommentJob(data *WorkflowData, mainJobNam needs = append(needs, createPullRequestJobName) } + // Determine permissions based on discussions field + // Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission + var permissions *Permissions + if data.SafeOutputs.AddComments.Discussions != nil && !*data.SafeOutputs.AddComments.Discussions { + permissions = NewPermissionsContentsReadIssuesWritePRWrite() + } else { + permissions = NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite() + } + // Use the shared builder function to create the job return c.buildSafeOutputJob(data, SafeOutputJobConfig{ JobName: "add_comment", @@ -121,7 +131,7 @@ func (c *Compiler) buildCreateOutputAddCommentJob(data *WorkflowData, mainJobNam MainJobName: mainJobName, CustomEnvVars: customEnvVars, Script: getAddCommentScript(), - Permissions: NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite(), + Permissions: permissions, Outputs: outputs, Condition: jobCondition, Needs: needs, diff --git a/pkg/workflow/hide_comment.go b/pkg/workflow/hide_comment.go index 694d0e1581c..c487a9d70a5 100644 --- a/pkg/workflow/hide_comment.go +++ b/pkg/workflow/hide_comment.go @@ -11,6 +11,7 @@ type HideCommentConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` AllowedReasons []string `yaml:"allowed-reasons,omitempty"` // List of allowed reasons for hiding comments (default: all reasons allowed) + Discussions *bool `yaml:"discussions,omitempty"` // When false, excludes discussions:write permission. Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission. } // parseHideCommentConfig handles hide-comment configuration diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index b4b8ce8feb7..2d4e304afa4 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -192,7 +192,7 @@ }, { "name": "add_comment", - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance.", + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance. NOTE: By default, this tool requires discussions:write permission. If your GitHub App lacks Discussions permission, set 'discussions: false' in the workflow's safe-outputs.add-comment configuration to exclude this permission.", "inputSchema": { "type": "object", "required": [ @@ -844,7 +844,7 @@ }, { "name": "hide_comment", - "description": "Hide a comment on a GitHub issue, pull request, or discussion. This collapses the comment and marks it as spam, abuse, off-topic, outdated, or resolved. Use this for inappropriate, off-topic, or outdated comments. The comment_id must be a GraphQL node ID (string like 'IC_kwDOABCD123456'), not a numeric REST API comment ID.", + "description": "Hide a comment on a GitHub issue, pull request, or discussion. This collapses the comment and marks it as spam, abuse, off-topic, outdated, or resolved. Use this for inappropriate, off-topic, or outdated comments. The comment_id must be a GraphQL node ID (string like 'IC_kwDOABCD123456'), not a numeric REST API comment ID. NOTE: By default, this tool requires discussions:write permission. If your GitHub App lacks Discussions permission, set 'discussions: false' in the workflow's safe-outputs.hide-comment configuration to exclude this permission.", "inputSchema": { "type": "object", "required": [ diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go index a8f1faf61be..57979abaf7c 100644 --- a/pkg/workflow/safe_outputs_permissions.go +++ b/pkg/workflow/safe_outputs_permissions.go @@ -30,7 +30,13 @@ func computePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio } if safeOutputs.AddComments != nil { safeOutputsPermissionsLog.Print("Adding permissions for add-comment") - permissions.Merge(NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite()) + // Check if discussions permission should be excluded (discussions: false) + // Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission + if safeOutputs.AddComments.Discussions != nil && !*safeOutputs.AddComments.Discussions { + permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite()) + } else { + permissions.Merge(NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite()) + } } if safeOutputs.CloseIssues != nil { safeOutputsPermissionsLog.Print("Adding permissions for close-issue") @@ -97,7 +103,13 @@ func computePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio } if safeOutputs.HideComment != nil { safeOutputsPermissionsLog.Print("Adding permissions for hide-comment") - permissions.Merge(NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite()) + // Check if discussions permission should be excluded (discussions: false) + // Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission + if safeOutputs.HideComment.Discussions != nil && !*safeOutputs.HideComment.Discussions { + permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite()) + } else { + permissions.Merge(NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite()) + } } if safeOutputs.DispatchWorkflow != nil { safeOutputsPermissionsLog.Print("Adding permissions for dispatch-workflow") diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go index c570e6deb46..3827f47ba85 100644 --- a/pkg/workflow/safe_outputs_permissions_test.go +++ b/pkg/workflow/safe_outputs_permissions_test.go @@ -72,7 +72,7 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { }, }, { - name: "add-comment includes all write permissions including discussions", + name: "add-comment default - includes discussions permission", safeOutputs: &SafeOutputsConfig{ AddComments: &AddCommentsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, @@ -86,7 +86,21 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { }, }, { - name: "hide-comment includes all write permissions including discussions", + name: "add-comment with discussions:false - no discussions permission", + safeOutputs: &SafeOutputsConfig{ + AddComments: &AddCommentsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, + Discussions: ptrBool(false), + }, + }, + expected: map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionIssues: PermissionWrite, + PermissionPullRequests: PermissionWrite, + }, + }, + { + name: "hide-comment default - includes discussions permission", safeOutputs: &SafeOutputsConfig{ HideComment: &HideCommentConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, @@ -99,6 +113,20 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { PermissionDiscussions: PermissionWrite, }, }, + { + name: "hide-comment with discussions:false - no discussions permission", + safeOutputs: &SafeOutputsConfig{ + HideComment: &HideCommentConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1}, + Discussions: ptrBool(false), + }, + }, + expected: map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionIssues: PermissionWrite, + PermissionPullRequests: PermissionWrite, + }, + }, { name: "add-labels only - no discussions permission", safeOutputs: &SafeOutputsConfig{