Skip to content
Merged

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 1 addition & 111 deletions .github/workflows/changeset.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .github/workflows/smoke-claude.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .github/workflows/smoke-codex.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions pkg/workflow/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,8 @@ func generateCacheMemorySteps(builder *strings.Builder, data *WorkflowData) {
// Use actions/cache/restore for restore-only caches or when threat detection is enabled
// When threat detection is enabled, we only restore the cache and defer saving to a separate job after detection
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The multi-line condition for threatDetectionEnabled looks correct. Consider extracting EngineDisabled && len(Steps) == 0 into a helper method like ThreatDetection.IsEffectivelyDisabled() for readability across the multiple call sites in this file.

// Use actions/cache for normal caches (which auto-saves via post-action)
threatDetectionEnabled := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil
threatDetectionEnabled := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0)
useRestoreOnly := cache.RestoreOnly || threatDetectionEnabled

var actionName string
Expand Down Expand Up @@ -531,7 +532,8 @@ func generateCacheMemoryArtifactUpload(builder *strings.Builder, data *WorkflowD

// Only upload artifacts when threat detection is enabled (needed for update_cache_memory job)
// When threat detection is disabled, cache is saved automatically by actions/cache post-action
threatDetectionEnabled := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil
threatDetectionEnabled := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0)
if !threatDetectionEnabled {
cacheLog.Print("Skipping cache-memory artifact upload (threat detection disabled)")
return
Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,8 @@ func (c *Compiler) buildQmdIndexingJobWrapper(data *WorkflowData) error {
// buildMemoryManagementJobs builds memory management jobs (push_repo_memory and update_cache_memory).
// These jobs handle artifact-based memory persistence to git branches and GitHub Actions cache.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same pattern as cache.go — if this condition is used in 3+ places, a shared helper would reduce future divergence risk. The logic correctly handles the case where the engine is disabled with no custom steps.

func (c *Compiler) buildMemoryManagementJobs(data *WorkflowData) error {
threatDetectionEnabledForSafeJobs := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil
threatDetectionEnabledForSafeJobs := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0)

// Build push_repo_memory job if repo-memory is configured
pushRepoMemoryJobName, err := c.buildPushRepoMemoryJobWrapper(data, threatDetectionEnabledForSafeJobs)
Expand Down
8 changes: 6 additions & 2 deletions pkg/workflow/compiler_safe_output_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ func (c *Compiler) buildSafeOutputsJobs(data *WorkflowData, jobName, markdownPat
compilerSafeOutputJobsLog.Print("Building safe outputs jobs")

// Detection is always enabled for safe-outputs workflows unless threat-detection is explicitly
// disabled (threat-detection: false). ThreatDetection is nil only when explicitly disabled.
threatDetectionEnabled := data.SafeOutputs.ThreatDetection != nil
// disabled (threat-detection: false) or the engine is disabled with no custom steps
// (threat-detection: { engine: false } with no steps). ThreatDetection is nil only when
// explicitly disabled. When engine is false with no custom steps, the detection job has
// nothing to run so it is skipped entirely.
threatDetectionEnabled := data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0)

// Build the separate detection job. Detection runs by default for all safe-outputs workflows
// and is only skipped when ThreatDetection is nil (i.e. threat-detection: false was set).
Comment thread
davidslater marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The predicate data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0 is duplicated in at least 6 files. Consider extracting a helper like isDetectionJobEnabled(so *SafeOutputsConfig) bool in a shared location to prevent future drift.

Expand Down
7 changes: 5 additions & 2 deletions pkg/workflow/compiler_safe_outputs_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa
// Compute permissions based on configured safe outputs (principle of least privilege)
permissions := ComputePermissionsForSafeOutputs(data.SafeOutputs)

// Track whether threat detection job is enabled for step conditions
threatDetectionEnabled := data.SafeOutputs.ThreatDetection != nil
// Track whether threat detection job is enabled for step conditions.
// When the engine is explicitly disabled and there are no custom steps,
// the detection job is skipped entirely (see buildDetectionJob).
threatDetectionEnabled := data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0)

// Note: GitHub App token minting step is added later (after setup/downloads)
// to ensure proper step ordering. See insertion logic below.
Expand Down
8 changes: 7 additions & 1 deletion pkg/workflow/compiler_yaml_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,13 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
// NOTE: Git patch generation has been moved to the safe-outputs MCP server
// The patch is now generated when create_pull_request or push_to_pull_request_branch
// tools are called, providing immediate error feedback if no changes are present.
if usesPatchesAndCheckouts(data.SafeOutputs) {
// Include patches in the artifact when:
// 1. Safe outputs needs them for checkout (non-staged create_pull_request/push_to_pull_request_branch)
// 2. Threat detection is enabled (detection job needs patches for security analysis, even when the
// safe-output handler is staged and doesn't need checkout itself)
threatDetectionNeedsPatches := data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Including patch files in the unified agent artifact when threat detection is enabled is important behavior but currently untested. Please add a compiler_yaml_main_job test that compiles a workflow where PR handlers are staged (so usesPatchesAndCheckouts() is false) while threat detection is enabled, and assert the upload-artifact paths include /tmp/gh-aw/aw-*.patch.

Copilot uses AI. Check for mistakes.
if usesPatchesAndCheckouts(data.SafeOutputs) || threatDetectionNeedsPatches {
artifactPaths = append(artifactPaths, "/tmp/gh-aw/aw-*.patch")
}

Expand Down
6 changes: 4 additions & 2 deletions pkg/workflow/notify_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa
customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_AGENT_CONCLUSION: ${{ needs.%s.result }}\n", mainJobName))

// Pass detection conclusion if threat detection is enabled (in separate detection job)
if data.SafeOutputs.ThreatDetection != nil {
if data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0) {
customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_DETECTION_CONCLUSION: ${{ needs.%s.outputs.detection_conclusion }}\n", constants.DetectionJobName))
notifyCommentLog.Print("Added detection conclusion environment variable to conclusion job")
}
Expand Down Expand Up @@ -438,7 +439,8 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa

// When threat detection is enabled, the conclusion job also depends on the detection job
// so that needs.detection.outputs.detection_conclusion is accessible.
if data.SafeOutputs.ThreatDetection != nil {
if data.SafeOutputs.ThreatDetection != nil &&
!(data.SafeOutputs.ThreatDetection.EngineDisabled && len(data.SafeOutputs.ThreatDetection.Steps) == 0) {
needs = append(needs, string(constants.DetectionJobName))
notifyCommentLog.Print("Added detection job dependency to conclusion job")
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/threat_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,16 @@ func (c *Compiler) buildDetectionJob(data *WorkflowData) (*Job, error) {
return nil, nil
}

// When the engine is explicitly disabled and there are no custom steps,
// there is nothing to run in the detection job — skip it entirely.
// The detection job would only create an empty detection.log and the parser
// would correctly fail with "No THREAT_DETECTION_RESULT found".
td := data.SafeOutputs.ThreatDetection
if td.EngineDisabled && len(td.Steps) == 0 {
Comment thread
davidslater marked this conversation as resolved.
Outdated
threatLog.Print("Threat detection engine disabled with no custom steps, skipping detection job")
return nil, nil
}

var steps []string

// Add setup action steps (same as agent job - installs the agentic engine)
Expand Down