From 74b149f4e8c560b2e72835c950fc5a6f629a600c Mon Sep 17 00:00:00 2001
From: Landon Cox <landon.cox@microsoft.com>
Date: Fri, 10 Apr 2026 14:28:53 -0700
Subject: [PATCH] fix: restore permission-discussions in GitHub App token
 fields
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PR #25508 removed permission-discussions from convertPermissionsToAppTokenFields()
based on two incorrect assumptions:

1. That unsupported inputs are silently ignored by actions/create-github-app-token.
   In reality, the action reads ALL INPUT_PERMISSION-* env vars from process.env
   (see lib/get-permissions-from-inputs.js), not just declared inputs.

2. That GitHub App tokens inherit full installation permissions by default.
   This is only true when ZERO permission-* inputs are set. When any permission-*
   field is specified (which the compiler always does), the token is scoped to
   only those permissions — omitting permission-discussions excludes discussions.

This caused create-discussion safe-outputs to fail with permissions errors and
fall back to issue creation when using GitHub App authentication.

Closes #25704

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../github_app_permissions_validation_test.go |  6 ++++--
 pkg/workflow/safe_outputs_app_config.go       | 19 +++++++++++--------
 pkg/workflow/safe_outputs_app_test.go         | 18 +++++++++---------
 3 files changed, 24 insertions(+), 19 deletions(-)

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)")
