diff --git a/.changeset/patch-bump-mcp-gateway-and-apm-versions.md b/.changeset/patch-bump-mcp-gateway-and-apm-versions.md new file mode 100644 index 00000000000..b48e846604c --- /dev/null +++ b/.changeset/patch-bump-mcp-gateway-and-apm-versions.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Bump the default MCP Gateway version from `v0.1.15` to `v0.1.17`. diff --git a/.changeset/patch-fallback-incremental-patch-fetch.md b/.changeset/patch-fallback-incremental-patch-fetch.md new file mode 100644 index 00000000000..c22a490b299 --- /dev/null +++ b/.changeset/patch-fallback-incremental-patch-fetch.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fixed incremental patch generation for `push_to_pull_request_branch` by falling back to an existing `origin/` tracking ref when `git fetch` fails, and added integration coverage for the fallback path. diff --git a/.changeset/patch-use-signed-commit-pushes.md b/.changeset/patch-use-signed-commit-pushes.md new file mode 100644 index 00000000000..739a8a7f204 --- /dev/null +++ b/.changeset/patch-use-signed-commit-pushes.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Replace direct `git push` with GraphQL commit replay so commits pushed by `push_to_pull_request_branch` and `create_pull_request` are GitHub-signed, with fallback to `git push` when GraphQL commit creation is unavailable. diff --git a/.github/workflows/ace-editor.lock.yml b/.github/workflows/ace-editor.lock.yml index bca98b9cc5e..11c810bd696 100644 --- a/.github/workflows/ace-editor.lock.yml +++ b/.github/workflows/ace-editor.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/agent-performance-analyzer.lock.yml b/.github/workflows/agent-performance-analyzer.lock.yml index 80939ccf213..b0e9e9ef22d 100644 --- a/.github/workflows/agent-performance-analyzer.lock.yml +++ b/.github/workflows/agent-performance-analyzer.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/agent-persona-explorer.lock.yml b/.github/workflows/agent-persona-explorer.lock.yml index 58d91a3ef1c..46408ac4c43 100644 --- a/.github/workflows/agent-persona-explorer.lock.yml +++ b/.github/workflows/agent-persona-explorer.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index 2204b261a4b..49994c6bc99 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -105,6 +105,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/archie.lock.yml b/.github/workflows/archie.lock.yml index 731c3d94110..3116b6b7221 100644 --- a/.github/workflows/archie.lock.yml +++ b/.github/workflows/archie.lock.yml @@ -110,6 +110,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index 6fd78884cf6..fb344952ce9 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index ebcbd190a76..cb700afcb7b 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml index c671c4a0cc6..3555a187a1e 100644 --- a/.github/workflows/auto-triage-issues.lock.yml +++ b/.github/workflows/auto-triage-issues.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml index f71970e1063..9950cd6b6ea 100644 --- a/.github/workflows/blog-auditor.lock.yml +++ b/.github/workflows/blog-auditor.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/bot-detection.lock.yml b/.github/workflows/bot-detection.lock.yml index e186e88edd5..2e4f84121a7 100644 --- a/.github/workflows/bot-detection.lock.yml +++ b/.github/workflows/bot-detection.lock.yml @@ -82,6 +82,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index 0f017f530ea..a653371231b 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -96,6 +96,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml index 9702a2f5525..50f21e48a3a 100644 --- a/.github/workflows/breaking-change-checker.lock.yml +++ b/.github/workflows/breaking-change-checker.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index d155e624985..483be87c766 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -102,6 +102,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index 71bba97fa8c..b25cb5ecc6c 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 7270e70f87f..d4c118d6ef1 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7b87023da6..9aa8049ee1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ on: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -162,6 +163,7 @@ jobs: integration: runs-on: ubuntu-latest + timeout-minutes: 25 permissions: contents: read strategy: @@ -391,6 +393,7 @@ jobs: canary_go: runs-on: ubuntu-latest + timeout-minutes: 15 needs: [integration] # test dependency removed - download-artifact fetches by name, not job dependency if: always() # Run even if some tests fail to report coverage permissions: @@ -444,6 +447,7 @@ jobs: update: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read concurrency: @@ -509,6 +513,7 @@ jobs: build: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -596,6 +601,7 @@ jobs: build-wasm: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -689,6 +695,7 @@ jobs: validate-yaml: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read steps: @@ -830,6 +837,7 @@ jobs: js: runs-on: ubuntu-latest + timeout-minutes: 10 needs: validate-yaml permissions: contents: read @@ -864,6 +872,7 @@ jobs: js-integration-live-api: runs-on: ubuntu-latest + timeout-minutes: 10 needs: validate-yaml permissions: contents: read @@ -916,6 +925,7 @@ jobs: # Only run benchmarks on main branch for performance tracking if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -992,6 +1002,7 @@ jobs: check-validator-sizes: name: Check validator file sizes runs-on: ubuntu-latest + timeout-minutes: 10 # Non-blocking: report violations but don't fail the build until existing files are cleaned up continue-on-error: true permissions: @@ -1023,6 +1034,7 @@ jobs: lint-go: runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: read concurrency: @@ -1123,6 +1135,7 @@ jobs: lint-js: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read concurrency: @@ -1160,6 +1173,7 @@ jobs: audit: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -1254,6 +1268,7 @@ jobs: actions-build: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read concurrency: @@ -1314,6 +1329,7 @@ jobs: # Only run fuzz tests on main branch (10s is insufficient for PRs) if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: read concurrency: @@ -1466,6 +1482,7 @@ jobs: security: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -1691,6 +1708,7 @@ jobs: health-smoke-copilot: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read actions: read @@ -1747,6 +1765,7 @@ jobs: mcp-server-compile-test: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read concurrency: @@ -1918,6 +1937,7 @@ jobs: cross-platform-build: name: Build & Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} + timeout-minutes: 20 permissions: contents: read strategy: @@ -2046,6 +2066,7 @@ jobs: alpine-container-test: name: Alpine Container Test runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: read concurrency: @@ -2180,6 +2201,7 @@ jobs: safe-outputs-conformance: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read steps: @@ -2232,6 +2254,7 @@ jobs: integration-add: name: Integration Add Workflows runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read concurrency: @@ -2428,6 +2451,7 @@ jobs: integration-unauthenticated-add: name: Integration Unauthenticated Add (Public Repo) runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read concurrency: @@ -2486,6 +2510,7 @@ jobs: integration-add-dispatch-workflow: name: Integration Add with dispatch-workflow Dependencies runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: read concurrency: diff --git a/.github/workflows/claude-code-user-docs-review.lock.yml b/.github/workflows/claude-code-user-docs-review.lock.yml index e91771a02ae..447d6dd0f05 100644 --- a/.github/workflows/claude-code-user-docs-review.lock.yml +++ b/.github/workflows/claude-code-user-docs-review.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index 66e58ce3853..bdb0cbba93b 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -79,6 +79,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index e932898b087..b1cf7397f68 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index d77dd923cbc..5d8ad6e6ab7 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -135,6 +135,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 7479bfdd304..7cac81145d2 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index 1c651e7f0cd..57b092ff59d 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -89,6 +89,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/codex-github-remote-mcp-test.lock.yml b/.github/workflows/codex-github-remote-mcp-test.lock.yml index 90fc6ac33b9..d1359dae297 100644 --- a/.github/workflows/codex-github-remote-mcp-test.lock.yml +++ b/.github/workflows/codex-github-remote-mcp-test.lock.yml @@ -78,6 +78,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml index 30a6560baaa..2bd4606f8f7 100644 --- a/.github/workflows/commit-changes-analyzer.lock.yml +++ b/.github/workflows/commit-changes-analyzer.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/constraint-solving-potd.lock.yml b/.github/workflows/constraint-solving-potd.lock.yml index a7e3f7ad56e..0cef22bb9e6 100644 --- a/.github/workflows/constraint-solving-potd.lock.yml +++ b/.github/workflows/constraint-solving-potd.lock.yml @@ -80,6 +80,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/contribution-check.lock.yml b/.github/workflows/contribution-check.lock.yml index cd89f74c5e4..92a75fad293 100644 --- a/.github/workflows/contribution-check.lock.yml +++ b/.github/workflows/contribution-check.lock.yml @@ -83,6 +83,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index fa798af3a1e..e05269a5f6d 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -88,6 +88,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/copilot-cli-deep-research.lock.yml b/.github/workflows/copilot-cli-deep-research.lock.yml index f0184263a2d..a474aebc435 100644 --- a/.github/workflows/copilot-cli-deep-research.lock.yml +++ b/.github/workflows/copilot-cli-deep-research.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml index 0d87f307e96..4546ac7d467 100644 --- a/.github/workflows/copilot-pr-merged-report.lock.yml +++ b/.github/workflows/copilot-pr-merged-report.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index 0412ad93e54..bae89962c20 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -88,6 +88,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index b899710a400..165321ba006 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 048382ae061..4161d8fc0a9 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -90,6 +90,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 7654f75308f..0fc8ef97289 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-architecture-diagram.lock.yml b/.github/workflows/daily-architecture-diagram.lock.yml index 2b2bee9e4af..9ff47b2e320 100644 --- a/.github/workflows/daily-architecture-diagram.lock.yml +++ b/.github/workflows/daily-architecture-diagram.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-assign-issue-to-user.lock.yml b/.github/workflows/daily-assign-issue-to-user.lock.yml index 73603dbcd18..0b344c58249 100644 --- a/.github/workflows/daily-assign-issue-to-user.lock.yml +++ b/.github/workflows/daily-assign-issue-to-user.lock.yml @@ -79,6 +79,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-choice-test.lock.yml b/.github/workflows/daily-choice-test.lock.yml index edd76499aee..44994500fe4 100644 --- a/.github/workflows/daily-choice-test.lock.yml +++ b/.github/workflows/daily-choice-test.lock.yml @@ -80,6 +80,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-cli-performance.lock.yml b/.github/workflows/daily-cli-performance.lock.yml index de0fec57cfd..8f235a5a5d4 100644 --- a/.github/workflows/daily-cli-performance.lock.yml +++ b/.github/workflows/daily-cli-performance.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-cli-tools-tester.lock.yml b/.github/workflows/daily-cli-tools-tester.lock.yml index 940f769ced7..727456981c3 100644 --- a/.github/workflows/daily-cli-tools-tester.lock.yml +++ b/.github/workflows/daily-cli-tools-tester.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index c5e86a7c58c..eb2307fb833 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-compiler-quality.lock.yml b/.github/workflows/daily-compiler-quality.lock.yml index 70e099e1414..9c19a51af30 100644 --- a/.github/workflows/daily-compiler-quality.lock.yml +++ b/.github/workflows/daily-compiler-quality.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml index bb328119c3b..17251225d0b 100644 --- a/.github/workflows/daily-copilot-token-report.lock.yml +++ b/.github/workflows/daily-copilot-token-report.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-doc-healer.lock.yml b/.github/workflows/daily-doc-healer.lock.yml index e7c7e08dcfa..7271732a91b 100644 --- a/.github/workflows/daily-doc-healer.lock.yml +++ b/.github/workflows/daily-doc-healer.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index fe88e00adc6..ae0820cb680 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-fact.lock.yml b/.github/workflows/daily-fact.lock.yml index 112ba86150b..bb915c21969 100644 --- a/.github/workflows/daily-fact.lock.yml +++ b/.github/workflows/daily-fact.lock.yml @@ -75,6 +75,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 972118ed4e9..3c8dcb314ee 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -89,6 +89,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index 1d4d82d4981..e8dd5c5acfe 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/daily-function-namer.lock.yml b/.github/workflows/daily-function-namer.lock.yml index c5efa56abd2..679642c7887 100644 --- a/.github/workflows/daily-function-namer.lock.yml +++ b/.github/workflows/daily-function-namer.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index 93a145c0e0a..d9d6cdbbdb5 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 8daae76cc42..acdcc1c072f 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-mcp-concurrency-analysis.lock.yml b/.github/workflows/daily-mcp-concurrency-analysis.lock.yml index 0f0c126409e..d9d6aaf3d52 100644 --- a/.github/workflows/daily-mcp-concurrency-analysis.lock.yml +++ b/.github/workflows/daily-mcp-concurrency-analysis.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 01677659f6f..fffd7029316 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -91,6 +91,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index 7eacfb171b6..94ca046ba9f 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-observability-report.lock.yml b/.github/workflows/daily-observability-report.lock.yml index 9fdec39f28d..915991debc4 100644 --- a/.github/workflows/daily-observability-report.lock.yml +++ b/.github/workflows/daily-observability-report.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index 440da124c51..de97a456b75 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/daily-regulatory.lock.yml b/.github/workflows/daily-regulatory.lock.yml index 5bad9391ffd..7292e741709 100644 --- a/.github/workflows/daily-regulatory.lock.yml +++ b/.github/workflows/daily-regulatory.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/daily-rendering-scripts-verifier.lock.yml b/.github/workflows/daily-rendering-scripts-verifier.lock.yml index 6721e3bbe81..1f5ae928eaa 100644 --- a/.github/workflows/daily-rendering-scripts-verifier.lock.yml +++ b/.github/workflows/daily-rendering-scripts-verifier.lock.yml @@ -89,6 +89,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml index 0fe7c7a3be9..311c303b928 100644 --- a/.github/workflows/daily-repo-chronicle.lock.yml +++ b/.github/workflows/daily-repo-chronicle.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-safe-output-integrator.lock.yml b/.github/workflows/daily-safe-output-integrator.lock.yml index 73ec8feef4d..f016ce1fb4e 100644 --- a/.github/workflows/daily-safe-output-integrator.lock.yml +++ b/.github/workflows/daily-safe-output-integrator.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index 37f08a1ee30..db9b42259e1 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -90,6 +90,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-safe-outputs-conformance.lock.yml b/.github/workflows/daily-safe-outputs-conformance.lock.yml index 7ecbe29f7b6..748d7f1c702 100644 --- a/.github/workflows/daily-safe-outputs-conformance.lock.yml +++ b/.github/workflows/daily-safe-outputs-conformance.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-secrets-analysis.lock.yml b/.github/workflows/daily-secrets-analysis.lock.yml index 71ccc14d058..0e949a8e01d 100644 --- a/.github/workflows/daily-secrets-analysis.lock.yml +++ b/.github/workflows/daily-secrets-analysis.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-security-red-team.lock.yml b/.github/workflows/daily-security-red-team.lock.yml index ebeaad69e4d..ea5a6d81073 100644 --- a/.github/workflows/daily-security-red-team.lock.yml +++ b/.github/workflows/daily-security-red-team.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index fc209faa9da..7fa35e4d73f 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/daily-syntax-error-quality.lock.yml b/.github/workflows/daily-syntax-error-quality.lock.yml index ff717ae62f9..4f4ee5d2e00 100644 --- a/.github/workflows/daily-syntax-error-quality.lock.yml +++ b/.github/workflows/daily-syntax-error-quality.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-team-evolution-insights.lock.yml b/.github/workflows/daily-team-evolution-insights.lock.yml index 4136e78da04..e16123b292c 100644 --- a/.github/workflows/daily-team-evolution-insights.lock.yml +++ b/.github/workflows/daily-team-evolution-insights.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml index 57a51bb7d0b..6d4b8f34d6c 100644 --- a/.github/workflows/daily-team-status.lock.yml +++ b/.github/workflows/daily-team-status.lock.yml @@ -94,6 +94,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/daily-testify-uber-super-expert.lock.yml b/.github/workflows/daily-testify-uber-super-expert.lock.yml index 955d429d70a..2690e5efdac 100644 --- a/.github/workflows/daily-testify-uber-super-expert.lock.yml +++ b/.github/workflows/daily-testify-uber-super-expert.lock.yml @@ -90,6 +90,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index 5a4286cae76..e655e42c901 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -22,7 +22,7 @@ # # Automatically updates GitHub Actions versions and creates a PR if changes are detected # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"9d7967bd6136b5508b4c77453fb2f1f2caf089b92359ad5375989524d06ba347","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"26f8e046e4f296368ce5e51b93ce99a0de7757fa1aa378fe826430c2f1951ba9","strict":true} name: "Daily Workflow Updater" "on": @@ -80,6 +80,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders @@ -1076,7 +1078,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,go.dev,golang.org,goproxy.io,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkg.go.dev,ppa.launchpad.net,proxy.golang.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,storage.googleapis.com,sum.golang.org,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"expires\":24,\"labels\":[\"dependencies\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[actions] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"expires\":24,\"labels\":[\"dependencies\",\"automation\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_files_policy\":\"allowed\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[actions] \"},\"missing_data\":{},\"missing_tool\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-workflow-updater.md b/.github/workflows/daily-workflow-updater.md index 054cdaf2ab2..93f8afde47c 100644 --- a/.github/workflows/daily-workflow-updater.md +++ b/.github/workflows/daily-workflow-updater.md @@ -28,6 +28,7 @@ safe-outputs: title-prefix: "[actions] " labels: [dependencies, automation] draft: false + protected-files: allowed tools: github: diff --git a/.github/workflows/dead-code-remover.lock.yml b/.github/workflows/dead-code-remover.lock.yml index 1acd34438b8..fb76c2cca43 100644 --- a/.github/workflows/dead-code-remover.lock.yml +++ b/.github/workflows/dead-code-remover.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 1f4b3f369c2..ad7572738ab 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/delight.lock.yml b/.github/workflows/delight.lock.yml index 6b1922a001b..e9ed6b8e708 100644 --- a/.github/workflows/delight.lock.yml +++ b/.github/workflows/delight.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/dependabot-burner.lock.yml b/.github/workflows/dependabot-burner.lock.yml index b77b8e6b73e..6a4682a2e10 100644 --- a/.github/workflows/dependabot-burner.lock.yml +++ b/.github/workflows/dependabot-burner.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index ca1790a04a2..0b722d62398 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index db91889867e..0bf6d2aeac0 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -90,6 +90,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 0f1a7301d97..b7bc0e6a248 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -79,6 +79,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index b1459785364..62f62a16ba6 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index 9ada94fae28..6c0db56bd71 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/discussion-task-miner.lock.yml b/.github/workflows/discussion-task-miner.lock.yml index 9849d806bfc..c42ad0cc70a 100644 --- a/.github/workflows/discussion-task-miner.lock.yml +++ b/.github/workflows/discussion-task-miner.lock.yml @@ -88,6 +88,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index 1a4729edebf..55cf3e0b48d 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/draft-pr-cleanup.lock.yml b/.github/workflows/draft-pr-cleanup.lock.yml index 5e2c682648f..8c5eb53b950 100644 --- a/.github/workflows/draft-pr-cleanup.lock.yml +++ b/.github/workflows/draft-pr-cleanup.lock.yml @@ -80,6 +80,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 08e035d4a6f..de789605a92 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/example-permissions-warning.lock.yml b/.github/workflows/example-permissions-warning.lock.yml index e796d8f08e6..9c322ce70be 100644 --- a/.github/workflows/example-permissions-warning.lock.yml +++ b/.github/workflows/example-permissions-warning.lock.yml @@ -78,6 +78,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index aebc19a1c0b..ba978c52b23 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/firewall-escape.lock.yml b/.github/workflows/firewall-escape.lock.yml index 35339091cd9..11f969d21a5 100644 --- a/.github/workflows/firewall-escape.lock.yml +++ b/.github/workflows/firewall-escape.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/firewall.lock.yml b/.github/workflows/firewall.lock.yml index 385e68931e6..0403cf1263d 100644 --- a/.github/workflows/firewall.lock.yml +++ b/.github/workflows/firewall.lock.yml @@ -78,6 +78,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/functional-pragmatist.lock.yml b/.github/workflows/functional-pragmatist.lock.yml index 2c735a3a093..05f8098f360 100644 --- a/.github/workflows/functional-pragmatist.lock.yml +++ b/.github/workflows/functional-pragmatist.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/github-mcp-structural-analysis.lock.yml b/.github/workflows/github-mcp-structural-analysis.lock.yml index 79706bd456d..d5b77ae5b74 100644 --- a/.github/workflows/github-mcp-structural-analysis.lock.yml +++ b/.github/workflows/github-mcp-structural-analysis.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 356b6940a6b..03f8fe626ed 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/github-remote-mcp-auth-test.lock.yml b/.github/workflows/github-remote-mcp-auth-test.lock.yml index 9eac4732252..282c2aa8a7e 100644 --- a/.github/workflows/github-remote-mcp-auth-test.lock.yml +++ b/.github/workflows/github-remote-mcp-auth-test.lock.yml @@ -81,6 +81,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index b8f997f9c6a..5bb6cad2e52 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/go-fan.lock.yml b/.github/workflows/go-fan.lock.yml index 13d280bb9dd..f173a85dcff 100644 --- a/.github/workflows/go-fan.lock.yml +++ b/.github/workflows/go-fan.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index 076eead7acb..893cf1258db 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 47f49051ad4..88ac93717b1 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/gpclean.lock.yml b/.github/workflows/gpclean.lock.yml index 40b5d3b4a30..c4691cbcfb5 100644 --- a/.github/workflows/gpclean.lock.yml +++ b/.github/workflows/gpclean.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index c2f4e751c00..a23d050aa30 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -101,6 +101,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index ccaa8941d0a..92d5375335d 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index f9d6383a45e..01b51fae0cb 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -81,6 +81,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index af419900953..4cbe18678e9 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -89,6 +89,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index f463681aa5f..bf66be081f0 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -422,6 +422,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml index 2e9552c555f..f86a900669e 100644 --- a/.github/workflows/issue-triage-agent.lock.yml +++ b/.github/workflows/issue-triage-agent.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index b188db865dc..c1fa895be0f 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -81,6 +81,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/layout-spec-maintainer.lock.yml b/.github/workflows/layout-spec-maintainer.lock.yml index 277dc3545a1..1ba5b4b1978 100644 --- a/.github/workflows/layout-spec-maintainer.lock.yml +++ b/.github/workflows/layout-spec-maintainer.lock.yml @@ -82,6 +82,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index d644dbeed3a..b35fafcd6a3 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index c6d9cfde079..445ca650489 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -100,6 +100,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index 0dd93ea5c7e..e02aafc20ed 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/metrics-collector.lock.yml b/.github/workflows/metrics-collector.lock.yml index 9050e0f3d07..65e05c7a80f 100644 --- a/.github/workflows/metrics-collector.lock.yml +++ b/.github/workflows/metrics-collector.lock.yml @@ -83,6 +83,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index 852cc567f74..c39b0cd61c5 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/org-health-report.lock.yml b/.github/workflows/org-health-report.lock.yml index 9924a9eac61..e2a07fd9a42 100644 --- a/.github/workflows/org-health-report.lock.yml +++ b/.github/workflows/org-health-report.lock.yml @@ -90,6 +90,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index baa45d0bc51..7d6db4ef02c 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -116,6 +116,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 14c4fa0a4c6..c4b4f889a04 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -98,6 +98,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index f3bfe0b7c72..88d607239fd 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -108,6 +108,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/portfolio-analyst.lock.yml b/.github/workflows/portfolio-analyst.lock.yml index a129897b394..e74178898e8 100644 --- a/.github/workflows/portfolio-analyst.lock.yml +++ b/.github/workflows/portfolio-analyst.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index a166f998097..e9fbcb30c04 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -126,6 +126,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/pr-triage-agent.lock.yml b/.github/workflows/pr-triage-agent.lock.yml index 6136f08d91d..78593475144 100644 --- a/.github/workflows/pr-triage-agent.lock.yml +++ b/.github/workflows/pr-triage-agent.lock.yml @@ -83,6 +83,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index 12132d6b4aa..76598aeb7ea 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -91,6 +91,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index 7c24694f890..0bf11c79cb6 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index 7851f7d6454..bae8faa3252 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -134,6 +134,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/refiner.lock.yml b/.github/workflows/refiner.lock.yml index 50078f0035b..45566f1c4e7 100644 --- a/.github/workflows/refiner.lock.yml +++ b/.github/workflows/refiner.lock.yml @@ -99,6 +99,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 51088127702..0a8ad14f615 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/repo-audit-analyzer.lock.yml b/.github/workflows/repo-audit-analyzer.lock.yml index 70f1a33eff1..68478a8e458 100644 --- a/.github/workflows/repo-audit-analyzer.lock.yml +++ b/.github/workflows/repo-audit-analyzer.lock.yml @@ -88,6 +88,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index 88a95bebf8e..0026d41fba8 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml index 59313203c6e..8041d0785cd 100644 --- a/.github/workflows/repository-quality-improver.lock.yml +++ b/.github/workflows/repository-quality-improver.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index 15aa56fc3f6..6aac3d8f1b7 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -88,6 +88,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml index 2cfe62fb59a..0154308b295 100644 --- a/.github/workflows/safe-output-health.lock.yml +++ b/.github/workflows/safe-output-health.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index 791459c8a20..089f6daaffe 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 90e03d5a40b..43fa6e6e73d 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -153,6 +153,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/security-alert-burndown.campaign.g.lock.yml b/.github/workflows/security-alert-burndown.campaign.g.lock.yml index eb25a3adda8..f27b9164a35 100644 --- a/.github/workflows/security-alert-burndown.campaign.g.lock.yml +++ b/.github/workflows/security-alert-burndown.campaign.g.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/security-compliance.lock.yml b/.github/workflows/security-compliance.lock.yml index 4192737ce39..4a9c07fca47 100644 --- a/.github/workflows/security-compliance.lock.yml +++ b/.github/workflows/security-compliance.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/security-review.lock.yml b/.github/workflows/security-review.lock.yml index a81b489d0db..b1f4debe362 100644 --- a/.github/workflows/security-review.lock.yml +++ b/.github/workflows/security-review.lock.yml @@ -98,6 +98,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 110b0b2cd97..ecfc3b61488 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/sergo.lock.yml b/.github/workflows/sergo.lock.yml index d22d0a451e1..b7262e5c160 100644 --- a/.github/workflows/sergo.lock.yml +++ b/.github/workflows/sergo.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 5c0bf66a4eb..6787b99884b 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -92,6 +92,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-agent-all-merged.lock.yml b/.github/workflows/smoke-agent-all-merged.lock.yml index 56469ad5309..81c33777627 100644 --- a/.github/workflows/smoke-agent-all-merged.lock.yml +++ b/.github/workflows/smoke-agent-all-merged.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-agent-all-none.lock.yml b/.github/workflows/smoke-agent-all-none.lock.yml index cba4550a129..3c05e0e135a 100644 --- a/.github/workflows/smoke-agent-all-none.lock.yml +++ b/.github/workflows/smoke-agent-all-none.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-agent-public-approved.lock.yml b/.github/workflows/smoke-agent-public-approved.lock.yml index 3fe2a59f447..09c71c11c52 100644 --- a/.github/workflows/smoke-agent-public-approved.lock.yml +++ b/.github/workflows/smoke-agent-public-approved.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-agent-public-none.lock.yml b/.github/workflows/smoke-agent-public-none.lock.yml index 26d28851359..270ad7cb55b 100644 --- a/.github/workflows/smoke-agent-public-none.lock.yml +++ b/.github/workflows/smoke-agent-public-none.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-agent-scoped-approved.lock.yml b/.github/workflows/smoke-agent-scoped-approved.lock.yml index e38b96346f7..538f22f5225 100644 --- a/.github/workflows/smoke-agent-scoped-approved.lock.yml +++ b/.github/workflows/smoke-agent-scoped-approved.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml index 56aa422879d..078f91b5182 100644 --- a/.github/workflows/smoke-call-workflow.lock.yml +++ b/.github/workflows/smoke-call-workflow.lock.yml @@ -91,6 +91,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index b113788e9ac..14fcefe2fd2 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -112,6 +112,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 69a6b28693f..c40a99e5107 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -102,6 +102,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate CODEX_API_KEY or OPENAI_API_KEY secret diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index 3cdc5c6d802..36fa8378108 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -101,6 +101,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 30f4fff4d51..a33d69ddc20 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -103,6 +103,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-create-cross-repo-pr.lock.yml b/.github/workflows/smoke-create-cross-repo-pr.lock.yml index 539923cb792..8ba5ed6fcf4 100644 --- a/.github/workflows/smoke-create-cross-repo-pr.lock.yml +++ b/.github/workflows/smoke-create-cross-repo-pr.lock.yml @@ -96,6 +96,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml index 08d7194473f..e9bc0c53fcc 100644 --- a/.github/workflows/smoke-gemini.lock.yml +++ b/.github/workflows/smoke-gemini.lock.yml @@ -102,6 +102,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate GEMINI_API_KEY secret diff --git a/.github/workflows/smoke-multi-pr.lock.yml b/.github/workflows/smoke-multi-pr.lock.yml index 983677ebc11..ee111689488 100644 --- a/.github/workflows/smoke-multi-pr.lock.yml +++ b/.github/workflows/smoke-multi-pr.lock.yml @@ -97,6 +97,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index e10e052885a..4e7cbe50d10 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-temporary-id.lock.yml b/.github/workflows/smoke-temporary-id.lock.yml index 97ac0a84044..14b533d0a44 100644 --- a/.github/workflows/smoke-temporary-id.lock.yml +++ b/.github/workflows/smoke-temporary-id.lock.yml @@ -95,6 +95,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-test-tools.lock.yml b/.github/workflows/smoke-test-tools.lock.yml index 4d620114c19..506a00981df 100644 --- a/.github/workflows/smoke-test-tools.lock.yml +++ b/.github/workflows/smoke-test-tools.lock.yml @@ -97,6 +97,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-update-cross-repo-pr.lock.yml b/.github/workflows/smoke-update-cross-repo-pr.lock.yml index 7ae3182e423..dfc94183aca 100644 --- a/.github/workflows/smoke-update-cross-repo-pr.lock.yml +++ b/.github/workflows/smoke-update-cross-repo-pr.lock.yml @@ -96,6 +96,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Checkout .github and .agents folders diff --git a/.github/workflows/smoke-workflow-call-with-inputs.lock.yml b/.github/workflows/smoke-workflow-call-with-inputs.lock.yml index 3532e0fbfad..62f581b456b 100644 --- a/.github/workflows/smoke-workflow-call-with-inputs.lock.yml +++ b/.github/workflows/smoke-workflow-call-with-inputs.lock.yml @@ -119,6 +119,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/smoke-workflow-call.lock.yml b/.github/workflows/smoke-workflow-call.lock.yml index 92ebe808e24..ca4689f4196 100644 --- a/.github/workflows/smoke-workflow-call.lock.yml +++ b/.github/workflows/smoke-workflow-call.lock.yml @@ -122,6 +122,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 452cc094c3f..d192ccc0a9c 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -99,6 +99,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml index e2dc6063b21..8103b134080 100644 --- a/.github/workflows/static-analysis-report.lock.yml +++ b/.github/workflows/static-analysis-report.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/step-name-alignment.lock.yml b/.github/workflows/step-name-alignment.lock.yml index bf2133fb8db..20b7882af06 100644 --- a/.github/workflows/step-name-alignment.lock.yml +++ b/.github/workflows/step-name-alignment.lock.yml @@ -81,6 +81,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/sub-issue-closer.lock.yml b/.github/workflows/sub-issue-closer.lock.yml index 35b53440bd8..6668de5cfa2 100644 --- a/.github/workflows/sub-issue-closer.lock.yml +++ b/.github/workflows/sub-issue-closer.lock.yml @@ -81,6 +81,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 5a79dddd590..e4103ca65c3 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index bc889df7694..91bcccda584 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -89,6 +89,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/terminal-stylist.lock.yml b/.github/workflows/terminal-stylist.lock.yml index 8aceb78a245..a8b7d95b040 100644 --- a/.github/workflows/terminal-stylist.lock.yml +++ b/.github/workflows/terminal-stylist.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/test-create-pr-error-handling.lock.yml b/.github/workflows/test-create-pr-error-handling.lock.yml index f35a49fda48..4f5640ad413 100644 --- a/.github/workflows/test-create-pr-error-handling.lock.yml +++ b/.github/workflows/test-create-pr-error-handling.lock.yml @@ -78,6 +78,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/test-dispatcher.lock.yml b/.github/workflows/test-dispatcher.lock.yml index 556e38f5dec..03f399fc874 100644 --- a/.github/workflows/test-dispatcher.lock.yml +++ b/.github/workflows/test-dispatcher.lock.yml @@ -77,6 +77,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index 39eca7b24cf..bae45f4e62b 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -77,6 +77,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/test-workflow.lock.yml b/.github/workflows/test-workflow.lock.yml index 27b145b3437..b575710fb67 100644 --- a/.github/workflows/test-workflow.lock.yml +++ b/.github/workflows/test-workflow.lock.yml @@ -82,6 +82,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 25e79d76324..77bf3ee11ea 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -106,6 +106,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml index 8aba7d29b54..f6b4f3e6798 100644 --- a/.github/workflows/typist.lock.yml +++ b/.github/workflows/typist.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/ubuntu-image-analyzer.lock.yml b/.github/workflows/ubuntu-image-analyzer.lock.yml index 70a82a3a07c..481a22668d2 100644 --- a/.github/workflows/ubuntu-image-analyzer.lock.yml +++ b/.github/workflows/ubuntu-image-analyzer.lock.yml @@ -88,6 +88,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index fe0ecaa83f8..fe4bdc8eca0 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -103,6 +103,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate ANTHROPIC_API_KEY secret diff --git a/.github/workflows/update-astro.lock.yml b/.github/workflows/update-astro.lock.yml index 6192e11866d..2ac4ad72543 100644 --- a/.github/workflows/update-astro.lock.yml +++ b/.github/workflows/update-astro.lock.yml @@ -86,6 +86,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index ff4088f5b0f..ca4d7c5477c 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/weekly-blog-post-writer.lock.yml b/.github/workflows/weekly-blog-post-writer.lock.yml new file mode 100644 index 00000000000..b44a1c8469c --- /dev/null +++ b/.github/workflows/weekly-blog-post-writer.lock.yml @@ -0,0 +1,1432 @@ +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw. DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Generates a weekly blog post summarizing gh-aw releases, changelogs, and highlights from the past week, then opens a pull request for review +# +# Resolved workflow manifest: +# Imports: +# - shared/mcp/qmd-docs.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"bccf86997b5e61ac97e28d9a6979b8c67b2605ba9fe717b26e672688edcf5a15","strict":true} + +name: "Weekly Blog Post Writer" +"on": + schedule: + - cron: "0 5 * * 1" + # Friendly format: weekly on monday (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Weekly Blog Post Writer" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_INFO_VERSION: "" + GH_AW_INFO_AGENT_VERSION: "latest" + GH_AW_INFO_WORKFLOW_NAME: "Weekly Blog Post Writer" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.24.2" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "weekly-blog-post-writer.lock.yml" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_EOF' + + Tools: create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/mcp/qmd-docs.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/weekly-blog-post-writer.md}} + GH_AW_PROMPT_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MEMORY_BRANCH_NAME: 'master' + GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Max File Size**: 10240 bytes (0.01 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 100 KB)\n" + GH_AW_MEMORY_DESCRIPTION: ' Agent of the Week history – tracks which workflows have been featured so we rotate fairly' + GH_AW_MEMORY_DIR: '/tmp/gh-aw/repo-memory/default/' + GH_AW_MEMORY_TARGET_REPO: ' of the current repository' + GH_AW_WIKI_NOTE: "\n\n> **GitHub Wiki**: This memory is backed by the GitHub Wiki for this repository. Files use GitHub Wiki Markdown syntax. Follow GitHub Wiki conventions when creating or editing pages (e.g., use standard Markdown headers, use `[[Page Name]]` syntax for internal wiki links, name page files with spaces replaced by hyphens or use the wiki page title as the filename)." + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_SERVER_URL: process.env.GH_AW_GITHUB_SERVER_URL, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MEMORY_BRANCH_NAME: process.env.GH_AW_MEMORY_BRANCH_NAME, + GH_AW_MEMORY_CONSTRAINTS: process.env.GH_AW_MEMORY_CONSTRAINTS, + GH_AW_MEMORY_DESCRIPTION: process.env.GH_AW_MEMORY_DESCRIPTION, + GH_AW_MEMORY_DIR: process.env.GH_AW_MEMORY_DIR, + GH_AW_MEMORY_TARGET_REPO: process.env.GH_AW_MEMORY_TARGET_REPO, + GH_AW_WIKI_NOTE: process.env.GH_AW_WIKI_NOTE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: weeklyblogpostwriter + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Set runtime paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_ENV" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_ENV" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_ENV" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Go for CLI build + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: go.mod + cache: true + - name: Build gh-aw CLI + run: | + echo "Building gh-aw CLI for linux/amd64..." + mkdir -p dist + VERSION=$(git describe --tags --always --dirty) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "-s -w -X main.version=${VERSION}" \ + -o dist/gh-aw-linux-amd64 \ + ./cmd/gh-aw + # Copy binary to root for direct execution in user-defined steps + cp dist/gh-aw-linux-amd64 ./gh-aw + chmod +x ./gh-aw + echo "✓ Built gh-aw CLI successfully" + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Build gh-aw Docker image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + platforms: linux/amd64 + push: false + load: true + tags: localhost/gh-aw:dev + build-args: | + BINARY=dist/gh-aw-linux-amd64 + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: '24' + package-manager-cache: false + - name: Create gh-aw temp directory + run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure gh CLI for GitHub Enterprise + run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh + env: + GH_TOKEN: ${{ github.token }} + - name: Install QMD + run: npm install -g @tobilu/qmd + - name: Restore QMD index cache + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} + path: ~/.cache/qmd + restore-keys: qmd-docs- + - name: Register QMD collections + run: | + qmd collection add "${GITHUB_WORKSPACE}" --name gh-aw --glob "docs/src/content/docs/**,.github/agents/**,.github/aw/**" 2>/dev/null || true + + # Repo memory git-based storage configuration from frontmatter processed below + - name: Clone wiki-memory branch (default) + env: + GH_TOKEN: ${{ github.token }} + GITHUB_SERVER_URL: ${{ github.server_url }} + BRANCH_NAME: master + TARGET_REPO: ${{ github.repository }}.wiki + MEMORY_DIR: /tmp/gh-aw/repo-memory/default + CREATE_ORPHAN: false + run: bash ${RUNNER_TEMP}/gh-aw/actions/clone_repo_memory_branch.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + (github.event.pull_request) || (github.event.issue.pull_request) + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.24.2 + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Install gh-aw extension + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + # Check if gh-aw extension is already installed + if gh extension list | grep -q "github/gh-aw"; then + echo "gh-aw extension already installed, upgrading..." + gh extension upgrade gh-aw || true + else + echo "Installing gh-aw extension..." + gh extension install github/gh-aw + fi + gh aw --version + # Copy the gh-aw binary to ${RUNNER_TEMP}/gh-aw for MCP server containerization + mkdir -p ${RUNNER_TEMP}/gh-aw + GH_AW_BIN=$(which gh-aw 2>/dev/null || find ~/.local/share/gh/extensions/gh-aw -name 'gh-aw' -type f 2>/dev/null | head -1) + if [ -n "$GH_AW_BIN" ] && [ -f "$GH_AW_BIN" ]; then + cp "$GH_AW_BIN" ${RUNNER_TEMP}/gh-aw/gh-aw + chmod +x ${RUNNER_TEMP}/gh-aw/gh-aw + echo "Copied gh-aw binary to ${RUNNER_TEMP}/gh-aw/gh-aw" + else + echo "::error::Failed to find gh-aw binary for MCP server" + exit 1 + fi + - name: Write Safe Outputs Config + run: | + mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"create_pull_request":{"expires":168,"max":1,"reviewers":["copilot"],"title_prefix":"[blog] "},"missing_data":{},"missing_tool":{},"noop":{"max":1},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + - name: Write Safe Outputs Tools + run: | + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_EOF' + { + "description_suffixes": { + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[blog] \". Labels [\"blog\"] will be automatically added. Reviewers [\"copilot\"] will be assigned." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_SAFE_OUTPUTS_TOOLS_META_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh + + - name: Setup MCP Scripts Config + run: | + mkdir -p ${RUNNER_TEMP}/gh-aw/mcp-scripts/logs + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/tools.json << 'GH_AW_MCP_SCRIPTS_TOOLS_EOF' + { + "serverName": "mcpscripts", + "version": "1.0.0", + "logDir": "${RUNNER_TEMP}/gh-aw/mcp-scripts/logs", + "tools": [ + { + "name": "qmd-query", + "description": "Find relevant file paths in project documentation using vector similarity search. Returns file paths and scores.", + "inputSchema": { + "properties": { + "min_score": { + "default": 0.4, + "description": "Minimum relevance score threshold (0–1)", + "type": "number" + }, + "query": { + "description": "Natural language search query", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "handler": "qmd-query.sh", + "timeout": 60 + } + ] + } + GH_AW_MCP_SCRIPTS_TOOLS_EOF + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs << 'GH_AW_MCP_SCRIPTS_SERVER_EOF' + const path = require("path"); + const { startHttpServer } = require("./mcp_scripts_mcp_server_http.cjs"); + const configPath = path.join(__dirname, "tools.json"); + const port = parseInt(process.env.GH_AW_MCP_SCRIPTS_PORT || "3000", 10); + const apiKey = process.env.GH_AW_MCP_SCRIPTS_API_KEY || ""; + startHttpServer(configPath, { + port: port, + stateless: true, + logDir: "${RUNNER_TEMP}/gh-aw/mcp-scripts/logs" + }).catch(error => { + console.error("Failed to start mcp-scripts HTTP server:", error); + process.exit(1); + }); + GH_AW_MCP_SCRIPTS_SERVER_EOF + chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/mcp-server.cjs + + - name: Setup MCP Scripts Tool Files + run: | + cat > ${RUNNER_TEMP}/gh-aw/mcp-scripts/qmd-query.sh << 'GH_AW_MCP_SCRIPTS_SH_QMD-QUERY_EOF' + #!/bin/bash + # Auto-generated mcp-script tool: qmd-query + # Find relevant file paths in project documentation using vector similarity search. Returns file paths and scores. + + set -euo pipefail + + set -e + qmd vsearch "$INPUT_QUERY" --files --min-score "${INPUT_MIN_SCORE:-0.4}" + + + GH_AW_MCP_SCRIPTS_SH_QMD-QUERY_EOF + chmod +x ${RUNNER_TEMP}/gh-aw/mcp-scripts/qmd-query.sh + + - name: Generate MCP Scripts Server Config + id: mcp-scripts-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3000 + + # Set outputs for next steps + { + echo "mcp_scripts_api_key=${API_KEY}" + echo "mcp_scripts_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "MCP Scripts server will run on port ${PORT}" + + - name: Start MCP Scripts HTTP Server + id: mcp-scripts-start + env: + DEBUG: '*' + GH_AW_MCP_SCRIPTS_PORT: ${{ steps.mcp-scripts-config.outputs.mcp_scripts_port }} + GH_AW_MCP_SCRIPTS_API_KEY: ${{ steps.mcp-scripts-config.outputs.mcp_scripts_api_key }} + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_MCP_SCRIPTS_PORT + export GH_AW_MCP_SCRIPTS_API_KEY + + bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_scripts_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_MCP_SCRIPTS_API_KEY: ${{ steps.mcp-scripts-start.outputs.api_key }} + GH_AW_MCP_SCRIPTS_PORT: ${{ steps.mcp-scripts-start.outputs.port }} + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_MCP_SCRIPTS_PORT -e GH_AW_MCP_SCRIPTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.17' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "agenticworkflows": { + "type": "stdio", + "container": "localhost/gh-aw:dev", + "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], + "args": ["--network", "host", "-w", "\${GITHUB_WORKSPACE}"], + "env": { + "DEBUG": "*", + "GITHUB_TOKEN": "\${GITHUB_TOKEN}", + "GITHUB_ACTOR": "\${GITHUB_ACTOR}", + "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "private:github/gh-aw" + ] + } + } + }, + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "repos,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "approved", + "repos": [ + "github/gh-aw" + ] + } + } + }, + "mcpscripts": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_MCP_SCRIPTS_PORT", + "headers": { + "Authorization": "\${GH_AW_MCP_SCRIPTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "private:github/gh-aw" + ] + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "private:github/gh-aw" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 30 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.2 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: dev + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh + - name: Copy Safe Outputs + if: always() + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Scripts logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_scripts_logs.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + # Upload repo memory as artifacts for push job + - name: Upload wiki-memory artifact (default) + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: repo-memory-default + path: /tmp/gh-aw/repo-memory/default + retention-days: 1 + if-no-files-found: ignore + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/mcp-scripts/logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + if-no-files-found: ignore + # --- Threat Detection (inline) --- + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }} + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Weekly Blog Post Writer" + WORKFLOW_DESCRIPTION: "Generates a weekly blog post summarizing gh-aw releases, changelogs, and highlights from the past week, then opens a pull request for review" + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.2 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: dev + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_detection_results + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Set detection conclusion + id: detection_conclusion + if: always() + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }} + run: | + if [[ "$RUN_DETECTION" != "true" ]]; then + echo "conclusion=skipped" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection was not needed, marking as skipped" + elif [[ "$DETECTION_SUCCESS" == "true" ]]; then + echo "conclusion=success" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection passed successfully" + else + echo "conclusion=failure" >> "$GITHUB_OUTPUT" + echo "success=false" >> "$GITHUB_OUTPUT" + echo "Detection found issues" + fi + + conclusion: + needs: + - activation + - agent + - push_repo_memory + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-weekly-blog-post-writer" + cancel-in-progress: false + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Weekly Blog Post Writer" + GH_AW_TRACKER_ID: "weekly-blog-post-writer" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Blog Post Writer" + GH_AW_TRACKER_ID: "weekly-blog-post-writer" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Blog Post Writer" + GH_AW_TRACKER_ID: "weekly-blog-post-writer" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "weekly-blog-post-writer" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_PUSH_REPO_MEMORY_RESULT: ${{ needs.push_repo_memory.result }} + GH_AW_REPO_MEMORY_VALIDATION_FAILED_default: ${{ needs.push_repo_memory.outputs.validation_failed_default }} + GH_AW_REPO_MEMORY_VALIDATION_ERROR_default: ${{ needs.push_repo_memory.outputs.validation_error_default }} + GH_AW_REPO_MEMORY_PATCH_SIZE_EXCEEDED_default: ${{ needs.push_repo_memory.outputs.patch_size_exceeded_default }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "30" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Blog Post Writer" + GH_AW_TRACKER_ID: "weekly-blog-post-writer" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Handle Create Pull Request Error + id: handle_create_pr_error + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Blog Post Writer" + GH_AW_TRACKER_ID: "weekly-blog-post-writer" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_create_pr_error.cjs'); + await main(); + + push_repo_memory: + needs: agent + if: always() && needs.agent.outputs.detection_success == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: "push-repo-memory-${{ github.repository }}" + cancel-in-progress: false + outputs: + patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} + validation_error_default: ${{ steps.push_repo_memory_default.outputs.validation_error }} + validation_failed_default: ${{ steps.push_repo_memory_default.outputs.validation_failed }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: . + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Download wiki-memory artifact (default) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: repo-memory-default + path: /tmp/gh-aw/repo-memory/default + - name: Push wiki-memory changes (default) + id: push_repo_memory_default + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + GITHUB_SERVER_URL: ${{ github.server_url }} + ARTIFACT_DIR: /tmp/gh-aw/repo-memory/default + MEMORY_ID: default + TARGET_REPO: ${{ github.repository }}.wiki + BRANCH_NAME: master + REPO_MEMORY_ALLOWED_REPOS: ${{ github.repository }}.wiki + MAX_FILE_SIZE: 10240 + MAX_FILE_COUNT: 100 + MAX_PATCH_SIZE: 10240 + ALLOWED_EXTENSIONS: '[]' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/push_repo_memory.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/weekly-blog-post-writer" + GH_AW_ENGINE_ID: "copilot" + GH_AW_TRACKER_ID: "weekly-blog-post-writer" + GH_AW_WORKFLOW_ID: "weekly-blog-post-writer" + GH_AW_WORKFLOW_NAME: "Weekly Blog Post Writer" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false + - name: Setup Scripts + uses: ./actions/setup + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"expires\":168,\"labels\":[\"blog\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"reviewers\":[\"copilot\"],\"title_prefix\":\"[blog] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Output Items Manifest + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: safe-output-items + path: /tmp/safe-output-items.jsonl + if-no-files-found: warn + diff --git a/.github/workflows/weekly-blog-post-writer.md b/.github/workflows/weekly-blog-post-writer.md new file mode 100644 index 00000000000..b30555ed860 --- /dev/null +++ b/.github/workflows/weekly-blog-post-writer.md @@ -0,0 +1,371 @@ +--- +name: Weekly Blog Post Writer +description: Generates a weekly blog post summarizing gh-aw releases, changelogs, and highlights from the past week, then opens a pull request for review +on: + schedule: weekly on monday + workflow_dispatch: +permissions: + contents: read + pull-requests: read + actions: read +tracker-id: weekly-blog-post-writer +engine: copilot +strict: true +timeout-minutes: 30 + +tools: + agentic-workflows: + edit: + bash: ["*"] + github: + lockdown: false + repos: + - github/gh-aw + min-integrity: approved + toolsets: + - repos + - pull_requests + repo-memory: + wiki: true + description: "Agent of the Week history – tracks which workflows have been featured so we rotate fairly" + +imports: + - shared/mcp/qmd-docs.md + +safe-outputs: + create-pull-request: + expires: 7d + title-prefix: "[blog] " + labels: [blog] + reviewers: [copilot] + draft: false +--- + +# Weekly Blog Post Writer + +You are the Weekly Blog Post Writer for the **GitHub Agentic Workflows** (`gh-aw`) project. Your job is to review what happened in the repository over the past week and write an engaging, informative blog post for the Astro Starlight documentation blog. + +## Context + +- **Repository**: ${{ github.repository }} +- **Run ID**: ${{ github.run_id }} +- **Run URL**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + +## Process + +### Step 1: Determine the Date Range + +Use bash to get today's date: + +```bash +TODAY=$(date -u +%Y-%m-%d) +echo "Today: $TODAY" +``` + +Store today's date for use throughout the workflow. You will use the GitHub API's `since` parameter (ISO 8601 format, e.g. `7 days ago`) to filter results rather than computing LAST_WEEK yourself. + +### Step 2: Review Recent Releases + +Use the GitHub `list_releases` tool to fetch all releases in the repository. Look for any releases published in the past 7 days. + +For each recent release: +- Note the **tag name** (e.g., `v1.2.3`) +- Note the **release URL**: `https://github.com/${{ github.repository }}/releases/tag/` +- Extract the **release notes** (body) which describes what changed +- Note the **published date** + +If there are no recent releases, still proceed — you will write about recent commits and pull requests instead. + +### Step 3: Review Recent Pull Requests + +Use the GitHub `list_pull_requests` tool to fetch pull requests that were **merged** in the past 7 days. Look at the merged PRs to understand what changed. + +For each merged PR: +- Note the **PR number and title** +- Note the **PR URL**: `https://github.com/${{ github.repository }}/pull/` +- Read the **body** for context on the change +- Note any interesting labels (new feature, bug fix, documentation, etc.) + +Focus on the most impactful and interesting changes — things users would care about. + +### Step 4: Identify Key Highlights + +From the releases and pull requests, identify the top 3–5 highlights to feature in the blog post: + +1. **New features or capabilities** — What can users do now that they couldn't before? +2. **Bug fixes or reliability improvements** — What problems were solved? +3. **Documentation or example improvements** — What resources are better now? +4. **Workflow improvements** — What agentic workflows were added or improved? +5. **Performance or security improvements** — Any technical wins? + +Prioritize by user impact and interestingness. + +### Step 5: Pick the Agent of the Week + +Every blog post must include an **Agent of the Week** spotlight that celebrates one of the active agentic workflows in the repository. + +#### 5-1: Load the Featured Workflows History + +Read the wiki memory to find the list of workflows already featured as Agent of the Week. The wiki file is at `/tmp/gh-aw/repo-memory/wiki/agent-of-the-week.md`. If it doesn't exist yet, start fresh — every workflow is eligible. + +```bash +cat /tmp/gh-aw/repo-memory/wiki/agent-of-the-week.md 2>/dev/null || echo "(no history yet)" +``` + +#### 5-2: List All Active Workflows + +Use the `agentic-workflows` MCP `list` tool to get all workflows in the repository: + +**Tool**: `list` +**Parameters**: `{}` + +From the list, exclude: +- Workflows already featured in the wiki history +- Test workflows (names starting with `test-`) +- The `weekly-blog-post-writer` itself + +If all workflows have been featured, reset and start again from the beginning (oldest featured first). + +#### 5-3: Query Recent Logs for the Chosen Workflow + +Pick the workflow that has been active most recently and **hasn't been featured yet** (or was featured longest ago). To understand what it actually does in practice, use the `agentic-workflows` MCP `logs` tool to fetch its recent run logs: + +**Tool**: `logs` +**Parameters**: +```json +{ + "workflow_name": "", + "count": 3, + "start_date": "-30d" +} +``` + +Read the logs to understand: +- What the workflow actually did in its recent runs +- Any funny moments, surprising outputs, or interesting patterns +- Typical inputs and outputs +- How often it runs and whether it's been busy lately + +#### 5-4: Write the Agent of the Week Section + +Write a fun, engaging spotlight section with these elements: + +1. **Name + link**: Workflow name as a GitHub link to `.github/workflows/.md` in the repository +2. **What it does**: One-sentence description of its job +3. **Recent adventures** (2–3 sentences): Based on the actual log data, describe what this workflow has been up to recently. Be specific — mention real outputs, patterns, or quirks you saw in the logs. This should feel like you're narrating a little story about the workflow's week. +4. **Funny anecdote** (1–2 sentences): Something amusing, surprising, or relatable from the logs — e.g., "This week it decided three different issues all needed the label 'cookie'" or "It reviewed 47 PRs and still found time to leave a thoughtful comment on a 2-year-old issue." +5. **Usage tip** (1 sentence): One practical insight about when or why to use this type of workflow. + +**Tone**: Warm, funny, and affectionate — like introducing a colleague at a team meeting. Reference the logs authentically; don't make things up. + +**Format**: +```markdown +## 🤖 Agent of the Week: [Workflow Name] + +[What it does — one sentence] + +[Recent adventures paragraph based on real log data] + +[Funny anecdote sentence] + +💡 **Usage tip**: [One practical insight] + +→ [View the workflow on GitHub](https://github.com/${{ github.repository }}/blob/main/.github/workflows/{workflow-filename}.md) +``` + +Where `{workflow-filename}` is the workflow's filename without the `.md` extension (e.g., `auto-triage-issues` for `auto-triage-issues.md`). + +#### 5-5: Update the Wiki History + +After choosing the workflow, update the wiki file at `/tmp/gh-aw/repo-memory/wiki/agent-of-the-week.md` using the `edit` tool. Append an entry with today's date and the chosen workflow name: + +```markdown +## Featured Workflows + + +- YYYY-MM-DD | +``` + +If the file doesn't exist, create it with the header above. The `edit` tool will write it to the wiki memory path automatically. + +### Step 6: Determine the Blog Post Filename + +Use today's date to form the blog post filename: + +```bash +date -u +%Y-%m-%d +``` + +The file should be named: `YYYY-MM-DD-weekly-update.md` +(e.g., `2026-03-18-weekly-update.md`) + +Check if a blog post with this name already exists in `docs/src/content/docs/blog/` by running: + +```bash +ls docs/src/content/docs/blog/YYYY-MM-DD-weekly-update.md 2>/dev/null && echo "exists" || echo "not found" +``` + +If the file already exists, use a different suffix like `YYYY-MM-DD-weekly-update-2.md`. + +### Step 7: Write the Blog Post + +Create a new file at `docs/src/content/docs/blog/YYYY-MM-DD-weekly-update.md` using the `edit` tool. + +The blog post must follow the **GitHub blog tone**: clear, helpful, developer-friendly, and enthusiastic about the features. Write in second person ("you") when talking about what users can do. Be specific — include exact version numbers, feature names, and link to GitHub URLs. Avoid jargon and keep sentences readable. + +Use the following frontmatter template: + +```markdown +--- +title: "Weekly Update – " +description: "" +authors: + - copilot +date: YYYY-MM-DD +--- +``` + +Then write the blog post body. Structure it as follows: + +#### Blog Post Structure + +1. **Opening paragraph** (2–3 sentences): Summarize what happened this week in a friendly, engaging way. Reference the repository and link to GitHub. + +2. **Release Highlights** (if there were releases): For each release, include: + - The version number linked to its GitHub release page + - A 2–3 sentence summary of the key changes + - Bullet points for notable features or fixes, each linked to the relevant PR or commit on GitHub + +3. **Notable Pull Requests** (if no releases, or to supplement releases): Highlight 3–5 merged PRs with: + - PR title linked to the PR URL on GitHub + - A sentence explaining the change and why it matters + +4. **🤖 Agent of the Week**: Include the full spotlight section written in Step 4b. This section is **required** in every blog post. + +5. **Closing paragraph** (1–2 sentences): Encourage readers to check out the release, try the new features, or contribute. Link to the repository or releases page on GitHub. + +#### Tone Guidelines + +- **Enthusiastic but professional**: Like the GitHub blog — excited about the work, but clear and informative +- **Developer-focused**: Speak to people who will use these features +- **Specific and linked**: Every mention of a version, PR, commit, or release should be a hyperlink to GitHub +- **No filler content**: If there's nothing notable this week, keep the post brief and honest about it +- **Active voice**: "We shipped X" not "X was shipped" + +#### GitHub URL Formats to Use + +Always link to GitHub URLs for traceability: +- **Release**: `https://github.com/${{ github.repository }}/releases/tag/vX.Y.Z` +- **Pull Request**: `https://github.com/${{ github.repository }}/pull/NUMBER` +- **Commit**: `https://github.com/${{ github.repository }}/commit/SHA` +- **Compare**: `https://github.com/${{ github.repository }}/compare/vX.Y.Z-1...vX.Y.Z` +- **Repository**: `https://github.com/${{ github.repository }}` + +#### Example Blog Post (for reference — do not copy this verbatim) + +```markdown +--- +title: "Weekly Update – March 18, 2026" +description: "This week brings v1.5.0 with improved MCP server support and a new codex engine." +authors: + - copilot +date: 2026-03-18 +--- + +Another week, another set of improvements to GitHub Agentic Workflows! Here's a look at what shipped in [github/gh-aw](https://github.com/github/gh-aw) this week. + +## Release: v1.5.0 + +[v1.5.0](https://github.com/github/gh-aw/releases/tag/v1.5.0) landed on March 15th, bringing several quality-of-life improvements for workflow authors. + +### What's New + +- **Improved MCP server support** ([#1234](https://github.com/github/gh-aw/pull/1234)): MCP servers now support remote configuration, making it easier to use hosted MCP services without local setup. +- **New `codex` engine option** ([#1235](https://github.com/github/gh-aw/pull/1235)): You can now run workflows using the Codex engine by setting `engine: codex` in your frontmatter. +- **Fixed schedule parsing for monthly crons** ([#1236](https://github.com/github/gh-aw/pull/1236)): Monthly schedules using `schedule: monthly` now compile correctly. + +## 🤖 Agent of the Week: auto-triage-issues + +The unsung hero of issue management — reads every new issue and labels it so the right people see it. + +This week `auto-triage-issues` processed 23 incoming issues, correctly labeling 21 of them on the first try. It spotted a subtle pattern: three separate reports of the same compile bug filed under completely different titles and quietly tagged them all with `duplicate` before anyone noticed. + +It also briefly labeled a feature request as `security` because the issue title contained the word "injection" — in reference to dependency injection. Classic. + +💡 **Usage tip**: Pair this with a `notify` workflow on the `security` label so the team gets paged for real security issues. + +→ [View the workflow on GitHub](https://github.com/github/gh-aw/blob/main/.github/workflows/auto-triage-issues.md) + +## Try It Out + +Update to [v1.5.0](https://github.com/github/gh-aw/releases/tag/v1.5.0) today and let us know what you think. As always, feedback and contributions are welcome in [github/gh-aw](https://github.com/github/gh-aw). +``` + +### Step 8: Create the Pull Request + +After creating the blog post file, use the `create-pull-request` safe output to open a pull request with: + +- **Title**: `Weekly blog post – ` +- **Body**: Include a summary of what the blog post covers and links to the releases/PRs that inspired it. + +Use this template for the PR body: + +```markdown +## Weekly Blog Post – + +This PR adds a weekly update blog post covering activity in [github/gh-aw](https://github.com/github/gh-aw) from the past week. + +### What's Covered + + + +### File Added + +- `docs/src/content/docs/blog/-weekly-update.md` + +--- +*Generated by the [weekly-blog-post-writer](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow.* +``` + +## No-Action Scenario + +If there were no releases and no noteworthy pull requests merged in the past 7 days: + +**Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures. + +```json +{"noop": {"message": "No action needed: No releases or notable pull requests merged in the past 7 days. Skipping blog post creation."}} +``` + +## Quality Standards + +Ensure the blog post: +- ✅ Has a valid Astro Starlight frontmatter block +- ✅ Uses `copilot` as the author +- ✅ Is dated with today's date in `YYYY-MM-DD` format +- ✅ Contains accurate information (no hallucinated releases or features) +- ✅ Links every release, PR, and commit reference to its GitHub URL +- ✅ Follows GitHub blog tone (helpful, developer-friendly, specific) +- ✅ Includes an **Agent of the Week** section with real log-based anecdotes +- ✅ Wiki memory updated with today's featured workflow +- ✅ Is between 300 and 1000 words (concise but informative) + +## Error Handling + +- If the GitHub API returns no data, try with a broader date range (14 days) +- If a blog file already exists for today's date, use a numbered suffix +- If you cannot fetch release data, write a PR-focused post instead +- Always create something useful — do not silently fail + +## Success Criteria + +You have successfully completed this task when: +- ✅ All releases and notable PRs from the past 7 days have been reviewed +- ✅ An Agent of the Week has been selected using the `agentic-workflows` MCP logs tool +- ✅ The wiki memory has been updated with the featured workflow +- ✅ A blog post file has been created in `docs/src/content/docs/blog/` +- ✅ The blog post uses correct Astro Starlight frontmatter +- ✅ All version/PR/commit references link to GitHub URLs +- ✅ The Agent of the Week section is present with real log-based anecdotes +- ✅ A pull request has been opened with the `blog` label, OR +- ✅ A `noop` call explains why no blog post was needed this week diff --git a/.github/workflows/weekly-editors-health-check.lock.yml b/.github/workflows/weekly-editors-health-check.lock.yml index fd0d6b9f019..15b6cf04734 100644 --- a/.github/workflows/weekly-editors-health-check.lock.yml +++ b/.github/workflows/weekly-editors-health-check.lock.yml @@ -81,6 +81,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml index d141e5c4def..8379a52d12f 100644 --- a/.github/workflows/weekly-issue-summary.lock.yml +++ b/.github/workflows/weekly-issue-summary.lock.yml @@ -89,6 +89,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml index 022ad62173d..44bdb4ad272 100644 --- a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml +++ b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml @@ -84,6 +84,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/workflow-generator.lock.yml b/.github/workflows/workflow-generator.lock.yml index 02de6dff97e..5b741a547f8 100644 --- a/.github/workflows/workflow-generator.lock.yml +++ b/.github/workflows/workflow-generator.lock.yml @@ -93,6 +93,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml index cb51e7192fe..007750bc179 100644 --- a/.github/workflows/workflow-health-manager.lock.yml +++ b/.github/workflows/workflow-health-manager.lock.yml @@ -87,6 +87,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/workflow-normalizer.lock.yml b/.github/workflows/workflow-normalizer.lock.yml index 02c51aeaf2f..e24093c80c7 100644 --- a/.github/workflows/workflow-normalizer.lock.yml +++ b/.github/workflows/workflow-normalizer.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/.github/workflows/workflow-skill-extractor.lock.yml b/.github/workflows/workflow-skill-extractor.lock.yml index 7b2c8cec87e..e18d85ac387 100644 --- a/.github/workflows/workflow-skill-extractor.lock.yml +++ b/.github/workflows/workflow-skill-extractor.lock.yml @@ -85,6 +85,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index db5696d4538..9f21355ab4c 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); /** @type {typeof import("crypto")} */ const crypto = require("crypto"); const { updateActivationComment } = require("./update_activation_comment.cjs"); +const { pushSignedCommits } = require("./push_signed_commits.cjs"); const { getTrackerID } = require("./get_tracker_id.cjs"); const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs"); @@ -81,6 +82,7 @@ function enforcePullRequestLimits(patchContent) { throw new Error(`E003: Cannot create pull request with more than ${MAX_FILES} files (received ${fileCount})`); } } + /** * Generate a patch preview with max 500 lines and 2000 chars for issue body * @param {string} patchContent - The full patch content @@ -739,7 +741,14 @@ async function main(config = {}) { core.info(`Renamed branch to ${branchName}`); } - await exec.exec(`git push origin ${branchName}`); + await pushSignedCommits({ + githubClient, + owner: repoParts.owner, + repo: repoParts.repo, + branch: branchName, + baseRef: `origin/${baseBranch}`, + cwd: process.cwd(), + }); core.info("Changes pushed to branch"); // Count new commits on PR branch relative to base, used to restrict @@ -900,7 +909,14 @@ ${patchPreview}`; core.info(`Renamed branch to ${branchName}`); } - await exec.exec(`git push origin ${branchName}`); + await pushSignedCommits({ + githubClient, + owner: repoParts.owner, + repo: repoParts.repo, + branch: branchName, + baseRef: `origin/${baseBranch}`, + cwd: process.cwd(), + }); core.info("Empty branch pushed successfully"); // Count new commits (will be 1 from the Initialize commit) diff --git a/actions/setup/js/generate_git_patch.cjs b/actions/setup/js/generate_git_patch.cjs index 50684574e29..c71f00c3a1c 100644 --- a/actions/setup/js/generate_git_patch.cjs +++ b/actions/setup/js/generate_git_patch.cjs @@ -165,7 +165,9 @@ async function generateGitPatch(branchName, baseBranch, options = {}) { // INCREMENTAL MODE (for push_to_pull_request_branch): // Only include commits that are new since origin/branchName. // This prevents including commits that already exist on the PR branch. - // We must explicitly fetch origin/branchName and fail if it doesn't exist. + // Prefer a fresh fetch of origin/branchName; fall back to the existing + // remote tracking ref (set up by the initial shallow checkout) when the + // fetch fails (e.g. due to shallow clone limitations or missing credentials). debugLog(`Strategy 1 (incremental): Fetching origin/${branchName}`); // Configure git authentication via GIT_CONFIG_* environment variables. @@ -183,15 +185,29 @@ async function generateGitPatch(branchName, baseBranch, options = {}) { baseRef = `origin/${branchName}`; debugLog(`Strategy 1 (incremental): Successfully fetched, baseRef=${baseRef}`); } catch (fetchError) { - // In incremental mode, we MUST have origin/branchName - no fallback - debugLog(`Strategy 1 (incremental): Fetch failed - ${getErrorMessage(fetchError)}`); - errorMessage = `Cannot generate incremental patch: failed to fetch origin/${branchName}. This typically happens when the remote branch doesn't exist yet or was force-pushed. Error: ${getErrorMessage(fetchError)}`; - // Don't try other strategies in incremental mode - return { - success: false, - error: errorMessage, - patchPath: patchPath, - }; + // Fetch failed. Check if origin/branchName already exists from the initial shallow checkout. + // This handles cases where git fetch fails due to shallow clone limitations or when + // GITHUB_TOKEN is unavailable in the MCP server process (e.g. after clean_git_credentials.sh). + // Using the existing remote tracking ref as a fallback is safe: it represents the state + // of the branch at checkout time, so the incremental patch will include all commits + // made by the agent since then. + debugLog(`Strategy 1 (incremental): Fetch failed - ${getErrorMessage(fetchError)}, checking for existing remote tracking ref`); + try { + execGitSync(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branchName}`], { cwd }); + // Remote tracking ref exists from initial shallow checkout — use it as base + baseRef = `origin/${branchName}`; + debugLog(`Strategy 1 (incremental): Using existing remote tracking ref as fallback, baseRef=${baseRef}`); + } catch (refCheckError) { + // No remote tracking ref at all — cannot safely generate an incremental patch. + // Report both errors: the original fetch failure and the missing ref. + debugLog(`Strategy 1 (incremental): No existing remote tracking ref found (${getErrorMessage(refCheckError)}), failing`); + errorMessage = `Cannot generate incremental patch: failed to fetch origin/${branchName} and no existing remote tracking ref found. This typically happens when the remote branch doesn't exist yet or was force-pushed. Fetch error: ${getErrorMessage(fetchError)}`; + return { + success: false, + error: errorMessage, + patchPath: patchPath, + }; + } } } else { // FULL MODE (for create_pull_request): diff --git a/actions/setup/js/git_patch_integration.test.cjs b/actions/setup/js/git_patch_integration.test.cjs index 17cbb8f8374..c998b3301ac 100644 --- a/actions/setup/js/git_patch_integration.test.cjs +++ b/actions/setup/js/git_patch_integration.test.cjs @@ -739,6 +739,51 @@ describe("git patch integration tests", () => { } }); + it("should fall back to existing remote tracking ref when fetch fails in incremental mode", async () => { + // Simulate a shallow checkout scenario: + // 1. feature-branch is created, first commit pushed to origin (origin/feature-branch exists) + // 2. Agent adds a new commit locally + // 3. Remote URL is broken so git fetch fails + // 4. We expect patch generation to succeed using the existing origin/feature-branch ref + execGit(["checkout", "-b", "shallow-fetch-fail"], { cwd: workingRepo }); + fs.writeFileSync(path.join(workingRepo, "base.txt"), "Base content\n"); + execGit(["add", "base.txt"], { cwd: workingRepo }); + execGit(["commit", "-m", "Base commit (already on remote)"], { cwd: workingRepo }); + + // Push so origin/shallow-fetch-fail tracking ref is set up (simulating shallow checkout) + execGit(["push", "-u", "origin", "shallow-fetch-fail"], { cwd: workingRepo }); + + // Add a new commit (the agent's work) + fs.writeFileSync(path.join(workingRepo, "agent-change.txt"), "Agent change\n"); + execGit(["add", "agent-change.txt"], { cwd: workingRepo }); + execGit(["commit", "-m", "Agent commit - should appear in patch"], { cwd: workingRepo }); + + // Break the remote URL to simulate fetch failure (e.g. missing credentials or network issue) + execGit(["remote", "set-url", "origin", "https://invalid.example.invalid/nonexistent-repo.git"], { cwd: workingRepo }); + + const restore = setTestEnv(workingRepo); + try { + // origin/shallow-fetch-fail still points to the base commit even though fetch will fail + const result = await generateGitPatch("shallow-fetch-fail", "main", { mode: "incremental" }); + + // Should succeed using the existing remote tracking ref as the base + expect(result.success).toBe(true); + expect(result.patchPath).toBeDefined(); + + const patchContent = fs.readFileSync(result.patchPath, "utf8"); + + // Should contain the agent's new commit + expect(patchContent).toContain("Agent commit - should appear in patch"); + + // Should NOT include the already-pushed base commit + expect(patchContent).not.toContain("Base commit (already on remote)"); + } finally { + // Restore remote URL before cleanup so afterEach can delete the directory + execGit(["remote", "set-url", "origin", bareRepoDir], { cwd: workingRepo }); + restore(); + } + }); + it("should include all commits in full mode even when origin/branch exists", async () => { // Create a feature branch with first commit execGit(["checkout", "-b", "full-mode-branch"], { cwd: workingRepo }); diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs new file mode 100644 index 00000000000..481de1bf247 --- /dev/null +++ b/actions/setup/js/push_signed_commits.cjs @@ -0,0 +1,115 @@ +// @ts-check +/// + +/** @type {typeof import("fs")} */ +const fs = require("fs"); +/** @type {typeof import("path")} */ +const path = require("path"); + +/** + * @fileoverview Signed Commit Push Helper + * + * Pushes local git commits to a remote branch using the GitHub GraphQL + * `createCommitOnBranch` mutation, so commits are cryptographically signed + * (verified) by GitHub. Falls back to a plain `git push` when the GraphQL + * approach is unavailable (e.g. GitHub Enterprise Server instances that do + * not support the mutation, or when branch-protection policies reject it). + * + * Both `create_pull_request.cjs` and `push_to_pull_request_branch.cjs` use + * this helper so the signed-commit logic lives in exactly one place. + */ + +/** + * Pushes local commits to a remote branch using the GitHub GraphQL + * `createCommitOnBranch` mutation so commits are cryptographically signed. + * Falls back to `git push` if the GraphQL approach fails (e.g. on GHES). + * + * @param {object} opts + * @param {any} opts.githubClient - Authenticated Octokit client with .graphql() + * @param {string} opts.owner - Repository owner + * @param {string} opts.repo - Repository name + * @param {string} opts.branch - Target branch name + * @param {string} opts.baseRef - Git ref of the remote head before commits were applied (used for rev-list) + * @param {string} opts.cwd - Working directory of the local git checkout + * @param {object} [opts.gitAuthEnv] - Environment variables for git push fallback auth + * @returns {Promise} + */ +async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, cwd, gitAuthEnv }) { + // Collect the commits introduced (oldest-first) + const { stdout: revListOut } = await exec.getExecOutput("git", ["rev-list", "--reverse", `${baseRef}..HEAD`], { cwd }); + const shas = revListOut.trim().split("\n").filter(Boolean); + + if (shas.length === 0) { + core.info("pushSignedCommits: no new commits to push via GraphQL"); + return; + } + + core.info(`pushSignedCommits: replaying ${shas.length} commit(s) via GraphQL createCommitOnBranch`); + + try { + for (const sha of shas) { + // Get the current remote HEAD OID (updated each iteration) + const { stdout: oidOut } = await exec.getExecOutput("git", ["ls-remote", "origin", `refs/heads/${branch}`], { cwd }); + const expectedHeadOid = oidOut.trim().split(/\s+/)[0]; + if (!expectedHeadOid) { + throw new Error(`Could not resolve remote HEAD OID for branch ${branch}`); + } + + // Full commit message (subject + body) + const { stdout: msgOut } = await exec.getExecOutput("git", ["log", "-1", "--format=%B", sha], { cwd }); + const message = msgOut.trim(); + const headline = message.split("\n")[0]; + const body = message.split("\n").slice(1).join("\n").trim(); + + // File changes for this commit (supports Add/Modify/Delete/Rename/Copy) + const { stdout: nameStatusOut } = await exec.getExecOutput("git", ["diff", "--name-status", `${sha}^`, sha], { cwd }); + /** @type {Array<{path: string, contents: string}>} */ + const additions = []; + /** @type {Array<{path: string}>} */ + const deletions = []; + + for (const line of nameStatusOut.trim().split("\n").filter(Boolean)) { + const parts = line.split("\t"); + const status = parts[0]; + if (status === "D") { + deletions.push({ path: parts[1] }); + } else if (status.startsWith("R") || status.startsWith("C")) { + // Rename or Copy: parts[1] = old path, parts[2] = new path + deletions.push({ path: parts[1] }); + const content = fs.readFileSync(path.join(cwd, parts[2])); + additions.push({ path: parts[2], contents: content.toString("base64") }); + } else { + // Added or Modified + const content = fs.readFileSync(path.join(cwd, parts[1])); + additions.push({ path: parts[1], contents: content.toString("base64") }); + } + } + + /** @type {any} */ + const input = { + branch: { repositoryNameWithOwner: `${owner}/${repo}`, branchName: branch }, + message: { headline, ...(body ? { body } : {}) }, + fileChanges: { additions, deletions }, + expectedHeadOid, + }; + + const result = await githubClient.graphql( + `mutation($input: CreateCommitOnBranchInput!) { + createCommitOnBranch(input: $input) { commit { oid } } + }`, + { input } + ); + const oid = result?.createCommitOnBranch?.commit?.oid; + core.info(`pushSignedCommits: signed commit created: ${oid}`); + } + core.info(`pushSignedCommits: all ${shas.length} commit(s) pushed as signed commits`); + } catch (graphqlError) { + core.warning(`pushSignedCommits: GraphQL signed push failed, falling back to git push: ${graphqlError instanceof Error ? graphqlError.message : String(graphqlError)}`); + await exec.exec("git", ["push", "origin", branch], { + cwd, + env: { ...process.env, ...(gitAuthEnv || {}) }, + }); + } +} + +module.exports = { pushSignedCommits }; diff --git a/actions/setup/js/push_signed_commits.test.cjs b/actions/setup/js/push_signed_commits.test.cjs new file mode 100644 index 00000000000..67e933976c8 --- /dev/null +++ b/actions/setup/js/push_signed_commits.test.cjs @@ -0,0 +1,433 @@ +/** + * Integration tests for push_signed_commits.cjs + * + * These tests run REAL git commands to verify that pushSignedCommits: + * 1. Correctly enumerates new commits via `git rev-list` + * 2. Reads file contents and builds the GraphQL payload + * 3. Calls the GitHub GraphQL `createCommitOnBranch` mutation for each commit + * 4. Falls back to `git push` when the GraphQL mutation fails + * + * A bare git repository is used as the stand-in "remote" so that ls-remote + * and push commands work without a real network connection. + * The GraphQL layer is always mocked because it requires a real GitHub API. + */ + +// @ts-check +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { createRequire } from "module"; +import fs from "fs"; +import path from "path"; +import { spawnSync } from "child_process"; +import os from "os"; + +const require = createRequire(import.meta.url); + +// Import module once – globals are resolved at call time, not import time. +const { pushSignedCommits } = require("./push_signed_commits.cjs"); + +// ────────────────────────────────────────────────────────────────────────────── +// Git helpers (real subprocess – no mocking) +// ────────────────────────────────────────────────────────────────────────────── + +/** + * @param {string[]} args + * @param {{ cwd?: string, allowFailure?: boolean }} [options] + */ +function execGit(args, options = {}) { + const result = spawnSync("git", args, { + encoding: "utf8", + env: { + ...process.env, + GIT_CONFIG_NOSYSTEM: "1", + HOME: os.tmpdir(), + }, + ...options, + }); + if (result.error) throw result.error; + if (result.status !== 0 && !options.allowFailure) { + throw new Error(`git ${args.join(" ")} failed (cwd=${options.cwd}):\n${result.stderr}`); + } + return result; +} + +/** + * Create a bare repository that acts as the remote "origin". + * @returns {string} Path to the bare repository + */ +function createBareRepo() { + const bareDir = fs.mkdtempSync(path.join(os.tmpdir(), "push-signed-bare-")); + execGit(["init", "--bare"], { cwd: bareDir }); + // Ensure the bare repo uses "main" as the default branch + execGit(["symbolic-ref", "HEAD", "refs/heads/main"], { cwd: bareDir }); + return bareDir; +} + +/** + * Clone the bare repo and set up a working copy with an initial commit on `main`. + * @param {string} bareDir + * @returns {string} Path to the working copy + */ +function createWorkingRepo(bareDir) { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "push-signed-work-")); + execGit(["clone", bareDir, "."], { cwd: workDir }); + execGit(["config", "user.name", "Test User"], { cwd: workDir }); + execGit(["config", "user.email", "test@example.com"], { cwd: workDir }); + + // Initial commit on main + fs.writeFileSync(path.join(workDir, "README.md"), "# Test\n"); + execGit(["add", "."], { cwd: workDir }); + execGit(["commit", "-m", "Initial commit"], { cwd: workDir }); + // Rename to main if git defaulted to master + execGit(["branch", "-M", "main"], { cwd: workDir }); + execGit(["push", "-u", "origin", "main"], { cwd: workDir }); + + return workDir; +} + +/** @param {string} dir */ +function cleanupDir(dir) { + if (dir && fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Global stubs required by push_signed_commits.cjs +// ────────────────────────────────────────────────────────────────────────────── + +/** + * Build an `exec` global stub that runs real git commands via spawnSync. + * @param {string} cwd + */ +function makeRealExec(cwd) { + return { + /** + * @param {string} program + * @param {string[]} args + * @param {{ cwd?: string }} [opts] + */ + getExecOutput: async (program, args, opts = {}) => { + const result = spawnSync(program, args, { + encoding: "utf8", + cwd: opts.cwd ?? cwd, + env: { + ...process.env, + GIT_CONFIG_NOSYSTEM: "1", + HOME: os.tmpdir(), + }, + }); + if (result.error) throw result.error; + return { exitCode: result.status ?? 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" }; + }, + /** + * @param {string} program + * @param {string[]} args + * @param {{ cwd?: string, env?: NodeJS.ProcessEnv }} [opts] + */ + exec: async (program, args, opts = {}) => { + const result = spawnSync(program, args, { + encoding: "utf8", + cwd: opts.cwd ?? cwd, + env: opts.env ?? { ...process.env, GIT_CONFIG_NOSYSTEM: "1", HOME: os.tmpdir() }, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`${program} ${args.join(" ")} failed:\n${result.stderr}`); + } + return result.status ?? 0; + }, + }; +} + +// ────────────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────────────── + +describe("push_signed_commits integration tests", () => { + let bareDir; + let workDir; + let mockCore; + let capturedGraphQLCalls; + + /** @returns {any} */ + function makeMockGithubClient(options = {}) { + const { failWithError = null, oid = "signed-oid-abc123" } = options; + return { + graphql: vi.fn(async query => { + if (failWithError) throw failWithError; + capturedGraphQLCalls.push({ oid, query }); + return { createCommitOnBranch: { commit: { oid } } }; + }), + }; + } + + beforeEach(() => { + bareDir = createBareRepo(); + workDir = createWorkingRepo(bareDir); + capturedGraphQLCalls = []; + + mockCore = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }; + + global.core = mockCore; + }); + + afterEach(() => { + cleanupDir(bareDir); + cleanupDir(workDir); + delete global.core; + delete global.exec; + vi.clearAllMocks(); + }); + + // ────────────────────────────────────────────────────── + // Happy path – GraphQL succeeds + // ────────────────────────────────────────────────────── + + describe("GraphQL signed commits (happy path)", () => { + it("should call GraphQL for a single new commit", async () => { + // Create a feature branch with one new file + execGit(["checkout", "-b", "feature-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "hello.txt"), "Hello World\n"); + execGit(["add", "hello.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add hello.txt"], { cwd: workDir }); + // Push the branch so ls-remote can resolve its OID + execGit(["push", "-u", "origin", "feature-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "feature-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + // Verify the mutation query targets createCommitOnBranch + const [query, variables] = githubClient.graphql.mock.calls[0]; + expect(query).toContain("createCommitOnBranch"); + expect(query).toContain("CreateCommitOnBranchInput"); + // Verify the input structure + expect(variables.input.branch.branchName).toBe("feature-branch"); + expect(variables.input.branch.repositoryNameWithOwner).toBe("test-owner/test-repo"); + expect(variables.input.message.headline).toBe("Add hello.txt"); + // hello.txt should appear in additions with base64 content + expect(variables.input.fileChanges.additions).toHaveLength(1); + expect(variables.input.fileChanges.additions[0].path).toBe("hello.txt"); + expect(Buffer.from(variables.input.fileChanges.additions[0].contents, "base64").toString()).toBe("Hello World\n"); + }); + + it("should call GraphQL once per commit for multiple new commits", async () => { + execGit(["checkout", "-b", "multi-commit-branch"], { cwd: workDir }); + + fs.writeFileSync(path.join(workDir, "file-a.txt"), "File A\n"); + execGit(["add", "file-a.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add file-a.txt"], { cwd: workDir }); + + fs.writeFileSync(path.join(workDir, "file-b.txt"), "File B\n"); + execGit(["add", "file-b.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add file-b.txt"], { cwd: workDir }); + + execGit(["push", "-u", "origin", "multi-commit-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "multi-commit-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(2); + const headlines = githubClient.graphql.mock.calls.map(c => c[1].input.message.headline); + expect(headlines).toEqual(["Add file-a.txt", "Add file-b.txt"]); + }); + + it("should include deletions when files are removed in a commit", async () => { + execGit(["checkout", "-b", "delete-branch"], { cwd: workDir }); + + // First add a file, push, then delete it + fs.writeFileSync(path.join(workDir, "to-delete.txt"), "Will be deleted\n"); + execGit(["add", "to-delete.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Add file to delete"], { cwd: workDir }); + execGit(["push", "-u", "origin", "delete-branch"], { cwd: workDir }); + + // Now delete the file + fs.unlinkSync(path.join(workDir, "to-delete.txt")); + execGit(["add", "-u"], { cwd: workDir }); + execGit(["commit", "-m", "Delete file"], { cwd: workDir }); + execGit(["push", "origin", "delete-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "delete-branch", + // Only replay the delete commit + baseRef: "delete-branch^", + cwd: workDir, + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + const callArg = githubClient.graphql.mock.calls[0][1].input; + expect(callArg.fileChanges.deletions).toEqual([{ path: "to-delete.txt" }]); + expect(callArg.fileChanges.additions).toHaveLength(0); + }); + + it("should handle commit with no file changes (empty commit)", async () => { + execGit(["checkout", "-b", "empty-diff-branch"], { cwd: workDir }); + execGit(["push", "-u", "origin", "empty-diff-branch"], { cwd: workDir }); + + // Allow an empty commit + execGit(["commit", "--allow-empty", "-m", "Empty commit"], { cwd: workDir }); + execGit(["push", "origin", "empty-diff-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "empty-diff-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + expect(githubClient.graphql).toHaveBeenCalledTimes(1); + const callArg = githubClient.graphql.mock.calls[0][1].input; + expect(callArg.fileChanges.additions).toHaveLength(0); + expect(callArg.fileChanges.deletions).toHaveLength(0); + }); + + it("should do nothing when there are no new commits", async () => { + execGit(["checkout", "-b", "no-commits-branch"], { cwd: workDir }); + execGit(["push", "-u", "origin", "no-commits-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + // baseRef points to the same HEAD – no commits to replay + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "no-commits-branch", + baseRef: "origin/no-commits-branch", + cwd: workDir, + }); + + expect(githubClient.graphql).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("no new commits")); + }); + }); + + // ────────────────────────────────────────────────────── + // Fallback path – GraphQL fails → git push + // ────────────────────────────────────────────────────── + + describe("git push fallback when GraphQL fails", () => { + it("should fall back to git push when GraphQL throws", async () => { + execGit(["checkout", "-b", "fallback-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "fallback.txt"), "Fallback content\n"); + execGit(["add", "fallback.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Fallback commit"], { cwd: workDir }); + execGit(["push", "-u", "origin", "fallback-branch"], { cwd: workDir }); + + // Add another commit that will be pushed via git push fallback + fs.writeFileSync(path.join(workDir, "extra.txt"), "Extra content\n"); + execGit(["add", "extra.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Extra commit"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient({ failWithError: new Error("GraphQL: not supported on GHES") }); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "fallback-branch", + baseRef: "origin/fallback-branch", + cwd: workDir, + }); + + // Should warn and fall back + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("falling back to git push")); + + // The commit should now be on the remote (verified via ls-remote) + const lsRemote = execGit(["ls-remote", bareDir, "refs/heads/fallback-branch"], { cwd: workDir }); + const remoteOid = lsRemote.stdout.trim().split(/\s+/)[0]; + const localOid = execGit(["rev-parse", "HEAD"], { cwd: workDir }).stdout.trim(); + expect(remoteOid).toBe(localOid); + }); + }); + + // ────────────────────────────────────────────────────── + // Commit message – multi-line body + // ────────────────────────────────────────────────────── + + describe("commit message handling", () => { + it("should include the commit body when present", async () => { + execGit(["checkout", "-b", "body-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "described.txt"), "content\n"); + execGit(["add", "described.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Subject line\n\nDetailed body text\n\nMore details here"], { cwd: workDir }); + execGit(["push", "-u", "origin", "body-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "body-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + const callArg = githubClient.graphql.mock.calls[0][1].input; + expect(callArg.message.headline).toBe("Subject line"); + expect(callArg.message.body).toContain("Detailed body text"); + }); + + it("should omit the body field when commit message has no body", async () => { + execGit(["checkout", "-b", "no-body-branch"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "nodesc.txt"), "nodesc\n"); + execGit(["add", "nodesc.txt"], { cwd: workDir }); + execGit(["commit", "-m", "Subject only"], { cwd: workDir }); + execGit(["push", "-u", "origin", "no-body-branch"], { cwd: workDir }); + + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "no-body-branch", + baseRef: "origin/main", + cwd: workDir, + }); + + const callArg = githubClient.graphql.mock.calls[0][1].input; + expect(callArg.message.headline).toBe("Subject only"); + expect(callArg.message.body).toBeUndefined(); + }); + }); +}); diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 9648bdc50be..9a63db223df 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -4,6 +4,7 @@ /** @type {typeof import("fs")} */ const fs = require("fs"); const { generateStagedPreview } = require("./staged_preview.cjs"); +const { pushSignedCommits } = require("./push_signed_commits.cjs"); const { updateActivationCommentWithCommit, updateActivationComment } = require("./update_activation_comment.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); @@ -481,10 +482,16 @@ async function main(config = {}) { return { success: false, error: "Failed to apply patch" }; } - // Push the applied commits to the branch (outside patch try/catch so push failures are not misattributed) + // Push the applied commits to the branch using signed GraphQL commits (outside patch try/catch so push failures are not misattributed) try { - await exec.exec("git", ["push", "origin", branchName], { - env: { ...process.env, ...gitAuthEnv }, + await pushSignedCommits({ + githubClient, + owner: repoParts.owner, + repo: repoParts.repo, + branch: branchName, + baseRef: remoteHeadBeforePatch || `origin/${branchName}`, + cwd: process.cwd(), + gitAuthEnv, }); core.info(`Changes committed and pushed to branch: ${branchName}`); } catch (pushError) { diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs index 74351e141f1..6c436211714 100644 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ b/actions/setup/js/push_to_pull_request_branch.test.cjs @@ -596,16 +596,30 @@ index 0000000..abc1234 mockExec.exec.mockResolvedValueOnce(0); // rev-parse mockExec.exec.mockResolvedValueOnce(0); // checkout - mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "before-sha\n", stderr: "" }); + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "before-sha\n", stderr: "" }); // git rev-parse HEAD (before patch) mockExec.exec.mockResolvedValueOnce(0); // git am + + // pushSignedCommits: git rev-list returns one SHA so the push is attempted + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "abc123\n", stderr: "" }); // git rev-list + // pushSignedCommits: git ls-remote returns remote HEAD OID + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "remote-oid\trefs/heads/feature-branch\n", stderr: "" }); // git ls-remote + // pushSignedCommits: git log -1 returns commit message + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "Test commit\n", stderr: "" }); // git log -1 + // pushSignedCommits: git diff --name-status returns file changes + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); // git diff --name-status (empty - no files) + + // GraphQL call fails, triggering fallback to git push + mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error: branch protection")); + + // Fallback git push also fails with non-fast-forward mockExec.exec.mockRejectedValueOnce(new Error("! [rejected] feature-branch -> feature-branch (non-fast-forward)")); const module = await loadModule(); const handler = await module.main({}); const result = await handler({ patch_path: patchPath }, {}); - // The error happens during push, which currently shows in patch apply failure + // The error happens during push expect(result.success).toBe(false); }); diff --git a/docs/src/content/docs/blog/2026-03-18-weekly-update.md b/docs/src/content/docs/blog/2026-03-18-weekly-update.md new file mode 100644 index 00000000000..81db2d247c6 --- /dev/null +++ b/docs/src/content/docs/blog/2026-03-18-weekly-update.md @@ -0,0 +1,67 @@ +--- +title: "Weekly Update – March 18, 2026" +description: "Seven releases in seven days: guard policy overhaul, new triggers, GHE improvements, and a healthy dose of quality-of-life fixes." +authors: + - copilot +date: 2026-03-18 +--- + +It's been a busy week in [github/gh-aw](https://github.com/github/gh-aw) — seven releases shipped between March 13 and March 17, covering everything from a security model overhaul to a new label-based trigger and a long-overdue terminal resize fix. Let's dig in. + +## Releases This Week + +### [v0.61.0](https://github.com/github/gh-aw/releases/tag/v0.61.0) — March 17 + +The freshest release focuses on reliability and developer experience: + +- **Automatic debug logging** ([#21406](https://github.com/github/gh-aw/pull/21406)): Set `ACTIONS_RUNNER_DEBUG=true` on your runner and full debug logging activates automatically — no more manually adding `DEBUG=*` to every troubleshooting run. +- **Cross-repo project item updates** ([#21404](https://github.com/github/gh-aw/pull/21404)): `update_project` now accepts a `target_repo` parameter, so org-level project boards can update fields on items from any repository. +- **GHE Cloud data residency support** ([#21408](https://github.com/github/gh-aw/pull/21408)): Compiled workflows now auto-inject a `GH_HOST` step, fixing `gh` CLI failures on `*.ghe.com` instances. +- **CI build artifacts** ([#21440](https://github.com/github/gh-aw/pull/21440)): The `build` CI job now uploads the compiled `gh-aw` binary as a downloadable artifact — handy for testing PRs without a local build. + +### [v0.60.0](https://github.com/github/gh-aw/releases/tag/v0.60.0) — March 17 + +This release rewires the security model. **Breaking change**: automatic `lockdown=true` is gone. Instead, the runtime now auto-configures guard policies on the GitHub MCP server — `min_integrity=approved` for public repos, `min_integrity=none` for private/internal. Remove any explicit `lockdown: false` from your frontmatter; it's no longer needed. + +Other highlights: + +- **GHES domain auto-allowlisting** ([#21301](https://github.com/github/gh-aw/pull/21301)): When `engine.api-target` points to a GHES instance, the compiler automatically adds GHES API hostnames to the firewall. No more silent blocks after every recompile. +- **`github-app:` auth in APM dependencies** ([#21286](https://github.com/github/gh-aw/pull/21286)): APM `dependencies:` can now use `github-app:` auth for cross-org private package access. + +### [v0.59.0](https://github.com/github/gh-aw/releases/tag/v0.59.0) — March 16 + +A feature-packed release with two breaking changes (field renames in `safe-outputs.allowed-domains`) and several new capabilities: + +- **Label Command Trigger** ([#21118](https://github.com/github/gh-aw/pull/21118)): Activate a workflow by adding a label to an issue, PR, or discussion. The label is automatically removed so it can be reapplied to re-trigger. +- **`gh aw domains` command** ([#21086](https://github.com/github/gh-aw/pull/21086)): Inspect the effective network domain configuration for all your workflows, with per-domain ecosystem annotations. +- **Pre-activation step injection** — New `on.steps` and `on.permissions` frontmatter fields let you inject custom steps and permissions into the activation job for advanced scenarios. + +### Earlier in the Week + +- [v0.58.3](https://github.com/github/gh-aw/releases/tag/v0.58.3) (March 15): MCP write-sink guard policy for non-GitHub MCP servers, Copilot pre-flight diagnostic for GHES, and a richer run details step summary. +- [v0.58.2](https://github.com/github/gh-aw/releases/tag/v0.58.2) (March 14): GHES auto-detection in `audit` and `add-wizard`, `excluded-files` support for `create-pull-request`, and clearer `run` command errors. +- [v0.58.1](https://github.com/github/gh-aw/releases/tag/v0.58.1) / [v0.58.0](https://github.com/github/gh-aw/releases/tag/v0.58.0) (March 13): `call-workflow` safe output for chaining workflows, `checkout: false` for agent jobs, custom OpenAI/Anthropic API endpoints, and 92 merged PRs in v0.58.0 alone. + +## Notable Pull Requests + +- **[Top-level `github-app` fallback](https://github.com/github/gh-aw/pull/21510)** ([#21510](https://github.com/github/gh-aw/pull/21510)): Define your GitHub App config once at the top level and let it propagate to safe-outputs, checkout, MCP, APM, and activation — instead of repeating it in every section. +- **[GitHub App-only permission scopes](https://github.com/github/gh-aw/pull/21511)** ([#21511](https://github.com/github/gh-aw/pull/21511)): 31 new `PermissionScope` constants cover repository, org, and user-level GitHub App permissions (e.g., `administration`, `members`, `environments`). +- **[Custom Huh theme](https://github.com/github/gh-aw/pull/21557)** ([#21557](https://github.com/github/gh-aw/pull/21557)): All 11 interactive CLI forms now use a Dracula-inspired theme consistent with the rest of the CLI's visual identity. +- **[Weekly blog post writer workflow](https://github.com/github/gh-aw/pull/21575)** ([#21575](https://github.com/github/gh-aw/pull/21575)): Yes, the workflow that wrote this post was itself merged this week. Meta! +- **[CI job timeout limits](https://github.com/github/gh-aw/pull/21601)** ([#21601](https://github.com/github/gh-aw/pull/21601)): All 25 CI jobs that relied on GitHub's 6-hour default now have explicit timeouts, preventing a stuck test from silently burning runner compute. + +## 🤖 Agent of the Week: auto-triage-issues + +The first-ever Agent of the Week goes to the workflow that handles the unglamorous but essential job of keeping the issue tracker from becoming a swamp. + +`auto-triage-issues` runs on a schedule and fires on every new issue, reading each one and deciding how to categorize it. This week it ran five times — three successful runs and two that were triggered by push events to a feature branch (which apparently fire the workflow but don't give it much to work with). On its scheduled run this morning, it found zero open issues in the repository, so it created a tidy summary discussion to announce the clean state, as instructed. On an earlier issues-triggered run, it attempted to triage issue [#21572](https://github.com/github/gh-aw/pull/21572) but hit empty results from GitHub MCP tools on all three read attempts — so it gracefully called `missing_data` and moved on rather than hallucinating a label. + +Across its recent runs it made 131 `search_repositories` calls. We're not sure why it finds repository searches so compelling, but clearly it's very thorough about knowing its neighborhood before making any decisions. + +💡 **Usage tip**: Pair `auto-triage-issues` with a notify workflow on specific labels (e.g., `security` or `needs-repro`) so the right people get pinged automatically without anyone having to watch the inbox. + +→ [View the workflow on GitHub](https://github.com/github/gh-aw/blob/main/.github/workflows/auto-triage-issues.md) + +## Try It Out + +Update to [v0.61.0](https://github.com/github/gh-aw/releases/tag/v0.61.0) to get all the improvements from this packed week. If you run workflows on GHES or in GHE Cloud, the new auto-detection and `GH_HOST` injection features are especially worth trying. As always, contributions and feedback are welcome in [github/gh-aw](https://github.com/github/gh-aw). diff --git a/docs/src/content/docs/reference/glossary.md b/docs/src/content/docs/reference/glossary.md index efe1f5852ed..89776be8e22 100644 --- a/docs/src/content/docs/reference/glossary.md +++ b/docs/src/content/docs/reference/glossary.md @@ -328,6 +328,10 @@ Markdown files with YAML frontmatter stored in `.github/agents/` defining intera A GitHub Personal Access Token with granular permission control, specifying exactly which repositories the token can access and what permissions it has. Created at github.com/settings/personal-access-tokens. +### `RUNNER_TEMP` / `${{ runner.temp }}` + +A GitHub Actions environment variable pointing to a per-job temporary directory on the runner. Agentic workflows store compiled scripts and runtime artifacts under `${RUNNER_TEMP}/gh-aw/` for compatibility with self-hosted runners that may not have write access to system directories. In shell `run:` blocks, use the shell variable form `${RUNNER_TEMP}`; in `with:` or `env:` YAML fields, use the expression form `${{ runner.temp }}`. + ## Development and Compilation ### CLI (Command Line Interface) @@ -408,6 +412,10 @@ A system-injected environment variable containing the gh-aw compiler version tha A system-injected environment variable containing the comma-separated list of domains allowed by the workflow's network configuration. Used by safe output jobs for URL sanitization — URLs from unlisted domains are redacted in AI-generated content before it is applied. Automatically populated from `network.allowed` domains and, when `engine.api-target` is set, includes the GHES/GHEC API hostname and base domain. Cannot be overridden by user-defined `env:` blocks. See [Environment Variables Reference](/gh-aw/reference/environment-variables/). +### `GH_HOST` + +An environment variable recognized by the `gh` CLI that specifies the GitHub hostname for GitHub Enterprise Server (GHES) or GitHub Enterprise Cloud (GHEC) deployments. When set, `gh` commands target the specified enterprise instance instead of `github.com`. Agentic workflows automatically configure this from `GITHUB_SERVER_URL` at agent job startup; the variable is also propagated to custom frontmatter jobs and the safe-outputs job so all `gh` calls target the correct enterprise host. See [Environment Variables Reference](/gh-aw/reference/environment-variables/). + ### Label Command Trigger (`label_command`) A trigger that activates a workflow when a specific label is added to an issue, pull request, or discussion. Unlike standard label filtering, the label command trigger automatically removes the applied label on activation so it can be reapplied to re-trigger the workflow. Configured via `label_command:` in the `on:` section; exposes `needs.activation.outputs.label_command` with the matched label name for downstream jobs. Can be combined with `slash_command:` to support both label-based and comment-based triggering. See [LabelOps patterns](/gh-aw/patterns/label-ops/). diff --git a/docs/src/content/docs/reference/permissions.md b/docs/src/content/docs/reference/permissions.md index b38bc95b0ee..e25fc38540f 100644 --- a/docs/src/content/docs/reference/permissions.md +++ b/docs/src/content/docs/reference/permissions.md @@ -43,6 +43,35 @@ permissions: This permission is safe to use and does not require safe-outputs, even in strict mode. +### GitHub App-Only Permissions + +Certain permission scopes cannot be granted to `GITHUB_TOKEN` and are forwarded instead as inputs to [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) when a GitHub App is configured. These scopes are omitted from the compiled workflow's `permissions:` block. + +**Repository-level:** `administration`, `environments`, `git-signing`, `vulnerability-alerts`, `workflows`, `repository-hooks`, `single-file`, `codespaces`, `repository-custom-properties` + +**Organization-level:** `organization-projects`, `members`, `organization-administration`, `team-discussions`, `organization-hooks`, `organization-members`, `organization-packages`, `organization-self-hosted-runners`, `organization-custom-org-roles`, `organization-custom-properties`, `organization-custom-repository-roles`, `organization-announcement-banners`, `organization-events`, `organization-plan`, `organization-user-blocking`, `organization-personal-access-token-requests`, `organization-personal-access-tokens`, `organization-copilot`, `organization-codespaces` + +**User-level:** `email-addresses`, `codespaces-lifecycle-admin`, `codespaces-metadata` + +These scopes must always be declared as `read`. Declaring `write` is a compile error; write operations through a GitHub App must go through [safe outputs](/gh-aw/reference/safe-outputs/), which provide a separate sanitized job for write operations. + +Declaring any of these scopes without a configured `github-app` causes a compile error. The GitHub App can be configured in `tools.github.github-app`, `safe-outputs.github-app`, or the top-level `github-app:` field — see [Tools](/gh-aw/reference/tools/) for configuration details. + +```aw wrap +permissions: + contents: read + workflows: read # GitHub App-only scope + members: read # GitHub App-only scope +tools: + github: + github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +``` + +> [!NOTE] +> Shorthand permissions (`read-all`, `write-all`, `all: read`) do not trigger the "GitHub App required" validation. + ## Configuration Specify individual permission levels: @@ -116,7 +145,9 @@ permissions: contents: read ``` -**Exception:** The `id-token: write` permission is explicitly allowed as it is used for OIDC authentication with cloud providers and does not grant repository write access. +**Exceptions:** +- `id-token: write` is allowed for OIDC authentication with cloud providers and does not grant repository write access. +- GitHub App-only scopes (see above) always refuse `write` at compile time regardless of this policy; use [safe outputs](/gh-aw/reference/safe-outputs/) for write operations that require a GitHub App. #### Migrating Existing Workflows diff --git a/docs/src/content/docs/setup/cli.md b/docs/src/content/docs/setup/cli.md index 27c2081a2c9..734b194bfad 100644 --- a/docs/src/content/docs/setup/cli.md +++ b/docs/src/content/docs/setup/cli.md @@ -83,7 +83,9 @@ Commands that support `--create-pull-request` (such as `gh aw add`, `gh aw add-w The compiled agent job automatically runs `configure_gh_for_ghe.sh` before the agent starts executing. The script detects the GitHub host from the `GITHUB_SERVER_URL` environment variable (set by GitHub Actions on GHES) and configures `gh` to authenticate against it. No configuration is required for the agent to use `gh` CLI commands on your GHES instance. -For custom `steps:` that run outside the agent sandbox (for example, pre-agent data gathering or post-agent reporting), source the helper script manually: +Custom workflow jobs (independent GitHub Actions jobs defined in workflow frontmatter) and the safe-outputs job automatically have `GH_HOST` derived from `GITHUB_SERVER_URL` at the start of each job. On github.com this is a no-op; on GHES/GHEC it ensures all `gh` CLI commands in the job target the correct instance without any manual setup. + +For custom `steps:` that require additional authentication setup (for example, when running `gh` commands without a `GH_TOKEN` in scope), the helper script is available: ```yaml wrap steps: @@ -98,7 +100,7 @@ steps: gh pr list --state open --limit 200 --json number,title ``` -The script is installed to `/opt/gh-aw/actions/configure_gh_for_ghe.sh` by the setup action. +The script is installed to `/opt/gh-aw/actions/configure_gh_for_ghe.sh` by the setup action. When `GH_TOKEN` is already set in the environment, the script skips `gh auth login` and only exports `GH_HOST` — the token handles authentication. > [!NOTE] > Custom steps run outside the agent firewall sandbox and have access to standard GitHub Actions environment variables including `GITHUB_SERVER_URL`, `GITHUB_TOKEN`, and `GH_TOKEN`. diff --git a/docs/src/content/docs/troubleshooting/common-issues.md b/docs/src/content/docs/troubleshooting/common-issues.md index 6d590ab663f..3b4ba07ccc1 100644 --- a/docs/src/content/docs/troubleshooting/common-issues.md +++ b/docs/src/content/docs/troubleshooting/common-issues.md @@ -305,6 +305,78 @@ If this command fails, the account associated with the token does not have a val > [!NOTE] > The `COPILOT_GITHUB_TOKEN` must belong to a user account with an active GitHub Copilot subscription. Organization-managed Copilot licenses may have additional restrictions on programmatic API access. +## GitHub Enterprise Server Issues + +### Copilot Engine Prerequisites on GHES + +Before running Copilot-based workflows on GHES, verify the following: + +**Site admin requirements:** +- **GitHub Connect** must be enabled — it connects GHES to github.com for Copilot cloud services. +- **Copilot licensing** must be purchased and activated at the enterprise level. +- The firewall must allow outbound HTTPS to `api.githubcopilot.com` and `api.enterprise.githubcopilot.com`. + +**Enterprise/org admin requirements:** +- Copilot seats must be assigned to the user whose PAT is used as `COPILOT_GITHUB_TOKEN`. +- The organization's Copilot policy must allow Copilot usage. + +**Workflow configuration:** + +```aw wrap +engine: + id: copilot + api-target: api.enterprise.githubcopilot.com +network: + allowed: + - defaults + - api.enterprise.githubcopilot.com +``` + +See [Enterprise API Endpoint](/gh-aw/reference/engines/#enterprise-api-endpoint-api-target) for GHEC/GHES `api-target` values. + +### Copilot GHES: Common Error Messages + +**`Error loading models: 400 Bad Request`** + +Copilot is not licensed at the enterprise level or the API proxy is routing incorrectly. Verify enterprise Copilot settings and confirm GitHub Connect is enabled. + +**`403 "unauthorized: not licensed to use Copilot"`** + +No Copilot license or seat is assigned to the PAT owner. Contact the site admin to enable Copilot at the enterprise level, then have an org admin assign a seat to the token owner. + +**`403 "Resource not accessible by personal access token"`** + +Wrong token type or missing permissions. Use a fine-grained PAT with the **Copilot Requests: Read** account permission, or a classic PAT with the `copilot` scope. See [`COPILOT_GITHUB_TOKEN`](/gh-aw/reference/auth/#copilot_github_token) for setup instructions. + +**`Could not resolve to a Repository`** + +`GH_HOST` is not set when running `gh` commands. This typically occurs in custom frontmatter jobs from older compiled workflows. Recompile with `gh aw compile` — compiled workflows now automatically export `GH_HOST` in custom jobs. + +For local CLI commands (`gh aw audit`, `gh aw add-wizard`), ensure you are inside a GHES repository clone or set `GH_HOST` explicitly: + +```bash wrap +GH_HOST=github.company.com gh aw audit +``` + +**Firewall blocks outbound HTTPS to `api.`** + +Add the GHES domain to your workflow's allowed list: + +```aw wrap +engine: + id: copilot + api-target: api.company.ghe.com +network: + allowed: + - defaults + - company.ghe.com + - api.company.ghe.com +``` + +**`gh aw add-wizard` or `gh aw init` creates a PR on github.com instead of GHES** + +Run these commands from inside a GHES repository clone — they auto-detect the GHES host from the git remote. If PR creation still fails, use `gh aw add` to generate the workflow file, then create the PR manually with `gh pr create`. + ## Context Expression Issues ### Unauthorized Expression diff --git a/go.mod b/go.mod index 4e7e9a0d876..0baf5dc664f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( charm.land/bubbletea/v2 v2.0.2 charm.land/lipgloss/v2 v2.0.2 github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc github.com/charmbracelet/x/exp/golden v0.0.0-20251215102626-e0db08df7383 github.com/cli/go-gh/v2 v2.13.0 github.com/creack/pty v1.1.24 @@ -42,7 +43,6 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect diff --git a/pkg/cli/add_interactive_auth.go b/pkg/cli/add_interactive_auth.go index 4d1aea4ac99..f00c4bb8bea 100644 --- a/pkg/cli/add_interactive_auth.go +++ b/pkg/cli/add_interactive_auth.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/styles" ) // checkGHAuthStatus verifies the user is logged in to GitHub CLI @@ -55,7 +56,7 @@ func (c *AddInteractiveConfig) checkGitRepository() error { return nil }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get repository info: %w", err) diff --git a/pkg/cli/add_interactive_engine.go b/pkg/cli/add_interactive_engine.go index fcd2e82ea33..7dbe6349aa9 100644 --- a/pkg/cli/add_interactive_engine.go +++ b/pkg/cli/add_interactive_engine.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -123,7 +124,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error { Options(engineOptions...). Value(&selectedEngine), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to select coding agent: %w", err) diff --git a/pkg/cli/add_interactive_git.go b/pkg/cli/add_interactive_git.go index 9cb366cb7c2..fcbf99c998a 100644 --- a/pkg/cli/add_interactive_git.go +++ b/pkg/cli/add_interactive_git.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -96,7 +97,7 @@ func (c *AddInteractiveConfig) createWorkflowPRAndConfigureSecret(ctx context.Co Options(options...). Value(&chosen), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if selectErr := selectForm.Run(); selectErr != nil { return fmt.Errorf("failed to get user input: %w", selectErr) @@ -129,7 +130,7 @@ func (c *AddInteractiveConfig) createWorkflowPRAndConfigureSecret(ctx context.Co Description("Add a prefix if required, for example: feat: or fix:"). Value(&newTitle), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if titleErr := titleForm.Run(); titleErr != nil { return fmt.Errorf("failed to get user input: %w", titleErr) } diff --git a/pkg/cli/add_interactive_orchestrator.go b/pkg/cli/add_interactive_orchestrator.go index a38b6f631f8..2e00285b1e5 100644 --- a/pkg/cli/add_interactive_orchestrator.go +++ b/pkg/cli/add_interactive_orchestrator.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/styles" ) var addInteractiveLog = logger.New("cli:add_interactive") @@ -228,7 +229,7 @@ func (c *AddInteractiveConfig) confirmChanges(workflowFiles, initFiles []string, Negative("No, cancel"). Value(&confirmed), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("confirmation failed: %w", err) diff --git a/pkg/cli/add_interactive_schedule.go b/pkg/cli/add_interactive_schedule.go index 9edb9fde84a..07cdafc4327 100644 --- a/pkg/cli/add_interactive_schedule.go +++ b/pkg/cli/add_interactive_schedule.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" "github.com/github/gh-aw/pkg/sliceutil" + "github.com/github/gh-aw/pkg/styles" ) var scheduleWizardLog = logger.New("cli:add_interactive_schedule") @@ -227,7 +228,7 @@ func (c *AddInteractiveConfig) selectScheduleFrequency() error { Options(options...). Value(&selected), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to select schedule frequency: %w", err) diff --git a/pkg/cli/add_interactive_workflow.go b/pkg/cli/add_interactive_workflow.go index de2bfc732fc..a2322601b49 100644 --- a/pkg/cli/add_interactive_workflow.go +++ b/pkg/cli/add_interactive_workflow.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -109,7 +110,7 @@ func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error Negative("No, I'll run later"). Value(&runNow), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return nil // Not critical, just skip diff --git a/pkg/cli/engine_secrets.go b/pkg/cli/engine_secrets.go index 0230f08d19d..f2989b331b2 100644 --- a/pkg/cli/engine_secrets.go +++ b/pkg/cli/engine_secrets.go @@ -13,6 +13,7 @@ import ( "github.com/github/gh-aw/pkg/repoutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -307,7 +308,7 @@ func promptForCopilotPATUnified(req SecretRequirement, config EngineSecretConfig return stringutil.ValidateCopilotPAT(s) }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get Copilot token: %w", err) @@ -355,7 +356,7 @@ func promptForSystemTokenUnified(req SecretRequirement, config EngineSecretConfi return nil }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get %s token: %w", req.Name, err) @@ -408,7 +409,7 @@ func promptForGenericAPIKeyUnified(req SecretRequirement, config EngineSecretCon return nil }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get %s API key: %w", label, err) diff --git a/pkg/cli/interactive.go b/pkg/cli/interactive.go index a0f2f00b2cc..a2403101b70 100644 --- a/pkg/cli/interactive.go +++ b/pkg/cli/interactive.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -98,7 +99,7 @@ func (b *InteractiveWorkflowBuilder) promptForWorkflowName() error { Value(&b.WorkflowName). Validate(ValidateWorkflowName), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) return form.Run() } @@ -222,7 +223,7 @@ func (b *InteractiveWorkflowBuilder) promptForConfiguration() error { ). Title("Instructions"). Description("Describe what you want this workflow to accomplish"), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return err @@ -267,7 +268,7 @@ func (b *InteractiveWorkflowBuilder) generateWorkflow(force bool) error { Negative("No, cancel"). Value(&overwrite), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := confirmForm.Run(); err != nil { return fmt.Errorf("confirmation failed: %w", err) diff --git a/pkg/cli/run_interactive.go b/pkg/cli/run_interactive.go index 132b1ef4f91..59193cb87f8 100644 --- a/pkg/cli/run_interactive.go +++ b/pkg/cli/run_interactive.go @@ -13,6 +13,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/sliceutil" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/tty" "github.com/github/gh-aw/pkg/workflow" ) @@ -189,7 +190,7 @@ func selectWorkflow(workflows []WorkflowOption) (*WorkflowOption, error) { Height(15). Value(&selected), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return nil, fmt.Errorf("workflow selection cancelled or failed: %w", err) @@ -313,7 +314,7 @@ func collectInputsWithMap(inputs map[string]*workflow.InputDefinition) ([]string } // Show the form - form := huh.NewForm(formGroups...).WithAccessible(console.IsAccessibleMode()) + form := huh.NewForm(formGroups...).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return nil, fmt.Errorf("input collection cancelled: %w", err) } @@ -350,7 +351,7 @@ func confirmExecution(wf *WorkflowOption, inputs []string) bool { Negative("No, cancel"). Value(&confirm), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { runInteractiveLog.Printf("Confirmation failed: %v", err) diff --git a/pkg/cli/workflows/test-top-level-github-app-activation.md b/pkg/cli/workflows/test-top-level-github-app-activation.md new file mode 100644 index 00000000000..00dce1efadc --- /dev/null +++ b/pkg/cli/workflows/test-top-level-github-app-activation.md @@ -0,0 +1,25 @@ +--- +on: + issues: + types: [opened] + reaction: eyes +permissions: + contents: read + +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +# Top-Level GitHub App Fallback for Activation + +This workflow demonstrates using a top-level github-app as a fallback for activation operations. + +The top-level `github-app` is automatically applied to the activation job (reactions, status +comments, skip-if checks) when no `on.github-app` is defined. + +When an issue is opened, react with 👀 and create a follow-up issue using the GitHub App token. diff --git a/pkg/cli/workflows/test-top-level-github-app-checkout.md b/pkg/cli/workflows/test-top-level-github-app-checkout.md new file mode 100644 index 00000000000..f8596805893 --- /dev/null +++ b/pkg/cli/workflows/test-top-level-github-app-checkout.md @@ -0,0 +1,27 @@ +--- +on: + issues: + types: [opened] +permissions: + contents: read + +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +checkout: + repository: myorg/private-repo + path: private +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +# Top-Level GitHub App Fallback for Checkout + +This workflow demonstrates using a top-level github-app as a fallback for checkout operations. + +The top-level `github-app` is automatically applied to checkout operations that do not have +their own `github-app` or `github-token` configured. + +This is useful for checking out private repositories using the GitHub App installation token. diff --git a/pkg/cli/workflows/test-top-level-github-app-dependencies.md b/pkg/cli/workflows/test-top-level-github-app-dependencies.md new file mode 100644 index 00000000000..098ecbfc91d --- /dev/null +++ b/pkg/cli/workflows/test-top-level-github-app-dependencies.md @@ -0,0 +1,26 @@ +--- +on: + issues: + types: [opened] +permissions: + contents: read + +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +dependencies: + packages: + - myorg/private-skill +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +# Top-Level GitHub App Fallback for APM Dependencies + +This workflow demonstrates using a top-level github-app as a fallback for APM dependencies. + +The top-level `github-app` is automatically applied to APM package installations when no +`dependencies.github-app` is configured. This allows installing APM packages from private +repositories across organizations using the GitHub App installation token. diff --git a/pkg/cli/workflows/test-top-level-github-app-mcp.md b/pkg/cli/workflows/test-top-level-github-app-mcp.md new file mode 100644 index 00000000000..7a46b57b669 --- /dev/null +++ b/pkg/cli/workflows/test-top-level-github-app-mcp.md @@ -0,0 +1,28 @@ +--- +on: + issues: + types: [opened] +permissions: + contents: read + +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +tools: + github: + mode: remote + toolsets: [default] +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +# Top-Level GitHub App Fallback for GitHub MCP Tool + +This workflow demonstrates using a top-level github-app as a fallback for tools.github +token minting operations. + +The top-level `github-app` is automatically applied to the GitHub MCP tool configuration +when no `tools.github.github-app` is defined. This mints a GitHub App installation access +token for the GitHub MCP server to use when making API calls. diff --git a/pkg/cli/workflows/test-top-level-github-app-override.md b/pkg/cli/workflows/test-top-level-github-app-override.md new file mode 100644 index 00000000000..a286fa0502f --- /dev/null +++ b/pkg/cli/workflows/test-top-level-github-app-override.md @@ -0,0 +1,33 @@ +--- +on: + issues: + types: [opened] + reaction: eyes + github-app: + app-id: ${{ vars.ACTIVATION_APP_ID }} + private-key: ${{ secrets.ACTIVATION_APP_KEY }} +permissions: + contents: read + +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + github-app: + app-id: ${{ vars.SAFE_OUTPUTS_APP_ID }} + private-key: ${{ secrets.SAFE_OUTPUTS_APP_KEY }} + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +# Section-Specific GitHub App Takes Precedence Over Top-Level + +This workflow demonstrates that section-specific github-app configurations take precedence +over the top-level github-app fallback. + +- `on.github-app` is explicitly set → activation uses ACTIVATION_APP_ID, not APP_ID +- `safe-outputs.github-app` is explicitly set → safe-outputs uses SAFE_OUTPUTS_APP_ID, not APP_ID + +The top-level github-app (APP_ID) is only used as a fallback when sections do not define +their own github-app. diff --git a/pkg/cli/workflows/test-top-level-github-app-safe-outputs.md b/pkg/cli/workflows/test-top-level-github-app-safe-outputs.md new file mode 100644 index 00000000000..3ef2ebe05a9 --- /dev/null +++ b/pkg/cli/workflows/test-top-level-github-app-safe-outputs.md @@ -0,0 +1,25 @@ +--- +on: + issues: + types: [opened] +permissions: + contents: read + +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + create-issue: + title-prefix: "[automated] " + labels: [automation] +engine: copilot +--- + +# Top-Level GitHub App Fallback for Safe Outputs + +This workflow demonstrates using a top-level github-app as a fallback for safe-outputs. + +The top-level `github-app` is automatically applied to the safe-outputs job when no +section-specific `github-app` is defined under `safe-outputs:`. + +When an issue is opened, analyze it and create a follow-up issue using the GitHub App token. diff --git a/pkg/console/confirm.go b/pkg/console/confirm.go index 98737b59a83..6edfbd2dcee 100644 --- a/pkg/console/confirm.go +++ b/pkg/console/confirm.go @@ -4,6 +4,7 @@ package console import ( "github.com/charmbracelet/huh" + "github.com/github/gh-aw/pkg/styles" ) // ConfirmAction shows an interactive confirmation dialog using Bubble Tea (huh) @@ -19,7 +20,7 @@ func ConfirmAction(title, affirmative, negative string) (bool, error) { Negative(negative). Value(&confirmed), ), - ).WithAccessible(IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(IsAccessibleMode()) if err := confirmForm.Run(); err != nil { return false, err diff --git a/pkg/console/input.go b/pkg/console/input.go index 9697c524f33..f203b8586e1 100644 --- a/pkg/console/input.go +++ b/pkg/console/input.go @@ -6,6 +6,7 @@ import ( "errors" "github.com/charmbracelet/huh" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/tty" ) @@ -34,7 +35,7 @@ func PromptSecretInput(title, description string) (string, error) { }). Value(&value), ), - ).WithAccessible(IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(IsAccessibleMode()) if err := form.Run(); err != nil { return "", err diff --git a/pkg/console/list.go b/pkg/console/list.go index 1a514bcc1c3..c97f0e09347 100644 --- a/pkg/console/list.go +++ b/pkg/console/list.go @@ -19,6 +19,9 @@ import ( var listLog = logger.New("console:list") +// listHeightPadding is the number of rows reserved for the list title, borders, and status bar +const listHeightPadding = 4 + // listModel is the Bubble Tea model for the interactive list type listModel struct { list list.Model @@ -47,6 +50,7 @@ func (m listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.WindowSizeMsg: m.list.SetWidth(msg.Width) + m.list.SetHeight(msg.Height - listHeightPadding) return m, nil } diff --git a/pkg/parser/import_field_extractor.go b/pkg/parser/import_field_extractor.go index 5621d441010..fd0a5d35141 100644 --- a/pkg/parser/import_field_extractor.go +++ b/pkg/parser/import_field_extractor.go @@ -50,6 +50,8 @@ type importAccumulator struct { // First on.github-token / on.github-app found across all imported files (first-wins strategy) activationGitHubToken string activationGitHubApp string // JSON-encoded GitHubAppConfig + // First top-level github-app found across all imported files (first-wins strategy) + topLevelGitHubApp string // JSON-encoded GitHubAppConfig } // newImportAccumulator creates and initializes a new importAccumulator. @@ -228,6 +230,14 @@ func (acc *importAccumulator) extractAllImportFields(content []byte, item import } } + // Extract top-level github-app from imported file (first-wins: only set if not yet populated) + if acc.topLevelGitHubApp == "" { + if appJSON := extractTopLevelGitHubApp(string(content)); appJSON != "" { + acc.topLevelGitHubApp = appJSON + log.Printf("Extracted top-level github-app from import: %s", item.fullPath) + } + } + // Extract and merge plugins from imported file (merge into set to avoid duplicates). // Handles both simple string format and object format with MCP configs. pluginsContent, err := extractFrontmatterField(string(content), "plugins", "[]") @@ -331,6 +341,7 @@ func (acc *importAccumulator) toImportsResult(topologicalOrder []string) *Import ImportInputs: acc.importInputs, MergedActivationGitHubToken: acc.activationGitHubToken, MergedActivationGitHubApp: acc.activationGitHubApp, + MergedTopLevelGitHubApp: acc.topLevelGitHubApp, } } @@ -370,11 +381,10 @@ func extractOnGitHubToken(content string) string { return token } -// extractOnGitHubApp returns the JSON-encoded on.github-app object from workflow content. -// Returns "" if the field is absent, not a valid object, or missing required fields. -func extractOnGitHubApp(content string) string { - appJSON, err := extractOnSectionAnyField(content, "github-app") - if err != nil || appJSON == "" || appJSON == "null" { +// validateGitHubAppJSON validates that a JSON-encoded GitHub App configuration has the required +// fields (app-id and private-key). Returns the input JSON if valid, or "" otherwise. +func validateGitHubAppJSON(appJSON string) string { + if appJSON == "" || appJSON == "null" { return "" } var appMap map[string]any @@ -389,3 +399,23 @@ func extractOnGitHubApp(content string) string { } return appJSON } + +// extractOnGitHubApp returns the JSON-encoded on.github-app object from workflow content. +// Returns "" if the field is absent, not a valid object, or missing required fields. +func extractOnGitHubApp(content string) string { + appJSON, err := extractOnSectionAnyField(content, "github-app") + if err != nil { + return "" + } + return validateGitHubAppJSON(appJSON) +} + +// extractTopLevelGitHubApp returns the JSON-encoded top-level github-app object from workflow content. +// Returns "" if the field is absent, not a valid object, or missing required fields. +func extractTopLevelGitHubApp(content string) string { + appJSON, err := extractFrontmatterField(content, "github-app", "") + if err != nil { + return "" + } + return validateGitHubAppJSON(appJSON) +} diff --git a/pkg/parser/import_processor.go b/pkg/parser/import_processor.go index 872afbf88ad..3acc23c63fa 100644 --- a/pkg/parser/import_processor.go +++ b/pkg/parser/import_processor.go @@ -34,6 +34,7 @@ type ImportsResult struct { MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames) MergedActivationGitHubToken string // GitHub token from on.github-token in first imported workflow that defines it MergedActivationGitHubApp string // JSON-encoded on.github-app from first imported workflow that defines it + MergedTopLevelGitHubApp string // JSON-encoded top-level github-app from first imported workflow that defines it MergedPostSteps string // Merged post-steps configuration from all imports (appended in order) MergedLabels []string // Merged labels from all imports (union of label names) MergedCaches []string // Merged cache configurations from all imports (appended in order) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a0f4abd5dc8..c5713c64db9 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -8162,6 +8162,10 @@ "additionalProperties": false } ] + }, + "github-app": { + "$ref": "#/$defs/github_app", + "description": "Top-level GitHub App configuration used as a fallback for all nested github-app token minting operations (on, safe-outputs, checkout, tools.github, dependencies). When a nested section does not define its own github-app, this top-level configuration is used automatically." } }, "additionalProperties": false, diff --git a/pkg/styles/huh_theme.go b/pkg/styles/huh_theme.go new file mode 100644 index 00000000000..c3110368c5d --- /dev/null +++ b/pkg/styles/huh_theme.go @@ -0,0 +1,74 @@ +//go:build !js && !wasm + +package styles + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +// HuhTheme returns a custom huh.Theme that maps the pkg/styles Dracula-inspired +// color palette to huh form fields, giving interactive forms the same visual +// identity as the rest of the CLI output. +func HuhTheme() *huh.Theme { + t := huh.ThemeBase() + + // Map the pkg/styles palette to lipgloss.AdaptiveColor for huh compatibility. + // huh uses github.com/charmbracelet/lipgloss, so we use that type here. + var ( + primary = lipgloss.AdaptiveColor{Light: hexColorPurpleLight, Dark: hexColorPurpleDark} + success = lipgloss.AdaptiveColor{Light: hexColorSuccessLight, Dark: hexColorSuccessDark} + errorColor = lipgloss.AdaptiveColor{Light: hexColorErrorLight, Dark: hexColorErrorDark} + warning = lipgloss.AdaptiveColor{Light: hexColorWarningLight, Dark: hexColorWarningDark} + comment = lipgloss.AdaptiveColor{Light: hexColorCommentLight, Dark: hexColorCommentDark} + fg = lipgloss.AdaptiveColor{Light: hexColorForegroundLight, Dark: hexColorForegroundDark} + bg = lipgloss.AdaptiveColor{Light: hexColorBackgroundLight, Dark: hexColorBackgroundDark} + border = lipgloss.AdaptiveColor{Light: hexColorBorderLight, Dark: hexColorBorderDark} + ) + + // Focused field styles + t.Focused.Base = t.Focused.Base.BorderForeground(border) + t.Focused.Card = t.Focused.Base + t.Focused.Title = t.Focused.Title.Foreground(primary).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(primary).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(primary) + t.Focused.Description = t.Focused.Description.Foreground(comment) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor) + + // Select / navigation indicators + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(warning) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(warning) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(warning) + + // List option styles + t.Focused.Option = t.Focused.Option.Foreground(fg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(warning) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(success) + t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(success) + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(fg) + t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(comment) + + // Button styles + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(bg).Background(primary).Bold(true) + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(fg).Background(bg) + t.Focused.Next = t.Focused.FocusedButton + + // Text input styles + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(warning) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(comment) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primary) + + // Blurred styles mirror focused but hide the border + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.Card = t.Blurred.Base + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + // Group header styles + t.Group.Title = t.Focused.Title + t.Group.Description = t.Focused.Description + + return t +} diff --git a/pkg/workflow/aw_info_tmp_test.go b/pkg/workflow/aw_info_tmp_test.go index d076f697b2f..1cc1c90d26d 100644 --- a/pkg/workflow/aw_info_tmp_test.go +++ b/pkg/workflow/aw_info_tmp_test.go @@ -64,6 +64,12 @@ This workflow tests that aw_info.json is generated in /tmp directory. t.Error("Expected step to call main(core, context) from generate_aw_info.cjs") } + // Verify setupGlobals is called before main so that global.core is available + // for modules like staged_preview.cjs that rely on it (fixes staged mode ReferenceError) + if !strings.Contains(lockStr, "setupGlobals(core, github, context, exec, io)") { + t.Error("Expected step to call setupGlobals before main to set global.core") + } + // Test 2: Verify compile-time env vars are set on the step if !strings.Contains(lockStr, "GH_AW_INFO_ENGINE_ID:") { t.Error("Expected GH_AW_INFO_ENGINE_ID env var to be set on the step") diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 7e3723dc35d..4d6b44d2b2b 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -131,6 +131,12 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath return formatCompilerError(markdownPath, "error", err.Error(), err) } + // Validate GitHub App-only permissions require a GitHub App to be configured + log.Printf("Validating GitHub App-only permissions") + if err := validateGitHubAppOnlyPermissions(workflowData); err != nil { + return formatCompilerError(markdownPath, "error", err.Error(), err) + } + // Validate agent file exists if specified in engine config log.Printf("Validating agent file if specified") if err := c.validateAgentFile(workflowData, markdownPath); err != nil { diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 90fd4d35641..af882d0dddd 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -564,6 +564,134 @@ func (c *Compiler) mergeJobsFromYAMLImports(mainJobs map[string]any, mergedJobsJ return result } +// extractTopLevelGitHubApp extracts the 'github-app' field from the top-level frontmatter. +// This provides a single GitHub App configuration that serves as a fallback for all nested +// github-app token minting operations (on, safe-outputs, checkout, tools.github, dependencies). +func extractTopLevelGitHubApp(frontmatter map[string]any) *GitHubAppConfig { + appAny, ok := frontmatter["github-app"] + if !ok { + return nil + } + appMap, ok := appAny.(map[string]any) + if !ok { + return nil + } + app := parseAppConfig(appMap) + if app.AppID == "" || app.PrivateKey == "" { + return nil + } + return app +} + +// resolveTopLevelGitHubApp resolves the top-level github-app for token minting fallback. +// Precedence: +// 1. Current workflow's top-level github-app (explicit override wins) +// 2. First top-level github-app found across imported shared workflows +// 3. Nil (no fallback configured) +func resolveTopLevelGitHubApp(frontmatter map[string]any, importsResult *parser.ImportsResult) *GitHubAppConfig { + if app := extractTopLevelGitHubApp(frontmatter); app != nil { + return app + } + if importsResult != nil && importsResult.MergedTopLevelGitHubApp != "" { + var appMap map[string]any + if err := json.Unmarshal([]byte(importsResult.MergedTopLevelGitHubApp), &appMap); err == nil { + app := parseAppConfig(appMap) + if app.AppID != "" && app.PrivateKey != "" { + orchestratorWorkflowLog.Print("Using top-level github-app from imported shared workflow") + return app + } + } + } + return nil +} + +// topLevelFallbackNeeded reports whether the top-level github-app should be applied as a +// fallback for a given section. It returns true when the section has neither an explicit +// github-app nor an explicit github-token already configured. +// +// Rules (consistent across all sections): +// - If a section-specific github-app is set → keep it, no fallback needed. +// - If a section-specific github-token is set → keep it, no fallback needed (a token +// already provides the auth; injecting a github-app would silently change precedence). +// - Otherwise → apply the top-level fallback. +func topLevelFallbackNeeded(app *GitHubAppConfig, token string) bool { + return app == nil && token == "" +} + +// applyTopLevelGitHubAppFallbacks applies the top-level github-app as a fallback for all +// nested github-app token minting operations when no section-specific github-app is configured. +// Precedence: section-specific github-app > section-specific github-token > top-level github-app. +// +// Every section uses topLevelFallbackNeeded to decide whether the fallback is required, +// ensuring consistent behaviour across all token-minting sites. +func applyTopLevelGitHubAppFallbacks(data *WorkflowData) { + fallback := data.TopLevelGitHubApp + if fallback == nil { + return + } + + // Fallback for activation (on.github-app / on.github-token) + if topLevelFallbackNeeded(data.ActivationGitHubApp, data.ActivationGitHubToken) { + orchestratorWorkflowLog.Print("Applying top-level github-app fallback for activation") + data.ActivationGitHubApp = fallback + } + + // Fallback for safe-outputs (safe-outputs.github-app / safe-outputs.github-token) + if data.SafeOutputs != nil && topLevelFallbackNeeded(data.SafeOutputs.GitHubApp, data.SafeOutputs.GitHubToken) { + orchestratorWorkflowLog.Print("Applying top-level github-app fallback for safe-outputs") + data.SafeOutputs.GitHubApp = fallback + } + + // Fallback for checkout configs (checkout.github-app / checkout.github-token per entry) + for _, cfg := range data.CheckoutConfigs { + if topLevelFallbackNeeded(cfg.GitHubApp, cfg.GitHubToken) { + orchestratorWorkflowLog.Print("Applying top-level github-app fallback for checkout") + cfg.GitHubApp = fallback + } + } + + // Fallback for tools.github (tools.github.github-app / tools.github.github-token). + // Also skipped when tools.github is explicitly disabled (github: false) — do not re-enable it. + if data.ParsedTools != nil && data.ParsedTools.GitHub != nil && + topLevelFallbackNeeded(data.ParsedTools.GitHub.GitHubApp, data.ParsedTools.GitHub.GitHubToken) && + data.Tools["github"] != false { + orchestratorWorkflowLog.Print("Applying top-level github-app fallback for tools.github") + data.ParsedTools.GitHub.GitHubApp = fallback + // Also update the raw tools map so applyDefaultTools (called from applyDefaults in + // processOnSectionAndFilters) does not lose the fallback when it rebuilds ParsedTools + // from the map. + appMap := map[string]any{ + "app-id": fallback.AppID, + "private-key": fallback.PrivateKey, + } + if fallback.Owner != "" { + appMap["owner"] = fallback.Owner + } + if len(fallback.Repositories) > 0 { + repos := make([]any, len(fallback.Repositories)) + for i, r := range fallback.Repositories { + repos[i] = r + } + appMap["repositories"] = repos + } + // Normalize data.Tools["github"] to a map so the github-app survives re-parsing. + // Configurations like `github: true` are normalized here rather than losing the fallback. + if github, ok := data.Tools["github"].(map[string]any); ok { + // Already a map; inject into existing settings. + github["github-app"] = appMap + } else { + // Non-map value (e.g. true) — create a fresh map. + data.Tools["github"] = map[string]any{"github-app": appMap} + } + } + + // Fallback for APM dependencies (dependencies.github-app; no github-token field) + if data.APMDependencies != nil && topLevelFallbackNeeded(data.APMDependencies.GitHubApp, "") { + orchestratorWorkflowLog.Print("Applying top-level github-app fallback for dependencies") + data.APMDependencies.GitHubApp = fallback + } +} + // extractAdditionalConfigurations extracts cache-memory, repo-memory, mcp-scripts, and safe-outputs configurations func (c *Compiler) extractAdditionalConfigurations( frontmatter map[string]any, @@ -611,6 +739,7 @@ func (c *Compiler) extractAdditionalConfigurations( workflowData.SkipBots = c.mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots) workflowData.ActivationGitHubToken = c.resolveActivationGitHubToken(frontmatter, importsResult) workflowData.ActivationGitHubApp = c.resolveActivationGitHubApp(frontmatter, importsResult) + workflowData.TopLevelGitHubApp = resolveTopLevelGitHubApp(frontmatter, importsResult) // Use the already extracted output configuration workflowData.SafeOutputs = safeOutputs @@ -678,6 +807,10 @@ func (c *Compiler) extractAdditionalConfigurations( // This ensures every workflow with safe-outputs has at least one meaningful action handler. applyDefaultCreateIssue(workflowData) + // Apply the top-level github-app as a fallback for all nested github-app token minting operations. + // This runs last so that all section-specific configurations have been resolved first. + applyTopLevelGitHubAppFallbacks(workflowData) + return nil } diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index c1e81d5e6a7..dc3ed383288 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -394,6 +394,7 @@ type WorkflowData struct { StatusComment *bool // whether to post status comments (default: true when ai-reaction is set, false otherwise) ActivationGitHubToken string // custom github token from on.github-token for reactions/comments ActivationGitHubApp *GitHubAppConfig // github app config from on.github-app for minting activation tokens + TopLevelGitHubApp *GitHubAppConfig // top-level github-app fallback for all nested github-app token minting operations LockForAgent bool // whether to lock the issue during agent workflow execution Jobs map[string]any // custom job configurations with dependencies Cache string // cache configuration diff --git a/pkg/workflow/compiler_yaml.go b/pkg/workflow/compiler_yaml.go index 5f20e7a4d61..86423302706 100644 --- a/pkg/workflow/compiler_yaml.go +++ b/pkg/workflow/compiler_yaml.go @@ -677,6 +677,8 @@ func (c *Compiler) generateCreateAwInfo(yaml *strings.Builder, data *WorkflowDat fmt.Fprintf(yaml, " uses: %s\n", GetActionPin("actions/github-script")) yaml.WriteString(" with:\n") yaml.WriteString(" script: |\n") + yaml.WriteString(" const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');\n") + yaml.WriteString(" setupGlobals(core, github, context, exec, io);\n") yaml.WriteString(" const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs');\n") yaml.WriteString(" await main(core, context);\n") } diff --git a/pkg/workflow/dangerous_permissions_validation_test.go b/pkg/workflow/dangerous_permissions_validation_test.go index 2fb31ce6f02..e627e0d5b45 100644 --- a/pkg/workflow/dangerous_permissions_validation_test.go +++ b/pkg/workflow/dangerous_permissions_validation_test.go @@ -153,7 +153,7 @@ func TestFindWritePermissions(t *testing.T) { { name: "write-all shorthand", permissions: NewPermissionsWriteAll(), - expectedWriteCount: 15, // All permission scopes except id-token (which is excluded) + expectedWriteCount: 14, // All GitHub Actions permission scopes except id-token and metadata (which are excluded) expectedScopes: nil, // Don't check specific scopes for shorthand }, { diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go index 66518fa8f3c..56592bcd72a 100644 --- a/pkg/workflow/frontmatter_types.go +++ b/pkg/workflow/frontmatter_types.go @@ -35,28 +35,78 @@ type RuntimesConfig struct { Ruby *RuntimeConfig `json:"ruby,omitempty"` // Ruby runtime } -// PermissionsConfig represents GitHub Actions permissions configuration -// Supports both shorthand (read-all, write-all) and detailed scope-based permissions +// GitHubActionsPermissionsConfig holds permission scopes supported by the GitHub Actions GITHUB_TOKEN. +// These scopes can be declared in the workflow's top-level permissions block and are enforced +// natively by GitHub Actions. +type GitHubActionsPermissionsConfig struct { + Actions string `json:"actions,omitempty"` + Checks string `json:"checks,omitempty"` + Contents string `json:"contents,omitempty"` + Deployments string `json:"deployments,omitempty"` + IDToken string `json:"id-token,omitempty"` + Issues string `json:"issues,omitempty"` + Discussions string `json:"discussions,omitempty"` + Packages string `json:"packages,omitempty"` + Pages string `json:"pages,omitempty"` + PullRequests string `json:"pull-requests,omitempty"` + RepositoryProjects string `json:"repository-projects,omitempty"` + SecurityEvents string `json:"security-events,omitempty"` + Statuses string `json:"statuses,omitempty"` +} + +// GitHubAppPermissionsConfig holds permission scopes that are exclusive to GitHub App +// installation access tokens (not supported by GITHUB_TOKEN). When any of these are +// specified, a GitHub App must be configured in the workflow. +type GitHubAppPermissionsConfig struct { + // Organization-level permissions (the common use-case placed first) + OrganizationProjects string `json:"organization-projects,omitempty"` + Members string `json:"members,omitempty"` + OrganizationAdministration string `json:"organization-administration,omitempty"` + TeamDiscussions string `json:"team-discussions,omitempty"` + OrganizationHooks string `json:"organization-hooks,omitempty"` + OrganizationMembers string `json:"organization-members,omitempty"` + OrganizationPackages string `json:"organization-packages,omitempty"` + OrganizationSelfHostedRunners string `json:"organization-self-hosted-runners,omitempty"` + OrganizationCustomOrgRoles string `json:"organization-custom-org-roles,omitempty"` + OrganizationCustomProperties string `json:"organization-custom-properties,omitempty"` + OrganizationCustomRepositoryRoles string `json:"organization-custom-repository-roles,omitempty"` + OrganizationAnnouncementBanners string `json:"organization-announcement-banners,omitempty"` + OrganizationEvents string `json:"organization-events,omitempty"` + OrganizationPlan string `json:"organization-plan,omitempty"` + OrganizationUserBlocking string `json:"organization-user-blocking,omitempty"` + OrganizationPersonalAccessTokenReqs string `json:"organization-personal-access-token-requests,omitempty"` + OrganizationPersonalAccessTokens string `json:"organization-personal-access-tokens,omitempty"` + OrganizationCopilot string `json:"organization-copilot,omitempty"` + OrganizationCodespaces string `json:"organization-codespaces,omitempty"` + // Repository-level permissions + Administration string `json:"administration,omitempty"` + Environments string `json:"environments,omitempty"` + GitSigning string `json:"git-signing,omitempty"` + VulnerabilityAlerts string `json:"vulnerability-alerts,omitempty"` + Workflows string `json:"workflows,omitempty"` + RepositoryHooks string `json:"repository-hooks,omitempty"` + SingleFile string `json:"single-file,omitempty"` + Codespaces string `json:"codespaces,omitempty"` + RepositoryCustomProperties string `json:"repository-custom-properties,omitempty"` + // User-level permissions + EmailAddresses string `json:"email-addresses,omitempty"` + CodespacesLifecycleAdmin string `json:"codespaces-lifecycle-admin,omitempty"` + CodespacesMetadata string `json:"codespaces-metadata,omitempty"` +} + +// PermissionsConfig represents GitHub Actions permissions configuration. +// Supports both shorthand (read-all, write-all) and detailed scope-based permissions. +// Embeds GitHubActionsPermissionsConfig for standard GITHUB_TOKEN scopes and +// GitHubAppPermissionsConfig for GitHub App-only scopes. type PermissionsConfig struct { // Shorthand permission (read-all, write-all, read, write, none) Shorthand string `json:"-"` // Not in JSON, set when parsing shorthand format - // Detailed permissions by scope - Actions string `json:"actions,omitempty"` - Checks string `json:"checks,omitempty"` - Contents string `json:"contents,omitempty"` - Deployments string `json:"deployments,omitempty"` - IDToken string `json:"id-token,omitempty"` - Issues string `json:"issues,omitempty"` - Discussions string `json:"discussions,omitempty"` - Packages string `json:"packages,omitempty"` - Pages string `json:"pages,omitempty"` - PullRequests string `json:"pull-requests,omitempty"` - RepositoryProjects string `json:"repository-projects,omitempty"` - SecurityEvents string `json:"security-events,omitempty"` - Statuses string `json:"statuses,omitempty"` - OrganizationProjects string `json:"organization-projects,omitempty"` - OrganizationPackages string `json:"organization-packages,omitempty"` + // GitHub Actions GITHUB_TOKEN permission scopes + GitHubActionsPermissionsConfig + + // GitHub App-only permission scopes (require a GitHub App to be configured) + GitHubAppPermissionsConfig } // PluginMCPConfig represents MCP configuration for a plugin @@ -356,6 +406,7 @@ func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, err for scope, level := range permissions { if levelStr, ok := level.(string); ok { switch scope { + // GitHub Actions permission scopes case "actions": config.Actions = levelStr case "checks": @@ -384,8 +435,67 @@ func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, err config.Statuses = levelStr case "organization-projects": config.OrganizationProjects = levelStr + // GitHub App-only permission scopes + case "administration": + config.Administration = levelStr + case "environments": + config.Environments = levelStr + case "git-signing": + config.GitSigning = levelStr + case "vulnerability-alerts": + config.VulnerabilityAlerts = levelStr + case "workflows": + config.Workflows = levelStr + case "repository-hooks": + config.RepositoryHooks = levelStr + case "single-file": + config.SingleFile = levelStr + case "codespaces": + config.Codespaces = levelStr + case "repository-custom-properties": + config.RepositoryCustomProperties = levelStr + case "members": + config.Members = levelStr + case "organization-administration": + config.OrganizationAdministration = levelStr + case "team-discussions": + config.TeamDiscussions = levelStr + case "organization-hooks": + config.OrganizationHooks = levelStr + case "organization-members": + config.OrganizationMembers = levelStr case "organization-packages": config.OrganizationPackages = levelStr + case "organization-self-hosted-runners": + config.OrganizationSelfHostedRunners = levelStr + case "organization-custom-org-roles": + config.OrganizationCustomOrgRoles = levelStr + case "organization-custom-properties": + config.OrganizationCustomProperties = levelStr + case "organization-custom-repository-roles": + config.OrganizationCustomRepositoryRoles = levelStr + case "organization-announcement-banners": + config.OrganizationAnnouncementBanners = levelStr + case "organization-events": + config.OrganizationEvents = levelStr + case "organization-plan": + config.OrganizationPlan = levelStr + case "organization-user-blocking": + config.OrganizationUserBlocking = levelStr + case "organization-personal-access-token-requests": + config.OrganizationPersonalAccessTokenReqs = levelStr + case "organization-personal-access-tokens": + config.OrganizationPersonalAccessTokens = levelStr + case "organization-copilot": + config.OrganizationCopilot = levelStr + case "organization-codespaces": + config.OrganizationCodespaces = levelStr + case "email-addresses": + config.EmailAddresses = levelStr + case "codespaces-lifecycle-admin": + config.CodespacesLifecycleAdmin = levelStr + case "codespaces-metadata": + config.CodespacesMetadata = levelStr } } } @@ -767,6 +877,7 @@ func permissionsConfigToMap(config *PermissionsConfig) map[string]any { result := make(map[string]any) + // GitHub Actions permission scopes if config.Actions != "" { result["actions"] = config.Actions } @@ -809,9 +920,102 @@ func permissionsConfigToMap(config *PermissionsConfig) map[string]any { if config.OrganizationProjects != "" { result["organization-projects"] = config.OrganizationProjects } + + // GitHub App-only permission scopes - repository-level + if config.Administration != "" { + result["administration"] = config.Administration + } + if config.Environments != "" { + result["environments"] = config.Environments + } + if config.GitSigning != "" { + result["git-signing"] = config.GitSigning + } + if config.VulnerabilityAlerts != "" { + result["vulnerability-alerts"] = config.VulnerabilityAlerts + } + if config.Workflows != "" { + result["workflows"] = config.Workflows + } + if config.RepositoryHooks != "" { + result["repository-hooks"] = config.RepositoryHooks + } + if config.SingleFile != "" { + result["single-file"] = config.SingleFile + } + if config.Codespaces != "" { + result["codespaces"] = config.Codespaces + } + if config.RepositoryCustomProperties != "" { + result["repository-custom-properties"] = config.RepositoryCustomProperties + } + + // GitHub App-only permission scopes - organization-level + if config.Members != "" { + result["members"] = config.Members + } + if config.OrganizationAdministration != "" { + result["organization-administration"] = config.OrganizationAdministration + } + if config.TeamDiscussions != "" { + result["team-discussions"] = config.TeamDiscussions + } + if config.OrganizationHooks != "" { + result["organization-hooks"] = config.OrganizationHooks + } + if config.OrganizationMembers != "" { + result["organization-members"] = config.OrganizationMembers + } if config.OrganizationPackages != "" { result["organization-packages"] = config.OrganizationPackages } + if config.OrganizationSelfHostedRunners != "" { + result["organization-self-hosted-runners"] = config.OrganizationSelfHostedRunners + } + if config.OrganizationCustomOrgRoles != "" { + result["organization-custom-org-roles"] = config.OrganizationCustomOrgRoles + } + if config.OrganizationCustomProperties != "" { + result["organization-custom-properties"] = config.OrganizationCustomProperties + } + if config.OrganizationCustomRepositoryRoles != "" { + result["organization-custom-repository-roles"] = config.OrganizationCustomRepositoryRoles + } + if config.OrganizationAnnouncementBanners != "" { + result["organization-announcement-banners"] = config.OrganizationAnnouncementBanners + } + if config.OrganizationEvents != "" { + result["organization-events"] = config.OrganizationEvents + } + if config.OrganizationPlan != "" { + result["organization-plan"] = config.OrganizationPlan + } + if config.OrganizationUserBlocking != "" { + result["organization-user-blocking"] = config.OrganizationUserBlocking + } + if config.OrganizationPersonalAccessTokenReqs != "" { + result["organization-personal-access-token-requests"] = config.OrganizationPersonalAccessTokenReqs + } + if config.OrganizationPersonalAccessTokens != "" { + result["organization-personal-access-tokens"] = config.OrganizationPersonalAccessTokens + } + if config.OrganizationCopilot != "" { + result["organization-copilot"] = config.OrganizationCopilot + } + if config.OrganizationCodespaces != "" { + result["organization-codespaces"] = config.OrganizationCodespaces + } + + // GitHub App-only permission scopes - user-level + if config.EmailAddresses != "" { + result["email-addresses"] = config.EmailAddresses + } + if config.CodespacesLifecycleAdmin != "" { + result["codespaces-lifecycle-admin"] = config.CodespacesLifecycleAdmin + } + if config.CodespacesMetadata != "" { + result["codespaces-metadata"] = config.CodespacesMetadata + } if len(result) == 0 { return nil diff --git a/pkg/workflow/github_app_permissions_validation.go b/pkg/workflow/github_app_permissions_validation.go new file mode 100644 index 00000000000..2158f7bceb4 --- /dev/null +++ b/pkg/workflow/github_app_permissions_validation.go @@ -0,0 +1,169 @@ +package workflow + +import ( + "errors" + "sort" + "strings" +) + +var githubAppPermissionsLog = newValidationLogger("github_app_permissions") + +// validateGitHubAppOnlyPermissions validates that when GitHub App-only permissions +// are specified in the workflow, a GitHub App is configured somewhere in the workflow, +// and that no GitHub App-only permission is declared with "write" access (write operations +// must be performed via safe-outputs, not through declared permissions). +// +// GitHub App-only permissions (e.g., members, administration) cannot be exercised +// through the GITHUB_TOKEN — they require a GitHub App installation access token. When such +// permissions are declared, a GitHub App must be configured via one of: +// - tools.github.github-app +// - safe-outputs.github-app +// - the top-level github-app field (for the activation/pre-activation jobs) +// +// Returns an error if GitHub App-only permissions are used without any GitHub App configured, +// or if "write" level is requested for any GitHub App-only scope. +func validateGitHubAppOnlyPermissions(workflowData *WorkflowData) error { + githubAppPermissionsLog.Print("Starting GitHub App-only permissions validation") + + if workflowData.Permissions == "" { + githubAppPermissionsLog.Print("No permissions defined, validation passed") + return nil + } + + permissions := NewPermissionsParser(workflowData.Permissions).ToPermissions() + if permissions == nil { + githubAppPermissionsLog.Print("Could not parse permissions, validation passed") + return nil + } + + // Find any GitHub App-only permission scopes that are *explicitly* declared. + // We must not use Get() here because shorthand permissions (read-all / write-all) and + // "all: read" would cause Get() to return a value for every scope, incorrectly + // requiring a GitHub App even when no App-only scopes were explicitly declared. + var appOnlyScopes []PermissionScope + for _, scope := range GetAllGitHubAppOnlyScopes() { + if _, exists := permissions.GetExplicit(scope); exists { + appOnlyScopes = append(appOnlyScopes, scope) + } + } + + if len(appOnlyScopes) == 0 { + githubAppPermissionsLog.Print("No GitHub App-only permissions found, validation passed") + return nil + } + + githubAppPermissionsLog.Printf("Found %d GitHub App-only permissions, checking for GitHub App configuration", len(appOnlyScopes)) + + // Check if "write" is requested for any read-only GitHub App-only scopes. + if err := validateGitHubAppOnlyPermissionsWrite(permissions, appOnlyScopes); err != nil { + return err + } + + // Check if any GitHub App is configured + if hasGitHubAppConfigured(workflowData) { + githubAppPermissionsLog.Print("GitHub App is configured, validation passed") + return nil + } + + // Format the error message + return formatGitHubAppRequiredError(appOnlyScopes) +} + +// validateGitHubAppOnlyPermissionsWrite checks that no GitHub App-only scope has been +// requested with "write" access. Write operations on GitHub App tokens must be performed +// via safe-outputs, not through declared permissions. +func validateGitHubAppOnlyPermissionsWrite(permissions *Permissions, appOnlyScopes []PermissionScope) error { + var writtenScopes []PermissionScope + for _, scope := range appOnlyScopes { + if level, exists := permissions.GetExplicit(scope); exists && level == PermissionWrite { + writtenScopes = append(writtenScopes, scope) + } + } + if len(writtenScopes) == 0 { + return nil + } + return formatWriteOnAppScopesError(writtenScopes) +} + +// formatWriteOnAppScopesError formats the error when "write" is requested for +// any GitHub App-only scope. All App-only scopes must be declared read-only; +// write operations are performed via safe-outputs. +func formatWriteOnAppScopesError(scopes []PermissionScope) error { + scopeStrs := make([]string, len(scopes)) + for i, s := range scopes { + scopeStrs[i] = string(s) + } + sort.Strings(scopeStrs) + + var lines []string + lines = append(lines, "GitHub App permissions must be declared as \"read\" only.") + lines = append(lines, "Write operations are performed via safe-outputs, not through declared permissions.") + lines = append(lines, "The following GitHub App-only permissions were declared with \"write\" access:") + lines = append(lines, "") + for _, s := range scopeStrs { + lines = append(lines, " - "+s) + } + lines = append(lines, "") + lines = append(lines, "Change the permission level to \"read\" or use safe-outputs for write operations.") + + return errors.New(strings.Join(lines, "\n")) +} + +func hasGitHubAppConfigured(workflowData *WorkflowData) bool { + // Check tools.github.github-app + if workflowData.ParsedTools != nil && + workflowData.ParsedTools.GitHub != nil && + workflowData.ParsedTools.GitHub.GitHubApp != nil { + githubAppPermissionsLog.Print("Found GitHub App in tools.github") + return true + } + + // Check safe-outputs.github-app + if workflowData.SafeOutputs != nil && workflowData.SafeOutputs.GitHubApp != nil { + githubAppPermissionsLog.Print("Found GitHub App in safe-outputs") + return true + } + + // Check the activation job github-app + if workflowData.ActivationGitHubApp != nil { + githubAppPermissionsLog.Print("Found GitHub App in activation config") + return true + } + + return false +} + +// formatGitHubAppRequiredError formats an error message when GitHub App-only permissions +// are used without a GitHub App configured. +func formatGitHubAppRequiredError(appOnlyScopes []PermissionScope) error { + // Sort for deterministic output + scopeStrs := make([]string, len(appOnlyScopes)) + for i, s := range appOnlyScopes { + scopeStrs[i] = string(s) + } + sort.Strings(scopeStrs) + + var lines []string + lines = append(lines, "GitHub App-only permissions require a GitHub App to be configured.") + lines = append(lines, "The following permissions are not supported by the GITHUB_TOKEN and") + lines = append(lines, "can only be exercised through a GitHub App installation access token:") + lines = append(lines, "") + for _, s := range scopeStrs { + lines = append(lines, " - "+s) + } + lines = append(lines, "") + lines = append(lines, "To fix this, configure a GitHub App in your workflow. For example:") + lines = append(lines, ""+"tools:") + lines = append(lines, " github:") + lines = append(lines, " github-app:") + lines = append(lines, " app-id: ${{ vars.APP_ID }}") + lines = append(lines, " private-key: ${{ secrets.APP_PRIVATE_KEY }}") + lines = append(lines, "") + lines = append(lines, "Or in the safe-outputs section:") + lines = append(lines, "safe-outputs:") + lines = append(lines, " github-app:") + lines = append(lines, " app-id: ${{ vars.APP_ID }}") + lines = append(lines, " private-key: ${{ secrets.APP_PRIVATE_KEY }}") + + return errors.New(strings.Join(lines, "\n")) +} diff --git a/pkg/workflow/github_app_permissions_validation_test.go b/pkg/workflow/github_app_permissions_validation_test.go new file mode 100644 index 00000000000..017b0a994cc --- /dev/null +++ b/pkg/workflow/github_app_permissions_validation_test.go @@ -0,0 +1,509 @@ +//go:build !integration + +package workflow + +import ( + "strings" + "testing" +) + +func TestValidateGitHubAppOnlyPermissions(t *testing.T) { + tests := []struct { + name string + permissions string + parsedTools *ToolsConfig + safeOutputs *SafeOutputsConfig + activationApp *GitHubAppConfig + shouldError bool + errorContains string + }{ + { + name: "no permissions - should pass", + permissions: "", + shouldError: false, + }, + { + name: "GitHub Actions-only permissions - should pass without github-app", + permissions: "permissions:\n contents: read\n issues: write", + shouldError: false, + }, + { + name: "organization-projects (App-only) without github-app - should error", + permissions: "permissions:\n organization-projects: read", + shouldError: true, + errorContains: "GitHub App-only permissions require a GitHub App", + }, + { + name: "members permission without github-app - should error", + permissions: "permissions:\n members: read", + shouldError: true, + errorContains: "GitHub App-only permissions require a GitHub App", + }, + { + name: "administration permission without github-app - should error", + permissions: "permissions:\n administration: read", + shouldError: true, + errorContains: "GitHub App-only permissions require a GitHub App", + }, + { + name: "members permission with tools.github.github-app - should pass", + permissions: "permissions:\n members: read", + parsedTools: &ToolsConfig{ + GitHub: &GitHubToolConfig{ + GitHubApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + }, + shouldError: false, + }, + { + name: "members permission with safe-outputs.github-app - should pass", + permissions: "permissions:\n members: read", + safeOutputs: &SafeOutputsConfig{ + GitHubApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + shouldError: false, + }, + { + name: "members permission with activation github-app - should pass", + permissions: "permissions:\n members: read", + activationApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + shouldError: false, + }, + { + name: "workflows permission without github-app - should error", + permissions: "permissions:\n workflows: write", + shouldError: true, + errorContains: "workflows", + }, + { + name: "vulnerability-alerts permission without github-app - should error", + permissions: "permissions:\n vulnerability-alerts: read", + shouldError: true, + errorContains: "vulnerability-alerts", + }, + { + name: "mixed Actions and App-only permissions with github-app - should pass", + permissions: "permissions:\n contents: read\n members: read\n administration: read", + parsedTools: &ToolsConfig{ + GitHub: &GitHubToolConfig{ + GitHubApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + }, + shouldError: false, + }, + { + name: "mixed Actions and App-only permissions without github-app - should error", + permissions: "permissions:\n contents: read\n members: read", + shouldError: true, + errorContains: "GitHub App-only permissions require a GitHub App", + }, + { + name: "read-all shorthand should NOT require a GitHub App", + permissions: "read-all", + shouldError: false, + }, + { + name: "write-all shorthand should NOT require a GitHub App", + permissions: "write-all", + shouldError: false, + }, + { + name: "all:read should NOT require a GitHub App", + permissions: "permissions:\n all: read", + shouldError: false, + }, + { + name: "organization-events with write - should error (no write on App-only scopes)", + permissions: "permissions:\n organization-events: write", + shouldError: true, + errorContains: "safe-outputs", + }, + { + name: "organization-plan with write - should error (no write on App-only scopes)", + permissions: "permissions:\n organization-plan: write", + shouldError: true, + errorContains: "safe-outputs", + }, + { + name: "email-addresses with write - should error (no write on App-only scopes)", + permissions: "permissions:\n email-addresses: write", + shouldError: true, + errorContains: "safe-outputs", + }, + { + name: "codespaces-metadata with write - should error (no write on App-only scopes)", + permissions: "permissions:\n codespaces-metadata: write", + shouldError: true, + errorContains: "safe-outputs", + }, + { + name: "organization-events with read and github-app - should pass", + permissions: "permissions:\n organization-events: read", + parsedTools: &ToolsConfig{ + GitHub: &GitHubToolConfig{ + GitHubApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + }, + shouldError: false, + }, + { + name: "administration with write - should error (no write on App-only scopes)", + permissions: "permissions:\n administration: write", + shouldError: true, + errorContains: "safe-outputs", + }, + { + name: "members with write - should error even with github-app (no write on App-only scopes)", + permissions: "permissions:\n members: write", + parsedTools: &ToolsConfig{ + GitHub: &GitHubToolConfig{ + GitHubApp: &GitHubAppConfig{ + AppID: "${{ vars.APP_ID }}", + PrivateKey: "${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + }, + shouldError: true, + errorContains: "safe-outputs", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + workflowData := &WorkflowData{ + Permissions: tt.permissions, + ParsedTools: tt.parsedTools, + SafeOutputs: tt.safeOutputs, + ActivationGitHubApp: tt.activationApp, + } + + err := validateGitHubAppOnlyPermissions(workflowData) + + if tt.shouldError { + if err == nil { + t.Errorf("Expected error but got none") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain %q, but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + }) + } +} + +func TestIsGitHubAppOnlyScope(t *testing.T) { + tests := []struct { + scope PermissionScope + expected bool + }{ + // GitHub Actions scopes - should NOT be GitHub App-only + {PermissionActions, false}, + {PermissionChecks, false}, + {PermissionContents, false}, + {PermissionDeployments, false}, + {PermissionIssues, false}, + {PermissionPackages, false}, + {PermissionPages, false}, + {PermissionPullRequests, false}, + {PermissionSecurityEvents, false}, + {PermissionStatuses, false}, + {PermissionDiscussions, false}, + // organization-projects is a GitHub App-only scope (not in GitHub Actions GITHUB_TOKEN) + {PermissionOrganizationProj, true}, + // GitHub App-only scopes - should return true + {PermissionAdministration, true}, + {PermissionMembers, true}, + {PermissionOrganizationAdministration, true}, + {PermissionEnvironments, true}, + {PermissionGitSigning, true}, + {PermissionTeamDiscussions, true}, + {PermissionVulnerabilityAlerts, true}, + {PermissionWorkflows, true}, + {PermissionRepositoryHooks, true}, + {PermissionOrganizationHooks, true}, + {PermissionOrganizationMembers, true}, + {PermissionOrganizationPackages, true}, + {PermissionOrganizationSelfHostedRunners, true}, + {PermissionSingleFile, true}, + {PermissionCodespaces, true}, + {PermissionEmailAddresses, true}, + } + + for _, tt := range tests { + t.Run(string(tt.scope), func(t *testing.T) { + result := IsGitHubAppOnlyScope(tt.scope) + if result != tt.expected { + t.Errorf("IsGitHubAppOnlyScope(%q) = %v, want %v", tt.scope, result, tt.expected) + } + }) + } +} + +func TestGetAllGitHubAppOnlyScopes(t *testing.T) { + scopes := GetAllGitHubAppOnlyScopes() + if len(scopes) == 0 { + t.Error("GetAllGitHubAppOnlyScopes should return at least one scope") + } + + // Verify some key scopes are included + keyScopes := []PermissionScope{ + PermissionAdministration, + PermissionMembers, + PermissionOrganizationAdministration, + PermissionEnvironments, + PermissionWorkflows, + PermissionVulnerabilityAlerts, + PermissionOrganizationPackages, + } + + scopeSet := make(map[PermissionScope]bool) + for _, s := range scopes { + scopeSet[s] = true + } + + for _, expected := range keyScopes { + if !scopeSet[expected] { + t.Errorf("Expected scope %q to be in GetAllGitHubAppOnlyScopes()", expected) + } + } + + // Verify that GitHub Actions scopes are NOT included + actionScopes := []PermissionScope{ + PermissionContents, + PermissionIssues, + PermissionPullRequests, + PermissionChecks, + PermissionIdToken, + } + for _, notExpected := range actionScopes { + if scopeSet[notExpected] { + t.Errorf("Expected scope %q to NOT be in GetAllGitHubAppOnlyScopes()", notExpected) + } + } +} + +func TestGitHubAppOnlyPermissionsRenderToYAML(t *testing.T) { + tests := []struct { + name string + permissions *Permissions + expectEmpty bool + shouldContain []string + shouldSkip []string + }{ + { + name: "members permission is skipped in GitHub Actions YAML", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionMembers, PermissionRead) + return p + }(), + shouldSkip: []string{"members: read"}, + }, + { + name: "administration permission is skipped in GitHub Actions YAML", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionAdministration, PermissionRead) + return p + }(), + shouldSkip: []string{"administration: read"}, + }, + { + name: "only App-only scopes returns empty string (not bare 'permissions:')", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionMembers, PermissionRead) + p.Set(PermissionAdministration, PermissionRead) + return p + }(), + expectEmpty: true, + shouldSkip: []string{"members: read", "administration: read", "permissions:"}, + }, + { + name: "contents permission IS included in GitHub Actions YAML", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionContents, PermissionRead) + return p + }(), + shouldContain: []string{"contents: read"}, + }, + { + name: "mixed permissions - only Actions scopes rendered", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionContents, PermissionRead) + p.Set(PermissionIssues, PermissionRead) + p.Set(PermissionMembers, PermissionRead) + p.Set(PermissionAdministration, PermissionRead) + return p + }(), + shouldContain: []string{"contents: read", "issues: read"}, + shouldSkip: []string{"members: read", "administration: read"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + yaml := tt.permissions.RenderToYAML() + + if tt.expectEmpty && yaml != "" { + t.Errorf("Expected empty YAML output but got:\n%s", yaml) + } + + for _, expected := range tt.shouldContain { + if !strings.Contains(yaml, expected) { + t.Errorf("Expected YAML to contain %q, but got:\n%s", expected, yaml) + } + } + + for _, notExpected := range tt.shouldSkip { + if strings.Contains(yaml, notExpected) { + t.Errorf("Expected YAML to NOT contain %q, but got:\n%s", notExpected, yaml) + } + } + }) + } +} + +func TestConvertPermissionsToAppTokenFields_GitHubAppOnly(t *testing.T) { + tests := []struct { + name string + permissions *Permissions + expectedFields map[string]string + absentFields []string + }{ + { + name: "members permission maps to permission-members", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionMembers, PermissionRead) + return p + }(), + expectedFields: map[string]string{ + "permission-members": "read", + }, + }, + { + name: "administration permission maps to permission-administration", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionAdministration, PermissionRead) + return p + }(), + expectedFields: map[string]string{ + "permission-administration": "read", + }, + }, + { + name: "workflows permission maps to permission-workflows", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionWorkflows, PermissionWrite) + return p + }(), + expectedFields: map[string]string{ + "permission-workflows": "write", + }, + }, + { + name: "vulnerability-alerts maps correctly", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionVulnerabilityAlerts, PermissionRead) + return p + }(), + expectedFields: map[string]string{ + "permission-vulnerability-alerts": "read", + }, + }, + { + name: "models permission is NOT mapped (no GitHub App equivalent)", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionModels, PermissionRead) + return p + }(), + absentFields: []string{"permission-models"}, + }, + { + name: "id-token permission is NOT mapped (not applicable to GitHub Apps)", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionIdToken, PermissionWrite) + return p + }(), + absentFields: []string{"permission-id-token"}, + }, + { + name: "organization-packages permission maps correctly", + permissions: func() *Permissions { + p := NewPermissions() + p.Set(PermissionOrganizationPackages, PermissionRead) + return p + }(), + expectedFields: map[string]string{ + "permission-organization-packages": "read", + }, + }, + { + name: "read-all shorthand does NOT produce App-only permission fields", + permissions: NewPermissionsReadAll(), + // App-only scopes must not appear when using shorthand + absentFields: []string{ + "permission-members", + "permission-administration", + "permission-workflows", + "permission-organization-projects", + }, + }, + { + name: "write-all shorthand does NOT produce App-only permission fields", + permissions: NewPermissionsWriteAll(), + absentFields: []string{ + "permission-members", + "permission-administration", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fields := convertPermissionsToAppTokenFields(tt.permissions) + + for key, expectedValue := range tt.expectedFields { + if actualValue, exists := fields[key]; !exists { + t.Errorf("Expected field %q to be present, but it was not. Got fields: %v", key, fields) + } else if actualValue != expectedValue { + t.Errorf("Expected field %q = %q, got %q", key, expectedValue, actualValue) + } + } + + for _, absentKey := range tt.absentFields { + if _, exists := fields[absentKey]; exists { + t.Errorf("Expected field %q to be absent, but it was present with value %q", absentKey, fields[absentKey]) + } + } + }) + } +} diff --git a/pkg/workflow/permissions.go b/pkg/workflow/permissions.go index 78dc40674f5..baa36c363d5 100644 --- a/pkg/workflow/permissions.go +++ b/pkg/workflow/permissions.go @@ -1,6 +1,8 @@ package workflow import ( + "slices" + "github.com/github/gh-aw/pkg/logger" ) @@ -9,6 +11,7 @@ var permissionsLog = logger.New("workflow:permissions") // convertStringToPermissionScope converts a string key to a PermissionScope func convertStringToPermissionScope(key string) PermissionScope { switch key { + // GitHub Actions permission scopes (supported by GITHUB_TOKEN) case "actions": return PermissionActions case "attestations": @@ -37,14 +40,77 @@ func convertStringToPermissionScope(key string) PermissionScope { return PermissionPullRequests case "repository-projects": return PermissionRepositoryProj - case "organization-projects": - return PermissionOrganizationProj case "security-events": return PermissionSecurityEvents case "statuses": return PermissionStatuses case "copilot-requests": return PermissionCopilotRequests + // GitHub App-only permission scopes (not supported by GITHUB_TOKEN, require a GitHub App) + // organization-projects is included here because it is a GitHub App-only scope + // (it is excluded from GetAllPermissionScopes() and skipped in YAML rendering). + case "organization-projects": + return PermissionOrganizationProj + case "administration": + return PermissionAdministration + case "members": + return PermissionMembers + case "organization-administration": + return PermissionOrganizationAdministration + case "environments": + return PermissionEnvironments + case "git-signing": + return PermissionGitSigning + case "team-discussions": + return PermissionTeamDiscussions + case "vulnerability-alerts": + return PermissionVulnerabilityAlerts + case "workflows": + return PermissionWorkflows + case "repository-hooks": + return PermissionRepositoryHooks + case "organization-hooks": + return PermissionOrganizationHooks + case "organization-members": + return PermissionOrganizationMembers + case "organization-packages": + return PermissionOrganizationPackages + case "organization-self-hosted-runners": + return PermissionOrganizationSelfHostedRunners + case "single-file": + return PermissionSingleFile + case "codespaces": + return PermissionCodespaces + case "email-addresses": + return PermissionEmailAddresses + case "repository-custom-properties": + return PermissionRepositoryCustomProperties + case "organization-custom-org-roles": + return PermissionOrganizationCustomOrgRoles + case "organization-custom-properties": + return PermissionOrganizationCustomProperties + case "organization-custom-repository-roles": + return PermissionOrganizationCustomRepositoryRoles + case "organization-announcement-banners": + return PermissionOrganizationAnnouncementBanners + case "organization-events": + return PermissionOrganizationEvents + case "organization-plan": + return PermissionOrganizationPlan + case "organization-user-blocking": + return PermissionOrganizationUserBlocking + case "organization-personal-access-token-requests": + return PermissionOrganizationPersonalAccessTokenReqs + case "organization-personal-access-tokens": + return PermissionOrganizationPersonalAccessTokens + case "organization-copilot": + return PermissionOrganizationCopilot + case "organization-codespaces": + return PermissionOrganizationCodespaces + case "codespaces-lifecycle-admin": + return PermissionCodespacesLifecycleAdmin + case "codespaces-metadata": + return PermissionCodespacesMetadata case "all": // "all" is a meta-key handled at the parser level; it is not a real scope return "" @@ -67,6 +133,7 @@ const ( type PermissionScope string const ( + // GitHub Actions permission scopes (supported by GITHUB_TOKEN) PermissionActions PermissionScope = "actions" PermissionAttestations PermissionScope = "attestations" PermissionChecks PermissionScope = "checks" @@ -87,9 +154,52 @@ const ( // PermissionCopilotRequests is a GitHub Actions permission scope used with the copilot-requests feature. // It enables use of the GitHub Actions token as the Copilot authentication token. PermissionCopilotRequests PermissionScope = "copilot-requests" + + // GitHub App-only permission scopes (not supported by GITHUB_TOKEN, require a GitHub App token). + // When any of these are specified in the workflow permissions, a GitHub App must be configured. + // These permissions are skipped when rendering GitHub Actions workflow YAML, but are passed + // as permission-* inputs when minting GitHub App installation access tokens. + + // Repository-level GitHub App permissions + PermissionAdministration PermissionScope = "administration" + PermissionEnvironments PermissionScope = "environments" + PermissionGitSigning PermissionScope = "git-signing" + PermissionVulnerabilityAlerts PermissionScope = "vulnerability-alerts" + PermissionWorkflows PermissionScope = "workflows" + PermissionRepositoryHooks PermissionScope = "repository-hooks" + PermissionSingleFile PermissionScope = "single-file" + PermissionCodespaces PermissionScope = "codespaces" + PermissionRepositoryCustomProperties PermissionScope = "repository-custom-properties" + + // Organization-level GitHub App permissions + PermissionMembers PermissionScope = "members" + PermissionOrganizationAdministration PermissionScope = "organization-administration" + PermissionTeamDiscussions PermissionScope = "team-discussions" + PermissionOrganizationHooks PermissionScope = "organization-hooks" + PermissionOrganizationMembers PermissionScope = "organization-members" + PermissionOrganizationPackages PermissionScope = "organization-packages" + PermissionOrganizationSelfHostedRunners PermissionScope = "organization-self-hosted-runners" + PermissionOrganizationCustomOrgRoles PermissionScope = "organization-custom-org-roles" + PermissionOrganizationCustomProperties PermissionScope = "organization-custom-properties" + PermissionOrganizationCustomRepositoryRoles PermissionScope = "organization-custom-repository-roles" + PermissionOrganizationAnnouncementBanners PermissionScope = "organization-announcement-banners" + PermissionOrganizationEvents PermissionScope = "organization-events" + PermissionOrganizationPlan PermissionScope = "organization-plan" + PermissionOrganizationUserBlocking PermissionScope = "organization-user-blocking" + PermissionOrganizationPersonalAccessTokenReqs PermissionScope = "organization-personal-access-token-requests" + PermissionOrganizationPersonalAccessTokens PermissionScope = "organization-personal-access-tokens" + PermissionOrganizationCopilot PermissionScope = "organization-copilot" + PermissionOrganizationCodespaces PermissionScope = "organization-codespaces" + + // User-level GitHub App permissions + PermissionEmailAddresses PermissionScope = "email-addresses" + PermissionCodespacesLifecycleAdmin PermissionScope = "codespaces-lifecycle-admin" + PermissionCodespacesMetadata PermissionScope = "codespaces-metadata" ) -// GetAllPermissionScopes returns all available permission scopes +// GetAllPermissionScopes returns all GitHub Actions permission scopes (supported by GITHUB_TOKEN). +// These are the scopes that can be set on the workflow's GITHUB_TOKEN. +// For GitHub App-only scopes, see GetAllGitHubAppOnlyScopes. func GetAllPermissionScopes() []PermissionScope { return []PermissionScope{ PermissionActions, @@ -106,12 +216,59 @@ func GetAllPermissionScopes() []PermissionScope { PermissionPages, PermissionPullRequests, PermissionRepositoryProj, - PermissionOrganizationProj, PermissionSecurityEvents, PermissionStatuses, } } +// GetAllGitHubAppOnlyScopes returns all GitHub App-only permission scopes. +// These scopes are not supported by GITHUB_TOKEN and require a GitHub App installation token. +// When any of these scopes are used in a workflow, a GitHub App must be configured. +func GetAllGitHubAppOnlyScopes() []PermissionScope { + return []PermissionScope{ + // Repository-level GitHub App permissions + PermissionAdministration, + PermissionEnvironments, + PermissionGitSigning, + PermissionVulnerabilityAlerts, + PermissionWorkflows, + PermissionRepositoryHooks, + PermissionSingleFile, + PermissionCodespaces, + PermissionRepositoryCustomProperties, + // Organization-level GitHub App permissions + PermissionOrganizationProj, + PermissionMembers, + PermissionOrganizationAdministration, + PermissionTeamDiscussions, + PermissionOrganizationHooks, + PermissionOrganizationMembers, + PermissionOrganizationPackages, + PermissionOrganizationSelfHostedRunners, + PermissionOrganizationCustomOrgRoles, + PermissionOrganizationCustomProperties, + PermissionOrganizationCustomRepositoryRoles, + PermissionOrganizationAnnouncementBanners, + PermissionOrganizationEvents, + PermissionOrganizationPlan, + PermissionOrganizationUserBlocking, + PermissionOrganizationPersonalAccessTokenReqs, + PermissionOrganizationPersonalAccessTokens, + PermissionOrganizationCopilot, + PermissionOrganizationCodespaces, + // User-level GitHub App permissions + PermissionEmailAddresses, + PermissionCodespacesLifecycleAdmin, + PermissionCodespacesMetadata, + } +} + +// IsGitHubAppOnlyScope returns true if the scope is a GitHub App-only permission +// (not supported by GITHUB_TOKEN). These scopes require a GitHub App to exercise. +func IsGitHubAppOnlyScope(scope PermissionScope) bool { + return slices.Contains(GetAllGitHubAppOnlyScopes(), scope) +} + // Permissions represents GitHub Actions permissions // It can be a shorthand (read-all, write-all, read, write, none) or a map of scopes to levels // It can also have an "all" permission that expands to all scopes diff --git a/pkg/workflow/permissions_operations.go b/pkg/workflow/permissions_operations.go index dfc54df035b..3e043b498a8 100644 --- a/pkg/workflow/permissions_operations.go +++ b/pkg/workflow/permissions_operations.go @@ -47,6 +47,19 @@ func (p *Permissions) Set(scope PermissionScope, level PermissionLevel) { p.permissions[scope] = level } +// GetExplicit returns the permission level only if the scope was explicitly declared in the +// permissions map. Unlike Get, it never returns a level derived from shorthand (read-all / +// write-all) or "all: read" defaults. Use this when you need to know what the user explicitly +// specified — for example, when deciding which GitHub App-only scopes to forward to +// actions/create-github-app-token, or when validating that App-only scopes are present. +func (p *Permissions) GetExplicit(scope PermissionScope) (PermissionLevel, bool) { + if p == nil { + return "", false + } + level, exists := p.permissions[scope] + return level, exists +} + // Get gets the permission level for a specific scope func (p *Permissions) Get(scope PermissionScope) (PermissionLevel, bool) { if p.shorthand != "" { @@ -253,12 +266,15 @@ func (p *Permissions) RenderToYAML() string { var lines []string lines = append(lines, "permissions:") + hasRenderable := false for _, scopeStr := range scopes { scope := PermissionScope(scopeStr) level := allPerms[scope] - // Skip organization-projects - it's only valid for GitHub App tokens, not workflow permissions - if scope == PermissionOrganizationProj { + // Skip GitHub App-only permissions - they are not valid GitHub Actions workflow permissions + // and cannot be set on the GITHUB_TOKEN. They are handled separately when minting + // GitHub App installation access tokens. + if IsGitHubAppOnlyScope(scope) { continue } @@ -267,6 +283,7 @@ func (p *Permissions) RenderToYAML() string { continue } + hasRenderable = true // Add 2 spaces for proper indentation under permissions: // When rendered in a job, the job renderer adds 4 spaces to the first line only, // so we need to pre-indent continuation lines with 4 additional spaces @@ -274,5 +291,13 @@ func (p *Permissions) RenderToYAML() string { lines = append(lines, fmt.Sprintf(" %s: %s", scope, level)) } + // If everything was skipped (all App-only or metadata), return as if empty + if !hasRenderable { + if p.explicitEmpty { + return "permissions: {}" + } + return "" + } + return strings.Join(lines, "\n") } diff --git a/pkg/workflow/permissions_operations_test.go b/pkg/workflow/permissions_operations_test.go index 9874d1bad20..05518baac88 100644 --- a/pkg/workflow/permissions_operations_test.go +++ b/pkg/workflow/permissions_operations_test.go @@ -350,23 +350,23 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionContents: PermissionWrite}), merge: NewPermissionsReadAll(), want: map[PermissionScope]PermissionLevel{ - PermissionContents: PermissionWrite, // preserved - PermissionActions: PermissionRead, // added - PermissionAttestations: PermissionRead, - PermissionChecks: PermissionRead, - PermissionDeployments: PermissionRead, - PermissionDiscussions: PermissionRead, - PermissionIssues: PermissionRead, - PermissionMetadata: PermissionRead, - PermissionPackages: PermissionRead, - PermissionPages: PermissionRead, - PermissionPullRequests: PermissionRead, - PermissionRepositoryProj: PermissionRead, - PermissionOrganizationProj: PermissionRead, - PermissionSecurityEvents: PermissionRead, - PermissionStatuses: PermissionRead, - PermissionModels: PermissionRead, + PermissionContents: PermissionWrite, // preserved + PermissionActions: PermissionRead, // added + PermissionAttestations: PermissionRead, + PermissionChecks: PermissionRead, + PermissionDeployments: PermissionRead, + PermissionDiscussions: PermissionRead, + PermissionIssues: PermissionRead, + PermissionMetadata: PermissionRead, + PermissionPackages: PermissionRead, + PermissionPages: PermissionRead, + PermissionPullRequests: PermissionRead, + PermissionRepositoryProj: PermissionRead, + PermissionSecurityEvents: PermissionRead, + PermissionStatuses: PermissionRead, + PermissionModels: PermissionRead, // Note: id-token is NOT included because it doesn't support read level + // Note: organization-projects is NOT included because it's a GitHub App-only scope }, }, { @@ -374,23 +374,23 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionContents: PermissionRead}), merge: NewPermissionsWriteAll(), want: map[PermissionScope]PermissionLevel{ - PermissionContents: PermissionRead, // preserved (not overwritten) - PermissionActions: PermissionWrite, - PermissionAttestations: PermissionWrite, - PermissionChecks: PermissionWrite, - PermissionDeployments: PermissionWrite, - PermissionDiscussions: PermissionWrite, - PermissionIdToken: PermissionWrite, // id-token supports write - PermissionIssues: PermissionWrite, - PermissionMetadata: PermissionWrite, - PermissionPackages: PermissionWrite, - PermissionPages: PermissionWrite, - PermissionPullRequests: PermissionWrite, - PermissionRepositoryProj: PermissionWrite, - PermissionOrganizationProj: PermissionWrite, - PermissionSecurityEvents: PermissionWrite, - PermissionStatuses: PermissionWrite, - PermissionModels: PermissionWrite, + PermissionContents: PermissionRead, // preserved (not overwritten) + PermissionActions: PermissionWrite, + PermissionAttestations: PermissionWrite, + PermissionChecks: PermissionWrite, + PermissionDeployments: PermissionWrite, + PermissionDiscussions: PermissionWrite, + PermissionIdToken: PermissionWrite, // id-token supports write + PermissionIssues: PermissionWrite, + PermissionMetadata: PermissionWrite, + PermissionPackages: PermissionWrite, + PermissionPages: PermissionWrite, + PermissionPullRequests: PermissionWrite, + PermissionRepositoryProj: PermissionWrite, + PermissionSecurityEvents: PermissionWrite, + PermissionStatuses: PermissionWrite, + PermissionModels: PermissionWrite, + // Note: organization-projects is NOT included because it's a GitHub App-only scope }, }, { @@ -398,23 +398,23 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionContents: PermissionWrite}), merge: NewPermissionsReadAll(), want: map[PermissionScope]PermissionLevel{ - PermissionContents: PermissionWrite, - PermissionActions: PermissionRead, - PermissionAttestations: PermissionRead, - PermissionChecks: PermissionRead, - PermissionDeployments: PermissionRead, - PermissionDiscussions: PermissionRead, - PermissionIssues: PermissionRead, - PermissionMetadata: PermissionRead, - PermissionPackages: PermissionRead, - PermissionPages: PermissionRead, - PermissionPullRequests: PermissionRead, - PermissionRepositoryProj: PermissionRead, - PermissionOrganizationProj: PermissionRead, - PermissionSecurityEvents: PermissionRead, - PermissionStatuses: PermissionRead, - PermissionModels: PermissionRead, + PermissionContents: PermissionWrite, + PermissionActions: PermissionRead, + PermissionAttestations: PermissionRead, + PermissionChecks: PermissionRead, + PermissionDeployments: PermissionRead, + PermissionDiscussions: PermissionRead, + PermissionIssues: PermissionRead, + PermissionMetadata: PermissionRead, + PermissionPackages: PermissionRead, + PermissionPages: PermissionRead, + PermissionPullRequests: PermissionRead, + PermissionRepositoryProj: PermissionRead, + PermissionSecurityEvents: PermissionRead, + PermissionStatuses: PermissionRead, + PermissionModels: PermissionRead, // Note: id-token is NOT included because it doesn't support read level + // Note: organization-projects is NOT included because it's a GitHub App-only scope }, }, { @@ -422,23 +422,22 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionIssues: PermissionRead}), merge: NewPermissionsWriteAll(), want: map[PermissionScope]PermissionLevel{ - PermissionIssues: PermissionRead, - PermissionActions: PermissionWrite, - PermissionAttestations: PermissionWrite, - PermissionChecks: PermissionWrite, - PermissionContents: PermissionWrite, - PermissionDeployments: PermissionWrite, - PermissionDiscussions: PermissionWrite, - PermissionIdToken: PermissionWrite, // id-token supports write - PermissionMetadata: PermissionWrite, - PermissionPackages: PermissionWrite, - PermissionPages: PermissionWrite, - PermissionPullRequests: PermissionWrite, - PermissionRepositoryProj: PermissionWrite, - PermissionOrganizationProj: PermissionWrite, - PermissionSecurityEvents: PermissionWrite, - PermissionStatuses: PermissionWrite, - PermissionModels: PermissionWrite, + PermissionIssues: PermissionRead, + PermissionActions: PermissionWrite, + PermissionAttestations: PermissionWrite, + PermissionChecks: PermissionWrite, + PermissionContents: PermissionWrite, + PermissionDeployments: PermissionWrite, + PermissionDiscussions: PermissionWrite, + PermissionIdToken: PermissionWrite, // id-token supports write + PermissionMetadata: PermissionWrite, + PermissionPackages: PermissionWrite, + PermissionPages: PermissionWrite, + PermissionPullRequests: PermissionWrite, + PermissionRepositoryProj: PermissionWrite, + PermissionSecurityEvents: PermissionWrite, + PermissionStatuses: PermissionWrite, + PermissionModels: PermissionWrite, }, }, { diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index 828b32d1ff9..92b7bbddf7f 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -197,17 +197,23 @@ func (c *Compiler) buildGitHubAppTokenMintStep(app *GitHubAppConfig, permissions // convertPermissionsToAppTokenFields converts job Permissions to permission-* action inputs // This follows GitHub's recommendation for explicit permission control -// Note: This only includes permissions that are valid for GitHub App tokens. -// Some GitHub Actions permissions (like 'discussions', 'models') don't have -// corresponding GitHub App permissions and are skipped. +// Note: This maps all permissions (both GitHub Actions and GitHub App-only) to their +// corresponding permission-* fields in actions/create-github-app-token. +// Some GitHub Actions permissions (like 'models', 'id-token', 'attestations', 'copilot-requests') +// don't have corresponding GitHub App permissions and are skipped. +// +// For GitHub Actions permissions (actions, checks, contents, …) we use Get() so that shorthand +// permissions like "read-all" are correctly expanded. +// For GitHub App-only permissions (administration, members, organization-secrets, …) we use +// GetExplicit() so that only scopes the user actually declared are forwarded — a "read-all" +// shorthand must never accidentally grant broad GitHub App-only permissions. func convertPermissionsToAppTokenFields(permissions *Permissions) map[string]string { fields := make(map[string]string) // Map GitHub Actions permissions to GitHub App permissions - // Only include permissions that exist in the actions/create-github-app-token action // See: https://github.com/actions/create-github-app-token#permissions - // Repository permissions that map directly + // GitHub Actions permissions that also exist in GitHub App if level, ok := permissions.Get(PermissionActions); ok { fields["permission-actions"] = string(level) } @@ -238,18 +244,117 @@ func convertPermissionsToAppTokenFields(permissions *Permissions) map[string]str if level, ok := permissions.Get(PermissionStatuses); ok { fields["permission-statuses"] = string(level) } - if level, ok := permissions.Get(PermissionOrganizationProj); ok { - fields["permission-organization-projects"] = string(level) - } if level, ok := permissions.Get(PermissionDiscussions); ok { fields["permission-discussions"] = string(level) } - // Note: The following GitHub Actions permissions do NOT have GitHub App equivalents: - // - models (no GitHub App permission for this) - // - id-token (not applicable to GitHub Apps) - // - attestations (no GitHub App permission for this) - // - repository-projects (removed - classic projects are sunset; use organization-projects for Projects v2 via PAT/GitHub App) + // GitHub App-only permissions (not available in GitHub Actions GITHUB_TOKEN). + // Use GetExplicit() so that shorthand permissions like "read-all" do not accidentally + // expand into broad GitHub App-only grants that the user never declared. + // Repository-level + if level, ok := permissions.GetExplicit(PermissionAdministration); ok { + fields["permission-administration"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionEnvironments); ok { + fields["permission-environments"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionGitSigning); ok { + fields["permission-git-signing"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionVulnerabilityAlerts); ok { + fields["permission-vulnerability-alerts"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionWorkflows); ok { + fields["permission-workflows"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionRepositoryHooks); ok { + fields["permission-repository-hooks"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionSingleFile); ok { + fields["permission-single-file"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionCodespaces); ok { + fields["permission-codespaces"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionRepositoryCustomProperties); ok { + fields["permission-repository-custom-properties"] = string(level) + } + // Organization-level + if level, ok := permissions.GetExplicit(PermissionOrganizationProj); ok { + fields["permission-organization-projects"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionMembers); ok { + fields["permission-members"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationAdministration); ok { + fields["permission-organization-administration"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionTeamDiscussions); ok { + fields["permission-team-discussions"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationHooks); ok { + fields["permission-organization-hooks"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationMembers); ok { + fields["permission-organization-members"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationPackages); ok { + fields["permission-organization-packages"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationSelfHostedRunners); ok { + fields["permission-organization-self-hosted-runners"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationCustomOrgRoles); ok { + fields["permission-organization-custom-org-roles"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationCustomProperties); ok { + fields["permission-organization-custom-properties"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationCustomRepositoryRoles); ok { + fields["permission-organization-custom-repository-roles"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationAnnouncementBanners); ok { + fields["permission-organization-announcement-banners"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationEvents); ok { + fields["permission-organization-events"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationPlan); ok { + fields["permission-organization-plan"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationUserBlocking); ok { + fields["permission-organization-user-blocking"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationPersonalAccessTokenReqs); ok { + fields["permission-organization-personal-access-token-requests"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationPersonalAccessTokens); ok { + fields["permission-organization-personal-access-tokens"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationCopilot); ok { + fields["permission-organization-copilot"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionOrganizationCodespaces); ok { + fields["permission-organization-codespaces"] = string(level) + } + // User-level + if level, ok := permissions.GetExplicit(PermissionEmailAddresses); ok { + fields["permission-email-addresses"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionCodespacesLifecycleAdmin); ok { + fields["permission-codespaces-lifecycle-admin"] = string(level) + } + if level, ok := permissions.GetExplicit(PermissionCodespacesMetadata); ok { + fields["permission-codespaces-metadata"] = string(level) + } + + // Note: The following GitHub Actions permissions do NOT have GitHub App equivalents + // and are therefore not mapped to permission-* fields: + // - models: no GitHub App permission for AI model access + // - id-token: not applicable to GitHub Apps (OIDC-specific) + // - attestations: no GitHub App permission for this + // - copilot-requests: GitHub Actions-specific Copilot authentication token + // - metadata: GitHub App metadata permission is automatically included (read-only) return fields } diff --git a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/basic-copilot.golden b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/basic-copilot.golden index 9941798c1f8..b94e8647f84 100644 --- a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/basic-copilot.golden +++ b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/basic-copilot.golden @@ -54,6 +54,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden index 51a6929101f..319f7225f14 100644 --- a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden +++ b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden @@ -68,6 +68,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/with-imports.golden b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/with-imports.golden index 3bc88456e4d..5ba4435f829 100644 --- a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/with-imports.golden +++ b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/with-imports.golden @@ -54,6 +54,8 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret diff --git a/pkg/workflow/top_level_github_app_import_test.go b/pkg/workflow/top_level_github_app_import_test.go new file mode 100644 index 00000000000..c653ad1cd92 --- /dev/null +++ b/pkg/workflow/top_level_github_app_import_test.go @@ -0,0 +1,837 @@ +//go:build !integration + +package workflow + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestTopLevelGitHubAppImport tests that a top-level github-app can be imported +// from a shared agent workflow and propagated as a fallback for all nested operations. +func TestTopLevelGitHubAppImport(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + // Create a temporary directory simulating .github/workflows layout + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + // Shared workflow that declares a top-level github-app + sharedWorkflow := `--- +github-app: + app-id: ${{ vars.SHARED_APP_ID }} + private-key: ${{ secrets.SHARED_APP_SECRET }} +safe-outputs: + create-issue: +--- + +# Shared GitHub App Configuration + +This shared workflow provides a top-level github-app for all operations. +` + require.NoError(t, os.WriteFile(filepath.Join(workflowsDir, "shared-app.md"), []byte(sharedWorkflow), 0644)) + + // Main workflow that imports the shared workflow but does NOT set its own github-app + mainWorkflow := `--- +on: issues +permissions: + contents: read +imports: + - ./shared-app.md +safe-outputs: + create-issue: +--- + +# Main Workflow + +This workflow imports the top-level github-app from the shared workflow. +` + mainFile := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mainFile, []byte(mainWorkflow), 0644)) + + // Change to workflows directory so relative imports resolve correctly + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + // The top-level github-app from the shared workflow should be resolved + require.NotNil(t, data.TopLevelGitHubApp, "TopLevelGitHubApp should be populated from import") + assert.Equal(t, "${{ vars.SHARED_APP_ID }}", data.TopLevelGitHubApp.AppID, + "TopLevelGitHubApp.AppID should come from the shared workflow") + assert.Equal(t, "${{ secrets.SHARED_APP_SECRET }}", data.TopLevelGitHubApp.PrivateKey, + "TopLevelGitHubApp.PrivateKey should come from the shared workflow") + + // The fallback should also propagate to safe-outputs (since safe-outputs has no explicit github-app) + require.NotNil(t, data.SafeOutputs, "SafeOutputs should be populated") + require.NotNil(t, data.SafeOutputs.GitHubApp, + "SafeOutputs.GitHubApp should be populated from the imported top-level github-app") + assert.Equal(t, "${{ vars.SHARED_APP_ID }}", data.SafeOutputs.GitHubApp.AppID, + "SafeOutputs should use the imported top-level github-app") +} + +// TestTopLevelGitHubAppImportOverride tests that the current workflow's own top-level +// github-app takes precedence over one imported from a shared workflow. +func TestTopLevelGitHubAppImportOverride(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + // Shared workflow with a top-level github-app + sharedWorkflow := `--- +github-app: + app-id: ${{ vars.SHARED_APP_ID }} + private-key: ${{ secrets.SHARED_APP_SECRET }} +safe-outputs: + create-issue: +--- + +# Shared GitHub App Configuration +` + require.NoError(t, os.WriteFile(filepath.Join(workflowsDir, "shared-app.md"), []byte(sharedWorkflow), 0644)) + + // Main workflow that has its OWN top-level github-app (should win) + mainWorkflow := `--- +on: issues +permissions: + contents: read +imports: + - ./shared-app.md +github-app: + app-id: ${{ vars.LOCAL_APP_ID }} + private-key: ${{ secrets.LOCAL_APP_SECRET }} +safe-outputs: + create-issue: +--- + +# Main Workflow + +This workflow's own top-level github-app takes precedence over the imported one. +` + mainFile := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mainFile, []byte(mainWorkflow), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + // The current workflow's top-level github-app should win + require.NotNil(t, data.TopLevelGitHubApp, "TopLevelGitHubApp should be populated") + assert.Equal(t, "${{ vars.LOCAL_APP_ID }}", data.TopLevelGitHubApp.AppID, + "Current workflow's github-app should take precedence over the imported one") + assert.Equal(t, "${{ secrets.LOCAL_APP_SECRET }}", data.TopLevelGitHubApp.PrivateKey, + "Current workflow's github-app should take precedence over the imported one") +} + +// TestTopLevelGitHubAppToolsGitHubTokenSkip tests that the fallback is NOT applied +// to tools.github when a custom github-token is already configured for the MCP server. +func TestTopLevelGitHubAppToolsGitHubTokenSkip(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + // Use ParseWorkflowFile directly with inline frontmatter + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + // Workflow with top-level github-app but tools.github uses an explicit github-token + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +tools: + github: + mode: remote + toolsets: [default] + github-token: ${{ secrets.CUSTOM_PAT }} +engine: copilot +--- + +# Workflow With Explicit GitHub Token for MCP + +When tools.github.github-token is set, the top-level github-app fallback should NOT override it. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + // The top-level github-app should be resolved at the top level + require.NotNil(t, data.TopLevelGitHubApp, "TopLevelGitHubApp should be populated") + + // But it must NOT be injected into tools.github because github-token takes precedence + require.NotNil(t, data.ParsedTools, "ParsedTools should be populated") + require.NotNil(t, data.ParsedTools.GitHub, "ParsedTools.GitHub should be populated") + assert.Equal(t, "${{ secrets.CUSTOM_PAT }}", data.ParsedTools.GitHub.GitHubToken, + "tools.github.github-token should be preserved") + assert.Nil(t, data.ParsedTools.GitHub.GitHubApp, + "tools.github.github-app should NOT be set when github-token is configured") +} + +// TestTopLevelGitHubAppToolsGitHubFalseSkip tests that the fallback is NOT applied +// to tools.github when github is explicitly disabled (github: false). +func TestTopLevelGitHubAppToolsGitHubFalseSkip(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + // Workflow with top-level github-app but tools.github explicitly disabled + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +tools: + github: false +engine: copilot +--- + +# Workflow With GitHub Tool Explicitly Disabled + +When tools.github is set to false, the top-level github-app fallback should NOT re-enable it. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + // The top-level github-app should be resolved at the top level + require.NotNil(t, data.TopLevelGitHubApp, "TopLevelGitHubApp should be populated") + + // tools.github should remain disabled — applyDefaultTools removes the key when false. + // After compilation, ParsedTools.GitHub should be nil (no GitHub MCP tool enabled). + assert.Nil(t, data.ParsedTools.GitHub, + "ParsedTools.GitHub should be nil when tools.github: false — fallback must not re-enable it") +} + +// workflow is propagated to the activation job (reactions/status comments). +func TestTopLevelGitHubAppImportActivation(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + sharedWorkflow := `--- +github-app: + app-id: ${{ vars.SHARED_APP_ID }} + private-key: ${{ secrets.SHARED_APP_SECRET }} +safe-outputs: + create-issue: +--- + +# Shared GitHub App Configuration +` + require.NoError(t, os.WriteFile(filepath.Join(workflowsDir, "shared-app.md"), []byte(sharedWorkflow), 0644)) + + // Workflow with a reaction trigger – no explicit on.github-app + mainWorkflow := `--- +on: + issues: + types: [opened] + reaction: eyes +permissions: + contents: read +imports: + - ./shared-app.md +safe-outputs: + create-issue: +--- + +# Main Workflow With Reaction +` + mainFile := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mainFile, []byte(mainWorkflow), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + // The imported top-level github-app should propagate to activation + require.NotNil(t, data.ActivationGitHubApp, + "ActivationGitHubApp should be populated from the imported top-level github-app") + assert.Equal(t, "${{ vars.SHARED_APP_ID }}", data.ActivationGitHubApp.AppID, + "Activation should use the imported top-level github-app") +} + +// TestTopLevelGitHubAppActivationOverride tests that an explicit on.github-app configuration +// takes precedence over the top-level github-app fallback. +func TestTopLevelGitHubAppActivationOverride(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: + issues: + types: [opened] + reaction: eyes + github-app: + app-id: ${{ vars.ACTIVATION_APP_ID }} + private-key: ${{ secrets.ACTIVATION_APP_KEY }} +permissions: + contents: read +github-app: + app-id: ${{ vars.TOP_LEVEL_APP_ID }} + private-key: ${{ secrets.TOP_LEVEL_APP_KEY }} +safe-outputs: + create-issue: +engine: copilot +--- + +# Workflow With Explicit on.github-app Override + +When on.github-app is explicitly set, it takes precedence over the top-level github-app. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.TopLevelGitHubApp, "TopLevelGitHubApp should be populated") + assert.Equal(t, "${{ vars.TOP_LEVEL_APP_ID }}", data.TopLevelGitHubApp.AppID, + "TopLevelGitHubApp should hold the top-level app") + + // on.github-app should win over the top-level fallback + require.NotNil(t, data.ActivationGitHubApp, "ActivationGitHubApp should be populated") + assert.Equal(t, "${{ vars.ACTIVATION_APP_ID }}", data.ActivationGitHubApp.AppID, + "ActivationGitHubApp should use the section-specific on.github-app, not the top-level fallback") +} + +// TestTopLevelGitHubAppActivationTokenSkip tests that the top-level github-app fallback +// is NOT applied to activation when on.github-token is explicitly configured, because +// at runtime app tokens take precedence over tokens and injecting the fallback would flip +// the user's intended auth precedence. +func TestTopLevelGitHubAppActivationTokenSkip(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: + issues: + types: [opened] + reaction: eyes + github-token: ${{ secrets.CUSTOM_ACTIVATION_TOKEN }} +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + create-issue: +engine: copilot +--- + +# Workflow With on.github-token — Fallback Must Not Apply + +When on.github-token is set, the top-level github-app must NOT be applied to activation. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.TopLevelGitHubApp, "TopLevelGitHubApp should be populated") + assert.Equal(t, "${{ secrets.CUSTOM_ACTIVATION_TOKEN }}", data.ActivationGitHubToken, + "ActivationGitHubToken should be preserved") + assert.Nil(t, data.ActivationGitHubApp, + "ActivationGitHubApp must be nil when on.github-token is set — fallback must not override it") +} + +// TestTopLevelGitHubAppSafeOutputsFallback tests that the top-level github-app is applied +// to safe-outputs when no section-specific github-app is configured. +func TestTopLevelGitHubAppSafeOutputsFallback(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + create-issue: +engine: copilot +--- + +# Top-level github-app fallback for safe-outputs. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.SafeOutputs, "SafeOutputs should be populated") + require.NotNil(t, data.SafeOutputs.GitHubApp, + "SafeOutputs.GitHubApp should be populated from top-level fallback") + assert.Equal(t, "${{ vars.APP_ID }}", data.SafeOutputs.GitHubApp.AppID, + "SafeOutputs should use the top-level github-app fallback") +} + +// TestTopLevelGitHubAppSafeOutputsOverride tests that a section-specific safe-outputs.github-app +// takes precedence over the top-level github-app fallback. +func TestTopLevelGitHubAppSafeOutputsOverride(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.TOP_LEVEL_APP_ID }} + private-key: ${{ secrets.TOP_LEVEL_APP_KEY }} +safe-outputs: + github-app: + app-id: ${{ vars.SAFE_OUTPUTS_APP_ID }} + private-key: ${{ secrets.SAFE_OUTPUTS_APP_KEY }} + create-issue: +engine: copilot +--- + +# Section-specific safe-outputs.github-app overrides top-level fallback. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.SafeOutputs, "SafeOutputs should be populated") + require.NotNil(t, data.SafeOutputs.GitHubApp, "SafeOutputs.GitHubApp should be populated") + assert.Equal(t, "${{ vars.SAFE_OUTPUTS_APP_ID }}", data.SafeOutputs.GitHubApp.AppID, + "SafeOutputs.GitHubApp should use the section-specific app, not the top-level fallback") +} + +// TestTopLevelGitHubAppSafeOutputsTokenSkip tests that the top-level github-app fallback +// is NOT applied to safe-outputs when safe-outputs.github-token is explicitly set. +func TestTopLevelGitHubAppSafeOutputsTokenSkip(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + github-token: ${{ secrets.CUSTOM_SAFE_OUTPUTS_TOKEN }} + create-issue: +engine: copilot +--- + +# safe-outputs.github-token is set — top-level github-app must not override it. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.SafeOutputs, "SafeOutputs should be populated") + assert.Equal(t, "${{ secrets.CUSTOM_SAFE_OUTPUTS_TOKEN }}", data.SafeOutputs.GitHubToken, + "SafeOutputs.GitHubToken should be preserved") + assert.Nil(t, data.SafeOutputs.GitHubApp, + "SafeOutputs.GitHubApp must be nil when safe-outputs.github-token is configured") +} + +// TestTopLevelGitHubAppCheckoutFallback tests that the top-level github-app is applied +// to a checkout entry that has no explicit auth (no github-app, no github-token). +func TestTopLevelGitHubAppCheckoutFallback(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +checkout: + repository: myorg/private-repo + path: private +safe-outputs: + create-issue: +engine: copilot +--- + +# Top-level github-app fallback for checkout. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.Len(t, data.CheckoutConfigs, 1, "Should have one checkout config") + require.NotNil(t, data.CheckoutConfigs[0].GitHubApp, + "CheckoutConfig.GitHubApp should be populated from top-level fallback") + assert.Equal(t, "${{ vars.APP_ID }}", data.CheckoutConfigs[0].GitHubApp.AppID, + "Checkout should use the top-level github-app fallback") +} + +// TestTopLevelGitHubAppCheckoutOverride tests that a section-specific checkout.github-app +// takes precedence over the top-level github-app fallback. +func TestTopLevelGitHubAppCheckoutOverride(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.TOP_LEVEL_APP_ID }} + private-key: ${{ secrets.TOP_LEVEL_APP_KEY }} +checkout: + - repository: myorg/private-repo + path: private + github-app: + app-id: ${{ vars.CHECKOUT_APP_ID }} + private-key: ${{ secrets.CHECKOUT_APP_KEY }} +safe-outputs: + create-issue: +engine: copilot +--- + +# checkout.github-app overrides the top-level github-app fallback. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.Len(t, data.CheckoutConfigs, 1, "Should have one checkout config") + require.NotNil(t, data.CheckoutConfigs[0].GitHubApp, "CheckoutConfig.GitHubApp should be populated") + assert.Equal(t, "${{ vars.CHECKOUT_APP_ID }}", data.CheckoutConfigs[0].GitHubApp.AppID, + "Checkout should use its own section-specific github-app, not the top-level fallback") +} + +// TestTopLevelGitHubAppCheckoutTokenSkip tests that the top-level github-app fallback is NOT +// applied to a checkout entry that has an explicit github-token set. +func TestTopLevelGitHubAppCheckoutTokenSkip(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +checkout: + - repository: myorg/private-repo + path: private + github-token: ${{ secrets.CHECKOUT_PAT }} +safe-outputs: + create-issue: +engine: copilot +--- + +# checkout.github-token is set — top-level github-app must not override it. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.Len(t, data.CheckoutConfigs, 1, "Should have one checkout config") + assert.Equal(t, "${{ secrets.CHECKOUT_PAT }}", data.CheckoutConfigs[0].GitHubToken, + "CheckoutConfig.GitHubToken should be preserved") + assert.Nil(t, data.CheckoutConfigs[0].GitHubApp, + "CheckoutConfig.GitHubApp must be nil when checkout.github-token is configured") +} + +// TestTopLevelGitHubAppToolsFallback tests that the top-level github-app is applied +// to tools.github when no section-specific auth is configured. +func TestTopLevelGitHubAppToolsFallback(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +tools: + github: + mode: remote + toolsets: [default] +safe-outputs: + create-issue: +engine: copilot +--- + +# Top-level github-app fallback for tools.github. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.ParsedTools, "ParsedTools should be populated") + require.NotNil(t, data.ParsedTools.GitHub, "ParsedTools.GitHub should be populated") + require.NotNil(t, data.ParsedTools.GitHub.GitHubApp, + "ParsedTools.GitHub.GitHubApp should be populated from top-level fallback") + assert.Equal(t, "${{ vars.APP_ID }}", data.ParsedTools.GitHub.GitHubApp.AppID, + "tools.github should use the top-level github-app fallback") +} + +// TestTopLevelGitHubAppToolsOverride tests that a section-specific tools.github.github-app +// takes precedence over the top-level github-app fallback. +func TestTopLevelGitHubAppToolsOverride(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.TOP_LEVEL_APP_ID }} + private-key: ${{ secrets.TOP_LEVEL_APP_KEY }} +tools: + github: + mode: remote + toolsets: [default] + github-app: + app-id: ${{ vars.MCP_APP_ID }} + private-key: ${{ secrets.MCP_APP_KEY }} +safe-outputs: + create-issue: +engine: copilot +--- + +# tools.github.github-app overrides the top-level github-app fallback. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.ParsedTools, "ParsedTools should be populated") + require.NotNil(t, data.ParsedTools.GitHub, "ParsedTools.GitHub should be populated") + require.NotNil(t, data.ParsedTools.GitHub.GitHubApp, "ParsedTools.GitHub.GitHubApp should be populated") + assert.Equal(t, "${{ vars.MCP_APP_ID }}", data.ParsedTools.GitHub.GitHubApp.AppID, + "tools.github should use its own section-specific github-app, not the top-level fallback") +} + +// TestTopLevelGitHubAppDependenciesFallback tests that the top-level github-app is applied +// to APM dependencies when no section-specific github-app is configured. +func TestTopLevelGitHubAppDependenciesFallback(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +dependencies: + packages: + - myorg/private-skill +safe-outputs: + create-issue: +engine: copilot +--- + +# Top-level github-app fallback for APM dependencies. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.APMDependencies, "APMDependencies should be populated") + require.NotNil(t, data.APMDependencies.GitHubApp, + "APMDependencies.GitHubApp should be populated from top-level fallback") + assert.Equal(t, "${{ vars.APP_ID }}", data.APMDependencies.GitHubApp.AppID, + "APM dependencies should use the top-level github-app fallback") +} + +// TestTopLevelGitHubAppDependenciesOverride tests that a section-specific dependencies.github-app +// takes precedence over the top-level github-app fallback. +func TestTopLevelGitHubAppDependenciesOverride(t *testing.T) { + compiler := NewCompilerWithVersion("1.0.0") + + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755)) + + workflowContent := `--- +on: issues +permissions: + contents: read +github-app: + app-id: ${{ vars.TOP_LEVEL_APP_ID }} + private-key: ${{ secrets.TOP_LEVEL_APP_KEY }} +dependencies: + packages: + - myorg/private-skill + github-app: + app-id: ${{ vars.DEPS_APP_ID }} + private-key: ${{ secrets.DEPS_APP_KEY }} +safe-outputs: + create-issue: +engine: copilot +--- + +# dependencies.github-app overrides the top-level github-app fallback. +` + mdPath := filepath.Join(workflowsDir, "main.md") + require.NoError(t, os.WriteFile(mdPath, []byte(workflowContent), 0644)) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(workflowsDir)) + defer func() { _ = os.Chdir(origDir) }() + + data, err := compiler.ParseWorkflowFile("main.md") + require.NoError(t, err) + + require.NotNil(t, data.APMDependencies, "APMDependencies should be populated") + require.NotNil(t, data.APMDependencies.GitHubApp, "APMDependencies.GitHubApp should be populated") + assert.Equal(t, "${{ vars.DEPS_APP_ID }}", data.APMDependencies.GitHubApp.AppID, + "APM dependencies should use section-specific github-app, not the top-level fallback") +} diff --git a/pkg/workflow/top_level_github_app_integration_test.go b/pkg/workflow/top_level_github_app_integration_test.go new file mode 100644 index 00000000000..984cbc06f49 --- /dev/null +++ b/pkg/workflow/top_level_github_app_integration_test.go @@ -0,0 +1,404 @@ +//go:build integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/github/gh-aw/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestTopLevelGitHubAppFallback tests that a top-level github-app field in the frontmatter +// is used as a fallback for all nested github-app token minting operations when no +// section-specific github-app is configured. +func TestTopLevelGitHubAppFallback(t *testing.T) { + tmpDir := testutil.TempDir(t, "top-level-github-app-test") + + t.Run("fallback applied to safe-outputs when no section-specific github-app", func(t *testing.T) { + content := `--- +name: Top Level GitHub App Safe Outputs Fallback +on: + issues: + types: [opened] +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying top-level github-app fallback for safe-outputs. +` + mdPath := filepath.Join(tmpDir, "test-safe-outputs-fallback.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow with top-level github-app should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-safe-outputs-fallback.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // The safe-outputs job should use the top-level github-app for token minting + assert.Contains(t, compiled, "id: safe-outputs-app-token", + "Safe outputs job should generate a token minting step") + assert.Contains(t, compiled, "app-id: ${{ vars.APP_ID }}", + "Token minting step should use the top-level APP_ID") + assert.Contains(t, compiled, "private-key: ${{ secrets.APP_PRIVATE_KEY }}", + "Token minting step should use the top-level APP_PRIVATE_KEY") + }) + + t.Run("fallback applied to activation when no on.github-app", func(t *testing.T) { + content := `--- +name: Top Level GitHub App Activation Fallback +on: + issues: + types: [opened] + reaction: eyes +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying top-level github-app fallback for activation. +` + mdPath := filepath.Join(tmpDir, "test-activation-fallback.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow with top-level github-app should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-activation-fallback.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // The activation job should use the top-level github-app for token minting + assert.Contains(t, compiled, "id: activation-app-token", + "Activation job should generate a token minting step using top-level github-app") + assert.Contains(t, compiled, "app-id: ${{ vars.APP_ID }}", + "Token minting step should use the top-level APP_ID") + // The reaction step should use the minted app token + assert.Contains(t, compiled, "github-token: ${{ steps.activation-app-token.outputs.token }}", + "Activation step should use the minted app token") + }) + + t.Run("fallback applied to checkout when no checkout.github-app", func(t *testing.T) { + content := `--- +name: Top Level GitHub App Checkout Fallback +on: + issues: + types: [opened] +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +checkout: + repository: myorg/private-repo + path: private +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying top-level github-app fallback for checkout. +` + mdPath := filepath.Join(tmpDir, "test-checkout-fallback.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow with top-level github-app should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-checkout-fallback.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // The checkout should use the top-level github-app for token minting + assert.Contains(t, compiled, "id: checkout-app-token-0", + "Checkout should generate a token minting step using top-level github-app") + assert.Contains(t, compiled, "app-id: ${{ vars.APP_ID }}", + "Token minting step should use the top-level APP_ID") + }) + + t.Run("fallback applied to tools.github when no tools.github.github-app", func(t *testing.T) { + content := `--- +name: Top Level GitHub App MCP Fallback +on: + issues: + types: [opened] +permissions: + contents: read + issues: read + pull-requests: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +tools: + github: + mode: remote + toolsets: [default] +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying top-level github-app fallback for tools.github. +` + mdPath := filepath.Join(tmpDir, "test-mcp-fallback.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow with top-level github-app should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-mcp-fallback.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // The agent job should use the top-level github-app for GitHub MCP token minting + assert.Contains(t, compiled, "id: github-mcp-app-token", + "Agent job should generate a GitHub MCP token minting step using top-level github-app") + assert.Contains(t, compiled, "app-id: ${{ vars.APP_ID }}", + "Token minting step should use the top-level APP_ID") + }) + + t.Run("fallback applied to APM dependencies when no dependencies.github-app", func(t *testing.T) { + content := `--- +name: Top Level GitHub App APM Dependencies Fallback +on: + issues: + types: [opened] +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +dependencies: + packages: + - myorg/private-skill +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying top-level github-app fallback for APM dependencies. +` + mdPath := filepath.Join(tmpDir, "test-dependencies-fallback.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow with top-level github-app should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-dependencies-fallback.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // The activation job should have an APM app token minting step using the top-level github-app + assert.Contains(t, compiled, "id: apm-app-token", + "Activation job should generate an APM app token minting step using top-level github-app") + assert.Contains(t, compiled, "app-id: ${{ vars.APP_ID }}", + "APM token minting step should use the top-level APP_ID") + }) + + t.Run("section-specific github-app takes precedence over top-level", func(t *testing.T) { + content := `--- +name: Section Specific GitHub App Precedence +on: + issues: + types: [opened] + reaction: eyes + github-app: + app-id: ${{ vars.ACTIVATION_APP_ID }} + private-key: ${{ secrets.ACTIVATION_APP_KEY }} +permissions: + contents: read +github-app: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} +safe-outputs: + github-app: + app-id: ${{ vars.SAFE_OUTPUTS_APP_ID }} + private-key: ${{ secrets.SAFE_OUTPUTS_APP_KEY }} + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying section-specific github-app takes precedence over top-level fallback. +` + mdPath := filepath.Join(tmpDir, "test-section-precedence.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow with section-specific github-app configs should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-section-precedence.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // The safe-outputs job should use SAFE_OUTPUTS_APP_ID (section-specific), not APP_ID (top-level) + assert.Contains(t, compiled, "app-id: ${{ vars.SAFE_OUTPUTS_APP_ID }}", + "Safe outputs job should use section-specific SAFE_OUTPUTS_APP_ID") + assert.Contains(t, compiled, "app-id: ${{ vars.ACTIVATION_APP_ID }}", + "Activation job should use section-specific ACTIVATION_APP_ID") + // The top-level APP_ID should NOT appear anywhere because it's overridden by section-specific + // configs in both on.github-app and safe-outputs.github-app. SAFE_OUTPUTS_APP_ID and + // ACTIVATION_APP_ID are distinct values from APP_ID, so their presence does not conflict + // with this assertion. + assert.NotContains(t, compiled, "app-id: ${{ vars.APP_ID }}", + "Top-level APP_ID should NOT be used when section-specific configs are present") + }) + + t.Run("no fallback applied when no top-level github-app", func(t *testing.T) { + content := `--- +name: No Top Level GitHub App +on: + issues: + types: [opened] +permissions: + contents: read +safe-outputs: + create-issue: + title-prefix: "[automated] " +engine: copilot +--- + +Test workflow verifying no token minting when no github-app is configured. +` + mdPath := filepath.Join(tmpDir, "test-no-fallback.md") + require.NoError(t, os.WriteFile(mdPath, []byte(content), 0600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mdPath) + require.NoError(t, err, "Workflow without github-app should compile successfully") + + lockPath := filepath.Join(tmpDir, "test-no-fallback.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + // No token minting steps should be generated + assert.NotContains(t, compiled, "id: safe-outputs-app-token", + "No safe-outputs token minting step should be generated without github-app") + assert.NotContains(t, compiled, "id: activation-app-token", + "No activation token minting step should be generated without github-app") + }) +} + +// TestTopLevelGitHubAppWorkflowFiles verifies that the sample workflow files in +// pkg/cli/workflows compile successfully and produce the expected token minting steps. +func TestTopLevelGitHubAppWorkflowFiles(t *testing.T) { + tmpDir := testutil.TempDir(t, "top-level-github-app-workflow-files-test") + + tests := []struct { + name string + workflowFile string + expectContains []string + }{ + { + name: "safe-outputs fallback workflow file", + workflowFile: "../cli/workflows/test-top-level-github-app-safe-outputs.md", + expectContains: []string{ + "id: safe-outputs-app-token", + "app-id: ${{ vars.APP_ID }}", + "private-key: ${{ secrets.APP_PRIVATE_KEY }}", + }, + }, + { + name: "activation fallback workflow file", + workflowFile: "../cli/workflows/test-top-level-github-app-activation.md", + expectContains: []string{ + "id: activation-app-token", + "app-id: ${{ vars.APP_ID }}", + "github-token: ${{ steps.activation-app-token.outputs.token }}", + }, + }, + { + name: "checkout fallback workflow file", + workflowFile: "../cli/workflows/test-top-level-github-app-checkout.md", + expectContains: []string{ + "id: checkout-app-token-0", + "app-id: ${{ vars.APP_ID }}", + }, + }, + { + name: "dependencies fallback workflow file", + workflowFile: "../cli/workflows/test-top-level-github-app-dependencies.md", + expectContains: []string{ + "id: apm-app-token", + "app-id: ${{ vars.APP_ID }}", + }, + }, + { + name: "section-specific override workflow file", + workflowFile: "../cli/workflows/test-top-level-github-app-override.md", + expectContains: []string{ + "app-id: ${{ vars.SAFE_OUTPUTS_APP_ID }}", + "app-id: ${{ vars.ACTIVATION_APP_ID }}", + }, + }, + { + name: "tools.github MCP fallback workflow file", + workflowFile: "../cli/workflows/test-top-level-github-app-mcp.md", + expectContains: []string{ + "id: github-mcp-app-token", + "app-id: ${{ vars.APP_ID }}", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + src, err := os.ReadFile(tt.workflowFile) + require.NoError(t, err, "Failed to read workflow file %s", tt.workflowFile) + + baseName := filepath.Base(tt.workflowFile) + mdDst := filepath.Join(tmpDir, baseName) + require.NoError(t, os.WriteFile(mdDst, src, 0600)) + + compiler := NewCompiler() + err = compiler.CompileWorkflow(mdDst) + require.NoError(t, err, "Workflow file %s should compile successfully", tt.workflowFile) + + lockName := strings.TrimSuffix(baseName, ".md") + ".lock.yml" + lockPath := filepath.Join(tmpDir, lockName) + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + for _, expected := range tt.expectContains { + assert.Contains(t, compiled, expected, + "Compiled workflow should contain %q", expected) + } + }) + } +}