diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 9a6ad7d5c94..bacc031a1c8 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -476,7 +476,7 @@ 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`). +When using `github-app`, issue-backed project item resolution also requires `issues: read` on the minted token (in addition to `organization-projects: write`). This applies to `update-project`, and also to `create-project` when `item_url` is used to resolve an issue into a project item. ```yaml wrap safe-outputs: diff --git a/pkg/workflow/safe_outputs_app_test.go b/pkg/workflow/safe_outputs_app_test.go index c773a54c2ad..f6ab6470d7a 100644 --- a/pkg/workflow/safe_outputs_app_test.go +++ b/pkg/workflow/safe_outputs_app_test.go @@ -202,3 +202,44 @@ Test workflow with update-project permissions. 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") } + +// TestSafeOutputsAppTokenCreateProjectWithItemURLIssuesReadPermission tests that issues read permission +// is included in the GitHub App token minting step when create-project is configured with item_url. +func TestSafeOutputsAppTokenCreateProjectWithItemURLIssuesReadPermission(t *testing.T) { + compiler := NewCompiler(WithVersion("1.0.0")) + + markdown := `--- +on: issues +safe-outputs: + create-project: + target-owner: "my-org" + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +--- + +# Test Workflow + +Test workflow with create-project item_url 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.CreateProjects, "CreateProjects 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 0ff9e46cb25..89ea00a0447 100644 --- a/pkg/workflow/safe_outputs_permissions.go +++ b/pkg/workflow/safe_outputs_permissions.go @@ -202,6 +202,7 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio if safeOutputs.CreateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjects.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-project") permissions.Merge(NewPermissionsContentsReadProjectsWrite()) + permissions.Set(PermissionIssues, PermissionRead) } if safeOutputs.UpdateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateProjects.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-project") diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go index cd5f37831e4..0ee03e7c2f0 100644 --- a/pkg/workflow/safe_outputs_permissions_test.go +++ b/pkg/workflow/safe_outputs_permissions_test.go @@ -384,7 +384,7 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { }, }, { - name: "create-project requires organization-projects write", + name: "create-project requires organization-projects write and issues read", safeOutputs: &SafeOutputsConfig{ CreateProjects: &CreateProjectsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("1")}, @@ -393,6 +393,7 @@ func TestComputePermissionsForSafeOutputs(t *testing.T) { expected: map[PermissionScope]PermissionLevel{ PermissionContents: PermissionRead, PermissionOrganizationProj: PermissionWrite, + PermissionIssues: PermissionRead, }, }, {