feat: Add conditional workspace checkout to detection job for patch context#23961
feat: Add conditional workspace checkout to detection job for patch context#23961
Conversation
…ontext When the agent produces a patch (has_patch == 'true'), the detection job now checks out the target repository so the threat detection engine can analyze code changes in the context of the surrounding codebase. This allows the engine to distinguish legitimate patterns from suspicious ones by examining existing dependencies, module structure, and calling code. Changes: - Add buildWorkspaceCheckoutForDetectionStep() that emits a conditional actions/checkout step with persist-credentials: false - Always grant contents: read on the detection job (was previously only in dev/script mode) since the checkout requires it - Update threat detection prompt template with codebase context section instructing the engine to use $GITHUB_WORKSPACE when a patch is present - Add 4 unit tests covering step presence, permissions, ordering, and custom steps scenarios - Recompile all 179 workflows and update wasm golden files Closes #23191 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a conditional actions/checkout in the threat detection job (only when needs.agent.outputs.has_patch == 'true') so the detection engine can analyze patches with full repository context, and updates the threat-detection prompt/docs accordingly.
Changes:
- Add
buildWorkspaceCheckoutForDetectionStep()and insert the conditional workspace checkout into the detection job step list. - Always grant
contents: readpermission to the detection job. - Update both threat-detection prompt copies to instruct the engine to use
$GITHUB_WORKSPACEwhen a patch/bundle is present; recompile lock workflows to include the new step.
Reviewed changes
Copilot reviewed 172 out of 172 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/threat_detection.go | Adds conditional workspace checkout and updates detection job permissions to always include contents: read. |
| pkg/workflow/threat_detection_test.go | Adds tests asserting the checkout step presence/condition/order and contents permission. |
| pkg/workflow/prompts/threat_detection.md | Documents using $GITHUB_WORKSPACE for contextual analysis when patch/bundle present. |
| actions/setup/md/threat_detection.md | Syncs prompt documentation with the package copy. |
| .github/workflows/*.lock.yml | Recompiled workflows to include the conditional checkout step (and permissions where applicable). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // buildWorkspaceCheckoutForDetectionStep creates a checkout step for the detection job. | ||
| // It runs only when the agent job produced a patch, so the detection engine can | ||
| // analyze code changes in the context of the surrounding codebase. | ||
| func (c *Compiler) buildWorkspaceCheckoutForDetectionStep(data *WorkflowData) []string { | ||
| checkoutPin := GetActionPin("actions/checkout") | ||
| if checkoutPin == "" { | ||
| threatLog.Print("No action pin found for actions/checkout, skipping workspace checkout step") | ||
| return nil | ||
| } | ||
|
|
||
| steps := []string{ | ||
| " - name: Checkout repository for patch context\n", | ||
| fmt.Sprintf(" if: needs.%s.outputs.has_patch == 'true'\n", constants.AgentJobName), | ||
| fmt.Sprintf(" uses: %s\n", checkoutPin), | ||
| " with:\n", | ||
| " persist-credentials: false\n", | ||
| } |
There was a problem hiding this comment.
The workspace checkout step is generated manually and ignores existing checkout configuration (WorkflowData.CheckoutConfigs / trial mode overrides) that the main agent job applies via CheckoutManager.GenerateDefaultCheckoutStep. If the agent job checked out a non-default repo/ref/token (or sparse checkout), the detection job will checkout a different workspace, so the threat detector may analyze the patch against the wrong codebase context. Consider generating this step via CheckoutManager.GenerateDefaultCheckoutStep (and then injecting the has_patch condition) so repo/ref/token/fetch-depth/sparse settings stay consistent with the agent job.
| // Add setup action steps (same as agent job - installs the agentic engine) | ||
| setupActionRef := c.resolveActionReference("./actions/setup", data) | ||
| if setupActionRef != "" || c.actionMode.IsScript() { | ||
| // For dev mode (local action path), checkout the actions folder first | ||
| steps = append(steps, c.generateCheckoutActionsFolder(data)...) | ||
| steps = append(steps, c.generateSetupStep(setupActionRef, SetupActionDestination, false)...) | ||
| } | ||
|
|
||
| // Download agent output artifact to access output files (prompt.txt, agent_output.json, patches). | ||
| // Use agent-downstream prefix since this job depends on the agent job. | ||
| agentArtifactPrefix := artifactPrefixExprForAgentDownstreamJob(data) | ||
| steps = append(steps, buildAgentOutputDownloadSteps(agentArtifactPrefix)...) | ||
|
|
||
| // Conditionally checkout the target repository so the detection engine can | ||
| // analyze patches in the context of the surrounding codebase. | ||
| steps = append(steps, c.buildWorkspaceCheckoutForDetectionStep(data)...) | ||
|
|
There was a problem hiding this comment.
In dev mode, generateCheckoutActionsFolder() checks out github/gh-aw into the workspace so the local ./actions/setup can run. This new workspace checkout step will then overwrite the workspace with the target repo when has_patch is true, which can cause the runner to fail the Setup Scripts post-step (it won’t be able to find actions/setup/action.yml). Add a final "Restore actions folder" step (similar to compiler_yaml_main_job.go and compiler_safe_outputs_job.go) gated with always() so the post-step can complete after a root-level checkout.
Summary
Adds a conditional
actions/checkoutstep to the detection job that runs only when the agent produces a patch (has_patch == 'true'). This gives the threat detection engine access to the full repository codebase, enabling it to analyze code changes in context rather than in isolation.Problem
The detection job runs on a fresh runner with no repository checkout. When a patch is present, the detection engine sees code changes entirely without context — it cannot see surrounding source files, existing patterns, dependency manifests, or project structure. This makes it difficult to distinguish legitimate changes from suspicious ones.
Solution
Conditional workspace checkout
buildWorkspaceCheckoutForDetectionStep()method emits a checkout step withif: needs.agent.outputs.has_patch == 'true'actions/checkoutwithpersist-credentials: false(security requirement)Always grant
contents: readpermissioncontents: readis a minimal read-only permission — no security concernUpdated threat detection prompt
$GITHUB_WORKSPACEwhen a patch is presentactions/setup/md/andpkg/workflow/prompts/)Tests
4 new unit tests:
TestWorkspaceCheckoutForDetectionStep— step present with correct condition, pin, and persist-credentialsTestDetectionJobAlwaysHasContentsRead— permissions include contents: read in production modeTestWorkspaceCheckoutPresentWithCustomSteps— step present even when engine disabled but custom steps existTestWorkspaceCheckoutStepOrdering— step appears after artifact download and before detection guardAll existing tests continue to pass. 179 workflows recompiled.
Files changed
pkg/workflow/threat_detection.gobuildWorkspaceCheckoutForDetectionStep(), updated permissions logicpkg/workflow/threat_detection_test.goactions/setup/md/threat_detection.mdpkg/workflow/prompts/threat_detection.md.lock.ymlfilesCloses #23191