From a7bb9e7d2872a18efb8b8462d8f1545b14b0977c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:22:32 +0000 Subject: [PATCH 1/7] Initial plan From 1263aebb7418ba42b74f2d628b0c71704c0a006a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:28:05 +0000 Subject: [PATCH 2/7] Initial plan: skip permissions for staged safe outputs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/faa27e25-cf6d-4c9f-84d7-d56982dd015b --- .github/workflows/smoke-codex.lock.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 7514dfdddf..a3c38f8c67 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -450,13 +450,20 @@ jobs: { "description": "Add the 'smoked' label to the current pull request (can only be called once)", "inputSchema": { - "additionalProperties": true, + "additionalProperties": false, "properties": { - "payload": { - "description": "JSON-encoded payload to pass to the action", + "labels": { + "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.", + "type": "string" + }, + "number": { + "description": "The number of the issue or pull request.", "type": "string" } }, + "required": [ + "labels" + ], "type": "object" }, "name": "add_smoked_label" @@ -1561,7 +1568,8 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} with: - payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }} + labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }} + number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }} - name: Upload safe output items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 From cba5672062910a8d7847a1b5a6267b05f3f34db9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:37:29 +0000 Subject: [PATCH 3/7] Skip permissions for staged safe outputs in ComputePermissionsForSafeOutputs When a safe output handler is marked as staged (either globally via safeOutputs.Staged or per-handler via BaseSafeOutputConfig.Staged), it only emits preview output and does not make real GitHub API calls. Therefore, such handlers should not contribute write permissions. - Add isHandlerStaged() helper to check staged state - Modify ComputePermissionsForSafeOutputs to skip permission merges for staged handlers - Add TestComputePermissionsForSafeOutputs_Staged with 8 test cases Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/faa27e25-cf6d-4c9f-84d7-d56982dd015b --- pkg/workflow/safe_outputs_permissions.go | 81 +++++++----- pkg/workflow/safe_outputs_permissions_test.go | 123 ++++++++++++++++++ 2 files changed, 170 insertions(+), 34 deletions(-) diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go index 833eea6493..4d46c710fa 100644 --- a/pkg/workflow/safe_outputs_permissions.go +++ b/pkg/workflow/safe_outputs_permissions.go @@ -43,6 +43,14 @@ func stepsRequireIDToken(steps []any) bool { return false } +// isHandlerStaged returns true when a safe output handler is effectively staged +// (i.e., it will only emit preview output, not make real API calls). A handler is +// staged when either the global safe-outputs staged flag is true, or the +// per-handler staged flag is true. Staged handlers do not require write permissions. +func isHandlerStaged(globalStaged, handlerStaged bool) bool { + return globalStaged || handlerStaged +} + // ComputePermissionsForSafeOutputs computes the minimal required permissions // based on the configured safe-outputs. This function is used by both the // consolidated safe outputs job and the conclusion job to ensure they only @@ -50,6 +58,8 @@ func stepsRequireIDToken(steps []any) bool { // // This implements the principle of least privilege by only including // permissions that are required by the configured safe outputs. +// Handlers that are staged (globally or per-handler) are skipped because +// staged mode only emits preview output and does not make any API calls. func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissions { if safeOutputs == nil { safeOutputsPermissionsLog.Print("No safe outputs configured, returning empty permissions") @@ -58,57 +68,60 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio permissions := NewPermissions() - // Merge permissions for all handler-managed types - if safeOutputs.CreateIssues != nil { + // Merge permissions for all handler-managed types. + // Staged handlers are skipped because they do not make real API calls. + if safeOutputs.CreateIssues != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateIssues.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-issue") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.CreateDiscussions != nil { + if safeOutputs.CreateDiscussions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateDiscussions.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-discussion") permissions.Merge(NewPermissionsContentsReadIssuesWriteDiscussionsWrite()) } - if safeOutputs.AddComments != nil { + if safeOutputs.AddComments != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AddComments.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for add-comment") permissions.Merge(buildAddCommentPermissions(safeOutputs.AddComments)) } - if safeOutputs.CloseIssues != nil { + if safeOutputs.CloseIssues != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CloseIssues.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for close-issue") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.CloseDiscussions != nil { + if safeOutputs.CloseDiscussions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CloseDiscussions.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for close-discussion") permissions.Merge(NewPermissionsContentsReadDiscussionsWrite()) } - if safeOutputs.AddLabels != nil { + if safeOutputs.AddLabels != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AddLabels.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for add-labels") permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite()) } - if safeOutputs.RemoveLabels != nil { + if safeOutputs.RemoveLabels != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.RemoveLabels.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for remove-labels") permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite()) } - if safeOutputs.UpdateIssues != nil { + if safeOutputs.UpdateIssues != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateIssues.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-issue") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.UpdateDiscussions != nil { + if safeOutputs.UpdateDiscussions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateDiscussions.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-discussion") permissions.Merge(NewPermissionsContentsReadDiscussionsWrite()) } - if safeOutputs.LinkSubIssue != nil { + if safeOutputs.LinkSubIssue != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.LinkSubIssue.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for link-sub-issue") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.UpdateRelease != nil { + if safeOutputs.UpdateRelease != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateRelease.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-release") permissions.Merge(NewPermissionsContentsWrite()) } - if safeOutputs.CreatePullRequestReviewComments != nil || safeOutputs.SubmitPullRequestReview != nil || - safeOutputs.ReplyToPullRequestReviewComment != nil || safeOutputs.ResolvePullRequestReviewThread != nil { + if (safeOutputs.CreatePullRequestReviewComments != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreatePullRequestReviewComments.Staged)) || + (safeOutputs.SubmitPullRequestReview != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.SubmitPullRequestReview.Staged)) || + (safeOutputs.ReplyToPullRequestReviewComment != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.ReplyToPullRequestReviewComment.Staged)) || + (safeOutputs.ResolvePullRequestReviewThread != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.ResolvePullRequestReviewThread.Staged)) { safeOutputsPermissionsLog.Print("Adding permissions for PR review operations") permissions.Merge(NewPermissionsContentsReadPRWrite()) } - if safeOutputs.CreatePullRequests != nil { + if safeOutputs.CreatePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreatePullRequests.Staged) { // Check fallback-as-issue setting to determine permissions if getFallbackAsIssue(safeOutputs.CreatePullRequests) { safeOutputsPermissionsLog.Print("Adding permissions for create-pull-request with fallback-as-issue") @@ -118,23 +131,23 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio permissions.Merge(NewPermissionsContentsWritePRWrite()) } } - if safeOutputs.PushToPullRequestBranch != nil { + if safeOutputs.PushToPullRequestBranch != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.PushToPullRequestBranch.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for push-to-pull-request-branch") permissions.Merge(NewPermissionsContentsWritePRWrite()) } - if safeOutputs.UpdatePullRequests != nil { + if safeOutputs.UpdatePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdatePullRequests.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-pull-request") permissions.Merge(NewPermissionsContentsReadPRWrite()) } - if safeOutputs.ClosePullRequests != nil { + if safeOutputs.ClosePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.ClosePullRequests.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for close-pull-request") permissions.Merge(NewPermissionsContentsReadPRWrite()) } - if safeOutputs.MarkPullRequestAsReadyForReview != nil { + if safeOutputs.MarkPullRequestAsReadyForReview != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.MarkPullRequestAsReadyForReview.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for mark-pull-request-as-ready-for-review") permissions.Merge(NewPermissionsContentsReadPRWrite()) } - if safeOutputs.HideComment != nil { + if safeOutputs.HideComment != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.HideComment.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for hide-comment") // Check if discussions permission should be excluded (discussions: false) // Default (nil or true) includes discussions:write for GitHub Apps with Discussions permission @@ -145,60 +158,60 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio permissions.Merge(NewPermissionsContentsReadIssuesWriteDiscussionsWrite()) } } - if safeOutputs.DispatchWorkflow != nil { + if safeOutputs.DispatchWorkflow != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.DispatchWorkflow.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for dispatch-workflow") permissions.Merge(NewPermissionsActionsWrite()) } // Project-related types - if safeOutputs.CreateProjects != nil { + if safeOutputs.CreateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjects.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-project") permissions.Merge(NewPermissionsContentsReadProjectsWrite()) } - if safeOutputs.UpdateProjects != nil { + if safeOutputs.UpdateProjects != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UpdateProjects.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for update-project") permissions.Merge(NewPermissionsContentsReadProjectsWrite()) } - if safeOutputs.CreateProjectStatusUpdates != nil { + if safeOutputs.CreateProjectStatusUpdates != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateProjectStatusUpdates.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-project-status-update") permissions.Merge(NewPermissionsContentsReadProjectsWrite()) } - if safeOutputs.AssignToAgent != nil { + if safeOutputs.AssignToAgent != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AssignToAgent.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for assign-to-agent") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.CreateAgentSessions != nil { + if safeOutputs.CreateAgentSessions != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateAgentSessions.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-agent-session") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.CreateCodeScanningAlerts != nil { + if safeOutputs.CreateCodeScanningAlerts != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreateCodeScanningAlerts.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for create-code-scanning-alert") permissions.Merge(NewPermissionsContentsReadSecurityEventsWrite()) } - if safeOutputs.AutofixCodeScanningAlert != nil { + if safeOutputs.AutofixCodeScanningAlert != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AutofixCodeScanningAlert.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for autofix-code-scanning-alert") permissions.Merge(NewPermissionsContentsReadSecurityEventsWriteActionsRead()) } - if safeOutputs.AssignToUser != nil { + if safeOutputs.AssignToUser != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AssignToUser.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for assign-to-user") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.UnassignFromUser != nil { + if safeOutputs.UnassignFromUser != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UnassignFromUser.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for unassign-from-user") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.AssignMilestone != nil { + if safeOutputs.AssignMilestone != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AssignMilestone.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for assign-milestone") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.SetIssueType != nil { + if safeOutputs.SetIssueType != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.SetIssueType.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for set-issue-type") permissions.Merge(NewPermissionsContentsReadIssuesWrite()) } - if safeOutputs.AddReviewer != nil { + if safeOutputs.AddReviewer != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.AddReviewer.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for add-reviewer") permissions.Merge(NewPermissionsContentsReadPRWrite()) } - if safeOutputs.UploadAssets != nil { + if safeOutputs.UploadAssets != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.UploadAssets.Staged) { safeOutputsPermissionsLog.Print("Adding permissions for upload-asset") permissions.Merge(NewPermissionsContentsWrite()) } diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go index 993542c0fb..43d1e0a11c 100644 --- a/pkg/workflow/safe_outputs_permissions_test.go +++ b/pkg/workflow/safe_outputs_permissions_test.go @@ -570,3 +570,126 @@ func TestComputePermissionsForSafeOutputs_IDToken(t *testing.T) { }) } } + +func TestComputePermissionsForSafeOutputs_Staged(t *testing.T) { + tests := []struct { + name string + safeOutputs *SafeOutputsConfig + expected map[PermissionScope]PermissionLevel + }{ + { + name: "global staged=true - no permissions for any handler", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreateIssues: &CreateIssuesConfig{}, + CreateDiscussions: &CreateDiscussionsConfig{}, + AddLabels: &AddLabelsConfig{}, + }, + expected: map[PermissionScope]PermissionLevel{}, + }, + { + name: "per-handler staged=true - staged handler contributes no permissions", + safeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}, + }, + AddLabels: &AddLabelsConfig{}, + }, + // create-issue is staged so it contributes nothing; add-labels is not staged + expected: map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionIssues: PermissionWrite, + PermissionPullRequests: PermissionWrite, + }, + }, + { + name: "all handlers per-handler staged - no permissions", + safeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}, + }, + CreateDiscussions: &CreateDiscussionsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}, + }, + }, + expected: map[PermissionScope]PermissionLevel{}, + }, + { + name: "global staged=true overrides per-handler staged=false", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreatePullRequests: &CreatePullRequestsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: false}, + }, + DispatchWorkflow: &DispatchWorkflowConfig{}, + }, + expected: map[PermissionScope]PermissionLevel{}, + }, + { + name: "global staged=false, one handler staged=true", + safeOutputs: &SafeOutputsConfig{ + Staged: false, + CreatePullRequests: &CreatePullRequestsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}, + }, + CloseIssues: &CloseIssuesConfig{}, + }, + // create-pull-request is staged; close-issue is not + expected: map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionIssues: PermissionWrite, + }, + }, + { + name: "global staged=true - upload-asset staged, no contents:write", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + UploadAssets: &UploadAssetsConfig{}, + }, + expected: map[PermissionScope]PermissionLevel{}, + }, + { + name: "pr review operations - all staged via global flag", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreatePullRequestReviewComments: &CreatePullRequestReviewCommentsConfig{}, + SubmitPullRequestReview: &SubmitPullRequestReviewConfig{}, + }, + expected: map[PermissionScope]PermissionLevel{}, + }, + { + name: "pr review operations - one staged, one not", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequestReviewComments: &CreatePullRequestReviewCommentsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}, + }, + SubmitPullRequestReview: &SubmitPullRequestReviewConfig{}, + }, + // submit-pull-request-review is not staged, so PR write permissions are added + expected: map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionPullRequests: PermissionWrite, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + permissions := ComputePermissionsForSafeOutputs(tt.safeOutputs) + require.NotNil(t, permissions, "Permissions should not be nil") + + // Check that all expected permissions are present + for scope, expectedLevel := range tt.expected { + actualLevel, exists := permissions.Get(scope) + assert.True(t, exists, "Permission scope %s should exist", scope) + assert.Equal(t, expectedLevel, actualLevel, "Permission level for %s should match", scope) + } + + // Check that no unexpected permissions are present + for scope := range permissions.permissions { + _, expected := tt.expected[scope] + assert.True(t, expected, "Unexpected permission scope: %s", scope) + } + }) + } +} From 9efbae89aee832063fc8215f51421df7cedb146c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:58:13 +0000 Subject: [PATCH 4/7] Add integration tests and workflow files for staged safe output permissions - Add test-staged-permissions-global.md: workflow with global staged:true - Add test-staged-permissions-per-handler.md: workflow with per-handler staged:true - Add TestCompileStagedSafeOutputsPermissionsGlobal: verifies no write permissions when safe-outputs.staged=true globally - Add TestCompileStagedSafeOutputsPermissionsPerHandler: verifies only non-staged handlers contribute write permissions - Add TestCompileStagedSafeOutputsPermissionsAllHandlersStaged: verifies no write permissions when all handlers are individually staged Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1afcbdcd-09a8-4d0b-b876-34a29e28d315 --- .github/workflows/poem-bot.lock.yml | 10 - .github/workflows/smoke-claude.lock.yml | 4 +- pkg/cli/compile_integration_test.go | 181 +++++++++++++++++- .../test-staged-permissions-global.md | 22 +++ .../test-staged-permissions-per-handler.md | 22 +++ 5 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 pkg/cli/workflows/test-staged-permissions-global.md create mode 100644 pkg/cli/workflows/test-staged-permissions-per-handler.md diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index c474fb2489..0409e6772f 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1294,11 +1294,6 @@ jobs: - upload_assets if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true')) runs-on: ubuntu-slim - permissions: - contents: write - discussions: write - issues: write - pull-requests: write concurrency: group: "gh-aw-conclusion-poem-bot" cancel-in-progress: false @@ -1492,11 +1487,6 @@ jobs: - agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim - permissions: - contents: write - discussions: write - issues: write - pull-requests: write timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/poem-bot" diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 782262dffc..7d6fae0a70 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2368,7 +2368,7 @@ jobs: if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true')) runs-on: ubuntu-slim permissions: - contents: write + contents: read discussions: write issues: write pull-requests: write @@ -2536,7 +2536,7 @@ jobs: if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim permissions: - contents: write + contents: read discussions: write issues: write pull-requests: write diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index 659ea65eea..2d447c8fb1 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -1036,8 +1036,185 @@ Verify staged safe-outputs with multiple handler types. } } -// TestCompileFromSubdirectoryCreatesActionsLockAtRoot tests that actions-lock.json -// is created at the repository root when compiling from a subdirectory +// TestCompileStagedSafeOutputsPermissionsGlobal verifies that when safe-outputs has +// global staged: true, the compiled safe_outputs job has no job-level permissions block +// (staged mode emits only preview output; no GitHub API writes are performed). +func TestCompileStagedSafeOutputsPermissionsGlobal(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + testWorkflow := `--- +name: Staged Global Permissions +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + staged: true + create-issue: + title-prefix: "[staged] " + max: 1 + add-labels: + max: 3 + create-discussion: + max: 1 +--- + +Verify that global staged mode removes all write permissions from the safe_outputs job. +` + testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-global-perms.md") + if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil { + t.Fatalf("Failed to write test workflow file: %v", err) + } + + cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) + } + + lockFilePath := filepath.Join(setup.workflowsDir, "staged-global-perms.lock.yml") + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + + // Global staged means no write API calls are made, so the safe_outputs job must + // have no job-level permissions block (permissions come from the workflow level). + if strings.Contains(lockContentStr, "issues: write") { + t.Errorf("Staged lock file should NOT contain 'issues: write' in safe_outputs job\nLock file content:\n%s", lockContentStr) + } + if strings.Contains(lockContentStr, "discussions: write") { + t.Errorf("Staged lock file should NOT contain 'discussions: write' in safe_outputs job\nLock file content:\n%s", lockContentStr) + } + if strings.Contains(lockContentStr, "pull-requests: write") { + t.Errorf("Staged lock file should NOT contain 'pull-requests: write' in safe_outputs job\nLock file content:\n%s", lockContentStr) + } + if strings.Contains(lockContentStr, "contents: write") { + t.Errorf("Staged lock file should NOT contain 'contents: write' in safe_outputs job\nLock file content:\n%s", lockContentStr) + } + + // Staged env var must still be present + if !strings.Contains(lockContentStr, `GH_AW_SAFE_OUTPUTS_STAGED: "true"`) { + t.Errorf("Lock file should contain GH_AW_SAFE_OUTPUTS_STAGED: \"true\"\nLock file content:\n%s", lockContentStr) + } +} + +// TestCompileStagedSafeOutputsPermissionsPerHandler verifies that when only specific +// safe-output handlers have staged: true, only those handlers' write permissions are +// omitted. Non-staged handlers still contribute their required permissions. +func TestCompileStagedSafeOutputsPermissionsPerHandler(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + testWorkflow := `--- +name: Staged Per-Handler Permissions +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: + staged: true + title-prefix: "[staged] " + max: 1 + add-labels: + max: 3 +--- + +Verify that per-handler staged mode removes only that handler's write permissions. +` + testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-perhandler-perms.md") + if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil { + t.Fatalf("Failed to write test workflow file: %v", err) + } + + cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) + } + + lockFilePath := filepath.Join(setup.workflowsDir, "staged-perhandler-perms.lock.yml") + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + + // add-labels is not staged and needs issues: write and pull-requests: write + if !strings.Contains(lockContentStr, "issues: write") { + t.Errorf("Lock file should contain 'issues: write' for non-staged add-labels\nLock file content:\n%s", lockContentStr) + } + if !strings.Contains(lockContentStr, "pull-requests: write") { + t.Errorf("Lock file should contain 'pull-requests: write' for non-staged add-labels\nLock file content:\n%s", lockContentStr) + } + + // create-issue is staged so it must NOT add issues: write on its own. + // However add-labels already contributes issues: write, so we can only verify + // that discussions and contents: write are absent (which create-issue does not add + // anyway). The key behaviour is verified via the unit tests in safe_outputs_permissions_test.go. + if strings.Contains(lockContentStr, "discussions: write") { + t.Errorf("Lock file should NOT contain 'discussions: write' when only add-labels and staged create-issue are configured\nLock file content:\n%s", lockContentStr) + } + if strings.Contains(lockContentStr, "contents: write") { + t.Errorf("Lock file should NOT contain 'contents: write'\nLock file content:\n%s", lockContentStr) + } +} + +// TestCompileStagedSafeOutputsPermissionsAllHandlersStaged verifies that when all +// handlers are per-handler staged, the safe_outputs job has no write permissions. +func TestCompileStagedSafeOutputsPermissionsAllHandlersStaged(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + testWorkflow := `--- +name: All Handlers Staged +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: + staged: true + max: 1 + create-discussion: + staged: true + max: 1 +--- + +Verify that when all handlers are per-handler staged, no write permissions appear. +` + testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-all-handlers.md") + if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil { + t.Fatalf("Failed to write test workflow file: %v", err) + } + + cmd := exec.Command(setup.binaryPath, "compile", testWorkflowPath) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) + } + + lockFilePath := filepath.Join(setup.workflowsDir, "staged-all-handlers.lock.yml") + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + + // All handlers are staged — no write permissions should appear in safe_outputs job + for _, perm := range []string{"issues: write", "discussions: write", "pull-requests: write", "contents: write"} { + if strings.Contains(lockContentStr, perm) { + t.Errorf("Staged lock file should NOT contain %q\nLock file content:\n%s", perm, lockContentStr) + } + } +} + func TestCompileFromSubdirectoryCreatesActionsLockAtRoot(t *testing.T) { setup := setupIntegrationTest(t) defer setup.cleanup() diff --git a/pkg/cli/workflows/test-staged-permissions-global.md b/pkg/cli/workflows/test-staged-permissions-global.md new file mode 100644 index 0000000000..b1d8a5aa1a --- /dev/null +++ b/pkg/cli/workflows/test-staged-permissions-global.md @@ -0,0 +1,22 @@ +--- +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + staged: true + create-issue: + title-prefix: "[staged] " + max: 1 + add-labels: + max: 3 + create-discussion: + max: 1 +--- + +# Test Staged Permissions (Global) + +Verify that when `staged: true` is set globally, the compiled safe_outputs job +has **no** job-level `permissions:` block (all handlers are staged, so no write +permissions are needed). diff --git a/pkg/cli/workflows/test-staged-permissions-per-handler.md b/pkg/cli/workflows/test-staged-permissions-per-handler.md new file mode 100644 index 0000000000..4f3421116a --- /dev/null +++ b/pkg/cli/workflows/test-staged-permissions-per-handler.md @@ -0,0 +1,22 @@ +--- +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + create-issue: + staged: true + title-prefix: "[staged] " + max: 1 + add-labels: + max: 3 +--- + +# Test Staged Permissions (Per-Handler) + +Verify that when only specific handlers have `staged: true`, the compiled +safe_outputs job only includes permissions required by the non-staged handlers. + +Here `create-issue` is staged (no write permissions for it), and `add-labels` +is not staged (needs `issues: write` and `pull-requests: write`). From eadce7b2c27dc99843758a9779e3334b653b92b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 05:09:12 +0000 Subject: [PATCH 5/7] Fix missing trailing slashes in docs internal links 22 links across blog posts, guides, and setup pages were missing trailing slashes, causing the Starlight link validator to fail the Doc Build CI job. Add trailing slashes to: - /gh-aw/setup/creating-workflows -> /gh-aw/setup/creating-workflows/ - /gh-aw/guides/agentic-authoring -> /gh-aw/guides/agentic-authoring/ - /gh-aw/agent-factory-status -> /gh-aw/agent-factory-status/ Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/79f8e4d7-5e14-498f-9e1e-3d0fbfd1b3ea --- .../docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md | 2 +- .../blog/2026-01-13-meet-the-workflows-advanced-analytics.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-campaigns.md | 2 +- .../2026-01-13-meet-the-workflows-continuous-improvement.md | 2 +- .../2026-01-13-meet-the-workflows-continuous-refactoring.md | 2 +- .../blog/2026-01-13-meet-the-workflows-continuous-simplicity.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-continuous-style.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-creative-culture.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-documentation.md | 2 +- .../blog/2026-01-13-meet-the-workflows-interactive-chatops.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-issue-management.md | 2 +- .../blog/2026-01-13-meet-the-workflows-metrics-analytics.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-multi-phase.md | 2 +- .../blog/2026-01-13-meet-the-workflows-operations-release.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-organization.md | 2 +- .../docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md | 2 +- .../blog/2026-01-13-meet-the-workflows-security-compliance.md | 2 +- .../blog/2026-01-13-meet-the-workflows-testing-validation.md | 2 +- .../blog/2026-01-13-meet-the-workflows-tool-infrastructure.md | 2 +- docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md | 2 +- docs/src/content/docs/guides/agentic-authoring.mdx | 2 +- docs/src/content/docs/setup/creating-workflows.mdx | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md b/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md index d9885be43b..0d00172198 100644 --- a/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md +++ b/docs/src/content/docs/blog/2026-01-12-welcome-to-pelis-agent-factory.md @@ -115,4 +115,4 @@ Want to start with automated agentic workflows on GitHub? See our [Quick Start]( ## Factory Status -[Current Factory Status](/gh-aw/agent-factory-status) +[Current Factory Status](/gh-aw/agent-factory-status/) diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md index 8f3d2ea0f4..0a221bdd2a 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-advanced-analytics.md @@ -73,7 +73,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md index 667aacf7ea..89411a28b9 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-campaigns.md @@ -57,7 +57,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md index d0622b0767..7aacfcc833 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-improvement.md @@ -142,7 +142,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Next Up: Continuous Documentation diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md index da456a7265..38bea6ad96 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-refactoring.md @@ -80,7 +80,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Next Up: Continuous Style diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md index b6ce54fac3..0b1a980fdb 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-simplicity.md @@ -76,7 +76,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Next Up: Continuous Refactoring diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md index 3f956fd8b8..80ea1fa793 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-continuous-style.md @@ -56,7 +56,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specification to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Next Up: Continuous Improvement diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md index e55119086c..179451dc88 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-creative-culture.md @@ -86,7 +86,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md index dc97f75dc0..210a0857f3 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-documentation.md @@ -100,7 +100,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md index 0ecacd352d..bdb41d08fb 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-interactive-chatops.md @@ -66,7 +66,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md index 8f2a461aef..55b6bb7a8a 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-issue-management.md @@ -73,7 +73,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md index 540c123db2..cbdf0caad2 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-metrics-analytics.md @@ -64,7 +64,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md index a7e19b4d70..2365c86431 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-multi-phase.md @@ -74,7 +74,7 @@ gh aw add-wizard githubnext/agentics/workflows/pr-fix.md Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md index 59f5468abc..df68110eba 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-operations-release.md @@ -47,7 +47,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md index bbf564f389..b9b6274eb7 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-organization.md @@ -64,7 +64,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md index 1bd2e62272..3d715577e2 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-quality-hygiene.md @@ -68,7 +68,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md index 52a367dbdd..996d8e8f00 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-security-compliance.md @@ -80,7 +80,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md index ec36f85605..0ed9216a43 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-testing-validation.md @@ -102,7 +102,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md index 48c93ca5d9..ce935c0ac5 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows-tool-infrastructure.md @@ -66,7 +66,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specifications to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Learn More diff --git a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md index c053e1a0a2..ebdc4a1219 100644 --- a/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md +++ b/docs/src/content/docs/blog/2026-01-13-meet-the-workflows.md @@ -92,7 +92,7 @@ gh aw add-wizard https://github.com/github/gh-aw/blob/v0.45.5/.github/workflows/ Then edit and remix the workflow specification to meet your needs, regenerate the lock file using `gh aw compile`, and push to your repository. See our [Quick Start](https://github.github.com/gh-aw/setup/quick-start/) for further installation and setup instructions. -You can also [create your own workflows](/gh-aw/setup/creating-workflows). +You can also [create your own workflows](/gh-aw/setup/creating-workflows/). ## Next Up: Code Quality & Refactoring Workflows diff --git a/docs/src/content/docs/guides/agentic-authoring.mdx b/docs/src/content/docs/guides/agentic-authoring.mdx index 78c513f598..cfdde6054a 100644 --- a/docs/src/content/docs/guides/agentic-authoring.mdx +++ b/docs/src/content/docs/guides/agentic-authoring.mdx @@ -8,7 +8,7 @@ import CopyEntireFileButton from '../../../components/CopyEntireFileButton.astro import Video from '../../../components/Video.astro'; Using our authoring agent is an effective way to create, debug, optimize your agentic workflows. -This is a continuation of the [Create Agentic Workflows](/gh-aw/setup/creating-workflows) page. +This is a continuation of the [Create Agentic Workflows](/gh-aw/setup/creating-workflows/) page. ## Configuring Your Repository diff --git a/docs/src/content/docs/setup/creating-workflows.mdx b/docs/src/content/docs/setup/creating-workflows.mdx index 16f40cb7d8..9e292eef22 100644 --- a/docs/src/content/docs/setup/creating-workflows.mdx +++ b/docs/src/content/docs/setup/creating-workflows.mdx @@ -148,4 +148,4 @@ To add a workflow from another repository, see [Reusing Workflows](/gh-aw/guides ## Learn More About Agentic Authoring -The [Agentic Authoring](/gh-aw/guides/agentic-authoring) contains additional techniques to leverage agents to help build better agentic workflows. \ No newline at end of file +The [Agentic Authoring](/gh-aw/guides/agentic-authoring/) contains additional techniques to leverage agents to help build better agentic workflows. \ No newline at end of file From f799c0cc2c4f98eff8ffe21108a02426c305e89c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 05:44:40 +0000 Subject: [PATCH 6/7] Add explicit permissions: {} for staged safe output handlers - usesPatchesAndCheckouts: respect staged status for create-pull-request and push-to-pull-request-branch, returning false when all PR handlers are staged (no checkout/git steps added for staged-only configs) - ComputePermissionsForSafeOutputs: return NewPermissionsEmpty() when safe_outputs is configured but all handlers are staged, so the compiled safe_outputs job renders "permissions: {}" explicitly - Tests: add staged cases to TestUsesPatchesAndCheckouts, add YAML rendering assertions to TestComputePermissionsForSafeOutputs_Staged, add TestComputePermissionsForSafeOutputs_StagedYAMLRendering - Recompile: poem-bot.lock.yml safe_outputs job now has permissions: {} Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d9d7cf59-7cee-4a99-8c2d-a04ad9fa45a9 --- .github/workflows/daily-choice-test.lock.yml | 2 + .../workflows/notion-issue-summary.lock.yml | 2 + .github/workflows/poem-bot.lock.yml | 36 +----------- .../workflows/smoke-call-workflow.lock.yml | 2 + .github/workflows/smoke-claude.lock.yml | 34 +---------- .github/workflows/smoke-codex.lock.yml | 16 ++---- .../safe_outputs_config_helpers_test.go | 56 +++++++++++++++++++ pkg/workflow/safe_outputs_permissions.go | 9 +++ pkg/workflow/safe_outputs_permissions_test.go | 46 +++++++++++++++ pkg/workflow/safe_outputs_runtime.go | 14 +++-- 10 files changed, 135 insertions(+), 82 deletions(-) diff --git a/.github/workflows/daily-choice-test.lock.yml b/.github/workflows/daily-choice-test.lock.yml index 2bef28cf96..6cc82db0d3 100644 --- a/.github/workflows/daily-choice-test.lock.yml +++ b/.github/workflows/daily-choice-test.lock.yml @@ -909,6 +909,7 @@ jobs: - test_environment if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true')) runs-on: ubuntu-slim + permissions: {} concurrency: group: "gh-aw-conclusion-daily-choice-test" cancel-in-progress: false @@ -1017,6 +1018,7 @@ jobs: needs: agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim + permissions: {} timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/daily-choice-test" diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index 91ba0ca85d..bdce432d71 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -748,6 +748,7 @@ jobs: - safe_outputs if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true')) runs-on: ubuntu-slim + permissions: {} concurrency: group: "gh-aw-conclusion-notion-issue-summary" cancel-in-progress: false @@ -981,6 +982,7 @@ jobs: needs: agent if: (!cancelled()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim + permissions: {} timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/notion-issue-summary" diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 0409e6772f..cf2746e865 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1158,7 +1158,6 @@ jobs: /tmp/gh-aw/agent/ /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json - /tmp/gh-aw/aw-*.patch if-no-files-found: ignore # --- Threat Detection (inline) --- - name: Check if detection needed @@ -1294,6 +1293,7 @@ jobs: - upload_assets if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true')) runs-on: ubuntu-slim + permissions: {} concurrency: group: "gh-aw-conclusion-poem-bot" cancel-in-progress: false @@ -1482,11 +1482,10 @@ jobs: await main(); safe_outputs: - needs: - - activation - - agent + needs: agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim + permissions: {} timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/poem-bot" @@ -1538,34 +1537,6 @@ jobs: mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Checkout repository - if: (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))) || (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch'))) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - - name: Configure Git credentials - if: (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))) || (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch'))) - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - name: Configure GH_HOST for enterprise compatibility shell: bash run: | @@ -1584,7 +1555,6 @@ jobs: GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"poetry\",\"creative\",\"automation\",\"ai-generated\",\"epic\",\"haiku\",\"sonnet\",\"limerick\"],\"max\":5},\"close_pull_request\":{\"max\":2,\"required_labels\":[\"poetry\",\"automation\"],\"required_title_prefix\":\"[🎨 POETRY]\",\"target\":\"*\"},\"create_agent_session\":{\"base\":\"main\",\"max\":1},\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"expires\":24,\"fallback_to_issue\":true,\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[📜 POETRY] \"},\"create_issue\":{\"expires\":48,\"group\":true,\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[🎭 POEM-BOT] \"},\"create_pull_request\":{\"draft\":false,\"expires\":48,\"labels\":[\"poetry\",\"automation\",\"creative-writing\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"reviewers\":[\"copilot\"],\"title_prefix\":\"[🎨 POETRY] \"},\"create_pull_request_review_comment\":{\"max\":2,\"side\":\"RIGHT\"},\"link_sub_issue\":{\"max\":3,\"parent_required_labels\":[\"poetry\",\"epic\"],\"parent_title_prefix\":\"[🎭 POEM-BOT]\",\"sub_required_labels\":[\"poetry\"],\"sub_title_prefix\":\"[🎭 POEM-BOT]\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"]},\"update_issue\":{\"allow_body\":true,\"allow_status\":true,\"allow_title\":true,\"max\":2,\"target\":\"*\"},\"upload_asset\":{\"allowed-exts\":[\".png\",\".jpg\",\".jpeg\"],\"branch\":\"assets/${{ github.workflow }}\",\"max-size\":10240}}" GH_AW_SAFE_OUTPUTS_STAGED: "true" - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml index d6a8b43ebc..35a462c19c 100644 --- a/.github/workflows/smoke-call-workflow.lock.yml +++ b/.github/workflows/smoke-call-workflow.lock.yml @@ -865,6 +865,7 @@ jobs: - safe_outputs if: (always()) && ((needs.agent.result != 'skipped') || (needs.activation.outputs.lockdown_check_failed == 'true')) runs-on: ubuntu-slim + permissions: {} concurrency: group: "gh-aw-conclusion-smoke-call-workflow" cancel-in-progress: false @@ -1004,6 +1005,7 @@ jobs: needs: agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim + permissions: {} timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/smoke-call-workflow" diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 7d6fae0a70..74fb9203af 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2221,7 +2221,6 @@ jobs: /tmp/gh-aw/agent/ /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json - /tmp/gh-aw/aw-*.patch if-no-files-found: ignore # --- Threat Detection (inline) --- - name: Check if detection needed @@ -2530,9 +2529,7 @@ jobs: await main(); safe_outputs: - needs: - - activation - - agent + needs: agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim permissions: @@ -2586,34 +2583,6 @@ jobs: mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Checkout repository - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - - name: Configure Git credentials - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - name: Configure GH_HOST for enterprise compatibility shell: bash run: | @@ -2655,7 +2624,6 @@ jobs: GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_SCRIPTS: "{\"post_slack_message\":\"safe_output_script_post_slack_message.cjs\"}" GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"add_reviewer\":{\"max\":2,\"target\":\"*\"},\"close_pull_request\":{\"max\":1,\"staged\":true},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\",\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CLAUDE.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"],\"staged\":true,\"target\":\"*\"},\"resolve_pull_request_review_thread\":{\"max\":5},\"submit_pull_request_review\":{\"footer\":\"always\",\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\"}}" - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index a3c38f8c67..7514dfdddf 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -450,20 +450,13 @@ jobs: { "description": "Add the 'smoked' label to the current pull request (can only be called once)", "inputSchema": { - "additionalProperties": false, + "additionalProperties": true, "properties": { - "labels": { - "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.", - "type": "string" - }, - "number": { - "description": "The number of the issue or pull request.", + "payload": { + "description": "JSON-encoded payload to pass to the action", "type": "string" } }, - "required": [ - "labels" - ], "type": "object" }, "name": "add_smoked_label" @@ -1568,8 +1561,7 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} with: - labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }} - number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }} + payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }} - name: Upload safe output items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/pkg/workflow/safe_outputs_config_helpers_test.go b/pkg/workflow/safe_outputs_config_helpers_test.go index 6c0700bbf1..8690a193a4 100644 --- a/pkg/workflow/safe_outputs_config_helpers_test.go +++ b/pkg/workflow/safe_outputs_config_helpers_test.go @@ -84,6 +84,62 @@ func TestUsesPatchesAndCheckouts(t *testing.T) { }, expected: true, }, + { + name: "returns false when CreatePullRequests is globally staged", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreatePullRequests: &CreatePullRequestsConfig{}, + }, + expected: false, + }, + { + name: "returns false when PushToPullRequestBranch is globally staged", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, + }, + expected: false, + }, + { + name: "returns false when both PR handlers are globally staged", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreatePullRequests: &CreatePullRequestsConfig{}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, + }, + expected: false, + }, + { + name: "returns false when CreatePullRequests is per-handler staged", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + }, + expected: false, + }, + { + name: "returns false when both PR handlers are per-handler staged", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + }, + expected: false, + }, + { + name: "returns true when CreatePullRequests is not staged but PushToPullRequestBranch is staged", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + }, + expected: true, + }, + { + name: "returns true when PushToPullRequestBranch is not staged but CreatePullRequests is staged", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, + }, + expected: true, + }, } for _, tt := range tests { diff --git a/pkg/workflow/safe_outputs_permissions.go b/pkg/workflow/safe_outputs_permissions.go index 4d46c710fa..7df2a668b2 100644 --- a/pkg/workflow/safe_outputs_permissions.go +++ b/pkg/workflow/safe_outputs_permissions.go @@ -232,6 +232,15 @@ func ComputePermissionsForSafeOutputs(safeOutputs *SafeOutputsConfig) *Permissio permissions.Set(PermissionIdToken, PermissionWrite) } + // If safeOutputs is configured but no permissions were accumulated (all handlers staged), + // return explicit empty permissions so the compiled safe_outputs job renders + // "permissions: {}" rather than omitting the block and inheriting workflow-level permissions. + // This makes the security posture self-documenting in the generated YAML. + if len(permissions.permissions) == 0 { + safeOutputsPermissionsLog.Print("All handlers staged; returning explicit empty permissions (permissions: {})") + return NewPermissionsEmpty() + } + safeOutputsPermissionsLog.Printf("Computed permissions with %d scopes", len(permissions.permissions)) return permissions } diff --git a/pkg/workflow/safe_outputs_permissions_test.go b/pkg/workflow/safe_outputs_permissions_test.go index 43d1e0a11c..93ea41634c 100644 --- a/pkg/workflow/safe_outputs_permissions_test.go +++ b/pkg/workflow/safe_outputs_permissions_test.go @@ -693,3 +693,49 @@ func TestComputePermissionsForSafeOutputs_Staged(t *testing.T) { }) } } + +// TestComputePermissionsForSafeOutputs_StagedYAMLRendering validates that fully-staged +// safe output configurations produce explicit "permissions: {}" in YAML rendering, +// rather than an empty string that would cause the job to inherit workflow-level permissions. +func TestComputePermissionsForSafeOutputs_StagedYAMLRendering(t *testing.T) { + tests := []struct { + name string + safeOutputs *SafeOutputsConfig + expectedRendered string + }{ + { + name: "globally staged - renders permissions: {}", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreateIssues: &CreateIssuesConfig{}, + AddLabels: &AddLabelsConfig{}, + }, + expectedRendered: "permissions: {}", + }, + { + name: "all per-handler staged - renders permissions: {}", + safeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + AddLabels: &AddLabelsConfig{BaseSafeOutputConfig: BaseSafeOutputConfig{Staged: true}}, + }, + expectedRendered: "permissions: {}", + }, + { + name: "staged PR handlers - renders permissions: {}", + safeOutputs: &SafeOutputsConfig{ + Staged: true, + CreatePullRequests: &CreatePullRequestsConfig{}, + }, + expectedRendered: "permissions: {}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + permissions := ComputePermissionsForSafeOutputs(tt.safeOutputs) + require.NotNil(t, permissions, "Permissions should not be nil") + rendered := permissions.RenderToYAML() + assert.Equal(t, tt.expectedRendered, rendered, "Fully-staged safe-outputs must render explicit empty permissions block") + }) + } +} diff --git a/pkg/workflow/safe_outputs_runtime.go b/pkg/workflow/safe_outputs_runtime.go index 81da2659cf..ce07fd36f0 100644 --- a/pkg/workflow/safe_outputs_runtime.go +++ b/pkg/workflow/safe_outputs_runtime.go @@ -28,13 +28,19 @@ func (c *Compiler) formatSafeOutputsRunsOn(safeOutputs *SafeOutputsConfig) strin } // usesPatchesAndCheckouts checks if the workflow uses safe outputs that require -// git patches and checkouts (create-pull-request or push-to-pull-request-branch) +// git patches and checkouts (create-pull-request or push-to-pull-request-branch). +// Staged handlers are excluded because they only emit preview output and do not +// perform real git operations or API calls. func usesPatchesAndCheckouts(safeOutputs *SafeOutputsConfig) bool { if safeOutputs == nil { return false } - result := safeOutputs.CreatePullRequests != nil || safeOutputs.PushToPullRequestBranch != nil - safeOutputsRuntimeLog.Printf("usesPatchesAndCheckouts: createPR=%v, pushToPRBranch=%v, result=%v", - safeOutputs.CreatePullRequests != nil, safeOutputs.PushToPullRequestBranch != nil, result) + createPRNeedsCheckout := safeOutputs.CreatePullRequests != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.CreatePullRequests.Staged) + pushToPRNeedsCheckout := safeOutputs.PushToPullRequestBranch != nil && !isHandlerStaged(safeOutputs.Staged, safeOutputs.PushToPullRequestBranch.Staged) + result := createPRNeedsCheckout || pushToPRNeedsCheckout + safeOutputsRuntimeLog.Printf("usesPatchesAndCheckouts: createPR=%v(needsCheckout=%v), pushToPRBranch=%v(needsCheckout=%v), result=%v", + safeOutputs.CreatePullRequests != nil, createPRNeedsCheckout, + safeOutputs.PushToPullRequestBranch != nil, pushToPRNeedsCheckout, + result) return result } From 396bfd6ee1d9ab416abb6944a94e3ffac904acdb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 05:58:13 +0000 Subject: [PATCH 7/7] Add changeset [skip-ci] --- .../patch-skip-write-permissions-staged-safe-outputs.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-skip-write-permissions-staged-safe-outputs.md diff --git a/.changeset/patch-skip-write-permissions-staged-safe-outputs.md b/.changeset/patch-skip-write-permissions-staged-safe-outputs.md new file mode 100644 index 0000000000..b8c59b5056 --- /dev/null +++ b/.changeset/patch-skip-write-permissions-staged-safe-outputs.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Skip write permissions for staged safe output handlers so staged-only safe outputs compile with explicit empty permissions and avoid unnecessary checkout/setup steps.