Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/changeset.lock.yml

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

6 changes: 6 additions & 0 deletions .github/workflows/dev.lock.yml

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

2 changes: 1 addition & 1 deletion .github/workflows/technical-doc-writer.lock.yml

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

14 changes: 12 additions & 2 deletions pkg/workflow/agentic_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,15 @@ This workflow tests the agentic output collection functionality.
}

// Verify job output declaration for GH_AW_SAFE_OUTPUTS
if !strings.Contains(lockContent, "outputs:\n output: ${{ steps.collect_output.outputs.output }}") {
if !strings.Contains(lockContent, "output: ${{ steps.collect_output.outputs.output }}") {
t.Error("Expected job output declaration for 'output'")
}

// Verify has_patch output is declared
if !strings.Contains(lockContent, "has_patch: ${{ steps.collect_output.outputs.has_patch }}") {
t.Error("Expected job output declaration for 'has_patch'")
}

// Verify GH_AW_SAFE_OUTPUTS is passed to Claude
if !strings.Contains(lockContent, "GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}") {
t.Error("Expected GH_AW_SAFE_OUTPUTS environment variable to be passed to engine")
Expand Down Expand Up @@ -187,10 +192,15 @@ This workflow tests that Codex engine gets GH_AW_SAFE_OUTPUTS but not engine out
}

// Verify that job outputs section includes output for GH_AW_SAFE_OUTPUTS
if !strings.Contains(lockContent, "outputs:\n output: ${{ steps.collect_output.outputs.output }}") {
if !strings.Contains(lockContent, "output: ${{ steps.collect_output.outputs.output }}") {
t.Error("Codex workflow should have job output declaration for 'output' (GH_AW_SAFE_OUTPUTS)")
}

// Verify has_patch output is declared
if !strings.Contains(lockContent, "has_patch: ${{ steps.collect_output.outputs.has_patch }}") {
t.Error("Codex workflow should have job output declaration for 'has_patch'")
}

// Verify that Codex workflow DOES have engine output collection steps
// (because GetDeclaredOutputFiles returns a non-empty list)
if !strings.Contains(lockContent, "- name: Upload engine output files") {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) (
outputs = map[string]string{
"output": "${{ steps.collect_output.outputs.output }}",
"output_types": "${{ steps.collect_output.outputs.output_types }}",
"has_patch": "${{ steps.collect_output.outputs.has_patch }}",
}
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/workflow/js/collect_ndjson_output.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -705,5 +705,11 @@ async function main() {
const outputTypes = Array.from(new Set(parsedItems.map(item => item.type)));
core.info(`output_types: ${outputTypes.join(", ")}`);
core.setOutput("output_types", outputTypes.join(","));

// Check if patch file exists for detection job conditional
const patchPath = "/tmp/gh-aw/aw.patch";
const hasPatch = fs.existsSync(patchPath);
core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`);
core.setOutput("has_patch", hasPatch ? "true" : "false");
}
await main();
16 changes: 15 additions & 1 deletion pkg/workflow/threat_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,23 @@ func (c *Compiler) buildThreatDetectionJob(data *WorkflowData, mainJobName strin
// Generate agent concurrency configuration (same as main agent job)
agentConcurrency := GenerateJobConcurrencyConfig(data)

// Build conditional: detection should run when there are safe outputs OR when there's a patch
// output_types != '' OR has_patch == 'true'
hasOutputTypes := BuildComparison(
BuildPropertyAccess(fmt.Sprintf("needs.%s.outputs.output_types", mainJobName)),
"!=",
BuildStringLiteral(""),
)
hasPatch := BuildComparison(
BuildPropertyAccess(fmt.Sprintf("needs.%s.outputs.has_patch", mainJobName)),
"==",
BuildStringLiteral("true"),
)
condition := BuildDisjunction(false, hasOutputTypes, hasPatch)

job := &Job{
Name: constants.DetectionJobName,
If: "",
If: condition.Render(),
RunsOn: "runs-on: ubuntu-latest",
Permissions: NewPermissionsEmpty().RenderToYAML(),
Concurrency: c.indentYAMLLines(agentConcurrency, " "),
Expand Down
60 changes: 60 additions & 0 deletions pkg/workflow/threat_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,3 +846,63 @@ func TestThreatDetectionEngineFalse(t *testing.T) {
t.Fatalf("Expected 1 custom step, got %d", len(config.ThreatDetection.Steps))
}
}

// TestDetectionJobSkipCondition verifies that the detection job has the correct
// conditional logic to skip when there are no safe outputs and no patches
func TestDetectionJobSkipCondition(t *testing.T) {
compiler := NewCompiler(false, "", "test")

frontmatter := map[string]any{
"on": "issues",
"safe-outputs": map[string]any{
"create-issue": map[string]any{},
},
}

// Extract safe outputs configuration
config := compiler.extractSafeOutputsConfig(frontmatter)
if config == nil {
t.Fatal("Expected safe outputs config to be created")
}

// Create workflow data with threat detection enabled
data := &WorkflowData{
SafeOutputs: config,
}

// Build the threat detection job
job, err := compiler.buildThreatDetectionJob(data, constants.AgentJobName)
if err != nil {
t.Fatalf("Failed to build detection job: %v", err)
}

// Verify the job has a conditional
if job.If == "" {
t.Error("Expected detection job to have an 'if' condition")
}

// Verify the condition checks for output_types
if !strings.Contains(job.If, "needs.agent.outputs.output_types") {
t.Error("Expected detection job condition to check output_types")
}

// Verify the condition checks for has_patch
if !strings.Contains(job.If, "needs.agent.outputs.has_patch") {
t.Error("Expected detection job condition to check has_patch")
}

// Verify the condition uses OR logic (||)
if !strings.Contains(job.If, "||") {
t.Error("Expected detection job condition to use OR logic (||)")
}

// Verify the condition checks output_types is not empty
if !strings.Contains(job.If, "!= ''") {
t.Error("Expected detection job condition to check output_types is not empty")
}

// Verify the condition checks has_patch equals 'true'
if !strings.Contains(job.If, "== 'true'") {
t.Error("Expected detection job condition to check has_patch equals 'true'")
}
}
Loading