diff --git a/pkg/workflow/github_app_permissions_validation_test.go b/pkg/workflow/github_app_permissions_validation_test.go index a37f7890506..8aab7df8bc8 100644 --- a/pkg/workflow/github_app_permissions_validation_test.go +++ b/pkg/workflow/github_app_permissions_validation_test.go @@ -438,13 +438,15 @@ func TestConvertPermissionsToAppTokenFields_GitHubAppOnly(t *testing.T) { }, }, { - name: "discussions permission is NOT mapped (actions/create-github-app-token does not declare permission-discussions)", + name: "discussions permission IS mapped (actions/create-github-app-token reads all INPUT_PERMISSION-* env vars)", permissions: func() *Permissions { p := NewPermissions() p.Set(PermissionDiscussions, PermissionWrite) return p }(), - absentFields: []string{"permission-discussions"}, + expectedFields: map[string]string{ + "permission-discussions": "write", + }, }, { name: "models permission is NOT mapped (no GitHub App equivalent)", diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index 5023c4bdac5..06564419f80 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -261,14 +261,17 @@ func convertPermissionsToAppTokenFields(permissions *Permissions) map[string]str if level, ok := permissions.Get(PermissionStatuses); ok { fields["permission-statuses"] = string(level) } - // Note: PermissionDiscussions ("discussions") is intentionally NOT mapped to "permission-discussions" - // here. The actions/create-github-app-token action does NOT declare "permission-discussions" as a - // supported input (see the generated inputs in its action.yml). Passing an unsupported input would - // be silently ignored, meaning the discussions scope would never be explicitly set. GitHub App - // installation tokens inherit the full set of app-installation permissions by default, so the token - // will have discussions access whenever the GitHub App installation itself was granted that permission. - // Repository-level discussions operations should therefore work without an explicit permission-discussions - // field. + // Note: "permission-discussions" is not a declared input in actions/create-github-app-token's action.yml, + // but the action reads ALL INPUT_PERMISSION-* env vars via process.env (see lib/get-permissions-from-inputs.js). + // GitHub Actions sets INPUT_PERMISSION-DISCUSSIONS for any `with: permission-discussions:` field, so + // the value IS forwarded to the GitHub API despite the "Unexpected input" warning. + // Crucially, when ANY permission-* input is specified the action scopes the token to ONLY those permissions + // (returning undefined → inherit-all only when zero permission-* inputs are present). Since the compiler + // always emits other permission-* fields, omitting permission-discussions causes the minted token to + // lack discussions access even when the GitHub App installation has that permission. + if level, ok := permissions.Get(PermissionDiscussions); ok { + fields["permission-discussions"] = string(level) + } // GitHub App-only permissions (not available in GitHub Actions GITHUB_TOKEN). // Use GetExplicit() so that shorthand permissions like "read-all" do not accidentally diff --git a/pkg/workflow/safe_outputs_app_test.go b/pkg/workflow/safe_outputs_app_test.go index c6b1754ba21..18e63c5778c 100644 --- a/pkg/workflow/safe_outputs_app_test.go +++ b/pkg/workflow/safe_outputs_app_test.go @@ -111,13 +111,13 @@ Test workflow without safe outputs. assert.Nil(t, workflowData.SafeOutputs, "SafeOutputs should be nil") } -// TestSafeOutputsAppTokenDiscussionsPermission tests that discussions permission is handled correctly -// in the GitHub App token minting step. +// TestSafeOutputsAppTokenDiscussionsPermission tests that discussions permission is included +// in the GitHub App token minting step when create-discussion is configured. // -// The actions/create-github-app-token action does NOT declare "permission-discussions" as a supported -// input (its generated action.yml only has "permission-team-discussions" for org-level team discussions). -// Therefore "permission-discussions" must NOT be emitted in the token mint step — the GitHub App -// installation token inherits the app's discussion permission from the installation itself. +// Although actions/create-github-app-token does not declare "permission-discussions" in its action.yml, +// the action reads ALL INPUT_PERMISSION-* env vars and forwards them to the GitHub API. When any +// permission-* input is specified, the token is scoped to only those permissions, so omitting +// permission-discussions would exclude discussions access from the minted token. func TestSafeOutputsAppTokenDiscussionsPermission(t *testing.T) { compiler := NewCompilerWithVersion("1.0.0") @@ -155,9 +155,9 @@ Test workflow with discussions permission. // Convert steps to string for easier assertion stepsStr := strings.Join(job.Steps, "") - // permission-discussions is NOT a valid input to actions/create-github-app-token and must not - // appear in the token mint step. Discussions access is inherited from the GitHub App installation. - assert.NotContains(t, stepsStr, "permission-discussions:", "GitHub App token must not use permission-discussions (not a valid action input)") + // permission-discussions must be present because when any permission-* input is set, + // actions/create-github-app-token scopes the token to only those permissions. + assert.Contains(t, stepsStr, "permission-discussions: write", "GitHub App token should include discussions write permission") // Other explicitly supported permission inputs should still be present assert.Contains(t, stepsStr, "permission-contents: read", "GitHub App token should include contents read permission") assert.Contains(t, stepsStr, "permission-issues: write", "GitHub App token should include issues write permission (create-discussion falls back to issue)")