Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new note documents update-project needing issues: read for issue-backed item resolution when using github-app, but create-project can also resolve an issue when item_url is set (it queries repository.issue to get the node id). To avoid users hitting the same permission-scoping problem on project creation, consider extending this documentation to mention the create-project + item_url case (or more generally that project item resolution requires issues: read).

Suggested change
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 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.

Copilot uses AI. Check for mistakes.

```yaml wrap
safe-outputs:
update-project:
Expand Down
41 changes: 41 additions & 0 deletions pkg/workflow/safe_outputs_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test covers the minted GitHub App token permissions for update-project, but there isn’t coverage for the analogous create-project behavior when item_url is provided (which triggers issue resolution in the handler). Adding a test that compiles a workflow with create-project + item_url and asserts permission-issues: read is present would help prevent regressions.

Suggested change
}
}
// TestSafeOutputsAppConfigurationCreateProjectWithItemURL tests that create-project with item_url
// requests issue read permission for GitHub App tokens.
func TestSafeOutputsAppConfigurationCreateProjectWithItemURL(t *testing.T) {
compiler := NewCompiler(WithVersion("1.0.0"))
markdown := `---
on: issues
safe-outputs:
create-project:
project: "https://github.com/orgs/my-org/projects/1"
item_url: "https://github.com/my-org/my-repo/issues/123"
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 when create-project resolves an issue from item_url")
assert.Contains(t, stepsStr, "permission-contents: read", "GitHub App token should include contents read permission")
}

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions pkg/workflow/safe_outputs_permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment on lines 206 to 210
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create-project can also require issues: read when item_url is provided: the JS handler calls a GraphQL repository { issue(number) { id } } query to resolve the issue node ID (see actions/setup/js/create_project.cjs:getIssueNodeId). Currently only update-project adds PermissionIssues: read, so a GitHub App token minted for create-project will be scoped without issues access and may fail when adding the initial issue item. Consider adding PermissionIssues: read to the shared project permission set (e.g., NewPermissionsContentsReadProjectsWrite) or explicitly to the create-project block as well.

Copilot uses AI. Check for mistakes.
if safeOutputs.CreateProjectStatusUpdates != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjectStatusUpdates.Staged) {
safeOutputsPermissionsLog.Print("Adding permissions for create-project-status-update")
Expand Down
13 changes: 13 additions & 0 deletions pkg/workflow/safe_outputs_permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading