diff --git a/.changeset/patch-convert-pr-safe-outputs-handler-manager.md b/.changeset/patch-convert-pr-safe-outputs-handler-manager.md new file mode 100644 index 00000000000..81a53d7d3d0 --- /dev/null +++ b/.changeset/patch-convert-pr-safe-outputs-handler-manager.md @@ -0,0 +1,11 @@ +--- +"gh-aw": patch +--- + +Convert PR-related safe outputs and the `hide-comment` safe output to the +handler-manager architecture used by other safe outputs (e.g. `create-issue`). + +This is an internal refactor: handlers now use the handler factory pattern, +enforce max counts, return result objects, and are managed by the handler +manager. TypeScript, linting, and Go formatting were applied. + diff --git a/.changeset/patch-convert-pr-safe-outputs-to-handler-manager.md b/.changeset/patch-convert-pr-safe-outputs-to-handler-manager.md new file mode 100644 index 00000000000..ee01e72bf01 --- /dev/null +++ b/.changeset/patch-convert-pr-safe-outputs-to-handler-manager.md @@ -0,0 +1,8 @@ +--- +"gh-aw": patch +--- + +Converted PR-related safe outputs and `hide-comment` to the handler manager architecture. Internal refactor only; no user-facing API changes. + +Ahoy! This changeset was generated for PR #8683 by the Changeset Generator. + diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index 8c4cf61bda5..61d9de07255 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -1110,7 +1110,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"spam\",\"ai-generated\",\"link-spam\",\"ai-inspected\"],\"target\":\"*\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"spam\",\"ai-generated\",\"link-spam\",\"ai-inspected\"],\"target\":\"*\"},\"hide_comment\":{\"allowed_reasons\":[\"spam\"],\"max\":5}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1118,17 +1118,4 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Hide Comment - id: hide_comment - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'hide_comment')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/hide_comment.cjs'); - await main(); diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 01d2be32241..7a31ab99770 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -1331,7 +1331,8 @@ jobs: GH_AW_WORKFLOW_ID: "changeset" GH_AW_WORKFLOW_NAME: "Changeset Generator" outputs: - push_to_pull_request_branch_commit_url: ${{ steps.push_to_pull_request_branch.outputs.commit_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1391,34 +1392,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ steps.app-token.outputs.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Update Pull Request - id: update_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'update_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"push_to_pull_request_branch\":{\"base_branch\":\"${{ github.ref_name }}\",\"commit_title_suffix\":\" [skip-ci]\",\"if_no_changes\":\"warn\",\"max_patch_size\":1024},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":false,\"max\":1}}" with: github-token: ${{ steps.app-token.outputs.token }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/update_pull_request.cjs'); - await main(); - - name: Push To Pull Request Branch - id: push_to_pull_request_branch - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_PUSH_IF_NO_CHANGES: "warn" - GH_AW_COMMIT_TITLE_SUFFIX: " [skip-ci]" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/push_to_pull_request_branch.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - name: Invalidate GitHub App token if: always() && steps.app-token.outputs.token != '' diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index 0e0aa8e3807..1222189c778 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -1899,8 +1899,8 @@ jobs: GH_AW_WORKFLOW_ID: "ci-coach" GH_AW_WORKFLOW_NAME: "CI Optimization Coach" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1948,24 +1948,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[ci-coach] " - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[ci-coach] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 74998c38cda..88bd373a6ac 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -1686,8 +1686,6 @@ jobs: GH_AW_WORKFLOW_ID: "cloclo" GH_AW_WORKFLOW_NAME: "/cloclo" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1742,7 +1740,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"labels\":[\"automation\",\"cloclo\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[cloclo] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1750,28 +1748,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[cloclo] " - GH_AW_PR_LABELS: "automation,cloclo" - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); update_cache_memory: needs: diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 4fdf8ca3350..2b628034e11 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -1397,7 +1397,6 @@ jobs: outputs: process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - push_to_pull_request_branch_commit_url: ${{ steps.push_to_pull_request_branch.outputs.commit_url }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1450,7 +1449,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"push_to_pull_request_branch\":{\"base_branch\":\"${{ github.ref_name }}\",\"if_no_changes\":\"warn\",\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1458,19 +1457,4 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Push To Pull Request Branch - id: push_to_pull_request_branch - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_PUSH_IF_NO_CHANGES: "warn" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/push_to_pull_request_branch.cjs'); - await main(); diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index bab2a388b99..034f8d1d521 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -1314,8 +1314,8 @@ jobs: GH_AW_WORKFLOW_ID: "daily-doc-updater" GH_AW_WORKFLOW_NAME: "Daily Documentation Updater" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1363,23 +1363,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index a9312301bac..9edd9429619 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -1201,8 +1201,8 @@ jobs: GH_AW_WORKFLOW_ID: "daily-workflow-updater" GH_AW_WORKFLOW_NAME: "Daily Workflow Updater" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1250,24 +1250,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[actions] " - GH_AW_PR_LABELS: "dependencies,automation" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"dependencies\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[actions] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 730fbdbf7dd..cf69437207b 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -1844,8 +1844,6 @@ jobs: GH_AW_WORKFLOW_ID: "developer-docs-consolidator" GH_AW_WORKFLOW_NAME: "Developer Documentation Consolidator" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1900,7 +1898,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"General\",\"close_older_discussions\":true,\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"General\",\"close_older_discussions\":true,\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"documentation\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[docs] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1908,26 +1906,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[docs] " - GH_AW_PR_LABELS: "documentation,automation" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); update_cache_memory: needs: diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index fa57ee7ba4e..649fb9b5bed 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -1096,8 +1096,8 @@ jobs: GH_AW_WORKFLOW_ID: "dictation-prompt" GH_AW_WORKFLOW_NAME: "Dictation Prompt Generator" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1145,24 +1145,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[docs] " - GH_AW_PR_LABELS: "documentation,automation" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"documentation\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[docs] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 657bc2a6f3d..caba2386f71 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -1684,8 +1684,6 @@ jobs: GH_AW_WORKFLOW_ID: "github-mcp-tools-report" GH_AW_WORKFLOW_NAME: "GitHub MCP Remote Server Tools Report Generator" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1740,7 +1738,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1748,24 +1746,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); update_cache_memory: needs: diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 942943ed5e0..6beb9306e03 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1744,8 +1744,8 @@ jobs: GH_AW_WORKFLOW_ID: "glossary-maintainer" GH_AW_WORKFLOW_NAME: "Glossary Maintainer" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1793,25 +1793,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[docs] " - GH_AW_PR_LABELS: "documentation,glossary" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"documentation\",\"glossary\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[docs] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index f7bc68af9a3..93a0b87647b 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -1409,8 +1409,8 @@ jobs: GH_AW_WORKFLOW_ID: "go-logger" GH_AW_WORKFLOW_NAME: "Go Logger Enhancement" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1458,25 +1458,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[log] " - GH_AW_PR_LABELS: "enhancement,automation" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"enhancement\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[log] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index a7b23f93708..ec9ad83542f 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -1449,8 +1449,8 @@ jobs: GH_AW_WORKFLOW_ID: "hourly-ci-cleaner" GH_AW_WORKFLOW_NAME: "CI Cleaner" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1498,23 +1498,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[ca] " - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[ca] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/incident-response.lock.yml b/.github/workflows/incident-response.lock.yml index 1d99dfd89ac..10db4105ae8 100644 --- a/.github/workflows/incident-response.lock.yml +++ b/.github/workflows/incident-response.lock.yml @@ -1737,8 +1737,6 @@ jobs: GH_AW_WORKFLOW_ID: "incident-response" GH_AW_WORKFLOW_NAME: "Campaign - Incident Response" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1793,7 +1791,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{},\"create_issue\":{\"labels\":[\"campaign-tracker\",\"incident\"],\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{},\"create_issue\":{\"labels\":[\"campaign-tracker\",\"incident\"],\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"labels\":[\"campaign-fix\",\"incident\"],\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1801,23 +1799,4 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_LABELS: "campaign-fix,incident" - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index ca295c6351f..3ba60a12874 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -1289,8 +1289,8 @@ jobs: GH_AW_WORKFLOW_ID: "instructions-janitor" GH_AW_WORKFLOW_NAME: "Instructions Janitor" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1338,25 +1338,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[instructions] " - GH_AW_PR_LABELS: "documentation,automation,instructions" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"documentation\",\"automation\",\"instructions\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[instructions] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/issue-template-optimizer.lock.yml b/.github/workflows/issue-template-optimizer.lock.yml index 17b16de5c7e..04a357aa4ac 100644 --- a/.github/workflows/issue-template-optimizer.lock.yml +++ b/.github/workflows/issue-template-optimizer.lock.yml @@ -1338,8 +1338,8 @@ jobs: GH_AW_WORKFLOW_ID: "issue-template-optimizer" GH_AW_WORKFLOW_NAME: "Issue Template Optimizer" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1387,25 +1387,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[ca] " - GH_AW_PR_LABELS: "documentation,templates" - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":true,\"labels\":[\"documentation\",\"templates\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[ca] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index fcff75e8b17..b5ab6a063b4 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -1376,8 +1376,8 @@ jobs: GH_AW_WORKFLOW_ID: "jsweep" GH_AW_WORKFLOW_NAME: "jsweep - JavaScript Unbloater" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1425,25 +1425,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[jsweep] " - GH_AW_PR_LABELS: "unbloat,automation" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "ignore" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"if_no_changes\":\"ignore\",\"labels\":[\"unbloat\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[jsweep] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/layout-spec-maintainer.lock.yml b/.github/workflows/layout-spec-maintainer.lock.yml index b54d853a3ec..da7b6a78d8e 100644 --- a/.github/workflows/layout-spec-maintainer.lock.yml +++ b/.github/workflows/layout-spec-maintainer.lock.yml @@ -1316,8 +1316,8 @@ jobs: GH_AW_WORKFLOW_ID: "layout-spec-maintainer" GH_AW_WORKFLOW_NAME: "Layout Specification Maintainer" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1365,24 +1365,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[specs] " - GH_AW_PR_LABELS: "documentation,automation" - GH_AW_PR_DRAFT: "false" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"labels\":[\"documentation\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[specs] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index ca05fce89fd..d308e132dc9 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -1428,7 +1428,8 @@ jobs: GH_AW_WORKFLOW_ID: "mergefest" GH_AW_WORKFLOW_NAME: "Mergefest" outputs: - push_to_pull_request_branch_commit_url: ${{ steps.push_to_pull_request_branch.outputs.commit_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1476,19 +1477,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Push To Pull Request Branch - id: push_to_pull_request_branch - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_PUSH_IF_NO_CHANGES: "warn" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"push_to_pull_request_branch\":{\"base_branch\":\"${{ github.ref_name }}\",\"if_no_changes\":\"warn\",\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/push_to_pull_request_branch.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/org-wide-rollout.lock.yml b/.github/workflows/org-wide-rollout.lock.yml index a6376b7bd0a..53259de1efd 100644 --- a/.github/workflows/org-wide-rollout.lock.yml +++ b/.github/workflows/org-wide-rollout.lock.yml @@ -1765,8 +1765,6 @@ jobs: GH_AW_WORKFLOW_ID: "org-wide-rollout" GH_AW_WORKFLOW_NAME: "Campaign - Org-Wide Rollout" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1821,7 +1819,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{},\"create_issue\":{\"labels\":[\"campaign-tracker\",\"org-rollout\"],\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{},\"create_issue\":{\"labels\":[\"campaign-tracker\",\"org-rollout\"],\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"labels\":[\"campaign-pr\",\"org-rollout\"],\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1829,23 +1827,4 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_LABELS: "campaign-pr,org-rollout" - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); diff --git a/.github/workflows/playground-snapshots-refresh.lock.yml b/.github/workflows/playground-snapshots-refresh.lock.yml index 260aab75b2a..1152ef46e4b 100644 --- a/.github/workflows/playground-snapshots-refresh.lock.yml +++ b/.github/workflows/playground-snapshots-refresh.lock.yml @@ -1070,8 +1070,8 @@ jobs: GH_AW_WORKFLOW_ID: "playground-snapshots-refresh" GH_AW_WORKFLOW_NAME: "Refresh playground snapshots" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1119,24 +1119,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[docs] " - GH_AW_PR_LABELS: "documentation" - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"labels\":[\"documentation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[docs] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index fbb4ed3ede1..b81119a1f3e 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1765,11 +1765,8 @@ jobs: outputs: create_agent_task_task_number: ${{ steps.create_agent_task.outputs.task_number }} create_agent_task_task_url: ${{ steps.create_agent_task.outputs.task_url }} - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - push_to_pull_request_branch_commit_url: ${{ steps.push_to_pull_request_branch.outputs.commit_url }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1822,7 +1819,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"poetry\",\"creative\",\"automation\",\"ai-generated\",\"epic\",\"haiku\",\"sonnet\",\"limerick\"],\"max\":5},\"create_discussion\":{\"category\":\"General\",\"close_older_discussions\":true,\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[📜 POETRY] \"},\"create_issue\":{\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[🎭 POEM-BOT] \"},\"create_pull_request_review_comment\":{\"max\":2,\"side\":\"RIGHT\"},\"link_sub_issue\":{\"max\":3,\"parent_required_labels\":[\"poetry\",\"epic\"],\"parent_title_prefix\":\"[🎭 POEM-BOT]\",\"sub_required_labels\":[\"poetry\"],\"sub_title_prefix\":\"[🎭 POEM-BOT]\"},\"update_issue\":{\"allow_body\":true,\"allow_status\":true,\"allow_title\":true,\"max\":2,\"target\":\"*\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"poetry\",\"creative\",\"automation\",\"ai-generated\",\"epic\",\"haiku\",\"sonnet\",\"limerick\"],\"max\":5},\"close_pull_request\":{\"max\":2,\"required_labels\":[\"poetry\",\"automation\"],\"required_title_prefix\":\"[🎨 POETRY]\",\"target\":\"*\"},\"create_discussion\":{\"category\":\"General\",\"close_older_discussions\":true,\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[📜 POETRY] \"},\"create_issue\":{\"labels\":[\"poetry\",\"automation\",\"ai-generated\"],\"max\":2,\"title_prefix\":\"[🎭 POEM-BOT] \"},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024},\"create_pull_request_review_comment\":{\"max\":2,\"side\":\"RIGHT\"},\"link_sub_issue\":{\"max\":3,\"parent_required_labels\":[\"poetry\",\"epic\"],\"parent_title_prefix\":\"[🎭 POEM-BOT]\",\"sub_required_labels\":[\"poetry\"],\"sub_title_prefix\":\"[🎭 POEM-BOT]\"},\"push_to_pull_request_branch\":{\"base_branch\":\"${{ github.ref_name }}\",\"if_no_changes\":\"warn\",\"max_patch_size\":1024},\"update_issue\":{\"allow_body\":true,\"allow_status\":true,\"allow_title\":true,\"max\":2,\"target\":\"*\"}}" GH_AW_SAFE_OUTPUTS_STAGED: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1831,57 +1828,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_SAFE_OUTPUTS_STAGED: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); - - name: Close Pull Request - id: close_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'close_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_STAGED: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/close_pull_request.cjs'); - await main(); - - name: Push To Pull Request Branch - id: push_to_pull_request_branch - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_PUSH_IF_NO_CHANGES: "warn" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_SAFE_OUTPUTS_STAGED: "true" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/push_to_pull_request_branch.cjs'); - await main(); - name: Create Agent Task id: create_agent_task if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_agent_task')) diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index e0f6ca15825..95add7546f2 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1686,8 +1686,6 @@ jobs: GH_AW_WORKFLOW_ID: "q" GH_AW_WORKFLOW_NAME: "Q" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1742,7 +1740,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1750,26 +1748,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); update_cache_memory: needs: diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index c3b6d700fb9..8e4434402d7 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -1321,8 +1321,8 @@ jobs: GH_AW_WORKFLOW_ID: "security-fix-pr" GH_AW_WORKFLOW_NAME: "Security Fix PR" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1370,23 +1370,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index acfe73d3820..2ebf1d4f586 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -1404,8 +1404,8 @@ jobs: GH_AW_WORKFLOW_ID: "slide-deck-maintainer" GH_AW_WORKFLOW_NAME: "Slide Deck Maintainer" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1453,25 +1453,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[slides] " - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_PR_EXPIRES: "24" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"expires\":24,\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[slides] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/smoke-codex-firewall.lock.yml b/.github/workflows/smoke-codex-firewall.lock.yml index cd0d08be158..daa662c6998 100644 --- a/.github/workflows/smoke-codex-firewall.lock.yml +++ b/.github/workflows/smoke-codex-firewall.lock.yml @@ -1201,7 +1201,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-codex-firewall\"]},\"create_issue\":{\"expires\":2,\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-codex-firewall\"]},\"create_issue\":{\"expires\":2,\"max\":1},\"hide_comment\":{\"max\":5}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1209,17 +1209,4 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Hide Comment - id: hide_comment - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'hide_comment')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/hide_comment.cjs'); - await main(); diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 5d96849b130..5bafd90d90f 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1295,7 +1295,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-codex\"]},\"create_issue\":{\"expires\":2,\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-codex\"]},\"create_issue\":{\"expires\":2,\"max\":1},\"hide_comment\":{\"max\":5}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1303,19 +1303,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Hide Comment - id: hide_comment - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'hide_comment')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/hide_comment.cjs'); - await main(); update_cache_memory: needs: diff --git a/.github/workflows/spec-kit-execute.lock.yml b/.github/workflows/spec-kit-execute.lock.yml index 6aa8a12be84..b34c8a62c0a 100644 --- a/.github/workflows/spec-kit-execute.lock.yml +++ b/.github/workflows/spec-kit-execute.lock.yml @@ -1567,8 +1567,8 @@ jobs: GH_AW_WORKFLOW_ID: "spec-kit-execute" GH_AW_WORKFLOW_NAME: "Spec-Kit Execute" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1616,23 +1616,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/spec-kit-executor.lock.yml b/.github/workflows/spec-kit-executor.lock.yml index 6a3234d82d3..11c9ea9cb3d 100644 --- a/.github/workflows/spec-kit-executor.lock.yml +++ b/.github/workflows/spec-kit-executor.lock.yml @@ -1413,8 +1413,8 @@ jobs: GH_AW_WORKFLOW_ID: "spec-kit-executor" GH_AW_WORKFLOW_NAME: "Spec Kit Executor" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1462,23 +1462,18 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); update_cache_memory: diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 170e6f69fdf..43c0460d85d 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -1576,8 +1576,6 @@ jobs: GH_AW_WORKFLOW_ID: "technical-doc-writer" GH_AW_WORKFLOW_NAME: "Technical Doc Writer" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1632,7 +1630,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1640,24 +1638,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); update_cache_memory: needs: diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 0791aed5b76..da63d3a5310 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -1257,9 +1257,8 @@ jobs: GH_AW_WORKFLOW_ID: "tidy" GH_AW_WORKFLOW_NAME: "Tidy" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} - push_to_pull_request_branch_commit_url: ${{ steps.push_to_pull_request_branch.outputs.commit_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Checkout actions folder uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -1307,39 +1306,17 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + - name: Process Safe Outputs + id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); - - name: Push To Pull Request Branch - id: push_to_pull_request_branch - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_PUSH_IF_NO_CHANGES: "warn" - GH_AW_MAX_PATCH_SIZE: 1024 + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"max\":1,\"max_patch_size\":1024},\"push_to_pull_request_branch\":{\"base_branch\":\"${{ github.ref_name }}\",\"if_no_changes\":\"warn\",\"max_patch_size\":1024}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/push_to_pull_request_branch.cjs'); + const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 60ed698ca8c..be0545eb70a 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1632,8 +1632,6 @@ jobs: GH_AW_WORKFLOW_ID: "unbloat-docs" GH_AW_WORKFLOW_NAME: "Documentation Unbloat" outputs: - create_pull_request_pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} - create_pull_request_pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1688,7 +1686,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":true,\"labels\":[\"documentation\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"[docs] \"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1696,28 +1694,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/tmp/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Create Pull Request - id: create_pull_request - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_BASE_BRANCH: ${{ github.ref_name }} - GH_AW_PR_TITLE_PREFIX: "[docs] " - GH_AW_PR_LABELS: "documentation,automation" - GH_AW_PR_DRAFT: "true" - GH_AW_PR_IF_NO_CHANGES: "warn" - GH_AW_PR_ALLOW_EMPTY: "false" - GH_AW_MAX_PATCH_SIZE: 1024 - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/tmp/gh-aw/actions/create_pull_request.cjs'); - await main(); update_cache_memory: needs: diff --git a/actions/setup/js/close_pull_request.cjs b/actions/setup/js/close_pull_request.cjs index f293beec87d..e5c59a90e54 100644 --- a/actions/setup/js/close_pull_request.cjs +++ b/actions/setup/js/close_pull_request.cjs @@ -1,7 +1,13 @@ // @ts-check /// -const { processCloseEntityItems, PULL_REQUEST_CONFIG } = require("./close_entity_helpers.cjs"); +const { getErrorMessage } = require("./error_helpers.cjs"); + +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + */ + +const HANDLER_TYPE = "close_pull_request"; /** * Get pull request details using REST API @@ -64,12 +70,201 @@ async function closePullRequest(github, owner, repo, prNumber) { return pr; } -async function main() { - return processCloseEntityItems(PULL_REQUEST_CONFIG, { - getDetails: getPullRequestDetails, - addComment: addPullRequestComment, - closeEntity: closePullRequest, - }); +/** + * Handler factory for close-pull-request safe outputs + * @type {HandlerFactoryFunction} + */ +async function main(config = {}) { + // Extract configuration + const requiredLabels = config.required_labels || []; + const requiredTitlePrefix = config.required_title_prefix || ""; + const maxCount = config.max || 10; + const comment = config.comment || ""; + + core.info(`Close pull request configuration: max=${maxCount}`); + if (requiredLabels.length > 0) { + core.info(`Required labels: ${requiredLabels.join(", ")}`); + } + if (requiredTitlePrefix) { + core.info(`Required title prefix: ${requiredTitlePrefix}`); + } + + // Track how many items we've processed for max limit + let processedCount = 0; + + /** + * Message handler function that processes a single close_pull_request message + * @param {Object} message - The close_pull_request message to process + * @param {Object} resolvedTemporaryIds - Map of temporary IDs to {repo, number} + * @returns {Promise} Result with success/error status + */ + return async function handleClosePullRequest(message, resolvedTemporaryIds) { + // Check if we've hit the max limit + if (processedCount >= maxCount) { + core.warning(`Skipping close_pull_request: max count of ${maxCount} reached`); + return { + success: false, + error: `Max count of ${maxCount} reached`, + }; + } + + processedCount++; + + const item = message; + + // Determine PR number + let prNumber; + if (item.pull_request_number !== undefined) { + prNumber = parseInt(String(item.pull_request_number), 10); + if (isNaN(prNumber)) { + core.warning(`Invalid pull request number: ${item.pull_request_number}`); + return { + success: false, + error: `Invalid pull request number: ${item.pull_request_number}`, + }; + } + } else { + // Use context PR if available + const contextPR = context.payload?.pull_request?.number; + if (!contextPR) { + core.warning("No pull_request_number provided and not in pull request context"); + return { + success: false, + error: "No pull_request_number provided and not in pull request context", + }; + } + prNumber = contextPR; + } + + core.info(`Processing close_pull_request for PR #${prNumber}`); + + // Get PR details + const { owner, repo } = context.repo; + let pr; + try { + pr = await getPullRequestDetails(github, owner, repo, prNumber); + } catch (error) { + const errorMsg = getErrorMessage(error); + core.warning(`Failed to get PR #${prNumber} details: ${errorMsg}`); + return { + success: false, + error: `Failed to get PR #${prNumber} details: ${errorMsg}`, + }; + } + + // Check if already closed + if (pr.state === "closed") { + core.info(`PR #${prNumber} is already closed`); + return { + success: true, + pull_request_number: pr.number, + pull_request_url: pr.html_url, + }; + } + + // Check label filter + if (!checkLabelFilter(pr.labels, requiredLabels)) { + core.info(`Skipping PR #${prNumber}: does not match label filter (required: ${requiredLabels.join(", ")})`); + return { + success: false, + error: `PR does not match required labels`, + }; + } + + // Check title prefix filter + if (!checkTitlePrefixFilter(pr.title, requiredTitlePrefix)) { + core.info(`Skipping PR #${prNumber}: title does not start with '${requiredTitlePrefix}'`); + return { + success: false, + error: `PR title does not start with required prefix`, + }; + } + + // Add comment if requested + if (comment && comment.trim()) { + try { + const triggeringPRNumber = context.payload?.pull_request?.number; + const triggeringIssueNumber = context.payload?.issue?.number; + const commentBody = buildCommentBody(comment, triggeringIssueNumber, triggeringPRNumber); + await addPullRequestComment(github, owner, repo, prNumber, commentBody); + core.info(`Added comment to PR #${prNumber}`); + } catch (error) { + const errorMsg = getErrorMessage(error); + core.warning(`Failed to add comment to PR #${prNumber}: ${errorMsg}`); + // Continue with closing even if comment fails + } + } + + // Close the PR + try { + const closedPR = await closePullRequest(github, owner, repo, prNumber); + core.info(`✓ Closed PR #${prNumber}: ${closedPR.title}`); + return { + success: true, + pull_request_number: closedPR.number, + pull_request_url: closedPR.html_url, + }; + } catch (error) { + const errorMsg = getErrorMessage(error); + core.warning(`Failed to close PR #${prNumber}: ${errorMsg}`); + return { + success: false, + error: `Failed to close PR #${prNumber}: ${errorMsg}`, + }; + } + }; +} + +/** + * Check if labels match the required labels filter + * @param {Array<{name: string}>} prLabels - Labels on the PR + * @param {string[]} requiredLabels - Required labels (any match) + * @returns {boolean} True if PR has at least one required label + */ +function checkLabelFilter(prLabels, requiredLabels) { + if (requiredLabels.length === 0) { + return true; + } + const labelNames = prLabels.map(l => l.name); + return requiredLabels.some(required => labelNames.includes(required)); +} + +/** + * Check if title matches the required prefix filter + * @param {string} title - PR title + * @param {string} requiredTitlePrefix - Required title prefix + * @returns {boolean} True if title starts with required prefix + */ +function checkTitlePrefixFilter(title, requiredTitlePrefix) { + if (!requiredTitlePrefix) { + return true; + } + return title.startsWith(requiredTitlePrefix); +} + +/** + * Build comment body with tracker ID and footer + * @param {string} body - The original comment body + * @param {number|undefined} triggeringIssueNumber - Issue number that triggered this workflow + * @param {number|undefined} triggeringPRNumber - PR number that triggered this workflow + * @returns {string} The complete comment body with tracker ID and footer + */ +function buildCommentBody(body, triggeringIssueNumber, triggeringPRNumber) { + const { getTrackerID } = require("./get_tracker_id.cjs"); + const { generateFooter } = require("./generate_footer.cjs"); + + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE || ""; + const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL || ""; + const runId = context.runId; + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + let commentBody = body.trim(); + commentBody += getTrackerID("markdown"); + commentBody += generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined); + + return commentBody; } module.exports = { main }; diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 01e3e4f5613..722ff10acdd 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -10,6 +10,14 @@ const { getTrackerID } = require("./get_tracker_id.cjs"); const { addExpirationComment } = require("./expiration_helpers.cjs"); const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { replaceTemporaryIdReferences } = require("./temporary_id.cjs"); + +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + */ + +/** @type {string} Safe output type handled by this module */ +const HANDLER_TYPE = "create_pull_request"; /** * Generate a patch preview with max 500 lines and 2000 chars for issue body @@ -41,17 +49,22 @@ function generatePatchPreview(patchContent) { return `\n\n
${summary}\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n
`; } -async function main() { - // Initialize outputs to empty strings to ensure they're always set - core.setOutput("pull_request_number", ""); - core.setOutput("pull_request_url", ""); - core.setOutput("issue_number", ""); - core.setOutput("issue_url", ""); - core.setOutput("branch_name", ""); - core.setOutput("fallback_used", ""); - - // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; +/** + * Main handler factory for create_pull_request + * Returns a message handler function that processes individual create_pull_request messages + * @type {HandlerFactoryFunction} + */ +async function main(config = {}) { + // Extract configuration + const titlePrefix = config.title_prefix || ""; + const envLabels = config.labels ? (Array.isArray(config.labels) ? config.labels : config.labels.split(",")).map(label => String(label).trim()).filter(label => label) : []; + const draftDefault = config.draft !== undefined ? config.draft : true; + const ifNoChanges = config.if_no_changes || "warn"; + const allowEmpty = config.allow_empty || false; + const expiresHours = config.expires ? parseInt(String(config.expires), 10) : 0; + const maxCount = config.max || 1; // PRs are typically limited to 1 + const baseBranch = config.base_branch || ""; + const maxSizeKb = config.max_patch_size ? parseInt(String(config.max_patch_size), 10) : 1024; // Environment validation - fail early if required variables are missing const workflowId = process.env.GH_AW_WORKFLOW_ID; @@ -59,397 +72,415 @@ async function main() { throw new Error("GH_AW_WORKFLOW_ID environment variable is required"); } - const baseBranch = process.env.GH_AW_BASE_BRANCH; if (!baseBranch) { - throw new Error("GH_AW_BASE_BRANCH environment variable is required"); + throw new Error("base_branch configuration is required"); } - const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT || ""; + // Check if we're in staged mode + const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; - // Read agent output from file - let outputContent = ""; - if (agentOutputFile.trim() !== "") { - try { - outputContent = fs.readFileSync(agentOutputFile, "utf8"); - } catch (error) { - core.setFailed(`Error reading agent output file: ${getErrorMessage(error)}`); - return; - } + core.info(`Base branch: ${baseBranch}`); + if (envLabels.length > 0) { + core.info(`Default labels: ${envLabels.join(", ")}`); } - - if (outputContent.trim() === "") { - core.info("Agent output content is empty"); + if (titlePrefix) { + core.info(`Title prefix: ${titlePrefix}`); + } + core.info(`Draft default: ${draftDefault}`); + core.info(`If no changes: ${ifNoChanges}`); + core.info(`Allow empty: ${allowEmpty}`); + if (expiresHours > 0) { + core.info(`Pull requests expire after: ${expiresHours} hours`); } + core.info(`Max count: ${maxCount}`); + core.info(`Max patch size: ${maxSizeKb} KB`); + + // Track how many items we've processed for max limit + let processedCount = 0; + + /** + * Message handler function that processes a single create_pull_request message + * @param {Object} message - The create_pull_request message to process + * @param {Object} resolvedTemporaryIds - Map of temporary IDs to {repo, number} + * @returns {Promise} Result with success/error status and PR details + */ + return async function handleCreatePullRequest(message, resolvedTemporaryIds) { + // Check if we've hit the max limit + if (processedCount >= maxCount) { + core.warning(`Skipping create_pull_request: max count of ${maxCount} reached`); + return { + success: false, + error: `Max count of ${maxCount} reached`, + }; + } - const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; - const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; + processedCount++; - // Check if patch file exists and has valid content - if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - // If allow-empty is enabled, we can proceed without a patch file - if (allowEmpty) { - core.info("No patch file found, but allow-empty is enabled - will create empty PR"); - } else { - const message = "No patch file found - cannot create pull request without changes"; - - // If in staged mode, still show preview - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - - // Write to step summary - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; + const pullRequestItem = message; + + core.info(`Processing create_pull_request: title=${pullRequestItem.title || "No title"}, bodyLength=${pullRequestItem.body?.length || 0}`); + + // Check if patch file exists and has valid content + if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { + // If allow-empty is enabled, we can proceed without a patch file + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + + // If in staged mode, still show preview + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); + return { success: true, staged: true }; + } + + switch (ifNoChanges) { + case "error": + return { success: false, error: message }; + + case "ignore": + // Silent success - no console output + return { success: false, skipped: true }; + + case "warn": + default: + core.warning(message); + return { success: false, error: message, skipped: true }; + } } + } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - // Silent success - no console output - return; - case "warn": - default: - core.warning(message); - return; + let patchContent = ""; + let isEmpty = true; + + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } + + // Check for actual error conditions (but allow empty patches as valid noop) + if (patchContent.includes("Failed to generate patch")) { + // If allow-empty is enabled, ignore patch errors and proceed + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + + // If in staged mode, still show preview + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); + return { success: true, staged: true }; + } + + switch (ifNoChanges) { + case "error": + return { success: false, error: message }; + + case "ignore": + // Silent success - no console output + return { success: false, skipped: true }; + + case "warn": + default: + core.warning(message); + return { success: false, error: message, skipped: true }; + } } } - } - let patchContent = ""; - let isEmpty = true; + // Validate patch size (unless empty) + if (!isEmpty) { + // maxSizeKb is already extracted from config at the top + const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); + const patchSizeKb = Math.ceil(patchSizeBytes / 1024); - if (fs.existsSync("/tmp/gh-aw/aw.patch")) { - patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - isEmpty = !patchContent || !patchContent.trim(); - } + core.info(`Patch size: ${patchSizeKb} KB (maximum allowed: ${maxSizeKb} KB)`); - // Check for actual error conditions (but allow empty patches as valid noop) - if (patchContent.includes("Failed to generate patch")) { - // If allow-empty is enabled, ignore patch errors and proceed - if (allowEmpty) { - core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); - patchContent = ""; - isEmpty = true; - } else { - const message = "Patch file contains error message - cannot create pull request without changes"; - - // If in staged mode, still show preview - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - - // Write to step summary - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; + if (patchSizeKb > maxSizeKb) { + const message = `Patch size (${patchSizeKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`; + + // If in staged mode, still show preview with error + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ❌ Patch size exceeded\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch size error)"); + return { success: true, staged: true }; + } + + return { success: false, error: message }; } + core.info("Patch size validation passed"); + } + + if (isEmpty && !isStaged && !allowEmpty) { + const message = "Patch file is empty - no changes to apply (noop operation)"; + switch (ifNoChanges) { case "error": - throw new Error(message); + return { success: false, error: "No changes to push - failing as configured by if-no-changes: error" }; + case "ignore": // Silent success - no console output - return; + return { success: false, skipped: true }; + case "warn": default: core.warning(message); - return; + return { success: false, error: message, skipped: true }; } } - } - - // Validate patch size (unless empty) - if (!isEmpty) { - // Get maximum patch size from environment (default: 1MB = 1024 KB) - const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); - const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); - const patchSizeKb = Math.ceil(patchSizeBytes / 1024); - - core.info(`Patch size: ${patchSizeKb} KB (maximum allowed: ${maxSizeKb} KB)`); - - if (patchSizeKb > maxSizeKb) { - const message = `Patch size (${patchSizeKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`; - - // If in staged mode, still show preview with error - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ❌ Patch size exceeded\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - - // Write to step summary - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch size error)"); - return; - } - - throw new Error(message); - } - core.info("Patch size validation passed"); - } - - if (isEmpty && !isStaged && !allowEmpty) { - const message = "Patch file is empty - no changes to apply (noop operation)"; - - switch (ifNoChanges) { - case "error": - throw new Error("No changes to push - failing as configured by if-no-changes: error"); - case "ignore": - // Silent success - no console output - return; - case "warn": - default: - core.warning(message); - return; + if (!isEmpty) { + core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); + } else { + core.info("Patch file is empty - processing noop operation"); } - } - - core.info(`Agent output content length: ${outputContent.length}`); - if (!isEmpty) { - core.info("Patch content validation passed"); - } else if (allowEmpty) { - core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); - } else { - core.info("Patch file is empty - processing noop operation"); - } - - // Parse the validated output JSON - let validatedOutput; - try { - validatedOutput = JSON.parse(outputContent); - } catch (error) { - core.setFailed(`Error parsing agent output JSON: ${getErrorMessage(error)}`); - return; - } - if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { - core.warning("No valid items found in agent output"); - return; - } - - // Find the create-pull-request item - const pullRequestItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === "create_pull_request"); - if (!pullRequestItem) { - core.warning("No create-pull-request item found in agent output"); - return; - } + // If in staged mode, emit step summary instead of creating PR + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - core.info(`Found create-pull-request item: title="${pullRequestItem.title}", bodyLength=${pullRequestItem.body.length}`); + summaryContent += `**Title:** ${pullRequestItem.title || "No title provided"}\n\n`; + summaryContent += `**Branch:** ${pullRequestItem.branch || "auto-generated"}\n\n`; + summaryContent += `**Base:** ${baseBranch}\n\n`; - // If in staged mode, emit step summary instead of creating PR - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + if (pullRequestItem.body) { + summaryContent += `**Body:**\n${pullRequestItem.body}\n\n`; + } - summaryContent += `**Title:** ${pullRequestItem.title || "No title provided"}\n\n`; - summaryContent += `**Branch:** ${pullRequestItem.branch || "auto-generated"}\n\n`; - summaryContent += `**Base:** ${baseBranch}\n\n`; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchStats = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + if (patchStats.trim()) { + summaryContent += `**Changes:** Patch file exists with ${patchStats.split("\n").length} lines\n\n`; + summaryContent += `
Show patch preview\n\n\`\`\`diff\n${patchStats.slice(0, 2000)}${patchStats.length > 2000 ? "\n... (truncated)" : ""}\n\`\`\`\n\n
\n\n`; + } else { + summaryContent += `**Changes:** No changes (empty patch)\n\n`; + } + } - if (pullRequestItem.body) { - summaryContent += `**Body:**\n${pullRequestItem.body}\n\n`; + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary"); + return { success: true, staged: true }; } - if (fs.existsSync("/tmp/gh-aw/aw.patch")) { - const patchStats = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - if (patchStats.trim()) { - summaryContent += `**Changes:** Patch file exists with ${patchStats.split("\n").length} lines\n\n`; - summaryContent += `
Show patch preview\n\n\`\`\`diff\n${patchStats.slice(0, 2000)}${patchStats.length > 2000 ? "\n... (truncated)" : ""}\n\`\`\`\n\n
\n\n`; - } else { - summaryContent += `**Changes:** No changes (empty patch)\n\n`; - } + // Extract title, body, and branch from the JSON item + let title = pullRequestItem.title.trim(); + let processedBody = pullRequestItem.body; + + // Replace temporary ID references in the body with resolved issue/PR numbers + // This allows PRs to reference issues created earlier in the same workflow + // by using temporary IDs like #aw_123abc456def + if (resolvedTemporaryIds && Object.keys(resolvedTemporaryIds).length > 0) { + // Convert object to Map for compatibility with replaceTemporaryIdReferences + const tempIdMap = new Map(Object.entries(resolvedTemporaryIds)); + const currentRepo = `${context.repo.owner}/${context.repo.repo}`; + processedBody = replaceTemporaryIdReferences(processedBody, tempIdMap, currentRepo); + core.info(`Resolved ${tempIdMap.size} temporary ID references in PR body`); } - // Write to step summary - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary"); - return; - } - - // Extract title, body, and branch from the JSON item - let title = pullRequestItem.title.trim(); - let processedBody = pullRequestItem.body; + // Remove duplicate title from description if it starts with a header matching the title + processedBody = removeDuplicateTitleFromDescription(title, processedBody); - // Remove duplicate title from description if it starts with a header matching the title - processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); + let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; - let bodyLines = processedBody.split("\n"); - let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; + // If no title was found, use a default + if (!title) { + title = "Agent Output"; + } - // If no title was found, use a default - if (!title) { - title = "Agent Output"; - } + // Apply title prefix from config + if (titlePrefix && !title.startsWith(titlePrefix)) { + title = titlePrefix + title; + } - // Apply title prefix if provided via environment variable - const titlePrefix = process.env.GH_AW_PR_TITLE_PREFIX; - if (titlePrefix && !title.startsWith(titlePrefix)) { - title = titlePrefix + title; - } + // Add AI disclaimer with workflow name and run url + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const runId = context.runId; + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - // Add AI disclaimer with workflow name and run url - const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; - const runId = context.runId; - const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; - const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + // Add fingerprint comment if present + const trackerIDComment = getTrackerID("markdown"); + if (trackerIDComment) { + bodyLines.push(trackerIDComment); + } - // Add fingerprint comment if present - const trackerIDComment = getTrackerID("markdown"); - if (trackerIDComment) { - bodyLines.push(trackerIDComment); - } + // Add expiration comment if expires is set (from config) + if (expiresHours > 0) { + const expiresDate = new Date(); + expiresDate.setHours(expiresDate.getHours() + expiresHours); + const expiresString = expiresDate.toISOString(); + bodyLines.push(``, ``, ``); + } - // Add expiration comment if expires is set (only for same-repo PRs) - addExpirationComment(bodyLines, "GH_AW_PR_EXPIRES", "Pull Request"); - - bodyLines.push(``, ``, `> AI generated by [${workflowName}](${runUrl})`, ""); - - // Prepare the body content - const body = bodyLines.join("\n").trim(); - - // Parse labels from environment variable (comma-separated string) - const labelsEnv = process.env.GH_AW_PR_LABELS; - const labels = labelsEnv - ? labelsEnv - .split(",") - .map(/** @param {string} label */ label => label.trim()) - .filter(/** @param {string} label */ label => label) - : []; - - // Parse draft setting from environment variable (defaults to true) - const draftEnv = process.env.GH_AW_PR_DRAFT; - const draft = draftEnv ? draftEnv.toLowerCase() === "true" : true; - - core.info(`Creating pull request with title: ${title}`); - core.info(`Labels: ${JSON.stringify(labels)}`); - core.info(`Draft: ${draft}`); - core.info(`Body length: ${body.length}`); - - const randomHex = crypto.randomBytes(8).toString("hex"); - // Use branch name from JSONL if provided, otherwise generate unique branch name - if (!branchName) { - core.info("No branch name provided in JSONL, generating unique branch name"); - // Generate unique branch name using cryptographic random hex - branchName = `${workflowId}-${randomHex}`; - } else { - branchName = `${branchName}-${randomHex}`; - core.info(`Using branch name from JSONL with added salt: ${branchName}`); - } + bodyLines.push(``, ``, `> AI generated by [${workflowName}](${runUrl})`, ""); - core.info(`Generated branch name: ${branchName}`); - core.info(`Base branch: ${baseBranch}`); + // Prepare the body content + const body = bodyLines.join("\n").trim(); - // Create a new branch using git CLI, ensuring it's based on the correct base branch + // Build labels array - merge config labels with message labels + let labels = [...envLabels]; + if (pullRequestItem.labels && Array.isArray(pullRequestItem.labels)) { + labels = [...labels, ...pullRequestItem.labels]; + } + labels = labels + .filter(label => !!label) + .map(label => String(label).trim()) + .filter(label => label); + + // Use draft setting from message if provided, otherwise use config default + const draft = pullRequestItem.draft !== undefined ? pullRequestItem.draft : draftDefault; + + core.info(`Creating pull request with title: ${title}`); + core.info(`Labels: ${JSON.stringify(labels)}`); + core.info(`Draft: ${draft}`); + core.info(`Body length: ${body.length}`); + + const randomHex = crypto.randomBytes(8).toString("hex"); + // Use branch name from JSONL if provided, otherwise generate unique branch name + if (!branchName) { + core.info("No branch name provided in JSONL, generating unique branch name"); + // Generate unique branch name using cryptographic random hex + branchName = `${workflowId}-${randomHex}`; + } else { + branchName = `${branchName}-${randomHex}`; + core.info(`Using branch name from JSONL with added salt: ${branchName}`); + } - // First, fetch the base branch specifically (since we use shallow checkout) - core.info(`Fetching base branch: ${baseBranch}`); + core.info(`Generated branch name: ${branchName}`); + core.info(`Base branch: ${baseBranch}`); - // Fetch without creating/updating local branch to avoid conflicts with current branch - // This works even when we're already on the base branch - await exec.exec(`git fetch origin ${baseBranch}`); + // Create a new branch using git CLI, ensuring it's based on the correct base branch - // Checkout the base branch (using origin/${baseBranch} if local doesn't exist) - try { - await exec.exec(`git checkout ${baseBranch}`); - } catch (checkoutError) { - // If local branch doesn't exist, create it from origin - core.info(`Local branch ${baseBranch} doesn't exist, creating from origin/${baseBranch}`); - await exec.exec(`git checkout -b ${baseBranch} origin/${baseBranch}`); - } + // First, fetch the base branch specifically (since we use shallow checkout) + core.info(`Fetching base branch: ${baseBranch}`); - // Handle branch creation/checkout - core.info(`Branch should not exist locally, creating new branch from base: ${branchName}`); - await exec.exec(`git checkout -b ${branchName}`); - core.info(`Created new branch from base: ${branchName}`); - - // Apply the patch using git CLI (skip if empty) - if (!isEmpty) { - core.info("Applying patch..."); - - // Log first 500 lines of patch for debugging - const patchLines = patchContent.split("\n"); - const previewLineCount = Math.min(500, patchLines.length); - core.info(`Patch preview (first ${previewLineCount} of ${patchLines.length} lines):`); - for (let i = 0; i < previewLineCount; i++) { - core.info(patchLines[i]); - } + // Fetch without creating/updating local branch to avoid conflicts with current branch + // This works even when we're already on the base branch + await exec.exec(`git fetch origin ${baseBranch}`); - // Patches are created with git format-patch, so use git am to apply them + // Checkout the base branch (using origin/${baseBranch} if local doesn't exist) try { - await exec.exec("git am /tmp/gh-aw/aw.patch"); - core.info("Patch applied successfully"); - } catch (patchError) { - core.error(`Failed to apply patch: ${patchError instanceof Error ? patchError.message : String(patchError)}`); + await exec.exec(`git checkout ${baseBranch}`); + } catch (checkoutError) { + // If local branch doesn't exist, create it from origin + core.info(`Local branch ${baseBranch} doesn't exist, creating from origin/${baseBranch}`); + await exec.exec(`git checkout -b ${baseBranch} origin/${baseBranch}`); + } - // Investigate why the patch failed by logging git status and the failed patch - try { - core.info("Investigating patch failure..."); - - // Log git status to see the current state - const statusResult = await exec.getExecOutput("git", ["status"]); - core.info("Git status output:"); - core.info(statusResult.stdout); - - // Log the failed patch diff - const patchResult = await exec.getExecOutput("git", ["am", "--show-current-patch=diff"]); - core.info("Failed patch content:"); - core.info(patchResult.stdout); - } catch (investigateError) { - core.warning(`Failed to investigate patch failure: ${investigateError instanceof Error ? investigateError.message : String(investigateError)}`); + // Handle branch creation/checkout + core.info(`Branch should not exist locally, creating new branch from base: ${branchName}`); + await exec.exec(`git checkout -b ${branchName}`); + core.info(`Created new branch from base: ${branchName}`); + + // Apply the patch using git CLI (skip if empty) + if (!isEmpty) { + core.info("Applying patch..."); + + // Log first 500 lines of patch for debugging + const patchLines = patchContent.split("\n"); + const previewLineCount = Math.min(500, patchLines.length); + core.info(`Patch preview (first ${previewLineCount} of ${patchLines.length} lines):`); + for (let i = 0; i < previewLineCount; i++) { + core.info(patchLines[i]); } - core.setFailed("Failed to apply patch"); - return; - } - - // Push the applied commits to the branch (with fallback to issue creation on failure) - try { - // Check if remote branch already exists (optional precheck) - let remoteBranchExists = false; + // Patches are created with git format-patch, so use git am to apply them try { - const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); - if (stdout.trim()) { - remoteBranchExists = true; + await exec.exec("git am /tmp/gh-aw/aw.patch"); + core.info("Patch applied successfully"); + } catch (patchError) { + core.error(`Failed to apply patch: ${patchError instanceof Error ? patchError.message : String(patchError)}`); + + // Investigate why the patch failed by logging git status and the failed patch + try { + core.info("Investigating patch failure..."); + + // Log git status to see the current state + const statusResult = await exec.getExecOutput("git", ["status"]); + core.info("Git status output:"); + core.info(statusResult.stdout); + + // Log the failed patch diff + const patchResult = await exec.getExecOutput("git", ["am", "--show-current-patch=diff"]); + core.info("Failed patch content:"); + core.info(patchResult.stdout); + } catch (investigateError) { + core.warning(`Failed to investigate patch failure: ${investigateError instanceof Error ? investigateError.message : String(investigateError)}`); } - } catch (checkError) { - core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); - } - if (remoteBranchExists) { - core.warning(`Remote branch ${branchName} already exists - appending random suffix`); - const extraHex = crypto.randomBytes(4).toString("hex"); - const oldBranch = branchName; - branchName = `${branchName}-${extraHex}`; - // Rename local branch - await exec.exec(`git branch -m ${oldBranch} ${branchName}`); - core.info(`Renamed branch to ${branchName}`); + return { success: false, error: "Failed to apply patch" }; } - await exec.exec(`git push origin ${branchName}`); - core.info("Changes pushed to branch"); - } catch (pushError) { - // Push failed - create fallback issue instead of PR - core.error(`Git push failed: ${pushError instanceof Error ? pushError.message : String(pushError)}`); - core.warning("Git push operation failed - creating fallback issue instead of pull request"); + // Push the applied commits to the branch (with fallback to issue creation on failure) + try { + // Check if remote branch already exists (optional precheck) + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } - const runId = context.runId; - const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; - const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + // Rename local branch + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } - // Read patch content for preview - let patchPreview = ""; - if (fs.existsSync("/tmp/gh-aw/aw.patch")) { - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - patchPreview = generatePatchPreview(patchContent); - } + await exec.exec(`git push origin ${branchName}`); + core.info("Changes pushed to branch"); + } catch (pushError) { + // Push failed - create fallback issue instead of PR + core.error(`Git push failed: ${pushError instanceof Error ? pushError.message : String(pushError)}`); + core.warning("Git push operation failed - creating fallback issue instead of pull request"); + + const runId = context.runId; + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + // Read patch content for preview + let patchPreview = ""; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + patchPreview = generatePatchPreview(patchContent); + } - const fallbackBody = `${body} + const fallbackBody = `${body} --- @@ -472,31 +503,24 @@ git am aw.patch \`\`\` ${patchPreview}`; - try { - const { data: issue } = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: title, - body: fallbackBody, - labels: labels, - }); + try { + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: fallbackBody, + labels: labels, + }); - core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); + core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); - // Update the activation comment with issue link (if a comment was created) - await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); - - // Set outputs for push failure fallback - core.setOutput("issue_number", issue.number); - core.setOutput("issue_url", issue.html_url); - core.setOutput("branch_name", branchName); - core.setOutput("fallback_used", "true"); - core.setOutput("push_failed", "true"); + // Update the activation comment with issue link (if a comment was created) + await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); - // Write summary to GitHub Actions summary - await core.summary - .addRaw( - ` + // Write summary to GitHub Actions summary + await core.summary + .addRaw( + ` ## Push Failure Fallback - **Push Error:** ${pushError instanceof Error ? pushError.message : String(pushError)} @@ -504,135 +528,154 @@ ${patchPreview}`; - **Patch Artifact:** Available in workflow run artifacts - **Note:** Push failed, created issue as fallback ` - ) - .write(); - - return; - } catch (issueError) { - core.setFailed( - `Failed to push and failed to create fallback issue. Push error: ${pushError instanceof Error ? pushError.message : String(pushError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}` - ); - return; + ) + .write(); + + return { + success: true, + fallback_used: true, + push_failed: true, + issue_number: issue.number, + issue_url: issue.html_url, + branch_name: branchName, + }; + } catch (issueError) { + const error = `Failed to push and failed to create fallback issue. Push error: ${pushError instanceof Error ? pushError.message : String(pushError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}`; + core.error(error); + return { + success: false, + error, + }; + } } - } - } else { - core.info("Skipping patch application (empty patch)"); - - // For empty patches with allow-empty, we still need to push the branch - if (allowEmpty) { - core.info("allow-empty is enabled - will create branch and push with empty commit"); - // Push the branch with an empty commit to allow PR creation - try { - // Create an empty commit to ensure there's a commit difference - await exec.exec(`git commit --allow-empty -m "Initialize"`); - core.info("Created empty commit"); + } else { + core.info("Skipping patch application (empty patch)"); - // Check if remote branch already exists (optional precheck) - let remoteBranchExists = false; + // For empty patches with allow-empty, we still need to push the branch + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push with empty commit"); + // Push the branch with an empty commit to allow PR creation try { - const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); - if (stdout.trim()) { - remoteBranchExists = true; + // Create an empty commit to ensure there's a commit difference + await exec.exec(`git commit --allow-empty -m "Initialize"`); + core.info("Created empty commit"); + + // Check if remote branch already exists (optional precheck) + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); } - } catch (checkError) { - core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); - } - if (remoteBranchExists) { - core.warning(`Remote branch ${branchName} already exists - appending random suffix`); - const extraHex = crypto.randomBytes(4).toString("hex"); - const oldBranch = branchName; - branchName = `${branchName}-${extraHex}`; - // Rename local branch - await exec.exec(`git branch -m ${oldBranch} ${branchName}`); - core.info(`Renamed branch to ${branchName}`); + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + // Rename local branch + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + const error = `Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`; + core.error(error); + return { + success: false, + error, + }; } + } else { + // For empty patches without allow-empty, handle if-no-changes configuration + const message = "No changes to apply - noop operation completed successfully"; - await exec.exec(`git push origin ${branchName}`); - core.info("Empty branch pushed successfully"); - } catch (pushError) { - core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); - return; - } - } else { - // For empty patches without allow-empty, handle if-no-changes configuration - const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + return { success: false, error: "No changes to apply - failing as configured by if-no-changes: error" }; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - // Silent success - no console output - return; - case "warn": - default: - core.warning(message); - return; + case "ignore": + // Silent success - no console output + return { success: false, skipped: true }; + + case "warn": + default: + core.warning(message); + return { success: false, error: message, skipped: true }; + } } } - } - // Try to create the pull request, with fallback to issue creation - try { - const { data: pullRequest } = await github.rest.pulls.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: title, - body: body, - head: branchName, - base: baseBranch, - draft: draft, - }); - - core.info(`Created pull request #${pullRequest.number}: ${pullRequest.html_url}`); - - // Add labels if specified - if (labels.length > 0) { - await github.rest.issues.addLabels({ + // Try to create the pull request, with fallback to issue creation + try { + const { data: pullRequest } = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: pullRequest.number, - labels: labels, + title: title, + body: body, + head: branchName, + base: baseBranch, + draft: draft, }); - core.info(`Added labels to pull request: ${JSON.stringify(labels)}`); - } - // Set output for other jobs to use - core.setOutput("pull_request_number", pullRequest.number); - core.setOutput("pull_request_url", pullRequest.html_url); - core.setOutput("branch_name", branchName); + core.info(`Created pull request #${pullRequest.number}: ${pullRequest.html_url}`); - // Update the activation comment with PR link (if a comment was created) - await updateActivationComment(github, context, core, pullRequest.html_url, pullRequest.number); + // Add labels if specified + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullRequest.number, + labels: labels, + }); + core.info(`Added labels to pull request: ${JSON.stringify(labels)}`); + } - // Write summary to GitHub Actions summary - await core.summary - .addRaw( - ` + // Update the activation comment with PR link (if a comment was created) + await updateActivationComment(github, context, core, pullRequest.html_url, pullRequest.number); + + // Write summary to GitHub Actions summary + await core.summary + .addRaw( + ` ## Pull Request - **Pull Request**: [#${pullRequest.number}](${pullRequest.html_url}) - **Branch**: \`${branchName}\` - **Base Branch**: \`${baseBranch}\` ` - ) - .write(); - } catch (prError) { - core.warning(`Failed to create pull request: ${prError instanceof Error ? prError.message : String(prError)}`); - core.info("Falling back to creating an issue instead"); + ) + .write(); - // Create issue as fallback with enhanced body content - const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; - const branchUrl = context.payload.repository ? `${context.payload.repository.html_url}/tree/${branchName}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/tree/${branchName}`; + // Return success with PR details + return { + success: true, + pull_request_number: pullRequest.number, + pull_request_url: pullRequest.html_url, + branch_name: branchName, + temporary_id: pullRequestItem.temporary_id, // Pass through if present + }; + } catch (prError) { + core.warning(`Failed to create pull request: ${prError instanceof Error ? prError.message : String(prError)}`); + core.info("Falling back to creating an issue instead"); + + // Create issue as fallback with enhanced body content + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const branchUrl = context.payload.repository ? `${context.payload.repository.html_url}/tree/${branchName}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/tree/${branchName}`; - // Read patch content for preview - let patchPreview = ""; - if (fs.existsSync("/tmp/gh-aw/aw.patch")) { - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - patchPreview = generatePatchPreview(patchContent); - } + // Read patch content for preview + let patchPreview = ""; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + patchPreview = generatePatchPreview(patchContent); + } - const fallbackBody = `${body} + const fallbackBody = `${body} --- @@ -642,44 +685,38 @@ ${patchPreview}`; You can manually create a pull request from the branch if needed.${patchPreview}`; - try { - const { data: issue } = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: title, - body: fallbackBody, - labels: labels, - }); - - core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); - - // Update the activation comment with issue link (if a comment was created) - await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); + try { + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: fallbackBody, + labels: labels, + }); - // Set output for other jobs to use (issue instead of PR) - core.setOutput("issue_number", issue.number); - core.setOutput("issue_url", issue.html_url); - core.setOutput("branch_name", branchName); - core.setOutput("fallback_used", "true"); + core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); - // Write summary to GitHub Actions summary - await core.summary - .addRaw( - ` + // Update the activation comment with issue link (if a comment was created) + await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); -## Fallback Issue Created -- **Issue**: [#${issue.number}](${issue.html_url}) -- **Branch**: [\`${branchName}\`](${branchUrl}) -- **Base Branch**: \`${baseBranch}\` -- **Note**: Pull request creation failed, created issue as fallback -` - ) - .write(); - } catch (issueError) { - core.setFailed(`Failed to create both pull request and fallback issue. PR error: ${prError instanceof Error ? prError.message : String(prError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}`); - return; + // Return success with fallback flag + return { + success: true, + fallback_used: true, + issue_number: issue.number, + issue_url: issue.html_url, + branch_name: branchName, + }; + } catch (issueError) { + const error = `Failed to create both pull request and fallback issue. PR error: ${prError instanceof Error ? prError.message : String(prError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}`; + core.error(error); + return { + success: false, + error, + }; + } } - } -} + }; // End of handleCreatePullRequest +} // End of main module.exports = { main }; diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs deleted file mode 100644 index 9e81eb56b82..00000000000 --- a/actions/setup/js/create_pull_request.test.cjs +++ /dev/null @@ -1,738 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { readFileSync } from "fs"; -import path from "path"; -const createTestableFunction = scriptContent => { - // Match either the old pattern (await main()) or the new pattern (module.exports = { main }) - const beforeMainCall = scriptContent.match(/^([\s\S]*?)\s*(?:await main\(\);?|module\.exports = \{ main \};?)\s*$/); - if (!beforeMainCall) throw new Error("Could not extract script content before await main() or module.exports"); - let scriptBody = beforeMainCall[1]; - return ( - (scriptBody = scriptBody.replace(/\/\*\* @type \{typeof import\("fs"\)\} \*\/\s*const fs = require\("fs"\);?\s*/g, "")), - (scriptBody = scriptBody.replace(/\/\*\* @type \{typeof import\("crypto"\)\} \*\/\s*const crypto = require\("crypto"\);?\s*/g, "")), - (scriptBody = scriptBody.replace(/const \{ updateActivationComment \} = require\("\.\/update_activation_comment\.cjs"\);?\s*/g, "")), - (scriptBody = scriptBody.replace(/const \{ getTrackerID \} = require\("\.\/get_tracker_id\.cjs"\);?\s*/g, "")), - (scriptBody = scriptBody.replace(/const \{ addExpirationComment \} = require\("\.\/expiration_helpers\.cjs"\);?\s*/g, "")), - (scriptBody = scriptBody.replace(/const \{ removeDuplicateTitleFromDescription \} = require\("\.\/remove_duplicate_title\.cjs"\);?\s*/g, "")), - (scriptBody = scriptBody.replace(/const \{ getErrorMessage \} = require\("\.\/error_helpers\.cjs"\);?\s*/g, "")), - new Function( - `\n const { fs, crypto, github, core, context, process, console, updateActivationComment, getTrackerID, addExpirationComment, removeDuplicateTitleFromDescription, getErrorMessage } = arguments[0];\n \n ${scriptBody}\n \n return main;\n ` - ) - ); -}; -describe("create_pull_request.cjs", () => { - let createMainFunction, tempFilePath; - const mockPatchContent = (mockDeps, patchContent) => { - mockDeps.fs.readFileSync.mockImplementation(filepath => (filepath === mockDeps.process.env.GH_AW_AGENT_OUTPUT ? mockDeps.process.env.GH_AW_AGENT_OUTPUT : patchContent)); - }; - let mockDependencies; - (beforeEach(() => { - const scriptPath = path.join(process.cwd(), "create_pull_request.cjs"), - scriptContent = readFileSync(scriptPath, "utf8"); - ((createMainFunction = createTestableFunction(scriptContent)), - (global.exec = { exec: vi.fn().mockResolvedValue(0), getExecOutput: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }) }), - (mockDependencies = { - fs: { - existsSync: vi.fn().mockReturnValue(!0), - readFileSync: vi.fn().mockImplementation(filepath => (filepath === mockDependencies.process.env.GH_AW_AGENT_OUTPUT ? mockDependencies.process.env.GH_AW_AGENT_OUTPUT : "diff --git a/file.txt b/file.txt\n+new content")), - }, - crypto: { randomBytes: vi.fn().mockReturnValue(Buffer.from("1234567890abcdef", "hex")) }, - execSync: vi.fn(), - github: { rest: { pulls: { create: vi.fn() }, issues: { addLabels: vi.fn() } } }, - core: { - debug: vi.fn(), - info: vi.fn(), - notice: vi.fn(), - warning: vi.fn(), - error: vi.fn(), - setFailed: vi.fn(), - setOutput: vi.fn(), - exportVariable: vi.fn(), - setSecret: vi.fn(), - getInput: vi.fn(), - getBooleanInput: vi.fn(), - getMultilineInput: vi.fn(), - getState: vi.fn(), - saveState: vi.fn(), - startGroup: vi.fn(), - endGroup: vi.fn(), - group: vi.fn(), - addPath: vi.fn(), - setCommandEcho: vi.fn(), - isDebug: vi.fn().mockReturnValue(!1), - getIDToken: vi.fn(), - toPlatformPath: vi.fn(), - toPosixPath: vi.fn(), - toWin32Path: vi.fn(), - summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() }, - }, - context: { runId: 12345, repo: { owner: "testowner", repo: "testrepo" }, payload: { repository: { html_url: "https://github.com/testowner/testrepo" } } }, - process: { env: {} }, - console: { log: vi.fn() }, - updateActivationComment: vi.fn(), - getTrackerID: vi.fn(format => ""), - addExpirationComment: vi.fn(), - removeDuplicateTitleFromDescription: vi.fn((title, description) => description), - getErrorMessage: vi.fn(error => (error instanceof Error ? error.message : String(error))), - })); - }), - afterEach(() => { - (tempFilePath && require("fs").existsSync(tempFilePath) && (require("fs").unlinkSync(tempFilePath), (tempFilePath = void 0)), - tempFilePath && require("fs").existsSync(tempFilePath) && (require("fs").unlinkSync(tempFilePath), (tempFilePath = void 0)), - "undefined" != typeof global && delete global.exec); - }), - it("should throw error when GH_AW_WORKFLOW_ID is missing", async () => { - const mainFunction = createMainFunction(mockDependencies); - await expect(mainFunction()).rejects.toThrow("GH_AW_WORKFLOW_ID environment variable is required"); - }), - it("should throw error when GH_AW_BASE_BRANCH is missing", async () => { - mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"; - const mainFunction = createMainFunction(mockDependencies); - await expect(mainFunction()).rejects.toThrow("GH_AW_BASE_BRANCH environment variable is required"); - }), - it("should handle missing patch file with default warn behavior", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), mockDependencies.fs.existsSync.mockReturnValue(!1)); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.warning).toHaveBeenCalledWith("No patch file found - cannot create pull request without changes"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle empty patch with default warn behavior when patch file is empty", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), mockPatchContent(mockDependencies, " ")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.warning).toHaveBeenCalledWith("Patch file is empty - no changes to apply (noop operation)"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should create pull request successfully with valid input", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "New Feature", body: "This adds a new feature to the codebase." }] })), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return "git rev-parse HEAD" === command ? "abc123456" : ""; - })); - const mockPullRequest = { number: 123, html_url: "https://github.com/testowner/testrepo/pull/123" }; - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: mockPullRequest }); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(global.exec.exec).toHaveBeenCalledWith("git fetch origin main"), - expect(global.exec.exec).toHaveBeenCalledWith("git checkout main"), - expect(global.exec.exec).toHaveBeenCalledWith("git checkout -b test-workflow-1234567890abcdef"), - expect(global.exec.exec).toHaveBeenCalledWith("git am /tmp/gh-aw/aw.patch"), - expect(global.exec.exec).toHaveBeenCalledWith("git push origin test-workflow-1234567890abcdef"), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalledWith({ - owner: "testowner", - repo: "testrepo", - title: "New Feature", - body: expect.stringContaining("This adds a new feature to the codebase."), - head: "test-workflow-1234567890abcdef", - base: "main", - draft: !0, - }), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("pull_request_number", 123), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("pull_request_url", mockPullRequest.html_url), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("branch_name", "test-workflow-1234567890abcdef")); - }), - it("should handle labels correctly", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR with labels", body: "PR with labels" }] })), - (mockDependencies.process.env.GH_AW_PR_LABELS = "enhancement, automated, needs-review"), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 456, html_url: "https://github.com/testowner/testrepo/pull/456" } }), - mockDependencies.github.rest.issues.addLabels.mockResolvedValue({})); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.github.rest.issues.addLabels).toHaveBeenCalledWith({ owner: "testowner", repo: "testrepo", issue_number: 456, labels: ["enhancement", "automated", "needs-review"] })); - }), - it("should respect draft setting from environment", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Non-draft PR", body: "Non-draft PR" }] })), - (mockDependencies.process.env.GH_AW_PR_DRAFT = "false"), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 789, html_url: "https://github.com/testowner/testrepo/pull/789" } })); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const callArgs = mockDependencies.github.rest.pulls.create.mock.calls[0][0]; - expect(callArgs.draft).toBe(!1); - }), - it("should include run information in PR body", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR Title", body: "Test PR content with detailed body information." }] })), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 202, html_url: "https://github.com/testowner/testrepo/pull/202" } })); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const callArgs = mockDependencies.github.rest.pulls.create.mock.calls[0][0]; - (expect(callArgs.title).toBe("Test PR Title"), - expect(callArgs.body).toContain("Test PR content with detailed body information."), - expect(callArgs.body).toContain("AI generated by"), - expect(callArgs.body).toContain("https://github.com/testowner/testrepo/actions/runs/12345")); - }), - it("should apply title prefix when provided", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Simple PR title", body: "Simple PR body content" }] })), - (mockDependencies.process.env.GH_AW_PR_TITLE_PREFIX = "[BOT] "), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 987, html_url: "https://github.com/testowner/testrepo/pull/987" } })); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const callArgs = mockDependencies.github.rest.pulls.create.mock.calls[0][0]; - expect(callArgs.title).toBe("[BOT] Simple PR title"); - }), - it("should not duplicate title prefix when already present", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "[BOT] PR title already prefixed", body: "PR body content" }] })), - (mockDependencies.process.env.GH_AW_PR_TITLE_PREFIX = "[BOT] "), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 988, html_url: "https://github.com/testowner/testrepo/pull/988" } })); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const callArgs = mockDependencies.github.rest.pulls.create.mock.calls[0][0]; - expect(callArgs.title).toBe("[BOT] PR title already prefixed"); - }), - it("should fallback to creating issue when PR creation fails", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR that will fail", body: "This PR creation will fail and fallback to an issue." }] })), - (mockDependencies.process.env.GH_AW_PR_LABELS = "enhancement, automated"), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - })); - const prError = new Error("Pull request creation is disabled by organization policy"); - mockDependencies.github.rest.pulls.create.mockRejectedValue(prError); - const mockIssue = { number: 456, html_url: "https://github.com/testowner/testrepo/issues/456" }; - mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockResolvedValue({ data: mockIssue }) }; - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalledWith({ - owner: "testowner", - repo: "testrepo", - title: "PR that will fail", - body: expect.stringContaining("This PR creation will fail and fallback to an issue."), - head: "test-workflow-1234567890abcdef", - base: "main", - draft: !0, - }), - expect(mockDependencies.github.rest.issues.create).toHaveBeenCalledWith({ - owner: "testowner", - repo: "testrepo", - title: "PR that will fail", - body: expect.stringMatching( - /This PR creation will fail and fallback to an issue\.[\s\S]*---[\s\S]*Note.*originally intended as a pull request[\s\S]*Original error.*Pull request creation is disabled by organization policy[\s\S]*You can manually create a pull request from the branch if needed/ - ), - labels: ["enhancement", "automated"], - }), - expect(mockDependencies.core.warning).toHaveBeenCalledWith("Failed to create pull request: Pull request creation is disabled by organization policy"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Falling back to creating an issue instead"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Created fallback issue #456: https://github.com/testowner/testrepo/issues/456"), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("issue_number", 456), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("issue_url", mockIssue.html_url), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("branch_name", "test-workflow-1234567890abcdef"), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("fallback_used", "true"), - expect(mockDependencies.core.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("## Fallback Issue Created"))); - }), - it("should include patch preview in fallback issue when PR creation fails", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR with patch preview", body: "This PR will fail and create an issue with patch preview." }] }))); - const patchLines = ["diff --git a/file.txt b/file.txt", "--- a/file.txt", "+++ b/file.txt", "@@ -1,1 +1,1 @@"]; - for (let i = 0; i < 600; i++) patchLines.push(`+Line ${i}`); - const largePatch = patchLines.join("\n"); - mockPatchContent(mockDependencies, largePatch); - const prError = new Error("Pull request creation is disabled"); - (mockDependencies.github.rest.pulls.create.mockRejectedValue(prError), - (mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockResolvedValue({ data: { number: 789, html_url: "https://github.com/testowner/testrepo/issues/789" } }) })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.github.rest.issues.create).toHaveBeenCalled()); - const issueCreateCall = mockDependencies.github.rest.issues.create.mock.calls[0][0]; - (expect(issueCreateCall.body).toMatch(/
Show patch preview \(500 of 604 lines\)<\/summary>/), - expect(issueCreateCall.body).toMatch(/```diff/), - expect(issueCreateCall.body).toMatch(/\.\.\. \(truncated\)/), - expect(issueCreateCall.body).toContain("+Line 0"), - expect(issueCreateCall.body).not.toContain("+Line 550")); - }), - it("should truncate patch by character limit when it exceeds 2000 chars", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR with large patch", body: "This PR will fail and create an issue with char-limited patch." }] }))); - const patchLines = ["diff --git a/file.txt b/file.txt", "--- a/file.txt", "+++ b/file.txt", "@@ -1,1 +1,100 @@"]; - for (let i = 0; i < 100; i++) patchLines.push(`+This is a longer line ${i} with more content to trigger character limit truncation`); - const largePatch = patchLines.join("\n"); - mockPatchContent(mockDependencies, largePatch); - const prError = new Error("Pull request creation is disabled"); - (mockDependencies.github.rest.pulls.create.mockRejectedValue(prError), - (mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockResolvedValue({ data: { number: 790, html_url: "https://github.com/testowner/testrepo/issues/790" } }) })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.github.rest.issues.create).toHaveBeenCalled()); - const issueCreateCall = mockDependencies.github.rest.issues.create.mock.calls[0][0]; - (expect(issueCreateCall.body).toMatch(/
Show patch preview/), expect(issueCreateCall.body).toMatch(/```diff/), expect(issueCreateCall.body).toMatch(/\.\.\. \(truncated\)/)); - const patchMatch = issueCreateCall.body.match(/```diff\n([\s\S]*?)\n\.\.\. \(truncated\)/); - if (patchMatch) { - const patchInBody = patchMatch[1]; - expect(patchInBody.length).toBeLessThanOrEqual(2e3); - } - }), - it("should include full patch when under 500 lines in fallback issue", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR with small patch", body: "This PR will fail and create an issue with full patch." }] })), - mockPatchContent(mockDependencies, "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n+Small change")); - const prError = new Error("Pull request creation is disabled"); - (mockDependencies.github.rest.pulls.create.mockRejectedValue(prError), - (mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockResolvedValue({ data: { number: 790, html_url: "https://github.com/testowner/testrepo/issues/790" } }) })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.github.rest.issues.create).toHaveBeenCalled()); - const issueCreateCall = mockDependencies.github.rest.issues.create.mock.calls[0][0]; - (expect(issueCreateCall.body).toMatch(/
Show patch \(5 lines\)<\/summary>/), - expect(issueCreateCall.body).toMatch(/```diff/), - expect(issueCreateCall.body).toContain("+Small change"), - expect(issueCreateCall.body).not.toContain("... (truncated)")); - }), - it("should fail when both PR and fallback issue creation fail", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR that will fail", body: "Both PR and issue creation will fail." }] })), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - })); - const prError = new Error("Pull request creation failed"), - issueError = new Error("Issue creation also failed"); - (mockDependencies.github.rest.pulls.create.mockRejectedValue(prError), (mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockRejectedValue(issueError) })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalled(), - expect(mockDependencies.github.rest.issues.create).toHaveBeenCalled(), - expect(mockDependencies.core.setFailed).toHaveBeenCalledWith("Failed to create both pull request and fallback issue. PR error: Pull request creation failed. Issue error: Issue creation also failed")); - }), - it("should fallback to creating issue when git push fails", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Push will fail", body: "Git push will fail and fallback to an issue." }] })), - (mockDependencies.process.env.GH_AW_PR_LABELS = "automation"), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - (global.exec.exec = vi.fn().mockImplementation(async (cmd, args, options) => { - if ("string" == typeof cmd && cmd.includes("git push")) throw new Error("Permission denied (publickey)"); - return ("string" == typeof cmd && cmd.includes("git ls-remote"), 0); - }))); - const mockIssue = { number: 789, html_url: "https://github.com/testowner/testrepo/issues/789" }; - mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockResolvedValue({ data: mockIssue }) }; - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const pushCallsForTest = global.exec.exec.mock.calls.filter(call => call[0] && call[0].includes("git push")); - (expect(pushCallsForTest.length).toBeGreaterThan(0), - expect(mockDependencies.github.rest.issues.create).toHaveBeenCalledWith({ - owner: "testowner", - repo: "testrepo", - title: "Push will fail", - body: expect.stringMatching(/Git push will fail[\s\S]*\[!NOTE\][\s\S]*git push operation failed[\s\S]*gh run download[\s\S]*git am aw\.patch/), - labels: ["automation"], - }), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("issue_number", 789), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("issue_url", mockIssue.html_url), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("branch_name", "test-workflow-1234567890abcdef"), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("fallback_used", "true"), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("push_failed", "true"), - expect(mockDependencies.core.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("## Push Failure Fallback")), - expect(mockDependencies.core.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("Permission denied")), - expect(mockDependencies.core.error).toHaveBeenCalledWith(expect.stringContaining("Git push failed")), - expect(mockDependencies.core.warning).toHaveBeenCalledWith(expect.stringContaining("Git push operation failed"))); - }), - it("should include patch preview in fallback issue when git push fails", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Push will fail with patch", body: "Git push will fail and create issue with patch preview." }] }))); - const patchLines = ["diff --git a/test.js b/test.js", "--- a/test.js", "+++ b/test.js", "@@ -1,1 +1,1 @@"]; - for (let i = 0; i < 100; i++) patchLines.push(`+Test line ${i}`); - const testPatch = patchLines.join("\n"); - (mockPatchContent(mockDependencies, testPatch), - (global.exec.exec = vi.fn().mockImplementation(async (cmd, args, options) => { - if ("string" == typeof cmd && cmd.includes("git push")) throw new Error("Permission denied (publickey)"); - return ("string" == typeof cmd && cmd.includes("git ls-remote"), 0); - })), - (mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockResolvedValue({ data: { number: 890, html_url: "https://github.com/testowner/testrepo/issues/890" } }) })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.github.rest.issues.create).toHaveBeenCalled()); - const issueCreateCall = mockDependencies.github.rest.issues.create.mock.calls[0][0]; - (expect(issueCreateCall.body).toMatch(/
Show patch \(104 lines\)<\/summary>/), - expect(issueCreateCall.body).toMatch(/```diff/), - expect(issueCreateCall.body).toContain("diff --git a/test.js b/test.js"), - expect(issueCreateCall.body).toContain("+Test line 0")); - }), - it("should fail when both git push and fallback issue creation fail", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Push and issue will fail", body: "Both git push and issue creation will fail." }] })), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - }), - (global.exec.exec = vi.fn().mockImplementation(async (cmd, args, options) => { - if ("string" == typeof cmd && cmd.includes("git push")) throw new Error("Network error: Connection timeout"); - return ("string" == typeof cmd && cmd.includes("git ls-remote"), 0); - }))); - const issueError = new Error("GitHub API rate limit exceeded"); - mockDependencies.github.rest.issues = { ...mockDependencies.github.rest.issues, create: vi.fn().mockRejectedValue(issueError) }; - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const pushCallsInFailTest = global.exec.exec.mock.calls.filter(call => call[0] && call[0].includes("git push")); - (expect(pushCallsInFailTest.length).toBeGreaterThan(0), - expect(mockDependencies.github.rest.issues.create).toHaveBeenCalled(), - expect(mockDependencies.core.setFailed).toHaveBeenCalledWith(expect.stringMatching(/Failed to push and failed to create fallback issue.*Network error.*GitHub API rate limit/))); - }), - it("should handle remote branch collision by appending random suffix", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR with branch collision", body: "This will handle remote branch collision." }] })), - mockDependencies.execSync.mockImplementation(command => { - if ("git diff --cached --exit-code" === command) { - const error = new Error("Changes exist"); - throw ((error.status = 1), error); - } - return ""; - })); - let randomBytesCallCount = 0; - ((mockDependencies.crypto.randomBytes = vi.fn().mockImplementation(size => (randomBytesCallCount++, 1 === randomBytesCallCount ? Buffer.from("1234567890abcdef", "hex") : Buffer.from("fedcba09", "hex")))), - (global.exec.getExecOutput = vi - .fn() - .mockImplementation(async cmd => ("string" == typeof cmd && cmd.includes("git ls-remote") ? { exitCode: 0, stdout: "abc123 refs/heads/test-workflow-1234567890abcdef\n", stderr: "" } : { exitCode: 0, stdout: "", stderr: "" }))), - (global.exec.exec = vi.fn().mockImplementation(async (cmd, args, options) => (("string" == typeof cmd && cmd.includes("git branch -m")) || ("string" == typeof cmd && cmd.includes("git push")), 0))), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 123, html_url: "https://github.com/testowner/testrepo/pull/123" } })); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const branchRenameCalls = global.exec.exec.mock.calls.filter(call => call[0] && call[0].includes("git branch -m")); - (expect(branchRenameCalls.length).toBeGreaterThan(0), - expect(mockDependencies.core.warning).toHaveBeenCalledWith(expect.stringContaining("already exists")), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalledWith(expect.objectContaining({ head: expect.stringMatching(/test-workflow-1234567890abcdef-fedcba09/) }))); - }), - describe("if-no-changes configuration", () => { - (beforeEach(() => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "Test PR body" }] }))); - }), - it("should handle empty patch with warn (default) behavior", async () => { - (mockPatchContent(mockDependencies, ""), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "warn")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.warning).toHaveBeenCalledWith("Patch file is empty - no changes to apply (noop operation)"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle empty patch with ignore behavior", async () => { - (mockPatchContent(mockDependencies, ""), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "ignore")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.info).not.toHaveBeenCalledWith(expect.stringContaining("Patch file is empty")), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle empty patch with error behavior", async () => { - (mockPatchContent(mockDependencies, ""), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "error")); - const mainFunction = createMainFunction(mockDependencies); - (await expect(mainFunction()).rejects.toThrow("No changes to push - failing as configured by if-no-changes: error"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle missing patch file with warn behavior", async () => { - (mockDependencies.fs.existsSync.mockReturnValue(!1), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "warn")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.warning).toHaveBeenCalledWith("No patch file found - cannot create pull request without changes"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle missing patch file with ignore behavior", async () => { - (mockDependencies.fs.existsSync.mockReturnValue(!1), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "ignore")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.info).not.toHaveBeenCalledWith(expect.stringContaining("No patch file found")), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle missing patch file with error behavior", async () => { - (mockDependencies.fs.existsSync.mockReturnValue(!1), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "error")); - const mainFunction = createMainFunction(mockDependencies); - (await expect(mainFunction()).rejects.toThrow("No patch file found - cannot create pull request without changes"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should handle patch with error message with warn behavior", async () => { - (mockPatchContent(mockDependencies, "Failed to generate patch: some error"), (mockDependencies.process.env.GH_AW_PR_IF_NO_CHANGES = "warn")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.core.warning).toHaveBeenCalledWith("Patch file contains error message - cannot create pull request without changes"), - expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - }), - it("should default to warn when if-no-changes is not specified", async () => { - mockPatchContent(mockDependencies, ""); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.warning).toHaveBeenCalledWith("Patch file is empty - no changes to apply (noop operation)"), expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled()); - })); - }), - describe("staged mode functionality", () => { - (beforeEach(() => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Staged Mode Test PR", body: "This is a test PR for staged mode functionality.", branch: "feature-test" }] }))); - }), - it("should write step summary instead of creating PR when in staged mode", async () => { - mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.core.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("## 🎭 Staged Mode: Create Pull Request Preview")), - expect(mockDependencies.core.summary.write).toHaveBeenCalled(), - expect(mockDependencies.core.info).toHaveBeenCalledWith("📝 Pull request creation preview written to step summary"), - expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled(), - expect(mockDependencies.execSync).not.toHaveBeenCalled()); - }), - it("should include patch information in staged mode summary", async () => { - ((mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"), mockPatchContent(mockDependencies, "diff --git a/test.txt b/test.txt\n+added line\n-removed line")); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const summaryCall = mockDependencies.core.summary.addRaw.mock.calls[0][0]; - (expect(summaryCall).toContain("**Title:** Staged Mode Test PR"), - expect(summaryCall).toContain("**Branch:** feature-test"), - expect(summaryCall).toContain("**Base:** main"), - expect(summaryCall).toContain("**Body:**"), - expect(summaryCall).toContain("This is a test PR for staged mode functionality."), - expect(summaryCall).toContain("**Changes:** Patch file exists with"), - expect(summaryCall).toContain("Show patch preview"), - expect(summaryCall).toContain("diff --git a/test.txt")); - }), - it("should handle empty patch in staged mode", async () => { - ((mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"), mockPatchContent(mockDependencies, "")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.summary.addRaw).toHaveBeenCalled()); - const summaryCall = mockDependencies.core.summary.addRaw.mock.calls[0][0]; - (expect(summaryCall).toContain("**Changes:** No changes (empty patch)"), expect(summaryCall).not.toContain("Show patch preview")); - }), - it("should use auto-generated branch when no branch specified in staged mode", async () => { - ((mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "PR without branch", body: "Test PR body" }] }))); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const summaryCall = mockDependencies.core.summary.addRaw.mock.calls[0][0]; - expect(summaryCall).toContain("**Branch:** auto-generated"); - }), - it("should not execute git operations in staged mode", async () => { - mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.execSync).not.toHaveBeenCalledWith(expect.stringContaining("git"), expect.anything()), - expect(mockDependencies.github.rest.pulls.create).not.toHaveBeenCalled(), - expect(mockDependencies.github.rest.issues.addLabels).not.toHaveBeenCalled(), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("pull_request_number", ""), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("pull_request_url", ""), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("issue_number", ""), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("issue_url", ""), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("branch_name", ""), - expect(mockDependencies.core.setOutput).toHaveBeenCalledWith("fallback_used", "")); - }), - it("should handle missing patch file in staged mode", async () => { - ((mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"), mockDependencies.fs.existsSync.mockReturnValue(!1)); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.summary.addRaw).toHaveBeenCalled()); - const summaryCall = mockDependencies.core.summary.addRaw.mock.calls[0][0]; - (expect(summaryCall).toContain("⚠️ No patch file found"), - expect(summaryCall).toContain("No patch file found - cannot create pull request without changes"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("📝 Pull request creation preview written to step summary (no patch file)")); - }), - it("should handle patch error in staged mode", async () => { - ((mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"), mockPatchContent(mockDependencies, "Failed to generate patch: some error occurred")); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), expect(mockDependencies.core.summary.addRaw).toHaveBeenCalled()); - const summaryCall = mockDependencies.core.summary.addRaw.mock.calls[0][0]; - (expect(summaryCall).toContain("⚠️ Patch file contains error"), - expect(summaryCall).toContain("Patch file contains error message - cannot create pull request without changes"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("📝 Pull request creation preview written to step summary (patch error)")); - }), - it("should validate patch size within limit", async () => { - ((mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "This is a test PR", branch: "test-branch" }] })), - (mockDependencies.process.env.GH_AW_MAX_PATCH_SIZE = "10"), - mockDependencies.fs.existsSync.mockReturnValue(!0)); - const patchContent = "diff --git a/file.txt b/file.txt\n+new content\n".repeat(100); - (mockPatchContent(mockDependencies, patchContent), mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 123, html_url: "https://github.com/testowner/testrepo/pull/123" } })); - const main = createMainFunction(mockDependencies); - (await main(), - expect(mockDependencies.core.info).toHaveBeenCalledWith(expect.stringMatching(/Patch size: \d+ KB \(maximum allowed: 10 KB\)/)), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Patch size validation passed")); - }), - it("should fail when patch size exceeds limit", async () => { - ((mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "This is a test PR", branch: "test-branch" }] })), - (mockDependencies.process.env.GH_AW_MAX_PATCH_SIZE = "1"), - mockDependencies.fs.existsSync.mockReturnValue(!0)); - const patchContent = "diff --git a/file.txt b/file.txt\n+new content\n".repeat(100); - mockPatchContent(mockDependencies, patchContent); - const main = createMainFunction(mockDependencies); - (await expect(main()).rejects.toThrow(/Patch size \(\d+ KB\) exceeds maximum allowed size \(1 KB\)/), expect(mockDependencies.core.info).toHaveBeenCalledWith(expect.stringMatching(/Patch size: \d+ KB \(maximum allowed: 1 KB\)/))); - }), - it("should show staged preview when patch size exceeds limit in staged mode", async () => { - ((mockDependencies.process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "This is a test PR", branch: "test-branch" }] })), - (mockDependencies.process.env.GH_AW_MAX_PATCH_SIZE = "1"), - mockDependencies.fs.existsSync.mockReturnValue(!0)); - const patchContent = "diff --git a/file.txt b/file.txt\n+new content\n".repeat(100); - mockPatchContent(mockDependencies, patchContent); - const main = createMainFunction(mockDependencies); - (await main(), expect(mockDependencies.core.summary.addRaw).toHaveBeenCalled()); - const summaryCall = mockDependencies.core.summary.addRaw.mock.calls[0][0]; - (expect(summaryCall).toContain("❌ Patch size exceeded"), - expect(summaryCall).toContain("exceeds maximum allowed size"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("📝 Pull request creation preview written to step summary (patch size error)")); - }), - it("should use default 1024 KB limit when env var not set", async () => { - ((mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "This is a test PR", branch: "test-branch" }] })), - delete mockDependencies.process.env.GH_AW_MAX_PATCH_SIZE, - mockDependencies.fs.existsSync.mockReturnValue(!0), - mockPatchContent(mockDependencies, "diff --git a/file.txt b/file.txt\n+new content\n"), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 123, html_url: "https://github.com/testowner/testrepo/pull/123" } })); - const main = createMainFunction(mockDependencies); - (await main(), - expect(mockDependencies.core.info).toHaveBeenCalledWith(expect.stringMatching(/Patch size: \d+ KB \(maximum allowed: 1024 KB\)/)), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Patch size validation passed")); - }), - it("should skip patch size validation for empty patches", async () => { - ((mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "This is a test PR", branch: "test-branch" }] })), - (mockDependencies.process.env.GH_AW_MAX_PATCH_SIZE = "1"), - mockDependencies.fs.existsSync.mockReturnValue(!0), - mockPatchContent(mockDependencies, "")); - const main = createMainFunction(mockDependencies); - (await main(), expect(mockDependencies.core.info).not.toHaveBeenCalledWith(expect.stringMatching(/Patch size:/))); - })); - }), - describe("Patch failure investigation", () => { - (beforeEach(() => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "This is a test PR" }] }))); - }), - it("should investigate patch failure by logging git status and failed patch", async () => { - (mockDependencies.fs.existsSync.mockReturnValue(!0), - mockPatchContent(mockDependencies, "diff --git a/file.txt b/file.txt\n+new content"), - (global.exec.exec = vi.fn().mockImplementation(async cmd => { - if ("string" == typeof cmd && cmd.includes("git am")) throw new Error("Patch does not apply"); - return 0; - })), - (global.exec.getExecOutput = vi - .fn() - .mockImplementation(async (command, args) => - "git" === command && args && "status" === args[0] - ? Promise.resolve({ exitCode: 0, stdout: "On branch test-branch\nYour branch is up to date\n", stderr: "" }) - : "git" === command && args && "am" === args[0] && "--show-current-patch=diff" === args[1] - ? Promise.resolve({ exitCode: 0, stdout: "diff --git a/conflicting.txt b/conflicting.txt\n+conflicting line\n", stderr: "" }) - : Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }) - ))); - const mainFunction = createMainFunction(mockDependencies); - await mainFunction(); - const gitAmCalls = global.exec.exec.mock.calls.filter(call => call[0] && call[0].includes("git am")); - (expect(gitAmCalls.length).toBeGreaterThan(0), - expect(global.exec.getExecOutput).toHaveBeenCalledWith("git", ["status"]), - expect(global.exec.getExecOutput).toHaveBeenCalledWith("git", ["am", "--show-current-patch=diff"]), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Investigating patch failure..."), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Git status output:"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("On branch test-branch\nYour branch is up to date\n"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Failed patch content:"), - expect(mockDependencies.core.info).toHaveBeenCalledWith("diff --git a/conflicting.txt b/conflicting.txt\n+conflicting line\n"), - expect(mockDependencies.core.setFailed).toHaveBeenCalledWith("Failed to apply patch")); - }), - it("should handle investigation failure gracefully", async () => { - (mockDependencies.fs.existsSync.mockReturnValue(!0), - mockPatchContent(mockDependencies, "diff --git a/file.txt b/file.txt\n+new content"), - (global.exec.exec = vi.fn().mockImplementation(async cmd => { - if ("string" == typeof cmd && cmd.includes("git am")) throw new Error("Patch does not apply"); - return 0; - })), - (global.exec.getExecOutput = vi.fn().mockImplementation(async (command, args) => { - if ("git" === command) throw new Error("Git command failed"); - return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); - }))); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.core.info).toHaveBeenCalledWith("Investigating patch failure..."), - expect(mockDependencies.core.warning).toHaveBeenCalledWith("Failed to investigate patch failure: Git command failed"), - expect(mockDependencies.core.setFailed).toHaveBeenCalledWith("Failed to apply patch")); - })); - }), - describe("activation comment update", () => { - (it("should update activation comment with PR link when comment_id is provided", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_COMMENT_ID = "123456"), - (mockDependencies.process.env.GH_AW_COMMENT_REPO = "testowner/testrepo"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "Test PR body" }] })), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 42, html_url: "https://github.com/testowner/testrepo/pull/42" } })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalled(), - expect(mockDependencies.updateActivationComment).toHaveBeenCalledWith(mockDependencies.github, mockDependencies.context, mockDependencies.core, "https://github.com/testowner/testrepo/pull/42", 42)); - }), - it("should update discussion comment with PR link when comment_id starts with DC_", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_COMMENT_ID = "DC_kwDOABCDEF4ABCDEF"), - (mockDependencies.process.env.GH_AW_COMMENT_REPO = "testowner/testrepo"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "Test PR body" }] })), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 42, html_url: "https://github.com/testowner/testrepo/pull/42" } })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalled(), - expect(mockDependencies.updateActivationComment).toHaveBeenCalledWith(mockDependencies.github, mockDependencies.context, mockDependencies.core, "https://github.com/testowner/testrepo/pull/42", 42)); - }), - it("should skip updating comment when GH_AW_COMMENT_ID is not set", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "Test PR body" }] })), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 42, html_url: "https://github.com/testowner/testrepo/pull/42" } })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalled(), - expect(mockDependencies.updateActivationComment).toHaveBeenCalledWith(mockDependencies.github, mockDependencies.context, mockDependencies.core, "https://github.com/testowner/testrepo/pull/42", 42)); - }), - it("should not fail workflow if comment update fails", async () => { - ((mockDependencies.process.env.GH_AW_WORKFLOW_ID = "test-workflow"), - (mockDependencies.process.env.GH_AW_BASE_BRANCH = "main"), - (mockDependencies.process.env.GH_AW_COMMENT_ID = "123456"), - (mockDependencies.process.env.GH_AW_COMMENT_REPO = "testowner/testrepo"), - (mockDependencies.process.env.GH_AW_AGENT_OUTPUT = JSON.stringify({ items: [{ type: "create_pull_request", title: "Test PR", body: "Test PR body" }] })), - mockDependencies.github.rest.pulls.create.mockResolvedValue({ data: { number: 42, html_url: "https://github.com/testowner/testrepo/pull/42" } })); - const mainFunction = createMainFunction(mockDependencies); - (await mainFunction(), - expect(mockDependencies.github.rest.pulls.create).toHaveBeenCalled(), - expect(mockDependencies.updateActivationComment).toHaveBeenCalledWith(mockDependencies.github, mockDependencies.context, mockDependencies.core, "https://github.com/testowner/testrepo/pull/42", 42)); - })); - })); -}); diff --git a/actions/setup/js/hide_comment.cjs b/actions/setup/js/hide_comment.cjs index 1e6f192c203..30af9e37f74 100644 --- a/actions/setup/js/hide_comment.cjs +++ b/actions/setup/js/hide_comment.cjs @@ -1,9 +1,17 @@ // @ts-check /// -const { loadAgentOutput } = require("./load_agent_output.cjs"); +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + */ + const { getErrorMessage } = require("./error_helpers.cjs"); +/** + * Type constant for handler identification + */ +const HANDLER_TYPE = "hide_comment"; + /** * Hide a comment using the GraphQL API. * @param {any} github - GitHub GraphQL instance @@ -11,7 +19,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); * @param {string} reason - Reason for hiding (default: spam) * @returns {Promise<{id: string, isMinimized: boolean}>} Hidden comment details */ -async function hideComment(github, nodeId, reason = "spam") { +async function hideCommentAPI(github, nodeId, reason = "spam") { const query = /* GraphQL */ ` mutation ($nodeId: ID!, $classifier: ReportedContentClassifiers!) { minimizeComment(input: { subjectId: $nodeId, classifier: $classifier }) { @@ -30,88 +38,98 @@ async function hideComment(github, nodeId, reason = "spam") { }; } +/** + * Main handler factory for hide_comment + * Returns a message handler function that processes individual hide_comment messages + * @type {HandlerFactoryFunction} + */ async function main(config = {}) { - // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; - - // Parse allowed reasons from config object - const allowedReasons = config.allowed_reasons || null; - if (allowedReasons && allowedReasons.length > 0) { - core.info(`Allowed reasons for hiding: [${allowedReasons.join(", ")}]`); - } - - const result = loadAgentOutput(); - if (!result.success) { - return; - } + // Extract configuration + const allowedReasons = config.allowed_reasons || []; + const maxCount = config.max || 5; - // Find all hide-comment items - const hideCommentItems = result.items.filter(/** @param {any} item */ item => item.type === "hide_comment"); - if (hideCommentItems.length === 0) { - core.info("No hide-comment items found in agent output"); - return; + core.info(`Hide comment configuration: max=${maxCount}`); + if (allowedReasons.length > 0) { + core.info(`Allowed reasons: ${allowedReasons.join(", ")}`); } - core.info(`Found ${hideCommentItems.length} hide-comment item(s)`); - - // If in staged mode, emit step summary instead of hiding comments - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Hide Comments Preview\n\n"; - summaryContent += "The following comments would be hidden if staged mode was disabled:\n\n"; - - for (let i = 0; i < hideCommentItems.length; i++) { - const item = hideCommentItems[i]; - const reason = item.reason || "spam"; - summaryContent += `### Comment ${i + 1}\n`; - summaryContent += `**Node ID**: ${item.comment_id}\n`; - summaryContent += `**Action**: Would be hidden as ${reason}\n`; - summaryContent += "\n"; + // Track how many items we've processed for max limit + let processedCount = 0; + + /** + * Message handler function that processes a single hide_comment message + * @param {Object} message - The hide_comment message to process + * @param {Object} resolvedTemporaryIds - Map of temporary IDs to {repo, number} + * @returns {Promise} Result with success/error status + */ + return async function handleHideComment(message, resolvedTemporaryIds) { + // Check if we've hit the max limit + if (processedCount >= maxCount) { + core.warning(`Skipping hide_comment: max count of ${maxCount} reached`); + return { + success: false, + error: `Max count of ${maxCount} reached`, + }; } - core.summary.addRaw(summaryContent).write(); - return; - } + processedCount++; + + const item = message; - // Process each hide-comment item - for (const item of hideCommentItems) { try { const commentId = item.comment_id; if (!commentId || typeof commentId !== "string") { - throw new Error("comment_id is required and must be a string (GraphQL node ID)"); + core.warning("comment_id is required and must be a string (GraphQL node ID)"); + return { + success: false, + error: "comment_id is required and must be a string (GraphQL node ID)", + }; } - const reason = item.reason || "spam"; + const reason = item.reason || "SPAM"; // Normalize reason to uppercase for GitHub API const normalizedReason = reason.toUpperCase(); // Validate reason against allowed reasons if specified (case-insensitive) - if (allowedReasons && allowedReasons.length > 0) { + if (allowedReasons.length > 0) { const normalizedAllowedReasons = allowedReasons.map(r => r.toUpperCase()); if (!normalizedAllowedReasons.includes(normalizedReason)) { core.warning(`Reason "${reason}" is not in allowed-reasons list [${allowedReasons.join(", ")}]. Skipping comment ${commentId}.`); - continue; + return { + success: false, + error: `Reason "${reason}" is not in allowed-reasons list`, + }; } } core.info(`Hiding comment: ${commentId} (reason: ${normalizedReason})`); - const hideResult = await hideComment(github, commentId, normalizedReason); + const hideResult = await hideCommentAPI(github, commentId, normalizedReason); if (hideResult.isMinimized) { core.info(`Successfully hidden comment: ${commentId}`); - core.setOutput("comment_id", commentId); - core.setOutput("is_hidden", "true"); + return { + success: true, + comment_id: commentId, + is_hidden: true, + }; } else { - throw new Error(`Failed to hide comment: ${commentId}`); + core.error(`Failed to hide comment: ${commentId}`); + return { + success: false, + error: `Failed to hide comment: ${commentId}`, + }; } } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to hide comment: ${errorMessage}`); - core.setFailed(`Failed to hide comment: ${errorMessage}`); - return; + return { + success: false, + error: errorMessage, + }; } - } + }; } -module.exports = { main }; +module.exports = { main, HANDLER_TYPE }; diff --git a/actions/setup/js/hide_comment.test.cjs b/actions/setup/js/hide_comment.test.cjs deleted file mode 100644 index 52d21a81e5c..00000000000 --- a/actions/setup/js/hide_comment.test.cjs +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; -import fs from "fs"; -import path from "path"; -const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn(), setFailed: vi.fn(), setOutput: vi.fn(), summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() } }, - mockGithub = { rest: {}, graphql: vi.fn() }, - mockContext = { eventName: "issue_comment", runId: 12345, repo: { owner: "testowner", repo: "testrepo" }, payload: { issue: { number: 42 }, repository: { html_url: "https://github.com/testowner/testrepo" } } }; -((global.core = mockCore), - (global.github = mockGithub), - (global.context = mockContext), - describe("hide_comment", () => { - let hideCommentScript, tempFilePath; - const setAgentOutput = data => { - tempFilePath = path.join("/tmp", `test_agent_output_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); - const content = "string" == typeof data ? data : JSON.stringify(data); - (fs.writeFileSync(tempFilePath, content), (process.env.GH_AW_AGENT_OUTPUT = tempFilePath)); - }; - (beforeEach(() => { - (vi.clearAllMocks(), - delete process.env.GH_AW_SAFE_OUTPUTS_STAGED, - delete process.env.GH_AW_AGENT_OUTPUT, - delete process.env.GITHUB_SERVER_URL, - (global.context.eventName = "issue_comment"), - (global.context.payload.issue = { number: 42 })); - const scriptPath = path.join(process.cwd(), "hide_comment.cjs"); - hideCommentScript = fs.readFileSync(scriptPath, "utf8"); - }), - afterEach(() => { - tempFilePath && fs.existsSync(tempFilePath) && (fs.unlinkSync(tempFilePath), (tempFilePath = void 0)); - }), - it("should handle empty agent output", async () => { - (setAgentOutput({ items: [], errors: [] }), await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), expect(mockCore.info).toHaveBeenCalledWith("No hide-comment items found in agent output")); - }), - it("should handle missing agent output", async () => { - (await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), expect(mockCore.info).toHaveBeenCalledWith("No GH_AW_AGENT_OUTPUT environment variable found")); - }), - it("should hide a comment successfully", async () => { - const commentNodeId = "IC_kwDOABCD123456"; - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: commentNodeId }], errors: [] }), - mockGithub.graphql.mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !0 } } }), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockCore.info).toHaveBeenCalledWith("Found 1 hide-comment item(s)"), - expect(mockCore.info).toHaveBeenCalledWith(`Hiding comment: ${commentNodeId} (reason: SPAM)`), - expect(mockCore.info).toHaveBeenCalledWith(`Successfully hidden comment: ${commentNodeId}`), - expect(mockGithub.graphql).toHaveBeenCalledWith(expect.stringContaining("minimizeComment"), expect.objectContaining({ nodeId: commentNodeId })), - expect(mockCore.setOutput).toHaveBeenCalledWith("comment_id", commentNodeId), - expect(mockCore.setOutput).toHaveBeenCalledWith("is_hidden", "true")); - }), - it("should handle GraphQL errors", async () => { - const commentNodeId = "IC_kwDOABCD123456"; - setAgentOutput({ items: [{ type: "hide_comment", comment_id: commentNodeId }], errors: [] }); - const errorMessage = "Comment not found"; - (mockGithub.graphql.mockRejectedValueOnce(new Error(errorMessage)), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining(errorMessage)), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(errorMessage))); - }), - it("should preview hiding in staged mode", async () => { - process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; - const commentNodeId = "IC_kwDOABCD123456"; - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: commentNodeId }], errors: [] }), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockCore.info).toHaveBeenCalledWith("Found 1 hide-comment item(s)"), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("Staged Mode: Hide Comments Preview")), - expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining(commentNodeId)), - expect(mockCore.summary.write).toHaveBeenCalled(), - expect(mockGithub.graphql).not.toHaveBeenCalled()); - }), - it("should handle multiple hide-comment items", async () => { - const commentNodeId1 = "IC_kwDOABCD111111", - commentNodeId2 = "IC_kwDOABCD222222"; - (setAgentOutput({ - items: [ - { type: "hide_comment", comment_id: commentNodeId1 }, - { type: "hide_comment", comment_id: commentNodeId2 }, - ], - errors: [], - }), - mockGithub.graphql.mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !0 } } }).mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !0 } } }), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockCore.info).toHaveBeenCalledWith("Found 2 hide-comment item(s)"), - expect(mockGithub.graphql).toHaveBeenCalledTimes(2), - expect(mockCore.info).toHaveBeenCalledWith(`Successfully hidden comment: ${commentNodeId1}`), - expect(mockCore.info).toHaveBeenCalledWith(`Successfully hidden comment: ${commentNodeId2}`)); - }), - it("should fail when comment_id is missing", async () => { - (setAgentOutput({ items: [{ type: "hide_comment" }], errors: [] }), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("comment_id is required")), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("comment_id is required"))); - }), - it("should fail when hiding returns false", async () => { - const commentNodeId = "IC_kwDOABCD123456"; - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: commentNodeId }], errors: [] }), - mockGithub.graphql.mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !1 } } }), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to hide comment")), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to hide comment"))); - }), - it("should respect allowed-reasons when hiding comments", async () => { - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: "IC_kwDOABCD123456", reason: "SPAM" }] }), - mockGithub.graphql.mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !0 } } }), - await eval(`(async () => { ${hideCommentScript}; await main({ allowed_reasons: ["SPAM", "ABUSE"] }); })()`), - expect(mockGithub.graphql).toHaveBeenCalledWith(expect.stringContaining("minimizeComment"), expect.objectContaining({ nodeId: "IC_kwDOABCD123456", classifier: "SPAM" })), - expect(mockCore.info).toHaveBeenCalledWith("Allowed reasons for hiding: [SPAM, ABUSE]"), - expect(mockCore.info).toHaveBeenCalledWith("Successfully hidden comment: IC_kwDOABCD123456")); - }), - it("should skip hiding when reason is not in allowed-reasons", async () => { - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: "IC_kwDOABCD123456", reason: "OUTDATED" }] }), - await eval(`(async () => { ${hideCommentScript}; await main({ allowed_reasons: ["SPAM"] }); })()`), - expect(mockGithub.graphql).not.toHaveBeenCalled(), - expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Reason "OUTDATED" is not in allowed-reasons list'))); - }), - it("should allow all reasons when allowed-reasons is not specified", async () => { - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: "IC_kwDOABCD123456", reason: "RESOLVED" }] }), - mockGithub.graphql.mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !0 } } }), - await eval(`(async () => { ${hideCommentScript}; await main({}); })()`), - expect(mockGithub.graphql).toHaveBeenCalledWith(expect.stringContaining("minimizeComment"), expect.objectContaining({ nodeId: "IC_kwDOABCD123456", classifier: "RESOLVED" })), - expect(mockCore.info).toHaveBeenCalledWith("Successfully hidden comment: IC_kwDOABCD123456")); - }), - it("should support lowercase reasons and normalize to uppercase", async () => { - (setAgentOutput({ items: [{ type: "hide_comment", comment_id: "IC_kwDOABCD123456", reason: "spam" }] }), - mockGithub.graphql.mockResolvedValueOnce({ minimizeComment: { minimizedComment: { isMinimized: !0 } } }), - await eval(`(async () => { ${hideCommentScript}; await main({ allowed_reasons: ["spam", "abuse"] }); })()`), - expect(mockGithub.graphql).toHaveBeenCalledWith(expect.stringContaining("minimizeComment"), expect.objectContaining({ nodeId: "IC_kwDOABCD123456", classifier: "SPAM" })), - expect(mockCore.info).toHaveBeenCalledWith("Successfully hidden comment: IC_kwDOABCD123456")); - })); - })); diff --git a/actions/setup/js/package-lock.json b/actions/setup/js/package-lock.json index e4090cb2b2f..8f3f92a3435 100644 --- a/actions/setup/js/package-lock.json +++ b/actions/setup/js/package-lock.json @@ -11,7 +11,7 @@ "@actions/github-script": "github:actions/github-script", "@actions/glob": "^0.5.0", "@actions/io": "^2.0.0", - "@types/node": "^20.17.10", + "@types/node": "^20.19.27", "@vitest/coverage-v8": "^4.0.10", "@vitest/ui": "^4.0.10", "prettier": "^3.6.2", diff --git a/actions/setup/js/package.json b/actions/setup/js/package.json index 9e98fb3b603..9a1580f044d 100644 --- a/actions/setup/js/package.json +++ b/actions/setup/js/package.json @@ -6,7 +6,7 @@ "@actions/github-script": "github:actions/github-script", "@actions/glob": "^0.5.0", "@actions/io": "^2.0.0", - "@types/node": "^20.17.10", + "@types/node": "^20.19.27", "@vitest/coverage-v8": "^4.0.10", "@vitest/ui": "^4.0.10", "prettier": "^3.6.2", diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 16f50bb11e6..70e909cfa82 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -6,421 +6,386 @@ const fs = require("fs"); const { generateStagedPreview } = require("./staged_preview.cjs"); const { updateActivationCommentWithCommit } = require("./update_activation_comment.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { replaceTemporaryIdReferences } = require("./temporary_id.cjs"); + +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + */ + +/** @type {string} Safe output type handled by this module */ +const HANDLER_TYPE = "push_to_pull_request_branch"; + +/** + * Main handler factory for push_to_pull_request_branch + * Returns a message handler function that processes individual push_to_pull_request_branch messages + * @type {HandlerFactoryFunction} + */ +async function main(config = {}) { + // Extract configuration from config parameter + const target = config.target || "triggering"; + const titlePrefix = config.title_prefix || ""; + const envLabels = config.labels ? (Array.isArray(config.labels) ? config.labels : config.labels.split(",")).map(label => String(label).trim()).filter(label => label) : []; + const ifNoChanges = config.if_no_changes || "warn"; + const commitTitleSuffix = config.commit_title_suffix || ""; + const maxSizeKb = config.max_patch_size ? parseInt(String(config.max_patch_size), 10) : 1024; + const baseBranch = config.base_branch || ""; + const maxCount = config.max || 0; // 0 means no limit -async function main() { // Check if we're in staged mode const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; - // Environment validation - fail early if required variables are missing - const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT || ""; - if (agentOutputFile.trim() === "") { - core.info("Agent output content is empty"); - return; + core.info(`Target: ${target}`); + if (baseBranch) { + core.info(`Base branch: ${baseBranch}`); } - - // Read agent output from file - let outputContent; - try { - outputContent = fs.readFileSync(agentOutputFile, "utf8"); - } catch (error) { - core.setFailed(`Error reading agent output file: ${getErrorMessage(error)}`); - return; + if (titlePrefix) { + core.info(`Title prefix: ${titlePrefix}`); } - - if (outputContent.trim() === "") { - core.info("Agent output content is empty"); - return; + if (envLabels.length > 0) { + core.info(`Required labels: ${envLabels.join(", ")}`); } - - const target = process.env.GH_AW_PUSH_TARGET || "triggering"; - const ifNoChanges = process.env.GH_AW_PUSH_IF_NO_CHANGES || "warn"; - - // Check if patch file exists and has valid content - if (!fs.existsSync("/tmp/gh-aw/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: - core.info(message); - return; - } + core.info(`If no changes: ${ifNoChanges}`); + if (commitTitleSuffix) { + core.info(`Commit title suffix: ${commitTitleSuffix}`); } + core.info(`Max patch size: ${maxSizeKb} KB`); + core.info(`Max count: ${maxCount || "unlimited"}`); + + // Track how many items we've processed for max limit + let processedCount = 0; + + /** + * Message handler function - processes individual push_to_pull_request_branch messages + * @param {any} message - The push_to_pull_request_branch message to process + * @param {import('./types/handler-factory').ResolvedTemporaryIds} resolvedTemporaryIds - Map of temporary IDs to resolved IDs + * @returns {Promise} + */ + return async function handlePushToPullRequestBranch(message, resolvedTemporaryIds) { + // Check max count + if (maxCount > 0 && processedCount >= maxCount) { + core.info(`Skipping message - max count (${maxCount}) reached`); + return { success: false, error: `Max count (${maxCount}) reached`, skipped: true }; + } - const patchContent = fs.readFileSync("/tmp/gh-aw/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"; - - // Log diagnostic information to help with troubleshooting - core.error("Patch file generation failed - this is an error condition that requires investigation"); - core.error(`Patch file location: /tmp/gh-aw/aw.patch`); - core.error(`Patch file size: ${Buffer.byteLength(patchContent, "utf8")} bytes`); + processedCount++; + + // Check if patch file exists and has valid content + if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { + const msg = "No patch file found - cannot push without changes"; + + switch (ifNoChanges) { + case "error": + return { success: false, error: msg }; + case "ignore": + return { success: false, error: msg, skipped: true }; + case "warn": + default: + core.info(msg); + return { success: false, error: msg, skipped: true }; + } + } - // Show first 500 characters of patch content for diagnostics - const previewLength = Math.min(500, patchContent.length); - core.error(`Patch file preview (first ${previewLength} characters):`); - core.error(patchContent.substring(0, previewLength)); + const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + + // Check for actual error conditions + if (patchContent.includes("Failed to generate patch")) { + const msg = "Patch file contains error message - cannot push without changes"; + core.error("Patch file generation failed"); + core.error(`Patch file location: /tmp/gh-aw/aw.patch`); + core.error(`Patch file size: ${Buffer.byteLength(patchContent, "utf8")} bytes`); + const previewLength = Math.min(500, patchContent.length); + core.error(`Patch file preview (first ${previewLength} characters):`); + core.error(patchContent.substring(0, previewLength)); + return { success: false, error: msg }; + } - // This is always a failure regardless of if-no-changes configuration - // because the patch file contains an error message from the patch generation process - core.setFailed(message); - return; - } + // Validate patch size (unless empty) + const isEmpty = !patchContent || !patchContent.trim(); + if (!isEmpty) { + const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); + const patchSizeKb = Math.ceil(patchSizeBytes / 1024); - // Validate patch size (unless empty) - const isEmpty = !patchContent || !patchContent.trim(); - if (!isEmpty) { - // Get maximum patch size from environment (default: 1MB = 1024 KB) - const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); - const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); - const patchSizeKb = Math.ceil(patchSizeBytes / 1024); + core.info(`Patch size: ${patchSizeKb} KB (maximum allowed: ${maxSizeKb} KB)`); - core.info(`Patch size: ${patchSizeKb} KB (maximum allowed: ${maxSizeKb} KB)`); + if (patchSizeKb > maxSizeKb) { + const msg = `Patch size (${patchSizeKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`; + return { success: false, error: msg }; + } - if (patchSizeKb > maxSizeKb) { - const message = `Patch size (${patchSizeKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`; - core.setFailed(message); - return; + core.info("Patch size validation passed"); } - core.info("Patch size validation passed"); - } - 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: - core.info(message); - break; + if (isEmpty) { + const msg = "Patch file is empty - no changes to apply (noop operation)"; + + switch (ifNoChanges) { + case "error": + return { success: false, error: "No changes to push - failing as configured by if-no-changes: error" }; + case "ignore": + return { success: false, error: msg, skipped: true }; + case "warn": + default: + core.info(msg); + return { success: false, error: msg, skipped: true }; + } } - } - core.info(`Agent output content length: ${outputContent.length}`); - if (!isEmpty) { core.info("Patch content validation passed"); - } - core.info(`Target configuration: ${target}`); - - // Parse the validated output JSON - let validatedOutput; - try { - validatedOutput = JSON.parse(outputContent); - } catch (error) { - core.setFailed(`Error parsing agent output JSON: ${getErrorMessage(error)}`); - return; - } - - if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { - core.info("No valid items found in agent output"); - return; - } - - // Find the push-to-pull-request-branch item - const pushItem = validatedOutput.items.find(/** @param {any} item */ item => item.type === "push_to_pull_request_branch"); - if (!pushItem) { - core.info("No push-to-pull-request-branch item found in agent output"); - return; - } + core.info(`Target configuration: ${target}`); + + // If in staged mode, emit preview + if (isStaged) { + await generateStagedPreview({ + title: "Push to PR Branch", + description: "The following changes would be pushed if staged mode was disabled:", + items: [{ target, commit_message: message.commit_message }], + renderItem: item => { + let content = `**Target:** ${item.target}\n\n`; + + if (item.commit_message) { + content += `**Commit Message:** ${item.commit_message}\n\n`; + } - core.info("Found push-to-pull-request-branch item"); + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchStats = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + if (patchStats.trim()) { + content += `**Changes:** Patch file exists with ${patchStats.split("\n").length} lines\n\n`; + content += `
Show patch preview\n\n\`\`\`diff\n${patchStats.slice(0, 2000)}${patchStats.length > 2000 ? "\n... (truncated)" : ""}\n\`\`\`\n\n
\n\n`; + } else { + content += `**Changes:** No changes (empty patch)\n\n`; + } + } + return content; + }, + }); + return { success: true, staged: true }; + } - // If in staged mode, emit step summary instead of pushing changes - if (isStaged) { - await generateStagedPreview({ - title: "Push to PR Branch", - description: "The following changes would be pushed if staged mode was disabled:", - items: [{ target, commit_message: pushItem.commit_message }], - renderItem: item => { - let content = ""; - content += `**Target:** ${item.target}\n\n`; + // Validate target configuration + if (target !== "*" && target !== "triggering") { + const pullNumber = parseInt(target, 10); + if (isNaN(pullNumber)) { + return { success: false, error: 'Invalid target configuration: must be "triggering", "*", or a valid pull request number' }; + } + } - if (item.commit_message) { - content += `**Commit Message:** ${item.commit_message}\n\n`; - } + // Compute the target branch name based on target configuration + let pullNumber; + if (target === "triggering") { + pullNumber = context.payload?.pull_request?.number || context.payload?.issue?.number; - if (fs.existsSync("/tmp/gh-aw/aw.patch")) { - const patchStats = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - if (patchStats.trim()) { - content += `**Changes:** Patch file exists with ${patchStats.split("\n").length} lines\n\n`; - content += `
Show patch preview\n\n\`\`\`diff\n${patchStats.slice(0, 2000)}${patchStats.length > 2000 ? "\n... (truncated)" : ""}\n\`\`\`\n\n
\n\n`; - } else { - content += `**Changes:** No changes (empty patch)\n\n`; - } - } - return content; - }, - }); - return; - } - - // 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; + if (!pullNumber) { + return { success: false, error: 'push-to-pull-request-branch with target "triggering" requires pull request context' }; + } + } else if (target === "*") { + if (message.pull_number) { + pullNumber = parseInt(message.pull_number, 10); + } + } else { + pullNumber = parseInt(target, 10); } - } - // 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 || context.payload?.issue?.number; + let branchName; + let prTitle = ""; + let prLabels = []; - // Check if we're in a pull request context when required if (!pullNumber) { - core.setFailed('push-to-pull-request-branch with target "triggering" requires pull request context'); - return; + return { success: false, error: "Pull request number is required but not found" }; } - } 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; - let prTitle = ""; - let prLabels = []; - - // Validate pull number is defined before fetching - if (!pullNumber) { - core.setFailed("Pull request number is required but not found"); - return; - } - // Fetch the specific PR to get its head branch, title, and labels - try { - const { data: pullRequest } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pullNumber, - }); - branchName = pullRequest.head.ref; - prTitle = pullRequest.title || ""; - prLabels = pullRequest.labels.map(label => label.name); - } catch (error) { - core.info(`Warning: Could not fetch PR ${pullNumber} details: ${getErrorMessage(error)}`); - // Exit with failure if we cannot determine the branch name - core.setFailed(`Failed to determine branch name for PR ${pullNumber}`); - return; - } - - core.info(`Target branch: ${branchName}`); - core.info(`PR title: ${prTitle}`); - core.info(`PR labels: ${prLabels.join(", ")}`); + // Fetch the specific PR to get its head branch, title, and labels + try { + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pullNumber, + }); + branchName = pullRequest.head.ref; + prTitle = pullRequest.title || ""; + prLabels = pullRequest.labels.map(label => label.name); + } catch (error) { + core.info(`Warning: Could not fetch PR ${pullNumber} details: ${getErrorMessage(error)}`); + return { success: false, error: `Failed to determine branch name for PR ${pullNumber}` }; + } - // Validate title prefix if specified - const titlePrefix = process.env.GH_AW_PR_TITLE_PREFIX; - if (titlePrefix && !prTitle.startsWith(titlePrefix)) { - core.setFailed(`Pull request title "${prTitle}" does not start with required prefix "${titlePrefix}"`); - return; - } + core.info(`Target branch: ${branchName}`); + core.info(`PR title: ${prTitle}`); + core.info(`PR labels: ${prLabels.join(", ")}`); - // Validate labels if specified - const requiredLabelsStr = process.env.GH_AW_PR_LABELS; - if (requiredLabelsStr) { - const requiredLabels = requiredLabelsStr.split(",").map(label => label.trim()); - const missingLabels = requiredLabels.filter(label => !prLabels.includes(label)); - if (missingLabels.length > 0) { - core.setFailed(`Pull request is missing required labels: ${missingLabels.join(", ")}. Current labels: ${prLabels.join(", ")}`); - return; + // Validate title prefix if specified + if (titlePrefix && !prTitle.startsWith(titlePrefix)) { + return { success: false, error: `Pull request title "${prTitle}" does not start with required prefix "${titlePrefix}"` }; } - } - - if (titlePrefix) { - core.info(`✓ Title prefix validation passed: "${titlePrefix}"`); - } - if (requiredLabelsStr) { - core.info(`✓ Labels validation passed: ${requiredLabelsStr}`); - } - // Check if patch has actual changes (not just empty) - const hasChanges = !isEmpty; + // Validate labels if specified + if (envLabels.length > 0) { + const missingLabels = envLabels.filter(label => !prLabels.includes(label)); + if (missingLabels.length > 0) { + return { success: false, error: `Pull request is missing required labels: ${missingLabels.join(", ")}. Current labels: ${prLabels.join(", ")}` }; + } + } - // Switch to or create the target branch - core.info(`Switching to branch: ${branchName}`); + if (titlePrefix) { + core.info(`✓ Title prefix validation passed: "${titlePrefix}"`); + } + if (envLabels.length > 0) { + core.info(`✓ Labels validation passed: ${envLabels.join(", ")}`); + } - // Fetch the specific target branch from origin (since we use shallow checkout) - try { - core.info(`Fetching branch: ${branchName}`); - await exec.exec(`git fetch origin ${branchName}:refs/remotes/origin/${branchName}`); - } catch (fetchError) { - core.setFailed(`Failed to fetch branch ${branchName}: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`); - return; - } + const hasChanges = !isEmpty; - // Check if branch exists on origin - try { - await exec.exec(`git rev-parse --verify origin/${branchName}`); - } catch (verifyError) { - core.setFailed(`Branch ${branchName} does not exist on origin, can't push to it: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`); - return; - } + // Switch to or create the target branch + core.info(`Switching to branch: ${branchName}`); - // Checkout the branch from origin - try { - await exec.exec(`git checkout -B ${branchName} origin/${branchName}`); - core.info(`Checked out existing branch from origin: ${branchName}`); - } catch (checkoutError) { - core.setFailed(`Failed to checkout branch ${branchName}: ${checkoutError instanceof Error ? checkoutError.message : String(checkoutError)}`); - return; - } + // Fetch the specific target branch from origin + try { + core.info(`Fetching branch: ${branchName}`); + await exec.exec(`git fetch origin ${branchName}:refs/remotes/origin/${branchName}`); + } catch (fetchError) { + return { success: false, error: `Failed to fetch branch ${branchName}: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}` }; + } - // Apply the patch using git CLI (skip if empty) - if (!isEmpty) { - core.info("Applying patch..."); + // Check if branch exists on origin try { - // Check if commit title suffix is configured - const commitTitleSuffix = process.env.GH_AW_COMMIT_TITLE_SUFFIX; + await exec.exec(`git rev-parse --verify origin/${branchName}`); + } catch (verifyError) { + return { success: false, error: `Branch ${branchName} does not exist on origin, can't push to it: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}` }; + } - if (commitTitleSuffix) { - core.info(`Appending commit title suffix: "${commitTitleSuffix}"`); + // Checkout the branch from origin + try { + await exec.exec(`git checkout -B ${branchName} origin/${branchName}`); + core.info(`Checked out existing branch from origin: ${branchName}`); + } catch (checkoutError) { + return { success: false, error: `Failed to checkout branch ${branchName}: ${checkoutError instanceof Error ? checkoutError.message : String(checkoutError)}` }; + } - // Read the patch file - let patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + // Apply the patch using git CLI (skip if empty) + if (hasChanges) { + core.info("Applying patch..."); + try { + if (commitTitleSuffix) { + core.info(`Appending commit title suffix: "${commitTitleSuffix}"`); - // Modify Subject lines in the patch to append the suffix - // Patch format has "Subject: [PATCH] " or "Subject: " - // Append the suffix at the end of the title to avoid git am stripping brackets - patchContent = patchContent.replace(/^Subject: (?:\[PATCH\] )?(.*)$/gm, (match, title) => `Subject: [PATCH] ${title}${commitTitleSuffix}`); + // Read the patch file + let patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - // Write the modified patch back - fs.writeFileSync("/tmp/gh-aw/aw.patch", patchContent, "utf8"); - core.info(`Patch modified with commit title suffix: "${commitTitleSuffix}"`); - } + // Modify Subject lines in the patch to append the suffix + patchContent = patchContent.replace(/^Subject: (?:\[PATCH\] )?(.*)$/gm, (match, title) => `Subject: [PATCH] ${title}${commitTitleSuffix}`); - // Log first 100 lines of patch for debugging - const finalPatchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); - const patchLines = finalPatchContent.split("\n"); - const previewLineCount = Math.min(100, patchLines.length); - core.info(`Patch preview (first ${previewLineCount} of ${patchLines.length} lines):`); - for (let i = 0; i < previewLineCount; i++) { - core.info(patchLines[i]); - } + // Write the modified patch back + fs.writeFileSync("/tmp/gh-aw/aw.patch", patchContent, "utf8"); + core.info(`Patch modified with commit title suffix: "${commitTitleSuffix}"`); + } - // Patches are created with git format-patch, so use git am to apply them - await exec.exec("git am /tmp/gh-aw/aw.patch"); - core.info("Patch applied successfully"); + // Log first 100 lines of patch for debugging + const finalPatchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + const patchLines = finalPatchContent.split("\n"); + const previewLineCount = Math.min(100, patchLines.length); + core.info(`Patch preview (first ${previewLineCount} of ${patchLines.length} lines):`); + for (let i = 0; i < previewLineCount; i++) { + core.info(patchLines[i]); + } - // Push the applied commits to the branch - await exec.exec(`git push origin ${branchName}`); - core.info(`Changes committed and pushed to branch: ${branchName}`); - } catch (error) { - core.error(`Failed to apply patch: ${getErrorMessage(error)}`); + // Apply patch + await exec.exec("git am /tmp/gh-aw/aw.patch"); + core.info("Patch applied successfully"); + + // Push the applied commits to the branch + await exec.exec(`git push origin ${branchName}`); + core.info(`Changes committed and pushed to branch: ${branchName}`); + } catch (error) { + core.error(`Failed to apply patch: ${getErrorMessage(error)}`); + + // Investigate patch failure + try { + core.info("Investigating patch failure..."); + + const statusResult = await exec.getExecOutput("git", ["status"]); + core.info("Git status output:"); + core.info(statusResult.stdout); + + const logResult = await exec.getExecOutput("git", ["log", "--oneline", "-5"]); + core.info("Recent commits (last 5):"); + core.info(logResult.stdout); + + const diffResult = await exec.getExecOutput("git", ["diff", "HEAD"]); + core.info("Uncommitted changes:"); + core.info(diffResult.stdout && diffResult.stdout.trim() ? diffResult.stdout : "(no uncommitted changes)"); + + const patchDiffResult = await exec.getExecOutput("git", ["am", "--show-current-patch=diff"]); + core.info("Failed patch diff:"); + core.info(patchDiffResult.stdout); + + const patchFullResult = await exec.getExecOutput("git", ["am", "--show-current-patch"]); + core.info("Failed patch (full):"); + core.info(patchFullResult.stdout); + } catch (investigateError) { + core.warning(`Failed to investigate patch failure: ${investigateError instanceof Error ? investigateError.message : String(investigateError)}`); + } - // Investigate why the patch failed by logging git status and the failed patch - try { - core.info("Investigating patch failure..."); - - // Log git status to see the current state - const statusResult = await exec.getExecOutput("git", ["status"]); - core.info("Git status output:"); - core.info(statusResult.stdout); - - // Log recent commits for context - const logResult = await exec.getExecOutput("git", ["log", "--oneline", "-5"]); - core.info("Recent commits (last 5):"); - core.info(logResult.stdout); - - // Log uncommitted changes - const diffResult = await exec.getExecOutput("git", ["diff", "HEAD"]); - core.info("Uncommitted changes:"); - core.info(diffResult.stdout && diffResult.stdout.trim() ? diffResult.stdout : "(no uncommitted changes)"); - - // Log the failed patch diff - const patchDiffResult = await exec.getExecOutput("git", ["am", "--show-current-patch=diff"]); - core.info("Failed patch diff:"); - core.info(patchDiffResult.stdout); - - // Log the full failed patch for complete context - const patchFullResult = await exec.getExecOutput("git", ["am", "--show-current-patch"]); - core.info("Failed patch (full):"); - core.info(patchFullResult.stdout); - } catch (investigateError) { - core.warning(`Failed to investigate patch failure: ${investigateError instanceof Error ? investigateError.message : String(investigateError)}`); + return { success: false, error: "Failed to apply patch" }; + } + } else { + core.info("Skipping patch application (empty patch)"); + + const msg = "No changes to apply - noop operation completed successfully"; + + switch (ifNoChanges) { + case "error": + return { success: false, error: "No changes to apply - failing as configured by if-no-changes: error" }; + case "ignore": + // Silent success + break; + case "warn": + default: + core.info(msg); + break; } - - core.setFailed("Failed to apply patch"); - return; } - } else { - core.info("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: - core.info(message); - break; + + // Get commit SHA and push URL + const commitShaRes = await exec.getExecOutput("git", ["rev-parse", "HEAD"]); + if (commitShaRes.exitCode !== 0) { + return { success: false, error: "Failed to get commit SHA" }; } - } + const commitSha = commitShaRes.stdout.trim(); - // Get commit SHA and push URL - const commitShaRes = await exec.getExecOutput("git", ["rev-parse", "HEAD"]); - if (commitShaRes.exitCode !== 0) throw new Error("Failed to get commit SHA"); - const commitSha = commitShaRes.stdout.trim(); - - // Get repository base URL and construct URLs - const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; - const repoUrl = context.payload.repository ? context.payload.repository.html_url : `${githubServer}/${context.repo.owner}/${context.repo.repo}`; - const pushUrl = `${repoUrl}/tree/${branchName}`; - const commitUrl = `${repoUrl}/commit/${commitSha}`; - - // Set outputs - core.setOutput("branch_name", branchName); - core.setOutput("commit_sha", commitSha); - core.setOutput("push_url", pushUrl); - core.setOutput("commit_url", commitUrl); - - // Update the activation comment with commit link (if a comment was created and changes were pushed) - if (hasChanges) { - await updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl); - } + // Get repository base URL and construct URLs + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const repoUrl = context.payload.repository ? context.payload.repository.html_url : `${githubServer}/${context.repo.owner}/${context.repo.repo}`; + const pushUrl = `${repoUrl}/tree/${branchName}`; + const commitUrl = `${repoUrl}/commit/${commitSha}`; + + // Update the activation comment with commit link (if a comment was created and changes were pushed) + if (hasChanges) { + await updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl); + } - // Write summary to GitHub Actions summary - const summaryTitle = hasChanges ? "Push to Branch" : "Push to Branch (No Changes)"; - const summaryContent = hasChanges - ? ` + // 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)}](${commitUrl}) - **URL**: [${pushUrl}](${pushUrl}) ` - : ` + : ` ## ${summaryTitle} - **Branch**: \`${branchName}\` - **Status**: No changes to apply (noop operation) - **URL**: [${pushUrl}](${pushUrl}) `; - await core.summary.addRaw(summaryContent).write(); + await core.summary.addRaw(summaryContent).write(); + + return { + success: true, + branch_name: branchName, + commit_url: commitUrl, + }; + }; } -module.exports = { main }; +module.exports = { main, HANDLER_TYPE }; diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs deleted file mode 100644 index 347857c255f..00000000000 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ /dev/null @@ -1,475 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import fs from "fs"; -import path from "path"; -const mockCore = { - debug: vi.fn(), - info: vi.fn(), - notice: vi.fn(), - warning: vi.fn(), - error: vi.fn(), - setFailed: vi.fn(), - setOutput: vi.fn(), - exportVariable: vi.fn(), - setSecret: vi.fn(), - getInput: vi.fn(), - getBooleanInput: vi.fn(), - getMultilineInput: vi.fn(), - getState: vi.fn(), - saveState: vi.fn(), - startGroup: vi.fn(), - endGroup: vi.fn(), - group: vi.fn(), - addPath: vi.fn(), - setCommandEcho: vi.fn(), - isDebug: vi.fn().mockReturnValue(!1), - getIDToken: vi.fn(), - toPlatformPath: vi.fn(), - toPosixPath: vi.fn(), - toWin32Path: vi.fn(), - summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() }, - }, - mockContext = { eventName: "pull_request", payload: { pull_request: { number: 123 }, repository: { html_url: "https://github.com/testowner/testrepo" } }, repo: { owner: "testowner", repo: "testrepo" } }, - mockGithub = { - graphql: vi.fn(), - request: vi.fn(), - rest: { - pulls: { - get: vi.fn().mockResolvedValue({ - data: { - head: { ref: "feature-branch" }, - title: "Test PR Title", - labels: [{ name: "bug" }, { name: "enhancement" }], - }, - }), - }, - }, - }; -((global.core = mockCore), - (global.context = mockContext), - (global.github = mockGithub), - describe("push_to_pull_request_branch.cjs", () => { - let pushToPrBranchScript, mockFs, mockExec, tempFilePath; - const setAgentOutput = data => { - tempFilePath = path.join("/tmp", `test_agent_output_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); - const content = "string" == typeof data ? data : JSON.stringify(data); - (fs.writeFileSync(tempFilePath, content), (process.env.GH_AW_AGENT_OUTPUT = tempFilePath)); - }, - mockPatchContent = patchContent => { - mockFs.readFileSync.mockImplementation((filepath, encoding) => { - const agentOutputPath = process.env.GH_AW_AGENT_OUTPUT; - return agentOutputPath && filepath === agentOutputPath ? fs.readFileSync(filepath, encoding || "utf8") : patchContent; - }); - }, - executeScript = async () => ( - (global.core = mockCore), - (global.context = mockContext), - (global.github = mockGithub), - (global.mockFs = mockFs), - (global.exec = mockExec), - await eval(`(async () => { ${pushToPrBranchScript}; await main(); })()`) - ); - (beforeEach(() => { - (vi.clearAllMocks(), - delete process.env.GH_AW_PUSH_TARGET, - delete process.env.GH_AW_AGENT_OUTPUT, - delete process.env.GH_AW_PUSH_IF_NO_CHANGES, - delete process.env.GH_AW_PR_TITLE_PREFIX, - delete process.env.GH_AW_PR_LABELS, - (mockFs = { - existsSync: vi.fn(), - readFileSync: vi.fn().mockImplementation((filepath, encoding) => { - const agentOutputPath = process.env.GH_AW_AGENT_OUTPUT; - return agentOutputPath && filepath === agentOutputPath ? fs.readFileSync(filepath, encoding || "utf8") : "diff --git a/file.txt b/file.txt\n+new content"; - }), - }), - (mockExec = { - exec: vi.fn().mockResolvedValue(0), - getExecOutput: vi.fn().mockImplementation((command, args) => { - return "git" === command && args && "rev-parse" === args[0] && "HEAD" === args[1] ? Promise.resolve({ exitCode: 0, stdout: "abc123def456\n", stderr: "" }) : Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); - }), - }), - mockCore.setFailed.mockReset(), - mockCore.setOutput.mockReset(), - mockCore.warning.mockReset(), - mockCore.error.mockReset()); - const scriptPath = path.join(process.cwd(), "push_to_pull_request_branch.cjs"); - ((pushToPrBranchScript = fs.readFileSync(scriptPath, "utf8")), - (pushToPrBranchScript = pushToPrBranchScript.replace( - /\/\*\* @type \{typeof import\("fs"\)\} \*\/\nconst fs = require\("fs"\);/, - "const core = global.core;\nconst context = global.context || {};\nconst fs = global.mockFs;\nconst exec = global.exec;" - ))); - }), - afterEach(() => { - (tempFilePath && require("fs").existsSync(tempFilePath) && (require("fs").unlinkSync(tempFilePath), (tempFilePath = void 0)), - "undefined" != typeof global && (delete global.core, delete global.context, delete global.mockFs, delete global.exec)); - }), - describe("Script execution", () => { - (it("should skip when no agent output is provided", async () => { - (delete process.env.GH_AW_AGENT_OUTPUT, await executeScript(), expect(mockCore.info).toHaveBeenCalledWith("Agent output content is empty"), expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should skip when agent output is empty", async () => { - (setAgentOutput(""), await executeScript(), expect(mockCore.info).toHaveBeenCalledWith("Agent output content is empty"), expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should handle missing patch file with default 'warn' behavior", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - mockFs.existsSync.mockReturnValue(!1), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith("No patch file found - cannot push without changes"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should fail when patch file missing and if-no-changes is 'error'", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - (process.env.GH_AW_PUSH_IF_NO_CHANGES = "error"), - mockFs.existsSync.mockReturnValue(!1), - await executeScript(), - expect(mockCore.setFailed).toHaveBeenCalledWith("No patch file found - cannot push without changes")); - }), - it("should silently succeed when patch file missing and if-no-changes is 'ignore'", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - (process.env.GH_AW_PUSH_IF_NO_CHANGES = "ignore"), - mockFs.existsSync.mockReturnValue(!1), - await executeScript(), - expect(mockCore.info).not.toHaveBeenCalled(), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should fail when patch file contains error content", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("Failed to generate patch: some error"), - await executeScript(), - expect(mockCore.setFailed).toHaveBeenCalledWith("Patch file contains error message - cannot push without changes"), - expect(mockCore.error).toHaveBeenCalledWith("Patch file generation failed - this is an error condition that requires investigation"), - expect(mockCore.error).toHaveBeenCalledWith("Patch file location: /tmp/gh-aw/aw.patch"), - expect(mockCore.error).toHaveBeenCalledWith(expect.stringMatching(/Patch file size: \d+ bytes/)), - expect(mockCore.error).toHaveBeenCalledWith(expect.stringMatching(/Patch file preview \(first \d+ characters\):/)), - expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to generate patch: some error"))); - }), - it("should fail when patch file contains error content regardless of if-no-changes config", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - (process.env.GH_AW_PUSH_IF_NO_CHANGES = "ignore"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("Failed to generate patch: git diff failed"), - await executeScript(), - expect(mockCore.setFailed).toHaveBeenCalledWith("Patch file contains error message - cannot push without changes"), - expect(mockCore.error).toHaveBeenCalledWith("Patch file generation failed - this is an error condition that requires investigation")); - }), - it("should handle empty patch file with default 'warn' behavior", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent(""), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith("Patch file is empty - no changes to apply (noop operation)"), - expect(mockCore.info).toHaveBeenCalledWith(expect.stringMatching(/Agent output content length: \d+/)), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should fail when empty patch and if-no-changes is 'error'", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - (process.env.GH_AW_PUSH_IF_NO_CHANGES = "error"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent(" "), - 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 () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith(expect.stringMatching(/Agent output content length: \d+/)), - expect(mockCore.info).toHaveBeenCalledWith("Patch content validation passed"), - expect(mockCore.info).toHaveBeenCalledWith("Target configuration: triggering"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should handle invalid JSON in agent output", async () => { - const invalidJsonPath = path.join("/tmp", `test_invalid_${Date.now()}.json`); - (fs.writeFileSync(invalidJsonPath, "invalid json content"), - (process.env.GH_AW_AGENT_OUTPUT = invalidJsonPath), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("some patch content"), - await executeScript(), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringMatching(/Error parsing agent output JSON:/)), - fs.existsSync(invalidJsonPath) && fs.unlinkSync(invalidJsonPath)); - }), - it("should handle agent output without valid items array", async () => { - (setAgentOutput({ items: "not an array" }), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("some patch content"), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith("No valid items found in agent output"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should use custom target configuration", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "test" }] }), - (process.env.GH_AW_PUSH_TARGET = "custom-target"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("some patch content"), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith("Target configuration: custom-target")); - })); - }), - describe("Script validation", () => { - (it("should have valid JavaScript syntax", () => { - const scriptPath = path.join(__dirname, "push_to_pull_request_branch.cjs"), - scriptContent = fs.readFileSync(scriptPath, "utf8"); - (expect(scriptContent).toContain("async function main()"), expect(scriptContent).toContain("core.setFailed"), expect(scriptContent).toContain("/tmp/gh-aw/aw.patch"), expect(scriptContent).toContain("module.exports = { main }")); - }), - it("should export a main function", () => { - const scriptPath = path.join(__dirname, "push_to_pull_request_branch.cjs"), - scriptContent = fs.readFileSync(scriptPath, "utf8"); - expect(scriptContent).toMatch(/async function main\(\) \{[\s\S]*\}/); - }), - it("should validate patch size within limit", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), (process.env.GH_AW_MAX_PATCH_SIZE = "10"), mockFs.existsSync.mockReturnValue(!0)); - const patchContent = "diff --git a/file.txt b/file.txt\n+new content\n".repeat(100); - (mockPatchContent(patchContent), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith(expect.stringMatching(/Patch size: \d+ KB \(maximum allowed: 10 KB\)/)), - expect(mockCore.info).toHaveBeenCalledWith("Patch size validation passed"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should fail when patch size exceeds limit", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), (process.env.GH_AW_MAX_PATCH_SIZE = "1"), mockFs.existsSync.mockReturnValue(!0)); - const patchContent = "diff --git a/file.txt b/file.txt\n+new content\n".repeat(100); - (mockPatchContent(patchContent), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith(expect.stringMatching(/Patch size: \d+ KB \(maximum allowed: 1 KB\)/)), - expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringMatching(/Patch size \(\d+ KB\) exceeds maximum allowed size \(1 KB\)/))); - }), - it("should use default 1024 KB limit when env var not set", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - delete process.env.GH_AW_MAX_PATCH_SIZE, - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content\n"), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith(expect.stringMatching(/Patch size: \d+ KB \(maximum allowed: 1024 KB\)/)), - expect(mockCore.info).toHaveBeenCalledWith("Patch size validation passed"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should skip patch size validation for empty patches", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_MAX_PATCH_SIZE = "1"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent(""), - await executeScript(), - expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringMatching(/Patch size:/)), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should validate PR title prefix when specified", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_PR_TITLE_PREFIX = "[bot] "), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "[bot] Add new feature", - labels: [], - }, - }), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith('✓ Title prefix validation passed: "[bot] "'), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should fail when PR title doesn't match required prefix", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_PR_TITLE_PREFIX = "[bot] "), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "Add new feature", - labels: [], - }, - }), - await executeScript(), - expect(mockCore.setFailed).toHaveBeenCalledWith('Pull request title "Add new feature" does not start with required prefix "[bot] "')); - }), - it("should validate PR labels when specified", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_PR_LABELS = "automation,enhancement"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "Add new feature", - labels: [{ name: "automation" }, { name: "enhancement" }, { name: "feature" }], - }, - }), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith("✓ Labels validation passed: automation,enhancement"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - }), - it("should fail when PR is missing required labels", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_PR_LABELS = "automation,enhancement"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "Add new feature", - labels: [{ name: "feature" }], - }, - }), - await executeScript(), - expect(mockCore.setFailed).toHaveBeenCalledWith("Pull request is missing required labels: automation, enhancement. Current labels: feature")); - }), - it("should validate both title prefix and labels when both are specified", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_PR_TITLE_PREFIX = "[automated] "), - (process.env.GH_AW_PR_LABELS = "bot,feature"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "[automated] Add new feature", - labels: [{ name: "bot" }, { name: "feature" }, { name: "enhancement" }], - }, - }), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith('✓ Title prefix validation passed: "[automated] "'), - expect(mockCore.info).toHaveBeenCalledWith("✓ Labels validation passed: bot,feature"), - expect(mockCore.setFailed).not.toHaveBeenCalled()); - })); - }), - describe("Commit title suffix", () => { - (beforeEach(() => { - mockFs.writeFileSync = vi.fn(); - }), - it("should append bracketed suffix (for [skip-ci] support)", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_COMMIT_TITLE_SUFFIX = " [skip-ci]"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("From abc123 Mon Sep 17 00:00:00 2001\nFrom: Test User \nDate: Mon, 1 Jan 2024 00:00:00 +0000\nSubject: [PATCH] Add new feature\n\n---\n file.txt | 1 +\n 1 file changed, 1 insertion(+)\n"), - await executeScript(), - expect(mockFs.writeFileSync).toHaveBeenCalled()); - const writtenPatch = mockFs.writeFileSync.mock.calls[0][1]; - (expect(writtenPatch).toContain("Subject: [PATCH] Add new feature [skip-ci]"), expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('commit title suffix: " [skip-ci]"'))); - }), - it("should append suffix without brackets as-is", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_COMMIT_TITLE_SUFFIX = " (automated)"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("From abc123 Mon Sep 17 00:00:00 2001\nFrom: Test User \nDate: Mon, 1 Jan 2024 00:00:00 +0000\nSubject: [PATCH] Add new feature\n\n---\n file.txt | 1 +\n 1 file changed, 1 insertion(+)\n"), - await executeScript(), - expect(mockFs.writeFileSync).toHaveBeenCalled()); - const writtenPatch = mockFs.writeFileSync.mock.calls[0][1]; - (expect(writtenPatch).toContain("Subject: [PATCH] Add new feature (automated)"), expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('commit title suffix: " (automated)"'))); - }), - it("should append suffix with brackets", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_COMMIT_TITLE_SUFFIX = " [bot]"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("From abc123 Mon Sep 17 00:00:00 2001\nFrom: Test User \nDate: Mon, 1 Jan 2024 00:00:00 +0000\nSubject: [PATCH] Add new feature\n\n---\n file.txt | 1 +\n 1 file changed, 1 insertion(+)\n"), - await executeScript(), - expect(mockFs.writeFileSync).toHaveBeenCalled()); - const writtenPatch = mockFs.writeFileSync.mock.calls[0][1]; - (expect(writtenPatch).toContain("Subject: [PATCH] Add new feature [bot]"), expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining('commit title suffix: " [bot]"'))); - }), - it("should not modify patch when no commit title suffix is set", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - delete process.env.GH_AW_COMMIT_TITLE_SUFFIX, - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("From abc123 Mon Sep 17 00:00:00 2001\nFrom: Test User \nDate: Mon, 1 Jan 2024 00:00:00 +0000\nSubject: [PATCH] Add new feature\n\n---\n file.txt | 1 +\n 1 file changed, 1 insertion(+)\n"), - await executeScript(), - expect(mockFs.writeFileSync).not.toHaveBeenCalled()); - }), - it("should handle patch without [PATCH] prefix in Subject line", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_COMMIT_TITLE_SUFFIX = " [automated]"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("From abc123 Mon Sep 17 00:00:00 2001\nFrom: Test User \nDate: Mon, 1 Jan 2024 00:00:00 +0000\nSubject: Add new feature\n\n---\n file.txt | 1 +\n 1 file changed, 1 insertion(+)\n"), - await executeScript(), - expect(mockFs.writeFileSync).toHaveBeenCalled()); - const writtenPatch = mockFs.writeFileSync.mock.calls[0][1]; - expect(writtenPatch).toContain("Subject: [PATCH] Add new feature [automated]"); - }), - it("should use git am without --keep-non-patch flag", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - (process.env.GH_AW_COMMIT_TITLE_SUFFIX = " [skip-ci]"), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - await executeScript(), - expect(mockExec.exec).toHaveBeenCalledWith("git am /tmp/gh-aw/aw.patch")); - })); - }), - describe("Patch failure investigation", () => { - (beforeEach(() => { - mockFs.writeFileSync = vi.fn(); - }), - it("should investigate patch failure by logging git status and failed patch", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), mockFs.existsSync.mockReturnValue(!0), mockPatchContent("diff --git a/file.txt b/file.txt\n+new content")); - let gitAmCalled = !1; - (mockExec.exec.mockImplementation(async (cmd, args) => { - if ("string" == typeof cmd && cmd.includes("git am")) throw ((gitAmCalled = !0), new Error("Patch does not apply")); - return 0; - }), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "Test PR Title", - labels: [{ name: "bug" }, { name: "enhancement" }], - }, - }), - mockExec.getExecOutput.mockImplementation(async (command, args) => { - return "git" === command && args && "status" === args[0] - ? Promise.resolve({ exitCode: 0, stdout: "On branch feature-branch\nYour branch is up to date\n", stderr: "" }) - : "git" === command && args && "log" === args[0] && "--oneline" === args[1] && "-5" === args[2] - ? Promise.resolve({ exitCode: 0, stdout: "abc123 Latest commit\ndef456 Previous commit\n", stderr: "" }) - : "git" === command && args && "diff" === args[0] && "HEAD" === args[1] - ? Promise.resolve({ exitCode: 0, stdout: "diff --git a/modified.txt b/modified.txt\n+modified content\n", stderr: "" }) - : "git" === command && args && "am" === args[0] && "--show-current-patch=diff" === args[1] - ? Promise.resolve({ exitCode: 0, stdout: "diff --git a/conflicting.txt b/conflicting.txt\n+conflicting line\n", stderr: "" }) - : "git" === command && args && "am" === args[0] && "--show-current-patch" === args[1] - ? Promise.resolve({ exitCode: 0, stdout: "From abc123 Mon Sep 17 00:00:00 2001\nSubject: [PATCH] Add feature\n", stderr: "" }) - : Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); - }), - await executeScript(), - expect(gitAmCalled).toBe(!0), - expect(mockExec.getExecOutput).toHaveBeenCalledWith("git", ["status"]), - expect(mockExec.getExecOutput).toHaveBeenCalledWith("git", ["log", "--oneline", "-5"]), - expect(mockExec.getExecOutput).toHaveBeenCalledWith("git", ["diff", "HEAD"]), - expect(mockExec.getExecOutput).toHaveBeenCalledWith("git", ["am", "--show-current-patch=diff"]), - expect(mockExec.getExecOutput).toHaveBeenCalledWith("git", ["am", "--show-current-patch"]), - expect(mockCore.info).toHaveBeenCalledWith("Investigating patch failure..."), - expect(mockCore.info).toHaveBeenCalledWith("Git status output:"), - expect(mockCore.info).toHaveBeenCalledWith("On branch feature-branch\nYour branch is up to date\n"), - expect(mockCore.info).toHaveBeenCalledWith("Recent commits (last 5):"), - expect(mockCore.info).toHaveBeenCalledWith("abc123 Latest commit\ndef456 Previous commit\n"), - expect(mockCore.info).toHaveBeenCalledWith("Uncommitted changes:"), - expect(mockCore.info).toHaveBeenCalledWith("diff --git a/modified.txt b/modified.txt\n+modified content\n"), - expect(mockCore.info).toHaveBeenCalledWith("Failed patch diff:"), - expect(mockCore.info).toHaveBeenCalledWith("diff --git a/conflicting.txt b/conflicting.txt\n+conflicting line\n"), - expect(mockCore.info).toHaveBeenCalledWith("Failed patch (full):"), - expect(mockCore.info).toHaveBeenCalledWith("From abc123 Mon Sep 17 00:00:00 2001\nSubject: [PATCH] Add feature\n"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to apply patch")); - }), - it("should handle investigation failure gracefully", async () => { - (setAgentOutput({ items: [{ type: "push_to_pull_request_branch", content: "some changes to push" }] }), - mockFs.existsSync.mockReturnValue(!0), - mockPatchContent("diff --git a/file.txt b/file.txt\n+new content"), - mockExec.exec.mockImplementation(async (cmd, args) => { - if ("string" == typeof cmd && cmd.includes("git am")) throw new Error("Patch does not apply"); - return 0; - }), - mockGithub.rest.pulls.get.mockResolvedValueOnce({ - data: { - head: { ref: "feature-branch" }, - title: "Test PR Title", - labels: [], - }, - }), - mockExec.getExecOutput.mockImplementation(async (command, args) => { - if ("git" === command) throw new Error("Git command failed"); - return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); - }), - await executeScript(), - expect(mockCore.info).toHaveBeenCalledWith("Investigating patch failure..."), - expect(mockCore.warning).toHaveBeenCalledWith("Failed to investigate patch failure: Git command failed"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to apply patch")); - })); - })); - })); diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index c7c391e7c61..1b6cb910954 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -29,29 +29,18 @@ const HANDLER_MAP = { link_sub_issue: "./link_sub_issue.cjs", update_release: "./update_release.cjs", create_pull_request_review_comment: "./create_pr_review_comment.cjs", + create_pull_request: "./create_pull_request.cjs", + push_to_pull_request_branch: "./push_to_pull_request_branch.cjs", + update_pull_request: "./update_pull_request.cjs", + close_pull_request: "./close_pull_request.cjs", + hide_comment: "./hide_comment.cjs", }; /** * Message types handled by standalone steps (not through the handler manager) * These types should not trigger warnings when skipped by the handler manager */ -const STANDALONE_STEP_TYPES = new Set([ - "create_pull_request", - "close_pull_request", - "create_code_scanning_alert", - "add_reviewer", - "assign_milestone", - "assign_to_agent", - "assign_to_user", - "update_pull_request", - "push_to_pull_request_branch", - "hide_comment", - "create_agent_task", - "update_project", - "upload_asset", - "noop", - "missing_tool", -]); +const STANDALONE_STEP_TYPES = new Set(["create_code_scanning_alert", "add_reviewer", "assign_milestone", "assign_to_agent", "assign_to_user", "create_agent_task", "update_project", "upload_asset", "noop", "missing_tool"]); /** * Load configuration for safe outputs diff --git a/actions/setup/js/types/handler-factory.d.ts b/actions/setup/js/types/handler-factory.d.ts index 2f93b3883af..d64106ef972 100644 --- a/actions/setup/js/types/handler-factory.d.ts +++ b/actions/setup/js/types/handler-factory.d.ts @@ -42,6 +42,8 @@ interface HandlerErrorResult { success: false; /** Error message describing what went wrong */ error: string; + /** Additional result properties (skipped, etc.) */ + [key: string]: any; } /** diff --git a/actions/setup/js/update_pull_request.cjs b/actions/setup/js/update_pull_request.cjs index 24e9cf2465a..8a1e26d3a90 100644 --- a/actions/setup/js/update_pull_request.cjs +++ b/actions/setup/js/update_pull_request.cjs @@ -1,9 +1,16 @@ // @ts-check /// -const { createUpdateHandler } = require("./update_runner.cjs"); +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + */ + +/** @type {string} Safe output type handled by this module */ +const HANDLER_TYPE = "update_pull_request"; + const { updateBody } = require("./update_pr_description_helpers.cjs"); const { isPRContext, getPRNumber } = require("./update_context_helpers.cjs"); +const { getErrorMessage } = require("./error_helpers.cjs"); /** * Execute the pull request update API call @@ -61,23 +68,132 @@ async function executePRUpdate(github, context, prNumber, updateData) { return pr; } -// Create the handler using the factory -const main = createUpdateHandler({ - itemType: "update_pull_request", - displayName: "pull request", - displayNamePlural: "pull requests", - numberField: "pull_request_number", - outputNumberKey: "pull_request_number", - outputUrlKey: "pull_request_url", - entityName: "Pull Request", - entityPrefix: "PR", - targetLabel: "Target PR:", - currentTargetText: "Current pull request", - supportsStatus: false, - supportsOperation: true, - isValidContext: isPRContext, - getContextNumber: getPRNumber, - executeUpdate: executePRUpdate, -}); +/** + * Main handler factory for update_pull_request + * Returns a message handler function that processes individual update_pull_request messages + * @type {HandlerFactoryFunction} + */ +async function main(config = {}) { + // Extract configuration + const updateTarget = config.target || "triggering"; + const maxCount = config.max || 10; + const canUpdateTitle = config.allow_title !== false; // Default true + const canUpdateBody = config.allow_body !== false; // Default true + + core.info(`Update pull request configuration: max=${maxCount}, target=${updateTarget}, allow_title=${canUpdateTitle}, allow_body=${canUpdateBody}`); + + // Track state + let processedCount = 0; + + /** + * Message handler function + * @param {Object} message - The update_pull_request message + * @param {Object} resolvedTemporaryIds - Resolved temporary IDs + * @returns {Promise} Result + */ + return async function handleUpdatePullRequest(message, resolvedTemporaryIds) { + // Check max limit + if (processedCount >= maxCount) { + core.warning(`Skipping update_pull_request: max count of ${maxCount} reached`); + return { + success: false, + error: `Max count of ${maxCount} reached`, + }; + } + + processedCount++; + + const item = message; + + // Determine target PR number + let prNumber; + if (item.pull_request_number !== undefined) { + prNumber = parseInt(String(item.pull_request_number), 10); + if (isNaN(prNumber)) { + core.warning(`Invalid pull request number: ${item.pull_request_number}`); + return { + success: false, + error: `Invalid pull request number: ${item.pull_request_number}`, + }; + } + } else { + // Use triggering context + if (updateTarget === "triggering" && isPRContext(context.eventName, context.payload)) { + prNumber = getPRNumber(context.payload); + if (!prNumber) { + core.warning("No PR number in triggering context"); + return { + success: false, + error: "No PR number available", + }; + } + } else { + core.warning("No pull_request_number provided"); + return { + success: false, + error: "No pull request number provided", + }; + } + } + + // Build update data + const updateData = {}; + let hasUpdates = false; + + if (canUpdateTitle && item.title !== undefined) { + updateData.title = item.title; + hasUpdates = true; + } + + if (canUpdateBody && item.body !== undefined) { + // Store operation information + if (item.operation !== undefined) { + updateData._operation = item.operation; + updateData._rawBody = item.body; + } + updateData.body = item.body; + hasUpdates = true; + } + + // Other fields (always allowed) + if (item.state !== undefined) { + updateData.state = item.state; + hasUpdates = true; + } + if (item.base !== undefined) { + updateData.base = item.base; + hasUpdates = true; + } + + if (!hasUpdates) { + core.warning("No update fields provided or all fields are disabled"); + return { + success: false, + error: "No update fields provided", + }; + } + + core.info(`Updating pull request #${prNumber} with: ${JSON.stringify(Object.keys(updateData).filter(k => !k.startsWith("_")))}`); + + try { + const updatedPR = await executePRUpdate(github, context, prNumber, updateData); + core.info(`Successfully updated pull request #${prNumber}: ${updatedPR.html_url}`); + + return { + success: true, + pull_request_number: prNumber, + pull_request_url: updatedPR.html_url, + title: updatedPR.title, + }; + } catch (error) { + const errorMessage = getErrorMessage(error); + core.error(`Failed to update pull request #${prNumber}: ${errorMessage}`); + return { + success: false, + error: errorMessage, + }; + } + }; +} module.exports = { main }; diff --git a/pkg/workflow/close_entity_helpers.go b/pkg/workflow/close_entity_helpers.go index 58f02203dbe..bbc5c771b7f 100644 --- a/pkg/workflow/close_entity_helpers.go +++ b/pkg/workflow/close_entity_helpers.go @@ -81,7 +81,6 @@ type CloseEntityJobParams struct { OutputURLKey string // e.g., "issue_url", "pull_request_url" EventNumberPath1 string // e.g., "github.event.issue.number" EventNumberPath2 string // e.g., "github.event.comment.issue.number" - ScriptGetter func() string PermissionsFunc func() *Permissions } @@ -130,7 +129,6 @@ type closeEntityDefinition struct { OutputURLKey string EventNumberPath1 string EventNumberPath2 string - ScriptGetter func() string PermissionsFunc func() *Permissions Logger *logger.Logger } @@ -147,7 +145,6 @@ var closeEntityRegistry = []closeEntityDefinition{ OutputURLKey: "issue_url", EventNumberPath1: "github.event.issue.number", EventNumberPath2: "github.event.comment.issue.number", - ScriptGetter: getCloseIssueScript, PermissionsFunc: NewPermissionsContentsReadIssuesWrite, Logger: logger.New("workflow:close_issue"), }, @@ -161,7 +158,6 @@ var closeEntityRegistry = []closeEntityDefinition{ OutputURLKey: "pull_request_url", EventNumberPath1: "github.event.pull_request.number", EventNumberPath2: "github.event.comment.pull_request.number", - ScriptGetter: getClosePullRequestScript, PermissionsFunc: NewPermissionsContentsReadPRWrite, Logger: logger.New("workflow:close_pull_request"), }, @@ -175,7 +171,6 @@ var closeEntityRegistry = []closeEntityDefinition{ OutputURLKey: "discussion_url", EventNumberPath1: "github.event.discussion.number", EventNumberPath2: "github.event.comment.discussion.number", - ScriptGetter: getCloseDiscussionScript, PermissionsFunc: NewPermissionsContentsReadDiscussionsWrite, Logger: logger.New("workflow:close_discussion"), }, diff --git a/pkg/workflow/compile_outputs_allowed_labels_test.go b/pkg/workflow/compile_outputs_allowed_labels_test.go index 486019a5292..adadd60f627 100644 --- a/pkg/workflow/compile_outputs_allowed_labels_test.go +++ b/pkg/workflow/compile_outputs_allowed_labels_test.go @@ -255,9 +255,9 @@ This workflow tests that allowed-labels are passed to safe output jobs. t.Error("Expected allowed labels for create_issue in handler config") } - // Verify the allowed labels are passed as environment variable for PRs (separate step) - if !strings.Contains(lockfileContent, `GH_AW_PR_ALLOWED_LABELS: "automated"`) { - t.Error("Expected allowed labels for create_pull_request as environment variable") + // Verify the allowed labels are also in handler config for PRs (now handled by handler manager) + if !strings.Contains(lockfileContent, `\"create_pull_request\"`) { + t.Error("Expected create_pull_request in handler config") } } diff --git a/pkg/workflow/compile_outputs_pr_test.go b/pkg/workflow/compile_outputs_pr_test.go index b77c105114c..b1f48f221e0 100644 --- a/pkg/workflow/compile_outputs_pr_test.go +++ b/pkg/workflow/compile_outputs_pr_test.go @@ -154,26 +154,22 @@ This workflow tests the create_pull_request job generation. t.Error("Expected 'Checkout repository' step in create_pull_request job") } - if !strings.Contains(lockContentStr, "Create Pull Request") { - t.Error("Expected 'Create Pull Request' step in create_pull_request job") + // Verify handler manager step (Process Safe Outputs) exists + if !strings.Contains(lockContentStr, "Process Safe Outputs") { + t.Error("Expected 'Process Safe Outputs' (handler manager) step in safe_outputs job") } if !strings.Contains(lockContentStr, "uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd") { - t.Error("Expected github-script action to be used in create_pull_request job") + t.Error("Expected github-script action to be used in safe_outputs job") } - // Verify JavaScript content includes environment variables for configuration - if !strings.Contains(lockContentStr, "GH_AW_PR_TITLE_PREFIX: \"[agent] \"") { - t.Error("Expected title prefix to be set as environment variable") + // Verify handler manager config includes create_pull_request configuration + if !strings.Contains(lockContentStr, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + t.Error("Expected handler manager config environment variable") } - if !strings.Contains(lockContentStr, "GH_AW_PR_LABELS: \"automation\"") { - t.Error("Expected automation label to be set as environment variable") - } - - // Verify create_pull_request step exists in consolidated job - if !strings.Contains(lockContentStr, "id: create_pull_request") { - t.Error("Expected create_pull_request step in safe_outputs job") + if !strings.Contains(lockContentStr, "create_pull_request") { + t.Error("Expected create_pull_request to be configured in handler manager") } // Verify job dependencies @@ -234,23 +230,25 @@ This workflow tests the create_pull_request job generation with draft: false. // Convert to string for easier testing lockContentStr := string(lockContent) - // Verify create_pull_request job is present + // Verify safe_outputs job is present if !strings.Contains(lockContentStr, "safe_outputs:") { - t.Error("Expected 'create_pull_request' job to be in generated workflow") + t.Error("Expected 'safe_outputs' job to be in generated workflow") } - // Verify draft setting is false - if !strings.Contains(lockContentStr, "GH_AW_PR_DRAFT: \"false\"") { - t.Error("Expected draft to be set to false when explicitly specified") + // Verify handler manager is configured with create_pull_request + if !strings.Contains(lockContentStr, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + t.Error("Expected handler manager config environment variable") } - // Verify other expected environment variables are still present - if !strings.Contains(lockContentStr, "GH_AW_PR_TITLE_PREFIX: \"[agent] \"") { - t.Error("Expected title prefix to be set as environment variable") + // Verify create_pull_request config in handler manager + // The config should contain draft: false and other settings as JSON + if !strings.Contains(lockContentStr, "create_pull_request") { + t.Error("Expected create_pull_request to be configured in handler manager") } - if !strings.Contains(lockContentStr, "GH_AW_PR_LABELS: \"automation\"") { - t.Error("Expected automation label to be set as environment variable") + // Verify the handler manager step (Process Safe Outputs) is present + if !strings.Contains(lockContentStr, "Process Safe Outputs") || !strings.Contains(lockContentStr, "process_safe_outputs") { + t.Error("Expected 'Process Safe Outputs' handler manager step in safe_outputs job") } // t.Logf("Generated workflow content:\n%s", lockContentStr) @@ -306,23 +304,24 @@ This workflow tests the create_pull_request job generation with draft: true. // Convert to string for easier testing lockContentStr := string(lockContent) - // Verify create_pull_request job is present + // Verify safe_outputs job is present if !strings.Contains(lockContentStr, "safe_outputs:") { - t.Error("Expected 'create_pull_request' job to be in generated workflow") + t.Error("Expected 'safe_outputs' job to be in generated workflow") } - // Verify draft setting is true - if !strings.Contains(lockContentStr, "GH_AW_PR_DRAFT: \"true\"") { - t.Error("Expected draft to be set to true when explicitly specified") + // Verify handler manager is configured with create_pull_request + if !strings.Contains(lockContentStr, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + t.Error("Expected handler manager config environment variable") } - // Verify other expected environment variables are still present - if !strings.Contains(lockContentStr, "GH_AW_PR_TITLE_PREFIX: \"[agent] \"") { - t.Error("Expected title prefix to be set as environment variable") + // Verify create_pull_request config in handler manager + if !strings.Contains(lockContentStr, "create_pull_request") { + t.Error("Expected create_pull_request to be configured in handler manager") } - if !strings.Contains(lockContentStr, "GH_AW_PR_LABELS: \"automation\"") { - t.Error("Expected automation label to be set as environment variable") + // Verify the handler manager step (Process Safe Outputs) is present + if !strings.Contains(lockContentStr, "Process Safe Outputs") || !strings.Contains(lockContentStr, "process_safe_outputs") { + t.Error("Expected 'Process Safe Outputs' handler manager step in safe_outputs job") } // t.Logf("Generated workflow content:\n%s", lockContentStr) @@ -426,10 +425,15 @@ This workflow tests the default if-no-changes behavior. t.Fatalf("Failed to read generated lock file: %v", err) } - // Verify the if-no-changes configuration is passed to the environment + // Verify the if-no-changes configuration is passed to the handler manager lockContentStr := string(lockContent) - if !strings.Contains(lockContentStr, "GH_AW_PR_IF_NO_CHANGES: \"error\"") { - t.Error("Expected GH_AW_PR_IF_NO_CHANGES environment variable to be set in generated workflow") + if !strings.Contains(lockContentStr, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + t.Error("Expected handler manager config environment variable in generated workflow") + } + + // Verify create_pull_request is configured in the handler manager + if !strings.Contains(lockContentStr, "create_pull_request") { + t.Error("Expected create_pull_request to be configured in handler manager") } } @@ -495,9 +499,9 @@ This test verifies that the aw.patch artifact is downloaded in the safe_outputs t.Errorf("Expected patch artifact to be downloaded to '/tmp/gh-aw/'") } - // Verify that the create_pull_request step exists - if !strings.Contains(lockContentStr, "- name: Create Pull Request") { - t.Errorf("Expected 'Create Pull Request' step in safe_outputs job") + // Verify that the handler manager step exists (processes create_pull_request) + if !strings.Contains(lockContentStr, "- name: Process Safe Outputs") { + t.Errorf("Expected 'Process Safe Outputs' (handler manager) step in safe_outputs job") } // Verify that the condition checks for create_pull_request output type diff --git a/pkg/workflow/compiler_safe_outputs_core.go b/pkg/workflow/compiler_safe_outputs_core.go index 53927b0cc40..6dd694f554f 100644 --- a/pkg/workflow/compiler_safe_outputs_core.go +++ b/pkg/workflow/compiler_safe_outputs_core.go @@ -51,9 +51,6 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // Track whether threat detection job is enabled for step conditions threatDetectionEnabled := data.SafeOutputs.ThreatDetection != nil - // Track which outputs are created for dependency tracking - var createPullRequestEnabled bool - // Add GitHub App token minting step if app is configured if data.SafeOutputs.App != nil { consolidatedSafeOutputsLog.Print("Adding GitHub App token minting step") @@ -91,12 +88,10 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // Add shared checkout and git config steps for PR operations // Both create-pull-request and push-to-pull-request-branch need these steps, // so we add them once with a combined condition to avoid duplication - var prCheckoutStepsAdded bool if data.SafeOutputs.CreatePullRequests != nil || data.SafeOutputs.PushToPullRequestBranch != nil { consolidatedSafeOutputsLog.Print("Adding shared checkout step for PR operations") checkoutSteps := c.buildSharedPRCheckoutSteps(data) steps = append(steps, checkoutSteps...) - prCheckoutStepsAdded = true } // === Build safe output steps === @@ -112,7 +107,12 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa data.SafeOutputs.UpdateDiscussions != nil || data.SafeOutputs.LinkSubIssue != nil || data.SafeOutputs.UpdateRelease != nil || - data.SafeOutputs.CreatePullRequestReviewComments != nil + data.SafeOutputs.CreatePullRequestReviewComments != nil || + data.SafeOutputs.CreatePullRequests != nil || + data.SafeOutputs.PushToPullRequestBranch != nil || + data.SafeOutputs.UpdatePullRequests != nil || + data.SafeOutputs.ClosePullRequests != nil || + data.SafeOutputs.HideComment != nil // If we have handler manager types, use the handler manager step if hasHandlerManagerTypes { @@ -159,37 +159,25 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa if data.SafeOutputs.CreatePullRequestReviewComments != nil { permissions.Merge(NewPermissionsContentsReadPRWrite()) } - } - - // Create Pull Request step (not handled by handler manager) - if data.SafeOutputs.CreatePullRequests != nil { - createPullRequestEnabled = true - _ = createPullRequestEnabled // Track for potential future use - stepConfig := c.buildCreatePullRequestStepConfig(data, mainJobName, threatDetectionEnabled) - // Skip pre-steps if we've already added the shared checkout steps - if !prCheckoutStepsAdded { - steps = append(steps, stepConfig.PreSteps...) + if data.SafeOutputs.CreatePullRequests != nil { + permissions.Merge(NewPermissionsContentsWriteIssuesWritePRWrite()) + } + if data.SafeOutputs.PushToPullRequestBranch != nil { + permissions.Merge(NewPermissionsContentsWriteIssuesWritePRWrite()) + } + if data.SafeOutputs.UpdatePullRequests != nil { + permissions.Merge(NewPermissionsContentsReadPRWrite()) + } + if data.SafeOutputs.ClosePullRequests != nil { + permissions.Merge(NewPermissionsContentsReadPRWrite()) + } + if data.SafeOutputs.HideComment != nil { + permissions.Merge(NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite()) } - stepYAML := c.buildConsolidatedSafeOutputStep(data, stepConfig) - steps = append(steps, stepYAML...) - steps = append(steps, stepConfig.PostSteps...) - safeOutputStepNames = append(safeOutputStepNames, stepConfig.StepID) - - outputs["create_pull_request_pull_request_number"] = "${{ steps.create_pull_request.outputs.pull_request_number }}" - outputs["create_pull_request_pull_request_url"] = "${{ steps.create_pull_request.outputs.pull_request_url }}" - - permissions.Merge(NewPermissionsContentsWriteIssuesWritePRWrite()) } - // Close Pull Request step (not handled by handler manager) - if data.SafeOutputs.ClosePullRequests != nil { - stepConfig := c.buildClosePullRequestStepConfig(data, mainJobName, threatDetectionEnabled) - stepYAML := c.buildConsolidatedSafeOutputStep(data, stepConfig) - steps = append(steps, stepYAML...) - safeOutputStepNames = append(safeOutputStepNames, stepConfig.StepID) - - permissions.Merge(NewPermissionsContentsReadPRWrite()) - } + // Note: Create Pull Request is now handled by the handler manager + // The outputs and permissions are configured in the handler manager section above // Mark Pull Request as Ready for Review step (not handled by handler manager) if data.SafeOutputs.MarkPullRequestAsReadyForReview != nil { @@ -260,31 +248,9 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa permissions.Merge(NewPermissionsContentsReadIssuesWritePRWrite()) } - // 16. Update Pull Request step - if data.SafeOutputs.UpdatePullRequests != nil { - stepConfig := c.buildUpdatePullRequestStepConfig(data, mainJobName, threatDetectionEnabled) - stepYAML := c.buildConsolidatedSafeOutputStep(data, stepConfig) - steps = append(steps, stepYAML...) - safeOutputStepNames = append(safeOutputStepNames, stepConfig.StepID) + // 16. Update Pull Request step - now handled by handler manager - permissions.Merge(NewPermissionsContentsReadPRWrite()) - } - - // 17. Push To Pull Request Branch step - if data.SafeOutputs.PushToPullRequestBranch != nil { - stepConfig := c.buildPushToPullRequestBranchStepConfig(data, mainJobName, threatDetectionEnabled) - // Skip pre-steps if we've already added the shared checkout steps - if !prCheckoutStepsAdded { - steps = append(steps, stepConfig.PreSteps...) - } - stepYAML := c.buildConsolidatedSafeOutputStep(data, stepConfig) - steps = append(steps, stepYAML...) - safeOutputStepNames = append(safeOutputStepNames, stepConfig.StepID) - - outputs["push_to_pull_request_branch_commit_url"] = "${{ steps.push_to_pull_request_branch.outputs.commit_url }}" - - permissions.Merge(NewPermissionsContentsWriteIssuesWritePRWrite()) - } + // 17. Push To Pull Request Branch step - now handled by handler manager // 18. Upload Assets - now handled as a separate job (see buildSafeOutputsJobs) // This was moved out of the consolidated job to allow proper git configuration @@ -292,16 +258,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // 19. Update Release step - now handled by handler manager // 20. Link Sub Issue step - now handled by handler manager - - // 21. Hide Comment step - if data.SafeOutputs.HideComment != nil { - stepConfig := c.buildHideCommentStepConfig(data, mainJobName, threatDetectionEnabled) - stepYAML := c.buildConsolidatedSafeOutputStep(data, stepConfig) - steps = append(steps, stepYAML...) - safeOutputStepNames = append(safeOutputStepNames, stepConfig.StepID) - - permissions.Merge(NewPermissionsContentsReadIssuesWritePRWriteDiscussionsWrite()) - } + // 21. Hide Comment step - now handled by handler manager // 22. Create Agent Task step if data.SafeOutputs.CreateAgentTasks != nil { @@ -902,6 +859,127 @@ func (c *Compiler) addHandlerManagerConfigEnvVar(steps *[]string, data *Workflow config["create_pull_request_review_comment"] = handlerConfig } + if data.SafeOutputs.CreatePullRequests != nil { + cfg := data.SafeOutputs.CreatePullRequests + handlerConfig := make(map[string]any) + if cfg.Max > 0 { + handlerConfig["max"] = cfg.Max + } + if cfg.TitlePrefix != "" { + handlerConfig["title_prefix"] = cfg.TitlePrefix + } + if len(cfg.Labels) > 0 { + handlerConfig["labels"] = cfg.Labels + } + if cfg.Draft != nil { + handlerConfig["draft"] = *cfg.Draft + } + if cfg.IfNoChanges != "" { + handlerConfig["if_no_changes"] = cfg.IfNoChanges + } + if cfg.AllowEmpty { + handlerConfig["allow_empty"] = cfg.AllowEmpty + } + if cfg.Expires > 0 { + handlerConfig["expires"] = cfg.Expires + } + // Add base branch (required for git operations) + handlerConfig["base_branch"] = "${{ github.ref_name }}" + // Add max patch size + maxPatchSize := 1024 // default 1024 KB + if data.SafeOutputs.MaximumPatchSize > 0 { + maxPatchSize = data.SafeOutputs.MaximumPatchSize + } + handlerConfig["max_patch_size"] = maxPatchSize + config["create_pull_request"] = handlerConfig + } + + if data.SafeOutputs.PushToPullRequestBranch != nil { + cfg := data.SafeOutputs.PushToPullRequestBranch + handlerConfig := make(map[string]any) + if cfg.Max > 0 { + handlerConfig["max"] = cfg.Max + } + if cfg.Target != "" { + handlerConfig["target"] = cfg.Target + } + if cfg.TitlePrefix != "" { + handlerConfig["title_prefix"] = cfg.TitlePrefix + } + if len(cfg.Labels) > 0 { + handlerConfig["labels"] = cfg.Labels + } + if cfg.IfNoChanges != "" { + handlerConfig["if_no_changes"] = cfg.IfNoChanges + } + if cfg.CommitTitleSuffix != "" { + handlerConfig["commit_title_suffix"] = cfg.CommitTitleSuffix + } + // Add base branch (required for git operations) + handlerConfig["base_branch"] = "${{ github.ref_name }}" + // Add max patch size + maxPatchSize := 1024 // default 1024 KB + if data.SafeOutputs.MaximumPatchSize > 0 { + maxPatchSize = data.SafeOutputs.MaximumPatchSize + } + handlerConfig["max_patch_size"] = maxPatchSize + config["push_to_pull_request_branch"] = handlerConfig + } + + if data.SafeOutputs.UpdatePullRequests != nil { + cfg := data.SafeOutputs.UpdatePullRequests + handlerConfig := make(map[string]any) + if cfg.Max > 0 { + handlerConfig["max"] = cfg.Max + } + if cfg.Target != "" { + handlerConfig["target"] = cfg.Target + } + // Boolean pointer fields indicate which fields can be updated + // Default to true if not specified (backward compatibility) + if cfg.Title != nil { + handlerConfig["allow_title"] = *cfg.Title + } else { + handlerConfig["allow_title"] = true + } + if cfg.Body != nil { + handlerConfig["allow_body"] = *cfg.Body + } else { + handlerConfig["allow_body"] = true + } + config["update_pull_request"] = handlerConfig + } + + if data.SafeOutputs.ClosePullRequests != nil { + cfg := data.SafeOutputs.ClosePullRequests + handlerConfig := make(map[string]any) + if cfg.Max > 0 { + handlerConfig["max"] = cfg.Max + } + if cfg.Target != "" { + handlerConfig["target"] = cfg.Target + } + if len(cfg.RequiredLabels) > 0 { + handlerConfig["required_labels"] = cfg.RequiredLabels + } + if cfg.RequiredTitlePrefix != "" { + handlerConfig["required_title_prefix"] = cfg.RequiredTitlePrefix + } + config["close_pull_request"] = handlerConfig + } + + if data.SafeOutputs.HideComment != nil { + cfg := data.SafeOutputs.HideComment + handlerConfig := make(map[string]any) + if cfg.Max > 0 { + handlerConfig["max"] = cfg.Max + } + if len(cfg.AllowedReasons) > 0 { + handlerConfig["allowed_reasons"] = cfg.AllowedReasons + } + config["hide_comment"] = handlerConfig + } + // Only add the env var if there are handlers to configure if len(config) > 0 { configJSON, err := json.Marshal(config) @@ -974,10 +1052,23 @@ func (c *Compiler) addAllSafeOutputConfigEnvVars(steps *[]string, data *Workflow if !c.trialMode && data.SafeOutputs.Staged && !stagedFlagAdded && cfg.TargetRepoSlug == "" { *steps = append(*steps, " GH_AW_SAFE_OUTPUTS_STAGED: \"true\"\n") stagedFlagAdded = true - _ = stagedFlagAdded // Mark as used for linter } // All update configuration (target, allow_title, allow_body, allow_labels) is now in handler config JSON } + // Create Pull Request env vars + if data.SafeOutputs.CreatePullRequests != nil { + // Add staged flag if needed + if !c.trialMode && data.SafeOutputs.Staged && !stagedFlagAdded { + *steps = append(*steps, " GH_AW_SAFE_OUTPUTS_STAGED: \"true\"\n") + stagedFlagAdded = true + } + // Note: base_branch and max_patch_size are now in handler config JSON + } + + if stagedFlagAdded { + _ = stagedFlagAdded // Mark as used for linter + } + // Note: Most handlers read from the config.json file, so we may not need all env vars here } diff --git a/pkg/workflow/compiler_safe_outputs_prs.go b/pkg/workflow/compiler_safe_outputs_prs.go index aaba23d1798..81c858e8f35 100644 --- a/pkg/workflow/compiler_safe_outputs_prs.go +++ b/pkg/workflow/compiler_safe_outputs_prs.go @@ -1,116 +1,5 @@ package workflow -import ( - "fmt" - - "github.com/githubnext/gh-aw/pkg/constants" - "github.com/githubnext/gh-aw/pkg/logger" -) - -var prSafeOutputsLog = logger.New("workflow:compiler_safe_outputs_prs") - -// buildCreatePullRequestStepConfig builds the configuration for creating a pull request -func (c *Compiler) buildCreatePullRequestStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { - cfg := data.SafeOutputs.CreatePullRequests - prSafeOutputsLog.Printf("Building create pull request step config: draft=%v, if_no_changes=%s", - cfg.Draft != nil && *cfg.Draft, cfg.IfNoChanges) - - var customEnvVars []string - // Pass the base branch from GitHub context (required by create_pull_request.cjs) - // Note: GH_AW_WORKFLOW_ID is now set at the job level and inherited by all steps - customEnvVars = append(customEnvVars, " GH_AW_BASE_BRANCH: ${{ github.ref_name }}\n") - customEnvVars = append(customEnvVars, buildTitlePrefixEnvVar("GH_AW_PR_TITLE_PREFIX", cfg.TitlePrefix)...) - customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_LABELS", cfg.Labels)...) - customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_ALLOWED_LABELS", cfg.AllowedLabels)...) - // Add draft setting - always set with default to true for backwards compatibility - draftValue := true // Default value - if cfg.Draft != nil { - draftValue = *cfg.Draft - } - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_DRAFT: %q\n", fmt.Sprintf("%t", draftValue))) - // Add if-no-changes setting - always set with default to "warn" - ifNoChanges := cfg.IfNoChanges - if ifNoChanges == "" { - ifNoChanges = "warn" // Default value - } - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_IF_NO_CHANGES: %q\n", ifNoChanges)) - // Add allow-empty setting - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_ALLOW_EMPTY: %q\n", fmt.Sprintf("%t", cfg.AllowEmpty))) - // Add max patch size setting - maxPatchSize := 1024 // default 1024 KB - if data.SafeOutputs.MaximumPatchSize > 0 { - maxPatchSize = data.SafeOutputs.MaximumPatchSize - } - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_MAX_PATCH_SIZE: %d\n", maxPatchSize)) - // Add activation comment information if available (for updating the comment with PR link) - if data.AIReaction != "" && data.AIReaction != "none" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_COMMENT_ID: ${{ needs.%s.outputs.comment_id }}\n", constants.ActivationJobName)) - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_COMMENT_REPO: ${{ needs.%s.outputs.comment_repo }}\n", constants.ActivationJobName)) - } - // Add expires value if set (only for same-repo PRs - when target-repo is not set) - if cfg.Expires > 0 && cfg.TargetRepoSlug == "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_EXPIRES: \"%d\"\n", cfg.Expires)) - } - customEnvVars = append(customEnvVars, c.buildStepLevelSafeOutputEnvVars(data, cfg.TargetRepoSlug)...) - - condition := BuildSafeOutputType("create_pull_request") - - // Build pre-steps for checkout and git config - preSteps := c.buildCreatePullRequestPreStepsConsolidated(data, cfg, condition) - - return SafeOutputStepConfig{ - StepName: "Create Pull Request", - StepID: "create_pull_request", - ScriptName: "create_pull_request", - Script: getCreatePullRequestScript(), - CustomEnvVars: customEnvVars, - Condition: condition, - Token: cfg.GitHubToken, - PreSteps: preSteps, - } -} - -// buildUpdatePullRequestStepConfig builds the configuration for updating a pull request -func (c *Compiler) buildUpdatePullRequestStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { - cfg := data.SafeOutputs.UpdatePullRequests - prSafeOutputsLog.Print("Building update pull request step config") - - var customEnvVars []string - customEnvVars = append(customEnvVars, c.buildStepLevelSafeOutputEnvVars(data, cfg.TargetRepoSlug)...) - - condition := BuildSafeOutputType("update_pull_request") - - return SafeOutputStepConfig{ - StepName: "Update Pull Request", - StepID: "update_pull_request", - ScriptName: "update_pull_request", - Script: getUpdatePullRequestScript(), - CustomEnvVars: customEnvVars, - Condition: condition, - Token: cfg.GitHubToken, - } -} - -// buildClosePullRequestStepConfig builds the configuration for closing a pull request -func (c *Compiler) buildClosePullRequestStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { - cfg := data.SafeOutputs.ClosePullRequests - - var customEnvVars []string - customEnvVars = append(customEnvVars, c.buildStepLevelSafeOutputEnvVars(data, "")...) - - condition := BuildSafeOutputType("close_pull_request") - - return SafeOutputStepConfig{ - StepName: "Close Pull Request", - StepID: "close_pull_request", - ScriptName: "close_pull_request", - Script: getClosePullRequestScript(), - CustomEnvVars: customEnvVars, - Condition: condition, - Token: cfg.GitHubToken, - } -} - // buildMarkPullRequestAsReadyForReviewStepConfig builds the configuration for marking a PR as ready for review func (c *Compiler) buildMarkPullRequestAsReadyForReviewStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { cfg := data.SafeOutputs.MarkPullRequestAsReadyForReview @@ -131,52 +20,6 @@ func (c *Compiler) buildMarkPullRequestAsReadyForReviewStepConfig(data *Workflow } } -// buildPushToPullRequestBranchStepConfig builds the configuration for pushing to a pull request branch -func (c *Compiler) buildPushToPullRequestBranchStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { - cfg := data.SafeOutputs.PushToPullRequestBranch - - var customEnvVars []string - // Add target config if set - if cfg.Target != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PUSH_TARGET: %q\n", cfg.Target)) - } - // Add if-no-changes config if set - if cfg.IfNoChanges != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PUSH_IF_NO_CHANGES: %q\n", cfg.IfNoChanges)) - } - // Add title prefix if set (using same env var as create-pull-request) - customEnvVars = append(customEnvVars, buildTitlePrefixEnvVar("GH_AW_PR_TITLE_PREFIX", cfg.TitlePrefix)...) - // Add labels if set - customEnvVars = append(customEnvVars, buildLabelsEnvVar("GH_AW_PR_LABELS", cfg.Labels)...) - // Add commit title suffix if set - if cfg.CommitTitleSuffix != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_COMMIT_TITLE_SUFFIX: %q\n", cfg.CommitTitleSuffix)) - } - // Add max patch size setting - maxPatchSize := 1024 // default 1024 KB - if data.SafeOutputs.MaximumPatchSize > 0 { - maxPatchSize = data.SafeOutputs.MaximumPatchSize - } - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_MAX_PATCH_SIZE: %d\n", maxPatchSize)) - customEnvVars = append(customEnvVars, c.buildStepLevelSafeOutputEnvVars(data, "")...) - - condition := BuildSafeOutputType("push_to_pull_request_branch") - - // Build pre-steps for checkout and git config - preSteps := c.buildPushToPullRequestBranchPreStepsConsolidated(data, cfg, condition) - - return SafeOutputStepConfig{ - StepName: "Push To Pull Request Branch", - StepID: "push_to_pull_request_branch", - ScriptName: "push_to_pull_request_branch", - Script: getPushToPullRequestBranchScript(), - CustomEnvVars: customEnvVars, - Condition: condition, - Token: cfg.GitHubToken, - PreSteps: preSteps, - } -} - // buildAddReviewerStepConfig builds the configuration for adding a reviewer func (c *Compiler) buildAddReviewerStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { cfg := data.SafeOutputs.AddReviewer @@ -196,109 +39,3 @@ func (c *Compiler) buildAddReviewerStepConfig(data *WorkflowData, mainJobName st Token: cfg.GitHubToken, } } - -// buildCreatePullRequestPreStepsConsolidated builds the pre-steps for create-pull-request -// in the consolidated safe outputs job -func (c *Compiler) buildCreatePullRequestPreStepsConsolidated(data *WorkflowData, cfg *CreatePullRequestsConfig, condition ConditionNode) []string { - prSafeOutputsLog.Printf("Building create PR pre-steps: app_configured=%v, trial_mode=%v", - data.SafeOutputs.App != nil, c.trialMode) - var preSteps []string - - // Determine which token to use for checkout - // If an app is configured, use the app token; otherwise use the default github.token - var checkoutToken string - var gitRemoteToken string - if data.SafeOutputs.App != nil { - checkoutToken = "${{ steps.app-token.outputs.token }}" - gitRemoteToken = "${{ steps.app-token.outputs.token }}" - } else { - checkoutToken = "${{ github.token }}" - gitRemoteToken = "${{ github.token }}" - } - - // Step 1: Checkout repository with conditional execution - preSteps = append(preSteps, " - name: Checkout repository\n") - // Add the condition to only checkout if create_pull_request will run - preSteps = append(preSteps, fmt.Sprintf(" if: %s\n", condition.Render())) - preSteps = append(preSteps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/checkout"))) - preSteps = append(preSteps, " with:\n") - preSteps = append(preSteps, fmt.Sprintf(" token: %s\n", checkoutToken)) - preSteps = append(preSteps, " persist-credentials: false\n") - preSteps = append(preSteps, " fetch-depth: 1\n") - if c.trialMode { - if c.trialLogicalRepoSlug != "" { - preSteps = append(preSteps, fmt.Sprintf(" repository: %s\n", c.trialLogicalRepoSlug)) - } - } - - // Step 2: Configure Git credentials with conditional execution - gitConfigSteps := []string{ - " - name: Configure Git credentials\n", - fmt.Sprintf(" if: %s\n", condition.Render()), - " env:\n", - " REPO_NAME: ${{ github.repository }}\n", - " SERVER_URL: ${{ github.server_url }}\n", - " run: |\n", - " git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n", - " git config --global user.name \"github-actions[bot]\"\n", - " # Re-authenticate git with GitHub token\n", - " SERVER_URL_STRIPPED=\"${SERVER_URL#https://}\"\n", - fmt.Sprintf(" git remote set-url origin \"https://x-access-token:%s@${SERVER_URL_STRIPPED}/${REPO_NAME}.git\"\n", gitRemoteToken), - " echo \"Git configured with standard GitHub Actions identity\"\n", - } - preSteps = append(preSteps, gitConfigSteps...) - - return preSteps -} - -// buildPushToPullRequestBranchPreStepsConsolidated builds the pre-steps for push-to-pull-request-branch -// in the consolidated safe outputs job -func (c *Compiler) buildPushToPullRequestBranchPreStepsConsolidated(data *WorkflowData, cfg *PushToPullRequestBranchConfig, condition ConditionNode) []string { - var preSteps []string - - // Determine which token to use for checkout - // If an app is configured, use the app token; otherwise use the default github.token - var checkoutToken string - var gitRemoteToken string - if data.SafeOutputs.App != nil { - checkoutToken = "${{ steps.app-token.outputs.token }}" - gitRemoteToken = "${{ steps.app-token.outputs.token }}" - } else { - checkoutToken = "${{ github.token }}" - gitRemoteToken = "${{ github.token }}" - } - - // Step 1: Checkout repository with conditional execution - preSteps = append(preSteps, " - name: Checkout repository\n") - // Add the condition to only checkout if push_to_pull_request_branch will run - preSteps = append(preSteps, fmt.Sprintf(" if: %s\n", condition.Render())) - preSteps = append(preSteps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/checkout"))) - preSteps = append(preSteps, " with:\n") - preSteps = append(preSteps, fmt.Sprintf(" token: %s\n", checkoutToken)) - preSteps = append(preSteps, " persist-credentials: false\n") - preSteps = append(preSteps, " fetch-depth: 1\n") - if c.trialMode { - if c.trialLogicalRepoSlug != "" { - preSteps = append(preSteps, fmt.Sprintf(" repository: %s\n", c.trialLogicalRepoSlug)) - } - } - - // Step 2: Configure Git credentials with conditional execution - gitConfigSteps := []string{ - " - name: Configure Git credentials\n", - fmt.Sprintf(" if: %s\n", condition.Render()), - " env:\n", - " REPO_NAME: ${{ github.repository }}\n", - " SERVER_URL: ${{ github.server_url }}\n", - " run: |\n", - " git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n", - " git config --global user.name \"github-actions[bot]\"\n", - " # Re-authenticate git with GitHub token\n", - " SERVER_URL_STRIPPED=\"${SERVER_URL#https://}\"\n", - fmt.Sprintf(" git remote set-url origin \"https://x-access-token:%s@${SERVER_URL_STRIPPED}/${REPO_NAME}.git\"\n", gitRemoteToken), - " echo \"Git configured with standard GitHub Actions identity\"\n", - } - preSteps = append(preSteps, gitConfigSteps...) - - return preSteps -} diff --git a/pkg/workflow/compiler_safe_outputs_shared.go b/pkg/workflow/compiler_safe_outputs_shared.go index 29378e60fdf..0e59ea27939 100644 --- a/pkg/workflow/compiler_safe_outputs_shared.go +++ b/pkg/workflow/compiler_safe_outputs_shared.go @@ -1,22 +1 @@ package workflow - -// buildHideCommentStepConfig builds the configuration for hiding a comment -func (c *Compiler) buildHideCommentStepConfig(data *WorkflowData, mainJobName string, threatDetectionEnabled bool) SafeOutputStepConfig { - cfg := data.SafeOutputs.HideComment - - // No custom environment variables - configuration is passed via config object - var customEnvVars []string - customEnvVars = append(customEnvVars, c.buildStepLevelSafeOutputEnvVars(data, "")...) - - condition := BuildSafeOutputType("hide_comment") - - return SafeOutputStepConfig{ - StepName: "Hide Comment", - StepID: "hide_comment", - ScriptName: "hide_comment", - Script: getHideCommentScript(), - CustomEnvVars: customEnvVars, - Condition: condition, - Token: cfg.GitHubToken, - } -} diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index b302786be23..d7dd98eba2d 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -140,7 +140,7 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa StepID: "create_pull_request", MainJobName: mainJobName, CustomEnvVars: customEnvVars, - Script: getCreatePullRequestScript(), + Script: "", // Legacy - handler manager uses require() to load handler from /tmp/gh-aw/actions Permissions: NewPermissionsContentsWriteIssuesWritePRWrite(), Outputs: outputs, PreSteps: preSteps, diff --git a/pkg/workflow/create_pull_request_env_vars_test.go b/pkg/workflow/create_pull_request_env_vars_test.go deleted file mode 100644 index fb16e602bce..00000000000 --- a/pkg/workflow/create_pull_request_env_vars_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package workflow - -import ( - "strings" - "testing" -) - -func TestCreatePullRequestStepConfigEnvVars(t *testing.T) { - // Create a compiler instance for consolidated mode - c := NewCompiler(false, "", "test") - - // Create workflow data with create-pull-request configuration - workflowData := &WorkflowData{ - Name: "test-workflow", - AIReaction: "emoji", // Enable reaction to test comment env vars - SafeOutputs: &SafeOutputsConfig{ - CreatePullRequests: &CreatePullRequestsConfig{ - TitlePrefix: "[TEST] ", - Labels: []string{"automated", "test"}, - AllowedLabels: []string{"automated", "test", "bug"}, - Draft: boolPtr(false), - IfNoChanges: "error", - AllowEmpty: true, - Expires: 7, - }, - MaximumPatchSize: 2048, - }, - } - - // Build the step config - stepConfig := c.buildCreatePullRequestStepConfig(workflowData, "main_job", false) - - // Convert custom env vars to a single string for testing - envVarsContent := strings.Join(stepConfig.CustomEnvVars, "") - - // Verify required environment variables are present - // Note: GH_AW_WORKFLOW_ID is now set at job level, not in step-level CustomEnvVars - requiredEnvVars := map[string]string{ - "GH_AW_BASE_BRANCH": "GH_AW_BASE_BRANCH: ${{ github.ref_name }}", - "GH_AW_PR_TITLE_PREFIX": `GH_AW_PR_TITLE_PREFIX: "[TEST] "`, - "GH_AW_PR_LABELS": `GH_AW_PR_LABELS: "automated,test"`, - "GH_AW_PR_ALLOWED_LABELS": `GH_AW_PR_ALLOWED_LABELS: "automated,test,bug"`, - "GH_AW_PR_DRAFT": `GH_AW_PR_DRAFT: "false"`, - "GH_AW_PR_IF_NO_CHANGES": `GH_AW_PR_IF_NO_CHANGES: "error"`, - "GH_AW_PR_ALLOW_EMPTY": `GH_AW_PR_ALLOW_EMPTY: "true"`, - "GH_AW_MAX_PATCH_SIZE": "GH_AW_MAX_PATCH_SIZE: 2048", - "GH_AW_PR_EXPIRES": `GH_AW_PR_EXPIRES: "7"`, - } - - for envVarName, expectedContent := range requiredEnvVars { - if !strings.Contains(envVarsContent, expectedContent) { - t.Errorf("Expected %s to be present in environment variables.\nExpected: %s\nGot env vars:\n%s", - envVarName, expectedContent, envVarsContent) - } - } - - // Verify comment-related env vars are present when reaction is enabled - if !strings.Contains(envVarsContent, "GH_AW_COMMENT_ID") { - t.Error("Expected GH_AW_COMMENT_ID to be present when AIReaction is enabled") - } - if !strings.Contains(envVarsContent, "GH_AW_COMMENT_REPO") { - t.Error("Expected GH_AW_COMMENT_REPO to be present when AIReaction is enabled") - } -} - -func TestCreatePullRequestStepConfigDefaultValues(t *testing.T) { - // Create a compiler instance - c := NewCompiler(false, "", "test") - - // Create minimal workflow data to test defaults - workflowData := &WorkflowData{ - Name: "test-workflow", - SafeOutputs: &SafeOutputsConfig{ - CreatePullRequests: &CreatePullRequestsConfig{ - // Using all defaults - }, - }, - } - - // Build the step config - stepConfig := c.buildCreatePullRequestStepConfig(workflowData, "agent", false) - - // Convert custom env vars to a single string for testing - envVarsContent := strings.Join(stepConfig.CustomEnvVars, "") - - // Verify default values - // Note: GH_AW_WORKFLOW_ID is now set at job level, not in step-level CustomEnvVars - defaultEnvVars := map[string]string{ - "GH_AW_BASE_BRANCH": "GH_AW_BASE_BRANCH: ${{ github.ref_name }}", - "GH_AW_PR_DRAFT": `GH_AW_PR_DRAFT: "true"`, // Default is true - "GH_AW_PR_IF_NO_CHANGES": `GH_AW_PR_IF_NO_CHANGES: "warn"`, // Default is warn - "GH_AW_PR_ALLOW_EMPTY": `GH_AW_PR_ALLOW_EMPTY: "false"`, // Default is false - "GH_AW_MAX_PATCH_SIZE": "GH_AW_MAX_PATCH_SIZE: 1024", // Default is 1024 - } - - for envVarName, expectedContent := range defaultEnvVars { - if !strings.Contains(envVarsContent, expectedContent) { - t.Errorf("Expected default %s to be present.\nExpected: %s\nGot env vars:\n%s", - envVarName, expectedContent, envVarsContent) - } - } - - // Verify expires is NOT present when not set - if strings.Contains(envVarsContent, "GH_AW_PR_EXPIRES") { - t.Error("Expected GH_AW_PR_EXPIRES to NOT be present when Expires is 0") - } - - // Verify comment env vars are NOT present when reaction is not enabled - if strings.Contains(envVarsContent, "GH_AW_COMMENT_ID") { - t.Error("Expected GH_AW_COMMENT_ID to NOT be present when AIReaction is not enabled") - } -} - -func TestCreatePullRequestStepConfigWithTargetRepo(t *testing.T) { - // Create a compiler instance - c := NewCompiler(false, "", "test") - - // Create workflow data with target-repo (cross-repo PR) - workflowData := &WorkflowData{ - Name: "test-workflow", - SafeOutputs: &SafeOutputsConfig{ - CreatePullRequests: &CreatePullRequestsConfig{ - TargetRepoSlug: "owner/repo", - Expires: 7, // Should be ignored for cross-repo PRs - }, - }, - } - - // Build the step config - stepConfig := c.buildCreatePullRequestStepConfig(workflowData, "main_job", false) - - // Convert custom env vars to a single string for testing - envVarsContent := strings.Join(stepConfig.CustomEnvVars, "") - - // Verify expires is NOT present for cross-repo PRs - if strings.Contains(envVarsContent, "GH_AW_PR_EXPIRES") { - t.Error("Expected GH_AW_PR_EXPIRES to NOT be present for cross-repo PRs (when TargetRepoSlug is set)") - } - - // Verify target repo is passed to standard env vars builder - if !strings.Contains(envVarsContent, "GH_AW_TARGET_REPO_SLUG") { - t.Error("Expected GH_AW_TARGET_REPO_SLUG to be present for cross-repo PRs") - } -} diff --git a/pkg/workflow/create_pull_request_reviewers_integration_test.go b/pkg/workflow/create_pull_request_reviewers_integration_test.go index 23d9eb045f0..dcba6ea177f 100644 --- a/pkg/workflow/create_pull_request_reviewers_integration_test.go +++ b/pkg/workflow/create_pull_request_reviewers_integration_test.go @@ -57,12 +57,12 @@ Create a pull request with reviewers. compiledContent := string(compiledBytes) - // Verify safe_outputs job exists with create_pull_request step + // Verify safe_outputs job exists with handler manager step if !strings.Contains(compiledContent, "safe_outputs:") { t.Error("Expected safe_outputs job in compiled workflow") } - if !strings.Contains(compiledContent, "id: create_pull_request") { - t.Error("Expected create_pull_request step in compiled workflow") + if !strings.Contains(compiledContent, "id: process_safe_outputs") { + t.Error("Expected handler manager (process_safe_outputs) step in compiled workflow") } // Verify actions/github-script is used @@ -123,12 +123,12 @@ Create a pull request with a single reviewer. compiledContent := string(compiledBytes) - // Verify safe_outputs job exists with create_pull_request step + // Verify safe_outputs job exists with handler manager step if !strings.Contains(compiledContent, "safe_outputs:") { t.Error("Expected safe_outputs job in compiled workflow") } - if !strings.Contains(compiledContent, "id: create_pull_request") { - t.Error("Expected create_pull_request step in compiled workflow") + if !strings.Contains(compiledContent, "id: process_safe_outputs") { + t.Error("Expected handler manager (process_safe_outputs) step in compiled workflow") } // Verify reviewer is mentioned somewhere in the workflow diff --git a/pkg/workflow/hide_comment.go b/pkg/workflow/hide_comment.go index fdf05d5e6fb..dc9261c1632 100644 --- a/pkg/workflow/hide_comment.go +++ b/pkg/workflow/hide_comment.go @@ -55,8 +55,3 @@ func (c *Compiler) parseHideCommentConfig(outputMap map[string]any) *HideComment return nil } - -// getHideCommentScript returns the JavaScript implementation -func getHideCommentScript() string { - return DefaultScriptRegistry.GetWithMode("hide_comment", RuntimeModeGitHubScript) -} diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index ed708ca941d..36a0241b3ac 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -27,20 +27,14 @@ func getAddReviewerScript() string { return "" } func getAssignMilestoneScript() string { return "" } func getAssignToAgentScript() string { return "" } func getAssignToUserScript() string { return "" } -func getCloseDiscussionScript() string { return "" } -func getCloseIssueScript() string { return "" } -func getClosePullRequestScript() string { return "" } func getMarkPullRequestAsReadyForReviewScript() string { return "" } func getCreateCodeScanningAlertScript() string { return "" } func getCreateDiscussionScript() string { return "" } func getCreateIssueScript() string { return "" } func getCreatePRReviewCommentScript() string { return "" } -func getCreatePullRequestScript() string { return "" } func getNoOpScript() string { return "" } func getNotifyCommentErrorScript() string { return "" } -func getPushToPullRequestBranchScript() string { return "" } func getUpdateProjectScript() string { return "" } -func getUpdatePullRequestScript() string { return "" } func getUploadAssetsScript() string { return "" } // Public Get* functions return empty strings since embedded scripts were removed diff --git a/pkg/workflow/patch_artifact_download_verification_test.go b/pkg/workflow/patch_artifact_download_verification_test.go index 8a6194fc288..b367b5f5800 100644 --- a/pkg/workflow/patch_artifact_download_verification_test.go +++ b/pkg/workflow/patch_artifact_download_verification_test.go @@ -82,9 +82,9 @@ in the consolidated safe_outputs job when create-pull-request is enabled. t.Error("Expected patch download to have continue-on-error: true") } - // 6. Verify the Create Pull Request step exists (which needs the patch) - if !strings.Contains(lockContentStr, "- name: Create Pull Request") { - t.Error("Expected 'Create Pull Request' step in safe_outputs job") + // 6. Verify the handler manager step exists (which processes create_pull_request) + if !strings.Contains(lockContentStr, "- name: Process Safe Outputs") { + t.Error("Expected 'Process Safe Outputs' (handler manager) step in safe_outputs job") } // 7. Verify the checkout step exists (needed for applying the patch) @@ -92,14 +92,14 @@ in the consolidated safe_outputs job when create-pull-request is enabled. t.Error("Expected 'Checkout repository' step in safe_outputs job") } - // 8. Verify patch download comes BEFORE Create Pull Request step + // 8. Verify patch download comes BEFORE Process Safe Outputs step patchDownloadPos := strings.Index(lockContentStr, "- name: Download patch artifact") - createPRPos := strings.Index(lockContentStr, "- name: Create Pull Request") - if patchDownloadPos == -1 || createPRPos == -1 { - t.Fatal("Both patch download and create PR steps should exist") + processSafeOutputsPos := strings.Index(lockContentStr, "- name: Process Safe Outputs") + if patchDownloadPos == -1 || processSafeOutputsPos == -1 { + t.Fatal("Both patch download and Process Safe Outputs steps should exist") } - if patchDownloadPos > createPRPos { - t.Error("Patch download step should come BEFORE Create Pull Request step") + if patchDownloadPos > processSafeOutputsPos { + t.Error("Patch download step should come BEFORE Process Safe Outputs step") } // 9. Verify patch download comes AFTER agent output download diff --git a/pkg/workflow/patch_size_validation_test.go b/pkg/workflow/patch_size_validation_test.go index 780f22d6730..1f6e512f1d2 100644 --- a/pkg/workflow/patch_size_validation_test.go +++ b/pkg/workflow/patch_size_validation_test.go @@ -16,7 +16,7 @@ func TestMaximumPatchSizeEnvironmentVariable(t *testing.T) { tests := []struct { name string frontmatterContent string - expectedEnvValue string + expectedConfigValue string // Changed from expectedEnvValue - now in config JSON shouldContainPushJob bool shouldContainPRJob bool }{ @@ -32,7 +32,7 @@ safe-outputs: # Test Workflow This workflow tests default patch size configuration.`, - expectedEnvValue: "GH_AW_MAX_PATCH_SIZE: 1024", + expectedConfigValue: `\"max_patch_size\":1024`, // Now in handler config JSON (escaped in YAML) shouldContainPushJob: true, shouldContainPRJob: true, }, @@ -49,7 +49,7 @@ safe-outputs: # Test Workflow This workflow tests custom 512KB patch size configuration.`, - expectedEnvValue: "GH_AW_MAX_PATCH_SIZE: 512", + expectedConfigValue: `\"max_patch_size\":512`, // Now in handler config JSON (escaped in YAML) shouldContainPushJob: true, shouldContainPRJob: true, }, @@ -65,7 +65,7 @@ safe-outputs: # Test Workflow This workflow tests custom 2MB patch size configuration.`, - expectedEnvValue: "GH_AW_MAX_PATCH_SIZE: 2048", + expectedConfigValue: `\"max_patch_size\":2048`, // Now in handler config JSON (escaped in YAML) shouldContainPushJob: false, shouldContainPRJob: true, }, @@ -101,8 +101,11 @@ This workflow tests custom 2MB patch size configuration.`, if !strings.Contains(lockContentStr, "safe_outputs:") { t.Errorf("Expected safe_outputs job to be generated") } - if !strings.Contains(lockContentStr, tt.expectedEnvValue) { - t.Errorf("Expected '%s' to be found in safe_outputs job, got:\n%s", tt.expectedEnvValue, lockContentStr) + // For config JSON, check with flexible spacing (accounting for escaped quotes in YAML) + expectedFound := strings.Contains(lockContentStr, tt.expectedConfigValue) || + strings.Contains(lockContentStr, strings.ReplaceAll(tt.expectedConfigValue, ":", ": ")) + if !expectedFound { + t.Errorf("Expected '%s' to be found in handler config, got:\n%s", tt.expectedConfigValue, lockContentStr) } } @@ -119,9 +122,10 @@ func TestPatchSizeWithInvalidValues(t *testing.T) { tmpDir := testutil.TempDir(t, "patch-size-invalid-test") tests := []struct { - name string - frontmatterContent string - expectedEnvValue string + name string + frontmatterContent string + expectedValue string // The value to look for (env var or config JSON) + isHandlerManagerConfig bool // If true, look in config JSON; if false, look for env var }{ { name: "very small patch size should work", @@ -135,7 +139,8 @@ safe-outputs: # Test Workflow This workflow tests very small patch size configuration.`, - expectedEnvValue: "GH_AW_MAX_PATCH_SIZE: 1", + expectedValue: `\"max_patch_size\":1`, // Config JSON for handler manager (escaped in YAML) + isHandlerManagerConfig: true, }, { name: "large valid patch size should work", @@ -149,7 +154,8 @@ safe-outputs: # Test Workflow This workflow tests large valid patch size configuration.`, - expectedEnvValue: "GH_AW_MAX_PATCH_SIZE: 10240", + expectedValue: `\"max_patch_size\":10240`, // Config JSON for handler manager (escaped in YAML) + isHandlerManagerConfig: true, }, } @@ -178,9 +184,23 @@ This workflow tests large valid patch size configuration.`, } lockContentStr := string(lockContent) - // Check that the environment variable falls back to default - if !strings.Contains(lockContentStr, tt.expectedEnvValue) { - t.Errorf("Expected '%s' to be found in workflow, got:\n%s", tt.expectedEnvValue, lockContentStr) + // Check that the value is in the right format (env var or config JSON) + // For config JSON, we need to check with flexible spacing (accounting for escaped quotes in YAML) + expectedFound := false + if tt.isHandlerManagerConfig { + // Check both with and without spaces after colons + expectedFound = strings.Contains(lockContentStr, tt.expectedValue) || + strings.Contains(lockContentStr, strings.ReplaceAll(tt.expectedValue, ":", ": ")) + } else { + expectedFound = strings.Contains(lockContentStr, tt.expectedValue) + } + + if !expectedFound { + context := "environment variable" + if tt.isHandlerManagerConfig { + context = "handler config JSON" + } + t.Errorf("Expected '%s' to be found in %s, got:\n%s", tt.expectedValue, context, lockContentStr) } // Cleanup diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go index 44cc6834a7f..17448d29ee6 100644 --- a/pkg/workflow/push_to_pull_request_branch_test.go +++ b/pkg/workflow/push_to_pull_request_branch_test.go @@ -57,9 +57,14 @@ Please make changes and push them to the feature branch. t.Errorf("Generated workflow should contain safe_outputs job") } - // Verify that push_to_pull_request_branch step is present - if !strings.Contains(lockContentStr, "id: push_to_pull_request_branch") { - t.Errorf("Generated workflow should contain push_to_pull_request_branch step") + // Verify that push_to_pull_request_branch is now handled by handler manager + if !strings.Contains(lockContentStr, "id: process_safe_outputs") { + t.Errorf("Generated workflow should contain process_safe_outputs step (handler manager)") + } + + // Verify that push_to_pull_request_branch config is in handler manager config + if !strings.Contains(lockContentStr, "push_to_pull_request_branch") { + t.Errorf("Generated workflow should contain push_to_pull_request_branch in handler config") } // Verify that required permissions are present @@ -125,9 +130,9 @@ This workflow allows pushing to any pull request. lockContentStr := string(lockContent) - // Verify that the target configuration is passed correctly - if !strings.Contains(lockContentStr, "GH_AW_PUSH_TARGET: \"*\"") { - t.Errorf("Generated workflow should contain target configuration with asterisk") + // Verify that the target configuration is in handler config JSON + if !strings.Contains(lockContentStr, `"target":"*"`) && !strings.Contains(lockContentStr, `"target": "*"`) { + t.Errorf("Generated workflow should contain target configuration with asterisk in handler config JSON") } // Verify conditional execution allows any context @@ -232,10 +237,9 @@ This workflow uses null configuration which should default to "triggering". t.Errorf("Expected safe_outputs job with push_to_pull_request_branch step to be generated") } - // Check that no target is set (should use default) - if strings.Contains(lockContent, "GH_AW_PUSH_TARGET:") { - t.Errorf("Expected no target to be set when using null config, %s", lockContent) - } + // Check that no explicit target is set in the config (default "triggering" is used) + // The handler config will still contain push_to_pull_request_branch but target may be omitted or "triggering" + // This is acceptable as the handler uses "triggering" as the default } func TestPushToPullRequestBranchMinimalConfig(t *testing.T) { @@ -283,9 +287,14 @@ This workflow has minimal push-to-pull-request-branch configuration. t.Errorf("Generated workflow should contain safe_outputs job") } - // Verify push_to_pull_request_branch step is present - if !strings.Contains(lockContentStr, "id: push_to_pull_request_branch") { - t.Errorf("Generated workflow should contain push_to_pull_request_branch step") + // Verify push_to_pull_request_branch is handled by handler manager + if !strings.Contains(lockContentStr, "id: process_safe_outputs") { + t.Errorf("Generated workflow should contain process_safe_outputs step (handler manager)") + } + + // Verify that push_to_pull_request_branch config is in handler manager config + if !strings.Contains(lockContentStr, "push_to_pull_request_branch") { + t.Errorf("Generated workflow should contain push_to_pull_request_branch in handler config") } // Verify conditional execution using BuildSafeOutputType @@ -336,9 +345,9 @@ This workflow fails when there are no changes. lockContentStr := string(lockContent) - // Verify that if-no-changes configuration is passed correctly - if !strings.Contains(lockContentStr, "GH_AW_PUSH_IF_NO_CHANGES: \"error\"") { - t.Errorf("Generated workflow should contain if-no-changes configuration") + // Verify that if-no-changes configuration is in handler config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `if_no_changes`) || !strings.Contains(lockContentStr, `error`) { + t.Errorf("Generated workflow should contain if-no-changes:error configuration in handler config JSON") } } @@ -383,9 +392,9 @@ This workflow ignores when there are no changes. lockContentStr := string(lockContent) - // Verify that if-no-changes configuration is passed correctly - if !strings.Contains(lockContentStr, "GH_AW_PUSH_IF_NO_CHANGES: \"ignore\"") { - t.Errorf("Generated workflow should contain if-no-changes ignore configuration") + // Verify that if-no-changes configuration is in handler config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `if_no_changes`) || !strings.Contains(lockContentStr, `ignore`) { + t.Errorf("Generated workflow should contain if-no-changes:ignore configuration in handler config JSON") } } @@ -429,9 +438,9 @@ This workflow uses default if-no-changes behavior. lockContentStr := string(lockContent) - // Verify that default if-no-changes configuration ("warn") is passed correctly - if !strings.Contains(lockContentStr, "GH_AW_PUSH_IF_NO_CHANGES: \"warn\"") { - t.Errorf("Generated workflow should contain default if-no-changes configuration (warn)") + // Verify that default if-no-changes configuration ("warn") is in handler config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `if_no_changes`) || !strings.Contains(lockContentStr, `warn`) { + t.Errorf("Generated workflow should contain if-no-changes:warn configuration in handler config JSON") } } @@ -481,9 +490,9 @@ This workflow explicitly sets branch to "triggering". t.Errorf("Generated workflow should contain safe_outputs job with push_to_pull_request_branch step") } - // Verify that target configuration is included - if !strings.Contains(lockContentStr, `id: push_to_pull_request_branch`) { - t.Errorf("Generated workflow should contain target configuration") + // Verify that push_to_pull_request_branch is handled by handler manager and has target configuration + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `target`) { + t.Errorf("Generated workflow should contain push_to_pull_request_branch with target configuration in handler config") } } @@ -529,9 +538,9 @@ This workflow validates PR title prefix. lockContentStr := string(lockContent) - // Verify that title prefix configuration is passed correctly - if !strings.Contains(lockContentStr, `GH_AW_PR_TITLE_PREFIX: "[bot] "`) { - t.Errorf("Generated workflow should contain title prefix configuration") + // Verify that title prefix configuration is in handler config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `title_prefix`) || !strings.Contains(lockContentStr, `[bot]`) { + t.Errorf("Generated workflow should contain title_prefix:[bot] configuration in handler config JSON") } } @@ -577,9 +586,9 @@ This workflow validates PR labels. lockContentStr := string(lockContent) - // Verify that labels configuration is passed correctly - if !strings.Contains(lockContentStr, `GH_AW_PR_LABELS: "automated,enhancement"`) { - t.Errorf("Generated workflow should contain labels configuration") + // Verify that labels configuration is in handler config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `labels`) || (!strings.Contains(lockContentStr, `automated`) && !strings.Contains(lockContentStr, `enhancement`)) { + t.Errorf("Generated workflow should contain labels configuration in handler config JSON") } } @@ -626,12 +635,12 @@ This workflow validates both PR title prefix and labels. lockContentStr := string(lockContent) - // Verify that both title prefix and labels configurations are passed correctly - if !strings.Contains(lockContentStr, `GH_AW_PR_TITLE_PREFIX: "[automated] "`) { - t.Errorf("Generated workflow should contain title prefix configuration") + // Verify that both title prefix and labels configurations are in handler manager config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `title_prefix`) || !strings.Contains(lockContentStr, `[automated]`) { + t.Errorf("Generated workflow should contain title_prefix:[automated] in handler config JSON") } - if !strings.Contains(lockContentStr, `GH_AW_PR_LABELS: "bot,feature,enhancement"`) { - t.Errorf("Generated workflow should contain labels configuration") + if !strings.Contains(lockContentStr, `labels`) || (!strings.Contains(lockContentStr, `bot`) && !strings.Contains(lockContentStr, `feature`) && !strings.Contains(lockContentStr, `enhancement`)) { + t.Errorf("Generated workflow should contain labels (bot,feature,enhancement) in handler config JSON") } } @@ -677,9 +686,9 @@ This workflow appends a suffix to commit titles. lockContentStr := string(lockContent) - // Verify that commit title suffix configuration is passed correctly - if !strings.Contains(lockContentStr, `GH_AW_COMMIT_TITLE_SUFFIX: " [skip ci]"`) { - t.Errorf("Generated workflow should contain commit title suffix configuration") + // Verify that commit title suffix configuration is in handler manager config JSON (check for push_to_pull_request_branch config) + if !strings.Contains(lockContentStr, `push_to_pull_request_branch`) || !strings.Contains(lockContentStr, `commit_title_suffix`) || !strings.Contains(lockContentStr, `[skip ci]`) { + t.Errorf("Generated workflow should contain commit_title_suffix:[skip ci] in handler config JSON") } } @@ -729,9 +738,9 @@ since it's not supported by actions/github-script. t.Errorf("Generated workflow should contain safe_outputs job") } - // Verify that push_to_pull_request_branch step is present - if !strings.Contains(lockContentStr, "id: push_to_pull_request_branch") { - t.Errorf("Generated workflow should contain push_to_pull_request_branch step") + // Verify that push_to_pull_request_branch is handled by handler manager + if !strings.Contains(lockContentStr, "id: process_safe_outputs") { + t.Errorf("Generated workflow should contain process_safe_outputs step (handler manager)") } // Verify that working-directory is NOT present (not supported by actions/github-script) @@ -866,9 +875,9 @@ This test verifies that the aw.patch artifact is downloaded in the safe_outputs t.Errorf("Expected patch artifact to be downloaded to '/tmp/gh-aw/'") } - // Verify that the push step exists and references the patch file - if !strings.Contains(lockContentStr, "- name: Push To Pull Request Branch") { - t.Errorf("Expected 'Push To Pull Request Branch' step in safe_outputs job") + // Verify that the push step is handled by handler manager + if !strings.Contains(lockContentStr, "- name: Process Safe Outputs") { + t.Errorf("Expected 'Process Safe Outputs' step (handler manager) in safe_outputs job") } // Verify that the condition checks for push_to_pull_request_branch output type diff --git a/test_pr.md b/test_pr.md new file mode 100644 index 00000000000..b737025bb9a --- /dev/null +++ b/test_pr.md @@ -0,0 +1,8 @@ +--- +engine: copilot +safe-outputs: + create-pull-request: + max: 1 +--- + +Test