From e545dbba3de6013cd696147b5c706ecff437d83c Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 23 Feb 2026 00:56:38 +0000 Subject: [PATCH 1/4] trigger token for CI --- .github/workflows/changeset.lock.yml | 1 + .github/workflows/ci-coach.lock.yml | 1 + .github/workflows/cloclo.lock.yml | 1 + .../workflows/code-scanning-fixer.lock.yml | 1 + .github/workflows/code-simplifier.lock.yml | 1 + .github/workflows/craft.lock.yml | 1 + .github/workflows/daily-doc-updater.lock.yml | 1 + .../daily-rendering-scripts-verifier.lock.yml | 1 + .../workflows/daily-workflow-updater.lock.yml | 1 + .../developer-docs-consolidator.lock.yml | 1 + .github/workflows/dictation-prompt.lock.yml | 1 + .../workflows/functional-pragmatist.lock.yml | 1 + .../github-mcp-tools-report.lock.yml | 1 + .../workflows/glossary-maintainer.lock.yml | 1 + .github/workflows/go-logger.lock.yml | 1 + .github/workflows/hourly-ci-cleaner.lock.yml | 1 + .../workflows/instructions-janitor.lock.yml | 1 + .github/workflows/jsweep.lock.yml | 1 + .../workflows/layout-spec-maintainer.lock.yml | 1 + .github/workflows/mergefest.lock.yml | 1 + .github/workflows/poem-bot.lock.yml | 1 + .github/workflows/q.lock.yml | 1 + .github/workflows/refiner.lock.yml | 1 + .../workflows/slide-deck-maintainer.lock.yml | 1 + .github/workflows/smoke-claude.lock.yml | 1 + .github/workflows/smoke-multi-pr.lock.yml | 1 + .github/workflows/smoke-project.lock.yml | 1 + .../workflows/technical-doc-writer.lock.yml | 1 + .../test-create-pr-error-handling.lock.yml | 1 + .github/workflows/tidy.lock.yml | 1 + .../workflows/ubuntu-image-analyzer.lock.yml | 1 + .github/workflows/unbloat-docs.lock.yml | 1 + .../weekly-editors-health-check.lock.yml | 1 + .../weekly-safe-outputs-spec-review.lock.yml | 1 + actions/setup/js/create_pull_request.cjs | 12 ++++ .../setup/js/push_to_pull_request_branch.cjs | 14 ++++ docs/src/content/docs/reference/auth.mdx | 64 +++++++++++++++++++ docs/src/content/docs/reference/faq.md | 20 ++++-- .../content/docs/reference/safe-outputs.md | 18 ++++++ pkg/parser/schemas/main_workflow_schema.json | 8 +++ pkg/workflow/compiler_safe_outputs_job.go | 24 +++++++ pkg/workflow/create_pull_request.go | 40 ++++++++---- pkg/workflow/push_to_pull_request_branch.go | 19 ++++-- 43 files changed, 232 insertions(+), 21 deletions(-) diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index b7d1b401f14..7cbdb2262b7 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -1167,6 +1167,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "codex" GH_AW_ENGINE_MODEL: "gpt-5.1-codex-mini" GH_AW_WORKFLOW_ID: "changeset" diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index fd2a7d72b96..68265ee59c6 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -1153,6 +1153,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "ci-coach-daily" GH_AW_WORKFLOW_ID: "ci-coach" diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 14e31470502..33a526007a7 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -1508,6 +1508,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐ŸŽค *Magnifique! Performance by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐ŸŽต Comme d'habitude! [{workflow_name}]({run_url}) takes the stage on this {event_type}...\",\"runSuccess\":\"๐ŸŽค Bravo! [{workflow_name}]({run_url}) has delivered a stunning performance! Standing ovation! ๐ŸŒŸ\",\"runFailure\":\"๐ŸŽต Intermission... [{workflow_name}]({run_url}) {status}. The show must go on... eventually!\"}" GH_AW_WORKFLOW_ID: "cloclo" diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index ed4b6e01db6..8e81ce80f44 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -1267,6 +1267,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "code-scanning-fixer" GH_AW_WORKFLOW_NAME: "Code Scanning Fixer" diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index c30567f096f..58d45dce15b 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -1124,6 +1124,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "code-simplifier" GH_AW_WORKFLOW_ID: "code-simplifier" diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 4ff1a3d405f..0527eb765e9 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -1163,6 +1163,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e โš’๏ธ *Crafted with care by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ› ๏ธ Master Crafter at work! [{workflow_name}]({run_url}) is forging a new workflow on this {event_type}...\",\"runSuccess\":\"โš’๏ธ Masterpiece complete! [{workflow_name}]({run_url}) has crafted your workflow. May it serve you well! ๐ŸŽ–๏ธ\",\"runFailure\":\"๐Ÿ› ๏ธ Forge cooling down! [{workflow_name}]({run_url}) {status}. The anvil awaits another attempt...\"}" GH_AW_WORKFLOW_ID: "craft" diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 0c94fc81111..826d1c7e2fd 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -1178,6 +1178,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_TRACKER_ID: "daily-doc-updater" GH_AW_WORKFLOW_ID: "daily-doc-updater" diff --git a/.github/workflows/daily-rendering-scripts-verifier.lock.yml b/.github/workflows/daily-rendering-scripts-verifier.lock.yml index eced6d180f9..2291c7543e8 100644 --- a/.github/workflows/daily-rendering-scripts-verifier.lock.yml +++ b/.github/workflows/daily-rendering-scripts-verifier.lock.yml @@ -1308,6 +1308,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_TRACKER_ID: "daily-rendering-scripts-verifier" GH_AW_WORKFLOW_ID: "daily-rendering-scripts-verifier" diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index 5d9435719f6..e598ac76c5c 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -1066,6 +1066,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "daily-workflow-updater" GH_AW_WORKFLOW_ID: "daily-workflow-updater" diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 1cd0de05f97..343f992d56d 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -1255,6 +1255,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "developer-docs-consolidator" GH_AW_WORKFLOW_NAME: "Developer Documentation Consolidator" diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index a5f7e2a1200..63d1b31ec63 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -1067,6 +1067,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "dictation-prompt" GH_AW_WORKFLOW_NAME: "Dictation Prompt Generator" diff --git a/.github/workflows/functional-pragmatist.lock.yml b/.github/workflows/functional-pragmatist.lock.yml index 65bf9aec941..57845657ffe 100644 --- a/.github/workflows/functional-pragmatist.lock.yml +++ b/.github/workflows/functional-pragmatist.lock.yml @@ -1074,6 +1074,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "functional-pragmatist" GH_AW_WORKFLOW_ID: "functional-pragmatist" diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 755ec3d332d..0a4d5c94067 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -1216,6 +1216,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "github-mcp-tools-report" GH_AW_WORKFLOW_NAME: "GitHub MCP Remote Server Tools Report Generator" diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 0173c9179b1..0bb9e65b6ab 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1143,6 +1143,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "glossary-maintainer" GH_AW_WORKFLOW_NAME: "Glossary Maintainer" diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index 3df49002133..fce764769ce 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -1343,6 +1343,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "go-logger" GH_AW_WORKFLOW_NAME: "Go Logger Enhancement" diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index 4a49fe00be4..9109c243394 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -1173,6 +1173,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "hourly-ci-cleaner" GH_AW_WORKFLOW_ID: "hourly-ci-cleaner" diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index 76081134fd3..92fd8bcb84b 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -1171,6 +1171,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "instructions-janitor" GH_AW_WORKFLOW_NAME: "Instructions Janitor" diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index b13d4f5c14e..ccf00b1ea79 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -1110,6 +1110,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "jsweep-daily" GH_AW_WORKFLOW_ID: "jsweep" diff --git a/.github/workflows/layout-spec-maintainer.lock.yml b/.github/workflows/layout-spec-maintainer.lock.yml index 028b1036d60..38c1d493c51 100644 --- a/.github/workflows/layout-spec-maintainer.lock.yml +++ b/.github/workflows/layout-spec-maintainer.lock.yml @@ -1103,6 +1103,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "layout-spec-maintainer" GH_AW_WORKFLOW_ID: "layout-spec-maintainer" diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index 1a5067515cf..ad91123f7d1 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -1153,6 +1153,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "mergefest" GH_AW_WORKFLOW_NAME: "Mergefest" diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 17d29959fa1..68f789a41aa 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1831,6 +1831,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: "gpt-5" GH_AW_SAFE_OUTPUTS_STAGED: "true" diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index b59eefeb5cf..bafbe47d45c 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1355,6 +1355,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐ŸŽฉ *Equipped by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ”ง Pay attention, 007! [{workflow_name}]({run_url}) is preparing your gadgets for this {event_type}...\",\"runSuccess\":\"๐ŸŽฉ Mission equipment ready! [{workflow_name}]({run_url}) has optimized your workflow. Use wisely, 007! ๐Ÿ”ซ\",\"runFailure\":\"๐Ÿ”ง Technical difficulties! [{workflow_name}]({run_url}) {status}. Even Q Branch has bad days...\"}" GH_AW_WORKFLOW_ID: "q" diff --git a/.github/workflows/refiner.lock.yml b/.github/workflows/refiner.lock.yml index 0b77b88977d..ee2b94232f8 100644 --- a/.github/workflows/refiner.lock.yml +++ b/.github/workflows/refiner.lock.yml @@ -1166,6 +1166,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"runStarted\":\"๐Ÿ” Starting code refinement... [{workflow_name}]({run_url}) is analyzing PR #${{ github.event.pull_request.number }} for style alignment and security issues\",\"runSuccess\":\"โœ… Refinement complete! [{workflow_name}]({run_url}) has created a PR with improvements for PR #${{ github.event.pull_request.number }}\",\"runFailure\":\"โŒ Refinement failed! [{workflow_name}]({run_url}) {status} while processing PR #${{ github.event.pull_request.number }}\"}" GH_AW_WORKFLOW_ID: "refiner" diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 721d318b4dc..46eacbb31fa 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -1213,6 +1213,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "slide-deck-maintainer" GH_AW_WORKFLOW_ID: "slide-deck-maintainer" diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 95c1a9e3c01..3369bb49ab0 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2723,6 +2723,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ’ฅ *[THE END] โ€” Illustrated by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ’ฅ **WHOOSH!** [{workflow_name}]({run_url}) springs into action on this {event_type}! *[Panel 1 begins...]*\",\"runSuccess\":\"๐ŸŽฌ **THE END** โ€” [{workflow_name}]({run_url}) **MISSION: ACCOMPLISHED!** The hero saves the day! โœจ\",\"runFailure\":\"๐Ÿ’ซ **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges...\"}" GH_AW_WORKFLOW_ID: "smoke-claude" diff --git a/.github/workflows/smoke-multi-pr.lock.yml b/.github/workflows/smoke-multi-pr.lock.yml index 5e1e770c114..1b7d0b25e43 100644 --- a/.github/workflows/smoke-multi-pr.lock.yml +++ b/.github/workflows/smoke-multi-pr.lock.yml @@ -1232,6 +1232,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Multi PR smoke test by [{workflow_name}]({run_url})*\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing multiple PR creation...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) successfully created multiple PRs.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) failed to create multiple PRs. Check the logs.\"}" GH_AW_WORKFLOW_ID: "smoke-multi-pr" diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 3b0b40b9996..7532ed61518 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1626,6 +1626,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Project smoke test report by [{workflow_name}]({run_url})*\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing project operations...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed successfully. All project operations validated.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) encountered failures. Check the logs for details.\"}" GH_AW_WORKFLOW_ID: "smoke-project" diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 72ad03e729a..7123e2b8acf 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -1216,6 +1216,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ“ *Documentation by [{workflow_name}]({run_url})*\",\"runStarted\":\"โœ๏ธ The Technical Writer begins! [{workflow_name}]({run_url}) is documenting this {event_type}...\",\"runSuccess\":\"๐Ÿ“ Documentation complete! [{workflow_name}]({run_url}) has written the docs. Clear as crystal! โœจ\",\"runFailure\":\"โœ๏ธ Writer's block! [{workflow_name}]({run_url}) {status}. The page remains blank...\"}" GH_AW_WORKFLOW_ID: "technical-doc-writer" diff --git a/.github/workflows/test-create-pr-error-handling.lock.yml b/.github/workflows/test-create-pr-error-handling.lock.yml index d098197d066..715904bfe4b 100644 --- a/.github/workflows/test-create-pr-error-handling.lock.yml +++ b/.github/workflows/test-create-pr-error-handling.lock.yml @@ -1145,6 +1145,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "test-create-pr-error-handling" GH_AW_WORKFLOW_NAME: "Test Create PR Error Handling" diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 29ad1a4ad4e..b8a7b983b1f 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -1252,6 +1252,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "tidy" GH_AW_WORKFLOW_NAME: "Tidy" diff --git a/.github/workflows/ubuntu-image-analyzer.lock.yml b/.github/workflows/ubuntu-image-analyzer.lock.yml index 58e58b312f5..f79edc8d1d0 100644 --- a/.github/workflows/ubuntu-image-analyzer.lock.yml +++ b/.github/workflows/ubuntu-image-analyzer.lock.yml @@ -1143,6 +1143,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "ubuntu-image-analyzer" GH_AW_WORKFLOW_ID: "ubuntu-image-analyzer" diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index a86eb254342..ae766b70875 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1429,6 +1429,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ—œ๏ธ *Compressed by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ“ฆ Time to slim down! [{workflow_name}]({run_url}) is trimming the excess from this {event_type}...\",\"runSuccess\":\"๐Ÿ—œ๏ธ Docs on a diet! [{workflow_name}]({run_url}) has removed the bloat. Lean and mean! ๐Ÿ’ช\",\"runFailure\":\"๐Ÿ“ฆ Unbloating paused! [{workflow_name}]({run_url}) {status}. The docs remain... fluffy.\"}" GH_AW_WORKFLOW_ID: "unbloat-docs" diff --git a/.github/workflows/weekly-editors-health-check.lock.yml b/.github/workflows/weekly-editors-health-check.lock.yml index 4ae90d21099..d88470bc2dd 100644 --- a/.github/workflows/weekly-editors-health-check.lock.yml +++ b/.github/workflows/weekly-editors-health-check.lock.yml @@ -1145,6 +1145,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "weekly-editors-health-check" GH_AW_WORKFLOW_ID: "weekly-editors-health-check" diff --git a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml index 807b28f3d63..03bb6f270a2 100644 --- a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml +++ b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml @@ -1065,6 +1065,7 @@ jobs: pull-requests: write timeout-minutes: 15 env: + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "weekly-safe-outputs-spec-review" GH_AW_WORKFLOW_ID: "weekly-safe-outputs-spec-review" diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index f8380196438..89092bb43f2 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -18,6 +18,7 @@ const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); +const { pushCITriggerCommit } = require("./ci_trigger_commit.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -839,6 +840,17 @@ ${patchPreview}`; ) .write(); + // Push an empty CI trigger commit if a CI trigger token is configured. + // This works around the GITHUB_TOKEN limitation where pushes don't trigger CI events. + const ciTriggerResult = await pushCITriggerCommit({ + branchName, + repoOwner: repoParts.owner, + repoName: repoParts.repo, + }); + if (ciTriggerResult.success && !ciTriggerResult.skipped) { + core.info("CI trigger commit pushed - CI checks should start shortly"); + } + // Return success with PR details return { success: true, diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 47d6fdeda7d..e413811da43 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -8,6 +8,7 @@ const { updateActivationCommentWithCommit } = require("./update_activation_comme const { getErrorMessage } = require("./error_helpers.cjs"); const { replaceTemporaryIdReferences } = require("./temporary_id.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); +const { pushCITriggerCommit } = require("./ci_trigger_commit.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -402,6 +403,19 @@ async function main(config = {}) { await core.summary.addRaw(summaryContent).write(); + // Push an empty CI trigger commit if a CI trigger token is configured and changes were pushed. + // This works around the GITHUB_TOKEN limitation where pushes don't trigger CI events. + if (hasChanges) { + const ciTriggerResult = await pushCITriggerCommit({ + branchName, + repoOwner: context.repo.owner, + repoName: context.repo.repo, + }); + if (ciTriggerResult.success && !ciTriggerResult.skipped) { + core.info("CI trigger commit pushed - CI checks should start shortly"); + } + } + return { success: true, branch_name: branchName, diff --git a/docs/src/content/docs/reference/auth.mdx b/docs/src/content/docs/reference/auth.mdx index fc80de2f0b3..725762f4156 100644 --- a/docs/src/content/docs/reference/auth.mdx +++ b/docs/src/content/docs/reference/auth.mdx @@ -26,6 +26,7 @@ You will need one of the following GitHub Actions secrets configured in your rep Depending on what your workflow needs to do, you may need additional GitHub tokens added as repository secrets: - **Lockdown mode, cross-repo access, or remote GitHub tools** โ€“ Add [`GH_AW_GITHUB_TOKEN`](#gh_aw_github_token) +- **Trigger CI on PRs created by workflows** โ€“ Add [`GH_AW_CI_TRIGGER_TOKEN`](#gh_aw_ci_trigger_token) - **GitHub Projects v2 operations** โ€“ Add [`GH_AW_PROJECT_GITHUB_TOKEN`](#gh_aw_project_github_token) - **Assign Copilot coding agent to issues/PRs** โ€“ Add [`GH_AW_AGENT_TOKEN`](#gh_aw_agent_token) - **MCP server with special permissions** โ€“ Add [`GH_AW_GITHUB_MCP_SERVER_TOKEN`](#gh_aw_github_mcp_server_token) @@ -193,6 +194,69 @@ gh aw secrets set GH_AW_GITHUB_MCP_SERVER_TOKEN --value "YOUR_MCP_PAT" --- +### `GH_AW_CI_TRIGGER_TOKEN` + +A Personal Access Token (PAT) or GitHub App token used to push an empty commit after PR creation or branch push, triggering CI events that the default `GITHUB_TOKEN` cannot trigger. + +Pull requests and pushes made with `GITHUB_TOKEN` do not trigger `pull_request`, `push`, or `pull_request_target` events. This is a [GitHub Actions security feature](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) to prevent recursive workflows. The `GH_AW_CI_TRIGGER_TOKEN` provides a minimal workaround: after the safe output creates the PR or pushes, an additional empty commit is pushed using this token, which triggers CI. + +**When to use**: + +- You need CI checks to run on PRs created by `create-pull-request` safe outputs. +- You need CI checks to run after `push-to-pull-request-branch` pushes. +- You want a global default CI trigger token for all safe outputs without configuring each one individually. + +**Setup**: + +Create a [fine-grained PAT](https://github.com/settings/personal-access-tokens/new) with: + +- **Repository access**: Select specific repos that run agentic workflows +- **Repository permissions**: + - Contents: Read & Write (required to push commits) + +Then add to repository secrets: + +```bash wrap +gh aw secrets set GH_AW_CI_TRIGGER_TOKEN --value "YOUR_CI_TRIGGER_PAT" +``` + +**Per-safe-output override**: + +You can also configure the token per safe output using `github-ci-trigger-token` in the workflow frontmatter, which takes precedence over the `GH_AW_CI_TRIGGER_TOKEN` secret: + +```yaml wrap +safe-outputs: + create-pull-request: + github-ci-trigger-token: ${{ secrets.MY_CUSTOM_CI_TOKEN }} +``` + +Or use `app` to use a GitHub App installation token: + +```yaml wrap +safe-outputs: + create-pull-request: + github-ci-trigger-token: app +``` + +**Token precedence**: per-safe-output `github-ci-trigger-token` โ†’ `GH_AW_CI_TRIGGER_TOKEN` secret. + +**Three ways to configure CI triggering:** + +| Method | Configuration | Description | +|--------|--------------|-------------| +| **Explicit token** | `github-ci-trigger-token: ${{ secrets.CI_TOKEN }}` | Use a specific PAT or token per safe output | +| **GitHub App** | `github-ci-trigger-token: app` | Use the configured GitHub App installation token | +| **Implicit secret** | Set `GH_AW_CI_TRIGGER_TOKEN` repository secret | No workflow config needed โ€” checked automatically | + +The implicit `GH_AW_CI_TRIGGER_TOKEN` secret is checked automatically when `create-pull-request` or `push-to-pull-request-branch` is configured, even without explicit `github-ci-trigger-token` in the workflow. Just add the secret to your repository. + +> [!TIP] +> Use a fine-grained PAT with `contents: write` permission scoped to your repository. This is the minimum permission needed to push commits. + +See [Triggering CI on Created Pull Requests](/gh-aw/reference/safe-outputs/#triggering-ci-on-created-pull-requests) for behavioral details. + +--- + ### `GH_AW_PROJECT_GITHUB_TOKEN` If using GitHub Projects v2 operations, you need to configure this GitHub Actions secret with a Personal Access Token (PAT) that has the appropriate scopes and permissions for Projects access. diff --git a/docs/src/content/docs/reference/faq.md b/docs/src/content/docs/reference/faq.md index 1954f067dd5..6e118795eed 100644 --- a/docs/src/content/docs/reference/faq.md +++ b/docs/src/content/docs/reference/faq.md @@ -303,13 +303,25 @@ This is expected GitHub Actions security behavior. Pull requests created using t GitHub Actions prevents the `GITHUB_TOKEN` from triggering new workflow runs to avoid infinite loops and uncontrolled automation chains. Without this protection, a workflow could create a PR, which triggers another workflow, which creates another PR, and so on indefinitely. -If you need CI checks to run on PRs created by agentic workflows, you have three options: +If you need CI checks to run on PRs created by agentic workflows, you have several options: -**Option 1: Use different authorization** +**Option 1: Use `github-ci-trigger-token` (Recommended)** -Configure your [`create-pull-request` safe output](/gh-aw/reference/safe-outputs/#pull-request-creation-create-pull-request) to use a PAT or a GitHub App. This allows PR creation to trigger CI workflows. +Add a `github-ci-trigger-token` to your `create-pull-request` or `push-to-pull-request-branch` safe output. This pushes an empty commit using a different token after PR creation/push, which triggers CI events without changing the overall PR authorization. -**Option 2: Use workflow_run trigger** +```yaml wrap +safe-outputs: + create-pull-request: + github-ci-trigger-token: ${{ secrets.CI_TRIGGER_PAT }} +``` + +You can also use `app` to use a GitHub App token, or set the `GH_AW_CI_TRIGGER_TOKEN` repository secret for a global default. See [Triggering CI on Created Pull Requests](/gh-aw/reference/safe-outputs/#triggering-ci-on-created-pull-requests) for details. + +**Option 2: Use different authorization for the entire safe output** + +Configure your [`create-pull-request` safe output](/gh-aw/reference/safe-outputs/#pull-request-creation-create-pull-request) to use a PAT or a GitHub App for all operations. This allows PR creation to trigger CI workflows, but changes the authorization for the entire PR creation process. + +**Option 3: Use workflow_run trigger** Configure your CI workflows to run on `workflow_run` events, which allows them to react to completed workflows: diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 004620662b5..074e31bb2a1 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -687,6 +687,7 @@ safe-outputs: base-branch: "vnext" # target branch for PR (default: github.base_ref || github.ref_name) fallback-as-issue: false # disable issue fallback (default: true) github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions + github-ci-trigger-token: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI ``` The `base-branch` field specifies which branch the pull request should target. This is particularly useful for cross-repository PRs where you need to target non-default branches (e.g., `vnext`, `release/v1.0`, `staging`). When not specified, defaults to `github.base_ref` (the PR's target branch) with a fallback to `github.ref_name` (the workflow's branch) for push events. @@ -707,6 +708,20 @@ safe-outputs: When `create-pull-request` is configured, git commands (`checkout`, `branch`, `switch`, `add`, `rm`, `commit`, `merge`) are automatically enabled. +#### Triggering CI on Created Pull Requests + +By default, pull requests created using `GITHUB_TOKEN` **do not trigger CI workflow runs** (this is a [GitHub Actions security feature](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) to prevent event cascades). To trigger CI checks on PRs created by agentic workflows, configure a CI trigger token: + +```yaml wrap +safe-outputs: + create-pull-request: + github-ci-trigger-token: ${{ secrets.CI_TOKEN }} # PAT or token to trigger CI +``` + +When configured, an empty commit is pushed to the PR branch using the specified token after PR creation. Since this push comes from a different authentication context, it triggers `push` and `pull_request` events normally. + +See [`GH_AW_CI_TRIGGER_TOKEN`](/gh-aw/reference/auth/#gh_aw_ci_trigger_token) for token setup, configuration methods (explicit token, GitHub App, or implicit secret), and required permissions. + ### Close Pull Request (`close-pull-request:`) Closes PRs without merging with optional comment. Filter by labels and title prefix. Target: `"triggering"` (PR event), `"*"` (any), or number. @@ -833,10 +848,13 @@ safe-outputs: max: 3 # max pushes per run (default: 1) if-no-changes: "warn" # "warn" (default), "error", or "ignore" github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions + github-ci-trigger-token: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI ``` When `push-to-pull-request-branch` is configured, git commands (`checkout`, `branch`, `switch`, `add`, `rm`, `commit`, `merge`) are automatically enabled. +Like `create-pull-request`, pushes made with `GITHUB_TOKEN` do not trigger CI events. Use `github-ci-trigger-token` or the [`GH_AW_CI_TRIGGER_TOKEN`](/gh-aw/reference/auth/#gh_aw_ci_trigger_token) secret to trigger CI after pushing. See [Triggering CI on Created Pull Requests](#triggering-ci-on-created-pull-requests) for details. + #### Fail-Fast on Code Push Failure If `push-to-pull-request-branch` (or `create-pull-request`) fails, the safe-output pipeline cancels all remaining non-code-push outputs. Each cancelled output is marked with an explicit reason such as "Cancelled: code push operation failed". The failure details appear in the agent failure issue or comment generated by the conclusion job. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index cecb12b4ced..ba9f3713aa3 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5119,6 +5119,10 @@ "type": "boolean", "description": "Controls the fallback behavior when pull request creation fails. When true (default), an issue is created as a fallback with the patch content. When false, no issue is created and the workflow fails with an error. Setting to false also removes the issues:write permission requirement.", "default": true + }, + "github-ci-trigger-token": { + "type": "string", + "description": "Token used to push an empty commit after PR creation to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}'), 'app' for GitHub App auth, or set the GH_AW_CI_TRIGGER_TOKEN repository secret for implicit usage." } }, "additionalProperties": false, @@ -6096,6 +6100,10 @@ "type": "boolean", "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", "examples": [true, false] + }, + "github-ci-trigger-token": { + "type": "string", + "description": "Token used to push an empty commit after pushing changes to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}'), 'app' for GitHub App auth, or set the GH_AW_CI_TRIGGER_TOKEN repository secret for implicit usage." } }, "additionalProperties": false diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 58800ac8589..bdcddb8603b 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -405,6 +405,30 @@ func (c *Compiler) buildJobLevelSafeOutputEnvVars(data *WorkflowData, workflowID } } + // Add CI trigger token if configured on create-pull-request or push-to-pull-request-branch. + // This token is used to push an empty commit after code changes to trigger CI events, + // working around the GITHUB_TOKEN limitation where events don't trigger other workflows. + // Precedence: per-safe-output github-ci-trigger-token > GH_AW_CI_TRIGGER_TOKEN secret + if data.SafeOutputs != nil { + var ciTriggerToken string + if data.SafeOutputs.CreatePullRequests != nil && data.SafeOutputs.CreatePullRequests.GithubCITriggerToken != "" { + ciTriggerToken = data.SafeOutputs.CreatePullRequests.GithubCITriggerToken + } else if data.SafeOutputs.PushToPullRequestBranch != nil && data.SafeOutputs.PushToPullRequestBranch.GithubCITriggerToken != "" { + ciTriggerToken = data.SafeOutputs.PushToPullRequestBranch.GithubCITriggerToken + } + + if ciTriggerToken == "app" { + envVars["GH_AW_CI_TRIGGER_TOKEN"] = "${{ steps.safe-outputs-app-token.outputs.token || '' }}" + consolidatedSafeOutputsJobLog.Print("CI trigger using GitHub App token") + } else if ciTriggerToken != "" { + envVars["GH_AW_CI_TRIGGER_TOKEN"] = ciTriggerToken + consolidatedSafeOutputsJobLog.Print("CI trigger using explicit token") + } else if data.SafeOutputs.CreatePullRequests != nil || data.SafeOutputs.PushToPullRequestBranch != nil { + // Fall back to GH_AW_CI_TRIGGER_TOKEN secret if set (implicit mode) + envVars["GH_AW_CI_TRIGGER_TOKEN"] = "${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }}" + } + } + // Note: Asset upload configuration is not needed here because upload_assets // is now handled as a separate job (see buildUploadAssetsJob) diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index bc4e4df6a53..2811eb4da32 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -23,18 +23,19 @@ type CreatePullRequestsConfig struct { BaseSafeOutputConfig `yaml:",inline"` TitlePrefix string `yaml:"title-prefix,omitempty"` Labels []string `yaml:"labels,omitempty"` - AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). - Reviewers []string `yaml:"reviewers,omitempty"` // List of users/bots to assign as reviewers to the pull request - Draft *string `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil), literal bool, and expression values - IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn" (default), "error", or "ignore" - AllowEmpty *string `yaml:"allow-empty,omitempty"` // Allow creating PR without patch file or with empty patch (useful for preparing feature branches) - TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository pull requests - AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that pull requests can be created in (additionally to the target-repo) - Expires int `yaml:"expires,omitempty"` // Hours until the pull request expires and should be automatically closed (only for same-repo PRs) - AutoMerge *string `yaml:"auto-merge,omitempty"` // Enable auto-merge for the pull request when all required checks pass - BaseBranch string `yaml:"base-branch,omitempty"` // Base branch for the pull request (defaults to github.ref_name if not specified) - Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. - FallbackAsIssue *bool `yaml:"fallback-as-issue,omitempty"` // When true (default), creates an issue if PR creation fails. When false, no fallback occurs and issues: write permission is not requested. + AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). + Reviewers []string `yaml:"reviewers,omitempty"` // List of users/bots to assign as reviewers to the pull request + Draft *string `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil), literal bool, and expression values + IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn" (default), "error", or "ignore" + AllowEmpty *string `yaml:"allow-empty,omitempty"` // Allow creating PR without patch file or with empty patch (useful for preparing feature branches) + TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository pull requests + AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that pull requests can be created in (additionally to the target-repo) + Expires int `yaml:"expires,omitempty"` // Hours until the pull request expires and should be automatically closed (only for same-repo PRs) + AutoMerge *string `yaml:"auto-merge,omitempty"` // Enable auto-merge for the pull request when all required checks pass + BaseBranch string `yaml:"base-branch,omitempty"` // Base branch for the pull request (defaults to github.ref_name if not specified) + Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. + FallbackAsIssue *bool `yaml:"fallback-as-issue,omitempty"` // When true (default), creates an issue if PR creation fails. When false, no fallback occurs and issues: write permission is not requested. + GithubCITriggerToken string `yaml:"github-ci-trigger-token,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT, "app" for GitHub App auth, or set GH_AW_CI_TRIGGER_TOKEN secret. } // buildCreateOutputPullRequestJob creates the create_pull_request job @@ -164,6 +165,21 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa createPRLog.Print("Footer disabled - XML markers will be included but visible footer content will be omitted") } + // Add CI trigger token if configured (for pushing an empty commit to trigger CI) + ciTriggerToken := data.SafeOutputs.CreatePullRequests.GithubCITriggerToken + if ciTriggerToken != "" { + if ciTriggerToken == "app" { + customEnvVars = append(customEnvVars, " GH_AW_CI_TRIGGER_TOKEN: ${{ steps.safe-outputs-app-token.outputs.token || '' }}\n") + createPRLog.Print("CI trigger using GitHub App token") + } else { + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_CI_TRIGGER_TOKEN: %s\n", ciTriggerToken)) + createPRLog.Printf("CI trigger using explicit token") + } + } else { + // Fall back to GH_AW_CI_TRIGGER_TOKEN secret if set + customEnvVars = append(customEnvVars, " GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }}\n") + } + // Add standard environment variables (metadata + staged/target repo) customEnvVars = append(customEnvVars, c.buildStandardSafeOutputEnvVars(data, data.SafeOutputs.CreatePullRequests.TargetRepoSlug)...) diff --git a/pkg/workflow/push_to_pull_request_branch.go b/pkg/workflow/push_to_pull_request_branch.go index 67f7acc0974..c1f4d280c2e 100644 --- a/pkg/workflow/push_to_pull_request_branch.go +++ b/pkg/workflow/push_to_pull_request_branch.go @@ -12,11 +12,12 @@ var pushToPullRequestBranchLog = logger.New("workflow:push_to_pull_request_branc // PushToPullRequestBranchConfig holds configuration for pushing changes to a specific branch from agent output type PushToPullRequestBranchConfig struct { BaseSafeOutputConfig `yaml:",inline"` - Target string `yaml:"target,omitempty"` // Target for push-to-pull-request-branch: like add-comment but for pull requests - TitlePrefix string `yaml:"title-prefix,omitempty"` // Required title prefix for pull request validation - Labels []string `yaml:"labels,omitempty"` // Required labels for pull request validation - IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") - CommitTitleSuffix string `yaml:"commit-title-suffix,omitempty"` // Optional suffix to append to generated commit titles + Target string `yaml:"target,omitempty"` // Target for push-to-pull-request-branch: like add-comment but for pull requests + TitlePrefix string `yaml:"title-prefix,omitempty"` // Required title prefix for pull request validation + Labels []string `yaml:"labels,omitempty"` // Required labels for pull request validation + IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") + CommitTitleSuffix string `yaml:"commit-title-suffix,omitempty"` // Optional suffix to append to generated commit titles + GithubCITriggerToken string `yaml:"github-ci-trigger-token,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT, "app" for GitHub App auth, or set GH_AW_CI_TRIGGER_TOKEN secret. } // buildCheckoutRepository generates a checkout step with optional target repository and custom token @@ -117,6 +118,14 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any) } } + // Parse github-ci-trigger-token (optional) - token for pushing empty commit to trigger CI + if ciTriggerToken, exists := configMap["github-ci-trigger-token"]; exists { + if ciTriggerTokenStr, ok := ciTriggerToken.(string); ok { + pushToBranchConfig.GithubCITriggerToken = ciTriggerTokenStr + pushToPullRequestBranchLog.Printf("CI trigger token configured") + } + } + // Parse common base fields with default max of 0 (no limit) c.parseBaseSafeOutputConfig(configMap, &pushToBranchConfig.BaseSafeOutputConfig, 0) } From bda175840837ebb5d956817e2c103e07b7a6c2c5 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 23 Feb 2026 00:57:57 +0000 Subject: [PATCH 2/4] trigger token for CI --- actions/setup/js/ci_trigger_commit.cjs | 82 ++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 actions/setup/js/ci_trigger_commit.cjs diff --git a/actions/setup/js/ci_trigger_commit.cjs b/actions/setup/js/ci_trigger_commit.cjs new file mode 100644 index 00000000000..20e41b1d085 --- /dev/null +++ b/actions/setup/js/ci_trigger_commit.cjs @@ -0,0 +1,82 @@ +// @ts-check +/// + +/** + * @fileoverview CI Trigger Commit Helper + * + * Pushes an empty commit to a branch using a different token to trigger CI events. + * This works around the GitHub Actions limitation where events created with + * GITHUB_TOKEN do not trigger other workflow runs. + * + * The token can come from: + * 1. Explicit `github-ci-trigger-token` in safe-outputs config (passed as GH_AW_CI_TRIGGER_TOKEN env var) + * 2. Implicit `GH_AW_CI_TRIGGER_TOKEN` repository secret + * 3. `github-ci-trigger-token: app` for GitHub App authentication + */ + +/** + * Push an empty commit to a branch using a CI trigger token. + * This commit is pushed with different authentication so that push/PR events + * are triggered for CI checks to run. + * + * @param {Object} options - Options for the CI trigger commit + * @param {string} options.branchName - The branch to push the empty commit to + * @param {string} options.repoOwner - Repository owner + * @param {string} options.repoName - Repository name + * @param {string} [options.commitMessage] - Custom commit message (default: "ci: trigger CI checks") + * @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>} + */ +async function pushCITriggerCommit({ branchName, repoOwner, repoName, commitMessage }) { + const ciTriggerToken = process.env.GH_AW_CI_TRIGGER_TOKEN; + + if (!ciTriggerToken || !ciTriggerToken.trim()) { + core.info("No CI trigger token configured - skipping CI trigger commit"); + return { success: true, skipped: true }; + } + + core.info("CI trigger token detected - pushing empty commit to trigger CI events"); + + try { + // Configure git remote with the CI trigger token for authentication + const remoteUrl = `https://x-access-token:${ciTriggerToken}@github.com/${repoOwner}/${repoName}.git`; + + // Add a temporary remote with the CI trigger token + try { + await exec.exec("git", ["remote", "remove", "ci-trigger"]); + } catch { + // Remote doesn't exist yet, that's fine + } + await exec.exec("git", ["remote", "add", "ci-trigger", remoteUrl]); + + // Create and push an empty commit + const message = commitMessage || "ci: trigger CI checks"; + await exec.exec("git", ["commit", "--allow-empty", "-m", message]); + await exec.exec("git", ["push", "ci-trigger", branchName]); + + core.info(`CI trigger commit pushed to ${branchName} successfully`); + + // Clean up the temporary remote + try { + await exec.exec("git", ["remote", "remove", "ci-trigger"]); + } catch { + // Non-fatal cleanup error + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to push CI trigger commit: ${errorMessage}`); + + // Clean up the temporary remote on failure + try { + await exec.exec("git", ["remote", "remove", "ci-trigger"]); + } catch { + // Non-fatal cleanup error + } + + // CI trigger failure is not fatal - the main push already succeeded + return { success: false, error: errorMessage }; + } +} + +module.exports = { pushCITriggerCommit }; From eb2f6b751776e21e8815eecbc2d120d6ef570fdb Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 23 Feb 2026 01:39:26 +0000 Subject: [PATCH 3/4] review adjustments --- .github/workflows/changeset.lock.yml | 1 - .github/workflows/ci-coach.lock.yml | 1 - .github/workflows/cloclo.lock.yml | 1 - .../workflows/code-scanning-fixer.lock.yml | 1 - .github/workflows/code-simplifier.lock.yml | 1 - .github/workflows/craft.lock.yml | 1 - .github/workflows/daily-doc-updater.lock.yml | 1 - .../daily-rendering-scripts-verifier.lock.yml | 1 - .../workflows/daily-workflow-updater.lock.yml | 1 - .../developer-docs-consolidator.lock.yml | 1 - .github/workflows/dictation-prompt.lock.yml | 1 - .../workflows/functional-pragmatist.lock.yml | 1 - .../github-mcp-tools-report.lock.yml | 1 - .../workflows/glossary-maintainer.lock.yml | 1 - .github/workflows/go-logger.lock.yml | 1 - .github/workflows/hourly-ci-cleaner.lock.yml | 1 - .../workflows/instructions-janitor.lock.yml | 1 - .github/workflows/jsweep.lock.yml | 1 - .../workflows/layout-spec-maintainer.lock.yml | 1 - actions/setup/js/ci_trigger_commit.cjs | 82 ---- actions/setup/js/create_pull_request.cjs | 8 +- actions/setup/js/extra_empty_commit.cjs | 119 ++++++ actions/setup/js/extra_empty_commit.test.cjs | 390 ++++++++++++++++++ .../setup/js/push_to_pull_request_branch.cjs | 8 +- docs/src/content/docs/reference/auth.mdx | 57 +-- docs/src/content/docs/reference/faq.md | 12 +- .../content/docs/reference/safe-outputs.md | 10 +- pkg/parser/schemas/main_workflow_schema.json | 8 +- pkg/workflow/compiler_safe_outputs_job.go | 28 +- pkg/workflow/create_pull_request.go | 51 ++- pkg/workflow/push_to_pull_request_branch.go | 24 +- 31 files changed, 593 insertions(+), 223 deletions(-) delete mode 100644 actions/setup/js/ci_trigger_commit.cjs create mode 100644 actions/setup/js/extra_empty_commit.cjs create mode 100644 actions/setup/js/extra_empty_commit.test.cjs diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 7cbdb2262b7..b7d1b401f14 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -1167,7 +1167,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "codex" GH_AW_ENGINE_MODEL: "gpt-5.1-codex-mini" GH_AW_WORKFLOW_ID: "changeset" diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index 68265ee59c6..fd2a7d72b96 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -1153,7 +1153,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "ci-coach-daily" GH_AW_WORKFLOW_ID: "ci-coach" diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 33a526007a7..14e31470502 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -1508,7 +1508,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐ŸŽค *Magnifique! Performance by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐ŸŽต Comme d'habitude! [{workflow_name}]({run_url}) takes the stage on this {event_type}...\",\"runSuccess\":\"๐ŸŽค Bravo! [{workflow_name}]({run_url}) has delivered a stunning performance! Standing ovation! ๐ŸŒŸ\",\"runFailure\":\"๐ŸŽต Intermission... [{workflow_name}]({run_url}) {status}. The show must go on... eventually!\"}" GH_AW_WORKFLOW_ID: "cloclo" diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 8e81ce80f44..ed4b6e01db6 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -1267,7 +1267,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "code-scanning-fixer" GH_AW_WORKFLOW_NAME: "Code Scanning Fixer" diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index 58d45dce15b..c30567f096f 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -1124,7 +1124,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "code-simplifier" GH_AW_WORKFLOW_ID: "code-simplifier" diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 0527eb765e9..4ff1a3d405f 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -1163,7 +1163,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e โš’๏ธ *Crafted with care by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ› ๏ธ Master Crafter at work! [{workflow_name}]({run_url}) is forging a new workflow on this {event_type}...\",\"runSuccess\":\"โš’๏ธ Masterpiece complete! [{workflow_name}]({run_url}) has crafted your workflow. May it serve you well! ๐ŸŽ–๏ธ\",\"runFailure\":\"๐Ÿ› ๏ธ Forge cooling down! [{workflow_name}]({run_url}) {status}. The anvil awaits another attempt...\"}" GH_AW_WORKFLOW_ID: "craft" diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 826d1c7e2fd..0c94fc81111 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -1178,7 +1178,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_TRACKER_ID: "daily-doc-updater" GH_AW_WORKFLOW_ID: "daily-doc-updater" diff --git a/.github/workflows/daily-rendering-scripts-verifier.lock.yml b/.github/workflows/daily-rendering-scripts-verifier.lock.yml index 2291c7543e8..eced6d180f9 100644 --- a/.github/workflows/daily-rendering-scripts-verifier.lock.yml +++ b/.github/workflows/daily-rendering-scripts-verifier.lock.yml @@ -1308,7 +1308,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_TRACKER_ID: "daily-rendering-scripts-verifier" GH_AW_WORKFLOW_ID: "daily-rendering-scripts-verifier" diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index e598ac76c5c..5d9435719f6 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -1066,7 +1066,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "daily-workflow-updater" GH_AW_WORKFLOW_ID: "daily-workflow-updater" diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 343f992d56d..1cd0de05f97 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -1255,7 +1255,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "developer-docs-consolidator" GH_AW_WORKFLOW_NAME: "Developer Documentation Consolidator" diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index 63d1b31ec63..a5f7e2a1200 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -1067,7 +1067,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "dictation-prompt" GH_AW_WORKFLOW_NAME: "Dictation Prompt Generator" diff --git a/.github/workflows/functional-pragmatist.lock.yml b/.github/workflows/functional-pragmatist.lock.yml index 57845657ffe..65bf9aec941 100644 --- a/.github/workflows/functional-pragmatist.lock.yml +++ b/.github/workflows/functional-pragmatist.lock.yml @@ -1074,7 +1074,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "functional-pragmatist" GH_AW_WORKFLOW_ID: "functional-pragmatist" diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 0a4d5c94067..755ec3d332d 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -1216,7 +1216,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "github-mcp-tools-report" GH_AW_WORKFLOW_NAME: "GitHub MCP Remote Server Tools Report Generator" diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 0bb9e65b6ab..0173c9179b1 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1143,7 +1143,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "glossary-maintainer" GH_AW_WORKFLOW_NAME: "Glossary Maintainer" diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index fce764769ce..3df49002133 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -1343,7 +1343,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "go-logger" GH_AW_WORKFLOW_NAME: "Go Logger Enhancement" diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index 9109c243394..4a49fe00be4 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -1173,7 +1173,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "hourly-ci-cleaner" GH_AW_WORKFLOW_ID: "hourly-ci-cleaner" diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index 92fd8bcb84b..76081134fd3 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -1171,7 +1171,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "instructions-janitor" GH_AW_WORKFLOW_NAME: "Instructions Janitor" diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index ccf00b1ea79..b13d4f5c14e 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -1110,7 +1110,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "jsweep-daily" GH_AW_WORKFLOW_ID: "jsweep" diff --git a/.github/workflows/layout-spec-maintainer.lock.yml b/.github/workflows/layout-spec-maintainer.lock.yml index 38c1d493c51..028b1036d60 100644 --- a/.github/workflows/layout-spec-maintainer.lock.yml +++ b/.github/workflows/layout-spec-maintainer.lock.yml @@ -1103,7 +1103,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "layout-spec-maintainer" GH_AW_WORKFLOW_ID: "layout-spec-maintainer" diff --git a/actions/setup/js/ci_trigger_commit.cjs b/actions/setup/js/ci_trigger_commit.cjs deleted file mode 100644 index 20e41b1d085..00000000000 --- a/actions/setup/js/ci_trigger_commit.cjs +++ /dev/null @@ -1,82 +0,0 @@ -// @ts-check -/// - -/** - * @fileoverview CI Trigger Commit Helper - * - * Pushes an empty commit to a branch using a different token to trigger CI events. - * This works around the GitHub Actions limitation where events created with - * GITHUB_TOKEN do not trigger other workflow runs. - * - * The token can come from: - * 1. Explicit `github-ci-trigger-token` in safe-outputs config (passed as GH_AW_CI_TRIGGER_TOKEN env var) - * 2. Implicit `GH_AW_CI_TRIGGER_TOKEN` repository secret - * 3. `github-ci-trigger-token: app` for GitHub App authentication - */ - -/** - * Push an empty commit to a branch using a CI trigger token. - * This commit is pushed with different authentication so that push/PR events - * are triggered for CI checks to run. - * - * @param {Object} options - Options for the CI trigger commit - * @param {string} options.branchName - The branch to push the empty commit to - * @param {string} options.repoOwner - Repository owner - * @param {string} options.repoName - Repository name - * @param {string} [options.commitMessage] - Custom commit message (default: "ci: trigger CI checks") - * @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>} - */ -async function pushCITriggerCommit({ branchName, repoOwner, repoName, commitMessage }) { - const ciTriggerToken = process.env.GH_AW_CI_TRIGGER_TOKEN; - - if (!ciTriggerToken || !ciTriggerToken.trim()) { - core.info("No CI trigger token configured - skipping CI trigger commit"); - return { success: true, skipped: true }; - } - - core.info("CI trigger token detected - pushing empty commit to trigger CI events"); - - try { - // Configure git remote with the CI trigger token for authentication - const remoteUrl = `https://x-access-token:${ciTriggerToken}@github.com/${repoOwner}/${repoName}.git`; - - // Add a temporary remote with the CI trigger token - try { - await exec.exec("git", ["remote", "remove", "ci-trigger"]); - } catch { - // Remote doesn't exist yet, that's fine - } - await exec.exec("git", ["remote", "add", "ci-trigger", remoteUrl]); - - // Create and push an empty commit - const message = commitMessage || "ci: trigger CI checks"; - await exec.exec("git", ["commit", "--allow-empty", "-m", message]); - await exec.exec("git", ["push", "ci-trigger", branchName]); - - core.info(`CI trigger commit pushed to ${branchName} successfully`); - - // Clean up the temporary remote - try { - await exec.exec("git", ["remote", "remove", "ci-trigger"]); - } catch { - // Non-fatal cleanup error - } - - return { success: true }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Failed to push CI trigger commit: ${errorMessage}`); - - // Clean up the temporary remote on failure - try { - await exec.exec("git", ["remote", "remove", "ci-trigger"]); - } catch { - // Non-fatal cleanup error - } - - // CI trigger failure is not fatal - the main push already succeeded - return { success: false, error: errorMessage }; - } -} - -module.exports = { pushCITriggerCommit }; diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 89092bb43f2..1c1bbc92756 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -18,7 +18,7 @@ const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); -const { pushCITriggerCommit } = require("./ci_trigger_commit.cjs"); +const { pushExtraEmptyCommit } = require("./extra_empty_commit.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -840,15 +840,15 @@ ${patchPreview}`; ) .write(); - // Push an empty CI trigger commit if a CI trigger token is configured. + // Push an extra empty commit if a token is configured. // This works around the GITHUB_TOKEN limitation where pushes don't trigger CI events. - const ciTriggerResult = await pushCITriggerCommit({ + const ciTriggerResult = await pushExtraEmptyCommit({ branchName, repoOwner: repoParts.owner, repoName: repoParts.repo, }); if (ciTriggerResult.success && !ciTriggerResult.skipped) { - core.info("CI trigger commit pushed - CI checks should start shortly"); + core.info("Extra empty commit pushed - CI checks should start shortly"); } // Return success with PR details diff --git a/actions/setup/js/extra_empty_commit.cjs b/actions/setup/js/extra_empty_commit.cjs new file mode 100644 index 00000000000..98dffcf30ba --- /dev/null +++ b/actions/setup/js/extra_empty_commit.cjs @@ -0,0 +1,119 @@ +// @ts-check +/// + +/** + * @fileoverview Extra Empty Commit Helper + * + * Pushes an empty commit to a branch using a different token to trigger CI events. + * This works around the GitHub Actions limitation where events created with + * GITHUB_TOKEN do not trigger other workflow runs. + * + * The token comes from `github-token-for-extra-empty-commit` in safe-outputs config + * (passed as GH_AW_EXTRA_EMPTY_COMMIT_TOKEN env var), or `app` for GitHub App authentication. + */ + +/** + * Push an empty commit to a branch using a dedicated token. + * This commit is pushed with different authentication so that push/PR events + * are triggered for CI checks to run. + * + * @param {Object} options - Options for the extra empty commit + * @param {string} options.branchName - The branch to push the empty commit to + * @param {string} options.repoOwner - Repository owner + * @param {string} options.repoName - Repository name + * @param {string} [options.commitMessage] - Custom commit message (default: "ci: trigger CI checks") + * @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>} + */ +async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage }) { + const token = process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN; + + if (!token || !token.trim()) { + core.info("No extra empty commit token configured - skipping"); + return { success: true, skipped: true }; + } + + core.info("Extra empty commit token detected - pushing empty commit to trigger CI events"); + + try { + // Cycle prevention: count empty commits in the last 60 commits on this branch. + // If 30 or more are empty, skip pushing to avoid infinite trigger loops. + const MAX_EMPTY_COMMITS = 30; + const COMMITS_TO_CHECK = 60; + let emptyCommitCount = 0; + + try { + let logOutput = ""; + // List last N commits: for each, output "COMMIT:" then changed file names. + // Empty commits will have no files listed after the hash line. + await exec.exec("git", ["log", `--max-count=${COMMITS_TO_CHECK}`, "--format=COMMIT:%H", "--name-only", "HEAD"], { + listeners: { + stdout: data => { + logOutput += data.toString(); + }, + }, + silent: true, + }); + // Split by COMMIT: markers; each chunk starts with the hash, followed by filenames + const chunks = logOutput.split("COMMIT:").filter(c => c.trim()); + for (const chunk of chunks) { + const lines = chunk.split("\n").filter(l => l.trim()); + // First line is the hash, remaining lines are changed files + if (lines.length <= 1) { + emptyCommitCount++; + } + } + } catch { + // If we can't check, default to allowing the push + emptyCommitCount = 0; + } + + if (emptyCommitCount >= MAX_EMPTY_COMMITS) { + core.warning(`Cycle prevention: found ${emptyCommitCount} empty commits in the last ${COMMITS_TO_CHECK} commits on ${branchName}. ` + `Skipping extra empty commit to avoid potential infinite loop.`); + return { success: true, skipped: true }; + } + + core.info(`Cycle check passed: ${emptyCommitCount} empty commit(s) in last ${COMMITS_TO_CHECK} (limit: ${MAX_EMPTY_COMMITS})`); + + // Configure git remote with the token for authentication + const remoteUrl = `https://x-access-token:${token}@github.com/${repoOwner}/${repoName}.git`; + + // Add a temporary remote with the token + try { + await exec.exec("git", ["remote", "remove", "ci-trigger"]); + } catch { + // Remote doesn't exist yet, that's fine + } + await exec.exec("git", ["remote", "add", "ci-trigger", remoteUrl]); + + // Create and push an empty commit + const message = commitMessage || "ci: trigger CI checks"; + await exec.exec("git", ["commit", "--allow-empty", "-m", message]); + await exec.exec("git", ["push", "ci-trigger", branchName]); + + core.info(`Extra empty commit pushed to ${branchName} successfully`); + + // Clean up the temporary remote + try { + await exec.exec("git", ["remote", "remove", "ci-trigger"]); + } catch { + // Non-fatal cleanup error + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + core.warning(`Failed to push extra empty commit: ${errorMessage}`); + + // Clean up the temporary remote on failure + try { + await exec.exec("git", ["remote", "remove", "ci-trigger"]); + } catch { + // Non-fatal cleanup error + } + + // Extra empty commit failure is not fatal - the main push already succeeded + return { success: false, error: errorMessage }; + } +} + +module.exports = { pushExtraEmptyCommit }; diff --git a/actions/setup/js/extra_empty_commit.test.cjs b/actions/setup/js/extra_empty_commit.test.cjs new file mode 100644 index 00000000000..3ba863a64b6 --- /dev/null +++ b/actions/setup/js/extra_empty_commit.test.cjs @@ -0,0 +1,390 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +describe("extra_empty_commit.cjs", () => { + let mockCore; + let mockExec; + let pushExtraEmptyCommit; + let originalEnv; + + beforeEach(() => { + originalEnv = process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN; + + mockCore = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + }; + + // Default exec mock: resolves successfully, no stdout output + mockExec = { + exec: vi.fn().mockResolvedValue(0), + }; + + global.core = mockCore; + global.exec = mockExec; + + // Clear module cache so env changes take effect + delete require.cache[require.resolve("./extra_empty_commit.cjs")]; + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN = originalEnv; + } else { + delete process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN; + } + delete global.core; + delete global.exec; + vi.clearAllMocks(); + }); + + /** + * Helper to configure the exec mock so that `git log` calls invoke the + * stdout listener with the supplied output string, while all other + * exec calls resolve normally. + */ + function mockGitLogOutput(logOutput) { + mockExec.exec.mockImplementation(async (cmd, args, options) => { + if (cmd === "git" && args && args[0] === "log" && options && options.listeners && options.listeners.stdout) { + options.listeners.stdout(Buffer.from(logOutput)); + } + return 0; + }); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Token presence + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe("when no extra empty commit token is set", () => { + it("should skip and return success with skipped=true", async () => { + delete process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN; + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true, skipped: true }); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No extra empty commit token")); + expect(mockExec.exec).not.toHaveBeenCalled(); + }); + + it("should skip when token is empty string", async () => { + process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN = ""; + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true, skipped: true }); + }); + + it("should skip when token is whitespace only", async () => { + process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN = " "; + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true, skipped: true }); + }); + }); + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Successful push (no cycle issues) + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe("when token is set and no cycle issues", () => { + beforeEach(() => { + process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN = "ghp_test_token_123"; + // Simulate git log showing 5 commits, all with file changes (non-empty) + const logOutput = ["COMMIT:aaa111", "file1.txt", "", "COMMIT:bbb222", "file2.txt", "file3.txt", "", "COMMIT:ccc333", "file4.txt", "", "COMMIT:ddd444", "file5.txt", "", "COMMIT:eee555", "file6.txt", ""].join("\n"); + mockGitLogOutput(logOutput); + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + }); + + it("should push an empty commit and return success", async () => { + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true }); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Extra empty commit token detected")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cycle check passed")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Extra empty commit pushed")); + }); + + it("should add and remove a ci-trigger remote", async () => { + await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + const execCalls = mockExec.exec.mock.calls; + // Find remote add call + const addRemote = execCalls.find(c => c[0] === "git" && c[1] && c[1][0] === "remote" && c[1][1] === "add"); + expect(addRemote).toBeDefined(); + expect(addRemote[1]).toEqual(["remote", "add", "ci-trigger", expect.stringContaining("x-access-token:ghp_test_token_123")]); + + // Find remote remove cleanup call (after push) + const removeRemoteCalls = execCalls.filter(c => c[0] === "git" && c[1] && c[1][0] === "remote" && c[1][1] === "remove"); + expect(removeRemoteCalls.length).toBeGreaterThanOrEqual(1); + }); + + it("should use default commit message when none provided", async () => { + await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + const commitCall = mockExec.exec.mock.calls.find(c => c[0] === "git" && c[1] && c[1][0] === "commit"); + expect(commitCall).toBeDefined(); + expect(commitCall[1]).toEqual(["commit", "--allow-empty", "-m", "ci: trigger CI checks"]); + }); + + it("should use custom commit message when provided", async () => { + await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + commitMessage: "chore: custom CI trigger", + }); + + const commitCall = mockExec.exec.mock.calls.find(c => c[0] === "git" && c[1] && c[1][0] === "commit"); + expect(commitCall[1]).toEqual(["commit", "--allow-empty", "-m", "chore: custom CI trigger"]); + }); + + it("should push to the correct branch", async () => { + await pushExtraEmptyCommit({ + branchName: "my-feature", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + const pushCall = mockExec.exec.mock.calls.find(c => c[0] === "git" && c[1] && c[1][0] === "push"); + expect(pushCall).toBeDefined(); + expect(pushCall[1]).toEqual(["push", "ci-trigger", "my-feature"]); + }); + }); + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Cycle prevention + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe("cycle prevention", () => { + beforeEach(() => { + process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN = "ghp_test_token_123"; + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + }); + + it("should skip when 30 or more empty commits found in last 60", async () => { + // Build git log output with 30 empty commits (hash only, no files) + const commits = []; + for (let i = 0; i < 30; i++) { + commits.push(`COMMIT:empty${i.toString().padStart(3, "0")}`); + commits.push(""); // blank line = no files + } + // Add some non-empty commits too + for (let i = 0; i < 10; i++) { + commits.push(`COMMIT:real${i.toString().padStart(3, "0")}`); + commits.push(`file${i}.txt`); + commits.push(""); + } + mockGitLogOutput(commits.join("\n")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true, skipped: true }); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Cycle prevention")); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("30 empty commits")); + + // Should NOT have pushed (no commit or push calls after the log check) + const commitCalls = mockExec.exec.mock.calls.filter(c => c[0] === "git" && c[1] && c[1][0] === "commit"); + expect(commitCalls).toHaveLength(0); + }); + + it("should allow push when fewer than 30 empty commits", async () => { + // 29 empty commits - just under the limit + const commits = []; + for (let i = 0; i < 29; i++) { + commits.push(`COMMIT:empty${i.toString().padStart(3, "0")}`); + commits.push(""); + } + for (let i = 0; i < 5; i++) { + commits.push(`COMMIT:real${i.toString().padStart(3, "0")}`); + commits.push(`file${i}.txt`); + commits.push(""); + } + mockGitLogOutput(commits.join("\n")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true }); + expect(mockCore.warning).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cycle check passed: 29 empty commit")); + }); + + it("should allow push when no commits exist (empty repo)", async () => { + mockGitLogOutput(""); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true }); + }); + + it("should allow push when all commits have file changes", async () => { + const commits = []; + for (let i = 0; i < 50; i++) { + commits.push(`COMMIT:hash${i.toString().padStart(3, "0")}`); + commits.push(`src/file${i}.go`); + commits.push(""); + } + mockGitLogOutput(commits.join("\n")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true }); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cycle check passed: 0 empty commit")); + }); + + it("should allow push if git log fails (defaults to 0 empty commits)", async () => { + // Make git log throw an error + mockExec.exec.mockImplementation(async (cmd, args, options) => { + if (cmd === "git" && args && args[0] === "log") { + throw new Error("git log failed"); + } + return 0; + }); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true }); + }); + + it("should skip at exactly 30 empty commits (boundary)", async () => { + const commits = []; + // Exactly 30 empty commits + for (let i = 0; i < 30; i++) { + commits.push(`COMMIT:empty${i.toString().padStart(3, "0")}`); + commits.push(""); + } + // 30 non-empty commits to fill up to 60 + for (let i = 0; i < 30; i++) { + commits.push(`COMMIT:real${i.toString().padStart(3, "0")}`); + commits.push(`file${i}.txt`); + commits.push(""); + } + mockGitLogOutput(commits.join("\n")); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result).toEqual({ success: true, skipped: true }); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Cycle prevention")); + }); + }); + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Error handling + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe("error handling", () => { + beforeEach(() => { + process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN = "ghp_test_token_123"; + // No empty commits in log + mockGitLogOutput("COMMIT:abc123\nfile.txt\n"); + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + }); + + it("should return error result when push fails", async () => { + let callCount = 0; + mockExec.exec.mockImplementation(async (cmd, args, options) => { + // Let git log succeed with stdout listener + if (cmd === "git" && args && args[0] === "log" && options && options.listeners) { + options.listeners.stdout(Buffer.from("COMMIT:abc123\nfile.txt\n")); + return 0; + } + // Let remote operations and commit succeed, but fail on push + if (cmd === "git" && args && args[0] === "push") { + throw new Error("authentication failed"); + } + return 0; + }); + + const result = await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + expect(result.success).toBe(false); + expect(result.error).toContain("authentication failed"); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to push extra empty commit")); + }); + + it("should clean up remote even when push fails", async () => { + const remoteRemoveCalls = []; + mockExec.exec.mockImplementation(async (cmd, args, options) => { + if (cmd === "git" && args && args[0] === "log" && options && options.listeners) { + options.listeners.stdout(Buffer.from("COMMIT:abc123\nfile.txt\n")); + return 0; + } + if (cmd === "git" && args && args[0] === "remote" && args[1] === "remove") { + remoteRemoveCalls.push(args); + return 0; + } + if (cmd === "git" && args && args[0] === "push") { + throw new Error("push failed"); + } + return 0; + }); + + await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + // Should have at least one remote remove call for cleanup + const ciTriggerRemoveCall = remoteRemoveCalls.find(args => args[2] === "ci-trigger"); + expect(ciTriggerRemoveCall).toBeDefined(); + }); + }); +}); diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index e413811da43..58078fcc4e8 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -8,7 +8,7 @@ const { updateActivationCommentWithCommit } = require("./update_activation_comme const { getErrorMessage } = require("./error_helpers.cjs"); const { replaceTemporaryIdReferences } = require("./temporary_id.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); -const { pushCITriggerCommit } = require("./ci_trigger_commit.cjs"); +const { pushExtraEmptyCommit } = require("./extra_empty_commit.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -403,16 +403,16 @@ async function main(config = {}) { await core.summary.addRaw(summaryContent).write(); - // Push an empty CI trigger commit if a CI trigger token is configured and changes were pushed. + // Push an extra empty commit if a token is configured and changes were pushed. // This works around the GITHUB_TOKEN limitation where pushes don't trigger CI events. if (hasChanges) { - const ciTriggerResult = await pushCITriggerCommit({ + const ciTriggerResult = await pushExtraEmptyCommit({ branchName, repoOwner: context.repo.owner, repoName: context.repo.repo, }); if (ciTriggerResult.success && !ciTriggerResult.skipped) { - core.info("CI trigger commit pushed - CI checks should start shortly"); + core.info("Extra empty commit pushed - CI checks should start shortly"); } } diff --git a/docs/src/content/docs/reference/auth.mdx b/docs/src/content/docs/reference/auth.mdx index 725762f4156..78eaa961f71 100644 --- a/docs/src/content/docs/reference/auth.mdx +++ b/docs/src/content/docs/reference/auth.mdx @@ -26,7 +26,7 @@ You will need one of the following GitHub Actions secrets configured in your rep Depending on what your workflow needs to do, you may need additional GitHub tokens added as repository secrets: - **Lockdown mode, cross-repo access, or remote GitHub tools** โ€“ Add [`GH_AW_GITHUB_TOKEN`](#gh_aw_github_token) -- **Trigger CI on PRs created by workflows** โ€“ Add [`GH_AW_CI_TRIGGER_TOKEN`](#gh_aw_ci_trigger_token) +- **Trigger CI on PRs created by workflows** โ€“ Use [`github-token-for-extra-empty-commit`](/gh-aw/reference/safe-outputs/#triggering-ci-on-created-pull-requests) - **GitHub Projects v2 operations** โ€“ Add [`GH_AW_PROJECT_GITHUB_TOKEN`](#gh_aw_project_github_token) - **Assign Copilot coding agent to issues/PRs** โ€“ Add [`GH_AW_AGENT_TOKEN`](#gh_aw_agent_token) - **MCP server with special permissions** โ€“ Add [`GH_AW_GITHUB_MCP_SERVER_TOKEN`](#gh_aw_github_mcp_server_token) @@ -194,61 +194,28 @@ gh aw secrets set GH_AW_GITHUB_MCP_SERVER_TOKEN --value "YOUR_MCP_PAT" --- -### `GH_AW_CI_TRIGGER_TOKEN` +### Triggering CI on Created Pull Requests -A Personal Access Token (PAT) or GitHub App token used to push an empty commit after PR creation or branch push, triggering CI events that the default `GITHUB_TOKEN` cannot trigger. +Pull requests and pushes made with `GITHUB_TOKEN` do not trigger `pull_request`, `push`, or `pull_request_target` events. This is a [GitHub Actions security feature](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) to prevent recursive workflows. -Pull requests and pushes made with `GITHUB_TOKEN` do not trigger `pull_request`, `push`, or `pull_request_target` events. This is a [GitHub Actions security feature](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) to prevent recursive workflows. The `GH_AW_CI_TRIGGER_TOKEN` provides a minimal workaround: after the safe output creates the PR or pushes, an additional empty commit is pushed using this token, which triggers CI. +To trigger CI, add `github-token-for-extra-empty-commit` to your `create-pull-request` or `push-to-pull-request-branch` safe output configuration. This pushes an additional empty commit using the specified token, which triggers CI events. -**When to use**: - -- You need CI checks to run on PRs created by `create-pull-request` safe outputs. -- You need CI checks to run after `push-to-pull-request-branch` pushes. -- You want a global default CI trigger token for all safe outputs without configuring each one individually. - -**Setup**: - -Create a [fine-grained PAT](https://github.com/settings/personal-access-tokens/new) with: - -- **Repository access**: Select specific repos that run agentic workflows -- **Repository permissions**: - - Contents: Read & Write (required to push commits) - -Then add to repository secrets: - -```bash wrap -gh aw secrets set GH_AW_CI_TRIGGER_TOKEN --value "YOUR_CI_TRIGGER_PAT" -``` - -**Per-safe-output override**: +**Two ways to configure:** -You can also configure the token per safe output using `github-ci-trigger-token` in the workflow frontmatter, which takes precedence over the `GH_AW_CI_TRIGGER_TOKEN` secret: - -```yaml wrap -safe-outputs: - create-pull-request: - github-ci-trigger-token: ${{ secrets.MY_CUSTOM_CI_TOKEN }} -``` +| Method | Configuration | Description | +|--------|--------------|-------------| +| **Explicit token** | `github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }}` | Use a specific PAT or token per safe output | +| **GitHub App** | `github-token-for-extra-empty-commit: app` | Use the configured GitHub App installation token | -Or use `app` to use a GitHub App installation token: +**Example:** ```yaml wrap safe-outputs: create-pull-request: - github-ci-trigger-token: app + github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} ``` -**Token precedence**: per-safe-output `github-ci-trigger-token` โ†’ `GH_AW_CI_TRIGGER_TOKEN` secret. - -**Three ways to configure CI triggering:** - -| Method | Configuration | Description | -|--------|--------------|-------------| -| **Explicit token** | `github-ci-trigger-token: ${{ secrets.CI_TOKEN }}` | Use a specific PAT or token per safe output | -| **GitHub App** | `github-ci-trigger-token: app` | Use the configured GitHub App installation token | -| **Implicit secret** | Set `GH_AW_CI_TRIGGER_TOKEN` repository secret | No workflow config needed โ€” checked automatically | - -The implicit `GH_AW_CI_TRIGGER_TOKEN` secret is checked automatically when `create-pull-request` or `push-to-pull-request-branch` is configured, even without explicit `github-ci-trigger-token` in the workflow. Just add the secret to your repository. +**Required permissions**: Create a [fine-grained PAT](https://github.com/settings/personal-access-tokens/new) with `Contents: Read & Write` scoped to the relevant repositories. > [!TIP] > Use a fine-grained PAT with `contents: write` permission scoped to your repository. This is the minimum permission needed to push commits. diff --git a/docs/src/content/docs/reference/faq.md b/docs/src/content/docs/reference/faq.md index 6e118795eed..8eb2a218af1 100644 --- a/docs/src/content/docs/reference/faq.md +++ b/docs/src/content/docs/reference/faq.md @@ -305,21 +305,23 @@ GitHub Actions prevents the `GITHUB_TOKEN` from triggering new workflow runs to If you need CI checks to run on PRs created by agentic workflows, you have several options: -**Option 1: Use `github-ci-trigger-token` (Recommended)** +**Option 1: Use `github-token-for-extra-empty-commit` (Recommended)** -Add a `github-ci-trigger-token` to your `create-pull-request` or `push-to-pull-request-branch` safe output. This pushes an empty commit using a different token after PR creation/push, which triggers CI events without changing the overall PR authorization. +Add a `github-token-for-extra-empty-commit` to your `create-pull-request` or `push-to-pull-request-branch` safe output. This pushes an empty commit using a different token after PR creation/push, which triggers CI events without changing the overall PR authorization. ```yaml wrap safe-outputs: create-pull-request: - github-ci-trigger-token: ${{ secrets.CI_TRIGGER_PAT }} + github-token-for-extra-empty-commit: ${{ secrets.CI_TRIGGER_PAT }} ``` -You can also use `app` to use a GitHub App token, or set the `GH_AW_CI_TRIGGER_TOKEN` repository secret for a global default. See [Triggering CI on Created Pull Requests](/gh-aw/reference/safe-outputs/#triggering-ci-on-created-pull-requests) for details. +See [Triggering CI on Created Pull Requests](/gh-aw/reference/safe-outputs/#triggering-ci-on-created-pull-requests) for details. **Option 2: Use different authorization for the entire safe output** -Configure your [`create-pull-request` safe output](/gh-aw/reference/safe-outputs/#pull-request-creation-create-pull-request) to use a PAT or a GitHub App for all operations. This allows PR creation to trigger CI workflows, but changes the authorization for the entire PR creation process. +Configure your [`create-pull-request` safe output](/gh-aw/reference/safe-outputs/#pull-request-creation-create-pull-request) to use a PAT or a GitHub App for all operations. This allows PR creation to trigger CI workflows, but changes the authorization for the entire PR creation process. The user or app associated with the token will be the author of the PR. + +```yaml wrap **Option 3: Use workflow_run trigger** diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 074e31bb2a1..b4ee5184d42 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -687,7 +687,7 @@ safe-outputs: base-branch: "vnext" # target branch for PR (default: github.base_ref || github.ref_name) fallback-as-issue: false # disable issue fallback (default: true) github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions - github-ci-trigger-token: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI + github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI ``` The `base-branch` field specifies which branch the pull request should target. This is particularly useful for cross-repository PRs where you need to target non-default branches (e.g., `vnext`, `release/v1.0`, `staging`). When not specified, defaults to `github.base_ref` (the PR's target branch) with a fallback to `github.ref_name` (the workflow's branch) for push events. @@ -715,12 +715,12 @@ By default, pull requests created using `GITHUB_TOKEN` **do not trigger CI workf ```yaml wrap safe-outputs: create-pull-request: - github-ci-trigger-token: ${{ secrets.CI_TOKEN }} # PAT or token to trigger CI + github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} # PAT or token to trigger CI ``` When configured, an empty commit is pushed to the PR branch using the specified token after PR creation. Since this push comes from a different authentication context, it triggers `push` and `pull_request` events normally. -See [`GH_AW_CI_TRIGGER_TOKEN`](/gh-aw/reference/auth/#gh_aw_ci_trigger_token) for token setup, configuration methods (explicit token, GitHub App, or implicit secret), and required permissions. +Use a secret expression (e.g. `${{ secrets.CI_TOKEN }}`) or `app` for GitHub App auth. See the [Authentication reference](/gh-aw/reference/auth/) for token setup and required permissions. ### Close Pull Request (`close-pull-request:`) @@ -848,12 +848,12 @@ safe-outputs: max: 3 # max pushes per run (default: 1) if-no-changes: "warn" # "warn" (default), "error", or "ignore" github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions - github-ci-trigger-token: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI + github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI ``` When `push-to-pull-request-branch` is configured, git commands (`checkout`, `branch`, `switch`, `add`, `rm`, `commit`, `merge`) are automatically enabled. -Like `create-pull-request`, pushes made with `GITHUB_TOKEN` do not trigger CI events. Use `github-ci-trigger-token` or the [`GH_AW_CI_TRIGGER_TOKEN`](/gh-aw/reference/auth/#gh_aw_ci_trigger_token) secret to trigger CI after pushing. See [Triggering CI on Created Pull Requests](#triggering-ci-on-created-pull-requests) for details. +Like `create-pull-request`, pushes made with `GITHUB_TOKEN` do not trigger CI events. Use `github-token-for-extra-empty-commit` to trigger CI after pushing. See [Triggering CI on Created Pull Requests](#triggering-ci-on-created-pull-requests) for details. #### Fail-Fast on Code Push Failure diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ba9f3713aa3..4bc94099a5c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5120,9 +5120,9 @@ "description": "Controls the fallback behavior when pull request creation fails. When true (default), an issue is created as a fallback with the patch content. When false, no issue is created and the workflow fails with an error. Setting to false also removes the issues:write permission requirement.", "default": true }, - "github-ci-trigger-token": { + "github-token-for-extra-empty-commit": { "type": "string", - "description": "Token used to push an empty commit after PR creation to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}'), 'app' for GitHub App auth, or set the GH_AW_CI_TRIGGER_TOKEN repository secret for implicit usage." + "description": "Token used to push an empty commit after PR creation to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}') or 'app' for GitHub App auth." } }, "additionalProperties": false, @@ -6101,9 +6101,9 @@ "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", "examples": [true, false] }, - "github-ci-trigger-token": { + "github-token-for-extra-empty-commit": { "type": "string", - "description": "Token used to push an empty commit after pushing changes to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}'), 'app' for GitHub App auth, or set the GH_AW_CI_TRIGGER_TOKEN repository secret for implicit usage." + "description": "Token used to push an empty commit after pushing changes to trigger CI events. Works around the GITHUB_TOKEN limitation where pushes don't trigger workflow runs. Use a secret expression (e.g. '${{ secrets.CI_TOKEN }}') or 'app' for GitHub App auth." } }, "additionalProperties": false diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index bdcddb8603b..6d35201fc5d 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -405,27 +405,23 @@ func (c *Compiler) buildJobLevelSafeOutputEnvVars(data *WorkflowData, workflowID } } - // Add CI trigger token if configured on create-pull-request or push-to-pull-request-branch. + // Add extra empty commit token if configured on create-pull-request or push-to-pull-request-branch. // This token is used to push an empty commit after code changes to trigger CI events, // working around the GITHUB_TOKEN limitation where events don't trigger other workflows. - // Precedence: per-safe-output github-ci-trigger-token > GH_AW_CI_TRIGGER_TOKEN secret if data.SafeOutputs != nil { - var ciTriggerToken string - if data.SafeOutputs.CreatePullRequests != nil && data.SafeOutputs.CreatePullRequests.GithubCITriggerToken != "" { - ciTriggerToken = data.SafeOutputs.CreatePullRequests.GithubCITriggerToken - } else if data.SafeOutputs.PushToPullRequestBranch != nil && data.SafeOutputs.PushToPullRequestBranch.GithubCITriggerToken != "" { - ciTriggerToken = data.SafeOutputs.PushToPullRequestBranch.GithubCITriggerToken + var extraEmptyCommitToken string + if data.SafeOutputs.CreatePullRequests != nil && data.SafeOutputs.CreatePullRequests.GithubTokenForExtraEmptyCommit != "" { + extraEmptyCommitToken = data.SafeOutputs.CreatePullRequests.GithubTokenForExtraEmptyCommit + } else if data.SafeOutputs.PushToPullRequestBranch != nil && data.SafeOutputs.PushToPullRequestBranch.GithubTokenForExtraEmptyCommit != "" { + extraEmptyCommitToken = data.SafeOutputs.PushToPullRequestBranch.GithubTokenForExtraEmptyCommit } - if ciTriggerToken == "app" { - envVars["GH_AW_CI_TRIGGER_TOKEN"] = "${{ steps.safe-outputs-app-token.outputs.token || '' }}" - consolidatedSafeOutputsJobLog.Print("CI trigger using GitHub App token") - } else if ciTriggerToken != "" { - envVars["GH_AW_CI_TRIGGER_TOKEN"] = ciTriggerToken - consolidatedSafeOutputsJobLog.Print("CI trigger using explicit token") - } else if data.SafeOutputs.CreatePullRequests != nil || data.SafeOutputs.PushToPullRequestBranch != nil { - // Fall back to GH_AW_CI_TRIGGER_TOKEN secret if set (implicit mode) - envVars["GH_AW_CI_TRIGGER_TOKEN"] = "${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }}" + if extraEmptyCommitToken == "app" { + envVars["GH_AW_EXTRA_EMPTY_COMMIT_TOKEN"] = "${{ steps.safe-outputs-app-token.outputs.token || '' }}" + consolidatedSafeOutputsJobLog.Print("Extra empty commit using GitHub App token") + } else if extraEmptyCommitToken != "" { + envVars["GH_AW_EXTRA_EMPTY_COMMIT_TOKEN"] = extraEmptyCommitToken + consolidatedSafeOutputsJobLog.Print("Extra empty commit using explicit token") } } diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index 2811eb4da32..f958c16ee6d 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -20,22 +20,22 @@ func getFallbackAsIssue(config *CreatePullRequestsConfig) bool { // CreatePullRequestsConfig holds configuration for creating GitHub pull requests from agent output type CreatePullRequestsConfig struct { - BaseSafeOutputConfig `yaml:",inline"` - TitlePrefix string `yaml:"title-prefix,omitempty"` - Labels []string `yaml:"labels,omitempty"` - AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). - Reviewers []string `yaml:"reviewers,omitempty"` // List of users/bots to assign as reviewers to the pull request - Draft *string `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil), literal bool, and expression values - IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn" (default), "error", or "ignore" - AllowEmpty *string `yaml:"allow-empty,omitempty"` // Allow creating PR without patch file or with empty patch (useful for preparing feature branches) - TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository pull requests - AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that pull requests can be created in (additionally to the target-repo) - Expires int `yaml:"expires,omitempty"` // Hours until the pull request expires and should be automatically closed (only for same-repo PRs) - AutoMerge *string `yaml:"auto-merge,omitempty"` // Enable auto-merge for the pull request when all required checks pass - BaseBranch string `yaml:"base-branch,omitempty"` // Base branch for the pull request (defaults to github.ref_name if not specified) - Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. - FallbackAsIssue *bool `yaml:"fallback-as-issue,omitempty"` // When true (default), creates an issue if PR creation fails. When false, no fallback occurs and issues: write permission is not requested. - GithubCITriggerToken string `yaml:"github-ci-trigger-token,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT, "app" for GitHub App auth, or set GH_AW_CI_TRIGGER_TOKEN secret. + BaseSafeOutputConfig `yaml:",inline"` + TitlePrefix string `yaml:"title-prefix,omitempty"` + Labels []string `yaml:"labels,omitempty"` + AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). + Reviewers []string `yaml:"reviewers,omitempty"` // List of users/bots to assign as reviewers to the pull request + Draft *string `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil), literal bool, and expression values + IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn" (default), "error", or "ignore" + AllowEmpty *string `yaml:"allow-empty,omitempty"` // Allow creating PR without patch file or with empty patch (useful for preparing feature branches) + TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository pull requests + AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that pull requests can be created in (additionally to the target-repo) + Expires int `yaml:"expires,omitempty"` // Hours until the pull request expires and should be automatically closed (only for same-repo PRs) + AutoMerge *string `yaml:"auto-merge,omitempty"` // Enable auto-merge for the pull request when all required checks pass + BaseBranch string `yaml:"base-branch,omitempty"` // Base branch for the pull request (defaults to github.ref_name if not specified) + Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. + FallbackAsIssue *bool `yaml:"fallback-as-issue,omitempty"` // When true (default), creates an issue if PR creation fails. When false, no fallback occurs and issues: write permission is not requested. + GithubTokenForExtraEmptyCommit string `yaml:"github-token-for-extra-empty-commit,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT or "app" for GitHub App auth. } // buildCreateOutputPullRequestJob creates the create_pull_request job @@ -165,19 +165,16 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa createPRLog.Print("Footer disabled - XML markers will be included but visible footer content will be omitted") } - // Add CI trigger token if configured (for pushing an empty commit to trigger CI) - ciTriggerToken := data.SafeOutputs.CreatePullRequests.GithubCITriggerToken - if ciTriggerToken != "" { - if ciTriggerToken == "app" { - customEnvVars = append(customEnvVars, " GH_AW_CI_TRIGGER_TOKEN: ${{ steps.safe-outputs-app-token.outputs.token || '' }}\n") - createPRLog.Print("CI trigger using GitHub App token") + // Add extra empty commit token if configured (for pushing an empty commit to trigger CI) + extraEmptyCommitToken := data.SafeOutputs.CreatePullRequests.GithubTokenForExtraEmptyCommit + if extraEmptyCommitToken != "" { + if extraEmptyCommitToken == "app" { + customEnvVars = append(customEnvVars, " GH_AW_EXTRA_EMPTY_COMMIT_TOKEN: ${{ steps.safe-outputs-app-token.outputs.token || '' }}\n") + createPRLog.Print("Extra empty commit using GitHub App token") } else { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_CI_TRIGGER_TOKEN: %s\n", ciTriggerToken)) - createPRLog.Printf("CI trigger using explicit token") + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_EXTRA_EMPTY_COMMIT_TOKEN: %s\n", extraEmptyCommitToken)) + createPRLog.Printf("Extra empty commit using explicit token") } - } else { - // Fall back to GH_AW_CI_TRIGGER_TOKEN secret if set - customEnvVars = append(customEnvVars, " GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }}\n") } // Add standard environment variables (metadata + staged/target repo) diff --git a/pkg/workflow/push_to_pull_request_branch.go b/pkg/workflow/push_to_pull_request_branch.go index c1f4d280c2e..0a0b9f99ad7 100644 --- a/pkg/workflow/push_to_pull_request_branch.go +++ b/pkg/workflow/push_to_pull_request_branch.go @@ -11,13 +11,13 @@ var pushToPullRequestBranchLog = logger.New("workflow:push_to_pull_request_branc // PushToPullRequestBranchConfig holds configuration for pushing changes to a specific branch from agent output type PushToPullRequestBranchConfig struct { - BaseSafeOutputConfig `yaml:",inline"` - Target string `yaml:"target,omitempty"` // Target for push-to-pull-request-branch: like add-comment but for pull requests - TitlePrefix string `yaml:"title-prefix,omitempty"` // Required title prefix for pull request validation - Labels []string `yaml:"labels,omitempty"` // Required labels for pull request validation - IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") - CommitTitleSuffix string `yaml:"commit-title-suffix,omitempty"` // Optional suffix to append to generated commit titles - GithubCITriggerToken string `yaml:"github-ci-trigger-token,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT, "app" for GitHub App auth, or set GH_AW_CI_TRIGGER_TOKEN secret. + BaseSafeOutputConfig `yaml:",inline"` + Target string `yaml:"target,omitempty"` // Target for push-to-pull-request-branch: like add-comment but for pull requests + TitlePrefix string `yaml:"title-prefix,omitempty"` // Required title prefix for pull request validation + Labels []string `yaml:"labels,omitempty"` // Required labels for pull request validation + IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") + CommitTitleSuffix string `yaml:"commit-title-suffix,omitempty"` // Optional suffix to append to generated commit titles + GithubTokenForExtraEmptyCommit string `yaml:"github-token-for-extra-empty-commit,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT or "app" for GitHub App auth. } // buildCheckoutRepository generates a checkout step with optional target repository and custom token @@ -118,11 +118,11 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any) } } - // Parse github-ci-trigger-token (optional) - token for pushing empty commit to trigger CI - if ciTriggerToken, exists := configMap["github-ci-trigger-token"]; exists { - if ciTriggerTokenStr, ok := ciTriggerToken.(string); ok { - pushToBranchConfig.GithubCITriggerToken = ciTriggerTokenStr - pushToPullRequestBranchLog.Printf("CI trigger token configured") + // Parse github-token-for-extra-empty-commit (optional) - token for pushing empty commit to trigger CI + if emptyCommitToken, exists := configMap["github-token-for-extra-empty-commit"]; exists { + if emptyCommitTokenStr, ok := emptyCommitToken.(string); ok { + pushToBranchConfig.GithubTokenForExtraEmptyCommit = emptyCommitTokenStr + pushToPullRequestBranchLog.Printf("Extra empty commit token configured") } } From 65975faa168de91f77085399253a3b6294125bf6 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 23 Feb 2026 01:42:34 +0000 Subject: [PATCH 4/4] review adjustments --- .github/workflows/mergefest.lock.yml | 1 - .github/workflows/poem-bot.lock.yml | 1 - .github/workflows/q.lock.yml | 1 - .github/workflows/refiner.lock.yml | 1 - .github/workflows/slide-deck-maintainer.lock.yml | 1 - .github/workflows/smoke-claude.lock.yml | 1 - .github/workflows/smoke-multi-pr.lock.yml | 1 - .github/workflows/smoke-project.lock.yml | 1 - .github/workflows/technical-doc-writer.lock.yml | 1 - .../test-create-pr-error-handling.lock.yml | 1 - .github/workflows/tidy.lock.yml | 1 - .github/workflows/ubuntu-image-analyzer.lock.yml | 1 - .github/workflows/unbloat-docs.lock.yml | 1 - .../weekly-editors-health-check.lock.yml | 1 - .../weekly-safe-outputs-spec-review.lock.yml | 1 - docs/src/content/docs/reference/faq.md | 15 +++------------ 16 files changed, 3 insertions(+), 27 deletions(-) diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index ad91123f7d1..1a5067515cf 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -1153,7 +1153,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "mergefest" GH_AW_WORKFLOW_NAME: "Mergefest" diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 68f789a41aa..17d29959fa1 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1831,7 +1831,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: "gpt-5" GH_AW_SAFE_OUTPUTS_STAGED: "true" diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index bafbe47d45c..b59eefeb5cf 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1355,7 +1355,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐ŸŽฉ *Equipped by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ”ง Pay attention, 007! [{workflow_name}]({run_url}) is preparing your gadgets for this {event_type}...\",\"runSuccess\":\"๐ŸŽฉ Mission equipment ready! [{workflow_name}]({run_url}) has optimized your workflow. Use wisely, 007! ๐Ÿ”ซ\",\"runFailure\":\"๐Ÿ”ง Technical difficulties! [{workflow_name}]({run_url}) {status}. Even Q Branch has bad days...\"}" GH_AW_WORKFLOW_ID: "q" diff --git a/.github/workflows/refiner.lock.yml b/.github/workflows/refiner.lock.yml index ee2b94232f8..0b77b88977d 100644 --- a/.github/workflows/refiner.lock.yml +++ b/.github/workflows/refiner.lock.yml @@ -1166,7 +1166,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"runStarted\":\"๐Ÿ” Starting code refinement... [{workflow_name}]({run_url}) is analyzing PR #${{ github.event.pull_request.number }} for style alignment and security issues\",\"runSuccess\":\"โœ… Refinement complete! [{workflow_name}]({run_url}) has created a PR with improvements for PR #${{ github.event.pull_request.number }}\",\"runFailure\":\"โŒ Refinement failed! [{workflow_name}]({run_url}) {status} while processing PR #${{ github.event.pull_request.number }}\"}" GH_AW_WORKFLOW_ID: "refiner" diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 46eacbb31fa..721d318b4dc 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -1213,7 +1213,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "slide-deck-maintainer" GH_AW_WORKFLOW_ID: "slide-deck-maintainer" diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 3369bb49ab0..95c1a9e3c01 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2723,7 +2723,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ’ฅ *[THE END] โ€” Illustrated by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ’ฅ **WHOOSH!** [{workflow_name}]({run_url}) springs into action on this {event_type}! *[Panel 1 begins...]*\",\"runSuccess\":\"๐ŸŽฌ **THE END** โ€” [{workflow_name}]({run_url}) **MISSION: ACCOMPLISHED!** The hero saves the day! โœจ\",\"runFailure\":\"๐Ÿ’ซ **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges...\"}" GH_AW_WORKFLOW_ID: "smoke-claude" diff --git a/.github/workflows/smoke-multi-pr.lock.yml b/.github/workflows/smoke-multi-pr.lock.yml index 1b7d0b25e43..5e1e770c114 100644 --- a/.github/workflows/smoke-multi-pr.lock.yml +++ b/.github/workflows/smoke-multi-pr.lock.yml @@ -1232,7 +1232,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Multi PR smoke test by [{workflow_name}]({run_url})*\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing multiple PR creation...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) successfully created multiple PRs.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) failed to create multiple PRs. Check the logs.\"}" GH_AW_WORKFLOW_ID: "smoke-multi-pr" diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 7532ed61518..3b0b40b9996 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1626,7 +1626,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Project smoke test report by [{workflow_name}]({run_url})*\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing project operations...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed successfully. All project operations validated.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) encountered failures. Check the logs for details.\"}" GH_AW_WORKFLOW_ID: "smoke-project" diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 7123e2b8acf..72ad03e729a 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -1216,7 +1216,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ“ *Documentation by [{workflow_name}]({run_url})*\",\"runStarted\":\"โœ๏ธ The Technical Writer begins! [{workflow_name}]({run_url}) is documenting this {event_type}...\",\"runSuccess\":\"๐Ÿ“ Documentation complete! [{workflow_name}]({run_url}) has written the docs. Clear as crystal! โœจ\",\"runFailure\":\"โœ๏ธ Writer's block! [{workflow_name}]({run_url}) {status}. The page remains blank...\"}" GH_AW_WORKFLOW_ID: "technical-doc-writer" diff --git a/.github/workflows/test-create-pr-error-handling.lock.yml b/.github/workflows/test-create-pr-error-handling.lock.yml index 715904bfe4b..d098197d066 100644 --- a/.github/workflows/test-create-pr-error-handling.lock.yml +++ b/.github/workflows/test-create-pr-error-handling.lock.yml @@ -1145,7 +1145,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_WORKFLOW_ID: "test-create-pr-error-handling" GH_AW_WORKFLOW_NAME: "Test Create PR Error Handling" diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index b8a7b983b1f..29ad1a4ad4e 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -1252,7 +1252,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "tidy" GH_AW_WORKFLOW_NAME: "Tidy" diff --git a/.github/workflows/ubuntu-image-analyzer.lock.yml b/.github/workflows/ubuntu-image-analyzer.lock.yml index f79edc8d1d0..58e58b312f5 100644 --- a/.github/workflows/ubuntu-image-analyzer.lock.yml +++ b/.github/workflows/ubuntu-image-analyzer.lock.yml @@ -1143,7 +1143,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "ubuntu-image-analyzer" GH_AW_WORKFLOW_ID: "ubuntu-image-analyzer" diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index ae766b70875..a86eb254342 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1429,7 +1429,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "claude" GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ—œ๏ธ *Compressed by [{workflow_name}]({run_url})*\",\"runStarted\":\"๐Ÿ“ฆ Time to slim down! [{workflow_name}]({run_url}) is trimming the excess from this {event_type}...\",\"runSuccess\":\"๐Ÿ—œ๏ธ Docs on a diet! [{workflow_name}]({run_url}) has removed the bloat. Lean and mean! ๐Ÿ’ช\",\"runFailure\":\"๐Ÿ“ฆ Unbloating paused! [{workflow_name}]({run_url}) {status}. The docs remain... fluffy.\"}" GH_AW_WORKFLOW_ID: "unbloat-docs" diff --git a/.github/workflows/weekly-editors-health-check.lock.yml b/.github/workflows/weekly-editors-health-check.lock.yml index d88470bc2dd..4ae90d21099 100644 --- a/.github/workflows/weekly-editors-health-check.lock.yml +++ b/.github/workflows/weekly-editors-health-check.lock.yml @@ -1145,7 +1145,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "weekly-editors-health-check" GH_AW_WORKFLOW_ID: "weekly-editors-health-check" diff --git a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml index 03bb6f270a2..807b28f3d63 100644 --- a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml +++ b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml @@ -1065,7 +1065,6 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} GH_AW_ENGINE_ID: "copilot" GH_AW_TRACKER_ID: "weekly-safe-outputs-spec-review" GH_AW_WORKFLOW_ID: "weekly-safe-outputs-spec-review" diff --git a/docs/src/content/docs/reference/faq.md b/docs/src/content/docs/reference/faq.md index 8eb2a218af1..c71076aa2a8 100644 --- a/docs/src/content/docs/reference/faq.md +++ b/docs/src/content/docs/reference/faq.md @@ -322,20 +322,11 @@ See [Triggering CI on Created Pull Requests](/gh-aw/reference/safe-outputs/#trig Configure your [`create-pull-request` safe output](/gh-aw/reference/safe-outputs/#pull-request-creation-create-pull-request) to use a PAT or a GitHub App for all operations. This allows PR creation to trigger CI workflows, but changes the authorization for the entire PR creation process. The user or app associated with the token will be the author of the PR. ```yaml wrap - -**Option 3: Use workflow_run trigger** - -Configure your CI workflows to run on `workflow_run` events, which allows them to react to completed workflows: - -```yaml wrap -on: - workflow_run: - workflows: ["Create Pull Request Workflow"] - types: [completed] +safe-outputs: + create-pull-request: + github-token: ${{ secrets.CI_USER_PAT }} ``` -This approach maintains security while allowing CI to run after PR creation. See [GitHub Actions workflow_run documentation](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run) for details. - ## Workflow Design ### Should I focus on one workflow, or write many different ones?