diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index ffcea125d0..5c244794b3 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -759,6 +759,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: @@ -931,14 +932,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 to GitHub Code Scanning - id: upload_code_scanning_sarif + - name: Upload SARIF artifact if: steps.process_safe_outputs.outputs.sarif_file != '' - uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} - wait-for-processing: true + 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 @@ -947,3 +948,35 @@ jobs: 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 }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_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: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_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/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index 903be76768..1dc79480d9 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -787,6 +787,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: @@ -1110,14 +1111,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 to GitHub Code Scanning - id: upload_code_scanning_sarif + - name: Upload SARIF artifact if: steps.process_safe_outputs.outputs.sarif_file != '' - uses: github/codeql-action/upload-sarif@0e9f55954318745b37b7933c693bc093f7336125 # v4.35.1 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - sarif_file: ${{ steps.process_safe_outputs.outputs.sarif_file }} - wait-for-processing: true + 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 @@ -1126,3 +1127,35 @@ jobs: 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 }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_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: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_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.lock.yml b/.github/workflows/smoke-claude.lock.yml index d18e8218f2..5b6bc94ea0 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":"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_9e151125965f1459_EOF' + cat << 'GH_AW_PROMPT_4ec99b0fd270d235_EOF' - GH_AW_PROMPT_9e151125965f1459_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_9e151125965f1459_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, 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_4ec99b0fd270d235_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_9e151125965f1459_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_9e151125965f1459_EOF + GH_AW_PROMPT_4ec99b0fd270d235_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9e151125965f1459_EOF' + cat << 'GH_AW_PROMPT_4ec99b0fd270d235_EOF' @@ -551,46 +551,50 @@ 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**: - - 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 + 18. **Push to PR Branch Testing**: + - 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 - 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 +607,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 +616,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 +631,7 @@ jobs: {"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}} ``` - GH_AW_PROMPT_9e151125965f1459_EOF + GH_AW_PROMPT_4ec99b0fd270d235_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -900,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_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_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_e61a50bc4dbfc828_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.", @@ -942,8 +946,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_0fcc6ff9d80b0fb9_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_b20132c1ca0f5de7_EOF' { "add_comment": { "defaultMax": 1, @@ -1019,6 +1023,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 +1273,7 @@ jobs: "customValidation": "requiresOneOf:title,body" } } - GH_AW_SAFE_OUTPUTS_VALIDATION_16c8d1ef6cbd7c54_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 @@ -1273,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_bd0753d0a6904c15_EOF' + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_d6f51c172b30cd14_EOF' { "serverName": "mcpscripts", "version": "1.0.0", @@ -1425,8 +1470,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_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"); @@ -1440,12 +1485,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_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_19c5863f6956cb95_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. @@ -1456,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_19c5863f6956cb95_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_96d28c752190963a_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. @@ -1593,9 +1638,9 @@ jobs: EOF fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-DISCUSSION-QUERY_96d28c752190963a_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_11317a0085a1ad91_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. @@ -1674,9 +1719,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-ISSUE-QUERY_11317a0085a1ad91_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_c30b3beeba855527_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. @@ -1761,9 +1806,9 @@ jobs: fi - GH_AW_MCP_SCRIPTS_SH_GITHUB-PR-QUERY_c30b3beeba855527_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_223c3ba39b6e7289_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. @@ -1774,9 +1819,9 @@ jobs: go $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_GO_223c3ba39b6e7289_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_7642664e59bacf91_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. @@ -1786,7 +1831,7 @@ jobs: echo "make $INPUT_ARGS" make $INPUT_ARGS - GH_AW_MCP_SCRIPTS_SH_MAKE_7642664e59bacf91_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 @@ -1859,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_ec38d8f15328bda6_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": { @@ -1998,7 +2043,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_ec38d8f15328bda6_EOF + GH_AW_MCP_CONFIG_82ac77150d4b9330_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -2348,6 +2393,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 +2401,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 +2505,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 +2727,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" @@ -2702,6 +2751,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 +2789,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_22707f2b142f0f65_EOF' // @ts-check /// // Auto-generated safe-output script handler: post-slack-message @@ -2759,7 +2809,7 @@ jobs: } module.exports = { main }; - GH_AW_SAFE_OUTPUT_SCRIPT_POST_SLACK_MESSAGE_d6fcf72c929f77e4_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 @@ -2769,7 +2819,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\":[\"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: | @@ -2777,6 +2827,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 +2890,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: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_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: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_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 4f1c3c3669..93af21f708 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 @@ -86,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: "*" @@ -148,46 +150,50 @@ 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**: - - 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 +18. **Push to PR Branch Testing**: + - 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 - 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 +206,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 +215,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 f2a9784432..ce0e086c07 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" @@ -109,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_output_jobs.go b/pkg/workflow/compiler_safe_output_jobs.go index 2ffcc0f014..f1d6d83c43 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 c4bff5baf3..3f589834a7 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -207,15 +207,22 @@ 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. + // 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("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 }}" + + // 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) @@ -599,6 +606,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 a404cfd89b..fd3ae74975 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" @@ -770,43 +771,91 @@ 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 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 - expectUploadStep bool - expectCustomToken string - expectDefaultToken bool + name string + config *CreateCodeScanningAlertsConfig + checkoutConfigs []*CheckoutConfig + expectUploadJob bool + 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 includes upload step with default token", + name: "default config creates separate upload job with static token computed directly", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{}, }, - expectUploadStep: true, - expectDefaultToken: true, + expectUploadJob: true, + expectTokenInSteps: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", }, { - name: "custom github-token is used in upload step", + name: "custom per-config github-token is used in upload step token", config: &CreateCodeScanningAlertsConfig{ BaseSafeOutputConfig: BaseSafeOutputConfig{ GitHubToken: "${{ secrets.GHAS_TOKEN }}", }, }, - expectUploadStep: true, - expectCustomToken: "${{ secrets.GHAS_TOKEN }}", + expectUploadJob: true, + expectTokenInSteps: "${{ secrets.GHAS_TOKEN }}", }, { - name: "staged mode does not include upload step", + name: "safe-outputs-level github-token is used in upload step token", + config: &CreateCodeScanningAlertsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{}, + }, + expectUploadJob: 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{ BaseSafeOutputConfig: BaseSafeOutputConfig{ Staged: true, }, }, - expectUploadStep: false, + expectUploadJob: false, }, } @@ -819,74 +868,143 @@ func TestCreateCodeScanningAlertIncludesSARIFUploadStep(t *testing.T) { Name: "Test Workflow", SafeOutputs: &SafeOutputsConfig{ CreateCodeScanningAlerts: tt.config, + GitHubToken: tt.safeOutputsGitHubToken, }, + CheckoutConfigs: tt.checkoutConfigs, } - 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") - // 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:")] + // 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") + + 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") + + // 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 + 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 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") + + // 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, "") + + // 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") + 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") + + 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") } - assert.Contains(t, uploadStepSection, "token:", - "Upload step should use 'token' input (not 'github-token')") - assert.NotContains(t, uploadStepSection, "github-token:", + + // 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") + 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 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'") - // 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") - - // 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") - - if tt.expectCustomToken != "" { - assert.Contains(t, stepsContent, tt.expectCustomToken, - "Upload step 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") + // 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, 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") + + // 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 { - 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") + 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 e46cd3d24a..236fc70739 100644 --- a/pkg/workflow/create_code_scanning_alert.go +++ b/pkg/workflow/create_code_scanning_alert.go @@ -2,7 +2,10 @@ package workflow import ( "fmt" + "path" + "strings" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" ) @@ -61,60 +64,156 @@ func (c *Compiler) parseCodeScanningAlertsConfig(outputMap map[string]any) *Crea return securityReportsConfig } -// buildUploadCodeScanningSARIFStep builds a step 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. This step runs after -// process_safe_outputs and uploads the SARIF file only when findings were generated -// (i.e., when the sarif_file output is non-empty). -func (c *Compiler) buildUploadCodeScanningSARIFStep(data *WorkflowData) []string { - createCodeScanningAlertLog.Print("Building SARIF upload step for code scanning alerts") +// 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. +// +// 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") + + // 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) + + 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 + // 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". + 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") + steps = append(steps, fmt.Sprintf(" token: %s\n", restoreToken)) + steps = append(steps, " persist-credentials: false\n") + steps = append(steps, " fetch-depth: 1\n") + + // 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. + 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: Upload SARIF file to GitHub Code Scanning. 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") + // 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 + 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. // 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 -func (c *Compiler) addUploadSARIFToken(steps *[]string, data *WorkflowData, configToken string) { +// 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 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 } - // 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" + 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 } - 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)) + + // 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)) } diff --git a/pkg/workflow/safe_outputs_config_helpers.go b/pkg/workflow/safe_outputs_config_helpers.go index 1b46c2db2d..869b7f81ff 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