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