From ae53fa1f956cdeff6deebac80f8a992d9a074e16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:11:41 +0000 Subject: [PATCH 01/10] Initial plan From af421228d8fd9c29297af986d00ff3936b8b8079 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:24:39 +0000 Subject: [PATCH 02/10] fix: add git checkout and ref/sha inputs to SARIF upload step The github/codeql-action/upload-sarif action requires the local git HEAD to match the commit being scanned. When the agent job checks out a different branch (e.g., when creating a PR), the upload fails with "commit not found". Two fixes are applied to buildUploadCodeScanningSARIFStep: 1. Add a "Reset git HEAD to triggering commit for SARIF upload" step that runs `git checkout ${{ github.sha }}` before the upload, ensuring the local HEAD matches the triggering commit. 2. Add `ref: ${{ github.ref }}` and `sha: ${{ github.sha }}` inputs to the upload-sarif action, explicitly associating the SARIF results with the correct triggering commit/ref. Both the reset step and the upload step are conditional on `steps.process_safe_outputs.outputs.sarif_file != ''`. Tests are updated to verify the new step and inputs are present. Fixes # Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d5cc43c1-ee6d-4fb2-a8f0-5bdd1b6840a3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../compiler_safe_outputs_job_test.go | 36 ++++++++++++++++--- pkg/workflow/create_code_scanning_alert.go | 23 ++++++++++-- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index a404cfd89b8..512bae43370 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -841,26 +841,52 @@ func TestCreateCodeScanningAlertIncludesSARIFUploadStep(t *testing.T) { "Upload step should reference sarif_file output") assert.Contains(t, stepsContent, "wait-for-processing: true", "Upload step should wait for processing") + // Verify ref and sha inputs are present to associate SARIF with triggering commit + assert.Contains(t, stepsContent, "ref: ${{ github.ref }}", + "Upload step should include ref input to associate SARIF with triggering commit") + assert.Contains(t, stepsContent, "sha: ${{ github.sha }}", + "Upload step should include sha input to associate SARIF with triggering commit") + // Verify the git checkout step is present before the upload step + assert.Contains(t, stepsContent, "Reset git HEAD to triggering commit for SARIF upload", + "A git checkout step must reset HEAD to github.sha before SARIF upload") + assert.Contains(t, stepsContent, "git checkout ${{ github.sha }}", + "The git checkout step must run git checkout with the triggering SHA") + // The reset step must also be conditional on sarif_file being set + resetStepStart := strings.Index(stepsContent, "Reset git HEAD to triggering commit for SARIF upload") + require.Greater(t, resetStepStart, -1, "Reset git HEAD step must exist") + resetStepSection := stepsContent[resetStepStart:] + nextStepIdx := strings.Index(resetStepSection[len(" - name:"):], " - name:") + if nextStepIdx > -1 { + resetStepSection = resetStepSection[:nextStepIdx+len(" - name:")] + } + assert.Contains(t, resetStepSection, "steps.process_safe_outputs.outputs.sarif_file != ''", + "Reset git HEAD step should only run when sarif_file output is set") // github/codeql-action/upload-sarif uses 'token' not 'github-token' // Extract the upload-sarif step section to check it specifically uploadStepStart := strings.Index(stepsContent, "- name: Upload SARIF to GitHub Code Scanning") require.Greater(t, uploadStepStart, -1, "Upload SARIF step must exist in steps content") uploadStepSection := stepsContent[uploadStepStart:] // Find the end of this step (next step starts with " - name:") - nextStepIdx := strings.Index(uploadStepSection[len(" - name:"):], " - name:") - if nextStepIdx > -1 { - uploadStepSection = uploadStepSection[:nextStepIdx+len(" - name:")] + nextUploadStepIdx := strings.Index(uploadStepSection[len(" - name:"):], " - name:") + if nextUploadStepIdx > -1 { + uploadStepSection = uploadStepSection[:nextUploadStepIdx+len(" - name:")] } assert.Contains(t, uploadStepSection, "token:", "Upload step should use 'token' input (not 'github-token')") assert.NotContains(t, uploadStepSection, "github-token:", "Upload step must not use 'github-token' - upload-sarif only accepts 'token'") + // Verify the reset step appears before the upload step + resetStepPos := strings.Index(stepsContent, "Reset git HEAD to triggering commit for SARIF upload") + uploadSARIFPos := strings.Index(stepsContent, "id: upload_code_scanning_sarif") + require.Greater(t, resetStepPos, -1, "Reset git HEAD step must exist") + require.Greater(t, uploadSARIFPos, -1, "upload_code_scanning_sarif step must exist") + assert.Less(t, resetStepPos, uploadSARIFPos, + "Reset git HEAD step must appear before upload_code_scanning_sarif step") + // Verify the upload step appears after the process_safe_outputs step processSafeOutputsPos := strings.Index(stepsContent, "id: process_safe_outputs") - uploadSARIFPos := strings.Index(stepsContent, "id: upload_code_scanning_sarif") require.Greater(t, processSafeOutputsPos, -1, "process_safe_outputs step must exist") - require.Greater(t, uploadSARIFPos, -1, "upload_code_scanning_sarif step must exist") assert.Greater(t, uploadSARIFPos, processSafeOutputsPos, "upload_code_scanning_sarif must appear after process_safe_outputs in compiled steps") diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index e46cd3d24a1..0aebf09ac3e 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -61,17 +61,30 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea return securityReportsConfig } -// buildUploadCodeScanningSARIFStep builds a step to upload the SARIF file generated by the +// buildUploadCodeScanningSARIFStep builds steps to upload the SARIF file generated by the // create_code_scanning_alert handler to the GitHub Code Scanning API. // // The create_code_scanning_alert.cjs handler writes findings to a SARIF file and sets -// the sarif_file step output on the process_safe_outputs step. This step runs after -// process_safe_outputs and uploads the SARIF file only when findings were generated +// the sarif_file step output on the process_safe_outputs step. These steps run after +// process_safe_outputs and upload the SARIF file only when findings were generated // (i.e., when the sarif_file output is non-empty). +// +// A git checkout step is included before the upload to reset the local HEAD to the +// triggering commit. This is necessary because the agent job may have checked out a +// different branch (e.g., when creating a PR), and github/codeql-action/upload-sarif +// requires the local HEAD to match the commit being scanned. Without this reset, +// the upload fails with "commit not found". func (c *Compiler) buildUploadCodeScanningSARIFStep(data *WorkflowData) []string { createCodeScanningAlertLog.Print("Building SARIF upload step for code scanning alerts") var steps []string + // Reset git HEAD to the triggering commit so github/codeql-action/upload-sarif can + // resolve the commit reference. The agent may have checked out a different branch + // during its work, causing "commit not found" errors during SARIF upload. + steps = append(steps, " - name: Reset git HEAD to triggering commit for SARIF upload\n") + steps = append(steps, " if: steps.process_safe_outputs.outputs.sarif_file != ''\n") + steps = append(steps, " run: git checkout ${{ github.sha }}\n") + steps = append(steps, " - name: Upload SARIF to GitHub Code Scanning\n") steps = append(steps, " id: upload_code_scanning_sarif\n") // Only run when findings were generated (sarif_file output is set by the handler) @@ -81,6 +94,10 @@ func (c *Compiler) buildUploadCodeScanningSARIFStep(data *WorkflowData) []string // NOTE: github/codeql-action/upload-sarif uses 'token' as the input name, not 'github-token' c.addUploadSARIFToken(&steps, data, data.SafeOutputs.CreateCodeScanningAlerts.GitHubToken) steps = append(steps, " sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }}\n") + // ref and sha ensure the upload is associated with the correct triggering commit, + // even when the local HEAD has been reset or the runner checked out a different ref. + steps = append(steps, " ref: ${{ github.ref }}\n") + steps = append(steps, " sha: ${{ github.sha }}\n") steps = append(steps, " wait-for-processing: true\n") return steps From b4df5c85b40bd5e8dd1cd6c4c9f15e966641bb5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:45:10 +0000 Subject: [PATCH 03/10] fix: use actions/checkout with checkout manager token for SARIF restore Instead of a raw `git checkout ${{ github.sha }}` command, use a proper `actions/checkout` step that leverages the checkout manager's information (user-configured checkout token from frontmatter) to restore the workspace to the triggering commit before SARIF upload. The new `resolveRestoreCheckoutToken` method consults the checkout manager for the user's configured checkout token (checkout: github-token: ...) and falls back to the PR checkout token chain if no override is present. This ensures credentials are handled consistently with the rest of the safe_outputs job and works correctly even without a prior PR checkout. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5ebfad75-31de-4db1-bb25-505c7ba62c67 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-malicious-code-scan.lock.yml | 5 ++ .github/workflows/daily-semgrep-scan.lock.yml | 5 ++ .../compiler_safe_outputs_job_test.go | 44 +++++++----- pkg/workflow/create_code_scanning_alert.go | 68 ++++++++++++++++--- 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 22149e6ccd3..80a0aea3a79 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -928,6 +928,9 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Reset git HEAD to triggering commit for SARIF upload + if: steps.process_safe_outputs.outputs.sarif_file != '' + run: git checkout ${{ github.sha }} - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif if: steps.process_safe_outputs.outputs.sarif_file != '' @@ -935,6 +938,8 @@ jobs: with: token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} + ref: ${{ github.ref }} + sha: ${{ github.sha }} wait-for-processing: true - name: Upload Safe Output Items if: always() diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index b1d66967591..7b37d59b857 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -1105,6 +1105,9 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Reset git HEAD to triggering commit for SARIF upload + if: steps.process_safe_outputs.outputs.sarif_file != '' + run: git checkout ${{ github.sha }} - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif if: steps.process_safe_outputs.outputs.sarif_file != '' @@ -1112,6 +1115,8 @@ jobs: with: token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} + ref: ${{ github.ref }} + sha: ${{ github.sha }} wait-for-processing: true - name: Upload Safe Output Items if: always() diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index 512bae43370..3e9bc84a036 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -846,21 +846,29 @@ func TestCreateCodeScanningAlertIncludesSARIFUploadStep(t *testing.T) { "Upload step should include ref input to associate SARIF with triggering commit") assert.Contains(t, stepsContent, "sha: ${{ github.sha }}", "Upload step should include sha input to associate SARIF with triggering commit") - // Verify the git checkout step is present before the upload step - assert.Contains(t, stepsContent, "Reset git HEAD to triggering commit for SARIF upload", - "A git checkout step must reset HEAD to github.sha before SARIF upload") - assert.Contains(t, stepsContent, "git checkout ${{ github.sha }}", - "The git checkout step must run git checkout with the triggering SHA") - // The reset step must also be conditional on sarif_file being set - resetStepStart := strings.Index(stepsContent, "Reset git HEAD to triggering commit for SARIF upload") - require.Greater(t, resetStepStart, -1, "Reset git HEAD step must exist") - resetStepSection := stepsContent[resetStepStart:] - nextStepIdx := strings.Index(resetStepSection[len(" - name:"):], " - name:") + // Verify the restore checkout step is present before the upload step. + // It uses actions/checkout (not a raw git command) with the checkout manager's + // token information to properly restore the workspace to the triggering commit. + assert.Contains(t, stepsContent, "Restore checkout to triggering commit for SARIF upload", + "A restore checkout step must reset workspace to github.sha before SARIF upload") + assert.Contains(t, stepsContent, "ref: ${{ github.sha }}", + "The restore checkout step must check out the triggering SHA") + assert.NotContains(t, stepsContent, "git checkout ${{ github.sha }}", + "Must use actions/checkout (not raw git command) to restore the checkout") + // The restore step must also be conditional on sarif_file being set + restoreStepStart := strings.Index(stepsContent, "Restore checkout to triggering commit for SARIF upload") + require.Greater(t, restoreStepStart, -1, "Restore checkout step must exist") + restoreStepSection := stepsContent[restoreStepStart:] + nextStepIdx := strings.Index(restoreStepSection[len(" - name:"):], " - name:") if nextStepIdx > -1 { - resetStepSection = resetStepSection[:nextStepIdx+len(" - name:")] + restoreStepSection = restoreStepSection[:nextStepIdx+len(" - name:")] } - assert.Contains(t, resetStepSection, "steps.process_safe_outputs.outputs.sarif_file != ''", - "Reset git HEAD step should only run when sarif_file output is set") + assert.Contains(t, restoreStepSection, "steps.process_safe_outputs.outputs.sarif_file != ''", + "Restore checkout step should only run when sarif_file output is set") + assert.Contains(t, restoreStepSection, "actions/checkout", + "Restore checkout step must use actions/checkout") + assert.Contains(t, restoreStepSection, "persist-credentials: false", + "Restore checkout step must disable credential persistence") // github/codeql-action/upload-sarif uses 'token' not 'github-token' // Extract the upload-sarif step section to check it specifically uploadStepStart := strings.Index(stepsContent, "- name: Upload SARIF to GitHub Code Scanning") @@ -876,13 +884,13 @@ func TestCreateCodeScanningAlertIncludesSARIFUploadStep(t *testing.T) { assert.NotContains(t, uploadStepSection, "github-token:", "Upload step must not use 'github-token' - upload-sarif only accepts 'token'") - // Verify the reset step appears before the upload step - resetStepPos := strings.Index(stepsContent, "Reset git HEAD to triggering commit for SARIF upload") + // Verify the restore step appears before the upload step + restoreStepPos := strings.Index(stepsContent, "Restore checkout to triggering commit for SARIF upload") uploadSARIFPos := strings.Index(stepsContent, "id: upload_code_scanning_sarif") - require.Greater(t, resetStepPos, -1, "Reset git HEAD step must exist") + require.Greater(t, restoreStepPos, -1, "Restore checkout step must exist") require.Greater(t, uploadSARIFPos, -1, "upload_code_scanning_sarif step must exist") - assert.Less(t, resetStepPos, uploadSARIFPos, - "Reset git HEAD step must appear before upload_code_scanning_sarif step") + assert.Less(t, restoreStepPos, uploadSARIFPos, + "Restore checkout step must appear before upload_code_scanning_sarif step") // Verify the upload step appears after the process_safe_outputs step processSafeOutputsPos := strings.Index(stepsContent, "id: process_safe_outputs") diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index 0aebf09ac3e..bcc5f387a00 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -69,21 +69,41 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea // process_safe_outputs and upload the SARIF file only when findings were generated // (i.e., when the sarif_file output is non-empty). // -// A git checkout step is included before the upload to reset the local HEAD to the -// triggering commit. This is necessary because the agent job may have checked out a -// different branch (e.g., when creating a PR), and github/codeql-action/upload-sarif -// requires the local HEAD to match the commit being scanned. Without this reset, -// the upload fails with "commit not found". +// A restore checkout step is included before the upload to reset the workspace to the +// triggering commit (github.sha). This is necessary because the safe_outputs job may +// have checked out a different branch for PR operations (e.g., the base branch), and +// github/codeql-action/upload-sarif requires the HEAD to match the commit being scanned. +// Without this restore, the upload fails with "commit not found". +// +// The restore step uses actions/checkout with the same token and repository settings +// from the checkout manager (user-configured checkout frontmatter), falling back to +// the PR checkout token when no override is present. func (c *Compiler) buildUploadCodeScanningSARIFStep(data *WorkflowData) []string { createCodeScanningAlertLog.Print("Building SARIF upload step for code scanning alerts") var steps []string - // Reset git HEAD to the triggering commit so github/codeql-action/upload-sarif can - // resolve the commit reference. The agent may have checked out a different branch - // during its work, causing "commit not found" errors during SARIF upload. - steps = append(steps, " - name: Reset git HEAD to triggering commit for SARIF upload\n") + // Restore the workspace to the triggering commit using actions/checkout. + // The checkout manager provides the correct token and repository settings from the + // user's checkout frontmatter configuration. The safe_outputs job may have checked + // out a different branch (e.g., when creating a PR), causing "commit not found" errors. + // + // Token precedence (via checkout manager then PR checkout fallback): + // 1. User-configured checkout.github-token (from frontmatter checkout: config) + // 2. GitHub App token (safe-outputs-app-token, if a GitHub App is configured) + // 3. PR checkout token (create-pull-request/push-to-pull-request-branch token, or safe-outputs token) + // 4. Default fallback: GH_AW_GITHUB_TOKEN || GITHUB_TOKEN + checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) + restoreToken := c.resolveRestoreCheckoutToken(checkoutMgr, data) + createCodeScanningAlertLog.Printf("Using restore checkout token for SARIF upload (derived from checkout manager)") + + steps = append(steps, " - name: Restore checkout to triggering commit for SARIF upload\n") steps = append(steps, " if: steps.process_safe_outputs.outputs.sarif_file != ''\n") - steps = append(steps, " run: git checkout ${{ github.sha }}\n") + steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/checkout"))) + steps = append(steps, " with:\n") + steps = append(steps, " ref: ${{ github.sha }}\n") + steps = append(steps, fmt.Sprintf(" token: %s\n", restoreToken)) + steps = append(steps, " persist-credentials: false\n") + steps = append(steps, " fetch-depth: 1\n") steps = append(steps, " - name: Upload SARIF to GitHub Code Scanning\n") steps = append(steps, " id: upload_code_scanning_sarif\n") @@ -135,3 +155,31 @@ func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, conf createCodeScanningAlertLog.Printf("Using token for SARIF upload from source: %s (upload-sarif uses 'token' not 'github-token')", tokenSource) *steps = append(*steps, fmt.Sprintf(" token: %s\n", effectiveToken)) } + +// resolveRestoreCheckoutToken returns the GitHub token to use for the pre-SARIF +// restore checkout step. It consults the checkout manager first (user-configured +// checkout frontmatter), then falls back to the PR checkout token chain. +// +// Token precedence: +// 1. GitHub App token minted for safe-outputs (if safe-outputs app is configured) +// 2. User-configured checkout.github-token from the default workspace checkout +// 3. PR checkout token (create-pull-request / push-to-pull-request-branch / safe-outputs token) +// 4. Default: GH_AW_GITHUB_TOKEN || GITHUB_TOKEN +func (c *Compiler) resolveRestoreCheckoutToken(checkoutMgr *CheckoutManager, data *WorkflowData) string { + // GitHub App for safe-outputs takes highest precedence + if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil { + //nolint:gosec // G101: False positive - this is a GitHub Actions expression template placeholder, not a hardcoded credential + return "${{ steps.safe-outputs-app-token.outputs.token }}" + } + + // User-configured checkout token from frontmatter (checkout: github-token: ...) + override := checkoutMgr.GetDefaultCheckoutOverride() + if override != nil && override.token != "" { + createCodeScanningAlertLog.Printf("Using checkout manager override token for SARIF restore checkout") + return getEffectiveSafeOutputGitHubToken(override.token) + } + + // Fall back to PR checkout token (which itself falls back to safe-outputs token or GITHUB_TOKEN) + prToken, _ := computeEffectivePRCheckoutToken(data.SafeOutputs) + return prToken +} From a78b257814350d497bddc91735e924ee2b51d5e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:01:51 +0000 Subject: [PATCH 04/10] chore: initial plan for code scanning job separation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5d63b40a-5b47-473d-80ed-f5d7ee0de07d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-malicious-code-scan.lock.yml | 9 +++++++-- .github/workflows/daily-semgrep-scan.lock.yml | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 80a0aea3a79..cf619ae0731 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -928,9 +928,14 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Reset git HEAD to triggering commit for SARIF upload + - name: Restore checkout to triggering commit for SARIF upload if: steps.process_safe_outputs.outputs.sarif_file != '' - run: git checkout ${{ github.sha }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif if: steps.process_safe_outputs.outputs.sarif_file != '' diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index 7b37d59b857..1ce86b6ecef 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -1105,9 +1105,14 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Reset git HEAD to triggering commit for SARIF upload + - name: Restore checkout to triggering commit for SARIF upload if: steps.process_safe_outputs.outputs.sarif_file != '' - run: git checkout ${{ github.sha }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif if: steps.process_safe_outputs.outputs.sarif_file != '' From c56d31f5ba3ddafc0ea81f4a43749234af17b3b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:13:09 +0000 Subject: [PATCH 05/10] feat: move SARIF upload into dedicated upload_code_scanning_sarif job The SARIF restore checkout and upload steps are moved out of the consolidated safe_outputs job into a dedicated upload_code_scanning_sarif job that: - needs: [safe_outputs] - if: needs.safe_outputs.outputs.sarif_file != '' (only runs when there is work) - permissions: contents:read, security-events:write This avoids the repo checkout (needed to restore HEAD to github.sha) from interfering with other safe-output operations (create-pull-request, push-to-pull-request-branch, etc.) that also modify the workspace checkout in the consolidated safe_outputs job. The safe_outputs job still exports sarif_file so the new job can gate on it. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5d63b40a-5b47-473d-80ed-f5d7ee0de07d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-malicious-code-scan.lock.yml | 31 ++- .github/workflows/daily-semgrep-scan.lock.yml | 31 ++- pkg/constants/job_constants.go | 1 + pkg/workflow/compiler_safe_output_jobs.go | 18 ++ pkg/workflow/compiler_safe_outputs_job.go | 11 +- .../compiler_safe_outputs_job_test.go | 195 +++++++++--------- pkg/workflow/create_code_scanning_alert.go | 101 ++++----- 7 files changed, 208 insertions(+), 180 deletions(-) diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index cf619ae0731..2883de79834 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -756,6 +756,7 @@ jobs: - activation - agent - safe_outputs + - upload_code_scanning_sarif if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') runs-on: ubuntu-slim permissions: @@ -928,8 +929,24 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Restore checkout to triggering commit for SARIF upload - if: steps.process_safe_outputs.outputs.sarif_file != '' + - name: Upload Safe Output Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + + upload_code_scanning_sarif: + needs: safe_outputs + if: needs.safe_outputs.outputs.sarif_file != '' + runs-on: ubuntu-slim + permissions: + contents: read + security-events: write + timeout-minutes: 10 + steps: + - name: Restore checkout to triggering commit uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} @@ -938,19 +955,11 @@ jobs: fetch-depth: 1 - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif - if: steps.process_safe_outputs.outputs.sarif_file != '' uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} + sarif_file: ${{ needs.safe_outputs.outputs.sarif_file }} ref: ${{ github.ref }} sha: ${{ github.sha }} wait-for-processing: true - - name: Upload Safe Output Items - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: safe-output-items - path: /tmp/gh-aw/safe-output-items.jsonl - if-no-files-found: ignore diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index 1ce86b6ecef..b77316f475a 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -784,6 +784,7 @@ jobs: - agent - detection - safe_outputs + - upload_code_scanning_sarif if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') runs-on: ubuntu-slim permissions: @@ -1105,8 +1106,24 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Restore checkout to triggering commit for SARIF upload - if: steps.process_safe_outputs.outputs.sarif_file != '' + - name: Upload Safe Output Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + + upload_code_scanning_sarif: + needs: safe_outputs + if: needs.safe_outputs.outputs.sarif_file != '' + runs-on: ubuntu-slim + permissions: + contents: read + security-events: write + timeout-minutes: 10 + steps: + - name: Restore checkout to triggering commit uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} @@ -1115,19 +1132,11 @@ jobs: fetch-depth: 1 - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif - if: steps.process_safe_outputs.outputs.sarif_file != '' uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} + sarif_file: ${{ needs.safe_outputs.outputs.sarif_file }} ref: ${{ github.ref }} sha: ${{ github.sha }} wait-for-processing: true - - name: Upload Safe Output Items - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: safe-output-items - path: /tmp/gh-aw/safe-output-items.jsonl - if-no-files-found: ignore diff --git a/pkg/constants/job_constants.go b/pkg/constants/job_constants.go index fb69c08d196..359a80c07c1 100644 --- a/pkg/constants/job_constants.go +++ b/pkg/constants/job_constants.go @@ -63,6 +63,7 @@ const PreActivationJobName JobName = "pre_activation" const DetectionJobName JobName = "detection" const SafeOutputsJobName JobName = "safe_outputs" const UploadAssetsJobName JobName = "upload_assets" +const UploadCodeScanningJobName JobName = "upload_code_scanning_sarif" const ConclusionJobName JobName = "conclusion" const UnlockJobName JobName = "unlock" diff --git a/pkg/workflow/compiler_safe_output_jobs.go b/pkg/workflow/compiler_safe_output_jobs.go index 2ffcc0f0143..f1d6d83c436 100644 --- a/pkg/workflow/compiler_safe_output_jobs.go +++ b/pkg/workflow/compiler_safe_output_jobs.go @@ -89,6 +89,24 @@ func (c *Compiler) buildSafeOutputsJobs(data *WorkflowData, jobName, markdownPat compilerSafeOutputJobsLog.Printf("Added separate upload_assets job") } + // Build upload_code_scanning_sarif job as a separate job if create-code-scanning-alert is configured. + // This job runs after safe_outputs and only when the safe_outputs job exported a SARIF file. + // It is separate to avoid the checkout step (needed to restore HEAD to github.sha) from + // interfering with other safe-output operations in the consolidated safe_outputs job. + if data.SafeOutputs != nil && data.SafeOutputs.CreateCodeScanningAlerts != nil && + !isHandlerStaged(false || data.SafeOutputs.Staged, data.SafeOutputs.CreateCodeScanningAlerts.Staged) { + compilerSafeOutputJobsLog.Print("Building separate upload_code_scanning_sarif job") + codeScanningJob, err := c.buildCodeScanningUploadJob(data) + if err != nil { + return fmt.Errorf("failed to build upload_code_scanning_sarif job: %w", err) + } + if err := c.jobManager.AddJob(codeScanningJob); err != nil { + return fmt.Errorf("failed to add upload_code_scanning_sarif job: %w", err) + } + safeOutputJobNames = append(safeOutputJobNames, codeScanningJob.Name) + compilerSafeOutputJobsLog.Printf("Added separate upload_code_scanning_sarif job") + } + // Build conditional call-workflow fan-out jobs if configured. // Each allowed worker gets its own `uses:` job with an `if:` condition that // checks whether safe_outputs selected it. Only one runs per execution. diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index c4bff5baf38..a99b75a0583 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -207,14 +207,11 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa } } - // 2. SARIF upload step — uploads the SARIF file generated by create_code_scanning_alert handler. - // The handler writes findings to a SARIF file and exposes its path via the sarif_file step output. - // This step runs only when findings were generated (sarif_file output is non-empty). + // 2. SARIF output — expose sarif_file from the handler so the dedicated + // upload_code_scanning_sarif job (built in buildCodeScanningUploadJob) can access it + // via needs.safe_outputs.outputs.sarif_file and decide whether to run. if data.SafeOutputs.CreateCodeScanningAlerts != nil && !isHandlerStaged(c.trialMode || data.SafeOutputs.Staged, data.SafeOutputs.CreateCodeScanningAlerts.Staged) { - consolidatedSafeOutputsJobLog.Print("Adding SARIF upload step for code scanning alerts") - uploadSARIFSteps := c.buildUploadCodeScanningSARIFStep(data) - steps = append(steps, uploadSARIFSteps...) - safeOutputStepNames = append(safeOutputStepNames, "upload_code_scanning_sarif") + consolidatedSafeOutputsJobLog.Print("Exposing sarif_file output for upload_code_scanning_sarif job") outputs["sarif_file"] = "${{ steps.process_safe_outputs.outputs.sarif_file }}" } diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index 3e9bc84a036..2d57f5499c4 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -770,43 +770,43 @@ func TestCallWorkflowOnly_UsesHandlerManagerStep(t *testing.T) { assert.Contains(t, stepsContent, "call_workflow", "Handler config should reference call_workflow") } -// TestCreateCodeScanningAlertIncludesSARIFUploadStep verifies that when create-code-scanning-alert -// is configured, the compiled safe_outputs job includes a step to upload the generated SARIF file -// to GitHub Code Scanning using github/codeql-action/upload-sarif. -func TestCreateCodeScanningAlertIncludesSARIFUploadStep(t *testing.T) { +// TestCreateCodeScanningAlertUploadJob verifies that when create-code-scanning-alert is configured, +// a dedicated upload_code_scanning_sarif job is created (separate from safe_outputs) and that +// the safe_outputs job exports sarif_file so the upload job can check whether to run. +func TestCreateCodeScanningAlertUploadJob(t *testing.T) { tests := []struct { name string config *CreateCodeScanningAlertsConfig - expectUploadStep bool + expectUploadJob bool expectCustomToken string expectDefaultToken bool }{ { - name: "default config includes upload step with default token", + name: "default config creates separate upload job with default token", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{}, }, - expectUploadStep: true, + expectUploadJob: true, expectDefaultToken: true, }, { - name: "custom github-token is used in upload step", + name: "custom github-token is used in upload job", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{ GitHubToken: "${{ secrets.GHAS_TOKEN }}", }, }, - expectUploadStep: true, + expectUploadJob: true, expectCustomToken: "${{ secrets.GHAS_TOKEN }}", }, { - name: "staged mode does not include upload step", + name: "staged mode does not create upload job", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{ Staged: true, }, }, - expectUploadStep: false, + expectUploadJob: false, }, } @@ -822,105 +822,96 @@ func TestCreateCodeScanningAlertIncludesSARIFUploadStep(t *testing.T) { }, } - job, stepNames, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test-workflow.md") - require.NoError(t, err, "Should compile without error") - require.NotNil(t, job, "safe_outputs job should be generated") - - stepsContent := strings.Join(job.Steps, "") - - if tt.expectUploadStep { - assert.Contains(t, stepsContent, "Upload SARIF to GitHub Code Scanning", - "Compiled job should include SARIF upload step") - assert.Contains(t, stepsContent, "id: upload_code_scanning_sarif", - "Upload step should have correct ID") - assert.Contains(t, stepsContent, "upload-sarif", - "Upload step should use github/codeql-action/upload-sarif") - assert.Contains(t, stepsContent, "steps.process_safe_outputs.outputs.sarif_file != ''", - "Upload step should only run when sarif_file output is set") - assert.Contains(t, stepsContent, "sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }}", - "Upload step should reference sarif_file output") - assert.Contains(t, stepsContent, "wait-for-processing: true", - "Upload step should wait for processing") - // Verify ref and sha inputs are present to associate SARIF with triggering commit - assert.Contains(t, stepsContent, "ref: ${{ github.ref }}", - "Upload step should include ref input to associate SARIF with triggering commit") - assert.Contains(t, stepsContent, "sha: ${{ github.sha }}", - "Upload step should include sha input to associate SARIF with triggering commit") - // Verify the restore checkout step is present before the upload step. - // It uses actions/checkout (not a raw git command) with the checkout manager's - // token information to properly restore the workspace to the triggering commit. - assert.Contains(t, stepsContent, "Restore checkout to triggering commit for SARIF upload", - "A restore checkout step must reset workspace to github.sha before SARIF upload") - assert.Contains(t, stepsContent, "ref: ${{ github.sha }}", - "The restore checkout step must check out the triggering SHA") - assert.NotContains(t, stepsContent, "git checkout ${{ github.sha }}", - "Must use actions/checkout (not raw git command) to restore the checkout") - // The restore step must also be conditional on sarif_file being set - restoreStepStart := strings.Index(stepsContent, "Restore checkout to triggering commit for SARIF upload") - require.Greater(t, restoreStepStart, -1, "Restore checkout step must exist") - restoreStepSection := stepsContent[restoreStepStart:] - nextStepIdx := strings.Index(restoreStepSection[len(" - name:"):], " - name:") - if nextStepIdx > -1 { - restoreStepSection = restoreStepSection[:nextStepIdx+len(" - name:")] - } - assert.Contains(t, restoreStepSection, "steps.process_safe_outputs.outputs.sarif_file != ''", - "Restore checkout step should only run when sarif_file output is set") - assert.Contains(t, restoreStepSection, "actions/checkout", - "Restore checkout step must use actions/checkout") - assert.Contains(t, restoreStepSection, "persist-credentials: false", - "Restore checkout step must disable credential persistence") - // github/codeql-action/upload-sarif uses 'token' not 'github-token' - // Extract the upload-sarif step section to check it specifically - uploadStepStart := strings.Index(stepsContent, "- name: Upload SARIF to GitHub Code Scanning") - require.Greater(t, uploadStepStart, -1, "Upload SARIF step must exist in steps content") - uploadStepSection := stepsContent[uploadStepStart:] - // Find the end of this step (next step starts with " - name:") - nextUploadStepIdx := strings.Index(uploadStepSection[len(" - name:"):], " - name:") - if nextUploadStepIdx > -1 { - uploadStepSection = uploadStepSection[:nextUploadStepIdx+len(" - name:")] - } - assert.Contains(t, uploadStepSection, "token:", - "Upload step should use 'token' input (not 'github-token')") - assert.NotContains(t, uploadStepSection, "github-token:", + // 1. Verify safe_outputs job exports sarif_file (needed by the upload job's if: condition) + safeOutputsJob, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test-workflow.md") + require.NoError(t, err, "safe_outputs job should build without error") + require.NotNil(t, safeOutputsJob, "safe_outputs job should be generated") + + safeOutputsSteps := strings.Join(safeOutputsJob.Steps, "") + + if tt.expectUploadJob { + // safe_outputs must export sarif_file so the upload job can check if there is work to do + assert.Contains(t, safeOutputsJob.Outputs, "sarif_file", + "safe_outputs job must export sarif_file output") + assert.Contains(t, safeOutputsJob.Outputs["sarif_file"], "steps.process_safe_outputs.outputs.sarif_file", + "sarif_file output must reference process_safe_outputs step") + + // The upload and restore steps must NOT be in safe_outputs itself + assert.NotContains(t, safeOutputsSteps, "upload-sarif", + "SARIF upload must NOT be a step in safe_outputs job") + assert.NotContains(t, safeOutputsSteps, "Upload SARIF to GitHub Code Scanning", + "SARIF upload step must NOT appear in safe_outputs job") + + // 2. Verify the dedicated upload job is built correctly + uploadJob, buildErr := compiler.buildCodeScanningUploadJob(workflowData) + require.NoError(t, buildErr, "upload_code_scanning_sarif job should build without error") + require.NotNil(t, uploadJob, "upload_code_scanning_sarif job should be created") + + assert.Equal(t, string(constants.UploadCodeScanningJobName), uploadJob.Name, + "Upload job must be named upload_code_scanning_sarif") + assert.Contains(t, uploadJob.Needs, string(constants.SafeOutputsJobName), + "Upload job must depend on safe_outputs") + assert.Contains(t, uploadJob.If, "sarif_file != ''", + "Upload job must only run when sarif_file is non-empty") + assert.Contains(t, uploadJob.If, string(constants.SafeOutputsJobName), + "Upload job if-condition must reference safe_outputs outputs") + + uploadSteps := strings.Join(uploadJob.Steps, "") + + // Restore checkout step must be present in the upload job + assert.Contains(t, uploadSteps, "Restore checkout to triggering commit", + "Upload job must restore workspace to triggering commit") + assert.Contains(t, uploadSteps, "ref: ${{ github.sha }}", + "Restore checkout must check out github.sha") + assert.Contains(t, uploadSteps, "persist-credentials: false", + "Restore checkout must disable credential persistence") + assert.NotContains(t, uploadSteps, "git checkout ${{ github.sha }}", + "Must use actions/checkout, not a raw git command") + + // Upload SARIF step must be present + assert.Contains(t, uploadSteps, "Upload SARIF to GitHub Code Scanning", + "Upload job must have SARIF upload step") + assert.Contains(t, uploadSteps, "upload-sarif", + "Upload job must use github/codeql-action/upload-sarif") + assert.Contains(t, uploadSteps, "wait-for-processing: true", + "Upload step must wait for processing") + // ref and sha pin the upload to the triggering commit + assert.Contains(t, uploadSteps, "ref: ${{ github.ref }}", + "Upload step must include ref input") + assert.Contains(t, uploadSteps, "sha: ${{ github.sha }}", + "Upload step must include sha input") + // sarif_file must come from safe_outputs job outputs + assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.sarif_file", + "Upload step must reference sarif_file from safe_outputs job outputs") + // Upload-sarif uses 'token' not 'github-token' + assert.Contains(t, uploadSteps, "token:", + "Upload step must use 'token' input (not 'github-token')") + assert.NotContains(t, uploadSteps, "github-token:", "Upload step must not use 'github-token' - upload-sarif only accepts 'token'") - // Verify the restore step appears before the upload step - restoreStepPos := strings.Index(stepsContent, "Restore checkout to triggering commit for SARIF upload") - uploadSARIFPos := strings.Index(stepsContent, "id: upload_code_scanning_sarif") - require.Greater(t, restoreStepPos, -1, "Restore checkout step must exist") - require.Greater(t, uploadSARIFPos, -1, "upload_code_scanning_sarif step must exist") - assert.Less(t, restoreStepPos, uploadSARIFPos, - "Restore checkout step must appear before upload_code_scanning_sarif step") - - // Verify the upload step appears after the process_safe_outputs step - processSafeOutputsPos := strings.Index(stepsContent, "id: process_safe_outputs") - require.Greater(t, processSafeOutputsPos, -1, "process_safe_outputs step must exist") - assert.Greater(t, uploadSARIFPos, processSafeOutputsPos, - "upload_code_scanning_sarif must appear after process_safe_outputs in compiled steps") - - // Verify the upload step is registered as a step name - assert.Contains(t, stepNames, "upload_code_scanning_sarif", - "upload_code_scanning_sarif should be in step names") - - // Verify sarif_file is exported as a job output - assert.Contains(t, job.Outputs, "sarif_file", - "Job should export sarif_file output") - assert.Contains(t, job.Outputs["sarif_file"], "steps.process_safe_outputs.outputs.sarif_file", - "sarif_file job output should reference process_safe_outputs step output") + // Restore step must appear before upload step + restorePos := strings.Index(uploadSteps, "Restore checkout to triggering commit") + uploadPos := strings.Index(uploadSteps, "Upload SARIF to GitHub Code Scanning") + require.Greater(t, restorePos, -1, "Restore checkout step must exist") + require.Greater(t, uploadPos, -1, "Upload SARIF step must exist") + assert.Less(t, restorePos, uploadPos, + "Restore checkout must appear before SARIF upload in the job steps") if tt.expectCustomToken != "" { - assert.Contains(t, stepsContent, tt.expectCustomToken, - "Upload step should use custom token") + assert.Contains(t, uploadSteps, tt.expectCustomToken, + "Upload job should use custom token") } if tt.expectDefaultToken { - assert.Contains(t, stepsContent, "GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN", - "Upload step should use default token fallback") + assert.Contains(t, uploadSteps, "GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN", + "Upload job should use default token fallback") } } else { - assert.NotContains(t, stepsContent, "Upload SARIF to GitHub Code Scanning", - "Staged mode should not include SARIF upload step") - assert.NotContains(t, stepNames, "upload_code_scanning_sarif", - "upload_code_scanning_sarif should not be in step names for staged mode") + // staged: safe_outputs should NOT export sarif_file + assert.NotContains(t, safeOutputsJob.Outputs, "sarif_file", + "staged mode: safe_outputs must not export sarif_file") + + // buildCodeScanningUploadJob should still produce a job structurally + // (staging is enforced by the caller in buildSafeOutputsJobs, not the builder itself) } }) } diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index bcc5f387a00..b4880a74507 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -3,6 +3,7 @@ package workflow import ( "fmt" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" ) @@ -61,43 +62,34 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea return securityReportsConfig } -// buildUploadCodeScanningSARIFStep builds steps to upload the SARIF file generated by the -// create_code_scanning_alert handler to the GitHub Code Scanning API. +// buildCodeScanningUploadJob creates a dedicated job that uploads the SARIF file produced by +// the create_code_scanning_alert handler to the GitHub Code Scanning API. // -// The create_code_scanning_alert.cjs handler writes findings to a SARIF file and sets -// the sarif_file step output on the process_safe_outputs step. These steps run after -// process_safe_outputs and upload the SARIF file only when findings were generated -// (i.e., when the sarif_file output is non-empty). +// This is a separate job (not a step inside safe_outputs) so that the checkout and SARIF +// upload do not interfere with other safe-output operations running in safe_outputs. // -// A restore checkout step is included before the upload to reset the workspace to the -// triggering commit (github.sha). This is necessary because the safe_outputs job may -// have checked out a different branch for PR operations (e.g., the base branch), and -// github/codeql-action/upload-sarif requires the HEAD to match the commit being scanned. -// Without this restore, the upload fails with "commit not found". -// -// The restore step uses actions/checkout with the same token and repository settings -// from the checkout manager (user-configured checkout frontmatter), falling back to -// the PR checkout token when no override is present. -func (c *Compiler) buildUploadCodeScanningSARIFStep(data *WorkflowData) []string { - createCodeScanningAlertLog.Print("Building SARIF upload step for code scanning alerts") - var steps []string - - // Restore the workspace to the triggering commit using actions/checkout. - // The checkout manager provides the correct token and repository settings from the - // user's checkout frontmatter configuration. The safe_outputs job may have checked - // out a different branch (e.g., when creating a PR), causing "commit not found" errors. - // - // Token precedence (via checkout manager then PR checkout fallback): - // 1. User-configured checkout.github-token (from frontmatter checkout: config) - // 2. GitHub App token (safe-outputs-app-token, if a GitHub App is configured) - // 3. PR checkout token (create-pull-request/push-to-pull-request-branch token, or safe-outputs token) - // 4. Default fallback: GH_AW_GITHUB_TOKEN || GITHUB_TOKEN +// The job: +// - depends on safe_outputs (needs: [safe_outputs]) +// - runs only when the safe_outputs job exported a SARIF file +// (if: needs.safe_outputs.outputs.sarif_file != ”) +// - restores the workspace to the triggering commit via actions/checkout before upload so +// that github/codeql-action/upload-sarif can resolve the commit reference +// - uploads the SARIF file with explicit ref/sha to pin the result to the triggering commit +func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) { + createCodeScanningAlertLog.Print("Building upload_code_scanning_sarif job") + + // Resolve the checkout token: consult the checkout manager for user-configured credentials, + // then fall back to the PR checkout token chain (safe-outputs token or GITHUB_TOKEN). checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) restoreToken := c.resolveRestoreCheckoutToken(checkoutMgr, data) - createCodeScanningAlertLog.Printf("Using restore checkout token for SARIF upload (derived from checkout manager)") - steps = append(steps, " - name: Restore checkout to triggering commit for SARIF upload\n") - steps = append(steps, " if: steps.process_safe_outputs.outputs.sarif_file != ''\n") + var steps []string + + // Step 1: Restore workspace to the triggering commit. + // The safe_outputs job may have checked out a different branch (e.g., the base branch for + // a PR) which would leave HEAD pointing at a different commit. The SARIF upload action + // requires HEAD to match the commit being scanned, otherwise it fails with "commit not found". + steps = append(steps, " - name: Restore checkout to triggering commit\n") steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/checkout"))) steps = append(steps, " with:\n") steps = append(steps, " ref: ${{ github.sha }}\n") @@ -105,22 +97,40 @@ func (c *Compiler) buildUploadCodeScanningSARIFStep(data *WorkflowData) []string steps = append(steps, " persist-credentials: false\n") steps = append(steps, " fetch-depth: 1\n") + // Step 2: Upload SARIF file to GitHub Code Scanning. + // The sarif_file path is passed from the safe_outputs job via job outputs. steps = append(steps, " - name: Upload SARIF to GitHub Code Scanning\n") - steps = append(steps, " id: upload_code_scanning_sarif\n") - // Only run when findings were generated (sarif_file output is set by the handler) - steps = append(steps, " if: steps.process_safe_outputs.outputs.sarif_file != ''\n") + steps = append(steps, fmt.Sprintf(" id: %s\n", constants.UploadCodeScanningJobName)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("github/codeql-action/upload-sarif"))) steps = append(steps, " with:\n") // NOTE: github/codeql-action/upload-sarif uses 'token' as the input name, not 'github-token' c.addUploadSARIFToken(&steps, data, data.SafeOutputs.CreateCodeScanningAlerts.GitHubToken) - steps = append(steps, " sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }}\n") - // ref and sha ensure the upload is associated with the correct triggering commit, - // even when the local HEAD has been reset or the runner checked out a different ref. + // sarif_file is passed from safe_outputs job output (set by create_code_scanning_alert.cjs handler) + steps = append(steps, fmt.Sprintf(" sarif_file: ${{ needs.%s.outputs.sarif_file }}\n", constants.SafeOutputsJobName)) + // ref and sha pin the upload to the exact triggering commit regardless of local git state steps = append(steps, " ref: ${{ github.ref }}\n") steps = append(steps, " sha: ${{ github.sha }}\n") steps = append(steps, " wait-for-processing: true\n") - return steps + // The job only runs when the safe_outputs job exported a non-empty SARIF file path. + jobCondition := fmt.Sprintf("needs.%s.outputs.sarif_file != ''", constants.SafeOutputsJobName) + + // Permissions: contents:read to checkout, security-events:write to upload SARIF + permissions := NewPermissionsContentsReadSecurityEventsWrite() + + job := &Job{ + Name: string(constants.UploadCodeScanningJobName), + If: jobCondition, + RunsOn: c.formatFrameworkJobRunsOn(data), + Environment: c.indentYAMLLines(resolveSafeOutputsEnvironment(data), " "), + Permissions: permissions.RenderToYAML(), + TimeoutMinutes: 10, + Steps: steps, + Needs: []string{string(constants.SafeOutputsJobName)}, + } + + createCodeScanningAlertLog.Print("Built upload_code_scanning_sarif job") + return job, nil } // addUploadSARIFToken adds the 'token' input for github/codeql-action/upload-sarif. @@ -161,17 +171,10 @@ func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, conf // checkout frontmatter), then falls back to the PR checkout token chain. // // Token precedence: -// 1. GitHub App token minted for safe-outputs (if safe-outputs app is configured) -// 2. User-configured checkout.github-token from the default workspace checkout -// 3. PR checkout token (create-pull-request / push-to-pull-request-branch / safe-outputs token) -// 4. Default: GH_AW_GITHUB_TOKEN || GITHUB_TOKEN +// 1. User-configured checkout.github-token from the default workspace checkout +// 2. PR checkout token (create-pull-request / push-to-pull-request-branch / safe-outputs token) +// 3. Default: GH_AW_GITHUB_TOKEN || GITHUB_TOKEN func (c *Compiler) resolveRestoreCheckoutToken(checkoutMgr *CheckoutManager, data *WorkflowData) string { - // GitHub App for safe-outputs takes highest precedence - if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil { - //nolint:gosec // G101: False positive - this is a GitHub Actions expression template placeholder, not a hardcoded credential - return "${{ steps.safe-outputs-app-token.outputs.token }}" - } - // User-configured checkout token from frontmatter (checkout: github-token: ...) override := checkoutMgr.GetDefaultCheckoutOverride() if override != nil && override.token != "" { From 46efb867238f1ff04b196c56b4c9e99df9af136e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:32:15 +0000 Subject: [PATCH 06/10] feat: merge main and use checkout_token from safe_outputs job outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge origin/main (includes feat: bundle token usage, fix: wasm golden files) - Add computeStaticCheckoutToken helper that computes a static checkout token (secret ref) without referencing any step output — safe to export as a job output - Export checkout_token from safe_outputs job when create-code-scanning-alert is configured; the value is the static PAT or default GH_AW_GITHUB_TOKEN || GITHUB_TOKEN (GitHub App tokens are excluded because they are minted and revoked in safe_outputs, so they would be invalid by the time the upload job runs) - buildCodeScanningUploadJob now uses needs.safe_outputs.outputs.checkout_token for both the restore checkout step and addUploadSARIFToken, removing the broken step-output reference (${{ steps.safe-outputs-app-token.outputs.token }}) that did not exist in the separate job context Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f1d25327-92dd-47a9-bbed-2121e981af50 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-malicious-code-scan.lock.yml | 5 +- .github/workflows/daily-semgrep-scan.lock.yml | 5 +- pkg/workflow/compiler_safe_outputs_job.go | 8 ++- .../compiler_safe_outputs_job_test.go | 63 +++++++++++------ pkg/workflow/create_code_scanning_alert.go | 67 +++++++------------ pkg/workflow/safe_outputs_config_helpers.go | 52 ++++++++++++++ 6 files changed, 133 insertions(+), 67 deletions(-) diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 037c1425662..c91da80b677 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -874,6 +874,7 @@ jobs: GH_AW_WORKFLOW_ID: "daily-malicious-code-scan" GH_AW_WORKFLOW_NAME: "Daily Malicious Code Scan Agent" outputs: + checkout_token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} @@ -953,14 +954,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + token: ${{ needs.safe_outputs.outputs.checkout_token }} persist-credentials: false fetch-depth: 1 - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + token: ${{ needs.safe_outputs.outputs.checkout_token }} sarif_file: ${{ needs.safe_outputs.outputs.sarif_file }} ref: ${{ github.ref }} sha: ${{ github.sha }} diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index 2708ee52a1c..bf4a1bf2abd 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -1053,6 +1053,7 @@ jobs: GH_AW_WORKFLOW_ID: "daily-semgrep-scan" GH_AW_WORKFLOW_NAME: "Daily Semgrep Scan" outputs: + checkout_token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} @@ -1132,14 +1133,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + token: ${{ needs.safe_outputs.outputs.checkout_token }} persist-credentials: false fetch-depth: 1 - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + token: ${{ needs.safe_outputs.outputs.checkout_token }} sarif_file: ${{ needs.safe_outputs.outputs.sarif_file }} ref: ${{ github.ref }} sha: ${{ github.sha }} diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index a99b75a0583..5708b481836 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -210,9 +210,15 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // 2. SARIF output — expose sarif_file from the handler so the dedicated // upload_code_scanning_sarif job (built in buildCodeScanningUploadJob) can access it // via needs.safe_outputs.outputs.sarif_file and decide whether to run. + // Also expose checkout_token so the upload job can checkout the repo without needing + // access to any GitHub App credentials (which are only minted in safe_outputs). if data.SafeOutputs.CreateCodeScanningAlerts != nil && !isHandlerStaged(c.trialMode || data.SafeOutputs.Staged, data.SafeOutputs.CreateCodeScanningAlerts.Staged) { - consolidatedSafeOutputsJobLog.Print("Exposing sarif_file output for upload_code_scanning_sarif job") + consolidatedSafeOutputsJobLog.Print("Exposing sarif_file and checkout_token outputs for upload_code_scanning_sarif job") outputs["sarif_file"] = "${{ steps.process_safe_outputs.outputs.sarif_file }}" + // Export a static checkout token (no step-output references) so the upload job can + // authenticate without needing the GitHub App credentials or a step from this job. + checkoutMgrForOutput := NewCheckoutManager(data.CheckoutConfigs) + outputs["checkout_token"] = computeStaticCheckoutToken(data.SafeOutputs, checkoutMgrForOutput) } // 3. Assign To Agent step (runs after handler managers) diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index 2d57f5499c4..58937568ae7 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -772,32 +772,45 @@ func TestCallWorkflowOnly_UsesHandlerManagerStep(t *testing.T) { // TestCreateCodeScanningAlertUploadJob verifies that when create-code-scanning-alert is configured, // a dedicated upload_code_scanning_sarif job is created (separate from safe_outputs) and that -// the safe_outputs job exports sarif_file so the upload job can check whether to run. +// the safe_outputs job exports sarif_file and checkout_token so the upload job can check whether +// to run and authenticate without needing GitHub App credentials. func TestCreateCodeScanningAlertUploadJob(t *testing.T) { tests := []struct { - name string - config *CreateCodeScanningAlertsConfig - expectUploadJob bool - expectCustomToken string - expectDefaultToken bool + name string + config *CreateCodeScanningAlertsConfig + expectUploadJob bool + expectCustomToken string + expectTokenFromOutputs bool // expect needs.safe_outputs.outputs.checkout_token + safeOutputsGitHubToken string }{ { - name: "default config creates separate upload job with default token", + name: "default config creates separate upload job using checkout_token from outputs", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{}, }, - expectUploadJob: true, - expectDefaultToken: true, + expectUploadJob: true, + expectTokenFromOutputs: true, }, { - name: "custom github-token is used in upload job", + name: "custom per-config github-token is used in upload step token", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{ GitHubToken: "${{ secrets.GHAS_TOKEN }}", }, }, - expectUploadJob: true, - expectCustomToken: "${{ secrets.GHAS_TOKEN }}", + expectUploadJob: true, + expectCustomToken: "${{ secrets.GHAS_TOKEN }}", + expectTokenFromOutputs: true, + }, + { + name: "safe-outputs-level github-token is used in upload step token", + config: &CreateCodeScanningAlertsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{}, + }, + expectUploadJob: true, + expectCustomToken: "${{ secrets.SO_TOKEN }}", + expectTokenFromOutputs: true, + safeOutputsGitHubToken: "${{ secrets.SO_TOKEN }}", }, { name: "staged mode does not create upload job", @@ -819,10 +832,11 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { Name: "Test Workflow", SafeOutputs: &SafeOutputsConfig{ CreateCodeScanningAlerts: tt.config, + GitHubToken: tt.safeOutputsGitHubToken, }, } - // 1. Verify safe_outputs job exports sarif_file (needed by the upload job's if: condition) + // 1. Verify safe_outputs job exports sarif_file and checkout_token safeOutputsJob, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test-workflow.md") require.NoError(t, err, "safe_outputs job should build without error") require.NotNil(t, safeOutputsJob, "safe_outputs job should be generated") @@ -836,6 +850,10 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.Contains(t, safeOutputsJob.Outputs["sarif_file"], "steps.process_safe_outputs.outputs.sarif_file", "sarif_file output must reference process_safe_outputs step") + // safe_outputs must export checkout_token for the upload job to use + assert.Contains(t, safeOutputsJob.Outputs, "checkout_token", + "safe_outputs job must export checkout_token output for the upload job") + // The upload and restore steps must NOT be in safe_outputs itself assert.NotContains(t, safeOutputsSteps, "upload-sarif", "SARIF upload must NOT be a step in safe_outputs job") @@ -868,6 +886,10 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.NotContains(t, uploadSteps, "git checkout ${{ github.sha }}", "Must use actions/checkout, not a raw git command") + // The restore checkout step must always use the checkout_token from safe_outputs outputs + assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.checkout_token", + "Restore checkout step must use checkout_token from safe_outputs outputs") + // Upload SARIF step must be present assert.Contains(t, uploadSteps, "Upload SARIF to GitHub Code Scanning", "Upload job must have SARIF upload step") @@ -899,19 +921,18 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { if tt.expectCustomToken != "" { assert.Contains(t, uploadSteps, tt.expectCustomToken, - "Upload job should use custom token") + "Upload SARIF token must use the per-config or safe-outputs github-token") } - if tt.expectDefaultToken { - assert.Contains(t, uploadSteps, "GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN", - "Upload job should use default token fallback") + if tt.expectTokenFromOutputs { + assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.checkout_token", + "Upload job should use checkout_token from safe_outputs outputs") } } else { - // staged: safe_outputs should NOT export sarif_file + // staged: safe_outputs should NOT export sarif_file or checkout_token assert.NotContains(t, safeOutputsJob.Outputs, "sarif_file", "staged mode: safe_outputs must not export sarif_file") - - // buildCodeScanningUploadJob should still produce a job structurally - // (staging is enforced by the caller in buildSafeOutputsJobs, not the builder itself) + assert.NotContains(t, safeOutputsJob.Outputs, "checkout_token", + "staged mode: safe_outputs must not export checkout_token") } }) } diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index b4880a74507..6fa0f28d3f7 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -78,10 +78,12 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) { createCodeScanningAlertLog.Print("Building upload_code_scanning_sarif job") - // Resolve the checkout token: consult the checkout manager for user-configured credentials, - // then fall back to the PR checkout token chain (safe-outputs token or GITHUB_TOKEN). - checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) - restoreToken := c.resolveRestoreCheckoutToken(checkoutMgr, data) + // The checkout token is passed from the safe_outputs job via its checkout_token output. + // This avoids needing GitHub App credentials in this job — the app token (if any) was + // already minted and revoked in safe_outputs, so we use the static token that safe_outputs + // exported. Falls back to the default GH_AW_GITHUB_TOKEN || GITHUB_TOKEN when no + // user-configured PAT is present. + restoreToken := fmt.Sprintf("${{ needs.%s.outputs.checkout_token }}", constants.SafeOutputsJobName) var steps []string @@ -135,54 +137,37 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) // addUploadSARIFToken adds the 'token' input for github/codeql-action/upload-sarif. // This action uses 'token' as the input name (not 'github-token' like other GitHub Actions). -// Uses precedence: config token > safe-outputs global github-token > GH_AW_GITHUB_TOKEN || GITHUB_TOKEN +// This runs inside the upload_code_scanning_sarif job (a separate job from safe_outputs), so +// the token is read from the safe_outputs job's checkout_token output rather than any step output. +// Uses precedence: config token > safe-outputs global github-token > safe_outputs.outputs.checkout_token func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, configToken string) { var safeOutputsToken string if data.SafeOutputs != nil { safeOutputsToken = data.SafeOutputs.GitHubToken } - // If app is configured, use app token - if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil { - *steps = append(*steps, " token: ${{ steps.safe-outputs-app-token.outputs.token }}\n") - return - } - - // Choose the first non-empty custom token for precedence + // Choose the first non-empty per-config or safe-outputs-level static PAT. + // GitHub App tokens are NOT used here because they are minted and revoked in safe_outputs; + // they are unavailable in this separate downstream job. effectiveCustomToken := configToken if effectiveCustomToken == "" { effectiveCustomToken = safeOutputsToken } - effectiveToken := getEffectiveSafeOutputGitHubToken(effectiveCustomToken) - // Log which token source is being used for debugging - tokenSource := "default (GH_AW_GITHUB_TOKEN || GITHUB_TOKEN)" - if configToken != "" { - tokenSource = "per-config github-token" - } else if safeOutputsToken != "" { - tokenSource = "safe-outputs github-token" - } - createCodeScanningAlertLog.Printf("Using token for SARIF upload from source: %s (upload-sarif uses 'token' not 'github-token')", tokenSource) - *steps = append(*steps, fmt.Sprintf(" token: %s\n", effectiveToken)) -} - -// resolveRestoreCheckoutToken returns the GitHub token to use for the pre-SARIF -// restore checkout step. It consults the checkout manager first (user-configured -// checkout frontmatter), then falls back to the PR checkout token chain. -// -// Token precedence: -// 1. User-configured checkout.github-token from the default workspace checkout -// 2. PR checkout token (create-pull-request / push-to-pull-request-branch / safe-outputs token) -// 3. Default: GH_AW_GITHUB_TOKEN || GITHUB_TOKEN -func (c *Compiler) resolveRestoreCheckoutToken(checkoutMgr *CheckoutManager, data *WorkflowData) string { - // User-configured checkout token from frontmatter (checkout: github-token: ...) - override := checkoutMgr.GetDefaultCheckoutOverride() - if override != nil && override.token != "" { - createCodeScanningAlertLog.Printf("Using checkout manager override token for SARIF restore checkout") - return getEffectiveSafeOutputGitHubToken(override.token) + if effectiveCustomToken != "" { + effectiveToken := getEffectiveSafeOutputGitHubToken(effectiveCustomToken) + tokenSource := "per-config github-token" + if configToken == "" { + tokenSource = "safe-outputs github-token" + } + createCodeScanningAlertLog.Printf("Using token for SARIF upload from source: %s (upload-sarif uses 'token' not 'github-token')", tokenSource) + *steps = append(*steps, fmt.Sprintf(" token: %s\n", effectiveToken)) + return } - // Fall back to PR checkout token (which itself falls back to safe-outputs token or GITHUB_TOKEN) - prToken, _ := computeEffectivePRCheckoutToken(data.SafeOutputs) - return prToken + // No per-config or safe-outputs token — use the checkout_token exported from the safe_outputs job. + // This is the static token (secret reference) computed by computeStaticCheckoutToken in safe_outputs, + // which falls back to GH_AW_GITHUB_TOKEN || GITHUB_TOKEN when no user-configured PAT is present. + createCodeScanningAlertLog.Printf("Using safe_outputs.outputs.checkout_token for SARIF upload token") + *steps = append(*steps, fmt.Sprintf(" token: ${{ needs.%s.outputs.checkout_token }}\n", constants.SafeOutputsJobName)) } diff --git a/pkg/workflow/safe_outputs_config_helpers.go b/pkg/workflow/safe_outputs_config_helpers.go index 1b46c2db2d3..869b7f81ff4 100644 --- a/pkg/workflow/safe_outputs_config_helpers.go +++ b/pkg/workflow/safe_outputs_config_helpers.go @@ -71,6 +71,58 @@ func computeEffectivePRCheckoutToken(safeOutputs *SafeOutputsConfig) (token stri return getEffectiveSafeOutputGitHubToken(""), false } +// computeStaticCheckoutToken returns the effective checkout token as a **static** GitHub +// Actions expression (secret reference or default). Unlike computeEffectivePRCheckoutToken, +// this function never returns a step-output expression (e.g. +// "${{ steps.safe-outputs-app-token.outputs.token }}") because step outputs are not +// accessible outside the job they were created in. +// +// This is the correct function to use when the token value needs to be exported as a +// job output for consumption by downstream jobs (e.g. upload_code_scanning_sarif). +// +// Token precedence: +// 1. Per-config PAT: create-pull-request.github-token +// 2. Per-config PAT: push-to-pull-request-branch.github-token +// 3. safe-outputs level PAT: safe-outputs.github-token +// 4. Default fallback (GH_AW_GITHUB_TOKEN || GITHUB_TOKEN) +// +// Note: GitHub App tokens are intentionally excluded because: +// - Minted app tokens are short-lived and revoked at the end of the safe_outputs job. +// - A downstream job that reads a revoked token from a job output would fail to authenticate. +// - When only a GitHub App is configured (no static PAT), the downstream job should use +// the default GITHUB_TOKEN, which has `contents: read` and is sufficient for checkout. +func computeStaticCheckoutToken(safeOutputs *SafeOutputsConfig, checkoutMgr *CheckoutManager) string { + // Priority 0: user-configured workspace checkout token (checkout: github-token:) + if checkoutMgr != nil { + override := checkoutMgr.GetDefaultCheckoutOverride() + if override != nil && override.token != "" { + return getEffectiveSafeOutputGitHubToken(override.token) + } + } + + if safeOutputs == nil { + return getEffectiveSafeOutputGitHubToken("") + } + + // Priority 1: per-config PAT for create-pull-request + if safeOutputs.CreatePullRequests != nil && safeOutputs.CreatePullRequests.GitHubToken != "" { + return getEffectiveSafeOutputGitHubToken(safeOutputs.CreatePullRequests.GitHubToken) + } + + // Priority 2: per-config PAT for push-to-pull-request-branch + if safeOutputs.PushToPullRequestBranch != nil && safeOutputs.PushToPullRequestBranch.GitHubToken != "" { + return getEffectiveSafeOutputGitHubToken(safeOutputs.PushToPullRequestBranch.GitHubToken) + } + + // Priority 3: safe-outputs level PAT (skip GitHub App — see function doc) + if safeOutputs.GitHubToken != "" { + return getEffectiveSafeOutputGitHubToken(safeOutputs.GitHubToken) + } + + // Priority 4: default + return getEffectiveSafeOutputGitHubToken("") +} + // computeEffectiveProjectToken computes the effective project token using the precedence: // 1. Per-config token (e.g., from update-project, create-project-status-update) // 2. Safe-outputs level token From ca36f1a5c308790604f1f87929bbf092285dad88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:01:49 +0000 Subject: [PATCH 07/10] feat: upload SARIF as artifact in safe_outputs and download in upload job; add smoke-claude test - Add SarifArtifactName, SarifArtifactDownloadPath, SarifFileName constants - buildSarifArtifactUploadStep: uploads SARIF from safe_outputs workspace as a GitHub Actions artifact (conditional on sarif_file != '', retention 1 day) - buildCodeScanningUploadJob: adds download step for the artifact before the CodeQL upload step; uses local artifact path instead of job-output path string - Use path.Join for SARIF path construction (per code review) - Update tests to verify upload/download/path pipeline - smoke-claude.md: add create-code-scanning-alert safe output (driver: "Smoke Claude") and test #12 in the prompt to post a dummy warning alert Agent-Logs-Url: https://github.com/github/gh-aw/sessions/75ad7032-2b7d-4459-ba40-57cbfa4f0190 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-malicious-code-scan.lock.yml | 16 +- .github/workflows/daily-semgrep-scan.lock.yml | 16 +- .github/workflows/smoke-claude.lock.yml | 190 +++++++++++++----- .github/workflows/smoke-claude.md | 29 ++- pkg/constants/job_constants.go | 15 ++ pkg/workflow/compiler_safe_outputs_job.go | 31 +++ .../compiler_safe_outputs_job_test.go | 49 ++++- pkg/workflow/create_code_scanning_alert.go | 25 ++- 8 files changed, 294 insertions(+), 77 deletions(-) diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index c91da80b677..bd3951e161b 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -933,6 +933,14 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Upload SARIF artifact + if: steps.process_safe_outputs.outputs.sarif_file != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: code-scanning-sarif + path: ${{ steps.process_safe_outputs.outputs.sarif_file }} + if-no-files-found: error + retention-days: 1 - name: Upload Safe Output Items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 @@ -957,12 +965,18 @@ jobs: token: ${{ needs.safe_outputs.outputs.checkout_token }} persist-credentials: false fetch-depth: 1 + - name: Download SARIF artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: code-scanning-sarif + path: /tmp/gh-aw/sarif/ - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: token: ${{ needs.safe_outputs.outputs.checkout_token }} - sarif_file: ${{ needs.safe_outputs.outputs.sarif_file }} + sarif_file: /tmp/gh-aw/sarif/code-scanning-alert.sarif ref: ${{ github.ref }} sha: ${{ github.sha }} wait-for-processing: true diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index bf4a1bf2abd..dd33a5db0f2 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -1112,6 +1112,14 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Upload SARIF artifact + if: steps.process_safe_outputs.outputs.sarif_file != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: code-scanning-sarif + path: ${{ steps.process_safe_outputs.outputs.sarif_file }} + if-no-files-found: error + retention-days: 1 - name: Upload Safe Output Items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 @@ -1136,12 +1144,18 @@ jobs: token: ${{ needs.safe_outputs.outputs.checkout_token }} persist-credentials: false fetch-depth: 1 + - name: Download SARIF artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: code-scanning-sarif + path: /tmp/gh-aw/sarif/ - name: Upload SARIF to GitHub Code Scanning id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: token: ${{ needs.safe_outputs.outputs.checkout_token }} - sarif_file: ${{ needs.safe_outputs.outputs.sarif_file }} + sarif_file: /tmp/gh-aw/sarif/code-scanning-alert.sarif ref: ${{ github.ref }} sha: ${{ github.sha }} wait-for-processing: true diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index d18e8218f21..b4dd5602b24 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"c8e85b0ffed195e6ee17c58abdbf3bbfcbd97a8f97be8d1041ee2fe72da2ce8b","agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"05a0c0106bd5bee59f415de4900021788daccaabb257182591d7656d4155b42f","agent_id":"claude"} name: "Smoke Claude" "on": @@ -201,9 +201,9 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_9e151125965f1459_EOF' + cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' - GH_AW_PROMPT_9e151125965f1459_EOF + GH_AW_PROMPT_c60a3b86c7def1cd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" @@ -211,12 +211,12 @@ jobs: cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9e151125965f1459_EOF' + cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' - Tools: add_comment(max:2), create_issue, close_pull_request, update_pull_request, create_pull_request_review_comment(max:5), submit_pull_request_review, resolve_pull_request_review_thread(max:5), add_labels, add_reviewer(max:2), push_to_pull_request_branch, missing_tool, missing_data, noop, post_slack_message - GH_AW_PROMPT_9e151125965f1459_EOF + Tools: add_comment(max:2), create_issue, close_pull_request, update_pull_request, create_pull_request_review_comment(max:5), submit_pull_request_review, resolve_pull_request_review_thread(max:5), add_labels, add_reviewer(max:2), push_to_pull_request_branch, create_code_scanning_alert, missing_tool, missing_data, noop, post_slack_message + GH_AW_PROMPT_c60a3b86c7def1cd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_9e151125965f1459_EOF' + cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' The following GitHub context information is available for this workflow: @@ -246,9 +246,9 @@ jobs: {{/if}} - GH_AW_PROMPT_9e151125965f1459_EOF + GH_AW_PROMPT_c60a3b86c7def1cd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9e151125965f1459_EOF' + cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' @@ -551,38 +551,43 @@ jobs: - Use `channel: "#smoke-tests"` and `message: "💥 Smoke test __GH_AW_GITHUB_RUN_ID__ passed — Claude engine nominal!"` - Verify the tool call succeeds + 12. **Code Scanning Alert Safe Output Testing**: Use the `create_code_scanning_alert` safe-output tool to post a dummy warning code scanning alert: + - Use `level: "warning"`, `message: "Smoke test dummy warning — Run __GH_AW_GITHUB_RUN_ID__"`, `file: "README.md"`, `line: 1` + - Verify the tool call succeeds + - This tests the SARIF artifact upload/download pipeline + ## PR Review Safe Outputs Testing **IMPORTANT**: The following tests require an open pull request. First, use the GitHub MCP tool to find an open PR in __GH_AW_GITHUB_REPOSITORY__ (or use the triggering PR if this is a pull_request event). Store the PR number for use in subsequent tests. - 12. **Update PR Testing**: Use the `update_pull_request` tool to update the PR's body by appending a test message: "✨ PR Review Safe Output Test - Run __GH_AW_GITHUB_RUN_ID__" + 13. **Update PR Testing**: Use the `update_pull_request` tool to update the PR's body by appending a test message: "✨ PR Review Safe Output Test - Run __GH_AW_GITHUB_RUN_ID__" - Use `pr_number: ` to target the open PR - Use `operation: "append"` and `body: "\n\n---\n✨ PR Review Safe Output Test - Run __GH_AW_GITHUB_RUN_ID__"` - Verify the tool call succeeds - 13. **PR Review Comment Testing**: Use the `create_pull_request_review_comment` tool to add review comments on the PR + 14. **PR Review Comment Testing**: Use the `create_pull_request_review_comment` tool to add review comments on the PR - Find a file in the PR's diff (use GitHub MCP to get PR files) - Add at least 2 review comments on different lines with constructive feedback - Use `pr_number: `, `path: ""`, `line: `, and `body: ""` - Verify the tool calls succeed - 14. **Submit PR Review Testing**: Use the `submit_pull_request_review` tool to submit a consolidated review + 15. **Submit PR Review Testing**: Use the `submit_pull_request_review` tool to submit a consolidated review - Use `pr_number: `, `event: "COMMENT"`, and `body: "💥 Automated smoke test review - all systems nominal!"` - Verify the review is submitted successfully - - Note: This will bundle all review comments from test #13 + - Note: This will bundle all review comments from test #14 - 15. **Resolve Review Thread Testing**: + 16. **Resolve Review Thread Testing**: - Use the GitHub MCP tool to list review threads on the PR - If any threads exist, use the `resolve_pull_request_review_thread` tool to resolve one thread - Use `thread_id: ""` from an existing thread - If no threads exist, mark this test as ⚠️ (skipped - no threads to resolve) - 16. **Add Reviewer Testing**: Use the `add_reviewer` tool to add a reviewer to the PR + 17. **Add Reviewer Testing**: Use the `add_reviewer` tool to add a reviewer to the PR - Use `pr_number: ` and `reviewers: ["copilot"]` (or another valid reviewer) - Verify the tool call succeeds - Note: May fail if reviewer is already assigned or doesn't have access - 17. **Push to PR Branch Testing**: + 18. **Push to PR Branch Testing**: - Create a test file at `/tmp/test-pr-push-__GH_AW_GITHUB_RUN_ID__.txt` with content "Test file for PR push" - Use git commands to check if we're on the PR branch - Use the `push_to_pull_request_branch` tool to push this change @@ -590,7 +595,7 @@ jobs: - Verify the push succeeds - Note: This test may be skipped if not on a PR branch or if the PR is from a fork - 18. **Close PR Testing** (CONDITIONAL - only if a test PR exists): + 19. **Close PR Testing** (CONDITIONAL - only if a test PR exists): - If you can identify a test/bot PR that can be safely closed, use the `close_pull_request` tool - Use `pr_number: ` and `comment: "Closing as part of smoke test - Run __GH_AW_GITHUB_RUN_ID__"` - If no suitable test PR exists, mark this test as ⚠️ (skipped - no safe PR to close) @@ -603,7 +608,7 @@ jobs: 1. **ALWAYS create an issue** with a summary of the smoke test run: - Title: "Smoke Test: Claude - __GH_AW_GITHUB_RUN_ID__" - Body should include: - - Test results (✅ for pass, ❌ for fail, ⚠️ for skipped) for each test (including PR review tests #12-18) + - Test results (✅ for pass, ❌ for fail, ⚠️ for skipped) for each test (including PR review tests #13-19) - Overall status: PASS (all passed), PARTIAL (some skipped), or FAIL (any failed) - Run URL: __GH_AW_GITHUB_SERVER_URL__/__GH_AW_GITHUB_REPOSITORY__/actions/runs/__GH_AW_GITHUB_RUN_ID__ - Timestamp @@ -612,8 +617,8 @@ jobs: - This issue MUST be created before any other safe output operations 2. **Only if this workflow was triggered by a pull_request event**: Use the `add_comment` tool to add a **very brief** comment (max 5-10 lines) to the triggering pull request (omit the `item_number` parameter to auto-target the triggering PR) with: - - Test results for core tests #1-11 (✅ or ❌) - - Test results for PR review tests #12-18 (✅, ❌, or ⚠️) + - Test results for core tests #1-12 (✅ or ❌) + - Test results for PR review tests #13-19 (✅, ❌, or ⚠️) - Overall status: PASS, PARTIAL, or FAIL 3. Use the `add_comment` tool with `item_number` set to the discussion number you extracted in step 9 to add a **fun comic-book style comment** to that discussion - be playful and use comic-book language like "💥 WHOOSH!" @@ -627,7 +632,7 @@ jobs: {"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}} ``` - GH_AW_PROMPT_9e151125965f1459_EOF + GH_AW_PROMPT_c60a3b86c7def1cd_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -900,12 +905,12 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_b918226e9c57e768_EOF' - {"add_comment":{"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-claude"]},"add_reviewer":{"max":2,"target":"*"},"close_pull_request":{"max":1,"staged":true},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-claude","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT","target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"post_slack_message":{"description":"Post a message to a fictitious Slack channel (smoke test only — no real Slack integration)","inputs":{"channel":{"default":"#general","description":"Slack channel name to post to","required":false,"type":"string"},"message":{"description":"Message text to post","required":false,"type":"string"}}},"push_to_pull_request_branch":{"allowed_files":[".github/smoke-claude-push-test.md"],"if_no_changes":"warn","max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"staged":true,"target":"*"},"resolve_pull_request_review_thread":{"max":5},"submit_pull_request_review":{"footer":"always","max":1},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_b918226e9c57e768_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_abf8a06446cf64d7_EOF' + {"add_comment":{"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-claude"]},"add_reviewer":{"max":2,"target":"*"},"close_pull_request":{"max":1,"staged":true},"create_code_scanning_alert":{"driver":"Smoke Claude"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-claude","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT","target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"post_slack_message":{"description":"Post a message to a fictitious Slack channel (smoke test only — no real Slack integration)","inputs":{"channel":{"default":"#general","description":"Slack channel name to post to","required":false,"type":"string"},"message":{"description":"Message text to post","required":false,"type":"string"}}},"push_to_pull_request_branch":{"allowed_files":[".github/smoke-claude-push-test.md"],"if_no_changes":"warn","max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"staged":true,"target":"*"},"resolve_pull_request_review_thread":{"max":5},"submit_pull_request_review":{"footer":"always","max":1},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"target":"*"}} + GH_AW_SAFE_OUTPUTS_CONFIG_abf8a06446cf64d7_EOF - name: Write Safe Outputs Tools run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_e61a50bc4dbfc828_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_21307181a707e81e_EOF' { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added.", @@ -942,8 +947,8 @@ jobs: } ] } - GH_AW_SAFE_OUTPUTS_TOOLS_META_e61a50bc4dbfc828_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_16c8d1ef6cbd7c54_EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_META_21307181a707e81e_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_93fe84dc8c044f49_EOF' { "add_comment": { "defaultMax": 1, @@ -1019,6 +1024,47 @@ jobs: } } }, + "create_code_scanning_alert": { + "defaultMax": 40, + "fields": { + "column": { + "optionalPositiveInteger": true + }, + "file": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "line": { + "required": true, + "positiveInteger": true + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 2048 + }, + "ruleIdSuffix": { + "type": "string", + "sanitize": true, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9_-]+$", + "patternError": "must contain only alphanumeric characters, hyphens, and underscores" + }, + "severity": { + "required": true, + "type": "string", + "enum": [ + "error", + "warning", + "info", + "note" + ] + } + } + }, "create_issue": { "defaultMax": 1, "fields": { @@ -1228,7 +1274,7 @@ jobs: "customValidation": "requiresOneOf:title,body" } } - GH_AW_SAFE_OUTPUTS_VALIDATION_16c8d1ef6cbd7c54_EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_93fe84dc8c044f49_EOF node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config @@ -1273,7 +1319,7 @@ jobs: - name: Setup MCP Scripts Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/mcp-scripts/logs - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_bd0753d0a6904c15_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_08274c8fa5e4f86a_EOF' { "serverName": "mcpscripts", "version": "1.0.0", @@ -1425,8 +1471,8 @@ jobs: } ] } - GH_AW_MCP_SCRIPTS_TOOLS_bd0753d0a6904c15_EOF - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_0461f86ed393cb9b_EOF' + GH_AW_MCP_SCRIPTS_TOOLS_08274c8fa5e4f86a_EOF + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_f32940a023588ab9_EOF' const path = require("path"); const { startHttpServer } = require("./mcp_scripts_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); @@ -1440,12 +1486,12 @@ jobs: console.error("Failed to start mcp-scripts HTTP server:", error); process.exit(1); }); - GH_AW_MCP_SCRIPTS_SERVER_0461f86ed393cb9b_EOF + GH_AW_MCP_SCRIPTS_SERVER_f32940a023588ab9_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs - name: Setup MCP Scripts Tool Files run: | - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_19c5863f6956cb95_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_70cb5e31f4e7f561_EOF' #!/bin/bash # Auto-generated mcp-script tool: gh # Execute any gh CLI command. This tool is accessible as 'mcpscripts-gh'. Provide the full command after 'gh' (e.g., args: 'pr list --limit 5'). The tool will run: gh . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1456,9 +1502,9 @@ jobs: echo " token: ${GH_AW_GH_TOKEN:0:6}..." GH_TOKEN="$GH_AW_GH_TOKEN" gh $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GH_19c5863f6956cb95_EOF + GH_AW_MCP_SCRIPTS_SH_GH_70cb5e31f4e7f561_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_96d28c752190963a_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_c43cfce700ad2129_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-discussion-query # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1593,9 +1639,9 @@ jobs: EOF fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_96d28c752190963a_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_c43cfce700ad2129_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_11317a0085a1ad91_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_92a544dc4c7de46d_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-issue-query # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1674,9 +1720,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_11317a0085a1ad91_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_92a544dc4c7de46d_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_c30b3beeba855527_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_f8ab02f7ce6a8ee5_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-pr-query # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1761,9 +1807,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_c30b3beeba855527_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_f8ab02f7ce6a8ee5_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/go.sh << 'GH_AW_MCP_SCRIPTS_SH_GO_223c3ba39b6e7289_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/go.sh << 'GH_AW_MCP_SCRIPTS_SH_GO_f08171d96ec9497f_EOF' #!/bin/bash # Auto-generated mcp-script tool: go # Execute any Go command. This tool is accessible as 'mcpscripts-go'. Provide the full command after 'go' (e.g., args: 'test ./...'). The tool will run: go . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1774,9 +1820,9 @@ jobs: go $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GO_223c3ba39b6e7289_EOF + GH_AW_MCP_SCRIPTS_SH_GO_f08171d96ec9497f_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/go.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/make.sh << 'GH_AW_MCP_SCRIPTS_SH_MAKE_7642664e59bacf91_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/make.sh << 'GH_AW_MCP_SCRIPTS_SH_MAKE_ab6ce21101ff9338_EOF' #!/bin/bash # Auto-generated mcp-script tool: make # Execute any Make target. This tool is accessible as 'mcpscripts-make'. Provide the target name(s) (e.g., args: 'build'). The tool will run: make . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1786,7 +1832,7 @@ jobs: echo "make $INPUT_ARGS" make $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_MAKE_7642664e59bacf91_EOF + GH_AW_MCP_SCRIPTS_SH_MAKE_ab6ce21101ff9338_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/make.sh - name: Generate MCP Scripts Server Config @@ -1859,7 +1905,7 @@ jobs: export GH_AW_ENGINE="claude" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_MCP_SCRIPTS_PORT -e GH_AW_MCP_SCRIPTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e GH_TOKEN -e TAVILY_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.12' - cat << GH_AW_MCP_CONFIG_ec38d8f15328bda6_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_b2fdcc7961148bf4_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "agenticworkflows": { @@ -1998,7 +2044,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_ec38d8f15328bda6_EOF + GH_AW_MCP_CONFIG_b2fdcc7961148bf4_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -2348,6 +2394,7 @@ jobs: - detection - safe_outputs - update_cache_memory + - upload_code_scanning_sarif if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') runs-on: ubuntu-slim permissions: @@ -2355,6 +2402,7 @@ jobs: discussions: write issues: write pull-requests: write + security-events: write concurrency: group: "gh-aw-conclusion-smoke-claude" cancel-in-progress: false @@ -2458,6 +2506,7 @@ jobs: GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e 💥 *[THE END] — Illustrated by [{workflow_name}]({run_url})*{effective_tokens_suffix}{history_link}\",\"runStarted\":\"💥 **WHOOSH!** [{workflow_name}]({run_url}) springs into action on this {event_type}! *[Panel 1 begins...]*\",\"runSuccess\":\"🎬 **THE END** — [{workflow_name}]({run_url}) **MISSION: ACCOMPLISHED!** The hero saves the day! ✨\",\"runFailure\":\"💫 **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges...\"}" + GH_AW_SAFE_OUTPUT_JOBS: "{\"upload_code_scanning_sarif\":\"\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -2679,6 +2728,7 @@ jobs: discussions: write issues: write pull-requests: write + security-events: write timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/smoke-claude" @@ -2690,6 +2740,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Smoke Claude" outputs: add_reviewer_reviewers_added: ${{ steps.process_safe_outputs.outputs.reviewers_added }} + checkout_token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} @@ -2702,6 +2753,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} steps: - name: Checkout actions folder uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -2739,7 +2791,7 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Setup Safe Outputs Custom Scripts run: | - cat > ${RUNNER_TEMP}/gh-aw/actions/safe_output_script_post_slack_message.cjs << 'GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_d6fcf72c929f77e4_EOF' + cat > ${RUNNER_TEMP}/gh-aw/actions/safe_output_script_post_slack_message.cjs << 'GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_95b4472f6a4f355b_EOF' // @ts-check /// // Auto-generated safe-output script handler: post-slack-message @@ -2759,7 +2811,7 @@ jobs: } module.exports = { main }; - GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_d6fcf72c929f77e4_EOF + GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_95b4472f6a4f355b_EOF - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -2769,7 +2821,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_SCRIPTS: "{\"post_slack_message\":\"safe_output_script_post_slack_message.cjs\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"add_reviewer\":{\"max\":2,\"target\":\"*\"},\"close_pull_request\":{\"max\":1,\"staged\":true},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-claude\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\",\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\".github/smoke-claude-push-test.md\"],\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"CLAUDE.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"],\"staged\":true,\"target\":\"*\"},\"resolve_pull_request_review_thread\":{\"max\":5},\"submit_pull_request_review\":{\"footer\":\"always\",\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"add_reviewer\":{\"max\":2,\"target\":\"*\"},\"close_pull_request\":{\"max\":1,\"staged\":true},\"create_code_scanning_alert\":{\"driver\":\"Smoke Claude\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-claude\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\",\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\".github/smoke-claude-push-test.md\"],\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"CLAUDE.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"],\"staged\":true,\"target\":\"*\"},\"resolve_pull_request_review_thread\":{\"max\":5},\"submit_pull_request_review\":{\"footer\":\"always\",\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -2777,6 +2829,14 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); + - name: Upload SARIF artifact + if: steps.process_safe_outputs.outputs.sarif_file != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: code-scanning-sarif + path: ${{ steps.process_safe_outputs.outputs.sarif_file }} + if-no-files-found: error + retention-days: 1 - name: Upload Safe Output Items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 @@ -2832,3 +2892,35 @@ jobs: key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} path: /tmp/gh-aw/cache-memory + upload_code_scanning_sarif: + needs: safe_outputs + if: needs.safe_outputs.outputs.sarif_file != '' + runs-on: ubuntu-slim + permissions: + contents: read + security-events: write + timeout-minutes: 10 + steps: + - name: Restore checkout to triggering commit + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + token: ${{ needs.safe_outputs.outputs.checkout_token }} + persist-credentials: false + fetch-depth: 1 + - name: Download SARIF artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: code-scanning-sarif + path: /tmp/gh-aw/sarif/ + - name: Upload SARIF to GitHub Code Scanning + id: upload_code_scanning_sarif + uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 + with: + token: ${{ needs.safe_outputs.outputs.checkout_token }} + sarif_file: /tmp/gh-aw/sarif/code-scanning-alert.sarif + ref: ${{ github.ref }} + sha: ${{ github.sha }} + wait-for-processing: true + diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index 4f1c3c36694..d8dd76b0167 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -64,6 +64,8 @@ safe-outputs: labels: [automation, testing] add-labels: allowed: [smoke-claude] + create-code-scanning-alert: + driver: "Smoke Claude" update-pull-request: title: true body: true @@ -148,38 +150,43 @@ timeout-minutes: 10 - Use `channel: "#smoke-tests"` and `message: "💥 Smoke test ${{ github.run_id }} passed — Claude engine nominal!"` - Verify the tool call succeeds +12. **Code Scanning Alert Safe Output Testing**: Use the `create_code_scanning_alert` safe-output tool to post a dummy warning code scanning alert: + - Use `level: "warning"`, `message: "Smoke test dummy warning — Run ${{ github.run_id }}"`, `file: "README.md"`, `line: 1` + - Verify the tool call succeeds + - This tests the SARIF artifact upload/download pipeline + ## PR Review Safe Outputs Testing **IMPORTANT**: The following tests require an open pull request. First, use the GitHub MCP tool to find an open PR in ${{ github.repository }} (or use the triggering PR if this is a pull_request event). Store the PR number for use in subsequent tests. -12. **Update PR Testing**: Use the `update_pull_request` tool to update the PR's body by appending a test message: "✨ PR Review Safe Output Test - Run ${{ github.run_id }}" +13. **Update PR Testing**: Use the `update_pull_request` tool to update the PR's body by appending a test message: "✨ PR Review Safe Output Test - Run ${{ github.run_id }}" - Use `pr_number: ` to target the open PR - Use `operation: "append"` and `body: "\n\n---\n✨ PR Review Safe Output Test - Run ${{ github.run_id }}"` - Verify the tool call succeeds -13. **PR Review Comment Testing**: Use the `create_pull_request_review_comment` tool to add review comments on the PR +14. **PR Review Comment Testing**: Use the `create_pull_request_review_comment` tool to add review comments on the PR - Find a file in the PR's diff (use GitHub MCP to get PR files) - Add at least 2 review comments on different lines with constructive feedback - Use `pr_number: `, `path: ""`, `line: `, and `body: ""` - Verify the tool calls succeed -14. **Submit PR Review Testing**: Use the `submit_pull_request_review` tool to submit a consolidated review +15. **Submit PR Review Testing**: Use the `submit_pull_request_review` tool to submit a consolidated review - Use `pr_number: `, `event: "COMMENT"`, and `body: "💥 Automated smoke test review - all systems nominal!"` - Verify the review is submitted successfully - - Note: This will bundle all review comments from test #13 + - Note: This will bundle all review comments from test #14 -15. **Resolve Review Thread Testing**: +16. **Resolve Review Thread Testing**: - Use the GitHub MCP tool to list review threads on the PR - If any threads exist, use the `resolve_pull_request_review_thread` tool to resolve one thread - Use `thread_id: ""` from an existing thread - If no threads exist, mark this test as ⚠️ (skipped - no threads to resolve) -16. **Add Reviewer Testing**: Use the `add_reviewer` tool to add a reviewer to the PR +17. **Add Reviewer Testing**: Use the `add_reviewer` tool to add a reviewer to the PR - Use `pr_number: ` and `reviewers: ["copilot"]` (or another valid reviewer) - Verify the tool call succeeds - Note: May fail if reviewer is already assigned or doesn't have access -17. **Push to PR Branch Testing**: +18. **Push to PR Branch Testing**: - Create a test file at `/tmp/test-pr-push-${{ github.run_id }}.txt` with content "Test file for PR push" - Use git commands to check if we're on the PR branch - Use the `push_to_pull_request_branch` tool to push this change @@ -187,7 +194,7 @@ timeout-minutes: 10 - Verify the push succeeds - Note: This test may be skipped if not on a PR branch or if the PR is from a fork -18. **Close PR Testing** (CONDITIONAL - only if a test PR exists): +19. **Close PR Testing** (CONDITIONAL - only if a test PR exists): - If you can identify a test/bot PR that can be safely closed, use the `close_pull_request` tool - Use `pr_number: ` and `comment: "Closing as part of smoke test - Run ${{ github.run_id }}"` - If no suitable test PR exists, mark this test as ⚠️ (skipped - no safe PR to close) @@ -200,7 +207,7 @@ timeout-minutes: 10 1. **ALWAYS create an issue** with a summary of the smoke test run: - Title: "Smoke Test: Claude - ${{ github.run_id }}" - Body should include: - - Test results (✅ for pass, ❌ for fail, ⚠️ for skipped) for each test (including PR review tests #12-18) + - Test results (✅ for pass, ❌ for fail, ⚠️ for skipped) for each test (including PR review tests #13-19) - Overall status: PASS (all passed), PARTIAL (some skipped), or FAIL (any failed) - Run URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - Timestamp @@ -209,8 +216,8 @@ timeout-minutes: 10 - This issue MUST be created before any other safe output operations 2. **Only if this workflow was triggered by a pull_request event**: Use the `add_comment` tool to add a **very brief** comment (max 5-10 lines) to the triggering pull request (omit the `item_number` parameter to auto-target the triggering PR) with: - - Test results for core tests #1-11 (✅ or ❌) - - Test results for PR review tests #12-18 (✅, ❌, or ⚠️) + - Test results for core tests #1-12 (✅ or ❌) + - Test results for PR review tests #13-19 (✅, ❌, or ⚠️) - Overall status: PASS, PARTIAL, or FAIL 3. Use the `add_comment` tool with `item_number` set to the discussion number you extracted in step 9 to add a **fun comic-book style comment** to that discussion - be playful and use comic-book language like "💥 WHOOSH!" diff --git a/pkg/constants/job_constants.go b/pkg/constants/job_constants.go index b8691dfdf37..ce0e086c079 100644 --- a/pkg/constants/job_constants.go +++ b/pkg/constants/job_constants.go @@ -110,6 +110,21 @@ const ActivationArtifactName = "activation" // that is already uploaded by the agent job. const SafeOutputItemsArtifactName = "safe-output-items" +// SarifArtifactName is the artifact name used to transfer the SARIF file generated by +// the create_code_scanning_alert handler from the safe_outputs job to the +// upload_code_scanning_sarif job. The safe_outputs job uploads the file under this name; +// the upload job downloads it and passes it to github/codeql-action/upload-sarif. +const SarifArtifactName = "code-scanning-sarif" + +// SarifArtifactDownloadPath is the local path where the upload_code_scanning_sarif job +// downloads the SARIF artifact. The file will be available at this path + the SARIF +// filename ("code-scanning-alert.sarif") after actions/download-artifact completes. +const SarifArtifactDownloadPath = "/tmp/gh-aw/sarif/" + +// SarifFileName is the name of the SARIF file generated by create_code_scanning_alert.cjs +// and uploaded / downloaded as part of the code-scanning-sarif artifact. +const SarifFileName = "code-scanning-alert.sarif" + // MCP server ID constants const SafeOutputsMCPServerID MCPServerID = "safeoutputs" diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 5708b481836..8d3d6c1ad6d 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -212,6 +212,9 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // via needs.safe_outputs.outputs.sarif_file and decide whether to run. // Also expose checkout_token so the upload job can checkout the repo without needing // access to any GitHub App credentials (which are only minted in safe_outputs). + // Additionally, upload the SARIF file as a GitHub Actions artifact so the upload job + // can retrieve the actual file (job outputs only carry the path string; the file itself + // only exists in the safe_outputs job workspace). if data.SafeOutputs.CreateCodeScanningAlerts != nil && !isHandlerStaged(c.trialMode || data.SafeOutputs.Staged, data.SafeOutputs.CreateCodeScanningAlerts.Staged) { consolidatedSafeOutputsJobLog.Print("Exposing sarif_file and checkout_token outputs for upload_code_scanning_sarif job") outputs["sarif_file"] = "${{ steps.process_safe_outputs.outputs.sarif_file }}" @@ -219,6 +222,10 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // authenticate without needing the GitHub App credentials or a step from this job. checkoutMgrForOutput := NewCheckoutManager(data.CheckoutConfigs) outputs["checkout_token"] = computeStaticCheckoutToken(data.SafeOutputs, checkoutMgrForOutput) + + // Upload the SARIF file as an artifact so the upload_code_scanning_sarif job + // (which runs in a separate, fresh workspace) can download and process it. + steps = append(steps, buildSarifArtifactUploadStep(agentArtifactPrefix)...) } // 3. Assign To Agent step (runs after handler managers) @@ -602,6 +609,30 @@ func buildSafeOutputItemsManifestUploadStep(prefix string) []string { } } +// buildSarifArtifactUploadStep builds the step that uploads the SARIF file generated by +// the create_code_scanning_alert handler as a GitHub Actions artifact. +// +// The SARIF file only exists in the safe_outputs job workspace. The dedicated +// upload_code_scanning_sarif job runs in a completely separate, fresh workspace so it +// cannot access the file via a job-output path string alone — it must download the +// artifact first. +// +// The step is conditional on the sarif_file output being non-empty (i.e. the handler +// actually produced findings), so it is skipped on clean runs. +// prefix is prepended to the artifact name for workflow_call contexts. +func buildSarifArtifactUploadStep(prefix string) []string { + return []string{ + " - name: Upload SARIF artifact\n", + " if: steps.process_safe_outputs.outputs.sarif_file != ''\n", + fmt.Sprintf(" uses: %s\n", GetActionPin("actions/upload-artifact")), + " with:\n", + fmt.Sprintf(" name: %s%s\n", prefix, constants.SarifArtifactName), + " path: ${{ steps.process_safe_outputs.outputs.sarif_file }}\n", + " if-no-files-found: error\n", + " retention-days: 1\n", + } +} + // scriptNameToHandlerName converts a script name like "post-slack-message" to a // JavaScript function name like "handlePostSlackMessage". func scriptNameToHandlerName(scriptName string) string { diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index 58937568ae7..faac24529cf 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -3,6 +3,7 @@ package workflow import ( + "path" "strings" "testing" @@ -772,8 +773,10 @@ func TestCallWorkflowOnly_UsesHandlerManagerStep(t *testing.T) { // TestCreateCodeScanningAlertUploadJob verifies that when create-code-scanning-alert is configured, // a dedicated upload_code_scanning_sarif job is created (separate from safe_outputs) and that -// the safe_outputs job exports sarif_file and checkout_token so the upload job can check whether -// to run and authenticate without needing GitHub App credentials. +// the safe_outputs job: +// - exports sarif_file and checkout_token outputs for the upload job +// - uploads the SARIF file as a GitHub Actions artifact so the upload job +// (which runs in a fresh workspace) can download it func TestCreateCodeScanningAlertUploadJob(t *testing.T) { tests := []struct { name string @@ -836,7 +839,7 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { }, } - // 1. Verify safe_outputs job exports sarif_file and checkout_token + // 1. Verify safe_outputs job exports sarif_file and checkout_token, and uploads artifact safeOutputsJob, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test-workflow.md") require.NoError(t, err, "safe_outputs job should build without error") require.NotNil(t, safeOutputsJob, "safe_outputs job should be generated") @@ -854,9 +857,18 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.Contains(t, safeOutputsJob.Outputs, "checkout_token", "safe_outputs job must export checkout_token output for the upload job") - // The upload and restore steps must NOT be in safe_outputs itself + // safe_outputs must upload the SARIF file as an artifact so the upload job + // (running in a fresh workspace) can download it + assert.Contains(t, safeOutputsSteps, constants.SarifArtifactName, + "safe_outputs job must upload the SARIF file as a GitHub Actions artifact") + assert.Contains(t, safeOutputsSteps, "Upload SARIF artifact", + "safe_outputs job must have a SARIF artifact upload step") + assert.Contains(t, safeOutputsSteps, "steps.process_safe_outputs.outputs.sarif_file != ''", + "SARIF artifact upload must be conditional on sarif_file being non-empty") + + // The SARIF upload-sarif steps must NOT be in safe_outputs itself assert.NotContains(t, safeOutputsSteps, "upload-sarif", - "SARIF upload must NOT be a step in safe_outputs job") + "SARIF codeql upload must NOT be a step in safe_outputs job") assert.NotContains(t, safeOutputsSteps, "Upload SARIF to GitHub Code Scanning", "SARIF upload step must NOT appear in safe_outputs job") @@ -890,6 +902,14 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.checkout_token", "Restore checkout step must use checkout_token from safe_outputs outputs") + // Download SARIF artifact step must be present in the upload job + assert.Contains(t, uploadSteps, "Download SARIF artifact", + "Upload job must download the SARIF artifact before uploading to Code Scanning") + assert.Contains(t, uploadSteps, constants.SarifArtifactName, + "Upload job must download the code-scanning-sarif artifact") + assert.Contains(t, uploadSteps, constants.SarifArtifactDownloadPath, + "Upload job must download artifact to the expected path") + // Upload SARIF step must be present assert.Contains(t, uploadSteps, "Upload SARIF to GitHub Code Scanning", "Upload job must have SARIF upload step") @@ -902,22 +922,29 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { "Upload step must include ref input") assert.Contains(t, uploadSteps, "sha: ${{ github.sha }}", "Upload step must include sha input") - // sarif_file must come from safe_outputs job outputs - assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.sarif_file", - "Upload step must reference sarif_file from safe_outputs job outputs") + // sarif_file must be the local path from the downloaded artifact (not a job output reference) + localSarifPath := path.Join(constants.SarifArtifactDownloadPath, constants.SarifFileName) + assert.Contains(t, uploadSteps, localSarifPath, + "Upload step must use the locally downloaded SARIF file path") + assert.NotContains(t, uploadSteps, "needs.safe_outputs.outputs.sarif_file", + "Upload step must NOT reference sarif_file from job outputs (use local artifact path instead)") // Upload-sarif uses 'token' not 'github-token' assert.Contains(t, uploadSteps, "token:", "Upload step must use 'token' input (not 'github-token')") assert.NotContains(t, uploadSteps, "github-token:", "Upload step must not use 'github-token' - upload-sarif only accepts 'token'") - // Restore step must appear before upload step + // Step ordering: restore → download → upload restorePos := strings.Index(uploadSteps, "Restore checkout to triggering commit") + downloadPos := strings.Index(uploadSteps, "Download SARIF artifact") uploadPos := strings.Index(uploadSteps, "Upload SARIF to GitHub Code Scanning") require.Greater(t, restorePos, -1, "Restore checkout step must exist") + require.Greater(t, downloadPos, -1, "Download SARIF artifact step must exist") require.Greater(t, uploadPos, -1, "Upload SARIF step must exist") - assert.Less(t, restorePos, uploadPos, - "Restore checkout must appear before SARIF upload in the job steps") + assert.Less(t, restorePos, downloadPos, + "Restore checkout must appear before SARIF download in the job steps") + assert.Less(t, downloadPos, uploadPos, + "SARIF download must appear before SARIF upload in the job steps") if tt.expectCustomToken != "" { assert.Contains(t, uploadSteps, tt.expectCustomToken, diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index 6fa0f28d3f7..26e5f9f5fee 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -2,6 +2,7 @@ package workflow import ( "fmt" + "path" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" @@ -85,6 +86,9 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) // user-configured PAT is present. restoreToken := fmt.Sprintf("${{ needs.%s.outputs.checkout_token }}", constants.SafeOutputsJobName) + // Artifact prefix for workflow_call context (so the download name matches the upload name). + agentArtifactPrefix := artifactPrefixExprForDownstreamJob(data) + var steps []string // Step 1: Restore workspace to the triggering commit. @@ -99,16 +103,29 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) steps = append(steps, " persist-credentials: false\n") steps = append(steps, " fetch-depth: 1\n") - // Step 2: Upload SARIF file to GitHub Code Scanning. - // The sarif_file path is passed from the safe_outputs job via job outputs. + // Step 2: Download the SARIF artifact produced by safe_outputs. + // The SARIF file was written to the safe_outputs job workspace and uploaded as an artifact. + // This job runs in a fresh workspace so we must download the artifact before uploading + // to GitHub Code Scanning. + sarifDownloadSteps := buildArtifactDownloadSteps(ArtifactDownloadConfig{ + ArtifactName: agentArtifactPrefix + constants.SarifArtifactName, + DownloadPath: constants.SarifArtifactDownloadPath, + StepName: "Download SARIF artifact", + }) + steps = append(steps, sarifDownloadSteps...) + + // The local SARIF file path after the artifact download completes. + localSarifPath := path.Join(constants.SarifArtifactDownloadPath, constants.SarifFileName) + + // Step 3: Upload SARIF file to GitHub Code Scanning. steps = append(steps, " - name: Upload SARIF to GitHub Code Scanning\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.UploadCodeScanningJobName)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("github/codeql-action/upload-sarif"))) steps = append(steps, " with:\n") // NOTE: github/codeql-action/upload-sarif uses 'token' as the input name, not 'github-token' c.addUploadSARIFToken(&steps, data, data.SafeOutputs.CreateCodeScanningAlerts.GitHubToken) - // sarif_file is passed from safe_outputs job output (set by create_code_scanning_alert.cjs handler) - steps = append(steps, fmt.Sprintf(" sarif_file: ${{ needs.%s.outputs.sarif_file }}\n", constants.SafeOutputsJobName)) + // sarif_file now references the locally-downloaded artifact, not the path from safe_outputs + steps = append(steps, fmt.Sprintf(" sarif_file: %s\n", localSarifPath)) // ref and sha pin the upload to the exact triggering commit regardless of local git state steps = append(steps, " ref: ${{ github.ref }}\n") steps = append(steps, " sha: ${{ github.sha }}\n") From 919b202da01392e60de1bbcb83da9eb41515fbe5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:31:47 +0000 Subject: [PATCH 08/10] fix: change smoke-claude push-to-pr allowed-files to non-protected smoke-test-files folder Agent-Logs-Url: https://github.com/github/gh-aw/sessions/94d427cd-4ccf-40bf-9761-a21ead9e4a48 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-claude.lock.yml | 77 ++++++++++++------------- .github/workflows/smoke-claude.md | 5 +- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index b4dd5602b24..3cf6b2b68c7 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"05a0c0106bd5bee59f415de4900021788daccaabb257182591d7656d4155b42f","agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"21ff673e699d808782479870bf351cdbf8f6b95415b21decc3c7721a95f0e281","agent_id":"claude"} name: "Smoke Claude" "on": @@ -201,9 +201,9 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' + cat << 'GH_AW_PROMPT_4ec99b0fd270d235_EOF' - GH_AW_PROMPT_c60a3b86c7def1cd_EOF + GH_AW_PROMPT_4ec99b0fd270d235_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" @@ -211,12 +211,12 @@ jobs: cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' + cat << 'GH_AW_PROMPT_4ec99b0fd270d235_EOF' Tools: add_comment(max:2), create_issue, close_pull_request, update_pull_request, create_pull_request_review_comment(max:5), submit_pull_request_review, resolve_pull_request_review_thread(max:5), add_labels, add_reviewer(max:2), push_to_pull_request_branch, create_code_scanning_alert, missing_tool, missing_data, noop, post_slack_message - GH_AW_PROMPT_c60a3b86c7def1cd_EOF + GH_AW_PROMPT_4ec99b0fd270d235_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' + cat << 'GH_AW_PROMPT_4ec99b0fd270d235_EOF' The following GitHub context information is available for this workflow: @@ -246,9 +246,9 @@ jobs: {{/if}} - GH_AW_PROMPT_c60a3b86c7def1cd_EOF + GH_AW_PROMPT_4ec99b0fd270d235_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_c60a3b86c7def1cd_EOF' + cat << 'GH_AW_PROMPT_4ec99b0fd270d235_EOF' @@ -588,8 +588,7 @@ jobs: - Note: May fail if reviewer is already assigned or doesn't have access 18. **Push to PR Branch Testing**: - - Create a test file at `/tmp/test-pr-push-__GH_AW_GITHUB_RUN_ID__.txt` with content "Test file for PR push" - - Use git commands to check if we're on the PR branch + - Create a test file at `smoke-test-files/smoke-claude-push-test.md` in the repository workspace with content "Smoke test push — Run __GH_AW_GITHUB_RUN_ID__" - Use the `push_to_pull_request_branch` tool to push this change - Use `pr_number: ` and `commit_message: "test: Add smoke test file"` - Verify the push succeeds @@ -632,7 +631,7 @@ jobs: {"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}} ``` - GH_AW_PROMPT_c60a3b86c7def1cd_EOF + GH_AW_PROMPT_4ec99b0fd270d235_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -905,12 +904,12 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_abf8a06446cf64d7_EOF' - {"add_comment":{"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-claude"]},"add_reviewer":{"max":2,"target":"*"},"close_pull_request":{"max":1,"staged":true},"create_code_scanning_alert":{"driver":"Smoke Claude"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-claude","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT","target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"post_slack_message":{"description":"Post a message to a fictitious Slack channel (smoke test only — no real Slack integration)","inputs":{"channel":{"default":"#general","description":"Slack channel name to post to","required":false,"type":"string"},"message":{"description":"Message text to post","required":false,"type":"string"}}},"push_to_pull_request_branch":{"allowed_files":[".github/smoke-claude-push-test.md"],"if_no_changes":"warn","max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"staged":true,"target":"*"},"resolve_pull_request_review_thread":{"max":5},"submit_pull_request_review":{"footer":"always","max":1},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_abf8a06446cf64d7_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_0e10b6117a7b2ef5_EOF' + {"add_comment":{"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-claude"]},"add_reviewer":{"max":2,"target":"*"},"close_pull_request":{"max":1,"staged":true},"create_code_scanning_alert":{"driver":"Smoke Claude"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-claude","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT","target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"post_slack_message":{"description":"Post a message to a fictitious Slack channel (smoke test only — no real Slack integration)","inputs":{"channel":{"default":"#general","description":"Slack channel name to post to","required":false,"type":"string"},"message":{"description":"Message text to post","required":false,"type":"string"}}},"push_to_pull_request_branch":{"allowed_files":["smoke-test-files/smoke-claude-push-test.md"],"if_no_changes":"warn","max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"staged":true,"target":"*"},"resolve_pull_request_review_thread":{"max":5},"submit_pull_request_review":{"footer":"always","max":1},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"target":"*"}} + GH_AW_SAFE_OUTPUTS_CONFIG_0e10b6117a7b2ef5_EOF - name: Write Safe Outputs Tools run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_21307181a707e81e_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_0fcc6ff9d80b0fb9_EOF' { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added.", @@ -947,8 +946,8 @@ jobs: } ] } - GH_AW_SAFE_OUTPUTS_TOOLS_META_21307181a707e81e_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_93fe84dc8c044f49_EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_META_0fcc6ff9d80b0fb9_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_b20132c1ca0f5de7_EOF' { "add_comment": { "defaultMax": 1, @@ -1274,7 +1273,7 @@ jobs: "customValidation": "requiresOneOf:title,body" } } - GH_AW_SAFE_OUTPUTS_VALIDATION_93fe84dc8c044f49_EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_b20132c1ca0f5de7_EOF node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config @@ -1319,7 +1318,7 @@ jobs: - name: Setup MCP Scripts Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/mcp-scripts/logs - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_08274c8fa5e4f86a_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_d6f51c172b30cd14_EOF' { "serverName": "mcpscripts", "version": "1.0.0", @@ -1471,8 +1470,8 @@ jobs: } ] } - GH_AW_MCP_SCRIPTS_TOOLS_08274c8fa5e4f86a_EOF - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_f32940a023588ab9_EOF' + GH_AW_MCP_SCRIPTS_TOOLS_d6f51c172b30cd14_EOF + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_38ef6c81187f1145_EOF' const path = require("path"); const { startHttpServer } = require("./mcp_scripts_mcp_server_http.cjs"); const configPath = path.join(__dirname, "tools.json"); @@ -1486,12 +1485,12 @@ jobs: console.error("Failed to start mcp-scripts HTTP server:", error); process.exit(1); }); - GH_AW_MCP_SCRIPTS_SERVER_f32940a023588ab9_EOF + GH_AW_MCP_SCRIPTS_SERVER_38ef6c81187f1145_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs - name: Setup MCP Scripts Tool Files run: | - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_70cb5e31f4e7f561_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh << 'GH_AW_MCP_SCRIPTS_SH_GH_2c5983231b4f4fe3_EOF' #!/bin/bash # Auto-generated mcp-script tool: gh # Execute any gh CLI command. This tool is accessible as 'mcpscripts-gh'. Provide the full command after 'gh' (e.g., args: 'pr list --limit 5'). The tool will run: gh . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1502,9 +1501,9 @@ jobs: echo " token: ${GH_AW_GH_TOKEN:0:6}..." GH_TOKEN="$GH_AW_GH_TOKEN" gh $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GH_70cb5e31f4e7f561_EOF + GH_AW_MCP_SCRIPTS_SH_GH_2c5983231b4f4fe3_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/gh.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_c43cfce700ad2129_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_76877b18b2f4e52f_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-discussion-query # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1639,9 +1638,9 @@ jobs: EOF fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_c43cfce700ad2129_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_76877b18b2f4e52f_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-discussion-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_92a544dc4c7de46d_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_3fcdce0ca0eeef3e_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-issue-query # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1720,9 +1719,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_92a544dc4c7de46d_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_3fcdce0ca0eeef3e_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-issue-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_f8ab02f7ce6a8ee5_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh << 'GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_b3f67904ec5beb3f_EOF' #!/bin/bash # Auto-generated mcp-script tool: github-pr-query # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. @@ -1807,9 +1806,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_f8ab02f7ce6a8ee5_EOF + GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_b3f67904ec5beb3f_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/github-pr-query.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/go.sh << 'GH_AW_MCP_SCRIPTS_SH_GO_f08171d96ec9497f_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/go.sh << 'GH_AW_MCP_SCRIPTS_SH_GO_4fce796bf6c344f8_EOF' #!/bin/bash # Auto-generated mcp-script tool: go # Execute any Go command. This tool is accessible as 'mcpscripts-go'. Provide the full command after 'go' (e.g., args: 'test ./...'). The tool will run: go . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1820,9 +1819,9 @@ jobs: go $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GO_f08171d96ec9497f_EOF + GH_AW_MCP_SCRIPTS_SH_GO_4fce796bf6c344f8_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/go.sh - cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/make.sh << 'GH_AW_MCP_SCRIPTS_SH_MAKE_ab6ce21101ff9338_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/make.sh << 'GH_AW_MCP_SCRIPTS_SH_MAKE_d92303afa15e5799_EOF' #!/bin/bash # Auto-generated mcp-script tool: make # Execute any Make target. This tool is accessible as 'mcpscripts-make'. Provide the target name(s) (e.g., args: 'build'). The tool will run: make . Use single quotes ' for complex args to avoid shell interpretation issues. @@ -1832,7 +1831,7 @@ jobs: echo "make $INPUT_ARGS" make $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_MAKE_ab6ce21101ff9338_EOF + GH_AW_MCP_SCRIPTS_SH_MAKE_d92303afa15e5799_EOF chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/make.sh - name: Generate MCP Scripts Server Config @@ -1905,7 +1904,7 @@ jobs: export GH_AW_ENGINE="claude" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_MCP_SCRIPTS_PORT -e GH_AW_MCP_SCRIPTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e GH_TOKEN -e TAVILY_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.12' - cat << GH_AW_MCP_CONFIG_b2fdcc7961148bf4_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_82ac77150d4b9330_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "agenticworkflows": { @@ -2044,7 +2043,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_b2fdcc7961148bf4_EOF + GH_AW_MCP_CONFIG_82ac77150d4b9330_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -2791,7 +2790,7 @@ jobs: echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Setup Safe Outputs Custom Scripts run: | - cat > ${RUNNER_TEMP}/gh-aw/actions/safe_output_script_post_slack_message.cjs << 'GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_95b4472f6a4f355b_EOF' + cat > ${RUNNER_TEMP}/gh-aw/actions/safe_output_script_post_slack_message.cjs << 'GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_22707f2b142f0f65_EOF' // @ts-check /// // Auto-generated safe-output script handler: post-slack-message @@ -2811,7 +2810,7 @@ jobs: } module.exports = { main }; - GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_95b4472f6a4f355b_EOF + GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_22707f2b142f0f65_EOF - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -2821,7 +2820,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_SCRIPTS: "{\"post_slack_message\":\"safe_output_script_post_slack_message.cjs\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"add_reviewer\":{\"max\":2,\"target\":\"*\"},\"close_pull_request\":{\"max\":1,\"staged\":true},\"create_code_scanning_alert\":{\"driver\":\"Smoke Claude\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-claude\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\",\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\".github/smoke-claude-push-test.md\"],\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"CLAUDE.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"],\"staged\":true,\"target\":\"*\"},\"resolve_pull_request_review_thread\":{\"max\":5},\"submit_pull_request_review\":{\"footer\":\"always\",\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"add_reviewer\":{\"max\":2,\"target\":\"*\"},\"close_pull_request\":{\"max\":1,\"staged\":true},\"create_code_scanning_alert\":{\"driver\":\"Smoke Claude\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-claude\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\",\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"smoke-test-files/smoke-claude-push-test.md\"],\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"CLAUDE.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"],\"staged\":true,\"target\":\"*\"},\"resolve_pull_request_review_thread\":{\"max\":5},\"submit_pull_request_review\":{\"footer\":\"always\",\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"target\":\"*\"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index d8dd76b0167..93af21f7085 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -88,7 +88,7 @@ safe-outputs: target: "*" if-no-changes: "warn" allowed-files: - - ".github/smoke-claude-push-test.md" + - "smoke-test-files/smoke-claude-push-test.md" add-reviewer: max: 2 target: "*" @@ -187,8 +187,7 @@ timeout-minutes: 10 - Note: May fail if reviewer is already assigned or doesn't have access 18. **Push to PR Branch Testing**: - - Create a test file at `/tmp/test-pr-push-${{ github.run_id }}.txt` with content "Test file for PR push" - - Use git commands to check if we're on the PR branch + - Create a test file at `smoke-test-files/smoke-claude-push-test.md` in the repository workspace with content "Smoke test push — Run ${{ github.run_id }}" - Use the `push_to_pull_request_branch` tool to push this change - Use `pr_number: ` and `commit_message: "test: Add smoke test file"` - Verify the push succeeds From c5db1f7bf3ca6f49249f02e7f46a8f8ce81361b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:32:18 +0000 Subject: [PATCH 09/10] fix: compute checkout token directly in upload job instead of passing through safe_outputs outputs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1af9a9e5-4b6d-4587-a320-595bbfe363cd Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-malicious-code-scan.lock.yml | 5 ++- .github/workflows/daily-semgrep-scan.lock.yml | 5 ++- .github/workflows/smoke-claude.lock.yml | 5 ++- pkg/workflow/compiler_safe_outputs_job.go | 11 +++---- pkg/workflow/create_code_scanning_alert.go | 32 +++++++++++-------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index bd3951e161b..5c244794b3c 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -874,7 +874,6 @@ jobs: GH_AW_WORKFLOW_ID: "daily-malicious-code-scan" GH_AW_WORKFLOW_NAME: "Daily Malicious Code Scan Agent" outputs: - checkout_token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} @@ -962,7 +961,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} - token: ${{ needs.safe_outputs.outputs.checkout_token }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false fetch-depth: 1 - name: Download SARIF artifact @@ -975,7 +974,7 @@ jobs: id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: - token: ${{ needs.safe_outputs.outputs.checkout_token }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} sarif_file: /tmp/gh-aw/sarif/code-scanning-alert.sarif ref: ${{ github.ref }} sha: ${{ github.sha }} diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index dd33a5db0f2..1dc79480d95 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -1053,7 +1053,6 @@ jobs: GH_AW_WORKFLOW_ID: "daily-semgrep-scan" GH_AW_WORKFLOW_NAME: "Daily Semgrep Scan" outputs: - checkout_token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} @@ -1141,7 +1140,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} - token: ${{ needs.safe_outputs.outputs.checkout_token }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false fetch-depth: 1 - name: Download SARIF artifact @@ -1154,7 +1153,7 @@ jobs: id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: - token: ${{ needs.safe_outputs.outputs.checkout_token }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} sarif_file: /tmp/gh-aw/sarif/code-scanning-alert.sarif ref: ${{ github.ref }} sha: ${{ github.sha }} diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 3cf6b2b68c7..5b6bc94ea0e 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2739,7 +2739,6 @@ jobs: GH_AW_WORKFLOW_NAME: "Smoke Claude" outputs: add_reviewer_reviewers_added: ${{ steps.process_safe_outputs.outputs.reviewers_added }} - checkout_token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} @@ -2904,7 +2903,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.sha }} - token: ${{ needs.safe_outputs.outputs.checkout_token }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false fetch-depth: 1 - name: Download SARIF artifact @@ -2917,7 +2916,7 @@ jobs: id: upload_code_scanning_sarif uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 with: - token: ${{ needs.safe_outputs.outputs.checkout_token }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} sarif_file: /tmp/gh-aw/sarif/code-scanning-alert.sarif ref: ${{ github.ref }} sha: ${{ github.sha }} diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 8d3d6c1ad6d..3f589834a70 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -210,18 +210,15 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // 2. SARIF output — expose sarif_file from the handler so the dedicated // upload_code_scanning_sarif job (built in buildCodeScanningUploadJob) can access it // via needs.safe_outputs.outputs.sarif_file and decide whether to run. - // Also expose checkout_token so the upload job can checkout the repo without needing - // access to any GitHub App credentials (which are only minted in safe_outputs). // Additionally, upload the SARIF file as a GitHub Actions artifact so the upload job // can retrieve the actual file (job outputs only carry the path string; the file itself // only exists in the safe_outputs job workspace). + // NOTE: We do NOT export checkout_token as a job output. GitHub Actions masks output + // values that contain secret references, so the downstream job would receive an empty + // string. The upload job computes the token directly from static secret references. if data.SafeOutputs.CreateCodeScanningAlerts != nil && !isHandlerStaged(c.trialMode || data.SafeOutputs.Staged, data.SafeOutputs.CreateCodeScanningAlerts.Staged) { - consolidatedSafeOutputsJobLog.Print("Exposing sarif_file and checkout_token outputs for upload_code_scanning_sarif job") + consolidatedSafeOutputsJobLog.Print("Exposing sarif_file output for upload_code_scanning_sarif job") outputs["sarif_file"] = "${{ steps.process_safe_outputs.outputs.sarif_file }}" - // Export a static checkout token (no step-output references) so the upload job can - // authenticate without needing the GitHub App credentials or a step from this job. - checkoutMgrForOutput := NewCheckoutManager(data.CheckoutConfigs) - outputs["checkout_token"] = computeStaticCheckoutToken(data.SafeOutputs, checkoutMgrForOutput) // Upload the SARIF file as an artifact so the upload_code_scanning_sarif job // (which runs in a separate, fresh workspace) can download and process it. diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index 26e5f9f5fee..ca85872de6d 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -79,12 +79,15 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) { createCodeScanningAlertLog.Print("Building upload_code_scanning_sarif job") - // The checkout token is passed from the safe_outputs job via its checkout_token output. - // This avoids needing GitHub App credentials in this job — the app token (if any) was - // already minted and revoked in safe_outputs, so we use the static token that safe_outputs - // exported. Falls back to the default GH_AW_GITHUB_TOKEN || GITHUB_TOKEN when no - // user-configured PAT is present. - restoreToken := fmt.Sprintf("${{ needs.%s.outputs.checkout_token }}", constants.SafeOutputsJobName) + // Compute the restore token directly in this job rather than reading it from the + // safe_outputs job's outputs. GitHub Actions masks job outputs that contain secret + // references (e.g. "${{ secrets.GITHUB_TOKEN }}"), so passing the token through + // safe_outputs.outputs.checkout_token would result in an empty string here. + // Since computeStaticCheckoutToken returns a plain static secret-reference expression + // (e.g. "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") it is safe to + // evaluate directly in any job, including this one. + checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) + restoreToken := computeStaticCheckoutToken(data.SafeOutputs, checkoutMgr) // Artifact prefix for workflow_call context (so the download name matches the upload name). agentArtifactPrefix := artifactPrefixExprForDownstreamJob(data) @@ -155,8 +158,8 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) // addUploadSARIFToken adds the 'token' input for github/codeql-action/upload-sarif. // This action uses 'token' as the input name (not 'github-token' like other GitHub Actions). // This runs inside the upload_code_scanning_sarif job (a separate job from safe_outputs), so -// the token is read from the safe_outputs job's checkout_token output rather than any step output. -// Uses precedence: config token > safe-outputs global github-token > safe_outputs.outputs.checkout_token +// the token must be computed directly in this job from static secret references. +// Uses precedence: config token > safe-outputs global github-token > default fallback func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, configToken string) { var safeOutputsToken string if data.SafeOutputs != nil { @@ -182,9 +185,12 @@ func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, conf return } - // No per-config or safe-outputs token — use the checkout_token exported from the safe_outputs job. - // This is the static token (secret reference) computed by computeStaticCheckoutToken in safe_outputs, - // which falls back to GH_AW_GITHUB_TOKEN || GITHUB_TOKEN when no user-configured PAT is present. - createCodeScanningAlertLog.Printf("Using safe_outputs.outputs.checkout_token for SARIF upload token") - *steps = append(*steps, fmt.Sprintf(" token: ${{ needs.%s.outputs.checkout_token }}\n", constants.SafeOutputsJobName)) + // No per-config or safe-outputs token — compute the static checkout token directly. + // This is the same logic as computeStaticCheckoutToken, which returns a plain static + // secret-reference expression. We cannot pass it through job outputs because GitHub Actions + // masks output values that contain secret references. + checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) + defaultToken := computeStaticCheckoutToken(data.SafeOutputs, checkoutMgr) + createCodeScanningAlertLog.Printf("Using computed static checkout token for SARIF upload token") + *steps = append(*steps, fmt.Sprintf(" token: %s\n", defaultToken)) } From e64db0e90f2625cbf75da41035d0b144ccc589a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:57:52 +0000 Subject: [PATCH 10/10] fix: support github-app checkout token minting in upload job and fix tests Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8a1bd4b1-943a-420a-95a3-69ef317b4ef5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../compiler_safe_outputs_job_test.go | 97 ++++++++++++++----- pkg/workflow/create_code_scanning_alert.go | 69 ++++++++----- 2 files changed, 117 insertions(+), 49 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index faac24529cf..fd3ae749759 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -774,25 +774,30 @@ func TestCallWorkflowOnly_UsesHandlerManagerStep(t *testing.T) { // TestCreateCodeScanningAlertUploadJob verifies that when create-code-scanning-alert is configured, // a dedicated upload_code_scanning_sarif job is created (separate from safe_outputs) and that // the safe_outputs job: -// - exports sarif_file and checkout_token outputs for the upload job +// - exports sarif_file output for the upload job // - uploads the SARIF file as a GitHub Actions artifact so the upload job // (which runs in a fresh workspace) can download it +// +// Token handling: the upload job computes tokens directly (static PAT or minted GitHub App token) +// rather than reading from safe_outputs job outputs, because GitHub Actions masks secret references +// in job outputs — "Skip output 'x' since it may contain secret". func TestCreateCodeScanningAlertUploadJob(t *testing.T) { tests := []struct { name string config *CreateCodeScanningAlertsConfig + checkoutConfigs []*CheckoutConfig expectUploadJob bool - expectCustomToken string - expectTokenFromOutputs bool // expect needs.safe_outputs.outputs.checkout_token + expectTokenInSteps string // expected token expression in upload job steps + expectAppTokenMintStep bool // expect a GitHub App token minting step in upload job safeOutputsGitHubToken string }{ { - name: "default config creates separate upload job using checkout_token from outputs", + name: "default config creates separate upload job with static token computed directly", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{}, }, - expectUploadJob: true, - expectTokenFromOutputs: true, + expectUploadJob: true, + expectTokenInSteps: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", }, { name: "custom per-config github-token is used in upload step token", @@ -801,9 +806,8 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { GitHubToken: "${{ secrets.GHAS_TOKEN }}", }, }, - expectUploadJob: true, - expectCustomToken: "${{ secrets.GHAS_TOKEN }}", - expectTokenFromOutputs: true, + expectUploadJob: true, + expectTokenInSteps: "${{ secrets.GHAS_TOKEN }}", }, { name: "safe-outputs-level github-token is used in upload step token", @@ -811,10 +815,39 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { BaseSafeOutputConfig: BaseSafeOutputConfig{}, }, expectUploadJob: true, - expectCustomToken: "${{ secrets.SO_TOKEN }}", - expectTokenFromOutputs: true, + expectTokenInSteps: "${{ secrets.SO_TOKEN }}", safeOutputsGitHubToken: "${{ secrets.SO_TOKEN }}", }, + { + name: "checkout with github-app mints a fresh app token in the upload job", + config: &CreateCodeScanningAlertsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{}, + }, + checkoutConfigs: []*CheckoutConfig{ + { + GitHubApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + }, + expectUploadJob: true, + expectTokenInSteps: "${{ steps.checkout-restore-app-token.outputs.token }}", + expectAppTokenMintStep: true, + }, + { + name: "checkout with github-token PAT uses that PAT directly in upload job", + config: &CreateCodeScanningAlertsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{}, + }, + checkoutConfigs: []*CheckoutConfig{ + { + GitHubToken: "${{ secrets.MY_CHECKOUT_PAT }}", + }, + }, + expectUploadJob: true, + expectTokenInSteps: "${{ secrets.MY_CHECKOUT_PAT }}", + }, { name: "staged mode does not create upload job", config: &CreateCodeScanningAlertsConfig{ @@ -837,9 +870,10 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { CreateCodeScanningAlerts: tt.config, GitHubToken: tt.safeOutputsGitHubToken, }, + CheckoutConfigs: tt.checkoutConfigs, } - // 1. Verify safe_outputs job exports sarif_file and checkout_token, and uploads artifact + // 1. Verify safe_outputs job exports sarif_file and uploads the artifact safeOutputsJob, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test-workflow.md") require.NoError(t, err, "safe_outputs job should build without error") require.NotNil(t, safeOutputsJob, "safe_outputs job should be generated") @@ -853,9 +887,10 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.Contains(t, safeOutputsJob.Outputs["sarif_file"], "steps.process_safe_outputs.outputs.sarif_file", "sarif_file output must reference process_safe_outputs step") - // safe_outputs must export checkout_token for the upload job to use - assert.Contains(t, safeOutputsJob.Outputs, "checkout_token", - "safe_outputs job must export checkout_token output for the upload job") + // safe_outputs must NOT export checkout_token — GitHub Actions masks secret + // references in job outputs, making them arrive empty in downstream jobs. + assert.NotContains(t, safeOutputsJob.Outputs, "checkout_token", + "safe_outputs job must NOT export checkout_token (secret refs are masked in job outputs)") // safe_outputs must upload the SARIF file as an artifact so the upload job // (running in a fresh workspace) can download it @@ -888,6 +923,11 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { uploadSteps := strings.Join(uploadJob.Steps, "") + // The upload job must NOT use needs.safe_outputs.outputs.checkout_token — it + // would arrive empty because GitHub Actions masks secret refs in job outputs. + assert.NotContains(t, uploadSteps, "needs.safe_outputs.outputs.checkout_token", + "Upload job must NOT read checkout_token from safe_outputs outputs (would be masked)") + // Restore checkout step must be present in the upload job assert.Contains(t, uploadSteps, "Restore checkout to triggering commit", "Upload job must restore workspace to triggering commit") @@ -898,9 +938,17 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.NotContains(t, uploadSteps, "git checkout ${{ github.sha }}", "Must use actions/checkout, not a raw git command") - // The restore checkout step must always use the checkout_token from safe_outputs outputs - assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.checkout_token", - "Restore checkout step must use checkout_token from safe_outputs outputs") + if tt.expectAppTokenMintStep { + // GitHub App checkout: a token minting step must appear before the restore checkout + assert.Contains(t, uploadSteps, "checkout-restore-app-token", + "Upload job must mint a GitHub App token before restoring checkout") + mintPos := strings.Index(uploadSteps, "checkout-restore-app-token") + restoreCheckoutPos := strings.Index(uploadSteps, "Restore checkout to triggering commit") + require.NotEqual(t, -1, mintPos, "App token minting step must be present in upload job steps") + require.NotEqual(t, -1, restoreCheckoutPos, "Restore checkout step must be present in upload job steps") + assert.Less(t, mintPos, restoreCheckoutPos, + "App token minting step must appear before the restore checkout step") + } // Download SARIF artifact step must be present in the upload job assert.Contains(t, uploadSteps, "Download SARIF artifact", @@ -946,16 +994,13 @@ func TestCreateCodeScanningAlertUploadJob(t *testing.T) { assert.Less(t, downloadPos, uploadPos, "SARIF download must appear before SARIF upload in the job steps") - if tt.expectCustomToken != "" { - assert.Contains(t, uploadSteps, tt.expectCustomToken, - "Upload SARIF token must use the per-config or safe-outputs github-token") - } - if tt.expectTokenFromOutputs { - assert.Contains(t, uploadSteps, "needs.safe_outputs.outputs.checkout_token", - "Upload job should use checkout_token from safe_outputs outputs") + // Verify the expected token expression appears in the upload job steps + if tt.expectTokenInSteps != "" { + assert.Contains(t, uploadSteps, tt.expectTokenInSteps, + "Upload job must use the expected token in its steps") } } else { - // staged: safe_outputs should NOT export sarif_file or checkout_token + // staged: safe_outputs should NOT export sarif_file assert.NotContains(t, safeOutputsJob.Outputs, "sarif_file", "staged mode: safe_outputs must not export sarif_file") assert.NotContains(t, safeOutputsJob.Outputs, "checkout_token", diff --git a/pkg/workflow/create_code_scanning_alert.go b/pkg/workflow/create_code_scanning_alert.go index ca85872de6d..236fc70739c 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -3,6 +3,7 @@ package workflow import ( "fmt" "path" + "strings" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" @@ -79,22 +80,41 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) { createCodeScanningAlertLog.Print("Building upload_code_scanning_sarif job") - // Compute the restore token directly in this job rather than reading it from the - // safe_outputs job's outputs. GitHub Actions masks job outputs that contain secret - // references (e.g. "${{ secrets.GITHUB_TOKEN }}"), so passing the token through - // safe_outputs.outputs.checkout_token would result in an empty string here. - // Since computeStaticCheckoutToken returns a plain static secret-reference expression - // (e.g. "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") it is safe to - // evaluate directly in any job, including this one. + // Compute the effective token for checkout/upload in this job. + // We cannot pass tokens through job outputs (GitHub Actions masks secret references). + // We must either compute the static token directly or mint a fresh GitHub App token. checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) - restoreToken := computeStaticCheckoutToken(data.SafeOutputs, checkoutMgr) + + var restoreToken string + var tokenMintSteps []string + + // Check if the default checkout uses GitHub App auth. If so, mint a fresh token + // in this job — activation/safe_outputs app tokens have expired by this point. + defaultOverride := checkoutMgr.GetDefaultCheckoutOverride() + if defaultOverride != nil && defaultOverride.githubApp != nil { + permissions := NewPermissionsContentsReadSecurityEventsWrite() + for _, step := range c.buildGitHubAppTokenMintStep(defaultOverride.githubApp, permissions, "") { + tokenMintSteps = append(tokenMintSteps, + strings.ReplaceAll(step, "id: safe-outputs-app-token", "id: checkout-restore-app-token")) + } + //nolint:gosec // G101: False positive - this is a GitHub Actions expression template, not a hardcoded credential + restoreToken = "${{ steps.checkout-restore-app-token.outputs.token }}" + } else { + // No GitHub App configured for checkout — compute a static secret reference + // directly. This is safe because secret references are evaluated in the job's own + // context (not through job outputs which would be masked by GitHub Actions). + restoreToken = computeStaticCheckoutToken(data.SafeOutputs, checkoutMgr) + } // Artifact prefix for workflow_call context (so the download name matches the upload name). agentArtifactPrefix := artifactPrefixExprForDownstreamJob(data) var steps []string - // Step 1: Restore workspace to the triggering commit. + // Prepend any token minting steps (needed when checkout uses GitHub App auth). + steps = append(steps, tokenMintSteps...) + + // Step: Restore workspace to the triggering commit. // The safe_outputs job may have checked out a different branch (e.g., the base branch for // a PR) which would leave HEAD pointing at a different commit. The SARIF upload action // requires HEAD to match the commit being scanned, otherwise it fails with "commit not found". @@ -106,7 +126,7 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) steps = append(steps, " persist-credentials: false\n") steps = append(steps, " fetch-depth: 1\n") - // Step 2: Download the SARIF artifact produced by safe_outputs. + // Step: Download the SARIF artifact produced by safe_outputs. // The SARIF file was written to the safe_outputs job workspace and uploaded as an artifact. // This job runs in a fresh workspace so we must download the artifact before uploading // to GitHub Code Scanning. @@ -120,13 +140,14 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) // The local SARIF file path after the artifact download completes. localSarifPath := path.Join(constants.SarifArtifactDownloadPath, constants.SarifFileName) - // Step 3: Upload SARIF file to GitHub Code Scanning. + // Step: Upload SARIF file to GitHub Code Scanning. steps = append(steps, " - name: Upload SARIF to GitHub Code Scanning\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.UploadCodeScanningJobName)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("github/codeql-action/upload-sarif"))) steps = append(steps, " with:\n") // NOTE: github/codeql-action/upload-sarif uses 'token' as the input name, not 'github-token' - c.addUploadSARIFToken(&steps, data, data.SafeOutputs.CreateCodeScanningAlerts.GitHubToken) + // Pass restoreToken as the fallback so GitHub App-minted tokens flow through consistently. + c.addUploadSARIFToken(&steps, data, data.SafeOutputs.CreateCodeScanningAlerts.GitHubToken, restoreToken) // sarif_file now references the locally-downloaded artifact, not the path from safe_outputs steps = append(steps, fmt.Sprintf(" sarif_file: %s\n", localSarifPath)) // ref and sha pin the upload to the exact triggering commit regardless of local git state @@ -158,9 +179,14 @@ func (c *Compiler) buildCodeScanningUploadJob(data *WorkflowData) (*Job, error) // addUploadSARIFToken adds the 'token' input for github/codeql-action/upload-sarif. // This action uses 'token' as the input name (not 'github-token' like other GitHub Actions). // This runs inside the upload_code_scanning_sarif job (a separate job from safe_outputs), so -// the token must be computed directly in this job from static secret references. -// Uses precedence: config token > safe-outputs global github-token > default fallback -func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, configToken string) { +// the token must be computed directly in this job from static secret references or a freshly +// minted GitHub App token. +// +// Token precedence: +// 1. Per-config github-token (configToken) +// 2. Safe-outputs level github-token +// 3. fallbackToken (either computeStaticCheckoutToken result or a minted app token) +func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, configToken string, fallbackToken string) { var safeOutputsToken string if data.SafeOutputs != nil { safeOutputsToken = data.SafeOutputs.GitHubToken @@ -185,12 +211,9 @@ func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, conf return } - // No per-config or safe-outputs token — compute the static checkout token directly. - // This is the same logic as computeStaticCheckoutToken, which returns a plain static - // secret-reference expression. We cannot pass it through job outputs because GitHub Actions - // masks output values that contain secret references. - checkoutMgr := NewCheckoutManager(data.CheckoutConfigs) - defaultToken := computeStaticCheckoutToken(data.SafeOutputs, checkoutMgr) - createCodeScanningAlertLog.Printf("Using computed static checkout token for SARIF upload token") - *steps = append(*steps, fmt.Sprintf(" token: %s\n", defaultToken)) + // No per-config or safe-outputs token — use the fallback token (static secret reference + // or minted GitHub App token) computed by the caller. This avoids the GitHub Actions + // behaviour of masking secret references when they are passed through job outputs. + createCodeScanningAlertLog.Printf("Using fallback token for SARIF upload token") + *steps = append(*steps, fmt.Sprintf(" token: %s\n", fallbackToken)) }