From 68eaad46a31b1fd6680694f9a7cc1f8855436a08 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 11 Sep 2025 01:40:55 +0100 Subject: [PATCH 1/6] push to pr branch --- .../test-ai-inference-github-models.lock.yml | 8 +- .../test-claude-add-issue-comment.lock.yml | 8 +- .../test-claude-add-issue-labels.lock.yml | 8 +- .../workflows/test-claude-command.lock.yml | 8 +- .../test-claude-create-issue.lock.yml | 8 +- ...reate-pull-request-review-comment.lock.yml | 102 +++--- ...aude-create-pull-request-review-comment.md | 10 +- .../test-claude-create-pull-request.lock.yml | 18 +- ...eate-repository-security-advisory.lock.yml | 8 +- .github/workflows/test-claude-mcp.lock.yml | 8 +- .../test-claude-push-to-branch.lock.yml | 82 ++--- .../workflows/test-claude-push-to-branch.md | 12 +- .../test-claude-update-issue.lock.yml | 8 +- .../test-codex-add-issue-comment.lock.yml | 8 +- .../test-codex-add-issue-labels.lock.yml | 8 +- .github/workflows/test-codex-command.lock.yml | 8 +- .../test-codex-create-issue.lock.yml | 8 +- ...reate-pull-request-review-comment.lock.yml | 100 +++--- ...odex-create-pull-request-review-comment.md | 8 +- .../test-codex-create-pull-request.lock.yml | 18 +- ...eate-repository-security-advisory.lock.yml | 8 +- .github/workflows/test-codex-mcp.lock.yml | 8 +- .../test-codex-push-to-branch.lock.yml | 78 ++--- .../workflows/test-codex-push-to-branch.md | 12 +- .../test-codex-update-issue.lock.yml | 8 +- .../test-custom-safe-outputs.lock.yml | 50 +-- .github/workflows/test-custom-safe-outputs.md | 8 +- .github/workflows/test-proxy.lock.yml | 8 +- docs/safe-outputs.md | 20 +- e2e.sh | 300 +++++++++--------- pkg/cli/templates/instructions.md | 4 +- pkg/parser/schemas/main_workflow_schema.json | 2 +- pkg/workflow/compiler.go | 58 ++-- pkg/workflow/git_commands_integration_test.go | 12 +- pkg/workflow/git_commands_test.go | 6 +- pkg/workflow/git_patch.go | 19 +- pkg/workflow/js.go | 2 +- pkg/workflow/js/collect_ndjson_output.cjs | 8 +- ...sh_to_branch.cjs => push_to_pr_branch.cjs} | 59 +++- ...ch.test.cjs => push_to_pr_branch.test.cjs} | 21 +- pkg/workflow/output_push_to_branch.go | 21 +- pkg/workflow/output_push_to_branch_test.go | 114 +++---- schemas/agent-output.json | 2 +- 43 files changed, 635 insertions(+), 639 deletions(-) rename pkg/workflow/js/{push_to_branch.cjs => push_to_pr_branch.cjs} (83%) rename pkg/workflow/js/{push_to_branch.test.cjs => push_to_pr_branch.test.cjs} (84%) diff --git a/.github/workflows/test-ai-inference-github-models.lock.yml b/.github/workflows/test-ai-inference-github-models.lock.yml index ac23d5291b3..5beffae5ffb 100644 --- a/.github/workflows/test-ai-inference-github-models.lock.yml +++ b/.github/workflows/test-ai-inference-github-models.lock.yml @@ -675,7 +675,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -984,12 +984,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1002,7 +1002,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index 95e9d4bcb7a..64fc28f5d27 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -862,7 +862,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1171,12 +1171,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1189,7 +1189,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index f8b65fdf872..de6892a4637 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -862,7 +862,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1171,12 +1171,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1189,7 +1189,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index f7924c7c455..510286e2476 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -1035,7 +1035,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1344,12 +1344,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1362,7 +1362,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index 0fbdf6ab4d1..e6f5a41d85f 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -535,7 +535,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -844,12 +844,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -862,7 +862,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml index 49a801abf1b..9fb67065db4 100644 --- a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -3,27 +3,31 @@ # gh aw compile name: "Test Claude Create Pull Request Review Comment" -"on": +on: + issues: + types: [opened, edited, reopened] + issue_comment: + types: [created, edited] pull_request: - types: - - opened - - synchronize - - reopened + types: [opened, edited, reopened] + pull_request_review_comment: + types: [created, edited] permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" - cancel-in-progress: true + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" run-name: "Test Claude Create Pull Request Review Comment" jobs: task: - if: contains(github.event.pull_request.title, 'prr') + if: > + ((contains(github.event.issue.body, '/test-claude-create-pull-request-review-comment')) || (contains(github.event.comment.body, '/test-claude-create-pull-request-review-comment'))) || + (contains(github.event.pull_request.body, '/test-claude-create-pull-request-review-comment')) runs-on: ubuntu-latest steps: - - name: Check team membership for workflow + - name: Check team membership for command workflow id: check-team-member uses: actions/github-script@v7 env: @@ -109,6 +113,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_REACTION: eyes + GITHUB_AW_COMMAND: test-claude-create-pull-request-review-comment with: script: | async function main() { @@ -286,7 +291,10 @@ jobs: test-claude-create-pull-request-review-comment: needs: task - if: contains(github.event.pull_request.title, 'prr') + if: > + contains(github.event.issue.body, '/test-claude-create-pull-request-review-comment') || + contains(github.event.comment.body, '/test-claude-create-pull-request-review-comment') || + contains(github.event.pull_request.body, '/test-claude-create-pull-request-review-comment') runs-on: ubuntu-latest permissions: read-all outputs: @@ -457,9 +465,9 @@ jobs: run: | mkdir -p /tmp/aw-prompts cat > $GITHUB_AW_PROMPT << 'EOF' - Analyze the pull request and create a few targeted review comments on the code changes. + Analyze the pull request #${github.event.pull_request.number} and create a few targeted review comments on the code changes. - Create 2-3 review comments focusing on: + You MUST create 2 review comments focusing on: 1. Code quality and best practices 2. Potential security issues or improvements 3. Performance optimizations or concerns @@ -622,28 +630,7 @@ jobs: # Ensure log file exists touch /tmp/test-claude-create-pull-request-review-comment.log - - name: Print Agent output - env: - GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - run: | - echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````json' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY - # Ensure there's a newline after the file content if it doesn't end with one - if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - fi - echo '``````' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() - uses: actions/upload-artifact@v4 - with: - name: safe_output.jsonl - path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Ingest agent output + - name: Collect agent output id: collect_output uses: actions/github-script@v7 env: @@ -808,14 +795,14 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed case "missing-tool": return 1000; // Allow many missing tool reports (default: unlimited) case "create-repository-security-advisory": - return 1000; // Allow many repository security advisories (default: unlimited) + return 1000; // Allow many security reports (default: unlimited) default: return 1; // Default to single item for unknown types } @@ -827,15 +814,6 @@ jobs: */ function repairJson(jsonStr) { let repaired = jsonStr.trim(); - // remove invalid control characters like - // U+0014 (DC4) — represented here as "\u0014" - // Escape control characters not allowed in JSON strings (U+0000 through U+001F) - // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; - repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { - const c = ch.charCodeAt(0); - return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); - }); // Fix single quotes to double quotes (must be done first) repaired = repaired.replace(/'/g, '"'); // Fix missing quotes around object keys @@ -905,7 +883,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -1117,12 +1094,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1135,7 +1112,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1350,14 +1327,33 @@ jobs: } // Call the main function await main(); - - name: Print sanitized agent output + - name: Print agent output to step summary + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} run: | + echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````json' >> $GITHUB_STEP_SUMMARY + cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY + # Ensure there's a newline after the file content if it doesn't end with one + if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo '``````' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY echo "## Processed Output" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo '``````json' >> $GITHUB_STEP_SUMMARY echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload sanitized agent output + - name: Upload agentic output file + if: always() && steps.collect_output.outputs.output != '' + uses: actions/upload-artifact@v4 + with: + name: safe_output.jsonl + path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Upload agent output JSON if: always() && env.GITHUB_AW_AGENT_OUTPUT uses: actions/upload-artifact@v4 with: @@ -1701,7 +1697,9 @@ jobs: create_pr_review_comment: needs: test-claude-create-pull-request-review-comment - if: github.event.pull_request.number + if: > + (contains(github.event.issue.body, '/test-claude-create-pull-request-review-comment') || contains(github.event.comment.body, '/test-claude-create-pull-request-review-comment') || + contains(github.event.pull_request.body, '/test-claude-create-pull-request-review-comment')) && (github.event.pull_request.number) runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/test-claude-create-pull-request-review-comment.md b/.github/workflows/test-claude-create-pull-request-review-comment.md index cb56a9e17b2..8e19752914a 100644 --- a/.github/workflows/test-claude-create-pull-request-review-comment.md +++ b/.github/workflows/test-claude-create-pull-request-review-comment.md @@ -1,22 +1,20 @@ --- on: - pull_request: - types: [opened, synchronize, reopened] + command: + name: test-claude-create-pull-request-review-comment reaction: eyes engine: id: claude -if: contains(github.event.pull_request.title, 'prr') - safe-outputs: create-pull-request-review-comment: max: 3 --- -Analyze the pull request and create a few targeted review comments on the code changes. +Analyze the pull request #${github.event.pull_request.number} and create a few targeted review comments on the code changes. -Create 2-3 review comments focusing on: +You MUST create 2 review comments focusing on: 1. Code quality and best practices 2. Potential security issues or improvements 3. Performance optimizations or concerns diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index 7a6b618d0de..fe035b13364 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -578,7 +578,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -887,12 +887,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -905,7 +905,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1492,14 +1492,14 @@ jobs: echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi - # Extract branch from push-to-branch line using simple grep and sed - elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-branch"'; then - echo "Found push-to-branch line: $line" - # For push-to-branch, we don't extract branch from JSONL since it's configured in the workflow + # Extract branch from push-to-pr-branch line using simple grep and sed + elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then + echo "Found push-to-pr-branch line: $line" + # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-branch target: $BRANCH_NAME" + echo "Using configured push-to-pr-branch target: $BRANCH_NAME" break fi fi diff --git a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml index c3e6b5f5681..91aa290572d 100644 --- a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml @@ -797,7 +797,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1106,12 +1106,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1124,7 +1124,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index 79fc4531d15..a6ddbb67e3f 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -817,7 +817,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1126,12 +1126,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1144,7 +1144,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-claude-push-to-branch.lock.yml b/.github/workflows/test-claude-push-to-branch.lock.yml index 421dd3c6232..9f19f84efdc 100644 --- a/.github/workflows/test-claude-push-to-branch.lock.yml +++ b/.github/workflows/test-claude-push-to-branch.lock.yml @@ -23,8 +23,8 @@ run-name: "Test Claude Push to Branch" jobs: task: if: > - ((contains(github.event.issue.body, '/test-claude-push-to-branch')) || (contains(github.event.comment.body, '/test-claude-push-to-branch'))) || - (contains(github.event.pull_request.body, '/test-claude-push-to-branch')) + ((contains(github.event.issue.body, '/test-claude-push-to-pr-branch')) || (contains(github.event.comment.body, '/test-claude-push-to-pr-branch'))) || + (contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch')) runs-on: ubuntu-latest steps: - name: Check team membership for command workflow @@ -95,11 +95,11 @@ jobs: } await main(); - test-claude-push-to-branch: + test-claude-push-to-pr-branch: needs: task if: > - contains(github.event.issue.body, '/test-claude-push-to-branch') || contains(github.event.comment.body, '/test-claude-push-to-branch') || - contains(github.event.pull_request.body, '/test-claude-push-to-branch') + contains(github.event.issue.body, '/test-claude-push-to-pr-branch') || contains(github.event.comment.body, '/test-claude-push-to-pr-branch') || + contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch') runs-on: ubuntu-latest permissions: read-all outputs: @@ -277,7 +277,7 @@ jobs: cat > $GITHUB_AW_PROMPT << 'EOF' # Test Claude Push to Branch - This test workflow specifically tests multi-commit functionality in push-to-branch. + This test workflow specifically tests multi-commit functionality in push-to-pr-branch. **IMPORTANT: Create multiple separate commits for this test case** @@ -285,12 +285,12 @@ jobs: ```markdown # Claude Push-to-Branch Multi-Commit Test - This file was created by the Claude agentic workflow to test the multi-commit push-to-branch functionality. + This file was created by the Claude agentic workflow to test the multi-commit push-to-pr-branch functionality. Created at: {{ current timestamp }} ## Purpose - This test verifies that multiple commits are properly applied when using push-to-branch. + This test verifies that multiple commits are properly applied when using push-to-pr-branch. ``` 2. **Second commit**: Create a Python script called "claude-script.py" with: @@ -305,7 +305,7 @@ jobs: def main(): print("Hello from Claude agentic workflow!") print(f"Current time: {datetime.datetime.now()}") - print("This script was created to test multi-commit push-to-branch functionality.") + print("This script was created to test multi-commit push-to-pr-branch functionality.") print("This is commit #2 in the multi-commit test.") if __name__ == "__main__": @@ -332,13 +332,13 @@ jobs: 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. 3. Indicate your intention to push to the branch by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": ```json - {"type": "push-to-branch", "message": "Commit message describing the changes"} + {"type": "push-to-pr-branch", "message": "Commit message describing the changes"} ``` 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up **Example JSONL file content:** ``` - {"type": "push-to-branch", "message": "Update documentation with latest changes"} + {"type": "push-to-pr-branch", "message": "Update documentation with latest changes"} ``` **Important Notes:** @@ -481,13 +481,13 @@ jobs: run: | # Copy the detailed execution file from Agentic Action if available if [ -n "${{ steps.agentic_execution.outputs.execution_file }}" ] && [ -f "${{ steps.agentic_execution.outputs.execution_file }}" ]; then - cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-claude-push-to-branch.log + cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-claude-push-to-pr-branch.log else - echo "No execution file output found from Agentic Action" >> /tmp/test-claude-push-to-branch.log + echo "No execution file output found from Agentic Action" >> /tmp/test-claude-push-to-pr-branch.log fi # Ensure log file exists - touch /tmp/test-claude-push-to-branch.log + touch /tmp/test-claude-push-to-pr-branch.log - name: Print Agent output env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -514,7 +514,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-branch\":{\"branch\":\"triggering\",\"enabled\":true}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"branch\":\"triggering\",\"enabled\":true}}" with: script: | async function main() { @@ -674,7 +674,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -983,12 +983,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1001,7 +1001,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1244,7 +1244,7 @@ jobs: if: always() uses: actions/github-script@v7 env: - AGENT_LOG_FILE: /tmp/test-claude-push-to-branch.log + AGENT_LOG_FILE: /tmp/test-claude-push-to-pr-branch.log with: script: | function main() { @@ -1561,8 +1561,8 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-claude-push-to-branch.log - path: /tmp/test-claude-push-to-branch.log + name: test-claude-push-to-pr-branch.log + path: /tmp/test-claude-push-to-pr-branch.log if-no-files-found: warn - name: Generate git patch if: always() @@ -1589,14 +1589,14 @@ jobs: echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi - # Extract branch from push-to-branch line using simple grep and sed - elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-branch"'; then - echo "Found push-to-branch line: $line" - # For push-to-branch, we don't extract branch from JSONL since it's configured in the workflow + # Extract branch from push-to-pr-branch line using simple grep and sed + elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then + echo "Found push-to-pr-branch line: $line" + # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-branch target: $BRANCH_NAME" + echo "Using configured push-to-pr-branch target: $BRANCH_NAME" break fi fi @@ -1682,20 +1682,20 @@ jobs: path: /tmp/aw.patch if-no-files-found: ignore - push_to_branch: - needs: test-claude-push-to-branch + push_to_pr_branch: + needs: test-claude-push-to-pr-branch if: > - (contains(github.event.issue.body, '/test-claude-push-to-branch') || contains(github.event.comment.body, '/test-claude-push-to-branch') || - contains(github.event.pull_request.body, '/test-claude-push-to-branch')) && (github.event.pull_request.number) + (contains(github.event.issue.body, '/test-claude-push-to-pr-branch') || contains(github.event.comment.body, '/test-claude-push-to-pr-branch') || + contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch')) && (github.event.pull_request.number) runs-on: ubuntu-latest permissions: contents: write pull-requests: read timeout-minutes: 10 outputs: - branch_name: ${{ steps.push_to_branch.outputs.branch_name }} - commit_sha: ${{ steps.push_to_branch.outputs.commit_sha }} - push_url: ${{ steps.push_to_branch.outputs.push_url }} + branch_name: ${{ steps.push_to_pr_branch.outputs.branch_name }} + commit_sha: ${{ steps.push_to_pr_branch.outputs.commit_sha }} + push_url: ${{ steps.push_to_pr_branch.outputs.push_url }} steps: - name: Download patch artifact continue-on-error: true @@ -1713,10 +1713,10 @@ jobs: git config --global user.name "${{ github.workflow }}" echo "Git configured with standard GitHub Actions identity" - name: Push to Branch - id: push_to_branch + id: push_to_pr_branch uses: actions/github-script@v7 env: - GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-claude-push-to-branch.outputs.output }} + GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-claude-push-to-pr-branch.outputs.output }} GITHUB_AW_PUSH_BRANCH: "triggering" GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" with: @@ -1813,15 +1813,15 @@ jobs: console.log("No valid items found in agent output"); return; } - // Find the push-to-branch item + // Find the push-to-pr-branch item const pushItem = validatedOutput.items.find( - /** @param {any} item */ item => item.type === "push-to-branch" + /** @param {any} item */ item => item.type === "push-to-pr-branch" ); if (!pushItem) { - console.log("No push-to-branch item found in agent output"); + console.log("No push-to-pr-branch item found in agent output"); return; } - console.log("Found push-to-branch item"); + console.log("Found push-to-pr-branch item"); // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number @@ -1836,7 +1836,7 @@ jobs: // Check if we're in a pull request context when required if (target === "triggering" && !context.payload.pull_request) { core.setFailed( - 'push-to-branch with target "triggering" requires pull request context' + 'push-to-pr-branch with target "triggering" requires pull request context' ); return; } diff --git a/.github/workflows/test-claude-push-to-branch.md b/.github/workflows/test-claude-push-to-branch.md index 2e7e7138d62..99ab00145eb 100644 --- a/.github/workflows/test-claude-push-to-branch.md +++ b/.github/workflows/test-claude-push-to-branch.md @@ -1,18 +1,18 @@ --- on: command: - name: test-claude-push-to-branch + name: test-claude-push-to-pr-branch engine: id: claude safe-outputs: - push-to-branch: + push-to-pr-branch: --- # Test Claude Push to Branch -This test workflow specifically tests multi-commit functionality in push-to-branch. +This test workflow specifically tests multi-commit functionality in push-to-pr-branch. **IMPORTANT: Create multiple separate commits for this test case** @@ -20,12 +20,12 @@ This test workflow specifically tests multi-commit functionality in push-to-bran ```markdown # Claude Push-to-Branch Multi-Commit Test - This file was created by the Claude agentic workflow to test the multi-commit push-to-branch functionality. + This file was created by the Claude agentic workflow to test the multi-commit push-to-pr-branch functionality. Created at: {{ current timestamp }} ## Purpose - This test verifies that multiple commits are properly applied when using push-to-branch. + This test verifies that multiple commits are properly applied when using push-to-pr-branch. ``` 2. **Second commit**: Create a Python script called "claude-script.py" with: @@ -40,7 +40,7 @@ This test workflow specifically tests multi-commit functionality in push-to-bran def main(): print("Hello from Claude agentic workflow!") print(f"Current time: {datetime.datetime.now()}") - print("This script was created to test multi-commit push-to-branch functionality.") + print("This script was created to test multi-commit push-to-pr-branch functionality.") print("This is commit #2 in the multi-commit test.") if __name__ == "__main__": diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index 846c7cfb3e4..12c756a319d 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -865,7 +865,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1174,12 +1174,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1192,7 +1192,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml index 34deb864dc3..966932410c7 100644 --- a/.github/workflows/test-codex-add-issue-comment.lock.yml +++ b/.github/workflows/test-codex-add-issue-comment.lock.yml @@ -693,7 +693,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1002,12 +1002,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1020,7 +1020,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml index 9c2b8c78acf..c74f4f487c9 100644 --- a/.github/workflows/test-codex-add-issue-labels.lock.yml +++ b/.github/workflows/test-codex-add-issue-labels.lock.yml @@ -693,7 +693,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1002,12 +1002,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1020,7 +1020,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index 174bd80d932..bd052245251 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -1035,7 +1035,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1344,12 +1344,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1362,7 +1362,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml index e4753ca11a9..e98cf06c492 100644 --- a/.github/workflows/test-codex-create-issue.lock.yml +++ b/.github/workflows/test-codex-create-issue.lock.yml @@ -366,7 +366,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -675,12 +675,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -693,7 +693,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml index 663593f9520..7808e8789f8 100644 --- a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml @@ -3,27 +3,31 @@ # gh aw compile name: "Test Codex Create Pull Request Review Comment" -"on": +on: + issues: + types: [opened, edited, reopened] + issue_comment: + types: [created, edited] pull_request: - types: - - opened - - synchronize - - reopened + types: [opened, edited, reopened] + pull_request_review_comment: + types: [created, edited] permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" - cancel-in-progress: true + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" run-name: "Test Codex Create Pull Request Review Comment" jobs: task: - if: contains(github.event.pull_request.title, 'prr') + if: > + ((contains(github.event.issue.body, '/test-codex-create-pull-request-review-comment')) || (contains(github.event.comment.body, '/test-codex-create-pull-request-review-comment'))) || + (contains(github.event.pull_request.body, '/test-codex-create-pull-request-review-comment')) runs-on: ubuntu-latest steps: - - name: Check team membership for workflow + - name: Check team membership for command workflow id: check-team-member uses: actions/github-script@v7 env: @@ -109,6 +113,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_REACTION: eyes + GITHUB_AW_COMMAND: test-codex-create-pull-request-review-comment with: script: | async function main() { @@ -286,7 +291,10 @@ jobs: test-codex-create-pull-request-review-comment: needs: task - if: contains(github.event.pull_request.title, 'prr') + if: > + contains(github.event.issue.body, '/test-codex-create-pull-request-review-comment') || + contains(github.event.comment.body, '/test-codex-create-pull-request-review-comment') || + contains(github.event.pull_request.body, '/test-codex-create-pull-request-review-comment') runs-on: ubuntu-latest permissions: read-all outputs: @@ -353,7 +361,7 @@ jobs: cat > $GITHUB_AW_PROMPT << 'EOF' Analyze the pull request and create a few targeted review comments on the code changes. - Create 2-3 review comments focusing on: + You MUST create 2 review comments focusing on: 1. Code quality and best practices 2. Potential security issues or improvements 3. Performance optimizations or concerns @@ -453,28 +461,7 @@ jobs: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - - name: Print Agent output - env: - GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - run: | - echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````json' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY - # Ensure there's a newline after the file content if it doesn't end with one - if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - fi - echo '``````' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() - uses: actions/upload-artifact@v4 - with: - name: safe_output.jsonl - path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Ingest agent output + - name: Collect agent output id: collect_output uses: actions/github-script@v7 env: @@ -639,14 +626,14 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed case "missing-tool": return 1000; // Allow many missing tool reports (default: unlimited) case "create-repository-security-advisory": - return 1000; // Allow many repository security advisories (default: unlimited) + return 1000; // Allow many security reports (default: unlimited) default: return 1; // Default to single item for unknown types } @@ -658,15 +645,6 @@ jobs: */ function repairJson(jsonStr) { let repaired = jsonStr.trim(); - // remove invalid control characters like - // U+0014 (DC4) — represented here as "\u0014" - // Escape control characters not allowed in JSON strings (U+0000 through U+001F) - // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; - repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { - const c = ch.charCodeAt(0); - return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); - }); // Fix single quotes to double quotes (must be done first) repaired = repaired.replace(/'/g, '"'); // Fix missing quotes around object keys @@ -736,7 +714,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -948,12 +925,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -966,7 +943,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1181,14 +1158,33 @@ jobs: } // Call the main function await main(); - - name: Print sanitized agent output + - name: Print agent output to step summary + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} run: | + echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````json' >> $GITHUB_STEP_SUMMARY + cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY + # Ensure there's a newline after the file content if it doesn't end with one + if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo '``````' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY echo "## Processed Output" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo '``````json' >> $GITHUB_STEP_SUMMARY echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload sanitized agent output + - name: Upload agentic output file + if: always() && steps.collect_output.outputs.output != '' + uses: actions/upload-artifact@v4 + with: + name: safe_output.jsonl + path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Upload agent output JSON if: always() && env.GITHUB_AW_AGENT_OUTPUT uses: actions/upload-artifact@v4 with: @@ -1462,7 +1458,9 @@ jobs: create_pr_review_comment: needs: test-codex-create-pull-request-review-comment - if: github.event.pull_request.number + if: > + (contains(github.event.issue.body, '/test-codex-create-pull-request-review-comment') || contains(github.event.comment.body, '/test-codex-create-pull-request-review-comment') || + contains(github.event.pull_request.body, '/test-codex-create-pull-request-review-comment')) && (github.event.pull_request.number) runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/test-codex-create-pull-request-review-comment.md b/.github/workflows/test-codex-create-pull-request-review-comment.md index 4ce19f9219b..972201e2049 100644 --- a/.github/workflows/test-codex-create-pull-request-review-comment.md +++ b/.github/workflows/test-codex-create-pull-request-review-comment.md @@ -1,14 +1,12 @@ --- on: - pull_request: - types: [opened, synchronize, reopened] + command: + name: test-codex-create-pull-request-review-comment reaction: eyes engine: id: codex -if: contains(github.event.pull_request.title, 'prr') - safe-outputs: create-pull-request-review-comment: max: 3 @@ -16,7 +14,7 @@ safe-outputs: Analyze the pull request and create a few targeted review comments on the code changes. -Create 2-3 review comments focusing on: +You MUST create 2 review comments focusing on: 1. Code quality and best practices 2. Potential security issues or improvements 3. Performance optimizations or concerns diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml index 23038b6b589..63d04ace022 100644 --- a/.github/workflows/test-codex-create-pull-request.lock.yml +++ b/.github/workflows/test-codex-create-pull-request.lock.yml @@ -432,7 +432,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -741,12 +741,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -759,7 +759,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1276,14 +1276,14 @@ jobs: echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi - # Extract branch from push-to-branch line using simple grep and sed - elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-branch"'; then - echo "Found push-to-branch line: $line" - # For push-to-branch, we don't extract branch from JSONL since it's configured in the workflow + # Extract branch from push-to-pr-branch line using simple grep and sed + elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then + echo "Found push-to-pr-branch line: $line" + # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-branch target: $BRANCH_NAME" + echo "Using configured push-to-pr-branch target: $BRANCH_NAME" break fi fi diff --git a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml index 78c13da76c8..1e2579c9ce6 100644 --- a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml @@ -628,7 +628,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -937,12 +937,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -955,7 +955,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml index 91fa57b1a8d..b88570eab05 100644 --- a/.github/workflows/test-codex-mcp.lock.yml +++ b/.github/workflows/test-codex-mcp.lock.yml @@ -645,7 +645,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -954,12 +954,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -972,7 +972,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-codex-push-to-branch.lock.yml b/.github/workflows/test-codex-push-to-branch.lock.yml index 6f605a4621c..2af7002cfac 100644 --- a/.github/workflows/test-codex-push-to-branch.lock.yml +++ b/.github/workflows/test-codex-push-to-branch.lock.yml @@ -23,8 +23,8 @@ run-name: "Test Codex Push to Branch" jobs: task: if: > - ((contains(github.event.issue.body, '/test-codex-push-to-branch')) || (contains(github.event.comment.body, '/test-codex-push-to-branch'))) || - (contains(github.event.pull_request.body, '/test-codex-push-to-branch')) + ((contains(github.event.issue.body, '/test-codex-push-to-pr-branch')) || (contains(github.event.comment.body, '/test-codex-push-to-pr-branch'))) || + (contains(github.event.pull_request.body, '/test-codex-push-to-pr-branch')) runs-on: ubuntu-latest steps: - name: Check team membership for command workflow @@ -95,11 +95,11 @@ jobs: } await main(); - test-codex-push-to-branch: + test-codex-push-to-pr-branch: needs: task if: > - contains(github.event.issue.body, '/test-codex-push-to-branch') || contains(github.event.comment.body, '/test-codex-push-to-branch') || - contains(github.event.pull_request.body, '/test-codex-push-to-branch') + contains(github.event.issue.body, '/test-codex-push-to-pr-branch') || contains(github.event.comment.body, '/test-codex-push-to-pr-branch') || + contains(github.event.pull_request.body, '/test-codex-push-to-pr-branch') runs-on: ubuntu-latest permissions: read-all outputs: @@ -171,7 +171,7 @@ jobs: cat > $GITHUB_AW_PROMPT << 'EOF' # Test Codex Push to Branch - This test workflow specifically tests multi-commit functionality in push-to-branch. + This test workflow specifically tests multi-commit functionality in push-to-pr-branch. **IMPORTANT: Create multiple separate commits for this test case** @@ -179,12 +179,12 @@ jobs: ```markdown # Codex Push-to-Branch Multi-Commit Test - This file was created by the Codex agentic workflow to test the multi-commit push-to-branch functionality. + This file was created by the Codex agentic workflow to test the multi-commit push-to-pr-branch functionality. Created at: {{ current timestamp }} ## Purpose - This test verifies that multiple commits are properly applied when using push-to-branch. + This test verifies that multiple commits are properly applied when using push-to-pr-branch. ``` 2. **Second commit**: Create a Python script called "codex-script.py" with: @@ -199,7 +199,7 @@ jobs: def main(): print("Hello from Codex agentic workflow!") print(f"Current time: {datetime.datetime.now()}") - print("This script was created to test multi-commit push-to-branch functionality.") + print("This script was created to test multi-commit push-to-pr-branch functionality.") print("This is commit #2 in the multi-commit test.") if __name__ == "__main__": @@ -226,13 +226,13 @@ jobs: 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. 3. Indicate your intention to push to the branch by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": ```json - {"type": "push-to-branch", "message": "Commit message describing the changes"} + {"type": "push-to-pr-branch", "message": "Commit message describing the changes"} ``` 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up **Example JSONL file content:** ``` - {"type": "push-to-branch", "message": "Update documentation with latest changes"} + {"type": "push-to-pr-branch", "message": "Update documentation with latest changes"} ``` **Important Notes:** @@ -301,7 +301,7 @@ jobs: # Run codex with log capture - pipefail ensures codex exit code is preserved codex exec \ -c model=o4-mini \ - --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-push-to-branch.log + --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-push-to-pr-branch.log env: GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -333,7 +333,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-branch\":{\"branch\":\"triggering\",\"enabled\":true}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"branch\":\"triggering\",\"enabled\":true}}" with: script: | async function main() { @@ -493,7 +493,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -802,12 +802,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -820,7 +820,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1053,7 +1053,7 @@ jobs: if: always() uses: actions/github-script@v7 env: - AGENT_LOG_FILE: /tmp/test-codex-push-to-branch.log + AGENT_LOG_FILE: /tmp/test-codex-push-to-pr-branch.log with: script: | function main() { @@ -1310,8 +1310,8 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-codex-push-to-branch.log - path: /tmp/test-codex-push-to-branch.log + name: test-codex-push-to-pr-branch.log + path: /tmp/test-codex-push-to-pr-branch.log if-no-files-found: warn - name: Generate git patch if: always() @@ -1338,14 +1338,14 @@ jobs: echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi - # Extract branch from push-to-branch line using simple grep and sed - elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-branch"'; then - echo "Found push-to-branch line: $line" - # For push-to-branch, we don't extract branch from JSONL since it's configured in the workflow + # Extract branch from push-to-pr-branch line using simple grep and sed + elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then + echo "Found push-to-pr-branch line: $line" + # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-branch target: $BRANCH_NAME" + echo "Using configured push-to-pr-branch target: $BRANCH_NAME" break fi fi @@ -1431,20 +1431,20 @@ jobs: path: /tmp/aw.patch if-no-files-found: ignore - push_to_branch: - needs: test-codex-push-to-branch + push_to_pr_branch: + needs: test-codex-push-to-pr-branch if: > - (contains(github.event.issue.body, '/test-codex-push-to-branch') || contains(github.event.comment.body, '/test-codex-push-to-branch') || - contains(github.event.pull_request.body, '/test-codex-push-to-branch')) && (github.event.pull_request.number) + (contains(github.event.issue.body, '/test-codex-push-to-pr-branch') || contains(github.event.comment.body, '/test-codex-push-to-pr-branch') || + contains(github.event.pull_request.body, '/test-codex-push-to-pr-branch')) && (github.event.pull_request.number) runs-on: ubuntu-latest permissions: contents: write pull-requests: read timeout-minutes: 10 outputs: - branch_name: ${{ steps.push_to_branch.outputs.branch_name }} - commit_sha: ${{ steps.push_to_branch.outputs.commit_sha }} - push_url: ${{ steps.push_to_branch.outputs.push_url }} + branch_name: ${{ steps.push_to_pr_branch.outputs.branch_name }} + commit_sha: ${{ steps.push_to_pr_branch.outputs.commit_sha }} + push_url: ${{ steps.push_to_pr_branch.outputs.push_url }} steps: - name: Download patch artifact continue-on-error: true @@ -1462,10 +1462,10 @@ jobs: git config --global user.name "${{ github.workflow }}" echo "Git configured with standard GitHub Actions identity" - name: Push to Branch - id: push_to_branch + id: push_to_pr_branch uses: actions/github-script@v7 env: - GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-codex-push-to-branch.outputs.output }} + GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-codex-push-to-pr-branch.outputs.output }} GITHUB_AW_PUSH_BRANCH: "triggering" GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" with: @@ -1562,15 +1562,15 @@ jobs: console.log("No valid items found in agent output"); return; } - // Find the push-to-branch item + // Find the push-to-pr-branch item const pushItem = validatedOutput.items.find( - /** @param {any} item */ item => item.type === "push-to-branch" + /** @param {any} item */ item => item.type === "push-to-pr-branch" ); if (!pushItem) { - console.log("No push-to-branch item found in agent output"); + console.log("No push-to-pr-branch item found in agent output"); return; } - console.log("Found push-to-branch item"); + console.log("Found push-to-pr-branch item"); // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number @@ -1585,7 +1585,7 @@ jobs: // Check if we're in a pull request context when required if (target === "triggering" && !context.payload.pull_request) { core.setFailed( - 'push-to-branch with target "triggering" requires pull request context' + 'push-to-pr-branch with target "triggering" requires pull request context' ); return; } diff --git a/.github/workflows/test-codex-push-to-branch.md b/.github/workflows/test-codex-push-to-branch.md index 261bf6be556..faf946b90db 100644 --- a/.github/workflows/test-codex-push-to-branch.md +++ b/.github/workflows/test-codex-push-to-branch.md @@ -1,18 +1,18 @@ --- on: command: - name: test-codex-push-to-branch + name: test-codex-push-to-pr-branch engine: id: codex safe-outputs: - push-to-branch: + push-to-pr-branch: --- # Test Codex Push to Branch -This test workflow specifically tests multi-commit functionality in push-to-branch. +This test workflow specifically tests multi-commit functionality in push-to-pr-branch. **IMPORTANT: Create multiple separate commits for this test case** @@ -20,12 +20,12 @@ This test workflow specifically tests multi-commit functionality in push-to-bran ```markdown # Codex Push-to-Branch Multi-Commit Test - This file was created by the Codex agentic workflow to test the multi-commit push-to-branch functionality. + This file was created by the Codex agentic workflow to test the multi-commit push-to-pr-branch functionality. Created at: {{ current timestamp }} ## Purpose - This test verifies that multiple commits are properly applied when using push-to-branch. + This test verifies that multiple commits are properly applied when using push-to-pr-branch. ``` 2. **Second commit**: Create a Python script called "codex-script.py" with: @@ -40,7 +40,7 @@ This test workflow specifically tests multi-commit functionality in push-to-bran def main(): print("Hello from Codex agentic workflow!") print(f"Current time: {datetime.datetime.now()}") - print("This script was created to test multi-commit push-to-branch functionality.") + print("This script was created to test multi-commit push-to-pr-branch functionality.") print("This is commit #2 in the multi-commit test.") if __name__ == "__main__": diff --git a/.github/workflows/test-codex-update-issue.lock.yml b/.github/workflows/test-codex-update-issue.lock.yml index eacb48cf426..8de6d029232 100644 --- a/.github/workflows/test-codex-update-issue.lock.yml +++ b/.github/workflows/test-codex-update-issue.lock.yml @@ -696,7 +696,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1005,12 +1005,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1023,7 +1023,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/.github/workflows/test-custom-safe-outputs.lock.yml b/.github/workflows/test-custom-safe-outputs.lock.yml index 9e40737666b..254500fdedc 100644 --- a/.github/workflows/test-custom-safe-outputs.lock.yml +++ b/.github/workflows/test-custom-safe-outputs.lock.yml @@ -95,7 +95,7 @@ jobs: - **create-pull-request**: Creates PRs with code changes - **add-issue-label**: Adds labels to issues/PRs - **update-issue**: Updates issue properties - - **push-to-branch**: Pushes changes to branches + - **push-to-pr-branch**: Pushes changes to branches - **missing-tool**: Reports missing functionality (test simulation) - **create-discussion**: Creates repository discussions - **create-pull-request-review-comment**: Creates PR review comments @@ -180,7 +180,7 @@ jobs: 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. 3. Indicate your intention to push to the branch by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": ```json - {"type": "push-to-branch", "message": "Commit message describing the changes"} + {"type": "push-to-pr-branch", "message": "Commit message describing the changes"} ``` 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up @@ -214,7 +214,7 @@ jobs: {"type": "add-issue-comment", "body": "This is related to the issue above."} {"type": "create-pull-request", "title": "Fix typo", "body": "Corrected spelling mistake in documentation"} {"type": "add-issue-label", "labels": ["bug", "priority-high"]} - {"type": "push-to-branch", "message": "Update documentation with latest changes"} + {"type": "push-to-pr-branch", "message": "Update documentation with latest changes"} {"type": "create-repository-security-advisory", "file": "src/auth.js", "line": 25, "severity": "error", "message": "Potential SQL injection vulnerability"} {"type": "missing-tool", "tool": "docker", "reason": "Need Docker to build container images", "alternatives": "Could use GitHub Actions build instead"} ``` @@ -325,10 +325,10 @@ jobs: run: | # Create another test file for branch push echo "# Branch Push Test File" > branch-push-test-$(date +%Y%m%d-%H%M%S).md - echo "This file tests the push-to-branch safe output functionality." >> branch-push-test-$(date +%Y%m%d-%H%M%S).md + echo "This file tests the push-to-pr-branch safe output functionality." >> branch-push-test-$(date +%Y%m%d-%H%M%S).md echo "Created by custom engine at: $(date)" >> branch-push-test-$(date +%Y%m%d-%H%M%S).md - echo '{"type": "push-to-branch", "message": "Custom engine test: Push to branch functionality\n\nThis commit was generated by the test-safe-outputs-custom-engine workflow to validate the push-to-branch safe output functionality.\n\nFiles created:\n- branch-push-test-[timestamp].md\n\nTest executed at: '"$(date)"'"}' >> $GITHUB_AW_SAFE_OUTPUTS + echo '{"type": "push-to-pr-branch", "message": "Custom engine test: Push to branch functionality\n\nThis commit was generated by the test-safe-outputs-custom-engine workflow to validate the push-to-pr-branch safe output functionality.\n\nFiles created:\n- branch-push-test-[timestamp].md\n\nTest executed at: '"$(date)"'"}' >> $GITHUB_AW_SAFE_OUTPUTS env: GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -388,7 +388,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"add-issue-label\":true,\"create-discussion\":{\"enabled\":true,\"max\":1},\"create-issue\":true,\"create-pull-request\":true,\"create-pull-request-review-comment\":{\"enabled\":true,\"max\":1},\"create-repository-security-advisory\":{\"enabled\":true,\"max\":5},\"missing-tool\":{\"enabled\":true,\"max\":5},\"push-to-branch\":{\"branch\":\"triggering\",\"enabled\":true,\"target\":\"*\"},\"update-issue\":true}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"add-issue-label\":true,\"create-discussion\":{\"enabled\":true,\"max\":1},\"create-issue\":true,\"create-pull-request\":true,\"create-pull-request-review-comment\":{\"enabled\":true,\"max\":1},\"create-repository-security-advisory\":{\"enabled\":true,\"max\":5},\"missing-tool\":{\"enabled\":true,\"max\":5},\"push-to-pr-branch\":{\"branch\":\"triggering\",\"enabled\":true,\"target\":\"*\"},\"update-issue\":true}" with: script: | async function main() { @@ -548,7 +548,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -857,12 +857,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -875,7 +875,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } @@ -1136,14 +1136,14 @@ jobs: echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi - # Extract branch from push-to-branch line using simple grep and sed - elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-branch"'; then - echo "Found push-to-branch line: $line" - # For push-to-branch, we don't extract branch from JSONL since it's configured in the workflow + # Extract branch from push-to-pr-branch line using simple grep and sed + elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then + echo "Found push-to-pr-branch line: $line" + # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-branch target: $BRANCH_NAME" + echo "Using configured push-to-pr-branch target: $BRANCH_NAME" break fi fi @@ -3093,7 +3093,7 @@ jobs: } await main(); - push_to_branch: + push_to_pr_branch: needs: test-safe-outputs-custom-engine if: always() runs-on: ubuntu-latest @@ -3102,9 +3102,9 @@ jobs: pull-requests: read timeout-minutes: 10 outputs: - branch_name: ${{ steps.push_to_branch.outputs.branch_name }} - commit_sha: ${{ steps.push_to_branch.outputs.commit_sha }} - push_url: ${{ steps.push_to_branch.outputs.push_url }} + branch_name: ${{ steps.push_to_pr_branch.outputs.branch_name }} + commit_sha: ${{ steps.push_to_pr_branch.outputs.commit_sha }} + push_url: ${{ steps.push_to_pr_branch.outputs.push_url }} steps: - name: Download patch artifact continue-on-error: true @@ -3122,7 +3122,7 @@ jobs: git config --global user.name "${{ github.workflow }}" echo "Git configured with standard GitHub Actions identity" - name: Push to Branch - id: push_to_branch + id: push_to_pr_branch uses: actions/github-script@v7 env: GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-safe-outputs-custom-engine.outputs.output }} @@ -3223,15 +3223,15 @@ jobs: console.log("No valid items found in agent output"); return; } - // Find the push-to-branch item + // Find the push-to-pr-branch item const pushItem = validatedOutput.items.find( - /** @param {any} item */ item => item.type === "push-to-branch" + /** @param {any} item */ item => item.type === "push-to-pr-branch" ); if (!pushItem) { - console.log("No push-to-branch item found in agent output"); + console.log("No push-to-pr-branch item found in agent output"); return; } - console.log("Found push-to-branch item"); + console.log("Found push-to-pr-branch item"); // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number @@ -3246,7 +3246,7 @@ jobs: // Check if we're in a pull request context when required if (target === "triggering" && !context.payload.pull_request) { core.setFailed( - 'push-to-branch with target "triggering" requires pull request context' + 'push-to-pr-branch with target "triggering" requires pull request context' ); return; } diff --git a/.github/workflows/test-custom-safe-outputs.md b/.github/workflows/test-custom-safe-outputs.md index 0c16df25527..f5ea8926eb3 100644 --- a/.github/workflows/test-custom-safe-outputs.md +++ b/.github/workflows/test-custom-safe-outputs.md @@ -31,7 +31,7 @@ safe-outputs: body: target: "*" max: 1 - push-to-branch: + push-to-pr-branch: target: "*" missing-tool: max: 5 @@ -85,10 +85,10 @@ engine: run: | # Create another test file for branch push echo "# Branch Push Test File" > branch-push-test-$(date +%Y%m%d-%H%M%S).md - echo "This file tests the push-to-branch safe output functionality." >> branch-push-test-$(date +%Y%m%d-%H%M%S).md + echo "This file tests the push-to-pr-branch safe output functionality." >> branch-push-test-$(date +%Y%m%d-%H%M%S).md echo "Created by custom engine at: $(date)" >> branch-push-test-$(date +%Y%m%d-%H%M%S).md - echo '{"type": "push-to-branch", "message": "Custom engine test: Push to branch functionality\n\nThis commit was generated by the test-safe-outputs-custom-engine workflow to validate the push-to-branch safe output functionality.\n\nFiles created:\n- branch-push-test-[timestamp].md\n\nTest executed at: '"$(date)"'"}' >> $GITHUB_AW_SAFE_OUTPUTS + echo '{"type": "push-to-pr-branch", "message": "Custom engine test: Push to branch functionality\n\nThis commit was generated by the test-safe-outputs-custom-engine workflow to validate the push-to-pr-branch safe output functionality.\n\nFiles created:\n- branch-push-test-[timestamp].md\n\nTest executed at: '"$(date)"'"}' >> $GITHUB_AW_SAFE_OUTPUTS - name: Generate Missing Tool Output run: | @@ -126,7 +126,7 @@ This is a comprehensive test workflow that exercises every available safe output - **create-pull-request**: Creates PRs with code changes - **add-issue-label**: Adds labels to issues/PRs - **update-issue**: Updates issue properties -- **push-to-branch**: Pushes changes to branches +- **push-to-pr-branch**: Pushes changes to branches - **missing-tool**: Reports missing functionality (test simulation) - **create-discussion**: Creates repository discussions - **create-pull-request-review-comment**: Creates PR review comments diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index 1d7bec7fee8..d8354eabff4 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -778,7 +778,7 @@ jobs: return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -1087,12 +1087,12 @@ jobs: } } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -1105,7 +1105,7 @@ jobs: typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/docs/safe-outputs.md b/docs/safe-outputs.md index 44925010565..c6e4365c1aa 100644 --- a/docs/safe-outputs.md +++ b/docs/safe-outputs.md @@ -13,7 +13,7 @@ One of the primary security features of GitHub Agentic Workflows is "safe output | **Repository Security Advisories** | `create-repository-security-advisory:` | Generate SARIF repository security advisories and upload to GitHub Code Scanning | unlimited | | **Label Addition** | `add-issue-label:` | Add labels to issues or pull requests | 3 | | **Issue Updates** | `update-issue:` | Update issue status, title, or body | 1 | -| **Push to Branch** | `push-to-branch:` | Push changes directly to a branch | 1 | +| **Push to Branch** | `push-to-pr-branch:` | Push changes directly to a branch | 1 | | **Missing Tool Reporting** | `missing-tool:` | Report missing tools or functionality needed to complete tasks | unlimited | ## Overview (`safe-outputs:`) @@ -377,20 +377,20 @@ Update the issue based on your analysis. You can change the title, body content, - Update count is limited by `max` setting (default: 1) - Only GitHub's `issues.update` API endpoint is used -### Push to Branch (`push-to-branch:`) +### Push to Pull Request Branch (`push-to-pr-branch:`) -Adding `push-to-branch:` to the `safe-outputs:` section declares that the workflow should conclude with pushing changes to a specific branch based on the agentic workflow's output. This is useful for applying code changes directly to a designated branch within pull requests. +Adding `push-to-pr-branch:` to the `safe-outputs:` section declares that the workflow should conclude with pushing additional changes to the branch associated with a pull request. This is useful for applying code changes directly to a designated branch within pull requests. **Basic Configuration:** ```yaml safe-outputs: - push-to-branch: + push-to-pr-branch: ``` **With Configuration:** ```yaml safe-outputs: - push-to-branch: + push-to-pr-branch: target: "*" # Optional: target for push operations # "triggering" (default) - only push in triggering PR context # "*" - allow pushes to any pull request (requires pull_request_number in agent output) @@ -419,7 +419,7 @@ Analyze the pull request and make necessary code improvements. ```yaml # Always succeed, warn when no changes (default behavior) safe-outputs: - push-to-branch: + push-to-pr-branch: branch: feature-branch if-no-changes: "warn" ``` @@ -427,7 +427,7 @@ safe-outputs: ```yaml # Fail when no changes are made (strict mode) safe-outputs: - push-to-branch: + push-to-pr-branch: branch: feature-branch if-no-changes: "error" ``` @@ -435,7 +435,7 @@ safe-outputs: ```yaml # Silent success, no output when no changes safe-outputs: - push-to-branch: + push-to-pr-branch: branch: feature-branch if-no-changes: "ignore" ``` @@ -465,7 +465,7 @@ Similar to GitHub's `actions/upload-artifact` action, you can configure how the - Label count is limited by `max` setting (default: 3) - exceeding this limit causes job failure - Only GitHub's `issues.addLabels` API endpoint is used (no removal endpoints) -When `create-pull-request` or `push-to-branch` are enabled in the `safe-outputs` configuration, the system automatically adds the following additional Claude tools to enable file editing and pull request creation: +When `create-pull-request` or `push-to-pr-branch` are enabled in the `safe-outputs` configuration, the system automatically adds the following additional Claude tools to enable file editing and pull request creation: ### Missing Tool Reporting (`missing-tool:`) @@ -506,7 +506,7 @@ The compiled workflow will have additional prompting describing that, to report ## Automatically Added Tools -When `create-pull-request` or `push-to-branch` are configured, these Claude tools are automatically added: +When `create-pull-request` or `push-to-pr-branch` are configured, these Claude tools are automatically added: - **Edit**: Allows editing existing files - **MultiEdit**: Allows making multiple edits to files in a single operation diff --git a/e2e.sh b/e2e.sh index 01048c1b036..50aa906ca5d 100755 --- a/e2e.sh +++ b/e2e.sh @@ -24,7 +24,6 @@ # --workflow-dispatch-only Run only workflow_dispatch triggered tests # --issue-triggered-only Run only issue-triggered tests # --command-triggered-only Run only command-triggered tests -# --pr-triggered-only Run only PR-triggered tests # --dry-run Show what would be tested without running # --help, -h Show help message # @@ -138,7 +137,6 @@ get_all_test_names() { get_workflow_dispatch_tests get_issue_triggered_tests get_command_triggered_tests - get_pr_triggered_tests } get_workflow_dispatch_tests() { @@ -165,11 +163,8 @@ get_issue_triggered_tests() { get_command_triggered_tests() { echo "test-claude-command" echo "test-codex-command" - echo "test-claude-push-to-branch" - echo "test-codex-push-to-branch" -} - -get_pr_triggered_tests() { + echo "test-claude-push-to-pr-branch" + echo "test-codex-push-to-pr-branch" echo "test-claude-create-pull-request-review-comment" echo "test-codex-create-pull-request-review-comment" } @@ -190,9 +185,6 @@ filter_tests_by_patterns() { "command-triggered") all_tests=($(get_command_triggered_tests)) ;; - "pr-triggered") - all_tests=($(get_pr_triggered_tests)) - ;; *) error "Unknown test type: $test_type" return 1 @@ -435,20 +427,42 @@ create_test_pr() { fi } -create_test_branch() { - local branch_name="$1" - local test_file_content="$2" +create_test_pr_with_branch() { + local title="$1" + local body="$2" + local branch="test-pr-$(date +%s)" - # Delete the branch if it already exists to ensure clean test - git push origin --delete "$branch_name" &>/dev/null || true + # Create a remote branch from main without changing local git state + git push origin "main:$branch" &>/dev/null - # Create a new branch from main without changing local git state - git push origin "main:$branch_name" &>/dev/null + # Create a commit on the remote branch using GitHub API to make it different from main + local commit_message="Test commit for PR" + local file_content="# Test PR Content\n\nThis is a test file created for PR testing at $(date)" + local file_path="test-file-$(date +%s).md" - # Get the initial SHA from the remote branch - local initial_sha=$(git ls-remote --heads origin "$branch_name" 2>/dev/null | cut -f1) + # Get the current SHA of the branch + local current_sha=$(git ls-remote --heads origin "$branch" 2>/dev/null | cut -f1) - echo "$initial_sha" + if [[ -n "$current_sha" ]]; then + # Create a new file on the branch using GitHub API + gh api repos/:owner/:repo/contents/"$file_path" \ + --method PUT \ + --field message="$commit_message" \ + --field content="$(echo -e "$file_content" | base64 -w 0)" \ + --field branch="$branch" &>/dev/null + + # Create a PR using the GitHub CLI + local pr_url=$(gh pr create --title "$title" --body "$body" --head "$branch" --base main 2>/dev/null) + + if [[ -n "$pr_url" ]]; then + local pr_number=$(echo "$pr_url" | grep -o '[0-9]\+$') + echo "$pr_number,$branch,$current_sha" + else + echo "" + fi + else + echo "" + fi } post_issue_command() { @@ -458,6 +472,13 @@ post_issue_command() { gh issue comment "$issue_number" --body "$command" &>/dev/null } +post_pr_command() { + local pr_number="$1" + local command="$2" + + gh pr comment "$pr_number" --body "$command" &>/dev/null +} + validate_issue_created() { local title_prefix="$1" local expected_labels="$2" @@ -708,6 +729,35 @@ validate_branch_updated() { fi } +validate_pr_review_comments() { + local pr_number="$1" + local ai_type="$2" # "Claude" or "Codex" + + # Get PR review comments using GitHub CLI (these are line-specific review comments) + local review_comments=$(gh api repos/:owner/:repo/pulls/"$pr_number"/reviews 2>/dev/null | jq -r '.[].body // empty' 2>/dev/null || echo "") + + # Also check for PR comments (general comments on the PR) + local pr_comments=$(gh pr view "$pr_number" --json comments --jq '.comments[]?.body // empty' 2>/dev/null || echo "") + + # Combine both types of comments for validation + local all_comments="$review_comments $pr_comments" + + if [[ -n "$all_comments" && "$all_comments" != " " ]]; then + # Check if any comment contains AI-specific content or expected patterns + if echo "$all_comments" | grep -qi "$ai_type\|review\|test\|automated\|workflow"; then + success "PR #$pr_number has review/comments (likely from $ai_type AI workflow)" + return 0 + else + # Accept any non-empty comments as success since AI might generate varied content + success "PR #$pr_number has review/comments (content validation passed)" + return 0 + fi + else + warning "(polling) PR #$pr_number missing expected review comments from $ai_type" + return 1 + fi +} + # Polling functions for workflow validation wait_for_issue_comment() { local issue_number="$1" @@ -814,6 +864,27 @@ wait_for_branch_update() { return 1 } +wait_for_pr_review_comments() { + local pr_number="$1" + local ai_type="$2" + local test_name="$3" + local max_wait=240 # Max wait time in seconds (4 minutes) + local waited=0 + + while [[ $waited -lt $max_wait ]]; do + if validate_pr_review_comments "$pr_number" "$ai_type"; then + PASSED_TESTS+=("$test_name") + return 0 + fi + info "..." + sleep 5 + waited=$((waited + 5)) + done + + FAILED_TESTS+=("$test_name") + return 1 +} + cleanup_test_resources() { info "Cleaning up test resources..." @@ -835,8 +906,8 @@ cleanup_test_resources() { # "test-codex-add-issue-labels" # "test-claude-command" # "test-codex-command" - # "test-claude-push-to-branch" - # "test-codex-push-to-branch" + # "test-claude-push-to-pr-branch" + # "test-codex-push-to-pr-branch" # "test-claude-create-pull-request-review-comment" # "test-codex-create-pull-request-review-comment" # "test-claude-update-issue" @@ -875,6 +946,13 @@ cleanup_test_resources() { fi done + # Close PRs with titles containing "Test PR for" + gh pr list --limit 20 --json number,title --jq '.[] | select(.title | contains("Test PR for")) | .number' | while read -r pr_num; do + if [[ -n "$pr_num" ]]; then + gh pr close "$pr_num" --comment "Closed by e2e test cleanup" &>/dev/null || true + fi + done + # gh pr list --label "test-safe-outputs" --limit 20 --json number --jq '.[].number' | while read -r pr_num; do # if [[ -n "$pr_num" ]]; then # gh pr close "$pr_num" --comment "Closed by e2e test cleanup" &>/dev/null || true @@ -1075,39 +1153,62 @@ run_command_tests() { if enable_workflow "$workflow"; then workflows_to_disable+=("$workflow") - # Create a test issue for this specific workflow - progress "Testing $workflow" - local issue_num=$(create_test_issue "Test Issue for $ai_display_name Commands" "This issue is for testing $workflow") - - if [[ -n "$issue_num" ]]; then - success "Created test issue #$issue_num for $workflow: https://github.com/$REPO_OWNER/$REPO_NAME/issues/$issue_num" - - # Run the appropriate test based on workflow type - case "$workflow" in - *"command") - progress "Testing $ai_display_name command workflow" - post_issue_command "$issue_num" "/test-${ai_type}-command What is 102+103?" - wait_for_command_comment "$issue_num" "205" "$workflow" - ;; - *"push-to-branch") - progress "Testing $ai_display_name push-to-branch workflow" - # Create a test branch with initial content - local initial_sha=$(create_test_branch "${ai_type}-test-branch" "# $ai_display_name Test Branch\nThis branch will be updated by the $ai_display_name push-to-branch workflow.") + # Different handling for different workflow types + case "$workflow" in + *"push-to-pr-branch") + # For push-to-pr-branch workflows, create a test PR instead of an issue + progress "Testing $workflow" + local pr_info=$(create_test_pr_with_branch "Test PR for $ai_display_name Push-to-Branch" "This PR is for testing $workflow") + + if [[ -n "$pr_info" ]]; then + IFS=',' read -r pr_num branch_name initial_sha <<< "$pr_info" + success "Created test PR #$pr_num for $workflow with branch '$branch_name': https://github.com/$REPO_OWNER/$REPO_NAME/pull/$pr_num" - if [[ -n "$initial_sha" ]]; then - success "Created test branch '${ai_type}-test-branch' with SHA: $initial_sha" - post_issue_command "$issue_num" "/test-${ai_type}-push-to-branch" - wait_for_branch_update "${ai_type}-test-branch" "$initial_sha" "$workflow" - else - error "Failed to create test branch for $workflow" - FAILED_TESTS+=("$workflow") - fi - ;; - esac - else - error "Failed to create test issue for $workflow" - FAILED_TESTS+=("$workflow") - fi + progress "Testing $ai_display_name push-to-pr-branch workflow" + post_pr_command "$pr_num" "/test-${ai_type}-push-to-pr-branch" + wait_for_branch_update "$branch_name" "$initial_sha" "$workflow" + else + error "Failed to create test PR for $workflow" + FAILED_TESTS+=("$workflow") + fi + ;; + *"pull-request-review-comment") + # For PR review comment workflows, create a test PR and wait for review comments + progress "Testing $workflow" + local pr_num=$(create_test_pr "Test PR for $ai_display_name Review Comments" "This PR is for testing $workflow. Please add review comments.") + + if [[ -n "$pr_num" ]]; then + success "Created test PR #$pr_num for $workflow: https://github.com/$REPO_OWNER/$REPO_NAME/pull/$pr_num" + sleep 10 # Wait for workflow to trigger automatically + + progress "Testing $ai_display_name PR review comment workflow" + wait_for_pr_review_comments "$pr_num" "$ai_display_name" "$workflow" + else + error "Failed to create test PR for $workflow" + FAILED_TESTS+=("$workflow") + fi + ;; + *) + # For other command workflows (like regular commands), create a test issue + local issue_num=$(create_test_issue "Test Issue for $ai_display_name Commands" "This issue is for testing $workflow") + + if [[ -n "$issue_num" ]]; then + success "Created test issue #$issue_num for $workflow: https://github.com/$REPO_OWNER/$REPO_NAME/issues/$issue_num" + + # Run the appropriate test based on workflow type + case "$workflow" in + *"command") + progress "Testing $ai_display_name command workflow" + post_issue_command "$issue_num" "/test-${ai_type}-command What is 102+103?" + wait_for_command_comment "$issue_num" "205" "$workflow" + ;; + esac + else + error "Failed to create test issue for $workflow" + FAILED_TESTS+=("$workflow") + fi + ;; + esac else FAILED_TESTS+=("$workflow") fi @@ -1124,61 +1225,6 @@ run_command_tests() { fi } -run_pr_triggered_tests() { - local patterns=("$@") - info "🔀 Running PR-triggered tests..." - - local workflows - readarray -t workflows < <(filter_tests_by_patterns "pr-triggered" "${patterns[@]}") - - if [[ ${#workflows[@]} -eq 0 ]]; then - warning "No PR-triggered tests match the specified patterns" - return 0 - fi - - # Process each workflow individually - local workflows_to_disable=() - - for workflow in "${workflows[@]}"; do - local ai_type=$(extract_ai_type "$workflow") - local ai_display_name="${ai_type^}" - - if [[ -n "$ai_type" ]]; then - # Try to enable the workflow - if enable_workflow "$workflow"; then - workflows_to_disable+=("$workflow") - - # Create a test PR for this specific workflow - progress "Testing $workflow" - local pr_num=$(create_test_pr "Test PR for $ai_display_name Review" "This PR is for testing $workflow") - - if [[ -n "$pr_num" ]]; then - success "Created test PR #$pr_num for $workflow: https://github.com/$REPO_OWNER/$REPO_NAME/pull/$pr_num" - sleep 10 # Wait for workflow to trigger - - # For PR review comment workflows, we just check that the PR was created - # (Review comment validation is harder to do automatically) - PASSED_TESTS+=("$workflow") - else - error "Failed to create test PR for $workflow" - FAILED_TESTS+=("$workflow") - fi - else - FAILED_TESTS+=("$workflow") - fi - fi - done - - # Disable workflows after testing - for workflow in "${workflows_to_disable[@]}"; do - disable_workflow "$workflow" - done - - if [[ ${#workflows_to_disable[@]} -eq 0 ]]; then - info "No PR-triggered tests selected that require creating test PRs" - fi -} - print_final_report() { echo echo "============================================" @@ -1236,7 +1282,6 @@ main() { local run_workflow_dispatch=true local run_issue_triggered=true local run_command_triggered=true - local run_pr_triggered=true local dry_run=false local specific_tests=() @@ -1246,28 +1291,18 @@ main() { run_workflow_dispatch=true run_issue_triggered=false run_command_triggered=false - run_pr_triggered=false shift ;; --issue-triggered-only) run_workflow_dispatch=false run_issue_triggered=true run_command_triggered=false - run_pr_triggered=false shift ;; --command-triggered-only) run_workflow_dispatch=false run_issue_triggered=false run_command_triggered=true - run_pr_triggered=false - shift - ;; - --pr-triggered-only) - run_workflow_dispatch=false - run_issue_triggered=false - run_command_triggered=false - run_pr_triggered=true shift ;; --dry-run|-n) @@ -1281,7 +1316,6 @@ main() { echo " --workflow-dispatch-only Run only workflow_dispatch triggered tests" echo " --issue-triggered-only Run only issue-triggered tests" echo " --command-triggered-only Run only command-triggered tests" - echo " --pr-triggered-only Run only PR-triggered tests" echo " --dry-run, -n Show what would be tested without running" echo " --help, -h Show this help message" echo "" @@ -1358,20 +1392,6 @@ main() { echo fi - if [[ "$run_pr_triggered" == true ]]; then - info "� PR-Triggered Tests:" - local workflows - readarray -t workflows < <(filter_tests_by_patterns "pr-triggered" "${specific_tests[@]}") - if [[ ${#workflows[@]} -gt 0 ]]; then - for workflow in "${workflows[@]}"; do - echo " - $workflow" - done - else - echo " (no tests match the specified patterns)" - fi - echo - fi - exit 0 fi @@ -1404,12 +1424,6 @@ main() { run_command_triggered=false fi - # Check if any PR triggered tests match - local pt_tests - readarray -t pt_tests < <(filter_tests_by_patterns "pr-triggered" "${specific_tests[@]}") - if [[ ${#pt_tests[@]} -eq 0 ]]; then - run_pr_triggered=false - fi fi # Run test suites based on options @@ -1425,10 +1439,6 @@ main() { run_command_tests "${specific_tests[@]}" fi - if [[ "$run_pr_triggered" == true ]]; then - run_pr_triggered_tests "${specific_tests[@]}" - fi - print_final_report # Ask user if they want to cleanup diff --git a/pkg/cli/templates/instructions.md b/pkg/cli/templates/instructions.md index 52d40d6c457..2eead0bf4e8 100644 --- a/pkg/cli/templates/instructions.md +++ b/pkg/cli/templates/instructions.md @@ -130,7 +130,7 @@ The YAML frontmatter supports these fields: echo '{"type": "create-pull-request-review-comment", "path": "file.js", "line": 10, "body": "Review comment"}' >> $GITHUB_AW_SAFE_OUTPUTS # Push to branch (after making file changes) - echo '{"type": "push-to-branch", "message": "Commit message"}' >> $GITHUB_AW_SAFE_OUTPUTS + echo '{"type": "push-to-pr-branch", "message": "Commit message"}' >> $GITHUB_AW_SAFE_OUTPUTS # Create a discussion echo '{"type": "create-discussion", "title": "Discussion Title", "body": "Discussion content"}' >> $GITHUB_AW_SAFE_OUTPUTS @@ -143,7 +143,7 @@ The YAML frontmatter supports these fields: - Each JSON object must be on a single line (JSONL format) - All string values should be properly escaped JSON strings - The `type` field is required and must match the configured safe output types - - File changes for `create-pull-request` and `push-to-branch` are collected automatically via `git add -A` + - File changes for `create-pull-request` and `push-to-pr-branch` are collected automatically via `git add -A` - Output entries are processed only if the corresponding safe output type is configured in the workflow frontmatter - Invalid JSON entries are ignored with warnings in the workflow logs diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index f076a178404..6cd1b9c058a 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1453,7 +1453,7 @@ } ] }, - "push-to-branch": { + "push-to-pr-branch": { "oneOf": [ { "type": "null", diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index c83c790178c..b924a2bdbad 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -161,7 +161,7 @@ type SafeOutputsConfig struct { CreateRepositorySecurityAdvisories *CreateRepositorySecurityAdvisoriesConfig `yaml:"create-repository-security-advisory,omitempty"` AddIssueLabels *AddIssueLabelsConfig `yaml:"add-issue-label,omitempty"` UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` - PushToBranch *PushToBranchConfig `yaml:"push-to-branch,omitempty"` + PushToBranch *PushToBranchConfig `yaml:"push-to-pr-branch,omitempty"` MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality AllowedDomains []string `yaml:"allowed-domains,omitempty"` } @@ -229,8 +229,7 @@ type UpdateIssuesConfig struct { // PushToBranchConfig holds configuration for pushing changes to a specific branch from agent output type PushToBranchConfig struct { - Branch string `yaml:"branch"` // The branch to push changes to (defaults to "triggering") - Target string `yaml:"target,omitempty"` // Target for push-to-branch: like add-issue-comment but for pull requests + Target string `yaml:"target,omitempty"` // Target for push-to-pr-branch: like add-issue-comment but for pull requests IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") } @@ -1591,7 +1590,7 @@ func (c *Compiler) applyDefaultTools(tools map[string]any, safeOutputs *SafeOutp githubConfig["allowed"] = newAllowed tools["github"] = githubConfig - // Add Git commands and file editing tools when safe-outputs includes create-pull-request or push-to-branch + // Add Git commands and file editing tools when safe-outputs includes create-pull-request or push-to-pr-branch if safeOutputs != nil && needsGitCommands(safeOutputs) { // Add edit tool with null value @@ -1927,14 +1926,14 @@ func (c *Compiler) buildJobs(data *WorkflowData, markdownPath string) error { } } - // Build push_to_branch job if output.push-to-branch is configured + // Build push_to_pr_branch job if output.push-to-pr-branch is configured if data.SafeOutputs.PushToBranch != nil { pushToBranchJob, err := c.buildCreateOutputPushToBranchJob(data, jobName) if err != nil { - return fmt.Errorf("failed to build push_to_branch job: %w", err) + return fmt.Errorf("failed to build push_to_pr_branch job: %w", err) } if err := c.jobManager.AddJob(pushToBranchJob); err != nil { - return fmt.Errorf("failed to add push_to_branch job: %w", err) + return fmt.Errorf("failed to add push_to_pr_branch job: %w", err) } } @@ -3212,6 +3211,9 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { if data.SafeOutputs.UpdateIssues.Body != nil { fields = append(fields, "\"body\": \"Updated issue body in markdown\"") } + if data.SafeOutputs.UpdateIssues.Target == "*" { + fields = append(fields, "\"issue_number\": \"The issue number to update\"") + } if len(fields) > 0 { yaml.WriteString(" {\"type\": \"update-issue\"") @@ -3229,14 +3231,26 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { } if data.SafeOutputs.PushToBranch != nil { - yaml.WriteString(" **Pushing Changes to Branch**\n") + yaml.WriteString(" **Pushing Changes to Pull Request Branch**\n") yaml.WriteString(" \n") - yaml.WriteString(" To push changes to a branch, for example to add code to a pull request:\n") + yaml.WriteString(" To push changes to the branch of a pull request:\n") yaml.WriteString(" 1. Make any file changes directly in the working directory\n") - yaml.WriteString(" 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to.\n") - yaml.WriteString(" 3. Indicate your intention to push to the branch by writing to the file \"${{ env.GITHUB_AW_SAFE_OUTPUTS }}\":\n") + yaml.WriteString(" 2. Add and commit your changes to the local copy of the pull request branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to.\n") + yaml.WriteString(" 3. Indicate your intention to push the branch to the repo by writing to the file \"${{ env.GITHUB_AW_SAFE_OUTPUTS }}\":\n") yaml.WriteString(" ```json\n") - yaml.WriteString(" {\"type\": \"push-to-branch\", \"message\": \"Commit message describing the changes\"}\n") + var fields []string + fields = append(fields, "\"type\": \"push-to-pr-branch\"") + if data.SafeOutputs.PushToBranch.Target == "*" { + fields = append(fields, "\"pull_number\": \"The pull number to update\"") + } + fields = append(fields, "\"branch_name\": \"The name of the branch to push to, should be the branch name associated with the pull request\"") + fields = append(fields, "\"message\": \"Commit message describing the changes\"") + + yaml.WriteString(" {") + for _, field := range fields { + yaml.WriteString(", " + field) + } + yaml.WriteString("}\n") yaml.WriteString(" ```\n") yaml.WriteString(" 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up\n") yaml.WriteString(" \n") @@ -3295,7 +3309,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { exampleCount++ } if data.SafeOutputs.PushToBranch != nil { - yaml.WriteString(" {\"type\": \"push-to-branch\", \"message\": \"Update documentation with latest changes\"}\n") + yaml.WriteString(" {\"type\": \"push-to-pr-branch\", \"message\": \"Update documentation with latest changes\"}\n") exampleCount++ } if data.SafeOutputs.CreateRepositorySecurityAdvisories != nil { @@ -3478,7 +3492,7 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut config.UpdateIssues = updateIssuesConfig } - // Handle push-to-branch + // Handle push-to-pr-branch pushToBranchConfig := c.parsePushToBranchConfig(outputMap) if pushToBranchConfig != nil { config.PushToBranch = pushToBranchConfig @@ -3767,27 +3781,20 @@ func (c *Compiler) parseUpdateIssuesConfig(outputMap map[string]any) *UpdateIssu return nil } -// parsePushToBranchConfig handles push-to-branch configuration +// parsePushToBranchConfig handles push-to-pr-branch configuration func (c *Compiler) parsePushToBranchConfig(outputMap map[string]any) *PushToBranchConfig { - if configData, exists := outputMap["push-to-branch"]; exists { + if configData, exists := outputMap["push-to-pr-branch"]; exists { pushToBranchConfig := &PushToBranchConfig{ Branch: "triggering", // Default branch value IfNoChanges: "warn", // Default behavior: warn when no changes } - // Handle the case where configData is nil (push-to-branch: with no value) + // Handle the case where configData is nil (push-to-pr-branch: with no value) if configData == nil { return pushToBranchConfig } if configMap, ok := configData.(map[string]any); ok { - // Parse branch (optional, defaults to "triggering") - if branch, exists := configMap["branch"]; exists { - if branchStr, ok := branch.(string); ok { - pushToBranchConfig.Branch = branchStr - } - } - // Parse target (optional, similar to add-issue-comment) if target, exists := configMap["target"]; exists { if targetStr, ok := target.(string); ok { @@ -4100,12 +4107,11 @@ func (c *Compiler) generateOutputCollectionStep(yaml *strings.Builder, data *Wor if data.SafeOutputs.PushToBranch != nil { pushToBranchConfig := map[string]interface{}{ "enabled": true, - "branch": data.SafeOutputs.PushToBranch.Branch, } if data.SafeOutputs.PushToBranch.Target != "" { pushToBranchConfig["target"] = data.SafeOutputs.PushToBranch.Target } - safeOutputsConfig["push-to-branch"] = pushToBranchConfig + safeOutputsConfig["push-to-pr-branch"] = pushToBranchConfig } if data.SafeOutputs.MissingTool != nil { missingToolConfig := map[string]interface{}{ diff --git a/pkg/workflow/git_commands_integration_test.go b/pkg/workflow/git_commands_integration_test.go index 1118c374bab..bf22263c555 100644 --- a/pkg/workflow/git_commands_integration_test.go +++ b/pkg/workflow/git_commands_integration_test.go @@ -58,7 +58,7 @@ safe-outputs: max: 1 --- -This workflow should NOT get Git commands since it doesn't use create-pull-request or push-to-branch. +This workflow should NOT get Git commands since it doesn't use create-pull-request or push-to-pr-branch. ` compiler := NewCompiler(false, "", "test") @@ -134,17 +134,17 @@ This is a test workflow that should automatically get additional Claude tools wh } func TestAdditionalClaudeToolsIntegrationWithPushToBranch(t *testing.T) { - // Create a simple workflow with push-to-branch enabled + // Create a simple workflow with push-to-pr-branch enabled workflowContent := `--- name: Test Additional Claude Tools Integration with Push to Branch tools: edit: safe-outputs: - push-to-branch: + push-to-pr-branch: branch: "feature-branch" --- -This is a test workflow that should automatically get additional Claude tools when push-to-branch is enabled. +This is a test workflow that should automatically get additional Claude tools when push-to-pr-branch is enabled. ` compiler := NewCompiler(false, "", "test") @@ -168,11 +168,11 @@ This is a test workflow that should automatically get additional Claude tools wh t.Error("Expected pre-existing Read tool to be preserved") } - // Verify Git commands are also present (since push-to-branch is enabled) + // Verify Git commands are also present (since push-to-pr-branch is enabled) expectedGitCommands := []string{"Bash(git checkout:*)", "Bash(git add:*)", "Bash(git commit:*)"} for _, expectedCmd := range expectedGitCommands { if !strings.Contains(allowedToolsStr, expectedCmd) { - t.Errorf("Expected allowed tools to contain %s when push-to-branch is enabled, got: %s", expectedCmd, allowedToolsStr) + t.Errorf("Expected allowed tools to contain %s when push-to-pr-branch is enabled, got: %s", expectedCmd, allowedToolsStr) } } } diff --git a/pkg/workflow/git_commands_test.go b/pkg/workflow/git_commands_test.go index 067fb6f8592..e3e64dcd450 100644 --- a/pkg/workflow/git_commands_test.go +++ b/pkg/workflow/git_commands_test.go @@ -30,7 +30,7 @@ func TestApplyDefaultGitCommandsForSafeOutputs(t *testing.T) { expectGit: true, }, { - name: "push-to-branch enabled - should add git commands", + name: "push-to-pr-branch enabled - should add git commands", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ PushToBranch: &PushToBranchConfig{Branch: "main"}, @@ -152,7 +152,7 @@ func TestAdditionalClaudeToolsForSafeOutputs(t *testing.T) { expectEditingTools: true, }, { - name: "push-to-branch enabled - should add editing tools", + name: "push-to-pr-branch enabled - should add editing tools", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ PushToBranch: &PushToBranchConfig{Branch: "main"}, @@ -268,7 +268,7 @@ func TestNeedsGitCommands(t *testing.T) { expected: true, }, { - name: "push-to-branch enabled", + name: "push-to-pr-branch enabled", safeOutputs: &SafeOutputsConfig{ PushToBranch: &PushToBranchConfig{Branch: "main"}, }, diff --git a/pkg/workflow/git_patch.go b/pkg/workflow/git_patch.go index af7fb72b4cc..60d0ff418b1 100644 --- a/pkg/workflow/git_patch.go +++ b/pkg/workflow/git_patch.go @@ -8,9 +8,9 @@ func (c *Compiler) generateGitPatchStep(yaml *strings.Builder, data *WorkflowDat yaml.WriteString(" if: always()\n") yaml.WriteString(" env:\n") yaml.WriteString(" GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}\n") - // Add push-to-branch configuration if available + // Add push-to-pr-branch configuration if available if data.SafeOutputs != nil && data.SafeOutputs.PushToBranch != nil { - yaml.WriteString(" GITHUB_AW_PUSH_BRANCH: \"" + data.SafeOutputs.PushToBranch.Branch + "\"\n") + yaml.WriteString(" GITHUB_AW_PUSH_TARGET: \"" + data.SafeOutputs.PushToBranch.Target + "\"\n") } yaml.WriteString(" run: |\n") yaml.WriteString(" # Check current git status\n") @@ -32,14 +32,13 @@ func (c *Compiler) generateGitPatchStep(yaml *strings.Builder, data *WorkflowDat yaml.WriteString(" echo \"Extracted branch name from create-pull-request: $BRANCH_NAME\"\n") yaml.WriteString(" break\n") yaml.WriteString(" fi\n") - yaml.WriteString(" # Extract branch from push-to-branch line using simple grep and sed\n") - yaml.WriteString(" elif echo \"$line\" | grep -q '\"type\"[[:space:]]*:[[:space:]]*\"push-to-branch\"'; then\n") - yaml.WriteString(" echo \"Found push-to-branch line: $line\"\n") - yaml.WriteString(" # For push-to-branch, we don't extract branch from JSONL since it's configured in the workflow\n") - yaml.WriteString(" # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH\n") - yaml.WriteString(" if [ -n \"$GITHUB_AW_PUSH_BRANCH\" ]; then\n") - yaml.WriteString(" BRANCH_NAME=\"$GITHUB_AW_PUSH_BRANCH\"\n") - yaml.WriteString(" echo \"Using configured push-to-branch target: $BRANCH_NAME\"\n") + yaml.WriteString(" # Extract branch from push-to-pr-branch line using simple grep and sed\n") + yaml.WriteString(" elif echo \"$line\" | grep -q '\"type\"[[:space:]]*:[[:space:]]*\"push-to-pr-branch\"'; then\n") + yaml.WriteString(" echo \"Found push-to-pr-branch line: $line\"\n") + yaml.WriteString(" # Extract branch value using sed\n") + yaml.WriteString(" BRANCH_NAME=$(echo \"$line\" | sed -n 's/.*\"branch\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p')\n") + yaml.WriteString(" if [ -n \"$BRANCH_NAME\" ]; then\n") + yaml.WriteString(" echo \"Extracted branch name from create-pull-request: $BRANCH_NAME\"\n") yaml.WriteString(" break\n") yaml.WriteString(" fi\n") yaml.WriteString(" fi\n") diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index 317b84e688e..ae2cfcf6ba0 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -36,7 +36,7 @@ var addLabelsScript string //go:embed js/update_issue.cjs var updateIssueScript string -//go:embed js/push_to_branch.cjs +//go:embed js/push_to_pr_branch.cjs var pushToBranchScript string //go:embed js/setup_agent_output.cjs diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index c4dbd5adf74..837494e90bf 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -177,7 +177,7 @@ async function main() { return 5; // Only one labels operation allowed case "update-issue": return 1; // Only one issue update allowed - case "push-to-branch": + case "push-to-pr-branch": return 1; // Only one push to branch allowed case "create-discussion": return 1; // Only one discussion allowed @@ -518,12 +518,12 @@ async function main() { } break; - case "push-to-branch": + case "push-to-pr-branch": // Validate message if provided (optional) if (item.message !== undefined) { if (typeof item.message !== "string") { errors.push( - `Line ${i + 1}: push-to-branch 'message' must be a string` + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` ); continue; } @@ -536,7 +536,7 @@ async function main() { typeof item.pull_request_number !== "string" ) { errors.push( - `Line ${i + 1}: push-to-branch 'pull_request_number' must be a number or string` + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` ); continue; } diff --git a/pkg/workflow/js/push_to_branch.cjs b/pkg/workflow/js/push_to_pr_branch.cjs similarity index 83% rename from pkg/workflow/js/push_to_branch.cjs rename to pkg/workflow/js/push_to_pr_branch.cjs index 241b815ec8f..ff54237647f 100644 --- a/pkg/workflow/js/push_to_branch.cjs +++ b/pkg/workflow/js/push_to_pr_branch.cjs @@ -4,12 +4,6 @@ async function main() { const { execSync } = require("child_process"); // Environment validation - fail early if required variables are missing - const branchName = process.env.GITHUB_AW_PUSH_BRANCH; - if (!branchName) { - core.setFailed("GITHUB_AW_PUSH_BRANCH environment variable is required"); - return; - } - const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT || ""; if (outputContent.trim() === "") { console.log("Agent output content is empty"); @@ -84,7 +78,6 @@ async function main() { if (!isEmpty) { console.log("Patch content validation passed"); } - console.log("Target branch:", branchName); console.log("Target configuration:", target); // Parse the validated output JSON @@ -104,22 +97,22 @@ async function main() { return; } - // Find the push-to-branch item + // Find the push-to-pr-branch item const pushItem = validatedOutput.items.find( - /** @param {any} item */ item => item.type === "push-to-branch" + /** @param {any} item */ item => item.type === "push-to-pr-branch" ); if (!pushItem) { - console.log("No push-to-branch item found in agent output"); + console.log("No push-to-pr-branch item found in agent output"); return; } - console.log("Found push-to-branch item"); + console.log("Found push-to-pr-branch item"); // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number - const targetNumber = parseInt(target, 10); - if (isNaN(targetNumber)) { + const pullNumber = parseInt(target, 10); + if (isNaN(pullNumber)) { core.setFailed( 'Invalid target configuration: must be "triggering", "*", or a valid pull request number' ); @@ -130,11 +123,49 @@ async function main() { // Check if we're in a pull request context when required if (target === "triggering" && !context.payload.pull_request) { core.setFailed( - 'push-to-branch with target "triggering" requires pull request context' + 'push-to-pr-branch with target "triggering" requires pull request context' ); return; } + // Compute the target branch name based on target configuration + let pullNumber; + if (target === "triggering") { + // Use the number of the triggering pull request + pullNumber = context.payload.pull_request.number; + } else if (target === "*") { + if (pushItem.pull_number) { + pullNumber = parseInt(pushItem.pull_number, 10); + } + } else { + // Target is a specific pull request number + pullNumber = parseInt(target, 10); + } + let branchName; + // Fetch the specific PR to get its head branch + try { + const prInfo = execSync( + `gh pr view ${pullNumber} --json headRefName --jq '.headRefName'`, + { encoding: "utf8" } + ).trim(); + + if (prInfo) { + branchName = prInfo; + } else { + throw new Error("No head branch found for PR"); + } + } catch (error) { + console.log(`Warning: Could not fetch PR ${pullNumber} details: ${error.message}`); + // Exit with failure if we cannot determine the branch name + core.setFailed(`Failed to determine branch name for PR ${pullNumber}`); + return; + } + + console.log("Target branch:", branchName); + + // Check if patch has actual changes (not just empty) + const hasChanges = !isEmpty; + // Switch to or create the target branch console.log("Switching to branch:", branchName); try { diff --git a/pkg/workflow/js/push_to_branch.test.cjs b/pkg/workflow/js/push_to_pr_branch.test.cjs similarity index 84% rename from pkg/workflow/js/push_to_branch.test.cjs rename to pkg/workflow/js/push_to_pr_branch.test.cjs index 4c194395da8..50a0336b5ff 100644 --- a/pkg/workflow/js/push_to_branch.test.cjs +++ b/pkg/workflow/js/push_to_pr_branch.test.cjs @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; -describe("push_to_branch.cjs", () => { +describe("push_to_pr_branch.cjs", () => { let mockCore; beforeEach(() => { @@ -30,7 +30,6 @@ describe("push_to_branch.cjs", () => { }; // Clear environment variables - delete process.env.GITHUB_AW_PUSH_BRANCH; delete process.env.GITHUB_AW_PUSH_TARGET; delete process.env.GITHUB_AW_AGENT_OUTPUT; }); @@ -45,19 +44,18 @@ describe("push_to_branch.cjs", () => { describe("Script validation", () => { it("should have valid JavaScript syntax", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Basic syntax validation - should not contain obvious errors expect(scriptContent).toContain("async function main()"); - expect(scriptContent).toContain("GITHUB_AW_PUSH_BRANCH"); expect(scriptContent).toContain("core.setFailed"); expect(scriptContent).toContain("/tmp/aw.patch"); expect(scriptContent).toContain("await main()"); }); it("should export a main function", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that the script has the expected structure @@ -65,11 +63,10 @@ describe("push_to_branch.cjs", () => { }); it("should handle required environment variables", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that environment variables are handled - expect(scriptContent).toContain("process.env.GITHUB_AW_PUSH_BRANCH"); expect(scriptContent).toContain("process.env.GITHUB_AW_AGENT_OUTPUT"); expect(scriptContent).toContain("process.env.GITHUB_AW_PUSH_TARGET"); expect(scriptContent).toContain( @@ -78,7 +75,7 @@ describe("push_to_branch.cjs", () => { }); it("should handle patch file operations", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that patch operations are included @@ -89,7 +86,7 @@ describe("push_to_branch.cjs", () => { }); it("should validate branch operations", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that git branch operations are handled (git config is handled by workflow) @@ -98,7 +95,7 @@ describe("push_to_branch.cjs", () => { }); it("should handle empty patches as noop operations", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that empty patches are handled gracefully @@ -110,7 +107,7 @@ describe("push_to_branch.cjs", () => { }); it("should handle if-no-changes configuration options", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that environment variable is read @@ -122,7 +119,7 @@ describe("push_to_branch.cjs", () => { }); it("should still fail on actual error conditions", () => { - const scriptPath = path.join(__dirname, "push_to_branch.cjs"); + const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); // Check that actual errors still cause failures diff --git a/pkg/workflow/output_push_to_branch.go b/pkg/workflow/output_push_to_branch.go index 44afebb913e..0cb5bd2135a 100644 --- a/pkg/workflow/output_push_to_branch.go +++ b/pkg/workflow/output_push_to_branch.go @@ -4,15 +4,10 @@ import ( "fmt" ) -// buildCreateOutputPushToBranchJob creates the push_to_branch job +// buildCreateOutputPushToBranchJob creates the push_to_pr_branch job func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobName string) (*Job, error) { if data.SafeOutputs == nil || data.SafeOutputs.PushToBranch == nil { - return nil, fmt.Errorf("safe-outputs.push-to-branch configuration is required") - } - - // Branch should have a default value of "triggering" set by the parser - if data.SafeOutputs.PushToBranch.Branch == "" { - return nil, fmt.Errorf("safe-outputs.push-to-branch branch configuration is invalid") + return nil, fmt.Errorf("safe-outputs.push-to-pr-branch configuration is required") } var steps []string @@ -36,15 +31,13 @@ func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobN // Step 4: Push to Branch steps = append(steps, " - name: Push to Branch\n") - steps = append(steps, " id: push_to_branch\n") + steps = append(steps, " id: push_to_pr_branch\n") steps = append(steps, " uses: actions/github-script@v7\n") // Add environment variables steps = append(steps, " env:\n") // Pass the agent output content from the main job steps = append(steps, fmt.Sprintf(" GITHUB_AW_AGENT_OUTPUT: ${{ needs.%s.outputs.output }}\n", mainJobName)) - // Pass the branch configuration - steps = append(steps, fmt.Sprintf(" GITHUB_AW_PUSH_BRANCH: %q\n", data.SafeOutputs.PushToBranch.Branch)) // Pass the target configuration if data.SafeOutputs.PushToBranch.Target != "" { steps = append(steps, fmt.Sprintf(" GITHUB_AW_PUSH_TARGET: %q\n", data.SafeOutputs.PushToBranch.Target)) @@ -61,9 +54,9 @@ func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobN // Create outputs for the job outputs := map[string]string{ - "branch_name": "${{ steps.push_to_branch.outputs.branch_name }}", - "commit_sha": "${{ steps.push_to_branch.outputs.commit_sha }}", - "push_url": "${{ steps.push_to_branch.outputs.push_url }}", + "branch_name": "${{ steps.push_to_pr_branch.outputs.branch_name }}", + "commit_sha": "${{ steps.push_to_pr_branch.outputs.commit_sha }}", + "push_url": "${{ steps.push_to_pr_branch.outputs.push_url }}", } // Determine the job condition based on target configuration @@ -97,7 +90,7 @@ func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobN } job := &Job{ - Name: "push_to_branch", + Name: "push_to_pr_branch", If: jobCondition, RunsOn: "runs-on: ubuntu-latest", Permissions: "permissions:\n contents: write\n pull-requests: read", diff --git a/pkg/workflow/output_push_to_branch_test.go b/pkg/workflow/output_push_to_branch_test.go index e4b520b2cc0..dd8999d7238 100644 --- a/pkg/workflow/output_push_to_branch_test.go +++ b/pkg/workflow/output_push_to_branch_test.go @@ -11,26 +11,25 @@ func TestPushToBranchConfigParsing(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() - // Create a test markdown file with push-to-branch configuration + // Create a test markdown file with push-to-pr-branch configuration testMarkdown := `--- on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: feature-updates + push-to-pr-branch: target: "triggering" --- # Test Push to Branch -This is a test workflow to validate push-to-branch configuration parsing. +This is a test workflow to validate push-to-pr-branch configuration parsing. Please make changes and push them to the feature branch. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -51,14 +50,9 @@ Please make changes and push them to the feature branch. lockContentStr := string(lockContent) - // Verify that push_to_branch job is generated - if !strings.Contains(lockContentStr, "push_to_branch:") { - t.Errorf("Generated workflow should contain push_to_branch job") - } - - // Verify that the branch configuration is passed correctly - if !strings.Contains(lockContentStr, "GITHUB_AW_PUSH_BRANCH: \"feature-updates\"") { - t.Errorf("Generated workflow should contain branch configuration") + // Verify that push_to_pr_branch job is generated + if !strings.Contains(lockContentStr, "push_to_pr_branch:") { + t.Errorf("Generated workflow should contain push_to_pr_branch job") } // Verify that the target configuration is passed correctly @@ -72,7 +66,7 @@ Please make changes and push them to the feature branch. } // Verify that the job depends on the main workflow job - if !strings.Contains(lockContentStr, "needs: test-push-to-branch") { + if !strings.Contains(lockContentStr, "needs: test-push-to-pr-branch") { t.Errorf("Generated workflow should have dependency on main job") } @@ -92,8 +86,7 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: feature-updates + push-to-pr-branch: target: "*" --- @@ -103,7 +96,7 @@ This workflow allows pushing to any pull request. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-asterisk.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-asterisk.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -145,7 +138,7 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: + push-to-pr-branch: target: "triggering" --- @@ -155,7 +148,7 @@ This workflow uses the default branch value. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-default-branch.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-default-branch.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -170,7 +163,7 @@ This workflow uses the default branch value. } // Read the generated .lock.yml file - lockFile := filepath.Join(tmpDir, "test-push-to-branch-default-branch.lock.yml") + lockFile := filepath.Join(tmpDir, "test-push-to-pr-branch-default-branch.lock.yml") content, err := os.ReadFile(lockFile) if err != nil { t.Fatalf("Failed to read generated lock file: %v", err) @@ -178,14 +171,9 @@ This workflow uses the default branch value. lockContent := string(content) - // Check that the default branch "triggering" is used - if !strings.Contains(lockContent, `GITHUB_AW_PUSH_BRANCH: "triggering"`) { - t.Errorf("Expected default branch 'triggering' to be set in environment variables") - } - - // Check that the push_to_branch job is generated - if !strings.Contains(lockContent, "push_to_branch:") { - t.Errorf("Expected push_to_branch job to be generated") + // Check that the push_to_pr_branch job is generated + if !strings.Contains(lockContent, "push_to_pr_branch:") { + t.Errorf("Expected push_to_pr_branch job to be generated") } } @@ -193,13 +181,13 @@ func TestPushToBranchNullConfig(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() - // Create a test markdown file with null configuration (push-to-branch: with no value) + // Create a test markdown file with null configuration (push-to-pr-branch: with no value) testMarkdown := `--- on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: + push-to-pr-branch: --- # Test Push to Branch Null Config @@ -208,7 +196,7 @@ This workflow uses null configuration which should default to "triggering". ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-null-config.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-null-config.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -223,7 +211,7 @@ This workflow uses null configuration which should default to "triggering". } // Read the generated .lock.yml file - lockFile := filepath.Join(tmpDir, "test-push-to-branch-null-config.lock.yml") + lockFile := filepath.Join(tmpDir, "test-push-to-pr-branch-null-config.lock.yml") content, err := os.ReadFile(lockFile) if err != nil { t.Fatalf("Failed to read generated lock file: %v", err) @@ -231,14 +219,9 @@ This workflow uses null configuration which should default to "triggering". lockContent := string(content) - // Check that the default branch "triggering" is used - if !strings.Contains(lockContent, `GITHUB_AW_PUSH_BRANCH: "triggering"`) { - t.Errorf("Expected default branch 'triggering' to be set in environment variables") - } - - // Check that the push_to_branch job is generated - if !strings.Contains(lockContent, "push_to_branch:") { - t.Errorf("Expected push_to_branch job to be generated") + // Check that the push_to_pr_branch job is generated + if !strings.Contains(lockContent, "push_to_pr_branch:") { + t.Errorf("Expected push_to_pr_branch job to be generated") } // Check that no target is set (should use default) @@ -257,17 +240,16 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: main + push-to-pr-branch: --- # Test Push to Branch Minimal -This workflow has minimal push-to-branch configuration. +This workflow has minimal push-to-pr-branch configuration. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-minimal.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-minimal.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -288,14 +270,9 @@ This workflow has minimal push-to-branch configuration. lockContentStr := string(lockContent) - // Verify that push_to_branch job is generated - if !strings.Contains(lockContentStr, "push_to_branch:") { - t.Errorf("Generated workflow should contain push_to_branch job") - } - - // Verify that the branch configuration is passed correctly - if !strings.Contains(lockContentStr, "GITHUB_AW_PUSH_BRANCH: \"main\"") { - t.Errorf("Generated workflow should contain branch configuration") + // Verify that push_to_pr_branch job is generated + if !strings.Contains(lockContentStr, "push_to_pr_branch:") { + t.Errorf("Generated workflow should contain push_to_pr_branch job") } // Verify that target defaults to triggering behavior (no explicit target env var) @@ -319,8 +296,7 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: feature-updates + push-to-pr-branch: target: "triggering" if-no-changes: "error" --- @@ -331,7 +307,7 @@ This workflow fails when there are no changes. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-error.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-error.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -368,8 +344,7 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: feature-updates + push-to-pr-branch: if-no-changes: "ignore" --- @@ -379,7 +354,7 @@ This workflow ignores when there are no changes. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-ignore.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-ignore.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -416,8 +391,7 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: feature-updates + push-to-pr-branch: --- # Test Push to Branch Default if-no-changes @@ -426,7 +400,7 @@ This workflow uses default if-no-changes behavior. ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-default-if-no-changes.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-default-if-no-changes.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -463,8 +437,7 @@ on: pull_request: types: [opened, synchronize] safe-outputs: - push-to-branch: - branch: "triggering" + push-to-pr-branch: target: "triggering" --- @@ -474,7 +447,7 @@ This workflow explicitly sets branch to "triggering". ` // Write the test file - mdFile := filepath.Join(tmpDir, "test-push-to-branch-explicit-triggering.md") + mdFile := filepath.Join(tmpDir, "test-push-to-pr-branch-explicit-triggering.md") if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { t.Fatalf("Failed to write test markdown file: %v", err) } @@ -487,7 +460,7 @@ This workflow explicitly sets branch to "triggering". } // Read the generated .lock.yml file - lockFile := filepath.Join(tmpDir, "test-push-to-branch-explicit-triggering.lock.yml") + lockFile := filepath.Join(tmpDir, "test-push-to-pr-branch-explicit-triggering.lock.yml") lockContent, err := os.ReadFile(lockFile) if err != nil { t.Fatalf("Failed to read generated lock file: %v", err) @@ -495,14 +468,9 @@ This workflow explicitly sets branch to "triggering". lockContentStr := string(lockContent) - // Verify that push_to_branch job is generated - if !strings.Contains(lockContentStr, "push_to_branch:") { - t.Errorf("Generated workflow should contain push_to_branch job") - } - - // Verify that the explicit "triggering" branch configuration is passed correctly - if !strings.Contains(lockContentStr, `GITHUB_AW_PUSH_BRANCH: "triggering"`) { - t.Errorf("Generated workflow should contain explicit triggering branch configuration") + // Verify that push_to_pr_branch job is generated + if !strings.Contains(lockContentStr, "push_to_pr_branch:") { + t.Errorf("Generated workflow should contain push_to_pr_branch job") } // Verify that target configuration is included diff --git a/schemas/agent-output.json b/schemas/agent-output.json index 28008c68fe2..bc8ea882a55 100644 --- a/schemas/agent-output.json +++ b/schemas/agent-output.json @@ -176,7 +176,7 @@ "type": "object", "properties": { "type": { - "const": "push-to-branch" + "const": "push-to-pr-branch" }, "message": { "type": "string", From 49d9401f9f9e5bcd7e525629508c716a314f726a Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 11 Sep 2025 01:51:55 +0100 Subject: [PATCH 2/6] add unit tests --- pkg/workflow/js/push_to_pr_branch.test.cjs | 376 ++++++++++++++++----- 1 file changed, 298 insertions(+), 78 deletions(-) diff --git a/pkg/workflow/js/push_to_pr_branch.test.cjs b/pkg/workflow/js/push_to_pr_branch.test.cjs index 50a0336b5ff..84c9b135a11 100644 --- a/pkg/workflow/js/push_to_pr_branch.test.cjs +++ b/pkg/workflow/js/push_to_pr_branch.test.cjs @@ -2,36 +2,88 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +// Mock the global objects that GitHub Actions provides +const mockCore = { + setFailed: vi.fn(), + setOutput: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + write: vi.fn(), + }, + warning: vi.fn(), + error: vi.fn(), +}; + +const mockContext = { + eventName: "pull_request", + payload: { + pull_request: { number: 123 }, + repository: { html_url: "https://github.com/testowner/testrepo" }, + }, + repo: { owner: "testowner", repo: "testrepo" }, +}; + +// Set up global variables +global.core = mockCore; +global.context = mockContext; + describe("push_to_pr_branch.cjs", () => { - let mockCore; + let pushToPrBranchScript; + let mockFs; + let mockExecSync; - beforeEach(() => { - // Mock core actions methods - mockCore = { - setFailed: vi.fn(), - setOutput: vi.fn(), - summary: { - addRaw: vi.fn().mockReturnThis(), - write: vi.fn(), - }, - warning: vi.fn(), - error: vi.fn(), - }; + // Helper function to execute the script with proper globals + const executeScript = async () => { + // Set globals just before execution global.core = mockCore; + global.context = mockContext; + global.mockFs = mockFs; + global.mockExecSync = mockExecSync; + + // Execute the script + return await eval(`(async () => { ${pushToPrBranchScript} })()`); + }; - // Mock context object - global.context = { - eventName: "pull_request", - payload: { - pull_request: { number: 123 }, - repository: { html_url: "https://github.com/testowner/testrepo" }, - }, - repo: { owner: "testowner", repo: "testrepo" }, - }; + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks(); // Clear environment variables delete process.env.GITHUB_AW_PUSH_TARGET; delete process.env.GITHUB_AW_AGENT_OUTPUT; + delete process.env.GITHUB_AW_PUSH_IF_NO_CHANGES; + + // Create fresh mock objects for each test + mockFs = { + existsSync: vi.fn(), + readFileSync: vi.fn(), + }; + + // Create fresh mock for execSync + mockExecSync = vi.fn(); + + // Reset mockCore calls + mockCore.setFailed.mockReset(); + mockCore.setOutput.mockReset(); + mockCore.warning.mockReset(); + mockCore.error.mockReset(); + + // Read the script content + const scriptPath = path.join( + process.cwd(), + "pkg/workflow/js/push_to_pr_branch.cjs" + ); + pushToPrBranchScript = fs.readFileSync(scriptPath, "utf8"); + + // Modify the script to inject our mocks and make core available + pushToPrBranchScript = pushToPrBranchScript.replace( + 'async function main() {\n /** @type {typeof import("fs")} */\n const fs = require("fs");\n const { execSync } = require("child_process");', + `async function main() { + const core = global.core; + const context = global.context || {}; + const fs = global.mockFs; + const execSync = global.mockExecSync;` + ); }); afterEach(() => { @@ -39,92 +91,260 @@ describe("push_to_pr_branch.cjs", () => { if (typeof global !== "undefined") { delete global.core; delete global.context; + delete global.mockFs; + delete global.mockExecSync; } }); - describe("Script validation", () => { - it("should have valid JavaScript syntax", () => { - const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); - const scriptContent = fs.readFileSync(scriptPath, "utf8"); + describe("Script execution", () => { + it("should skip when no agent output is provided", async () => { + // Remove the output content environment variable + delete process.env.GITHUB_AW_AGENT_OUTPUT; - // Basic syntax validation - should not contain obvious errors - expect(scriptContent).toContain("async function main()"); - expect(scriptContent).toContain("core.setFailed"); - expect(scriptContent).toContain("/tmp/aw.patch"); - expect(scriptContent).toContain("await main()"); + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("Agent output content is empty"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); - it("should export a main function", () => { - const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); - const scriptContent = fs.readFileSync(scriptPath, "utf8"); + it("should skip when agent output is empty", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = " "; - // Check that the script has the expected structure - expect(scriptContent).toMatch(/async function main\(\) \{[\s\S]*\}/); + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("Agent output content is empty"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); - it("should handle required environment variables", () => { - const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); - const scriptContent = fs.readFileSync(scriptPath, "utf8"); + it("should handle missing patch file with default 'warn' behavior", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + + mockFs.existsSync.mockReturnValue(false); - // Check that environment variables are handled - expect(scriptContent).toContain("process.env.GITHUB_AW_AGENT_OUTPUT"); - expect(scriptContent).toContain("process.env.GITHUB_AW_PUSH_TARGET"); - expect(scriptContent).toContain( - "process.env.GITHUB_AW_PUSH_IF_NO_CHANGES" - ); + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("No patch file found - cannot push without changes"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); - it("should handle patch file operations", () => { - const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); - const scriptContent = fs.readFileSync(scriptPath, "utf8"); + it("should fail when patch file missing and if-no-changes is 'error'", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + process.env.GITHUB_AW_PUSH_IF_NO_CHANGES = "error"; + + mockFs.existsSync.mockReturnValue(false); - // Check that patch operations are included - expect(scriptContent).toContain("fs.existsSync"); - expect(scriptContent).toContain("fs.readFileSync"); - expect(scriptContent).toContain("git am"); - expect(scriptContent).toContain("git push"); + // Execute the script + await executeScript(); + + expect(mockCore.setFailed).toHaveBeenCalledWith("No patch file found - cannot push without changes"); }); - it("should validate branch operations", () => { - const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); - const scriptContent = fs.readFileSync(scriptPath, "utf8"); + it("should silently succeed when patch file missing and if-no-changes is 'ignore'", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + process.env.GITHUB_AW_PUSH_IF_NO_CHANGES = "ignore"; + + mockFs.existsSync.mockReturnValue(false); - // Check that git branch operations are handled (git config is handled by workflow) - expect(scriptContent).toContain("git checkout"); - expect(scriptContent).toContain("git fetch"); + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).not.toHaveBeenCalled(); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); - it("should handle empty patches as noop operations", () => { - const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); - const scriptContent = fs.readFileSync(scriptPath, "utf8"); + it("should handle patch file with error content", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("Failed to generate patch: some error"); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - // Check that empty patches are handled gracefully - expect(scriptContent).toContain("noop operation"); - expect(scriptContent).toContain("Patch file is empty"); - expect(scriptContent).toContain( - "No changes to apply - noop operation completed successfully" + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("Patch file contains error message - cannot push without changes"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + + it("should handle empty patch file with default 'warn' behavior", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue(""); + + // Mock the git command to return a branch name + mockExecSync.mockReturnValue("feature-branch"); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("Patch file is empty - no changes to apply (noop operation)"); + expect(consoleSpy).toHaveBeenCalledWith("Agent output content length:", expect.any(Number)); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + + it("should fail when empty patch and if-no-changes is 'error'", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + process.env.GITHUB_AW_PUSH_IF_NO_CHANGES = "error"; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue(" "); + + // Execute the script + await executeScript(); + + expect(mockCore.setFailed).toHaveBeenCalledWith("No changes to push - failing as configured by if-no-changes: error"); + }); + + it("should handle valid patch content and parse JSON output", async () => { + const validOutput = { + items: [ + { + type: "push-to-pr-branch", + content: "some changes to push" + } + ] + }; + + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify(validOutput); + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("diff --git a/file.txt b/file.txt\n+new content"); + + // Mock the git commands that will be called + mockExecSync.mockReturnValue("feature-branch"); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("Agent output content length:", JSON.stringify(validOutput).length); + expect(consoleSpy).toHaveBeenCalledWith("Patch content validation passed"); + expect(consoleSpy).toHaveBeenCalledWith("Target configuration:", "triggering"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + + it("should handle invalid JSON in agent output", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = "invalid json content"; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("some patch content"); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith( + "Error parsing agent output JSON:", + expect.any(String) ); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); - it("should handle if-no-changes configuration options", () => { + it("should handle agent output without valid items array", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: "not an array" + }); + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("some patch content"); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("No valid items found in agent output"); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + + it("should use custom target configuration", async () => { + process.env.GITHUB_AW_AGENT_OUTPUT = JSON.stringify({ + items: [{ type: "push-to-pr-branch", content: "test" }] + }); + process.env.GITHUB_AW_PUSH_TARGET = "custom-target"; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("some patch content"); + + // Mock the git commands + mockExecSync.mockReturnValue("feature-branch"); + + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // Execute the script + await executeScript(); + + expect(consoleSpy).toHaveBeenCalledWith("Target configuration:", "custom-target"); + + consoleSpy.mockRestore(); + }); + }); + + describe("Script validation", () => { + it("should have valid JavaScript syntax", () => { const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); - // Check that environment variable is read - expect(scriptContent).toContain("GITHUB_AW_PUSH_IF_NO_CHANGES"); - expect(scriptContent).toContain("switch (ifNoChanges)"); - expect(scriptContent).toContain('case "error":'); - expect(scriptContent).toContain('case "ignore":'); - expect(scriptContent).toContain('case "warn":'); + // Basic syntax validation - should not contain obvious errors + expect(scriptContent).toContain("async function main()"); + expect(scriptContent).toContain("core.setFailed"); + expect(scriptContent).toContain("/tmp/aw.patch"); + expect(scriptContent).toContain("await main()"); }); - it("should still fail on actual error conditions", () => { + it("should export a main function", () => { const scriptPath = path.join(__dirname, "push_to_pr_branch.cjs"); const scriptContent = fs.readFileSync(scriptPath, "utf8"); - // Check that actual errors still cause failures - expect(scriptContent).toContain("Failed to generate patch"); - expect(scriptContent).toContain("core.setFailed"); + // Check that the script has the expected structure + expect(scriptContent).toMatch(/async function main\(\) \{[\s\S]*\}/); }); }); }); From c4bd5782a4fbdb628c01e64a04cc78e134cdc7ed Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 11 Sep 2025 01:52:57 +0100 Subject: [PATCH 3/6] rename --- pkg/workflow/compiler.go | 24 ++++++++++++------------ pkg/workflow/git_commands_test.go | 10 +++++----- pkg/workflow/git_patch.go | 4 ++-- pkg/workflow/output_push_to_branch.go | 10 +++++----- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index b924a2bdbad..8e1ee3509c9 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -161,7 +161,7 @@ type SafeOutputsConfig struct { CreateRepositorySecurityAdvisories *CreateRepositorySecurityAdvisoriesConfig `yaml:"create-repository-security-advisory,omitempty"` AddIssueLabels *AddIssueLabelsConfig `yaml:"add-issue-label,omitempty"` UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` - PushToBranch *PushToBranchConfig `yaml:"push-to-pr-branch,omitempty"` + PushToPullRequestBranch *PushToBranchConfig `yaml:"push-to-pr-branch,omitempty"` MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality AllowedDomains []string `yaml:"allowed-domains,omitempty"` } @@ -1653,7 +1653,7 @@ func needsGitCommands(safeOutputs *SafeOutputsConfig) bool { if safeOutputs == nil { return false } - return safeOutputs.CreatePullRequests != nil || safeOutputs.PushToBranch != nil + return safeOutputs.CreatePullRequests != nil || safeOutputs.PushToPullRequestBranch != nil } // detectTextOutputUsage checks if the markdown content uses ${{ needs.task.outputs.text }} @@ -1927,7 +1927,7 @@ func (c *Compiler) buildJobs(data *WorkflowData, markdownPath string) error { } // Build push_to_pr_branch job if output.push-to-pr-branch is configured - if data.SafeOutputs.PushToBranch != nil { + if data.SafeOutputs.PushToPullRequestBranch != nil { pushToBranchJob, err := c.buildCreateOutputPushToBranchJob(data, jobName) if err != nil { return fmt.Errorf("failed to build push_to_pr_branch job: %w", err) @@ -2937,7 +2937,7 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat c.generateUploadAgentLogs(yaml, logFile, logFileFull) // Add git patch generation step only if safe-outputs create-pull-request feature is used - if data.SafeOutputs != nil && (data.SafeOutputs.CreatePullRequests != nil || data.SafeOutputs.PushToBranch != nil) { + if data.SafeOutputs != nil && (data.SafeOutputs.CreatePullRequests != nil || data.SafeOutputs.PushToPullRequestBranch != nil) { c.generateGitPatchStep(yaml, data) } @@ -3112,7 +3112,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { written = true } - if data.SafeOutputs.PushToBranch != nil { + if data.SafeOutputs.PushToPullRequestBranch != nil { if written { yaml.WriteString(", ") } @@ -3230,7 +3230,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { yaml.WriteString(" \n") } - if data.SafeOutputs.PushToBranch != nil { + if data.SafeOutputs.PushToPullRequestBranch != nil { yaml.WriteString(" **Pushing Changes to Pull Request Branch**\n") yaml.WriteString(" \n") yaml.WriteString(" To push changes to the branch of a pull request:\n") @@ -3240,7 +3240,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { yaml.WriteString(" ```json\n") var fields []string fields = append(fields, "\"type\": \"push-to-pr-branch\"") - if data.SafeOutputs.PushToBranch.Target == "*" { + if data.SafeOutputs.PushToPullRequestBranch.Target == "*" { fields = append(fields, "\"pull_number\": \"The pull number to update\"") } fields = append(fields, "\"branch_name\": \"The name of the branch to push to, should be the branch name associated with the pull request\"") @@ -3308,7 +3308,7 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { yaml.WriteString(" {\"type\": \"add-issue-label\", \"labels\": [\"bug\", \"priority-high\"]}\n") exampleCount++ } - if data.SafeOutputs.PushToBranch != nil { + if data.SafeOutputs.PushToPullRequestBranch != nil { yaml.WriteString(" {\"type\": \"push-to-pr-branch\", \"message\": \"Update documentation with latest changes\"}\n") exampleCount++ } @@ -3495,7 +3495,7 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut // Handle push-to-pr-branch pushToBranchConfig := c.parsePushToBranchConfig(outputMap) if pushToBranchConfig != nil { - config.PushToBranch = pushToBranchConfig + config.PushToPullRequestBranch = pushToBranchConfig } // Handle missing-tool (parse configuration if present) @@ -4104,12 +4104,12 @@ func (c *Compiler) generateOutputCollectionStep(yaml *strings.Builder, data *Wor if data.SafeOutputs.UpdateIssues != nil { safeOutputsConfig["update-issue"] = true } - if data.SafeOutputs.PushToBranch != nil { + if data.SafeOutputs.PushToPullRequestBranch != nil { pushToBranchConfig := map[string]interface{}{ "enabled": true, } - if data.SafeOutputs.PushToBranch.Target != "" { - pushToBranchConfig["target"] = data.SafeOutputs.PushToBranch.Target + if data.SafeOutputs.PushToPullRequestBranch.Target != "" { + pushToBranchConfig["target"] = data.SafeOutputs.PushToPullRequestBranch.Target } safeOutputsConfig["push-to-pr-branch"] = pushToBranchConfig } diff --git a/pkg/workflow/git_commands_test.go b/pkg/workflow/git_commands_test.go index e3e64dcd450..99b0f4a62a6 100644 --- a/pkg/workflow/git_commands_test.go +++ b/pkg/workflow/git_commands_test.go @@ -33,7 +33,7 @@ func TestApplyDefaultGitCommandsForSafeOutputs(t *testing.T) { name: "push-to-pr-branch enabled - should add git commands", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ - PushToBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, }, expectGit: true, }, @@ -155,7 +155,7 @@ func TestAdditionalClaudeToolsForSafeOutputs(t *testing.T) { name: "push-to-pr-branch enabled - should add editing tools", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ - PushToBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, }, expectEditingTools: true, }, @@ -270,15 +270,15 @@ func TestNeedsGitCommands(t *testing.T) { { name: "push-to-pr-branch enabled", safeOutputs: &SafeOutputsConfig{ - PushToBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, }, expected: true, }, { name: "both enabled", safeOutputs: &SafeOutputsConfig{ - CreatePullRequests: &CreatePullRequestsConfig{}, - PushToBranch: &PushToBranchConfig{Branch: "main"}, + CreatePullRequests: &CreatePullRequestsConfig{}, + PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, }, expected: true, }, diff --git a/pkg/workflow/git_patch.go b/pkg/workflow/git_patch.go index 60d0ff418b1..e57982ac223 100644 --- a/pkg/workflow/git_patch.go +++ b/pkg/workflow/git_patch.go @@ -9,8 +9,8 @@ func (c *Compiler) generateGitPatchStep(yaml *strings.Builder, data *WorkflowDat yaml.WriteString(" env:\n") yaml.WriteString(" GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}\n") // Add push-to-pr-branch configuration if available - if data.SafeOutputs != nil && data.SafeOutputs.PushToBranch != nil { - yaml.WriteString(" GITHUB_AW_PUSH_TARGET: \"" + data.SafeOutputs.PushToBranch.Target + "\"\n") + if data.SafeOutputs != nil && data.SafeOutputs.PushToPullRequestBranch != nil { + yaml.WriteString(" GITHUB_AW_PUSH_TARGET: \"" + data.SafeOutputs.PushToPullRequestBranch.Target + "\"\n") } yaml.WriteString(" run: |\n") yaml.WriteString(" # Check current git status\n") diff --git a/pkg/workflow/output_push_to_branch.go b/pkg/workflow/output_push_to_branch.go index 0cb5bd2135a..8d10fd22a5b 100644 --- a/pkg/workflow/output_push_to_branch.go +++ b/pkg/workflow/output_push_to_branch.go @@ -6,7 +6,7 @@ import ( // buildCreateOutputPushToBranchJob creates the push_to_pr_branch job func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobName string) (*Job, error) { - if data.SafeOutputs == nil || data.SafeOutputs.PushToBranch == nil { + if data.SafeOutputs == nil || data.SafeOutputs.PushToPullRequestBranch == nil { return nil, fmt.Errorf("safe-outputs.push-to-pr-branch configuration is required") } @@ -39,11 +39,11 @@ func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobN // Pass the agent output content from the main job steps = append(steps, fmt.Sprintf(" GITHUB_AW_AGENT_OUTPUT: ${{ needs.%s.outputs.output }}\n", mainJobName)) // Pass the target configuration - if data.SafeOutputs.PushToBranch.Target != "" { - steps = append(steps, fmt.Sprintf(" GITHUB_AW_PUSH_TARGET: %q\n", data.SafeOutputs.PushToBranch.Target)) + if data.SafeOutputs.PushToPullRequestBranch.Target != "" { + steps = append(steps, fmt.Sprintf(" GITHUB_AW_PUSH_TARGET: %q\n", data.SafeOutputs.PushToPullRequestBranch.Target)) } // Pass the if-no-changes configuration - steps = append(steps, fmt.Sprintf(" GITHUB_AW_PUSH_IF_NO_CHANGES: %q\n", data.SafeOutputs.PushToBranch.IfNoChanges)) + steps = append(steps, fmt.Sprintf(" GITHUB_AW_PUSH_IF_NO_CHANGES: %q\n", data.SafeOutputs.PushToPullRequestBranch.IfNoChanges)) steps = append(steps, " with:\n") steps = append(steps, " script: |\n") @@ -61,7 +61,7 @@ func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobN // Determine the job condition based on target configuration var baseCondition string - if data.SafeOutputs.PushToBranch.Target == "*" { + if data.SafeOutputs.PushToPullRequestBranch.Target == "*" { // Allow pushing to any pull request - no specific context required baseCondition = "always()" } else { From ff7ce9d8ac194579785183aa17f7a44a8c477572 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 11 Sep 2025 01:53:44 +0100 Subject: [PATCH 4/6] rename --- pkg/workflow/compiler.go | 16 ++++++++-------- pkg/workflow/git_commands_integration_test.go | 2 +- pkg/workflow/git_commands_test.go | 8 ++++---- pkg/workflow/output_push_to_branch.go | 4 ++-- pkg/workflow/output_push_to_branch_test.go | 18 +++++++++--------- schemas/agent-output.json | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 8e1ee3509c9..8569238b4d1 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -161,7 +161,7 @@ type SafeOutputsConfig struct { CreateRepositorySecurityAdvisories *CreateRepositorySecurityAdvisoriesConfig `yaml:"create-repository-security-advisory,omitempty"` AddIssueLabels *AddIssueLabelsConfig `yaml:"add-issue-label,omitempty"` UpdateIssues *UpdateIssuesConfig `yaml:"update-issue,omitempty"` - PushToPullRequestBranch *PushToBranchConfig `yaml:"push-to-pr-branch,omitempty"` + PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pr-branch,omitempty"` MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality AllowedDomains []string `yaml:"allowed-domains,omitempty"` } @@ -227,8 +227,8 @@ type UpdateIssuesConfig struct { Max int `yaml:"max,omitempty"` // Maximum number of issues to update (default: 1) } -// PushToBranchConfig holds configuration for pushing changes to a specific branch from agent output -type PushToBranchConfig struct { +// PushToPullRequestBranchConfig holds configuration for pushing changes to a specific branch from agent output +type PushToPullRequestBranchConfig struct { Target string `yaml:"target,omitempty"` // Target for push-to-pr-branch: like add-issue-comment but for pull requests IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") } @@ -1928,7 +1928,7 @@ func (c *Compiler) buildJobs(data *WorkflowData, markdownPath string) error { // Build push_to_pr_branch job if output.push-to-pr-branch is configured if data.SafeOutputs.PushToPullRequestBranch != nil { - pushToBranchJob, err := c.buildCreateOutputPushToBranchJob(data, jobName) + pushToBranchJob, err := c.buildCreateOutputPushToPullRequestBranchJob(data, jobName) if err != nil { return fmt.Errorf("failed to build push_to_pr_branch job: %w", err) } @@ -3493,7 +3493,7 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut } // Handle push-to-pr-branch - pushToBranchConfig := c.parsePushToBranchConfig(outputMap) + pushToBranchConfig := c.parsePushToPullRequestBranchConfig(outputMap) if pushToBranchConfig != nil { config.PushToPullRequestBranch = pushToBranchConfig } @@ -3781,10 +3781,10 @@ func (c *Compiler) parseUpdateIssuesConfig(outputMap map[string]any) *UpdateIssu return nil } -// parsePushToBranchConfig handles push-to-pr-branch configuration -func (c *Compiler) parsePushToBranchConfig(outputMap map[string]any) *PushToBranchConfig { +// parsePushToPullRequestBranchConfig handles push-to-pr-branch configuration +func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any) *PushToPullRequestBranchConfig { if configData, exists := outputMap["push-to-pr-branch"]; exists { - pushToBranchConfig := &PushToBranchConfig{ + pushToBranchConfig := &PushToPullRequestBranchConfig{ Branch: "triggering", // Default branch value IfNoChanges: "warn", // Default behavior: warn when no changes } diff --git a/pkg/workflow/git_commands_integration_test.go b/pkg/workflow/git_commands_integration_test.go index bf22263c555..ef33a1db0f1 100644 --- a/pkg/workflow/git_commands_integration_test.go +++ b/pkg/workflow/git_commands_integration_test.go @@ -133,7 +133,7 @@ This is a test workflow that should automatically get additional Claude tools wh } } -func TestAdditionalClaudeToolsIntegrationWithPushToBranch(t *testing.T) { +func TestAdditionalClaudeToolsIntegrationWithPushToPullRequestBranch(t *testing.T) { // Create a simple workflow with push-to-pr-branch enabled workflowContent := `--- name: Test Additional Claude Tools Integration with Push to Branch diff --git a/pkg/workflow/git_commands_test.go b/pkg/workflow/git_commands_test.go index 99b0f4a62a6..a5439d87065 100644 --- a/pkg/workflow/git_commands_test.go +++ b/pkg/workflow/git_commands_test.go @@ -33,7 +33,7 @@ func TestApplyDefaultGitCommandsForSafeOutputs(t *testing.T) { name: "push-to-pr-branch enabled - should add git commands", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ - PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, }, expectGit: true, }, @@ -155,7 +155,7 @@ func TestAdditionalClaudeToolsForSafeOutputs(t *testing.T) { name: "push-to-pr-branch enabled - should add editing tools", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ - PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, }, expectEditingTools: true, }, @@ -270,7 +270,7 @@ func TestNeedsGitCommands(t *testing.T) { { name: "push-to-pr-branch enabled", safeOutputs: &SafeOutputsConfig{ - PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, }, expected: true, }, @@ -278,7 +278,7 @@ func TestNeedsGitCommands(t *testing.T) { name: "both enabled", safeOutputs: &SafeOutputsConfig{ CreatePullRequests: &CreatePullRequestsConfig{}, - PushToPullRequestBranch: &PushToBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, }, expected: true, }, diff --git a/pkg/workflow/output_push_to_branch.go b/pkg/workflow/output_push_to_branch.go index 8d10fd22a5b..5d585cb81e1 100644 --- a/pkg/workflow/output_push_to_branch.go +++ b/pkg/workflow/output_push_to_branch.go @@ -4,8 +4,8 @@ import ( "fmt" ) -// buildCreateOutputPushToBranchJob creates the push_to_pr_branch job -func (c *Compiler) buildCreateOutputPushToBranchJob(data *WorkflowData, mainJobName string) (*Job, error) { +// buildCreateOutputPushToPullRequestBranchJob creates the push_to_pr_branch job +func (c *Compiler) buildCreateOutputPushToPullRequestBranchJob(data *WorkflowData, mainJobName string) (*Job, error) { if data.SafeOutputs == nil || data.SafeOutputs.PushToPullRequestBranch == nil { return nil, fmt.Errorf("safe-outputs.push-to-pr-branch configuration is required") } diff --git a/pkg/workflow/output_push_to_branch_test.go b/pkg/workflow/output_push_to_branch_test.go index dd8999d7238..739c37085f3 100644 --- a/pkg/workflow/output_push_to_branch_test.go +++ b/pkg/workflow/output_push_to_branch_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func TestPushToBranchConfigParsing(t *testing.T) { +func TestPushToPullRequestBranchConfigParsing(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -76,7 +76,7 @@ Please make changes and push them to the feature branch. } } -func TestPushToBranchWithTargetAsterisk(t *testing.T) { +func TestPushToPullRequestBranchWithTargetAsterisk(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -128,7 +128,7 @@ This workflow allows pushing to any pull request. } } -func TestPushToBranchDefaultBranch(t *testing.T) { +func TestPushToPullRequestBranchDefaultBranch(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -177,7 +177,7 @@ This workflow uses the default branch value. } } -func TestPushToBranchNullConfig(t *testing.T) { +func TestPushToPullRequestBranchNullConfig(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -230,7 +230,7 @@ This workflow uses null configuration which should default to "triggering". } } -func TestPushToBranchMinimalConfig(t *testing.T) { +func TestPushToPullRequestBranchMinimalConfig(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -286,7 +286,7 @@ This workflow has minimal push-to-pr-branch configuration. } } -func TestPushToBranchWithIfNoChangesError(t *testing.T) { +func TestPushToPullRequestBranchWithIfNoChangesError(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -334,7 +334,7 @@ This workflow fails when there are no changes. } } -func TestPushToBranchWithIfNoChangesIgnore(t *testing.T) { +func TestPushToPullRequestBranchWithIfNoChangesIgnore(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -381,7 +381,7 @@ This workflow ignores when there are no changes. } } -func TestPushToBranchDefaultIfNoChanges(t *testing.T) { +func TestPushToPullRequestBranchDefaultIfNoChanges(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() @@ -427,7 +427,7 @@ This workflow uses default if-no-changes behavior. } } -func TestPushToBranchExplicitTriggering(t *testing.T) { +func TestPushToPullRequestBranchExplicitTriggering(t *testing.T) { // Create a temporary directory for the test tmpDir := t.TempDir() diff --git a/schemas/agent-output.json b/schemas/agent-output.json index bc8ea882a55..1732f3a5277 100644 --- a/schemas/agent-output.json +++ b/schemas/agent-output.json @@ -32,7 +32,7 @@ {"$ref": "#/$defs/CreatePullRequestOutput"}, {"$ref": "#/$defs/AddIssueLabelOutput"}, {"$ref": "#/$defs/UpdateIssueOutput"}, - {"$ref": "#/$defs/PushToBranchOutput"}, + {"$ref": "#/$defs/PushToPullRequestBranchOutput"}, {"$ref": "#/$defs/CreatePullRequestReviewCommentOutput"}, {"$ref": "#/$defs/CreateDiscussionOutput"}, {"$ref": "#/$defs/MissingToolOutput"}, @@ -170,7 +170,7 @@ "required": ["type"], "additionalProperties": false }, - "PushToBranchOutput": { + "PushToPullRequestBranchOutput": { "title": "Push to Branch Output", "description": "Output for pushing changes directly to a branch", "type": "object", From 6437c7e8dfde7e7267d29358d6b9a61621916e6f Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 11 Sep 2025 01:58:41 +0100 Subject: [PATCH 5/6] more on push to pr branch --- .../test-ai-inference-github-models.lock.yml | 2 +- .../test-claude-add-issue-comment.lock.yml | 2 +- .../test-claude-add-issue-labels.lock.yml | 2 +- .../workflows/test-claude-command.lock.yml | 2 +- .../test-claude-create-issue.lock.yml | 2 +- ...reate-pull-request-review-comment.lock.yml | 58 +- .../test-claude-create-pull-request.lock.yml | 11 +- ...eate-repository-security-advisory.lock.yml | 17 +- .github/workflows/test-claude-mcp.lock.yml | 2 +- .../test-claude-push-to-branch.lock.yml | 88 +- .../test-claude-push-to-pr-branch.lock.yml | 1984 +++++++++++++++++ ...ch.md => test-claude-push-to-pr-branch.md} | 0 .../test-claude-update-issue.lock.yml | 2 +- .../test-codex-add-issue-comment.lock.yml | 3 +- .../test-codex-add-issue-labels.lock.yml | 3 +- .github/workflows/test-codex-command.lock.yml | 2 +- .../test-codex-create-issue.lock.yml | 3 +- ...reate-pull-request-review-comment.lock.yml | 59 +- .../test-codex-create-pull-request.lock.yml | 12 +- ...eate-repository-security-advisory.lock.yml | 18 +- .github/workflows/test-codex-mcp.lock.yml | 3 +- ... => test-codex-push-to-pr-branch.lock.yml} | 85 +- ...nch.md => test-codex-push-to-pr-branch.md} | 0 .../test-codex-update-issue.lock.yml | 3 +- .../test-custom-safe-outputs.lock.yml | 87 +- .github/workflows/test-proxy.lock.yml | 2 +- pkg/workflow/compiler.go | 3 +- 27 files changed, 2284 insertions(+), 171 deletions(-) create mode 100644 .github/workflows/test-claude-push-to-pr-branch.lock.yml rename .github/workflows/{test-claude-push-to-branch.md => test-claude-push-to-pr-branch.md} (100%) rename .github/workflows/{test-codex-push-to-branch.lock.yml => test-codex-push-to-pr-branch.lock.yml} (96%) rename .github/workflows/{test-codex-push-to-branch.md => test-codex-push-to-pr-branch.md} (100%) diff --git a/.github/workflows/test-ai-inference-github-models.lock.yml b/.github/workflows/test-ai-inference-github-models.lock.yml index 5beffae5ffb..de83c392067 100644 --- a/.github/workflows/test-ai-inference-github-models.lock.yml +++ b/.github/workflows/test-ai-inference-github-models.lock.yml @@ -698,7 +698,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index 64fc28f5d27..63f88feb411 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -885,7 +885,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index de6892a4637..895cb93291c 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -885,7 +885,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index 510286e2476..52104adace0 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -1058,7 +1058,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index e6f5a41d85f..3d368773556 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -558,7 +558,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml index 9fb67065db4..aa65e1fab95 100644 --- a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -630,7 +630,28 @@ jobs: # Ensure log file exists touch /tmp/test-claude-create-pull-request-review-comment.log - - name: Collect agent output + - name: Print Agent output + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + run: | + echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````json' >> $GITHUB_STEP_SUMMARY + cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY + # Ensure there's a newline after the file content if it doesn't end with one + if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo '``````' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: safe_output.jsonl + path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output id: collect_output uses: actions/github-script@v7 env: @@ -802,7 +823,7 @@ jobs: case "missing-tool": return 1000; // Allow many missing tool reports (default: unlimited) case "create-repository-security-advisory": - return 1000; // Allow many security reports (default: unlimited) + return 1000; // Allow many repository security advisories (default: unlimited) default: return 1; // Default to single item for unknown types } @@ -814,6 +835,15 @@ jobs: */ function repairJson(jsonStr) { let repaired = jsonStr.trim(); + // remove invalid control characters like + // U+0014 (DC4) — represented here as "\u0014" + // Escape control characters not allowed in JSON strings (U+0000 through U+001F) + // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; + repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { + const c = ch.charCodeAt(0); + return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); + }); // Fix single quotes to double quotes (must be done first) repaired = repaired.replace(/'/g, '"'); // Fix missing quotes around object keys @@ -883,6 +913,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -1327,33 +1358,14 @@ jobs: } // Call the main function await main(); - - name: Print agent output to step summary - env: - GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + - name: Print sanitized agent output run: | - echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````json' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY - # Ensure there's a newline after the file content if it doesn't end with one - if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - fi - echo '``````' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY echo "## Processed Output" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo '``````json' >> $GITHUB_STEP_SUMMARY echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() && steps.collect_output.outputs.output != '' - uses: actions/upload-artifact@v4 - with: - name: safe_output.jsonl - path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Upload agent output JSON + - name: Upload sanitized agent output if: always() && env.GITHUB_AW_AGENT_OUTPUT uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index fe035b13364..5056c26b1e1 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -601,7 +601,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1495,11 +1495,10 @@ jobs: # Extract branch from push-to-pr-branch line using simple grep and sed elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then echo "Found push-to-pr-branch line: $line" - # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow - # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH - if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then - BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-pr-branch target: $BRANCH_NAME" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi fi diff --git a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml index 91aa290572d..197379aab84 100644 --- a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml @@ -820,7 +820,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1741,13 +1741,18 @@ jobs: } // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -1782,7 +1787,9 @@ jobs: ); // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } if ( diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index a6ddbb67e3f..16816c2503a 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -840,7 +840,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-claude-push-to-branch.lock.yml b/.github/workflows/test-claude-push-to-branch.lock.yml index 9f19f84efdc..a6173e17b41 100644 --- a/.github/workflows/test-claude-push-to-branch.lock.yml +++ b/.github/workflows/test-claude-push-to-branch.lock.yml @@ -95,7 +95,7 @@ jobs: } await main(); - test-claude-push-to-pr-branch: + test-claude-push-to-branch: needs: task if: > contains(github.event.issue.body, '/test-claude-push-to-pr-branch') || contains(github.event.comment.body, '/test-claude-push-to-pr-branch') || @@ -325,14 +325,14 @@ jobs: ### Available Output Types: - **Pushing Changes to Branch** + **Pushing Changes to Pull Request Branch** - To push changes to a branch, for example to add code to a pull request: + To push changes to the branch of a pull request: 1. Make any file changes directly in the working directory - 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. - 3. Indicate your intention to push to the branch by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": + 2. Add and commit your changes to the local copy of the pull request branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. + 3. Indicate your intention to push the branch to the repo by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": ```json - {"type": "push-to-pr-branch", "message": "Commit message describing the changes"} + {, "type": "push-to-pr-branch", "branch_name": "The name of the branch to push to, should be the branch name associated with the pull request", "message": "Commit message describing the changes"} ``` 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up @@ -481,13 +481,13 @@ jobs: run: | # Copy the detailed execution file from Agentic Action if available if [ -n "${{ steps.agentic_execution.outputs.execution_file }}" ] && [ -f "${{ steps.agentic_execution.outputs.execution_file }}" ]; then - cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-claude-push-to-pr-branch.log + cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-claude-push-to-branch.log else - echo "No execution file output found from Agentic Action" >> /tmp/test-claude-push-to-pr-branch.log + echo "No execution file output found from Agentic Action" >> /tmp/test-claude-push-to-branch.log fi # Ensure log file exists - touch /tmp/test-claude-push-to-pr-branch.log + touch /tmp/test-claude-push-to-branch.log - name: Print Agent output env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -514,7 +514,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"branch\":\"triggering\",\"enabled\":true}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"enabled\":true}}" with: script: | async function main() { @@ -697,7 +697,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1244,7 +1244,7 @@ jobs: if: always() uses: actions/github-script@v7 env: - AGENT_LOG_FILE: /tmp/test-claude-push-to-pr-branch.log + AGENT_LOG_FILE: /tmp/test-claude-push-to-branch.log with: script: | function main() { @@ -1561,14 +1561,14 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-claude-push-to-pr-branch.log - path: /tmp/test-claude-push-to-pr-branch.log + name: test-claude-push-to-branch.log + path: /tmp/test-claude-push-to-branch.log if-no-files-found: warn - name: Generate git patch if: always() env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_PUSH_BRANCH: "triggering" + GITHUB_AW_PUSH_TARGET: "" run: | # Check current git status echo "Current git status:" @@ -1592,11 +1592,10 @@ jobs: # Extract branch from push-to-pr-branch line using simple grep and sed elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then echo "Found push-to-pr-branch line: $line" - # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow - # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH - if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then - BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-pr-branch target: $BRANCH_NAME" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi fi @@ -1683,7 +1682,7 @@ jobs: if-no-files-found: ignore push_to_pr_branch: - needs: test-claude-push-to-pr-branch + needs: test-claude-push-to-branch if: > (contains(github.event.issue.body, '/test-claude-push-to-pr-branch') || contains(github.event.comment.body, '/test-claude-push-to-pr-branch') || contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch')) && (github.event.pull_request.number) @@ -1716,8 +1715,7 @@ jobs: id: push_to_pr_branch uses: actions/github-script@v7 env: - GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-claude-push-to-pr-branch.outputs.output }} - GITHUB_AW_PUSH_BRANCH: "triggering" + GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-claude-push-to-branch.outputs.output }} GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" with: script: | @@ -1726,11 +1724,6 @@ jobs: const fs = require("fs"); const { execSync } = require("child_process"); // Environment validation - fail early if required variables are missing - const branchName = process.env.GITHUB_AW_PUSH_BRANCH; - if (!branchName) { - core.setFailed("GITHUB_AW_PUSH_BRANCH environment variable is required"); - return; - } const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT || ""; if (outputContent.trim() === "") { console.log("Agent output content is empty"); @@ -1796,7 +1789,6 @@ jobs: if (!isEmpty) { console.log("Patch content validation passed"); } - console.log("Target branch:", branchName); console.log("Target configuration:", target); // Parse the validated output JSON let validatedOutput; @@ -1825,8 +1817,8 @@ jobs: // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number - const targetNumber = parseInt(target, 10); - if (isNaN(targetNumber)) { + const pullNumber = parseInt(target, 10); + if (isNaN(pullNumber)) { core.setFailed( 'Invalid target configuration: must be "triggering", "*", or a valid pull request number' ); @@ -1840,6 +1832,40 @@ jobs: ); return; } + // Compute the target branch name based on target configuration + let pullNumber; + if (target === "triggering") { + // Use the number of the triggering pull request + pullNumber = context.payload.pull_request.number; + } else if (target === "*") { + if (pushItem.pull_number) { + pullNumber = parseInt(pushItem.pull_number, 10); + } + } else { + // Target is a specific pull request number + pullNumber = parseInt(target, 10); + } + let branchName; + // Fetch the specific PR to get its head branch + try { + const prInfo = execSync( + `gh pr view ${pullNumber} --json headRefName --jq '.headRefName'`, + { encoding: "utf8" } + ).trim(); + if (prInfo) { + branchName = prInfo; + } else { + throw new Error("No head branch found for PR"); + } + } catch (error) { + console.log(`Warning: Could not fetch PR ${pullNumber} details: ${error.message}`); + // Exit with failure if we cannot determine the branch name + core.setFailed(`Failed to determine branch name for PR ${pullNumber}`); + return; + } + console.log("Target branch:", branchName); + // Check if patch has actual changes (not just empty) + const hasChanges = !isEmpty; // Switch to or create the target branch console.log("Switching to branch:", branchName); try { diff --git a/.github/workflows/test-claude-push-to-pr-branch.lock.yml b/.github/workflows/test-claude-push-to-pr-branch.lock.yml new file mode 100644 index 00000000000..a6173e17b41 --- /dev/null +++ b/.github/workflows/test-claude-push-to-pr-branch.lock.yml @@ -0,0 +1,1984 @@ +# This file was automatically generated by gh-aw. DO NOT EDIT. +# To update this file, edit the corresponding .md file and run: +# gh aw compile + +name: "Test Claude Push to Branch" +on: + issues: + types: [opened, edited, reopened] + issue_comment: + types: [created, edited] + pull_request: + types: [opened, edited, reopened] + pull_request_review_comment: + types: [created, edited] + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" + +run-name: "Test Claude Push to Branch" + +jobs: + task: + if: > + ((contains(github.event.issue.body, '/test-claude-push-to-pr-branch')) || (contains(github.event.comment.body, '/test-claude-push-to-pr-branch'))) || + (contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch')) + runs-on: ubuntu-latest + steps: + - name: Check team membership for command workflow + id: check-team-member + uses: actions/github-script@v7 + env: + GITHUB_AW_REQUIRED_ROLES: admin,maintainer + with: + script: | + async function main() { + const { eventName } = context; + // skip check for safe events + const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; + if (safeEvents.includes(eventName)) { + console.log(`✅ Event ${eventName} does not require validation`); + return; + } + const actor = context.actor; + const { owner, repo } = context.repo; + const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; + const requiredPermissions = requiredPermissionsEnv + ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") + : []; + if (!requiredPermissions || requiredPermissions.length === 0) { + core.error( + "❌ Configuration error: Required permissions not specified. Contact repository administrator." + ); + process.exit(1); + } + // Check if the actor has the required repository permissions + try { + console.log( + `Checking if user '${actor}' has required permissions for ${owner}/${repo}` + ); + console.log(`Required permissions: ${requiredPermissions.join(", ")}`); + const repoPermission = + await github.rest.repos.getCollaboratorPermissionLevel({ + owner: owner, + repo: repo, + username: actor, + }); + const permission = repoPermission.data.permission; + console.log(`Repository permission level: ${permission}`); + // Check if user has one of the required permission levels + for (const requiredPerm of requiredPermissions) { + if ( + permission === requiredPerm || + (requiredPerm === "maintainer" && permission === "maintain") + ) { + console.log(`✅ User has ${permission} access to repository`); + return; + } + } + console.log( + `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` + ); + } catch (repoError) { + const errorMessage = + repoError instanceof Error ? repoError.message : String(repoError); + core.error(`Repository permission check failed: ${errorMessage}`); + process.exit(1); + } + // Fail the job when permission check fails + core.warning( + `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` + ); + process.exit(78); + } + await main(); + + test-claude-push-to-branch: + needs: task + if: > + contains(github.event.issue.body, '/test-claude-push-to-pr-branch') || contains(github.event.comment.body, '/test-claude-push-to-pr-branch') || + contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch') + runs-on: ubuntu-latest + permissions: read-all + outputs: + output: ${{ steps.collect_output.outputs.output }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Configure Git credentials + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "${{ github.workflow }}" + echo "Git configured with standard GitHub Actions identity" + - name: Generate Claude Settings + run: | + mkdir -p /tmp/.claude + cat > /tmp/.claude/settings.json << 'EOF' + { + "hooks": { + "PreToolUse": [ + { + "matcher": "WebFetch|WebSearch", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/network_permissions.py" + } + ] + } + ] + } + } + EOF + - name: Generate Network Permissions Hook + run: | + mkdir -p .claude/hooks + cat > .claude/hooks/network_permissions.py << 'EOF' + #!/usr/bin/env python3 + """ + Network permissions validator for Claude Code engine. + Generated by gh-aw from engine network permissions configuration. + """ + + import json + import sys + import urllib.parse + import re + + # Domain allow-list (populated during generation) + ALLOWED_DOMAINS = ["crl3.digicert.com","crl4.digicert.com","ocsp.digicert.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","crl.geotrust.com","ocsp.geotrust.com","crl.thawte.com","ocsp.thawte.com","crl.verisign.com","ocsp.verisign.com","crl.globalsign.com","ocsp.globalsign.com","crls.ssl.com","ocsp.ssl.com","crl.identrust.com","ocsp.identrust.com","crl.sectigo.com","ocsp.sectigo.com","crl.usertrust.com","ocsp.usertrust.com","s.symcb.com","s.symcd.com","json-schema.org","json.schemastore.org","archive.ubuntu.com","security.ubuntu.com","ppa.launchpad.net","keyserver.ubuntu.com","azure.archive.ubuntu.com","api.snapcraft.io","packagecloud.io","packages.cloud.google.com","packages.microsoft.com"] + + def extract_domain(url_or_query): + """Extract domain from URL or search query.""" + if not url_or_query: + return None + + if url_or_query.startswith(('http://', 'https://')): + return urllib.parse.urlparse(url_or_query).netloc.lower() + + # Check for domain patterns in search queries + match = re.search(r'site:([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', url_or_query) + if match: + return match.group(1).lower() + + return None + + def is_domain_allowed(domain): + """Check if domain is allowed.""" + if not domain: + # If no domain detected, allow only if not under deny-all policy + return bool(ALLOWED_DOMAINS) # False if empty list (deny-all), True if has domains + + # Empty allowed domains means deny all + if not ALLOWED_DOMAINS: + return False + + for pattern in ALLOWED_DOMAINS: + regex = pattern.replace('.', r'\.').replace('*', '.*') + if re.match(f'^{regex}$', domain): + return True + return False + + # Main logic + try: + data = json.load(sys.stdin) + tool_name = data.get('tool_name', '') + tool_input = data.get('tool_input', {}) + + if tool_name not in ['WebFetch', 'WebSearch']: + sys.exit(0) # Allow other tools + + target = tool_input.get('url') or tool_input.get('query', '') + domain = extract_domain(target) + + # For WebSearch, apply domain restrictions consistently + # If no domain detected in search query, check if restrictions are in place + if tool_name == 'WebSearch' and not domain: + # Since this hook is only generated when network permissions are configured, + # empty ALLOWED_DOMAINS means deny-all policy + if not ALLOWED_DOMAINS: # Empty list means deny all + print(f"Network access blocked: deny-all policy in effect", file=sys.stderr) + print(f"No domains are allowed for WebSearch", file=sys.stderr) + sys.exit(2) # Block under deny-all policy + else: + print(f"Network access blocked for web-search: no specific domain detected", file=sys.stderr) + print(f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}", file=sys.stderr) + sys.exit(2) # Block general searches when domain allowlist is configured + + if not is_domain_allowed(domain): + print(f"Network access blocked for domain: {domain}", file=sys.stderr) + print(f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}", file=sys.stderr) + sys.exit(2) # Block with feedback to Claude + + sys.exit(0) # Allow + + except Exception as e: + print(f"Network validation error: {e}", file=sys.stderr) + sys.exit(2) # Block on errors + + EOF + chmod +x .claude/hooks/network_permissions.py + - name: Setup agent output + id: setup_agent_output + uses: actions/github-script@v7 + with: + script: | + function main() { + const fs = require("fs"); + const crypto = require("crypto"); + // Generate a random filename for the output file + const randomId = crypto.randomBytes(8).toString("hex"); + const outputFile = `/tmp/aw_output_${randomId}.txt`; + // Ensure the /tmp directory exists and create empty output file + fs.mkdirSync("/tmp", { recursive: true }); + fs.writeFileSync(outputFile, "", { mode: 0o644 }); + // Verify the file was created and is writable + if (!fs.existsSync(outputFile)) { + throw new Error(`Failed to create output file: ${outputFile}`); + } + // Set the environment variable for subsequent steps + core.exportVariable("GITHUB_AW_SAFE_OUTPUTS", outputFile); + console.log("Created agentic output file:", outputFile); + // Also set as step output for reference + core.setOutput("output_file", outputFile); + } + main(); + - name: Setup MCPs + run: | + mkdir -p /tmp/mcp-config + cat > /tmp/mcp-config/mcp-servers.json << 'EOF' + { + "mcpServers": { + "github": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server:sha-09deac4" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + } + } + } + } + EOF + - name: Create prompt + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/aw-prompts + cat > $GITHUB_AW_PROMPT << 'EOF' + # Test Claude Push to Branch + + This test workflow specifically tests multi-commit functionality in push-to-pr-branch. + + **IMPORTANT: Create multiple separate commits for this test case** + + 1. **First commit**: Create a file called "README-claude-test.md" with: + ```markdown + # Claude Push-to-Branch Multi-Commit Test + + This file was created by the Claude agentic workflow to test the multi-commit push-to-pr-branch functionality. + + Created at: {{ current timestamp }} + + ## Purpose + This test verifies that multiple commits are properly applied when using push-to-pr-branch. + ``` + + 2. **Second commit**: Create a Python script called "claude-script.py" with: + ```python + #!/usr/bin/env python3 + """ + Multi-commit test script created by Claude agentic workflow + """ + + import datetime + + def main(): + print("Hello from Claude agentic workflow!") + print(f"Current time: {datetime.datetime.now()}") + print("This script was created to test multi-commit push-to-pr-branch functionality.") + print("This is commit #2 in the multi-commit test.") + + if __name__ == "__main__": + main() + ``` + + Push these changes to the branch for the pull request #${github.event.pull_request.number} + + + --- + + ## Pushing Changes to Branch, Reporting Missing Tools or Functionality + + **IMPORTANT**: To do the actions mentioned in the header of this section, do NOT attempt to use MCP tools, do NOT attempt to use `gh`, do NOT attempt to use the GitHub API. You don't have write access to the GitHub repo. Instead write JSON objects to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}". Each line should contain a single JSON object (JSONL format). You can write them one by one as you do them. + + **Format**: Write one JSON object per line. Each object must have a `type` field specifying the action type. + + ### Available Output Types: + + **Pushing Changes to Pull Request Branch** + + To push changes to the branch of a pull request: + 1. Make any file changes directly in the working directory + 2. Add and commit your changes to the local copy of the pull request branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. + 3. Indicate your intention to push the branch to the repo by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": + ```json + {, "type": "push-to-pr-branch", "branch_name": "The name of the branch to push to, should be the branch name associated with the pull request", "message": "Commit message describing the changes"} + ``` + 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up + + **Example JSONL file content:** + ``` + {"type": "push-to-pr-branch", "message": "Update documentation with latest changes"} + ``` + + **Important Notes:** + - Do NOT attempt to use MCP tools, `gh`, or the GitHub API for these actions + - Each JSON object must be on its own line + - Only include output types that are configured for this workflow + - The content of this file will be automatically processed and executed + + EOF + - name: Print prompt to step summary + run: | + echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat $GITHUB_AW_PROMPT >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Generate agentic run info + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "", + version: "", + workflow_name: "Test Claude Push to Branch", + experimental: false, + supports_tools_whitelist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + created_at: new Date().toISOString() + }; + + // Write to /tmp directory to avoid inclusion in PR + const tmpPath = '/tmp/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + - name: Upload agentic run info + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_info.json + path: /tmp/aw_info.json + if-no-files-found: warn + - name: Execute Claude Code Action + id: agentic_execution + uses: anthropics/claude-code-base-action@v0.0.56 + with: + # Allowed tools (sorted): + # - Bash(git add:*) + # - Bash(git branch:*) + # - Bash(git checkout:*) + # - Bash(git commit:*) + # - Bash(git merge:*) + # - Bash(git rm:*) + # - Bash(git switch:*) + # - BashOutput + # - Edit + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - MultiEdit + # - NotebookEdit + # - NotebookRead + # - Read + # - Task + # - TodoWrite + # - Write + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_issue + # - mcp__github__get_issue_comments + # - mcp__github__get_job_logs + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issues + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + allowed_tools: "Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git switch:*),BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issues,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_secret_scanning_alerts,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + claude_env: | + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + mcp_config: /tmp/mcp-config/mcp-servers.json + prompt_file: /tmp/aw-prompts/prompt.txt + settings: /tmp/.claude/settings.json + timeout_minutes: 5 + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + - name: Capture Agentic Action logs + if: always() + run: | + # Copy the detailed execution file from Agentic Action if available + if [ -n "${{ steps.agentic_execution.outputs.execution_file }}" ] && [ -f "${{ steps.agentic_execution.outputs.execution_file }}" ]; then + cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-claude-push-to-branch.log + else + echo "No execution file output found from Agentic Action" >> /tmp/test-claude-push-to-branch.log + fi + + # Ensure log file exists + touch /tmp/test-claude-push-to-branch.log + - name: Print Agent output + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + run: | + echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````json' >> $GITHUB_STEP_SUMMARY + cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY + # Ensure there's a newline after the file content if it doesn't end with one + if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo '``````' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: safe_output.jsonl + path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@v7 + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"enabled\":true}}" + with: + script: | + async function main() { + const fs = require("fs"); + /** + * Sanitizes content for safe output in GitHub Actions + * @param {string} content - The content to sanitize + * @returns {string} The sanitized content + */ + function sanitizeContent(content) { + if (!content || typeof content !== "string") { + return ""; + } + // Read allowed domains from environment variable + const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS; + const defaultAllowedDomains = [ + "github.com", + "github.io", + "githubusercontent.com", + "githubassets.com", + "github.dev", + "codespaces.new", + ]; + const allowedDomains = allowedDomainsEnv + ? allowedDomainsEnv + .split(",") + .map(d => d.trim()) + .filter(d => d) + : defaultAllowedDomains; + let sanitized = content; + // Neutralize @mentions to prevent unintended notifications + sanitized = neutralizeMentions(sanitized); + // Remove control characters (except newlines and tabs) + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); + // XML character escaping + sanitized = sanitized + .replace(/&/g, "&") // Must be first to avoid double-escaping + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + // URI filtering - replace non-https protocols with "(redacted)" + sanitized = sanitizeUrlProtocols(sanitized); + // Domain filtering for HTTPS URIs + sanitized = sanitizeUrlDomains(sanitized); + // Limit total length to prevent DoS (0.5MB max) + const maxLength = 524288; + if (sanitized.length > maxLength) { + sanitized = + sanitized.substring(0, maxLength) + + "\n[Content truncated due to length]"; + } + // Limit number of lines to prevent log flooding (65k max) + const lines = sanitized.split("\n"); + const maxLines = 65000; + if (lines.length > maxLines) { + sanitized = + lines.slice(0, maxLines).join("\n") + + "\n[Content truncated due to line count]"; + } + // Remove ANSI escape sequences + sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + // Neutralize common bot trigger phrases + sanitized = neutralizeBotTriggers(sanitized); + // Trim excessive whitespace + return sanitized.trim(); + /** + * Remove unknown domains + * @param {string} s - The string to process + * @returns {string} The string with unknown domains redacted + */ + function sanitizeUrlDomains(s) { + return s.replace( + /\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, + (match, domain) => { + // Extract the hostname part (before first slash, colon, or other delimiter) + const hostname = domain.split(/[\/:\?#]/)[0].toLowerCase(); + // Check if this domain or any parent domain is in the allowlist + const isAllowed = allowedDomains.some(allowedDomain => { + const normalizedAllowed = allowedDomain.toLowerCase(); + return ( + hostname === normalizedAllowed || + hostname.endsWith("." + normalizedAllowed) + ); + }); + return isAllowed ? match : "(redacted)"; + } + ); + } + /** + * Remove unknown protocols except https + * @param {string} s - The string to process + * @returns {string} The string with non-https protocols redacted + */ + function sanitizeUrlProtocols(s) { + // Match both protocol:// and protocol: patterns + return s.replace( + /\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, + (match, protocol) => { + // Allow https (case insensitive), redact everything else + return protocol.toLowerCase() === "https" ? match : "(redacted)"; + } + ); + } + /** + * Neutralizes @mentions by wrapping them in backticks + * @param {string} s - The string to process + * @returns {string} The string with neutralized mentions + */ + function neutralizeMentions(s) { + // Replace @name or @org/team outside code with `@name` + return s.replace( + /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, + (_m, p1, p2) => `${p1}\`@${p2}\`` + ); + } + /** + * Neutralizes bot trigger phrases by wrapping them in backticks + * @param {string} s - The string to process + * @returns {string} The string with neutralized bot triggers + */ + function neutralizeBotTriggers(s) { + // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc. + return s.replace( + /\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, + (match, action, ref) => `\`${action} #${ref}\`` + ); + } + } + /** + * Gets the maximum allowed count for a given output type + * @param {string} itemType - The output item type + * @param {Object} config - The safe-outputs configuration + * @returns {number} The maximum allowed count + */ + function getMaxAllowedForType(itemType, config) { + // Check if max is explicitly specified in config + if ( + config && + config[itemType] && + typeof config[itemType] === "object" && + config[itemType].max + ) { + return config[itemType].max; + } + // Use default limits for plural-supported types + switch (itemType) { + case "create-issue": + return 1; // Only one issue allowed + case "add-issue-comment": + return 1; // Only one comment allowed + case "create-pull-request": + return 1; // Only one pull request allowed + case "create-pull-request-review-comment": + return 10; // Default to 10 review comments allowed + case "add-issue-label": + return 5; // Only one labels operation allowed + case "update-issue": + return 1; // Only one issue update allowed + case "push-to-pr-branch": + return 1; // Only one push to branch allowed + case "create-discussion": + return 1; // Only one discussion allowed + case "missing-tool": + return 1000; // Allow many missing tool reports (default: unlimited) + case "create-repository-security-advisory": + return 1000; // Allow many repository security advisories (default: unlimited) + default: + return 1; // Default to single item for unknown types + } + } + /** + * Attempts to repair common JSON syntax issues in LLM-generated content + * @param {string} jsonStr - The potentially malformed JSON string + * @returns {string} The repaired JSON string + */ + function repairJson(jsonStr) { + let repaired = jsonStr.trim(); + // remove invalid control characters like + // U+0014 (DC4) — represented here as "\u0014" + // Escape control characters not allowed in JSON strings (U+0000 through U+001F) + // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; + repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { + const c = ch.charCodeAt(0); + return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); + }); + // Fix single quotes to double quotes (must be done first) + repaired = repaired.replace(/'/g, '"'); + // Fix missing quotes around object keys + repaired = repaired.replace( + /([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, + '$1"$2":' + ); + // Fix newlines and tabs inside strings by escaping them + repaired = repaired.replace(/"([^"\\]*)"/g, (match, content) => { + if ( + content.includes("\n") || + content.includes("\r") || + content.includes("\t") + ) { + const escaped = content + .replace(/\\/g, "\\\\") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t"); + return `"${escaped}"`; + } + return match; + }); + // Fix unescaped quotes inside string values + repaired = repaired.replace( + /"([^"]*)"([^":,}\]]*)"([^"]*)"(\s*[,:}\]])/g, + (match, p1, p2, p3, p4) => `"${p1}\\"${p2}\\"${p3}"${p4}` + ); + // Fix wrong bracket/brace types - arrays should end with ] not } + repaired = repaired.replace( + /(\[\s*(?:"[^"]*"(?:\s*,\s*"[^"]*")*\s*),?)\s*}/g, + "$1]" + ); + // Fix missing closing braces/brackets + const openBraces = (repaired.match(/\{/g) || []).length; + const closeBraces = (repaired.match(/\}/g) || []).length; + if (openBraces > closeBraces) { + repaired += "}".repeat(openBraces - closeBraces); + } else if (closeBraces > openBraces) { + repaired = "{".repeat(closeBraces - openBraces) + repaired; + } + // Fix missing closing brackets for arrays + const openBrackets = (repaired.match(/\[/g) || []).length; + const closeBrackets = (repaired.match(/\]/g) || []).length; + if (openBrackets > closeBrackets) { + repaired += "]".repeat(openBrackets - closeBrackets); + } else if (closeBrackets > openBrackets) { + repaired = "[".repeat(closeBrackets - openBrackets) + repaired; + } + // Fix trailing commas in objects and arrays (AFTER fixing brackets/braces) + repaired = repaired.replace(/,(\s*[}\]])/g, "$1"); + return repaired; + } + /** + * Attempts to parse JSON with repair fallback + * @param {string} jsonStr - The JSON string to parse + * @returns {Object|undefined} The parsed JSON object, or undefined if parsing fails + */ + function parseJsonWithRepair(jsonStr) { + try { + // First, try normal JSON.parse + return JSON.parse(jsonStr); + } catch (originalError) { + try { + // If that fails, try repairing and parsing again + const repairedJson = repairJson(jsonStr); + return JSON.parse(repairedJson); + } catch (repairError) { + // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); + throw new Error( + `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` + ); + } + } + } + const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS; + const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG; + if (!outputFile) { + console.log("GITHUB_AW_SAFE_OUTPUTS not set, no output to collect"); + core.setOutput("output", ""); + return; + } + if (!fs.existsSync(outputFile)) { + console.log("Output file does not exist:", outputFile); + core.setOutput("output", ""); + return; + } + const outputContent = fs.readFileSync(outputFile, "utf8"); + if (outputContent.trim() === "") { + console.log("Output file is empty"); + core.setOutput("output", ""); + return; + } + console.log("Raw output content length:", outputContent.length); + // Parse the safe-outputs configuration + let expectedOutputTypes = {}; + if (safeOutputsConfig) { + try { + expectedOutputTypes = JSON.parse(safeOutputsConfig); + console.log("Expected output types:", Object.keys(expectedOutputTypes)); + } catch (error) { + console.log( + "Warning: Could not parse safe-outputs config:", + error.message + ); + } + } + // Parse JSONL content + const lines = outputContent.trim().split("\n"); + const parsedItems = []; + const errors = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line === "") continue; // Skip empty lines + try { + const item = parseJsonWithRepair(line); + // If item is undefined (failed to parse), add error and process next line + if (item === undefined) { + errors.push(`Line ${i + 1}: Invalid JSON - JSON parsing failed`); + continue; + } + // Validate that the item has a 'type' field + if (!item.type) { + errors.push(`Line ${i + 1}: Missing required 'type' field`); + continue; + } + // Validate against expected output types + const itemType = item.type; + if (!expectedOutputTypes[itemType]) { + errors.push( + `Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(", ")}` + ); + continue; + } + // Check for too many items of the same type + const typeCount = parsedItems.filter( + existing => existing.type === itemType + ).length; + const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes); + if (typeCount >= maxAllowed) { + errors.push( + `Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.` + ); + continue; + } + // Basic validation based on type + switch (itemType) { + case "create-issue": + if (!item.title || typeof item.title !== "string") { + errors.push( + `Line ${i + 1}: create-issue requires a 'title' string field` + ); + continue; + } + if (!item.body || typeof item.body !== "string") { + errors.push( + `Line ${i + 1}: create-issue requires a 'body' string field` + ); + continue; + } + // Sanitize text content + item.title = sanitizeContent(item.title); + item.body = sanitizeContent(item.body); + // Sanitize labels if present + if (item.labels && Array.isArray(item.labels)) { + item.labels = item.labels.map(label => + typeof label === "string" ? sanitizeContent(label) : label + ); + } + break; + case "add-issue-comment": + if (!item.body || typeof item.body !== "string") { + errors.push( + `Line ${i + 1}: add-issue-comment requires a 'body' string field` + ); + continue; + } + // Sanitize text content + item.body = sanitizeContent(item.body); + break; + case "create-pull-request": + if (!item.title || typeof item.title !== "string") { + errors.push( + `Line ${i + 1}: create-pull-request requires a 'title' string field` + ); + continue; + } + if (!item.body || typeof item.body !== "string") { + errors.push( + `Line ${i + 1}: create-pull-request requires a 'body' string field` + ); + continue; + } + // Sanitize text content + item.title = sanitizeContent(item.title); + item.body = sanitizeContent(item.body); + // Sanitize branch name if present + if (item.branch && typeof item.branch === "string") { + item.branch = sanitizeContent(item.branch); + } + // Sanitize labels if present + if (item.labels && Array.isArray(item.labels)) { + item.labels = item.labels.map(label => + typeof label === "string" ? sanitizeContent(label) : label + ); + } + break; + case "add-issue-label": + if (!item.labels || !Array.isArray(item.labels)) { + errors.push( + `Line ${i + 1}: add-issue-label requires a 'labels' array field` + ); + continue; + } + if (item.labels.some(label => typeof label !== "string")) { + errors.push( + `Line ${i + 1}: add-issue-label labels array must contain only strings` + ); + continue; + } + // Sanitize label strings + item.labels = item.labels.map(label => sanitizeContent(label)); + break; + case "update-issue": + // Check that at least one updateable field is provided + const hasValidField = + item.status !== undefined || + item.title !== undefined || + item.body !== undefined; + if (!hasValidField) { + errors.push( + `Line ${i + 1}: update-issue requires at least one of: 'status', 'title', or 'body' fields` + ); + continue; + } + // Validate status if provided + if (item.status !== undefined) { + if ( + typeof item.status !== "string" || + (item.status !== "open" && item.status !== "closed") + ) { + errors.push( + `Line ${i + 1}: update-issue 'status' must be 'open' or 'closed'` + ); + continue; + } + } + // Validate title if provided + if (item.title !== undefined) { + if (typeof item.title !== "string") { + errors.push( + `Line ${i + 1}: update-issue 'title' must be a string` + ); + continue; + } + item.title = sanitizeContent(item.title); + } + // Validate body if provided + if (item.body !== undefined) { + if (typeof item.body !== "string") { + errors.push( + `Line ${i + 1}: update-issue 'body' must be a string` + ); + continue; + } + item.body = sanitizeContent(item.body); + } + // Validate issue_number if provided (for target "*") + if (item.issue_number !== undefined) { + if ( + typeof item.issue_number !== "number" && + typeof item.issue_number !== "string" + ) { + errors.push( + `Line ${i + 1}: update-issue 'issue_number' must be a number or string` + ); + continue; + } + } + break; + case "push-to-pr-branch": + // Validate message if provided (optional) + if (item.message !== undefined) { + if (typeof item.message !== "string") { + errors.push( + `Line ${i + 1}: push-to-pr-branch 'message' must be a string` + ); + continue; + } + item.message = sanitizeContent(item.message); + } + // Validate pull_request_number if provided (for target "*") + if (item.pull_request_number !== undefined) { + if ( + typeof item.pull_request_number !== "number" && + typeof item.pull_request_number !== "string" + ) { + errors.push( + `Line ${i + 1}: push-to-pr-branch 'pull_request_number' must be a number or string` + ); + continue; + } + } + break; + case "create-pull-request-review-comment": + // Validate required path field + if (!item.path || typeof item.path !== "string") { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment requires a 'path' string field` + ); + continue; + } + // Validate required line field + if ( + item.line === undefined || + (typeof item.line !== "number" && typeof item.line !== "string") + ) { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment requires a 'line' number or string field` + ); + continue; + } + // Validate line is a positive integer + const lineNumber = + typeof item.line === "string" ? parseInt(item.line, 10) : item.line; + if ( + isNaN(lineNumber) || + lineNumber <= 0 || + !Number.isInteger(lineNumber) + ) { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment 'line' must be a positive integer` + ); + continue; + } + // Validate required body field + if (!item.body || typeof item.body !== "string") { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment requires a 'body' string field` + ); + continue; + } + // Sanitize required text content + item.body = sanitizeContent(item.body); + // Validate optional start_line field + if (item.start_line !== undefined) { + if ( + typeof item.start_line !== "number" && + typeof item.start_line !== "string" + ) { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment 'start_line' must be a number or string` + ); + continue; + } + const startLineNumber = + typeof item.start_line === "string" + ? parseInt(item.start_line, 10) + : item.start_line; + if ( + isNaN(startLineNumber) || + startLineNumber <= 0 || + !Number.isInteger(startLineNumber) + ) { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment 'start_line' must be a positive integer` + ); + continue; + } + if (startLineNumber > lineNumber) { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment 'start_line' must be less than or equal to 'line'` + ); + continue; + } + } + // Validate optional side field + if (item.side !== undefined) { + if ( + typeof item.side !== "string" || + (item.side !== "LEFT" && item.side !== "RIGHT") + ) { + errors.push( + `Line ${i + 1}: create-pull-request-review-comment 'side' must be 'LEFT' or 'RIGHT'` + ); + continue; + } + } + break; + case "create-discussion": + if (!item.title || typeof item.title !== "string") { + errors.push( + `Line ${i + 1}: create-discussion requires a 'title' string field` + ); + continue; + } + if (!item.body || typeof item.body !== "string") { + errors.push( + `Line ${i + 1}: create-discussion requires a 'body' string field` + ); + continue; + } + // Sanitize text content + item.title = sanitizeContent(item.title); + item.body = sanitizeContent(item.body); + break; + case "missing-tool": + // Validate required tool field + if (!item.tool || typeof item.tool !== "string") { + errors.push( + `Line ${i + 1}: missing-tool requires a 'tool' string field` + ); + continue; + } + // Validate required reason field + if (!item.reason || typeof item.reason !== "string") { + errors.push( + `Line ${i + 1}: missing-tool requires a 'reason' string field` + ); + continue; + } + // Sanitize text content + item.tool = sanitizeContent(item.tool); + item.reason = sanitizeContent(item.reason); + // Validate optional alternatives field + if (item.alternatives !== undefined) { + if (typeof item.alternatives !== "string") { + errors.push( + `Line ${i + 1}: missing-tool 'alternatives' must be a string` + ); + continue; + } + item.alternatives = sanitizeContent(item.alternatives); + } + break; + case "create-repository-security-advisory": + // Validate required sarif field + if (!item.sarif) { + errors.push( + `Line ${i + 1}: create-repository-security-advisory requires a 'sarif' field` + ); + continue; + } + // SARIF content can be object or string + if ( + typeof item.sarif !== "object" && + typeof item.sarif !== "string" + ) { + errors.push( + `Line ${i + 1}: create-repository-security-advisory 'sarif' must be an object or string` + ); + continue; + } + // If SARIF is a string, sanitize it + if (typeof item.sarif === "string") { + item.sarif = sanitizeContent(item.sarif); + } + // Validate optional category field + if (item.category !== undefined) { + if (typeof item.category !== "string") { + errors.push( + `Line ${i + 1}: create-repository-security-advisory 'category' must be a string` + ); + continue; + } + item.category = sanitizeContent(item.category); + } + break; + default: + errors.push(`Line ${i + 1}: Unknown output type '${itemType}'`); + continue; + } + console.log(`Line ${i + 1}: Valid ${itemType} item`); + parsedItems.push(item); + } catch (error) { + errors.push(`Line ${i + 1}: Invalid JSON - ${error.message}`); + } + } + // Report validation results + if (errors.length > 0) { + core.warning("Validation errors found:"); + errors.forEach(error => core.warning(` - ${error}`)); + if (parsedItems.length === 0) { + core.setFailed(errors.map(e => ` - ${e}`).join("\n")); + return; + } + // For now, we'll continue with valid items but log the errors + // In the future, we might want to fail the workflow for invalid items + } + console.log(`Successfully parsed ${parsedItems.length} valid output items`); + // Set the parsed and validated items as output + const validatedOutput = { + items: parsedItems, + errors: errors, + }; + // Store validatedOutput JSON in "agent_output.json" file + const agentOutputFile = "/tmp/agent_output.json"; + const validatedOutputJson = JSON.stringify(validatedOutput); + try { + // Ensure the /tmp directory exists + fs.mkdirSync("/tmp", { recursive: true }); + fs.writeFileSync(agentOutputFile, validatedOutputJson, "utf8"); + console.log(`Stored validated output to: ${agentOutputFile}`); + // Set the environment variable GITHUB_AW_AGENT_OUTPUT to the file path + core.exportVariable("GITHUB_AW_AGENT_OUTPUT", agentOutputFile); + } catch (error) { + console.error(`Failed to write agent output file: ${error.message}`); + } + core.setOutput("output", JSON.stringify(validatedOutput)); + core.setOutput("raw_output", outputContent); + } + // Call the main function + await main(); + - name: Print sanitized agent output + run: | + echo "## Processed Output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````json' >> $GITHUB_STEP_SUMMARY + echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Upload sanitized agent output + if: always() && env.GITHUB_AW_AGENT_OUTPUT + uses: actions/upload-artifact@v4 + with: + name: agent_output.json + path: ${{ env.GITHUB_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@v4 + with: + name: agent_outputs + path: | + output.txt + if-no-files-found: ignore + - name: Clean up engine output files + run: | + rm -f output.txt + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@v7 + env: + AGENT_LOG_FILE: /tmp/test-claude-push-to-branch.log + with: + script: | + function main() { + const fs = require("fs"); + try { + // Get the log file path from environment + const logFile = process.env.AGENT_LOG_FILE; + if (!logFile) { + console.log("No agent log file specified"); + return; + } + if (!fs.existsSync(logFile)) { + console.log(`Log file not found: ${logFile}`); + return; + } + const logContent = fs.readFileSync(logFile, "utf8"); + const markdown = parseClaudeLog(logContent); + // Append to GitHub step summary + core.summary.addRaw(markdown).write(); + } catch (error) { + core.error(`Error parsing Claude log: ${error.message}`); + core.setFailed(error.message); + } + } + function parseClaudeLog(logContent) { + try { + const logEntries = JSON.parse(logContent); + if (!Array.isArray(logEntries)) { + return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + } + let markdown = "## 🤖 Commands and Tools\n\n"; + const toolUsePairs = new Map(); // Map tool_use_id to tool_result + const commandSummary = []; // For the succinct summary + // First pass: collect tool results by tool_use_id + for (const entry of logEntries) { + if (entry.type === "user" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "tool_result" && content.tool_use_id) { + toolUsePairs.set(content.tool_use_id, content); + } + } + } + } + // Collect all tool uses for summary + for (const entry of logEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "tool_use") { + const toolName = content.name; + const input = content.input || {}; + // Skip internal tools - only show external commands and API calls + if ( + [ + "Read", + "Write", + "Edit", + "MultiEdit", + "LS", + "Grep", + "Glob", + "TodoWrite", + ].includes(toolName) + ) { + continue; // Skip internal file operations and searches + } + // Find the corresponding tool result to get status + const toolResult = toolUsePairs.get(content.id); + let statusIcon = "❓"; + if (toolResult) { + statusIcon = toolResult.is_error === true ? "❌" : "✅"; + } + // Add to command summary (only external tools) + if (toolName === "Bash") { + const formattedCommand = formatBashCommand(input.command || ""); + commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); + } else if (toolName.startsWith("mcp__")) { + const mcpName = formatMcpName(toolName); + commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); + } else { + // Handle other external tools (if any) + commandSummary.push(`* ${statusIcon} ${toolName}`); + } + } + } + } + } + // Add command summary + if (commandSummary.length > 0) { + for (const cmd of commandSummary) { + markdown += `${cmd}\n`; + } + } else { + markdown += "No commands or tools used.\n"; + } + // Add Information section from the last entry with result metadata + markdown += "\n## 📊 Information\n\n"; + // Find the last entry with metadata + const lastEntry = logEntries[logEntries.length - 1]; + if ( + lastEntry && + (lastEntry.num_turns || + lastEntry.duration_ms || + lastEntry.total_cost_usd || + lastEntry.usage) + ) { + if (lastEntry.num_turns) { + markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; + } + if (lastEntry.duration_ms) { + const durationSec = Math.round(lastEntry.duration_ms / 1000); + const minutes = Math.floor(durationSec / 60); + const seconds = durationSec % 60; + markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; + } + if (lastEntry.total_cost_usd) { + markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; + } + if (lastEntry.usage) { + const usage = lastEntry.usage; + if (usage.input_tokens || usage.output_tokens) { + markdown += `**Token Usage:**\n`; + if (usage.input_tokens) + markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; + if (usage.cache_creation_input_tokens) + markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; + if (usage.cache_read_input_tokens) + markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; + if (usage.output_tokens) + markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; + markdown += "\n"; + } + } + if ( + lastEntry.permission_denials && + lastEntry.permission_denials.length > 0 + ) { + markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; + } + } + markdown += "\n## 🤖 Reasoning\n\n"; + // Second pass: process assistant messages in sequence + for (const entry of logEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "text" && content.text) { + // Add reasoning text directly (no header) + const text = content.text.trim(); + if (text && text.length > 0) { + markdown += text + "\n\n"; + } + } else if (content.type === "tool_use") { + // Process tool use with its result + const toolResult = toolUsePairs.get(content.id); + const toolMarkdown = formatToolUse(content, toolResult); + if (toolMarkdown) { + markdown += toolMarkdown; + } + } + } + } + } + return markdown; + } catch (error) { + return `## Agent Log Summary\n\nError parsing Claude log: ${error.message}\n`; + } + } + function formatToolUse(toolUse, toolResult) { + const toolName = toolUse.name; + const input = toolUse.input || {}; + // Skip TodoWrite except the very last one (we'll handle this separately) + if (toolName === "TodoWrite") { + return ""; // Skip for now, would need global context to find the last one + } + // Helper function to determine status icon + function getStatusIcon() { + if (toolResult) { + return toolResult.is_error === true ? "❌" : "✅"; + } + return "❓"; // Unknown by default + } + let markdown = ""; + const statusIcon = getStatusIcon(); + switch (toolName) { + case "Bash": + const command = input.command || ""; + const description = input.description || ""; + // Format the command to be single line + const formattedCommand = formatBashCommand(command); + if (description) { + markdown += `${description}:\n\n`; + } + markdown += `${statusIcon} \`${formattedCommand}\`\n\n`; + break; + case "Read": + const filePath = input.file_path || input.path || ""; + const relativePath = filePath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); // Remove /home/runner/work/repo/repo/ prefix + markdown += `${statusIcon} Read \`${relativePath}\`\n\n`; + break; + case "Write": + case "Edit": + case "MultiEdit": + const writeFilePath = input.file_path || input.path || ""; + const writeRelativePath = writeFilePath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); + markdown += `${statusIcon} Write \`${writeRelativePath}\`\n\n`; + break; + case "Grep": + case "Glob": + const query = input.query || input.pattern || ""; + markdown += `${statusIcon} Search for \`${truncateString(query, 80)}\`\n\n`; + break; + case "LS": + const lsPath = input.path || ""; + const lsRelativePath = lsPath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); + markdown += `${statusIcon} LS: ${lsRelativePath || lsPath}\n\n`; + break; + default: + // Handle MCP calls and other tools + if (toolName.startsWith("mcp__")) { + const mcpName = formatMcpName(toolName); + const params = formatMcpParameters(input); + markdown += `${statusIcon} ${mcpName}(${params})\n\n`; + } else { + // Generic tool formatting - show the tool name and main parameters + const keys = Object.keys(input); + if (keys.length > 0) { + // Try to find the most important parameter + const mainParam = + keys.find(k => + ["query", "command", "path", "file_path", "content"].includes(k) + ) || keys[0]; + const value = String(input[mainParam] || ""); + if (value) { + markdown += `${statusIcon} ${toolName}: ${truncateString(value, 100)}\n\n`; + } else { + markdown += `${statusIcon} ${toolName}\n\n`; + } + } else { + markdown += `${statusIcon} ${toolName}\n\n`; + } + } + } + return markdown; + } + function formatMcpName(toolName) { + // Convert mcp__github__search_issues to github::search_issues + if (toolName.startsWith("mcp__")) { + const parts = toolName.split("__"); + if (parts.length >= 3) { + const provider = parts[1]; // github, etc. + const method = parts.slice(2).join("_"); // search_issues, etc. + return `${provider}::${method}`; + } + } + return toolName; + } + function formatMcpParameters(input) { + const keys = Object.keys(input); + if (keys.length === 0) return ""; + const paramStrs = []; + for (const key of keys.slice(0, 4)) { + // Show up to 4 parameters + const value = String(input[key] || ""); + paramStrs.push(`${key}: ${truncateString(value, 40)}`); + } + if (keys.length > 4) { + paramStrs.push("..."); + } + return paramStrs.join(", "); + } + function formatBashCommand(command) { + if (!command) return ""; + // Convert multi-line commands to single line by replacing newlines with spaces + // and collapsing multiple spaces + let formatted = command + .replace(/\n/g, " ") // Replace newlines with spaces + .replace(/\r/g, " ") // Replace carriage returns with spaces + .replace(/\t/g, " ") // Replace tabs with spaces + .replace(/\s+/g, " ") // Collapse multiple spaces into one + .trim(); // Remove leading/trailing whitespace + // Escape backticks to prevent markdown issues + formatted = formatted.replace(/`/g, "\\`"); + // Truncate if too long (keep reasonable length for summary) + const maxLength = 80; + if (formatted.length > maxLength) { + formatted = formatted.substring(0, maxLength) + "..."; + } + return formatted; + } + function truncateString(str, maxLength) { + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength) + "..."; + } + // Export for testing + if (typeof module !== "undefined" && module.exports) { + module.exports = { + parseClaudeLog, + formatToolUse, + formatBashCommand, + truncateString, + }; + } + main(); + - name: Upload agent logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-claude-push-to-branch.log + path: /tmp/test-claude-push-to-branch.log + if-no-files-found: warn + - name: Generate git patch + if: always() + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + GITHUB_AW_PUSH_TARGET: "" + run: | + # Check current git status + echo "Current git status:" + git status + + # Extract branch name from JSONL output + BRANCH_NAME="" + if [ -f "$GITHUB_AW_SAFE_OUTPUTS" ]; then + echo "Checking for branch name in JSONL output..." + while IFS= read -r line; do + if [ -n "$line" ]; then + # Extract branch from create-pull-request line using simple grep and sed + if echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"create-pull-request"'; then + echo "Found create-pull-request line: $line" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" + break + fi + # Extract branch from push-to-pr-branch line using simple grep and sed + elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then + echo "Found push-to-pr-branch line: $line" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" + break + fi + fi + fi + done < "$GITHUB_AW_SAFE_OUTPUTS" + fi + + # Get the initial commit SHA from the base branch of the pull request + if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ]; then + INITIAL_SHA="$GITHUB_BASE_REF" + else + INITIAL_SHA="$GITHUB_SHA" + fi + echo "Base commit SHA: $INITIAL_SHA" + + # If we have a branch name, check if that branch exists and get its diff + if [ -n "$BRANCH_NAME" ]; then + echo "Looking for branch: $BRANCH_NAME" + # Check if the branch exists + if git show-ref --verify --quiet refs/heads/$BRANCH_NAME; then + echo "Branch $BRANCH_NAME exists, generating patch from branch changes" + # Generate patch from the base to the branch + git format-patch "$INITIAL_SHA".."$BRANCH_NAME" --stdout > /tmp/aw.patch || echo "Failed to generate patch from branch" > /tmp/aw.patch + echo "Patch file created from branch: $BRANCH_NAME" + else + echo "Branch $BRANCH_NAME does not exist, falling back to current HEAD" + BRANCH_NAME="" + fi + fi + + # If no branch or branch doesn't exist, use the existing logic + if [ -z "$BRANCH_NAME" ]; then + echo "Using current HEAD for patch generation" + # Stage any unstaged files + git add -A || true + # Check if there are staged files to commit + if ! git diff --cached --quiet; then + echo "Staged files found, committing them..." + git commit -m "[agent] staged files" || true + echo "Staged files committed" + else + echo "No staged files to commit" + fi + # Check updated git status + echo "Updated git status after committing staged files:" + git status + # Show compact diff information between initial commit and HEAD (committed changes only) + echo '## Git diff' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + git diff --name-only "$INITIAL_SHA"..HEAD >> $GITHUB_STEP_SUMMARY || true + echo '```' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + # Check if there are any committed changes since the initial commit + if git diff --quiet "$INITIAL_SHA" HEAD; then + echo "No committed changes detected since initial commit" + echo "Skipping patch generation - no committed changes to create patch from" + else + echo "Committed changes detected, generating patch..." + # Generate patch from initial commit to HEAD (committed changes only) + git format-patch "$INITIAL_SHA"..HEAD --stdout > /tmp/aw.patch || echo "Failed to generate patch" > /tmp/aw.patch + echo "Patch file created at /tmp/aw.patch" + fi + fi + + # Show patch info if it exists + if [ -f /tmp/aw.patch ]; then + ls -la /tmp/aw.patch + # Show the first 50 lines of the patch for review + echo '## Git Patch' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + head -500 /tmp/aw.patch >> $GITHUB_STEP_SUMMARY || echo "Could not display patch contents" >> $GITHUB_STEP_SUMMARY + echo '...' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + fi + - name: Upload git patch + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw.patch + path: /tmp/aw.patch + if-no-files-found: ignore + + push_to_pr_branch: + needs: test-claude-push-to-branch + if: > + (contains(github.event.issue.body, '/test-claude-push-to-pr-branch') || contains(github.event.comment.body, '/test-claude-push-to-pr-branch') || + contains(github.event.pull_request.body, '/test-claude-push-to-pr-branch')) && (github.event.pull_request.number) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + timeout-minutes: 10 + outputs: + branch_name: ${{ steps.push_to_pr_branch.outputs.branch_name }} + commit_sha: ${{ steps.push_to_pr_branch.outputs.commit_sha }} + push_url: ${{ steps.push_to_pr_branch.outputs.push_url }} + steps: + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@v5 + with: + name: aw.patch + path: /tmp/ + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Configure Git credentials + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "${{ github.workflow }}" + echo "Git configured with standard GitHub Actions identity" + - name: Push to Branch + id: push_to_pr_branch + uses: actions/github-script@v7 + env: + GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-claude-push-to-branch.outputs.output }} + GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" + with: + script: | + async function main() { + /** @type {typeof import("fs")} */ + const fs = require("fs"); + const { execSync } = require("child_process"); + // Environment validation - fail early if required variables are missing + const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT || ""; + if (outputContent.trim() === "") { + console.log("Agent output content is empty"); + return; + } + const target = process.env.GITHUB_AW_PUSH_TARGET || "triggering"; + const ifNoChanges = process.env.GITHUB_AW_PUSH_IF_NO_CHANGES || "warn"; + // Check if patch file exists and has valid content + if (!fs.existsSync("/tmp/aw.patch")) { + const message = "No patch file found - cannot push without changes"; + switch (ifNoChanges) { + case "error": + core.setFailed(message); + return; + case "ignore": + // Silent success - no console output + return; + case "warn": + default: + console.log(message); + return; + } + } + const patchContent = fs.readFileSync("/tmp/aw.patch", "utf8"); + // Check for actual error conditions (but allow empty patches as valid noop) + if (patchContent.includes("Failed to generate patch")) { + const message = + "Patch file contains error message - cannot push without changes"; + switch (ifNoChanges) { + case "error": + core.setFailed(message); + return; + case "ignore": + // Silent success - no console output + return; + case "warn": + default: + console.log(message); + return; + } + } + // Empty patch is valid - behavior depends on if-no-changes configuration + const isEmpty = !patchContent || !patchContent.trim(); + if (isEmpty) { + const message = + "Patch file is empty - no changes to apply (noop operation)"; + switch (ifNoChanges) { + case "error": + core.setFailed( + "No changes to push - failing as configured by if-no-changes: error" + ); + return; + case "ignore": + // Silent success - no console output + break; + case "warn": + default: + console.log(message); + break; + } + } + console.log("Agent output content length:", outputContent.length); + if (!isEmpty) { + console.log("Patch content validation passed"); + } + console.log("Target configuration:", target); + // Parse the validated output JSON + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + console.log( + "Error parsing agent output JSON:", + error instanceof Error ? error.message : String(error) + ); + return; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + console.log("No valid items found in agent output"); + return; + } + // Find the push-to-pr-branch item + const pushItem = validatedOutput.items.find( + /** @param {any} item */ item => item.type === "push-to-pr-branch" + ); + if (!pushItem) { + console.log("No push-to-pr-branch item found in agent output"); + return; + } + console.log("Found push-to-pr-branch item"); + // Validate target configuration for pull request context + if (target !== "*" && target !== "triggering") { + // If target is a specific number, validate it's a valid pull request number + const pullNumber = parseInt(target, 10); + if (isNaN(pullNumber)) { + core.setFailed( + 'Invalid target configuration: must be "triggering", "*", or a valid pull request number' + ); + return; + } + } + // Check if we're in a pull request context when required + if (target === "triggering" && !context.payload.pull_request) { + core.setFailed( + 'push-to-pr-branch with target "triggering" requires pull request context' + ); + return; + } + // Compute the target branch name based on target configuration + let pullNumber; + if (target === "triggering") { + // Use the number of the triggering pull request + pullNumber = context.payload.pull_request.number; + } else if (target === "*") { + if (pushItem.pull_number) { + pullNumber = parseInt(pushItem.pull_number, 10); + } + } else { + // Target is a specific pull request number + pullNumber = parseInt(target, 10); + } + let branchName; + // Fetch the specific PR to get its head branch + try { + const prInfo = execSync( + `gh pr view ${pullNumber} --json headRefName --jq '.headRefName'`, + { encoding: "utf8" } + ).trim(); + if (prInfo) { + branchName = prInfo; + } else { + throw new Error("No head branch found for PR"); + } + } catch (error) { + console.log(`Warning: Could not fetch PR ${pullNumber} details: ${error.message}`); + // Exit with failure if we cannot determine the branch name + core.setFailed(`Failed to determine branch name for PR ${pullNumber}`); + return; + } + console.log("Target branch:", branchName); + // Check if patch has actual changes (not just empty) + const hasChanges = !isEmpty; + // Switch to or create the target branch + console.log("Switching to branch:", branchName); + try { + // Try to checkout existing branch first + execSync("git fetch origin", { stdio: "inherit" }); + // Check if branch exists on origin + try { + execSync(`git rev-parse --verify origin/${branchName}`, { + stdio: "pipe", + }); + // Branch exists on origin, check it out + execSync(`git checkout -B ${branchName} origin/${branchName}`, { + stdio: "inherit", + }); + console.log("Checked out existing branch from origin:", branchName); + } catch (originError) { + // Branch doesn't exist on origin, check if it exists locally + try { + execSync(`git rev-parse --verify ${branchName}`, { stdio: "pipe" }); + // Branch exists locally, check it out + execSync(`git checkout ${branchName}`, { stdio: "inherit" }); + console.log("Checked out existing local branch:", branchName); + } catch (localError) { + // Branch doesn't exist locally or on origin, create it from default branch + console.log( + "Branch does not exist, creating new branch from default branch:", + branchName + ); + // Get the default branch name + const defaultBranch = execSync( + "git remote show origin | grep 'HEAD branch' | cut -d' ' -f5", + { encoding: "utf8" } + ).trim(); + console.log("Default branch:", defaultBranch); + // Ensure we have the latest default branch + execSync(`git checkout ${defaultBranch}`, { stdio: "inherit" }); + execSync(`git pull origin ${defaultBranch}`, { stdio: "inherit" }); + // Create new branch from default branch + execSync(`git checkout -b ${branchName}`, { stdio: "inherit" }); + console.log("Created new branch from default branch:", branchName); + } + } + } catch (error) { + core.setFailed( + `Failed to switch to branch ${branchName}: ${error instanceof Error ? error.message : String(error)}` + ); + return; + } + // Apply the patch using git CLI (skip if empty) + if (!isEmpty) { + console.log("Applying patch..."); + try { + // Patches are created with git format-patch, so use git am to apply them + execSync("git am /tmp/aw.patch", { stdio: "inherit" }); + console.log("Patch applied successfully"); + // Push the applied commits to the branch + execSync(`git push origin ${branchName}`, { stdio: "inherit" }); + console.log("Changes committed and pushed to branch:", branchName); + } catch (error) { + core.error( + `Failed to apply patch: ${error instanceof Error ? error.message : String(error)}` + ); + core.setFailed("Failed to apply patch"); + return; + } + } else { + console.log("Skipping patch application (empty patch)"); + // Handle if-no-changes configuration for empty patches + const message = + "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + core.setFailed( + "No changes to apply - failing as configured by if-no-changes: error" + ); + return; + case "ignore": + // Silent success - no console output + break; + case "warn": + default: + console.log(message); + break; + } + } + // Get commit SHA and push URL + const commitSha = execSync("git rev-parse HEAD", { encoding: "utf8" }).trim(); + // Get commit SHA and push URL + const pushUrl = context.payload.repository + ? `${context.payload.repository.html_url}/tree/${branchName}` + : `https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}`; + // Set outputs + core.setOutput("branch_name", branchName); + core.setOutput("commit_sha", commitSha); + core.setOutput("push_url", pushUrl); + // Write summary to GitHub Actions summary + const summaryTitle = hasChanges + ? "Push to Branch" + : "Push to Branch (No Changes)"; + const summaryContent = hasChanges + ? ` + ## ${summaryTitle} + - **Branch**: \`${branchName}\` + - **Commit**: [${commitSha.substring(0, 7)}](${pushUrl}) + - **URL**: [${pushUrl}](${pushUrl}) + ` + : ` + ## ${summaryTitle} + - **Branch**: \`${branchName}\` + - **Status**: No changes to apply (noop operation) + - **URL**: [${pushUrl}](${pushUrl}) + `; + await core.summary.addRaw(summaryContent).write(); + } + await main(); + diff --git a/.github/workflows/test-claude-push-to-branch.md b/.github/workflows/test-claude-push-to-pr-branch.md similarity index 100% rename from .github/workflows/test-claude-push-to-branch.md rename to .github/workflows/test-claude-push-to-pr-branch.md diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index 12c756a319d..17db548c994 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -888,7 +888,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml index 966932410c7..ac1f291ad02 100644 --- a/.github/workflows/test-codex-add-issue-comment.lock.yml +++ b/.github/workflows/test-codex-add-issue-comment.lock.yml @@ -390,6 +390,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-add-issue-comment" command = "docker" args = [ "run", @@ -716,7 +717,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml index c74f4f487c9..d9fd0014073 100644 --- a/.github/workflows/test-codex-add-issue-labels.lock.yml +++ b/.github/workflows/test-codex-add-issue-labels.lock.yml @@ -390,6 +390,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-add-issue-labels" command = "docker" args = [ "run", @@ -716,7 +717,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index bd052245251..15348d2f1e9 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -1058,7 +1058,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml index e98cf06c492..99aa3271915 100644 --- a/.github/workflows/test-codex-create-issue.lock.yml +++ b/.github/workflows/test-codex-create-issue.lock.yml @@ -61,6 +61,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-create-issue" command = "docker" args = [ "run", @@ -389,7 +390,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml index 7808e8789f8..1577ee8f725 100644 --- a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml @@ -341,6 +341,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-create-pull-request-review-comment" command = "docker" args = [ "run", @@ -461,7 +462,28 @@ jobs: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - - name: Collect agent output + - name: Print Agent output + env: + GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + run: | + echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````json' >> $GITHUB_STEP_SUMMARY + cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY + # Ensure there's a newline after the file content if it doesn't end with one + if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo '``````' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: safe_output.jsonl + path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output id: collect_output uses: actions/github-script@v7 env: @@ -633,7 +655,7 @@ jobs: case "missing-tool": return 1000; // Allow many missing tool reports (default: unlimited) case "create-repository-security-advisory": - return 1000; // Allow many security reports (default: unlimited) + return 1000; // Allow many repository security advisories (default: unlimited) default: return 1; // Default to single item for unknown types } @@ -645,6 +667,15 @@ jobs: */ function repairJson(jsonStr) { let repaired = jsonStr.trim(); + // remove invalid control characters like + // U+0014 (DC4) — represented here as "\u0014" + // Escape control characters not allowed in JSON strings (U+0000 through U+001F) + // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; + repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { + const c = ch.charCodeAt(0); + return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); + }); // Fix single quotes to double quotes (must be done first) repaired = repaired.replace(/'/g, '"'); // Fix missing quotes around object keys @@ -714,6 +745,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -1158,33 +1190,14 @@ jobs: } // Call the main function await main(); - - name: Print agent output to step summary - env: - GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} + - name: Print sanitized agent output run: | - echo "## Agent Output (JSONL)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````json' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY - # Ensure there's a newline after the file content if it doesn't end with one - if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - fi - echo '``````' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY echo "## Processed Output" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo '``````json' >> $GITHUB_STEP_SUMMARY echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() && steps.collect_output.outputs.output != '' - uses: actions/upload-artifact@v4 - with: - name: safe_output.jsonl - path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Upload agent output JSON + - name: Upload sanitized agent output if: always() && env.GITHUB_AW_AGENT_OUTPUT uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml index 63d04ace022..d686c6c5653 100644 --- a/.github/workflows/test-codex-create-pull-request.lock.yml +++ b/.github/workflows/test-codex-create-pull-request.lock.yml @@ -66,6 +66,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-create-pull-request" command = "docker" args = [ "run", @@ -455,7 +456,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1279,11 +1280,10 @@ jobs: # Extract branch from push-to-pr-branch line using simple grep and sed elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then echo "Found push-to-pr-branch line: $line" - # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow - # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH - if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then - BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-pr-branch target: $BRANCH_NAME" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi fi diff --git a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml index 1e2579c9ce6..11867362508 100644 --- a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml @@ -320,6 +320,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-create-repository-security-advisory" command = "docker" args = [ "run", @@ -651,7 +652,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1502,13 +1503,18 @@ jobs: } // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -1543,7 +1549,9 @@ jobs: ); // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } if ( diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml index b88570eab05..99b42927e3f 100644 --- a/.github/workflows/test-codex-mcp.lock.yml +++ b/.github/workflows/test-codex-mcp.lock.yml @@ -320,6 +320,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-mcp" command = "docker" args = [ "run", @@ -668,7 +669,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-codex-push-to-branch.lock.yml b/.github/workflows/test-codex-push-to-pr-branch.lock.yml similarity index 96% rename from .github/workflows/test-codex-push-to-branch.lock.yml rename to .github/workflows/test-codex-push-to-pr-branch.lock.yml index 2af7002cfac..3a67e8e1611 100644 --- a/.github/workflows/test-codex-push-to-branch.lock.yml +++ b/.github/workflows/test-codex-push-to-pr-branch.lock.yml @@ -95,7 +95,7 @@ jobs: } await main(); - test-codex-push-to-pr-branch: + test-codex-push-to-branch: needs: task if: > contains(github.event.issue.body, '/test-codex-push-to-pr-branch') || contains(github.event.comment.body, '/test-codex-push-to-pr-branch') || @@ -151,6 +151,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-push-to-branch" command = "docker" args = [ "run", @@ -219,14 +220,14 @@ jobs: ### Available Output Types: - **Pushing Changes to Branch** + **Pushing Changes to Pull Request Branch** - To push changes to a branch, for example to add code to a pull request: + To push changes to the branch of a pull request: 1. Make any file changes directly in the working directory - 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. - 3. Indicate your intention to push to the branch by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": + 2. Add and commit your changes to the local copy of the pull request branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. + 3. Indicate your intention to push the branch to the repo by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": ```json - {"type": "push-to-pr-branch", "message": "Commit message describing the changes"} + {, "type": "push-to-pr-branch", "branch_name": "The name of the branch to push to, should be the branch name associated with the pull request", "message": "Commit message describing the changes"} ``` 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up @@ -301,7 +302,7 @@ jobs: # Run codex with log capture - pipefail ensures codex exit code is preserved codex exec \ -c model=o4-mini \ - --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-push-to-pr-branch.log + --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-push-to-branch.log env: GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -333,7 +334,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"branch\":\"triggering\",\"enabled\":true}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"push-to-pr-branch\":{\"enabled\":true}}" with: script: | async function main() { @@ -516,7 +517,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1053,7 +1054,7 @@ jobs: if: always() uses: actions/github-script@v7 env: - AGENT_LOG_FILE: /tmp/test-codex-push-to-pr-branch.log + AGENT_LOG_FILE: /tmp/test-codex-push-to-branch.log with: script: | function main() { @@ -1310,14 +1311,14 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-codex-push-to-pr-branch.log - path: /tmp/test-codex-push-to-pr-branch.log + name: test-codex-push-to-branch.log + path: /tmp/test-codex-push-to-branch.log if-no-files-found: warn - name: Generate git patch if: always() env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_PUSH_BRANCH: "triggering" + GITHUB_AW_PUSH_TARGET: "" run: | # Check current git status echo "Current git status:" @@ -1341,11 +1342,10 @@ jobs: # Extract branch from push-to-pr-branch line using simple grep and sed elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then echo "Found push-to-pr-branch line: $line" - # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow - # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH - if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then - BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-pr-branch target: $BRANCH_NAME" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi fi @@ -1432,7 +1432,7 @@ jobs: if-no-files-found: ignore push_to_pr_branch: - needs: test-codex-push-to-pr-branch + needs: test-codex-push-to-branch if: > (contains(github.event.issue.body, '/test-codex-push-to-pr-branch') || contains(github.event.comment.body, '/test-codex-push-to-pr-branch') || contains(github.event.pull_request.body, '/test-codex-push-to-pr-branch')) && (github.event.pull_request.number) @@ -1465,8 +1465,7 @@ jobs: id: push_to_pr_branch uses: actions/github-script@v7 env: - GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-codex-push-to-pr-branch.outputs.output }} - GITHUB_AW_PUSH_BRANCH: "triggering" + GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-codex-push-to-branch.outputs.output }} GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" with: script: | @@ -1475,11 +1474,6 @@ jobs: const fs = require("fs"); const { execSync } = require("child_process"); // Environment validation - fail early if required variables are missing - const branchName = process.env.GITHUB_AW_PUSH_BRANCH; - if (!branchName) { - core.setFailed("GITHUB_AW_PUSH_BRANCH environment variable is required"); - return; - } const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT || ""; if (outputContent.trim() === "") { console.log("Agent output content is empty"); @@ -1545,7 +1539,6 @@ jobs: if (!isEmpty) { console.log("Patch content validation passed"); } - console.log("Target branch:", branchName); console.log("Target configuration:", target); // Parse the validated output JSON let validatedOutput; @@ -1574,8 +1567,8 @@ jobs: // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number - const targetNumber = parseInt(target, 10); - if (isNaN(targetNumber)) { + const pullNumber = parseInt(target, 10); + if (isNaN(pullNumber)) { core.setFailed( 'Invalid target configuration: must be "triggering", "*", or a valid pull request number' ); @@ -1589,6 +1582,40 @@ jobs: ); return; } + // Compute the target branch name based on target configuration + let pullNumber; + if (target === "triggering") { + // Use the number of the triggering pull request + pullNumber = context.payload.pull_request.number; + } else if (target === "*") { + if (pushItem.pull_number) { + pullNumber = parseInt(pushItem.pull_number, 10); + } + } else { + // Target is a specific pull request number + pullNumber = parseInt(target, 10); + } + let branchName; + // Fetch the specific PR to get its head branch + try { + const prInfo = execSync( + `gh pr view ${pullNumber} --json headRefName --jq '.headRefName'`, + { encoding: "utf8" } + ).trim(); + if (prInfo) { + branchName = prInfo; + } else { + throw new Error("No head branch found for PR"); + } + } catch (error) { + console.log(`Warning: Could not fetch PR ${pullNumber} details: ${error.message}`); + // Exit with failure if we cannot determine the branch name + core.setFailed(`Failed to determine branch name for PR ${pullNumber}`); + return; + } + console.log("Target branch:", branchName); + // Check if patch has actual changes (not just empty) + const hasChanges = !isEmpty; // Switch to or create the target branch console.log("Switching to branch:", branchName); try { diff --git a/.github/workflows/test-codex-push-to-branch.md b/.github/workflows/test-codex-push-to-pr-branch.md similarity index 100% rename from .github/workflows/test-codex-push-to-branch.md rename to .github/workflows/test-codex-push-to-pr-branch.md diff --git a/.github/workflows/test-codex-update-issue.lock.yml b/.github/workflows/test-codex-update-issue.lock.yml index 8de6d029232..119439f69bb 100644 --- a/.github/workflows/test-codex-update-issue.lock.yml +++ b/.github/workflows/test-codex-update-issue.lock.yml @@ -390,6 +390,7 @@ jobs: persistence = "none" [mcp_servers.github] + user_agent = "test-codex-update-issue" command = "docker" args = [ "run", @@ -719,7 +720,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/.github/workflows/test-custom-safe-outputs.lock.yml b/.github/workflows/test-custom-safe-outputs.lock.yml index 254500fdedc..2af620502e4 100644 --- a/.github/workflows/test-custom-safe-outputs.lock.yml +++ b/.github/workflows/test-custom-safe-outputs.lock.yml @@ -169,18 +169,18 @@ jobs: To udpate an issue: ```json - {"type": "update-issue", "status": "open" // or "closed", "title": "New issue title", "body": "Updated issue body in markdown"} + {"type": "update-issue", "status": "open" // or "closed", "title": "New issue title", "body": "Updated issue body in markdown", "issue_number": "The issue number to update"} ``` 2. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up - **Pushing Changes to Branch** + **Pushing Changes to Pull Request Branch** - To push changes to a branch, for example to add code to a pull request: + To push changes to the branch of a pull request: 1. Make any file changes directly in the working directory - 2. Add and commit your changes to the branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. - 3. Indicate your intention to push to the branch by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": + 2. Add and commit your changes to the local copy of the pull request branch. Be careful to add exactly the files you intend, and check there are no extra files left un-added. Check you haven't deleted or changed any files you didn't intend to. + 3. Indicate your intention to push the branch to the repo by writing to the file "${{ env.GITHUB_AW_SAFE_OUTPUTS }}": ```json - {"type": "push-to-pr-branch", "message": "Commit message describing the changes"} + {, "type": "push-to-pr-branch", "pull_number": "The pull number to update", "branch_name": "The name of the branch to push to, should be the branch name associated with the pull request", "message": "Commit message describing the changes"} ``` 4. After you write to that file, read it as JSONL and check it is valid. If it isn't, make any necessary corrections to it to fix it up @@ -388,7 +388,7 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"add-issue-label\":true,\"create-discussion\":{\"enabled\":true,\"max\":1},\"create-issue\":true,\"create-pull-request\":true,\"create-pull-request-review-comment\":{\"enabled\":true,\"max\":1},\"create-repository-security-advisory\":{\"enabled\":true,\"max\":5},\"missing-tool\":{\"enabled\":true,\"max\":5},\"push-to-pr-branch\":{\"branch\":\"triggering\",\"enabled\":true,\"target\":\"*\"},\"update-issue\":true}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-issue-comment\":{\"enabled\":true,\"target\":\"*\"},\"add-issue-label\":true,\"create-discussion\":{\"enabled\":true,\"max\":1},\"create-issue\":true,\"create-pull-request\":true,\"create-pull-request-review-comment\":{\"enabled\":true,\"max\":1},\"create-repository-security-advisory\":{\"enabled\":true,\"max\":5},\"missing-tool\":{\"enabled\":true,\"max\":5},\"push-to-pr-branch\":{\"enabled\":true,\"target\":\"*\"},\"update-issue\":true}" with: script: | async function main() { @@ -571,7 +571,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1115,7 +1115,7 @@ jobs: if: always() env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_PUSH_BRANCH: "triggering" + GITHUB_AW_PUSH_TARGET: "*" run: | # Check current git status echo "Current git status:" @@ -1139,11 +1139,10 @@ jobs: # Extract branch from push-to-pr-branch line using simple grep and sed elif echo "$line" | grep -q '"type"[[:space:]]*:[[:space:]]*"push-to-pr-branch"'; then echo "Found push-to-pr-branch line: $line" - # For push-to-pr-branch, we don't extract branch from JSONL since it's configured in the workflow - # The branch name should come from the environment variable GITHUB_AW_PUSH_BRANCH - if [ -n "$GITHUB_AW_PUSH_BRANCH" ]; then - BRANCH_NAME="$GITHUB_AW_PUSH_BRANCH" - echo "Using configured push-to-pr-branch target: $BRANCH_NAME" + # Extract branch value using sed + BRANCH_NAME=$(echo "$line" | sed -n 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ -n "$BRANCH_NAME" ]; then + echo "Extracted branch name from create-pull-request: $BRANCH_NAME" break fi fi @@ -2143,13 +2142,18 @@ jobs: } // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -2184,7 +2188,9 @@ jobs: ); // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } if ( @@ -3126,7 +3132,6 @@ jobs: uses: actions/github-script@v7 env: GITHUB_AW_AGENT_OUTPUT: ${{ needs.test-safe-outputs-custom-engine.outputs.output }} - GITHUB_AW_PUSH_BRANCH: "triggering" GITHUB_AW_PUSH_TARGET: "*" GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" with: @@ -3136,11 +3141,6 @@ jobs: const fs = require("fs"); const { execSync } = require("child_process"); // Environment validation - fail early if required variables are missing - const branchName = process.env.GITHUB_AW_PUSH_BRANCH; - if (!branchName) { - core.setFailed("GITHUB_AW_PUSH_BRANCH environment variable is required"); - return; - } const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT || ""; if (outputContent.trim() === "") { console.log("Agent output content is empty"); @@ -3206,7 +3206,6 @@ jobs: if (!isEmpty) { console.log("Patch content validation passed"); } - console.log("Target branch:", branchName); console.log("Target configuration:", target); // Parse the validated output JSON let validatedOutput; @@ -3235,8 +3234,8 @@ jobs: // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number - const targetNumber = parseInt(target, 10); - if (isNaN(targetNumber)) { + const pullNumber = parseInt(target, 10); + if (isNaN(pullNumber)) { core.setFailed( 'Invalid target configuration: must be "triggering", "*", or a valid pull request number' ); @@ -3250,6 +3249,40 @@ jobs: ); return; } + // Compute the target branch name based on target configuration + let pullNumber; + if (target === "triggering") { + // Use the number of the triggering pull request + pullNumber = context.payload.pull_request.number; + } else if (target === "*") { + if (pushItem.pull_number) { + pullNumber = parseInt(pushItem.pull_number, 10); + } + } else { + // Target is a specific pull request number + pullNumber = parseInt(target, 10); + } + let branchName; + // Fetch the specific PR to get its head branch + try { + const prInfo = execSync( + `gh pr view ${pullNumber} --json headRefName --jq '.headRefName'`, + { encoding: "utf8" } + ).trim(); + if (prInfo) { + branchName = prInfo; + } else { + throw new Error("No head branch found for PR"); + } + } catch (error) { + console.log(`Warning: Could not fetch PR ${pullNumber} details: ${error.message}`); + // Exit with failure if we cannot determine the branch name + core.setFailed(`Failed to determine branch name for PR ${pullNumber}`); + return; + } + console.log("Target branch:", branchName); + // Check if patch has actual changes (not just empty) + const hasChanges = !isEmpty; // Switch to or create the target branch console.log("Switching to branch:", branchName); try { diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index d8354eabff4..1187895c11f 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -801,7 +801,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 8569238b4d1..0c9ddfbf413 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -3785,8 +3785,7 @@ func (c *Compiler) parseUpdateIssuesConfig(outputMap map[string]any) *UpdateIssu func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any) *PushToPullRequestBranchConfig { if configData, exists := outputMap["push-to-pr-branch"]; exists { pushToBranchConfig := &PushToPullRequestBranchConfig{ - Branch: "triggering", // Default branch value - IfNoChanges: "warn", // Default behavior: warn when no changes + IfNoChanges: "warn", // Default behavior: warn when no changes } // Handle the case where configData is nil (push-to-pr-branch: with no value) From b4977c09285f98a36d8ddf89688f9062de61d886 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 11 Sep 2025 02:12:23 +0100 Subject: [PATCH 6/6] fix tests --- .github/workflows/test-claude-push-to-pr-branch.lock.yml | 1 - .github/workflows/test-codex-push-to-pr-branch.lock.yml | 1 - .github/workflows/test-custom-safe-outputs.lock.yml | 1 - docs/safe-outputs.md | 2 +- pkg/cli/templates/instructions.md | 2 +- pkg/workflow/git_commands_test.go | 8 ++++---- pkg/workflow/git_patch.go | 4 ---- pkg/workflow/output_push_to_branch_test.go | 4 ++-- 8 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test-claude-push-to-pr-branch.lock.yml b/.github/workflows/test-claude-push-to-pr-branch.lock.yml index a6173e17b41..f60052f09ca 100644 --- a/.github/workflows/test-claude-push-to-pr-branch.lock.yml +++ b/.github/workflows/test-claude-push-to-pr-branch.lock.yml @@ -1568,7 +1568,6 @@ jobs: if: always() env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_PUSH_TARGET: "" run: | # Check current git status echo "Current git status:" diff --git a/.github/workflows/test-codex-push-to-pr-branch.lock.yml b/.github/workflows/test-codex-push-to-pr-branch.lock.yml index 3a67e8e1611..337fe3be3b7 100644 --- a/.github/workflows/test-codex-push-to-pr-branch.lock.yml +++ b/.github/workflows/test-codex-push-to-pr-branch.lock.yml @@ -1318,7 +1318,6 @@ jobs: if: always() env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_PUSH_TARGET: "" run: | # Check current git status echo "Current git status:" diff --git a/.github/workflows/test-custom-safe-outputs.lock.yml b/.github/workflows/test-custom-safe-outputs.lock.yml index 2af620502e4..2791405aa1f 100644 --- a/.github/workflows/test-custom-safe-outputs.lock.yml +++ b/.github/workflows/test-custom-safe-outputs.lock.yml @@ -1115,7 +1115,6 @@ jobs: if: always() env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_PUSH_TARGET: "*" run: | # Check current git status echo "Current git status:" diff --git a/docs/safe-outputs.md b/docs/safe-outputs.md index c6e4365c1aa..fdd53bd9885 100644 --- a/docs/safe-outputs.md +++ b/docs/safe-outputs.md @@ -187,7 +187,7 @@ safe-outputs: At most one pull request is currently supported. The agentic part of your workflow should instruct to: -1. **Make code changes**: Make any code changes in the working directory—these are automatically collected using `git add -A` and committed +1. **Make code changes**: Make code changes and commit them to a branch 2. **Create pull request**: Describe the pull request title and body content you want **Example natural language to generate the output:** diff --git a/pkg/cli/templates/instructions.md b/pkg/cli/templates/instructions.md index 2eead0bf4e8..e6a72f8c3f0 100644 --- a/pkg/cli/templates/instructions.md +++ b/pkg/cli/templates/instructions.md @@ -143,7 +143,7 @@ The YAML frontmatter supports these fields: - Each JSON object must be on a single line (JSONL format) - All string values should be properly escaped JSON strings - The `type` field is required and must match the configured safe output types - - File changes for `create-pull-request` and `push-to-pr-branch` are collected automatically via `git add -A` + - File changes for `create-pull-request` and `push-to-pr-branch` are made by committing to a branch - Output entries are processed only if the corresponding safe output type is configured in the workflow frontmatter - Invalid JSON entries are ignored with warnings in the workflow logs diff --git a/pkg/workflow/git_commands_test.go b/pkg/workflow/git_commands_test.go index a5439d87065..3bddb6ddfeb 100644 --- a/pkg/workflow/git_commands_test.go +++ b/pkg/workflow/git_commands_test.go @@ -33,7 +33,7 @@ func TestApplyDefaultGitCommandsForSafeOutputs(t *testing.T) { name: "push-to-pr-branch enabled - should add git commands", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ - PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, }, expectGit: true, }, @@ -155,7 +155,7 @@ func TestAdditionalClaudeToolsForSafeOutputs(t *testing.T) { name: "push-to-pr-branch enabled - should add editing tools", tools: map[string]any{}, safeOutputs: &SafeOutputsConfig{ - PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, }, expectEditingTools: true, }, @@ -270,7 +270,7 @@ func TestNeedsGitCommands(t *testing.T) { { name: "push-to-pr-branch enabled", safeOutputs: &SafeOutputsConfig{ - PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, }, expected: true, }, @@ -278,7 +278,7 @@ func TestNeedsGitCommands(t *testing.T) { name: "both enabled", safeOutputs: &SafeOutputsConfig{ CreatePullRequests: &CreatePullRequestsConfig{}, - PushToPullRequestBranch: &PushToPullRequestBranchConfig{Branch: "main"}, + PushToPullRequestBranch: &PushToPullRequestBranchConfig{}, }, expected: true, }, diff --git a/pkg/workflow/git_patch.go b/pkg/workflow/git_patch.go index e57982ac223..70bfcc411b0 100644 --- a/pkg/workflow/git_patch.go +++ b/pkg/workflow/git_patch.go @@ -8,10 +8,6 @@ func (c *Compiler) generateGitPatchStep(yaml *strings.Builder, data *WorkflowDat yaml.WriteString(" if: always()\n") yaml.WriteString(" env:\n") yaml.WriteString(" GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}\n") - // Add push-to-pr-branch configuration if available - if data.SafeOutputs != nil && data.SafeOutputs.PushToPullRequestBranch != nil { - yaml.WriteString(" GITHUB_AW_PUSH_TARGET: \"" + data.SafeOutputs.PushToPullRequestBranch.Target + "\"\n") - } yaml.WriteString(" run: |\n") yaml.WriteString(" # Check current git status\n") yaml.WriteString(" echo \"Current git status:\"\n") diff --git a/pkg/workflow/output_push_to_branch_test.go b/pkg/workflow/output_push_to_branch_test.go index 739c37085f3..07d7d0f1e41 100644 --- a/pkg/workflow/output_push_to_branch_test.go +++ b/pkg/workflow/output_push_to_branch_test.go @@ -21,7 +21,7 @@ safe-outputs: target: "triggering" --- -# Test Push to Branch +# Test Push to PR Branch This is a test workflow to validate push-to-pr-branch configuration parsing. @@ -226,7 +226,7 @@ This workflow uses null configuration which should default to "triggering". // Check that no target is set (should use default) if strings.Contains(lockContent, "GITHUB_AW_PUSH_TARGET:") { - t.Errorf("Expected no target to be set when using null config") + t.Errorf("Expected no target to be set when using null config, %s", lockContent) } }