From c37e73eb4591dbab72211bc5f6d497509c7b0655 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:39:32 +0000 Subject: [PATCH 1/6] Initial plan From 363923f30fa618ec0fdbec70a5412ee946eb677c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:52:25 +0000 Subject: [PATCH 2/6] Initial plan: fix workflow_call event permissions for reactions/status-comments Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/pr-code-quality-reviewer.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-code-quality-reviewer.lock.yml b/.github/workflows/pr-code-quality-reviewer.lock.yml index 40f9477777c..5c633517b95 100644 --- a/.github/workflows/pr-code-quality-reviewer.lock.yml +++ b/.github/workflows/pr-code-quality-reviewer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"661ab4bd42bd83eff79d7d460f50a12e3cee567ac434b2d37a2480d9a0e20965","body_hash":"4d3260d49c3010a13ea7c49bba10f51f2b9f1c1473ceca69008cfe766a9b273c","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.63","copilot-sdk":"1.0.1"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"57c59bcb86591e8b21ad20298687f8d1adf895d5c4cf6f909d3339905428cc28","body_hash":"4d3260d49c3010a13ea7c49bba10f51f2b9f1c1473ceca69008cfe766a9b273c","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.63","copilot-sdk":"1.0.1"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.4","digest":"sha256:b268ebf37df2428b19efcb383f001d65dc6a5ec10af43feb886d1a8477ab0e3a","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.4@sha256:b268ebf37df2428b19efcb383f001d65dc6a5ec10af43feb886d1a8477ab0e3a"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.4","digest":"sha256:3ea0d12a2d124db8ed6e2d18aff040e30ab3568161f258a132fccdeede4198cd","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.4@sha256:3ea0d12a2d124db8ed6e2d18aff040e30ab3568161f258a132fccdeede4198cd"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.4","digest":"sha256:72c378c029d2fad4684847ab44c329e526ac6b1a78cdf97656870ea11d201545","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.4@sha256:72c378c029d2fad4684847ab44c329e526ac6b1a78cdf97656870ea11d201545"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.4","digest":"sha256:87979038897e40caed22245b64d1daa796390d2dca289b99d3d1174c85740af8","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.4@sha256:87979038897e40caed22245b64d1daa796390d2dca289b99d3d1174c85740af8"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.26","digest":"sha256:d3b03f54eee3a8176818c9a52087623e45b7f644a28814337fcc0838e2534490","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.26@sha256:d3b03f54eee3a8176818c9a52087623e45b7f644a28814337fcc0838e2534490"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # From a1e2ecf0f09385c0fc8c6737f34d6787a447dd90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:58:41 +0000 Subject: [PATCH 3/6] fix: grant broad interaction permissions for workflow_call trigger with reactions/status-comments Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../activation_permissions_scope_test.go | 98 +++++++++++++++++++ .../compiler_activation_job_builder.go | 32 ++++++ 2 files changed, 130 insertions(+) diff --git a/pkg/workflow/activation_permissions_scope_test.go b/pkg/workflow/activation_permissions_scope_test.go index 57212c6cd10..2c0de6fba67 100644 --- a/pkg/workflow/activation_permissions_scope_test.go +++ b/pkg/workflow/activation_permissions_scope_test.go @@ -543,3 +543,101 @@ engine: copilot assert.Contains(t, activationJobSection, "pull-requests: write", "activation job should include pull-requests:write for slash_command PR comment reactions") assert.NotContains(t, activationJobSection, "discussions: write", "activation job should not include discussions:write for slash_command PR comment reactions") } + +// Tests for workflow_call trigger + reaction/status-comment permissions (issue #39372). +// When a workflow declares workflow_call as its trigger it acts as a reusable workflow and can +// be called from ANY caller event. The compiler cannot know the caller's event type at compile +// time, so it must grant the full set of permissions that the configured reactions / status-comments +// could require. + +func TestActivationPermissionsWorkflowCallReaction(t *testing.T) { + tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-reaction") + testFile := filepath.Join(tmpDir, "workflow-call-reaction.md") + testContent := `--- +on: + workflow_call: + reaction: eyes +engine: copilot +--- + +# Reusable workflow with reaction +` + + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err, "failed to write test workflow") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + + activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) + assert.Contains(t, activationJobSection, "issues: write", "activation job should include issues: write when workflow_call + reaction are configured") + assert.Contains(t, activationJobSection, "pull-requests: write", "activation job should include pull-requests: write when workflow_call + reaction are configured") + assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call + reaction are configured") +} + +func TestActivationPermissionsWorkflowCallStatusComment(t *testing.T) { + tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-status-comment") + testFile := filepath.Join(tmpDir, "workflow-call-status-comment.md") + testContent := `--- +on: + workflow_call: + reaction: none + status-comment: true +engine: copilot +--- + +# Reusable workflow with status-comment only +` + + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err, "failed to write test workflow") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + + activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) + assert.Contains(t, activationJobSection, "issues: write", "activation job should include issues: write when workflow_call + status-comment are configured") + // pull-requests:write is only needed for PR reactions, not status-comments (which use the issues API) + assert.NotContains(t, activationJobSection, "pull-requests: write", "activation job should not include pull-requests: write for status-comment-only (status-comments use issues API)") + // discussions:write is included because status-comment includes discussions by default + assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call + status-comment with default discussion target are configured") +} + +func TestActivationPermissionsWorkflowCallAndIssuesTriggerReaction(t *testing.T) { + tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-and-issues-reaction") + testFile := filepath.Join(tmpDir, "workflow-call-and-issues-reaction.md") + testContent := `--- +on: + workflow_call: + issues: + types: [labeled] + reaction: eyes +engine: copilot +--- + +# Reusable workflow with workflow_call and issues trigger +` + + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err, "failed to write test workflow") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + + activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) + assert.Contains(t, activationJobSection, "issues: write", "activation job should include issues: write when workflow_call+issues triggers + reaction are configured") + assert.Contains(t, activationJobSection, "pull-requests: write", "activation job should include pull-requests: write when workflow_call is present (broad permissions)") + assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call is present (broad permissions)") +} diff --git a/pkg/workflow/compiler_activation_job_builder.go b/pkg/workflow/compiler_activation_job_builder.go index 51b38178b79..449654cddf5 100644 --- a/pkg/workflow/compiler_activation_job_builder.go +++ b/pkg/workflow/compiler_activation_job_builder.go @@ -726,6 +726,7 @@ func (c *Compiler) addActivationArtifactUploadStep(ctx *activationJobBuildContex func (c *Compiler) buildActivationPermissions(ctx *activationJobBuildContext) (string, error) { permsMap := c.buildActivationBasePermissions(ctx) c.addCentralizedCommandActivationPermissions(permsMap, ctx) + c.addWorkflowCallActivationPermissions(permsMap, ctx) c.addActivationLabelPermissions(permsMap, ctx) if err := c.addActivationScriptPermissions(permsMap, ctx); err != nil { return "", err @@ -777,6 +778,37 @@ func (c *Compiler) addCentralizedCommandActivationPermissions(permsMap map[Permi } } +// addWorkflowCallActivationPermissions supplements the activation job's permission map when the +// workflow is triggered via workflow_call (i.e. it is used as a reusable workflow). +// +// At compile time it is impossible to know which GitHub event will fire in the *calling* workflow, +// so the compiler cannot restrict permissions to a specific event type (e.g. "issues" or +// "pull_request"). Instead it falls back to the broad permission set: all permission scopes that +// the configured reactions / status-comments could ever need are granted, respecting the per-type +// opt-out flags (reaction.issues, reaction.pull-requests, etc.). +// +// This mirrors the handling for centralized slash_command workflows that compile to workflow_dispatch +// (see addCentralizedCommandActivationPermissions). +func (c *Compiler) addWorkflowCallActivationPermissions(permsMap map[PermissionScope]PermissionLevel, ctx *activationJobBuildContext) { + if !hasWorkflowCallTrigger(ctx.data.On) { + return + } + if !ctx.hasReaction && !ctx.hasStatusComment { + return + } + compilerActivationJobLog.Print("workflow_call trigger detected; applying broad interaction permissions for reactions/status-comments") + addBroadActivationInteractionPermissions(permsMap, activationInteractionPermissionsOptions{ + hasReaction: ctx.hasReaction, + reactionIncludesIssues: ctx.reactionIssues, + reactionIncludesPullRequests: ctx.reactionPullRequests, + reactionIncludesDiscussions: ctx.reactionDiscussions, + hasStatusComment: ctx.hasStatusComment, + statusCommentIncludesIssues: ctx.statusCommentIssues, + statusCommentIncludesPullRequests: ctx.statusCommentPRs, + statusCommentIncludesDiscussions: ctx.statusCommentDiscussions, + }) +} + func (c *Compiler) addActivationLabelPermissions(permsMap map[PermissionScope]PermissionLevel, ctx *activationJobBuildContext) { if ctx.data.LockForAgent { permsMap[PermissionIssues] = PermissionWrite From a0710b17b99d1dd9574992f88e580d8e969ec586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:03:15 +0000 Subject: [PATCH 4/6] fix: clarify test comment messages for workflow_call status-comment permissions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/activation_permissions_scope_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/activation_permissions_scope_test.go b/pkg/workflow/activation_permissions_scope_test.go index 2c0de6fba67..9f8d7a0e92f 100644 --- a/pkg/workflow/activation_permissions_scope_test.go +++ b/pkg/workflow/activation_permissions_scope_test.go @@ -605,10 +605,11 @@ engine: copilot activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) assert.Contains(t, activationJobSection, "issues: write", "activation job should include issues: write when workflow_call + status-comment are configured") - // pull-requests:write is only needed for PR reactions, not status-comments (which use the issues API) - assert.NotContains(t, activationJobSection, "pull-requests: write", "activation job should not include pull-requests: write for status-comment-only (status-comments use issues API)") - // discussions:write is included because status-comment includes discussions by default - assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call + status-comment with default discussion target are configured") + // pull-requests:write is only needed for PR reactions (addBroadActivationInteractionPermissions only sets it for + // hasReaction && reactionIncludesPullRequests); PR status-comments post via the issues API so issues:write suffices. + assert.NotContains(t, activationJobSection, "pull-requests: write", "activation job should not include pull-requests: write for status-comment-only (PR status-comments use the issues API scope, not pull-requests)") + // discussions:write is included because status-comment defaults include discussions (statusCommentIncludesDiscussions=true by default) + assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call + status-comment are configured (discussions enabled by default)") } func TestActivationPermissionsWorkflowCallAndIssuesTriggerReaction(t *testing.T) { From ab39b5a693583e9f5bdd688347aa880e2e822503 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:28:26 +0000 Subject: [PATCH 5/6] fix: tighten workflow_call interaction permission scoping Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../activation_permissions_scope_test.go | 87 +++++++++++++++++++ pkg/workflow/compiler_activation_job.go | 6 +- .../compiler_activation_job_builder.go | 19 +++- 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/activation_permissions_scope_test.go b/pkg/workflow/activation_permissions_scope_test.go index 9f8d7a0e92f..a0da29fecfb 100644 --- a/pkg/workflow/activation_permissions_scope_test.go +++ b/pkg/workflow/activation_permissions_scope_test.go @@ -224,6 +224,28 @@ func TestAddActivationInteractionPermissionsMapFallbackRespectsStatusCommentDisc assert.False(t, hasDiscussions, "fallback should omit discussions:write when status-comment.discussions is false and reactions are disabled") } +func TestAddActivationInteractionPermissionsMapFallbackIgnoresStatusCommentDefaultsWhenDisabled(t *testing.T) { + permsMap := map[PermissionScope]PermissionLevel{} + + addActivationInteractionPermissionsMap(permsMap, activationInteractionPermissionsOptions{ + onSection: "on: [", + hasReaction: true, + reactionIncludesIssues: false, + reactionIncludesPullRequests: false, + reactionIncludesDiscussions: true, + hasStatusComment: false, + statusCommentIncludesIssues: true, + statusCommentIncludesPullRequests: true, + statusCommentIncludesDiscussions: true, + }) + + _, hasIssues := permsMap[PermissionIssues] + assert.False(t, hasIssues, "fallback should not include issues:write when status-comment is disabled") + _, hasPullRequests := permsMap[PermissionPullRequests] + assert.False(t, hasPullRequests, "fallback should not include pull-requests:write for discussions-only reactions") + assert.Equal(t, PermissionWrite, permsMap[PermissionDiscussions], "fallback should include discussions:write for discussions reactions") +} + func TestActivationPermissionsStatusCommentIssuesDisabled(t *testing.T) { tmpDir := testutil.TempDir(t, "activation-perms-status-comment-issues-disabled") testFile := filepath.Join(tmpDir, "status-comment-issues-disabled.md") @@ -579,6 +601,39 @@ engine: copilot assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call + reaction are configured") } +func TestActivationPermissionsWorkflowCallReactionDiscussionsOnly(t *testing.T) { + tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-reaction-discussions-only") + testFile := filepath.Join(tmpDir, "workflow-call-reaction-discussions-only.md") + testContent := `--- +on: + workflow_call: + reaction: + type: eyes + issues: false + pull-requests: false + discussions: true +engine: copilot +--- + +# Reusable workflow with discussions-only reaction +` + + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err, "failed to write test workflow") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + + activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) + assert.NotContains(t, activationJobSection, "issues: write", "activation job should not include issues: write when workflow_call + discussions-only reaction are configured") + assert.NotContains(t, activationJobSection, "pull-requests: write", "activation job should not include pull-requests: write when workflow_call + discussions-only reaction are configured") + assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call + discussions-only reaction are configured") +} + func TestActivationPermissionsWorkflowCallStatusComment(t *testing.T) { tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-status-comment") testFile := filepath.Join(tmpDir, "workflow-call-status-comment.md") @@ -642,3 +697,35 @@ engine: copilot assert.Contains(t, activationJobSection, "pull-requests: write", "activation job should include pull-requests: write when workflow_call is present (broad permissions)") assert.Contains(t, activationJobSection, "discussions: write", "activation job should include discussions: write when workflow_call is present (broad permissions)") } + +func TestActivationPermissionsWorkflowCallReactionWithGitHubApp(t *testing.T) { + tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-reaction-app") + testFile := filepath.Join(tmpDir, "workflow-call-reaction-app.md") + testContent := `--- +on: + workflow_call: + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_KEY }} + reaction: eyes +engine: copilot +--- + +# Reusable workflow with reaction and activation GitHub App +` + + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err, "failed to write test workflow") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + + activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) + assert.Contains(t, activationJobSection, "permission-issues: write", "activation app token should include permission-issues: write when workflow_call + reaction are configured") + assert.Contains(t, activationJobSection, "permission-pull-requests: write", "activation app token should include permission-pull-requests: write when workflow_call + reaction are configured") + assert.Contains(t, activationJobSection, "permission-discussions: write", "activation app token should include permission-discussions: write when workflow_call + reaction are configured") +} diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index f68e6b98c3d..761f0e3ccb3 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -185,14 +185,16 @@ func addBroadActivationInteractionPermissions( } needsIssuesWriteForReaction := options.hasReaction && (options.reactionIncludesIssues || options.reactionIncludesPullRequests) - needsIssuesWriteForStatusComment := options.statusCommentIncludesIssues || options.statusCommentIncludesPullRequests + needsIssuesWriteForStatusComment := options.hasStatusComment && + (options.statusCommentIncludesIssues || options.statusCommentIncludesPullRequests) if needsIssuesWriteForReaction || needsIssuesWriteForStatusComment { permsMap[PermissionIssues] = PermissionWrite } if options.hasReaction && options.reactionIncludesPullRequests { permsMap[PermissionPullRequests] = PermissionWrite } - if (options.hasReaction && options.reactionIncludesDiscussions) || options.statusCommentIncludesDiscussions { + if (options.hasReaction && options.reactionIncludesDiscussions) || + (options.hasStatusComment && options.statusCommentIncludesDiscussions) { permsMap[PermissionDiscussions] = PermissionWrite } } diff --git a/pkg/workflow/compiler_activation_job_builder.go b/pkg/workflow/compiler_activation_job_builder.go index 449654cddf5..151eca9f93b 100644 --- a/pkg/workflow/compiler_activation_job_builder.go +++ b/pkg/workflow/compiler_activation_job_builder.go @@ -249,6 +249,21 @@ func buildActivationAppTokenPermissions(ctx *activationJobBuildContext) *Permiss statusCommentIncludesDiscussions: ctx.statusCommentDiscussions, }, ) + if hasWorkflowCallTrigger(ctx.data.On) && (ctx.hasReaction || ctx.hasStatusComment) { + addActivationInteractionPermissions( + appPerms, + activationInteractionPermissionsOptions{ + hasReaction: ctx.hasReaction, + reactionIncludesIssues: ctx.reactionIssues, + reactionIncludesPullRequests: ctx.reactionPullRequests, + reactionIncludesDiscussions: ctx.reactionDiscussions, + hasStatusComment: ctx.hasStatusComment, + statusCommentIncludesIssues: ctx.statusCommentIssues, + statusCommentIncludesPullRequests: ctx.statusCommentPRs, + statusCommentIncludesDiscussions: ctx.statusCommentDiscussions, + }, + ) + } // Keep this aligned with addActivationLabelPermissions: app-token scopes are // computed separately from GITHUB_TOKEN scopes because app-token permissions // only apply to steps using the minted app token, while label permissions in @@ -787,8 +802,8 @@ func (c *Compiler) addCentralizedCommandActivationPermissions(permsMap map[Permi // the configured reactions / status-comments could ever need are granted, respecting the per-type // opt-out flags (reaction.issues, reaction.pull-requests, etc.). // -// This mirrors the handling for centralized slash_command workflows that compile to workflow_dispatch -// (see addCentralizedCommandActivationPermissions). +// Because the caller event type is unknown at compile time, this path always uses the broad +// fallback (addBroadActivationInteractionPermissions) instead of event-aware trigger parsing. func (c *Compiler) addWorkflowCallActivationPermissions(permsMap map[PermissionScope]PermissionLevel, ctx *activationJobBuildContext) { if !hasWorkflowCallTrigger(ctx.data.On) { return From 397b3f6c95e9dae34146a3e2557c9f0ff1f405b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:53:01 +0000 Subject: [PATCH 6/6] test: add workflow_call app-token opt-out coverage Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../activation_permissions_scope_test.go | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pkg/workflow/activation_permissions_scope_test.go b/pkg/workflow/activation_permissions_scope_test.go index a0da29fecfb..0f783dab9a7 100644 --- a/pkg/workflow/activation_permissions_scope_test.go +++ b/pkg/workflow/activation_permissions_scope_test.go @@ -729,3 +729,39 @@ engine: copilot assert.Contains(t, activationJobSection, "permission-pull-requests: write", "activation app token should include permission-pull-requests: write when workflow_call + reaction are configured") assert.Contains(t, activationJobSection, "permission-discussions: write", "activation app token should include permission-discussions: write when workflow_call + reaction are configured") } + +func TestActivationPermissionsWorkflowCallReactionDiscussionsOnlyWithGitHubApp(t *testing.T) { + tmpDir := testutil.TempDir(t, "activation-perms-workflow-call-reaction-discussions-only-app") + testFile := filepath.Join(tmpDir, "workflow-call-reaction-discussions-only-app.md") + testContent := `--- +on: + workflow_call: + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_KEY }} + reaction: + type: eyes + issues: false + pull-requests: false + discussions: true +engine: copilot +--- + +# Reusable workflow with discussions-only reaction and activation GitHub App +` + + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err, "failed to write test workflow") + + compiler := NewCompiler() + err = compiler.CompileWorkflow(testFile) + require.NoError(t, err, "failed to compile workflow") + + lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + + activationJobSection := extractJobSection(string(lockContent), string(constants.ActivationJobName)) + assert.NotContains(t, activationJobSection, "permission-issues: write", "activation app token should not include permission-issues: write for workflow_call + discussions-only reaction") + assert.NotContains(t, activationJobSection, "permission-pull-requests: write", "activation app token should not include permission-pull-requests: write for workflow_call + discussions-only reaction") + assert.Contains(t, activationJobSection, "permission-discussions: write", "activation app token should include permission-discussions: write for workflow_call + discussions-only reaction") +}