diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index 8b4b891afba..1eaeb3e5581 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -1981,6 +1981,81 @@ Call notify_dynamic to send notifications. } } +// TestCompileStagedSafeOutputsUpdateDiscussionWithTargetRepo verifies that the exact +// issue scenario from GitHub issue "staged: true does not work within individual safe outputs" +// compiles correctly: update-discussion with staged: true, target-repo, and additional options +// must include staged: true in GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG and exclude discussions: write. +func TestCompileStagedSafeOutputsUpdateDiscussionWithTargetRepo(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + testWorkflow := `--- +name: Staged Update Discussion +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +safe-outputs: + github-token: ${{ secrets.READ_DISCUSSIONS_TOKEN }} + allowed-github-references: [mona/lisa] + create-issue: + github-token: ${{ secrets.WRITE_ISSUE_TOKEN }} + title-prefix: "[MonaLisa]" + close-older-issues: true + expires: 7d + update-discussion: + staged: true + target: "*" + target-repo: mona/discussions + max: 120 + labels: + allowed-labels: + - Label1 + - Label2 +--- + +Test that per-handler staged mode for update-discussion with cross-repo config compiles correctly. +` + testWorkflowPath := filepath.Join(setup.workflowsDir, "staged-update-discussion.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-update-discussion.lock.yml") + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + + // GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG must include staged: true for update_discussion + if !strings.Contains(lockContentStr, `\"staged\":true`) { + t.Errorf("GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG should include staged:true for update_discussion\nLock file content:\n%s", lockContentStr) + } + + // update-discussion is staged, so discussions: write must NOT be in safe_outputs job permissions + if strings.Contains(lockContentStr, "discussions: write") { + t.Errorf("Lock file should NOT contain 'discussions: write' when update-discussion is staged\nLock file content:\n%s", lockContentStr) + } + + // create-issue is NOT staged, so issues: write must be present + if !strings.Contains(lockContentStr, "issues: write") { + t.Errorf("Lock file should contain 'issues: write' for non-staged create-issue\nLock file content:\n%s", lockContentStr) + } + + // Global GH_AW_SAFE_OUTPUTS_STAGED must NOT be set (only individual handler is staged) + if strings.Contains(lockContentStr, `GH_AW_SAFE_OUTPUTS_STAGED: "true"`) { + t.Errorf("Lock file should NOT set global GH_AW_SAFE_OUTPUTS_STAGED when only individual handlers are staged\nLock file content:\n%s", lockContentStr) + } +} + // TestCompileWithActionsRepoFlag verifies that the --actions-repo flag causes the // custom repository to be used in action mode instead of the default github/gh-aw-actions. func TestCompileWithActionsRepoFlag(t *testing.T) { diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index ed638ccfae3..94b20bb2ece 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -1828,6 +1828,190 @@ func TestHandlerConfigStagedMode(t *testing.T) { }, handlerKey: "call_workflow", }, + { + name: "close_issue staged", + safeOutputs: &SafeOutputsConfig{ + CloseIssues: &CloseEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "close_issue", + }, + { + name: "close_discussion staged", + safeOutputs: &SafeOutputsConfig{ + CloseDiscussions: &CloseEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "close_discussion", + }, + { + name: "create_discussion staged", + safeOutputs: &SafeOutputsConfig{ + CreateDiscussions: &CreateDiscussionsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "create_discussion", + }, + { + name: "hide_comment staged", + safeOutputs: &SafeOutputsConfig{ + HideComment: &HideCommentConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "hide_comment", + }, + { + name: "add_reviewer staged", + safeOutputs: &SafeOutputsConfig{ + AddReviewer: &AddReviewerConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "add_reviewer", + }, + { + name: "update_release staged", + safeOutputs: &SafeOutputsConfig{ + UpdateRelease: &UpdateReleaseConfig{ + UpdateEntityConfig: UpdateEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + }, + handlerKey: "update_release", + }, + { + name: "remove_labels staged", + safeOutputs: &SafeOutputsConfig{ + RemoveLabels: &RemoveLabelsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "remove_labels", + }, + { + name: "assign_to_user staged", + safeOutputs: &SafeOutputsConfig{ + AssignToUser: &AssignToUserConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "assign_to_user", + }, + { + name: "unassign_from_user staged", + safeOutputs: &SafeOutputsConfig{ + UnassignFromUser: &UnassignFromUserConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "unassign_from_user", + }, + { + name: "create_code_scanning_alert staged", + safeOutputs: &SafeOutputsConfig{ + CreateCodeScanningAlerts: &CreateCodeScanningAlertsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "create_code_scanning_alert", + }, + { + name: "autofix_code_scanning_alert staged", + safeOutputs: &SafeOutputsConfig{ + AutofixCodeScanningAlert: &AutofixCodeScanningAlertConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "autofix_code_scanning_alert", + }, + { + name: "link_sub_issue staged", + safeOutputs: &SafeOutputsConfig{ + LinkSubIssue: &LinkSubIssueConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "link_sub_issue", + }, + { + name: "assign_milestone staged", + safeOutputs: &SafeOutputsConfig{ + AssignMilestone: &AssignMilestoneConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "assign_milestone", + }, + { + name: "set_issue_type staged", + safeOutputs: &SafeOutputsConfig{ + SetIssueType: &SetIssueTypeConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "set_issue_type", + }, + { + name: "mark_pull_request_as_ready_for_review staged", + safeOutputs: &SafeOutputsConfig{ + MarkPullRequestAsReadyForReview: &MarkPullRequestAsReadyForReviewConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "mark_pull_request_as_ready_for_review", + }, + { + name: "update_discussion staged with target-repo (cross-repo config)", + safeOutputs: &SafeOutputsConfig{ + UpdateDiscussions: &UpdateDiscussionsConfig{ + UpdateEntityConfig: UpdateEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + SafeOutputTargetConfig: SafeOutputTargetConfig{ + Target: "*", + TargetRepoSlug: "mona/discussions", + }, + }, + }, + }, + handlerKey: "update_discussion", + }, } for _, tt := range tests {