From fde609ba861a8720d3af9ea8f3f43bc3381e0f8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:37:32 +0000 Subject: [PATCH 1/3] Initial plan From 8856f14ed86a5064536bdaf68c31f634fcef6e1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:30:15 +0000 Subject: [PATCH 2/3] test: add comprehensive staged mode tests for all handler types and cross-repo config Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/419e2a90-90a2-4701-95fa-db6081ff0d14 --- pkg/cli/compile_integration_test.go | 75 +++++++ .../compiler_safe_outputs_config_test.go | 184 ++++++++++++++++++ 2 files changed, 259 insertions(+) diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index 43e6738edda..ed01c1dd81d 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -1980,3 +1980,78 @@ Call notify_dynamic to send notifications. t.Errorf("Lock file should preserve the GitHub Actions expression in allowed_repositories\nLock file content:\n%s", lockContentStr) } } + +// 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) + } +} 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 { From e1024c0086fdd6bac6a3e59166c524fd030d90fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:35:56 +0000 Subject: [PATCH 3/3] chore: fix indentation in test file after merge --- pkg/cli/compile_integration_test.go | 140 ++++++++++++++-------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index 2d659943e76..1eaeb3e5581 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -2059,92 +2059,92 @@ Test that per-handler staged mode for update-discussion with cross-repo config c // 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) { -setup := setupIntegrationTest(t) -defer setup.cleanup() + setup := setupIntegrationTest(t) + defer setup.cleanup() -// Use the canonical test workflow fixture -srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-actions-repo.md") -dstPath := filepath.Join(setup.workflowsDir, "test-actions-repo.md") + // Use the canonical test workflow fixture + srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-actions-repo.md") + dstPath := filepath.Join(setup.workflowsDir, "test-actions-repo.md") -srcContent, err := os.ReadFile(srcPath) -if err != nil { -t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err) -} -if err := os.WriteFile(dstPath, srcContent, 0644); err != nil { -t.Fatalf("Failed to write workflow to test dir: %v", err) -} + srcContent, err := os.ReadFile(srcPath) + if err != nil { + t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err) + } + if err := os.WriteFile(dstPath, srcContent, 0644); err != nil { + t.Fatalf("Failed to write workflow to test dir: %v", err) + } -cmd := exec.Command(setup.binaryPath, "compile", -"--action-mode", "action", -"--actions-repo", "myorg/custom-aw-actions", -"--action-tag", "v9.9.9", -dstPath, -) -output, err := cmd.CombinedOutput() -if err != nil { -t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) -} + cmd := exec.Command(setup.binaryPath, "compile", + "--action-mode", "action", + "--actions-repo", "myorg/custom-aw-actions", + "--action-tag", "v9.9.9", + dstPath, + ) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) + } -lockFilePath := filepath.Join(setup.workflowsDir, "test-actions-repo.lock.yml") -lockContent, err := os.ReadFile(lockFilePath) -if err != nil { -t.Fatalf("Failed to read lock file: %v", err) -} -lockContentStr := string(lockContent) + lockFilePath := filepath.Join(setup.workflowsDir, "test-actions-repo.lock.yml") + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) -// The custom repo should appear in the lock file -if !strings.Contains(lockContentStr, "myorg/custom-aw-actions") { -t.Errorf("Lock file should contain the custom actions repo 'myorg/custom-aw-actions'\nLock file content:\n%s", lockContentStr) -} + // The custom repo should appear in the lock file + if !strings.Contains(lockContentStr, "myorg/custom-aw-actions") { + t.Errorf("Lock file should contain the custom actions repo 'myorg/custom-aw-actions'\nLock file content:\n%s", lockContentStr) + } -// The default repo should NOT appear -if strings.Contains(lockContentStr, "github/gh-aw-actions") { -t.Errorf("Lock file should NOT contain the default 'github/gh-aw-actions' when overridden\nLock file content:\n%s", lockContentStr) -} + // The default repo should NOT appear + if strings.Contains(lockContentStr, "github/gh-aw-actions") { + t.Errorf("Lock file should NOT contain the default 'github/gh-aw-actions' when overridden\nLock file content:\n%s", lockContentStr) + } -t.Logf("Actions repo flag test passed - custom repo baked into lock file: %s", lockFilePath) + t.Logf("Actions repo flag test passed - custom repo baked into lock file: %s", lockFilePath) } // TestCompileWithActionsRepoDefaultFallback verifies that without --actions-repo, the default // github/gh-aw-actions repository is used in the lock file when action mode is set. func TestCompileWithActionsRepoDefaultFallback(t *testing.T) { -setup := setupIntegrationTest(t) -defer setup.cleanup() + setup := setupIntegrationTest(t) + defer setup.cleanup() -// Use the canonical test workflow fixture -srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-actions-repo.md") -dstPath := filepath.Join(setup.workflowsDir, "test-actions-repo.md") + // Use the canonical test workflow fixture + srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-actions-repo.md") + dstPath := filepath.Join(setup.workflowsDir, "test-actions-repo.md") -srcContent, err := os.ReadFile(srcPath) -if err != nil { -t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err) -} -if err := os.WriteFile(dstPath, srcContent, 0644); err != nil { -t.Fatalf("Failed to write workflow to test dir: %v", err) -} + srcContent, err := os.ReadFile(srcPath) + if err != nil { + t.Fatalf("Failed to read source workflow file %s: %v", srcPath, err) + } + if err := os.WriteFile(dstPath, srcContent, 0644); err != nil { + t.Fatalf("Failed to write workflow to test dir: %v", err) + } -// No --actions-repo flag; action mode with a tag -cmd := exec.Command(setup.binaryPath, "compile", -"--action-mode", "action", -"--action-tag", "v9.9.9", -dstPath, -) -output, err := cmd.CombinedOutput() -if err != nil { -t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) -} + // No --actions-repo flag; action mode with a tag + cmd := exec.Command(setup.binaryPath, "compile", + "--action-mode", "action", + "--action-tag", "v9.9.9", + dstPath, + ) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) + } -lockFilePath := filepath.Join(setup.workflowsDir, "test-actions-repo.lock.yml") -lockContent, err := os.ReadFile(lockFilePath) -if err != nil { -t.Fatalf("Failed to read lock file: %v", err) -} -lockContentStr := string(lockContent) + lockFilePath := filepath.Join(setup.workflowsDir, "test-actions-repo.lock.yml") + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) -// Default repo should appear in the lock file -if !strings.Contains(lockContentStr, "github/gh-aw-actions") { -t.Errorf("Lock file should contain the default 'github/gh-aw-actions' when no override is specified\nLock file content:\n%s", lockContentStr) -} + // Default repo should appear in the lock file + if !strings.Contains(lockContentStr, "github/gh-aw-actions") { + t.Errorf("Lock file should contain the default 'github/gh-aw-actions' when no override is specified\nLock file content:\n%s", lockContentStr) + } -t.Logf("Default actions repo test passed - default repo baked into lock file: %s", lockFilePath) + t.Logf("Default actions repo test passed - default repo baked into lock file: %s", lockFilePath) }