From 25a17273d77bf8b6d1993d55a8a193e5a84301be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:11:29 +0000 Subject: [PATCH 1/4] Initial plan From b69824eaae76990bd98986a0b80d9a2a57d6de15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:32:49 +0000 Subject: [PATCH 2/4] fix: imported safe-output fragments no longer override explicit threat-detection: false When a workflow sets threat-detection: false (or enabled: false), add a ThreatDetectionExplicitlyDisabled flag to SafeOutputsConfig. The flag is set in extractSafeOutputsConfig when the threat-detection key is present but resolves to nil. mergeSafeOutputConfig now guards the ThreatDetection merge with !result.ThreatDetectionExplicitlyDisabled so imported fragments cannot re-enable threat detection that was explicitly disabled. Adds three tests: - TestMergeSafeOutputsThreatDetectionExplicitDisableNotOverridden (unit) - TestMergeSafeOutputsThreatDetectionImportedWhenMainHasNone (regression) - TestSafeOutputsImportDoesNotReenableThreatDetection (integration) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_types.go | 107 +++++++++++----------- pkg/workflow/imports.go | 4 +- pkg/workflow/safe_outputs_config.go | 3 + pkg/workflow/safe_outputs_import_test.go | 108 +++++++++++++++++++++++ 4 files changed, 168 insertions(+), 54 deletions(-) diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index cdd43bf97e4..34909422abe 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -473,59 +473,60 @@ type BaseSafeOutputConfig struct { // SafeOutputsConfig holds configuration for automatic output routes type SafeOutputsConfig struct { - CreateIssues *CreateIssuesConfig `yaml:"create-issue,omitempty"` - CreateDiscussions *CreateDiscussionsConfig `yaml:"create-discussion,omitempty"` - UpdateDiscussions *UpdateDiscussionsConfig `yaml:"update-discussion,omitempty"` - CloseDiscussions *CloseDiscussionsConfig `yaml:"close-discussion,omitempty"` - CloseIssues *CloseIssuesConfig `yaml:"close-issue,omitempty"` - ClosePullRequests *ClosePullRequestsConfig `yaml:"close-pull-request,omitempty"` - MarkPullRequestAsReadyForReview *MarkPullRequestAsReadyForReviewConfig `yaml:"mark-pull-request-as-ready-for-review,omitempty"` - AddComments *AddCommentsConfig `yaml:"add-comment,omitempty"` - CreatePullRequests *CreatePullRequestsConfig `yaml:"create-pull-request,omitempty"` - CreatePullRequestReviewComments *CreatePullRequestReviewCommentsConfig `yaml:"create-pull-request-review-comment,omitempty"` - SubmitPullRequestReview *SubmitPullRequestReviewConfig `yaml:"submit-pull-request-review,omitempty"` // Submit a PR review with status (APPROVE, REQUEST_CHANGES, COMMENT) - ReplyToPullRequestReviewComment *ReplyToPullRequestReviewCommentConfig `yaml:"reply-to-pull-request-review-comment,omitempty"` // Reply to existing review comments on PRs - ResolvePullRequestReviewThread *ResolvePullRequestReviewThreadConfig `yaml:"resolve-pull-request-review-thread,omitempty"` // Resolve a review thread on a pull request - CreateCodeScanningAlerts *CreateCodeScanningAlertsConfig `yaml:"create-code-scanning-alerts,omitempty"` - AutofixCodeScanningAlert *AutofixCodeScanningAlertConfig `yaml:"autofix-code-scanning-alert,omitempty"` - AddLabels *AddLabelsConfig `yaml:"add-labels,omitempty"` - RemoveLabels *RemoveLabelsConfig `yaml:"remove-labels,omitempty"` - AddReviewer *AddReviewerConfig `yaml:"add-reviewer,omitempty"` - AssignMilestone *AssignMilestoneConfig `yaml:"assign-milestone,omitempty"` - AssignToAgent *AssignToAgentConfig `yaml:"assign-to-agent,omitempty"` - AssignToUser *AssignToUserConfig `yaml:"assign-to-user,omitempty"` // Assign users to issues - UnassignFromUser *UnassignFromUserConfig `yaml:"unassign-from-user,omitempty"` // Remove assignees from issues - UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` - UpdatePullRequests *UpdatePullRequestsConfig `yaml:"update-pull-request,omitempty"` // Update GitHub pull request title/body - PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pull-request-branch,omitempty"` - UploadAssets *UploadAssetsConfig `yaml:"upload-asset,omitempty"` - UpdateRelease *UpdateReleaseConfig `yaml:"update-release,omitempty"` // Update GitHub release descriptions - CreateAgentSessions *CreateAgentSessionConfig `yaml:"create-agent-session,omitempty"` // Create GitHub Copilot coding agent sessions - UpdateProjects *UpdateProjectConfig `yaml:"update-project,omitempty"` // Smart project board management (create/add/update) - CreateProjects *CreateProjectsConfig `yaml:"create-project,omitempty"` // Create GitHub Projects V2 - CreateProjectStatusUpdates *CreateProjectStatusUpdateConfig `yaml:"create-project-status-update,omitempty"` // Create GitHub project status updates - LinkSubIssue *LinkSubIssueConfig `yaml:"link-sub-issue,omitempty"` // Link issues as sub-issues - HideComment *HideCommentConfig `yaml:"hide-comment,omitempty"` // Hide comments - DispatchWorkflow *DispatchWorkflowConfig `yaml:"dispatch-workflow,omitempty"` // Dispatch workflow_dispatch events to other workflows - MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality - MissingData *MissingDataConfig `yaml:"missing-data,omitempty"` // Optional for reporting missing data required to achieve goals - NoOp *NoOpConfig `yaml:"noop,omitempty"` // No-op output for logging only (always available as fallback) - ThreatDetection *ThreatDetectionConfig `yaml:"threat-detection,omitempty"` // Threat detection configuration - Jobs map[string]*SafeJobConfig `yaml:"jobs,omitempty"` // Safe-jobs configuration (moved from top-level) - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App credentials for token minting - AllowedDomains []string `yaml:"allowed-domains,omitempty"` - AllowGitHubReferences []string `yaml:"allowed-github-references,omitempty"` // Allowed repositories for GitHub references (e.g., ["repo", "org/repo2"]) - Staged bool `yaml:"staged,omitempty"` // If true, emit step summary messages instead of making GitHub API calls - Env map[string]string `yaml:"env,omitempty"` // Environment variables to pass to safe output jobs - GitHubToken string `yaml:"github-token,omitempty"` // GitHub token for safe output jobs - MaximumPatchSize int `yaml:"max-patch-size,omitempty"` // Maximum allowed patch size in KB (defaults to 1024) - RunsOn string `yaml:"runs-on,omitempty"` // Runner configuration for safe-outputs jobs - Messages *SafeOutputMessagesConfig `yaml:"messages,omitempty"` // Custom message templates for footer and notifications - Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs - Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included) - GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false) - MaxBotMentions *string `yaml:"max-bot-mentions,omitempty"` // Maximum bot trigger references (e.g. 'fixes #123') allowed before filtering. Default: 10. Supports integer or GitHub Actions expression. - AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured) + CreateIssues *CreateIssuesConfig `yaml:"create-issue,omitempty"` + CreateDiscussions *CreateDiscussionsConfig `yaml:"create-discussion,omitempty"` + UpdateDiscussions *UpdateDiscussionsConfig `yaml:"update-discussion,omitempty"` + CloseDiscussions *CloseDiscussionsConfig `yaml:"close-discussion,omitempty"` + CloseIssues *CloseIssuesConfig `yaml:"close-issue,omitempty"` + ClosePullRequests *ClosePullRequestsConfig `yaml:"close-pull-request,omitempty"` + MarkPullRequestAsReadyForReview *MarkPullRequestAsReadyForReviewConfig `yaml:"mark-pull-request-as-ready-for-review,omitempty"` + AddComments *AddCommentsConfig `yaml:"add-comment,omitempty"` + CreatePullRequests *CreatePullRequestsConfig `yaml:"create-pull-request,omitempty"` + CreatePullRequestReviewComments *CreatePullRequestReviewCommentsConfig `yaml:"create-pull-request-review-comment,omitempty"` + SubmitPullRequestReview *SubmitPullRequestReviewConfig `yaml:"submit-pull-request-review,omitempty"` // Submit a PR review with status (APPROVE, REQUEST_CHANGES, COMMENT) + ReplyToPullRequestReviewComment *ReplyToPullRequestReviewCommentConfig `yaml:"reply-to-pull-request-review-comment,omitempty"` // Reply to existing review comments on PRs + ResolvePullRequestReviewThread *ResolvePullRequestReviewThreadConfig `yaml:"resolve-pull-request-review-thread,omitempty"` // Resolve a review thread on a pull request + CreateCodeScanningAlerts *CreateCodeScanningAlertsConfig `yaml:"create-code-scanning-alerts,omitempty"` + AutofixCodeScanningAlert *AutofixCodeScanningAlertConfig `yaml:"autofix-code-scanning-alert,omitempty"` + AddLabels *AddLabelsConfig `yaml:"add-labels,omitempty"` + RemoveLabels *RemoveLabelsConfig `yaml:"remove-labels,omitempty"` + AddReviewer *AddReviewerConfig `yaml:"add-reviewer,omitempty"` + AssignMilestone *AssignMilestoneConfig `yaml:"assign-milestone,omitempty"` + AssignToAgent *AssignToAgentConfig `yaml:"assign-to-agent,omitempty"` + AssignToUser *AssignToUserConfig `yaml:"assign-to-user,omitempty"` // Assign users to issues + UnassignFromUser *UnassignFromUserConfig `yaml:"unassign-from-user,omitempty"` // Remove assignees from issues + UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` + UpdatePullRequests *UpdatePullRequestsConfig `yaml:"update-pull-request,omitempty"` // Update GitHub pull request title/body + PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pull-request-branch,omitempty"` + UploadAssets *UploadAssetsConfig `yaml:"upload-asset,omitempty"` + UpdateRelease *UpdateReleaseConfig `yaml:"update-release,omitempty"` // Update GitHub release descriptions + CreateAgentSessions *CreateAgentSessionConfig `yaml:"create-agent-session,omitempty"` // Create GitHub Copilot coding agent sessions + UpdateProjects *UpdateProjectConfig `yaml:"update-project,omitempty"` // Smart project board management (create/add/update) + CreateProjects *CreateProjectsConfig `yaml:"create-project,omitempty"` // Create GitHub Projects V2 + CreateProjectStatusUpdates *CreateProjectStatusUpdateConfig `yaml:"create-project-status-update,omitempty"` // Create GitHub project status updates + LinkSubIssue *LinkSubIssueConfig `yaml:"link-sub-issue,omitempty"` // Link issues as sub-issues + HideComment *HideCommentConfig `yaml:"hide-comment,omitempty"` // Hide comments + DispatchWorkflow *DispatchWorkflowConfig `yaml:"dispatch-workflow,omitempty"` // Dispatch workflow_dispatch events to other workflows + MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality + MissingData *MissingDataConfig `yaml:"missing-data,omitempty"` // Optional for reporting missing data required to achieve goals + NoOp *NoOpConfig `yaml:"noop,omitempty"` // No-op output for logging only (always available as fallback) + ThreatDetection *ThreatDetectionConfig `yaml:"threat-detection,omitempty"` // Threat detection configuration + Jobs map[string]*SafeJobConfig `yaml:"jobs,omitempty"` // Safe-jobs configuration (moved from top-level) + App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App credentials for token minting + AllowedDomains []string `yaml:"allowed-domains,omitempty"` + AllowGitHubReferences []string `yaml:"allowed-github-references,omitempty"` // Allowed repositories for GitHub references (e.g., ["repo", "org/repo2"]) + Staged bool `yaml:"staged,omitempty"` // If true, emit step summary messages instead of making GitHub API calls + Env map[string]string `yaml:"env,omitempty"` // Environment variables to pass to safe output jobs + GitHubToken string `yaml:"github-token,omitempty"` // GitHub token for safe output jobs + MaximumPatchSize int `yaml:"max-patch-size,omitempty"` // Maximum allowed patch size in KB (defaults to 1024) + RunsOn string `yaml:"runs-on,omitempty"` // Runner configuration for safe-outputs jobs + Messages *SafeOutputMessagesConfig `yaml:"messages,omitempty"` // Custom message templates for footer and notifications + Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs + Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included) + GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false) + MaxBotMentions *string `yaml:"max-bot-mentions,omitempty"` // Maximum bot trigger references (e.g. 'fixes #123') allowed before filtering. Default: 10. Supports integer or GitHub Actions expression. + AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured) + ThreatDetectionExplicitlyDisabled bool `yaml:"-"` // Internal: true when threat-detection was explicitly disabled in frontmatter (e.g. threat-detection: false) } // SafeOutputMessagesConfig holds custom message templates for safe-output footer and notification messages diff --git a/pkg/workflow/imports.go b/pkg/workflow/imports.go index d6a2ef8b002..4c00926550f 100644 --- a/pkg/workflow/imports.go +++ b/pkg/workflow/imports.go @@ -630,7 +630,9 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c * if result.NoOp == nil && importedConfig.NoOp != nil { result.NoOp = importedConfig.NoOp } - if result.ThreatDetection == nil && importedConfig.ThreatDetection != nil { + // ThreatDetection is a workflow-level concern. Only merge from import when the main workflow + // has not explicitly disabled it (threat-detection: false / enabled: false). + if !result.ThreatDetectionExplicitlyDisabled && result.ThreatDetection == nil && importedConfig.ThreatDetection != nil { result.ThreatDetection = importedConfig.ThreatDetection } diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index d4cf92d096c..872d62470ac 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -400,6 +400,9 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut threatDetectionConfig := c.parseThreatDetectionConfig(outputMap) if threatDetectionConfig != nil { config.ThreatDetection = threatDetectionConfig + } else if _, exists := outputMap["threat-detection"]; exists { + // threat-detection key was present but resolved to nil (e.g. threat-detection: false) + config.ThreatDetectionExplicitlyDisabled = true } // Handle runs-on configuration diff --git a/pkg/workflow/safe_outputs_import_test.go b/pkg/workflow/safe_outputs_import_test.go index 228d030d238..7a37b3c7014 100644 --- a/pkg/workflow/safe_outputs_import_test.go +++ b/pkg/workflow/safe_outputs_import_test.go @@ -1823,3 +1823,111 @@ safe-outputs: assert.Equal(t, "Shared started", workflowData.SafeOutputs.Messages.RunStarted, "RunStarted should come from shared") assert.Equal(t, "Shared failure", workflowData.SafeOutputs.Messages.RunFailure, "RunFailure should come from shared") } + +// TestMergeSafeOutputsThreatDetectionExplicitDisableNotOverridden tests that when the main workflow +// explicitly disables threat-detection, imported fragments with no threat-detection key (which +// would auto-enable it) do not override the explicit disable. +func TestMergeSafeOutputsThreatDetectionExplicitDisableNotOverridden(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + // Simulate main workflow that explicitly disabled threat-detection. + // The ThreatDetectionExplicitlyDisabled flag is set by extractSafeOutputsConfig + // when threat-detection: false (or enabled: false) is in the frontmatter. + topConfig := &SafeOutputsConfig{ + ThreatDetection: nil, // threat-detection: false → parseThreatDetectionConfig returns nil + ThreatDetectionExplicitlyDisabled: true, // set by extractSafeOutputsConfig when the key exists but is disabled + AddComments: &AddCommentsConfig{}, + } + + // Import fragment that has safe-outputs but no threat-detection key. + // extractSafeOutputsConfig auto-enables ThreatDetection for such fragments, + // so importedConfig.ThreatDetection will be &ThreatDetectionConfig{}. + importedJSON := []string{ + `{"add-comment":{"max":1}}`, + } + + result, err := compiler.MergeSafeOutputs(topConfig, importedJSON) + require.NoError(t, err, "MergeSafeOutputs should not error") + require.NotNil(t, result, "Result should not be nil") + + // The explicit disable must survive the merge: threat detection must remain nil. + assert.Nil(t, result.ThreatDetection, "ThreatDetection must remain nil when explicitly disabled by main workflow") +} + +// TestMergeSafeOutputsThreatDetectionImportedWhenMainHasNone tests that when the main workflow +// has no safe-outputs (topSafeOutputs == nil), threat detection can still be set by an import. +func TestMergeSafeOutputsThreatDetectionImportedWhenMainHasNone(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + // Import fragment with safe-outputs that will auto-enable ThreatDetection. + importedJSON := []string{ + `{"add-comment":{"max":1}}`, + } + + result, err := compiler.MergeSafeOutputs(nil, importedJSON) + require.NoError(t, err, "MergeSafeOutputs should not error") + require.NotNil(t, result, "Result should not be nil") + + // With no explicit disable from main, threat detection should be auto-enabled by the import. + assert.NotNil(t, result.ThreatDetection, "ThreatDetection should be auto-enabled from import when main has no safe-outputs") +} + +// TestSafeOutputsImportDoesNotReenableThreatDetection is an integration test that reproduces +// the bug where an imported fragment re-enables threat-detection that was explicitly disabled +// in the main workflow. This caused a compilation error when sandbox.agent was also false. +func TestSafeOutputsImportDoesNotReenableThreatDetection(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + require.NoError(t, err, "Failed to create workflows directory") + + // Fragment with safe-outputs but no threat-detection key (mimics safe-output-add-comment.md) + sharedWorkflow := `--- +safe-outputs: + add-comment: + max: 1 +--- + +# Shared Add Comment Fragment +` + + sharedFile := filepath.Join(workflowsDir, "safe-output-add-comment.md") + err = os.WriteFile(sharedFile, []byte(sharedWorkflow), 0644) + require.NoError(t, err, "Failed to write shared file") + + // Main workflow: sandbox.agent disabled + threat-detection explicitly disabled + mainWorkflow := `--- +on: issues +engine: copilot +strict: false +sandbox: + agent: false +imports: + - ./safe-output-add-comment.md +safe-outputs: + activation-comments: false + threat-detection: false +--- + +# Main Workflow +` + + mainFile := filepath.Join(workflowsDir, "main.md") + err = os.WriteFile(mainFile, []byte(mainWorkflow), 0644) + require.NoError(t, err, "Failed to write main file") + + oldDir, err := os.Getwd() + require.NoError(t, err, "Failed to get current directory") + err = os.Chdir(workflowsDir) + require.NoError(t, err, "Failed to change directory") + defer func() { _ = os.Chdir(oldDir) }() + + workflowData, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err, "ParseWorkflowFile should not error when threat-detection is explicitly disabled") + require.NotNil(t, workflowData.SafeOutputs, "SafeOutputs should not be nil") + + // The explicit disable must survive the import merge. + assert.Nil(t, workflowData.SafeOutputs.ThreatDetection, "ThreatDetection must remain nil when explicitly disabled by main workflow") +} From eb45e44f3e88788e6a7328954aa34dfc64c5153c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:15:35 +0000 Subject: [PATCH 3/4] refactor: simplify threat-detection merge by checking raw config key instead of sentinel Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_types.go | 107 +++++++++++------------ pkg/workflow/imports.go | 10 ++- pkg/workflow/safe_outputs_config.go | 3 - pkg/workflow/safe_outputs_import_test.go | 32 +++---- 4 files changed, 73 insertions(+), 79 deletions(-) diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 34909422abe..cdd43bf97e4 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -473,60 +473,59 @@ type BaseSafeOutputConfig struct { // SafeOutputsConfig holds configuration for automatic output routes type SafeOutputsConfig struct { - CreateIssues *CreateIssuesConfig `yaml:"create-issue,omitempty"` - CreateDiscussions *CreateDiscussionsConfig `yaml:"create-discussion,omitempty"` - UpdateDiscussions *UpdateDiscussionsConfig `yaml:"update-discussion,omitempty"` - CloseDiscussions *CloseDiscussionsConfig `yaml:"close-discussion,omitempty"` - CloseIssues *CloseIssuesConfig `yaml:"close-issue,omitempty"` - ClosePullRequests *ClosePullRequestsConfig `yaml:"close-pull-request,omitempty"` - MarkPullRequestAsReadyForReview *MarkPullRequestAsReadyForReviewConfig `yaml:"mark-pull-request-as-ready-for-review,omitempty"` - AddComments *AddCommentsConfig `yaml:"add-comment,omitempty"` - CreatePullRequests *CreatePullRequestsConfig `yaml:"create-pull-request,omitempty"` - CreatePullRequestReviewComments *CreatePullRequestReviewCommentsConfig `yaml:"create-pull-request-review-comment,omitempty"` - SubmitPullRequestReview *SubmitPullRequestReviewConfig `yaml:"submit-pull-request-review,omitempty"` // Submit a PR review with status (APPROVE, REQUEST_CHANGES, COMMENT) - ReplyToPullRequestReviewComment *ReplyToPullRequestReviewCommentConfig `yaml:"reply-to-pull-request-review-comment,omitempty"` // Reply to existing review comments on PRs - ResolvePullRequestReviewThread *ResolvePullRequestReviewThreadConfig `yaml:"resolve-pull-request-review-thread,omitempty"` // Resolve a review thread on a pull request - CreateCodeScanningAlerts *CreateCodeScanningAlertsConfig `yaml:"create-code-scanning-alerts,omitempty"` - AutofixCodeScanningAlert *AutofixCodeScanningAlertConfig `yaml:"autofix-code-scanning-alert,omitempty"` - AddLabels *AddLabelsConfig `yaml:"add-labels,omitempty"` - RemoveLabels *RemoveLabelsConfig `yaml:"remove-labels,omitempty"` - AddReviewer *AddReviewerConfig `yaml:"add-reviewer,omitempty"` - AssignMilestone *AssignMilestoneConfig `yaml:"assign-milestone,omitempty"` - AssignToAgent *AssignToAgentConfig `yaml:"assign-to-agent,omitempty"` - AssignToUser *AssignToUserConfig `yaml:"assign-to-user,omitempty"` // Assign users to issues - UnassignFromUser *UnassignFromUserConfig `yaml:"unassign-from-user,omitempty"` // Remove assignees from issues - UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` - UpdatePullRequests *UpdatePullRequestsConfig `yaml:"update-pull-request,omitempty"` // Update GitHub pull request title/body - PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pull-request-branch,omitempty"` - UploadAssets *UploadAssetsConfig `yaml:"upload-asset,omitempty"` - UpdateRelease *UpdateReleaseConfig `yaml:"update-release,omitempty"` // Update GitHub release descriptions - CreateAgentSessions *CreateAgentSessionConfig `yaml:"create-agent-session,omitempty"` // Create GitHub Copilot coding agent sessions - UpdateProjects *UpdateProjectConfig `yaml:"update-project,omitempty"` // Smart project board management (create/add/update) - CreateProjects *CreateProjectsConfig `yaml:"create-project,omitempty"` // Create GitHub Projects V2 - CreateProjectStatusUpdates *CreateProjectStatusUpdateConfig `yaml:"create-project-status-update,omitempty"` // Create GitHub project status updates - LinkSubIssue *LinkSubIssueConfig `yaml:"link-sub-issue,omitempty"` // Link issues as sub-issues - HideComment *HideCommentConfig `yaml:"hide-comment,omitempty"` // Hide comments - DispatchWorkflow *DispatchWorkflowConfig `yaml:"dispatch-workflow,omitempty"` // Dispatch workflow_dispatch events to other workflows - MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality - MissingData *MissingDataConfig `yaml:"missing-data,omitempty"` // Optional for reporting missing data required to achieve goals - NoOp *NoOpConfig `yaml:"noop,omitempty"` // No-op output for logging only (always available as fallback) - ThreatDetection *ThreatDetectionConfig `yaml:"threat-detection,omitempty"` // Threat detection configuration - Jobs map[string]*SafeJobConfig `yaml:"jobs,omitempty"` // Safe-jobs configuration (moved from top-level) - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App credentials for token minting - AllowedDomains []string `yaml:"allowed-domains,omitempty"` - AllowGitHubReferences []string `yaml:"allowed-github-references,omitempty"` // Allowed repositories for GitHub references (e.g., ["repo", "org/repo2"]) - Staged bool `yaml:"staged,omitempty"` // If true, emit step summary messages instead of making GitHub API calls - Env map[string]string `yaml:"env,omitempty"` // Environment variables to pass to safe output jobs - GitHubToken string `yaml:"github-token,omitempty"` // GitHub token for safe output jobs - MaximumPatchSize int `yaml:"max-patch-size,omitempty"` // Maximum allowed patch size in KB (defaults to 1024) - RunsOn string `yaml:"runs-on,omitempty"` // Runner configuration for safe-outputs jobs - Messages *SafeOutputMessagesConfig `yaml:"messages,omitempty"` // Custom message templates for footer and notifications - Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs - Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included) - GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false) - MaxBotMentions *string `yaml:"max-bot-mentions,omitempty"` // Maximum bot trigger references (e.g. 'fixes #123') allowed before filtering. Default: 10. Supports integer or GitHub Actions expression. - AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured) - ThreatDetectionExplicitlyDisabled bool `yaml:"-"` // Internal: true when threat-detection was explicitly disabled in frontmatter (e.g. threat-detection: false) + CreateIssues *CreateIssuesConfig `yaml:"create-issue,omitempty"` + CreateDiscussions *CreateDiscussionsConfig `yaml:"create-discussion,omitempty"` + UpdateDiscussions *UpdateDiscussionsConfig `yaml:"update-discussion,omitempty"` + CloseDiscussions *CloseDiscussionsConfig `yaml:"close-discussion,omitempty"` + CloseIssues *CloseIssuesConfig `yaml:"close-issue,omitempty"` + ClosePullRequests *ClosePullRequestsConfig `yaml:"close-pull-request,omitempty"` + MarkPullRequestAsReadyForReview *MarkPullRequestAsReadyForReviewConfig `yaml:"mark-pull-request-as-ready-for-review,omitempty"` + AddComments *AddCommentsConfig `yaml:"add-comment,omitempty"` + CreatePullRequests *CreatePullRequestsConfig `yaml:"create-pull-request,omitempty"` + CreatePullRequestReviewComments *CreatePullRequestReviewCommentsConfig `yaml:"create-pull-request-review-comment,omitempty"` + SubmitPullRequestReview *SubmitPullRequestReviewConfig `yaml:"submit-pull-request-review,omitempty"` // Submit a PR review with status (APPROVE, REQUEST_CHANGES, COMMENT) + ReplyToPullRequestReviewComment *ReplyToPullRequestReviewCommentConfig `yaml:"reply-to-pull-request-review-comment,omitempty"` // Reply to existing review comments on PRs + ResolvePullRequestReviewThread *ResolvePullRequestReviewThreadConfig `yaml:"resolve-pull-request-review-thread,omitempty"` // Resolve a review thread on a pull request + CreateCodeScanningAlerts *CreateCodeScanningAlertsConfig `yaml:"create-code-scanning-alerts,omitempty"` + AutofixCodeScanningAlert *AutofixCodeScanningAlertConfig `yaml:"autofix-code-scanning-alert,omitempty"` + AddLabels *AddLabelsConfig `yaml:"add-labels,omitempty"` + RemoveLabels *RemoveLabelsConfig `yaml:"remove-labels,omitempty"` + AddReviewer *AddReviewerConfig `yaml:"add-reviewer,omitempty"` + AssignMilestone *AssignMilestoneConfig `yaml:"assign-milestone,omitempty"` + AssignToAgent *AssignToAgentConfig `yaml:"assign-to-agent,omitempty"` + AssignToUser *AssignToUserConfig `yaml:"assign-to-user,omitempty"` // Assign users to issues + UnassignFromUser *UnassignFromUserConfig `yaml:"unassign-from-user,omitempty"` // Remove assignees from issues + UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` + UpdatePullRequests *UpdatePullRequestsConfig `yaml:"update-pull-request,omitempty"` // Update GitHub pull request title/body + PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pull-request-branch,omitempty"` + UploadAssets *UploadAssetsConfig `yaml:"upload-asset,omitempty"` + UpdateRelease *UpdateReleaseConfig `yaml:"update-release,omitempty"` // Update GitHub release descriptions + CreateAgentSessions *CreateAgentSessionConfig `yaml:"create-agent-session,omitempty"` // Create GitHub Copilot coding agent sessions + UpdateProjects *UpdateProjectConfig `yaml:"update-project,omitempty"` // Smart project board management (create/add/update) + CreateProjects *CreateProjectsConfig `yaml:"create-project,omitempty"` // Create GitHub Projects V2 + CreateProjectStatusUpdates *CreateProjectStatusUpdateConfig `yaml:"create-project-status-update,omitempty"` // Create GitHub project status updates + LinkSubIssue *LinkSubIssueConfig `yaml:"link-sub-issue,omitempty"` // Link issues as sub-issues + HideComment *HideCommentConfig `yaml:"hide-comment,omitempty"` // Hide comments + DispatchWorkflow *DispatchWorkflowConfig `yaml:"dispatch-workflow,omitempty"` // Dispatch workflow_dispatch events to other workflows + MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality + MissingData *MissingDataConfig `yaml:"missing-data,omitempty"` // Optional for reporting missing data required to achieve goals + NoOp *NoOpConfig `yaml:"noop,omitempty"` // No-op output for logging only (always available as fallback) + ThreatDetection *ThreatDetectionConfig `yaml:"threat-detection,omitempty"` // Threat detection configuration + Jobs map[string]*SafeJobConfig `yaml:"jobs,omitempty"` // Safe-jobs configuration (moved from top-level) + App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App credentials for token minting + AllowedDomains []string `yaml:"allowed-domains,omitempty"` + AllowGitHubReferences []string `yaml:"allowed-github-references,omitempty"` // Allowed repositories for GitHub references (e.g., ["repo", "org/repo2"]) + Staged bool `yaml:"staged,omitempty"` // If true, emit step summary messages instead of making GitHub API calls + Env map[string]string `yaml:"env,omitempty"` // Environment variables to pass to safe output jobs + GitHubToken string `yaml:"github-token,omitempty"` // GitHub token for safe output jobs + MaximumPatchSize int `yaml:"max-patch-size,omitempty"` // Maximum allowed patch size in KB (defaults to 1024) + RunsOn string `yaml:"runs-on,omitempty"` // Runner configuration for safe-outputs jobs + Messages *SafeOutputMessagesConfig `yaml:"messages,omitempty"` // Custom message templates for footer and notifications + Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs + Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included) + GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false) + MaxBotMentions *string `yaml:"max-bot-mentions,omitempty"` // Maximum bot trigger references (e.g. 'fixes #123') allowed before filtering. Default: 10. Supports integer or GitHub Actions expression. + AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured) } // SafeOutputMessagesConfig holds custom message templates for safe-output footer and notification messages diff --git a/pkg/workflow/imports.go b/pkg/workflow/imports.go index 4c00926550f..dc7f4af4304 100644 --- a/pkg/workflow/imports.go +++ b/pkg/workflow/imports.go @@ -630,10 +630,12 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c * if result.NoOp == nil && importedConfig.NoOp != nil { result.NoOp = importedConfig.NoOp } - // ThreatDetection is a workflow-level concern. Only merge from import when the main workflow - // has not explicitly disabled it (threat-detection: false / enabled: false). - if !result.ThreatDetectionExplicitlyDisabled && result.ThreatDetection == nil && importedConfig.ThreatDetection != nil { - result.ThreatDetection = importedConfig.ThreatDetection + // ThreatDetection is a workflow-level concern; only merge from an import that + // explicitly carries a threat-detection key (not just an auto-enabled default). + if result.ThreatDetection == nil { + if _, hasTD := config["threat-detection"]; hasTD && importedConfig.ThreatDetection != nil { + result.ThreatDetection = importedConfig.ThreatDetection + } } // Merge meta-configuration fields (only set if empty/zero in result) diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 872d62470ac..d4cf92d096c 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -400,9 +400,6 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut threatDetectionConfig := c.parseThreatDetectionConfig(outputMap) if threatDetectionConfig != nil { config.ThreatDetection = threatDetectionConfig - } else if _, exists := outputMap["threat-detection"]; exists { - // threat-detection key was present but resolved to nil (e.g. threat-detection: false) - config.ThreatDetectionExplicitlyDisabled = true } // Handle runs-on configuration diff --git a/pkg/workflow/safe_outputs_import_test.go b/pkg/workflow/safe_outputs_import_test.go index 7a37b3c7014..a8e0b00283a 100644 --- a/pkg/workflow/safe_outputs_import_test.go +++ b/pkg/workflow/safe_outputs_import_test.go @@ -1825,23 +1825,19 @@ safe-outputs: } // TestMergeSafeOutputsThreatDetectionExplicitDisableNotOverridden tests that when the main workflow -// explicitly disables threat-detection, imported fragments with no threat-detection key (which -// would auto-enable it) do not override the explicit disable. +// explicitly disables threat-detection, imported fragments with no threat-detection key do not +// re-enable it. func TestMergeSafeOutputsThreatDetectionExplicitDisableNotOverridden(t *testing.T) { compiler := NewCompilerWithVersion("1.0.0") - // Simulate main workflow that explicitly disabled threat-detection. - // The ThreatDetectionExplicitlyDisabled flag is set by extractSafeOutputsConfig - // when threat-detection: false (or enabled: false) is in the frontmatter. + // Simulate main workflow that explicitly disabled threat-detection: + // threat-detection: false → parseThreatDetectionConfig returns nil. topConfig := &SafeOutputsConfig{ - ThreatDetection: nil, // threat-detection: false → parseThreatDetectionConfig returns nil - ThreatDetectionExplicitlyDisabled: true, // set by extractSafeOutputsConfig when the key exists but is disabled - AddComments: &AddCommentsConfig{}, + ThreatDetection: nil, + AddComments: &AddCommentsConfig{}, } - // Import fragment that has safe-outputs but no threat-detection key. - // extractSafeOutputsConfig auto-enables ThreatDetection for such fragments, - // so importedConfig.ThreatDetection will be &ThreatDetectionConfig{}. + // Import fragment with safe-outputs but no threat-detection key. importedJSON := []string{ `{"add-comment":{"max":1}}`, } @@ -1854,22 +1850,22 @@ func TestMergeSafeOutputsThreatDetectionExplicitDisableNotOverridden(t *testing. assert.Nil(t, result.ThreatDetection, "ThreatDetection must remain nil when explicitly disabled by main workflow") } -// TestMergeSafeOutputsThreatDetectionImportedWhenMainHasNone tests that when the main workflow -// has no safe-outputs (topSafeOutputs == nil), threat detection can still be set by an import. -func TestMergeSafeOutputsThreatDetectionImportedWhenMainHasNone(t *testing.T) { +// TestMergeSafeOutputsThreatDetectionImportedWhenExplicit tests that an import that explicitly +// carries a threat-detection key can set it when the main workflow has not configured it. +func TestMergeSafeOutputsThreatDetectionImportedWhenExplicit(t *testing.T) { compiler := NewCompilerWithVersion("1.0.0") - // Import fragment with safe-outputs that will auto-enable ThreatDetection. + // Import fragment that explicitly enables threat-detection. importedJSON := []string{ - `{"add-comment":{"max":1}}`, + `{"add-comment":{"max":1},"threat-detection":{"enabled":true}}`, } result, err := compiler.MergeSafeOutputs(nil, importedJSON) require.NoError(t, err, "MergeSafeOutputs should not error") require.NotNil(t, result, "Result should not be nil") - // With no explicit disable from main, threat detection should be auto-enabled by the import. - assert.NotNil(t, result.ThreatDetection, "ThreatDetection should be auto-enabled from import when main has no safe-outputs") + // Import explicitly set threat-detection, so it should be present. + assert.NotNil(t, result.ThreatDetection, "ThreatDetection should be set when explicitly configured in import") } // TestSafeOutputsImportDoesNotReenableThreatDetection is an integration test that reproduces From 7c9871c62fd42574414c4a3eee3f5b070f76a7bf Mon Sep 17 00:00:00 2001 From: Smoke Test Bot Date: Wed, 25 Feb 2026 01:03:05 +0000 Subject: [PATCH 4/4] test: Add smoke test file for run 22377081592 --- tmp-smoke-test-22377081592.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 tmp-smoke-test-22377081592.txt diff --git a/tmp-smoke-test-22377081592.txt b/tmp-smoke-test-22377081592.txt new file mode 100644 index 00000000000..a715ec58f7d --- /dev/null +++ b/tmp-smoke-test-22377081592.txt @@ -0,0 +1 @@ +Test file for PR push - smoke test run 22377081592