From 6480967b124ada2f43b953a8656400bc979c70a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:03:51 +0000 Subject: [PATCH 1/5] Initial plan From 43c90f967eb1c1be165eb7ad15277dc600ee7149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:14:04 +0000 Subject: [PATCH 2/5] Initial exploration complete Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/technical-doc-writer.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 820d77b2004..7f4701e57f4 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -2271,7 +2271,7 @@ jobs: run: | set -o pipefail sudo -E awf --env-all --allow-domains '*.githubusercontent.com,api.enterprise.githubcopilot.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info \ - "npx -y @github/copilot@0.0.354 --add-dir /tmp/gh-aw/ --log-level all --disable-builtin-mcps --agent \"\${GITHUB_WORKSPACE}/.github/agents/technical-doc-writer.md\" --allow-tool github --allow-tool safeoutputs --allow-tool shell --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt \"\$(cat /tmp/gh-aw/aw-prompts/prompt.txt)\"" \ + "npx -y @github/copilot@0.0.354 --add-dir /tmp/gh-aw/ --log-level all --disable-builtin-mcps --agent technical-doc-writer --allow-tool github --allow-tool safeoutputs --allow-tool shell --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt \"\$(cat /tmp/gh-aw/aw-prompts/prompt.txt)\"" \ 2>&1 | tee /tmp/gh-aw/agent-stdio.log # Move preserved Copilot logs to expected location From 9284b84f2c3268d06ffb40b9f40cd19e623bbfb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:24:06 +0000 Subject: [PATCH 3/5] Add detection job skip condition based on safe outputs and patches Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/changeset.lock.yml | 6 +++ pkg/workflow/agentic_output_test.go | 14 +++++- pkg/workflow/compiler_jobs.go | 1 + pkg/workflow/js/collect_ndjson_output.cjs | 6 +++ pkg/workflow/threat_detection.go | 16 +++++- pkg/workflow/threat_detection_test.go | 60 +++++++++++++++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 207f3331a36..4fdba0bac0e 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -741,6 +741,7 @@ jobs: env: GH_AW_SAFE_OUTPUTS: /tmp/gh-aw/safeoutputs/outputs.jsonl outputs: + has_patch: ${{ steps.collect_output.outputs.has_patch }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} steps: @@ -3180,6 +3181,10 @@ jobs: const outputTypes = Array.from(new Set(parsedItems.map(item => item.type))); core.info(`output_types: ${outputTypes.join(", ")}`); core.setOutput("output_types", outputTypes.join(",")); + 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(); - name: Upload sanitized agent output @@ -4858,6 +4863,7 @@ jobs: detection: needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' runs-on: ubuntu-latest permissions: {} timeout-minutes: 10 diff --git a/pkg/workflow/agentic_output_test.go b/pkg/workflow/agentic_output_test.go index 3be753e5c96..f0cd4642d63 100644 --- a/pkg/workflow/agentic_output_test.go +++ b/pkg/workflow/agentic_output_test.go @@ -79,9 +79,14 @@ 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 }}") { @@ -187,9 +192,14 @@ 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) diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go index f4ec903b61d..2c74d34dabf 100644 --- a/pkg/workflow/compiler_jobs.go +++ b/pkg/workflow/compiler_jobs.go @@ -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 }}", } } diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index 302ed192d21..0a096687fde 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -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(); diff --git a/pkg/workflow/threat_detection.go b/pkg/workflow/threat_detection.go index 30121859982..2accbf97e4e 100644 --- a/pkg/workflow/threat_detection.go +++ b/pkg/workflow/threat_detection.go @@ -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, " "), diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index 53b60964e7b..2a6ae39e05f 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -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'") + } +} From 23377421355c2d779ab7b2102dc2cd1a9e4c40ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:25:34 +0000 Subject: [PATCH 4/5] Format code --- pkg/workflow/agentic_output_test.go | 4 ++-- pkg/workflow/js/collect_ndjson_output.cjs | 2 +- pkg/workflow/threat_detection_test.go | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/workflow/agentic_output_test.go b/pkg/workflow/agentic_output_test.go index f0cd4642d63..ac869e29cff 100644 --- a/pkg/workflow/agentic_output_test.go +++ b/pkg/workflow/agentic_output_test.go @@ -82,7 +82,7 @@ This workflow tests the agentic output collection functionality. 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'") @@ -195,7 +195,7 @@ This workflow tests that Codex engine gets GH_AW_SAFE_OUTPUTS but not engine out 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'") diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index 0a096687fde..d6b5e222d72 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -705,7 +705,7 @@ 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); diff --git a/pkg/workflow/threat_detection_test.go b/pkg/workflow/threat_detection_test.go index 2a6ae39e05f..5250ca26fd8 100644 --- a/pkg/workflow/threat_detection_test.go +++ b/pkg/workflow/threat_detection_test.go @@ -851,56 +851,56 @@ func TestThreatDetectionEngineFalse(t *testing.T) { // 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'") From 8272951f82ac11e16ad7ec58aeb268a1e42a0945 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 06:26:47 +0000 Subject: [PATCH 5/5] Format code and verify implementation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.lock.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 0b1901393f5..43a2bbbc15f 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -146,6 +146,7 @@ jobs: env: GH_AW_SAFE_OUTPUTS: /tmp/gh-aw/safeoutputs/outputs.jsonl outputs: + has_patch: ${{ steps.collect_output.outputs.has_patch }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} steps: @@ -2409,6 +2410,10 @@ jobs: const outputTypes = Array.from(new Set(parsedItems.map(item => item.type))); core.info(`output_types: ${outputTypes.join(", ")}`); core.setOutput("output_types", outputTypes.join(",")); + 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(); - name: Upload sanitized agent output @@ -3720,6 +3725,7 @@ jobs: detection: needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' runs-on: ubuntu-latest permissions: {} concurrency: