From 8b2dce459f5c62b6c3fdf80fb78048651aa6a059 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:30:41 +0000 Subject: [PATCH 1/2] Initial plan From 921eff1c0909a6d3c8d0a560f054a330d92ae544 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:44:12 +0000 Subject: [PATCH 2/2] fix: add issues read permission for update-project github-app tokens Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1b2c460b-5a3e-433c-97bb-a8696ae7e211 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../content/docs/reference/safe-outputs.md | 2 + pkg/workflow/safe_outputs_app_test.go | 41 +++++++++++++++++++ pkg/workflow/safe_outputs_permissions.go | 1 + pkg/workflow/safe_outputs_permissions_test.go | 13 ++++++ 4 files changed, 57 insertions(+) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 690e638b961..9a6ad7d5c94 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -476,6 +476,8 @@ Optionally include `item_url` (GitHub issue URL) to add the issue as the first p Manages GitHub Projects boards. Requires a write-capable PAT or GitHub App token ([project token authentication](/gh-aw/patterns/project-ops/#project-token-authentication)); default `GITHUB_TOKEN` lacks Projects v2 access. Update-only by default; set `create_if_missing: true` to create boards (requires appropriate token permissions). +When using `github-app`, issue-backed project item resolution also requires `issues: read` on the minted token (in addition to `organization-projects: write`). + ```yaml wrap safe-outputs: update-project: diff --git a/pkg/workflow/safe_outputs_app_test.go b/pkg/workflow/safe_outputs_app_test.go index 40279c10732..c773a54c2ad 100644 --- a/pkg/workflow/safe_outputs_app_test.go +++ b/pkg/workflow/safe_outputs_app_test.go @@ -161,3 +161,44 @@ Test workflow with discussions permission. 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)") } + +// TestSafeOutputsAppTokenUpdateProjectIssuesReadPermission tests that issues read permission +// is included in the GitHub App token minting step when update-project is configured. +func TestSafeOutputsAppTokenUpdateProjectIssuesReadPermission(t *testing.T) { + compiler := NewCompiler(WithVersion("1.0.0")) + + markdown := `--- +on: issues +safe-outputs: + update-project: + project: "https://github.com/orgs/my-org/projects/1" + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +--- + +# Test Workflow + +Test workflow with update-project permissions. +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.md") + err := os.WriteFile(testFile, []byte(markdown), 0644) + require.NoError(t, err, "Failed to write test file") + + workflowData, err := compiler.ParseWorkflowFile(testFile) + require.NoError(t, err, "Failed to parse markdown content") + require.NotNil(t, workflowData.SafeOutputs, "SafeOutputs should not be nil") + require.NotNil(t, workflowData.SafeOutputs.UpdateProjects, "UpdateProjects should not be nil") + + job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, "main", testFile) + require.NoError(t, err, "Failed to build safe_outputs job") + require.NotNil(t, job, "Job should not be nil") + + stepsStr := strings.Join(job.Steps, "") + + assert.Contains(t, stepsStr, "permission-organization-projects: write", "GitHub App token should include organization projects write permission") + assert.Contains(t, stepsStr, "permission-issues: read", "GitHub App token should include issues read permission for issue-backed project items") + assert.Contains(t, stepsStr, "permission-contents: read", "GitHub App token should include contents read permission") +} diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go index 88b3705c3d2..0ff9e46cb25 100644 --- a/pkg/workflow/safe_outputs_permissions.go +++ b/pkg/workflow/safe_outputs_permissions.go @@ -206,6 +206,7 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio if safeOutputs.UpdateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateProjects.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-project") permissions.Merge(NewPermissionsContentsReadProjectsWrite()) + permissions.Set(PermissionIssues, PermissionRead) } if safeOutputs.CreateProjectStatusUpdates != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjectStatusUpdates.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-project-status-update") diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go index 7da98e75053..cd5f37831e4 100644 --- a/pkg/workflow/safe_outputs_permissions_test.go +++ b/pkg/workflow/safe_outputs_permissions_test.go @@ -395,6 +395,19 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { PermissionOrganizationProj: PermissionWrite, }, }, + { + name: "update-project requires organization-projects write and issues read", + safeOutputs: &SafeOutputsConfig{ + UpdateProjects: &UpdateProjectConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")}, + }, + }, + expected: map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionOrganizationProj: PermissionWrite, + PermissionIssues: PermissionRead, + }, + }, } for _, tt := range tests {