diff --git a/.changeset/patch-add-github-token-configuration.md b/.changeset/patch-add-github-token-configuration.md new file mode 100644 index 0000000000..fdcbb903ec --- /dev/null +++ b/.changeset/patch-add-github-token-configuration.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add support for top-level `github-token` configuration in workflow frontmatter diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index 12e7eda650..39b5cbb0df 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -3209,6 +3209,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Artifacts Summary" GITHUB_AW_DISCUSSION_CATEGORY: "artifacts" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3657,6 +3658,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index 6eb7759cfa..e10f20d671 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -3073,6 +3073,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Agentic Workflow Audit Agent" GITHUB_AW_DISCUSSION_CATEGORY: "audits" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3535,6 +3536,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index e7d428aad5..61233c4b70 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -613,6 +613,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Brave Web Search Agent" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -4488,6 +4489,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -4726,6 +4728,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Brave Web Search Agent" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; diff --git a/.github/workflows/changeset-generator.lock.yml b/.github/workflows/changeset-generator.lock.yml index 7405c7d75f..9a51547fa4 100644 --- a/.github/workflows/changeset-generator.lock.yml +++ b/.github/workflows/changeset-generator.lock.yml @@ -3682,6 +3682,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -3924,6 +3925,7 @@ jobs: GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); async function main() { diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 6fde528595..91e7c7e638 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -112,6 +112,7 @@ jobs: GITHUB_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/ci-doctor.md" GITHUB_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/main/workflows/ci-doctor.md" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -3707,6 +3708,7 @@ jobs: GITHUB_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/main/workflows/ci-doctor.md" GITHUB_AW_ISSUE_TITLE_PREFIX: "${{ github.workflow }}" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -4206,6 +4208,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -4357,6 +4360,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "CI Failure Doctor" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index aa05bd5107..5028484226 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -2982,6 +2982,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -3634,6 +3635,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index 167266e59a..2371b9043d 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -3343,6 +3343,7 @@ jobs: GITHUB_AW_DISCUSSION_TITLE_PREFIX: "[copilot-agent-analysis] " GITHUB_AW_DISCUSSION_CATEGORY: "audits" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3805,6 +3806,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 89b1bbed8b..8e68e155a1 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -3124,6 +3124,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -3776,6 +3777,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index c7a3cc8721..53d5460018 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -3289,6 +3289,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Daily News" GITHUB_AW_DISCUSSION_CATEGORY: "daily-news" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3737,6 +3738,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 980dc367a3..9c7e6ca5ad 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -3200,6 +3200,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Dev" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -3697,6 +3698,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 69d5261f6c..4d09afe8c0 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -1334,7 +1334,7 @@ jobs: env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml GITHUB_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -2825,6 +2825,7 @@ jobs: GITHUB_AW_ISSUE_TITLE_PREFIX: "[duplicate-code] " GITHUB_AW_ISSUE_LABELS: "code-quality,automated-analysis" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -3243,7 +3244,7 @@ jobs: env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml GITHUB_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} @@ -3322,6 +3323,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index 4774ee7d45..c9d3ad6bbe 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -2785,6 +2785,7 @@ jobs: GITHUB_AW_ISSUE_TITLE_PREFIX: "[workflow-analysis] " GITHUB_AW_ISSUE_LABELS: "automation,ci-improvement" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -3298,6 +3299,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 1bcaa71531..d172341886 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -964,7 +964,7 @@ jobs: "type": "http", "url": "https://api.githubcopilot.com/mcp/", "headers": { - "Authorization": "Bearer ${{ secrets.GH_AW_GITHUB_TOKEN }}" + "Authorization": "Bearer ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" } }, "safe_outputs": { @@ -3291,6 +3291,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "GitHub MCP Remote Server Tools Report Generator" GITHUB_AW_DISCUSSION_CATEGORY: "audits" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3562,6 +3563,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -4214,6 +4216,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 7890af2251..118a65fb30 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -2900,6 +2900,7 @@ jobs: GITHUB_AW_ISSUE_TITLE_PREFIX: "[ast-grep] " GITHUB_AW_ISSUE_LABELS: "code-quality,ast-grep" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -3411,6 +3412,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 2f5edea714..5fc5af9a83 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -596,6 +596,7 @@ jobs: GITHUB_AW_LABELS_ALLOWED: "bug,feature,enhancement,documentation" GITHUB_AW_LABELS_MAX_COUNT: 1 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -2836,6 +2837,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index 3f4835147f..2c91f353d5 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -3136,6 +3136,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Lockfile Statistics Analysis Agent" GITHUB_AW_DISCUSSION_CATEGORY: "audits" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3598,6 +3599,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 50286cc5f6..d46ccee5ac 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -4171,6 +4171,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "MCP Inspector Agent" GITHUB_AW_DISCUSSION_CATEGORY: "audits" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -4619,6 +4620,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index 78c449c689..49208b7bab 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -631,6 +631,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Resource Summarizer Agent" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -4438,6 +4439,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -4679,6 +4681,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Resource Summarizer Agent" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 2e62561aed..cbcd97e46c 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -3795,6 +3795,7 @@ jobs: GITHUB_AW_ISSUE_TITLE_PREFIX: "[task] " GITHUB_AW_ISSUE_LABELS: "task,ai-generated" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -4292,6 +4293,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 338461c3a5..f3c32c74ec 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -644,6 +644,7 @@ jobs: GITHUB_AW_COMMENT_TARGET: "*" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -960,6 +961,7 @@ jobs: GITHUB_AW_LABELS_MAX_COUNT: 5 GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -4567,6 +4569,7 @@ jobs: GITHUB_AW_ISSUE_LABELS: "poetry,automation,ai-generated" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -4866,6 +4869,7 @@ jobs: GITHUB_AW_PR_REVIEW_COMMENT_SIDE: "RIGHT" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -5196,6 +5200,7 @@ jobs: GITHUB_AW_MAX_PATCH_SIZE: 1024 GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -5832,6 +5837,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -6076,6 +6082,7 @@ jobs: GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); async function main() { @@ -6375,6 +6382,7 @@ jobs: GITHUB_AW_UPDATE_TARGET: "*" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; @@ -6608,6 +6616,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Poem Bot - A Creative Agentic Workflow" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; @@ -6739,6 +6748,7 @@ jobs: GITHUB_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const path = require("path"); diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index 575e4e3c96..c8117dd03e 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -647,6 +647,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Q" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -4652,6 +4653,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -5288,6 +5290,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -5536,6 +5539,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Q" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index d05ede3537..fde89eda2f 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -3229,6 +3229,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Repository Tree Map Generator" GITHUB_AW_DISCUSSION_CATEGORY: "dev" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3677,6 +3678,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index ca9fac4f87..797d4b878d 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -3205,6 +3205,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Basic Research Agent" GITHUB_AW_DISCUSSION_CATEGORY: "research" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const agentOutputFile = process.env.GITHUB_AW_AGENT_OUTPUT; @@ -3653,6 +3654,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index ff5f51022e..31caf3df0d 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -655,6 +655,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Scout" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -4851,6 +4852,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -5102,6 +5104,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Scout" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index b47fca6f5c..b71dbac879 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -3069,6 +3069,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -3721,6 +3722,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 0cb880ebac..327c2011a4 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2743,6 +2743,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Smoke Claude" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -3256,6 +3257,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index ac1381b55a..1082fc791f 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1080,7 +1080,7 @@ jobs: env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml GITHUB_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} @@ -2571,6 +2571,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Smoke Codex" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -2989,7 +2990,7 @@ jobs: env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} CODEX_HOME: /tmp/gh-aw/mcp-config - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/config.toml GITHUB_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} @@ -3068,6 +3069,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index aaa4db4ad8..c62f61418c 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -3155,6 +3155,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Smoke Copilot" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -3654,6 +3655,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/smoke-genaiscript.lock.yml b/.github/workflows/smoke-genaiscript.lock.yml index 5e37b92b6a..778c2d0f8d 100644 --- a/.github/workflows/smoke-genaiscript.lock.yml +++ b/.github/workflows/smoke-genaiscript.lock.yml @@ -1881,6 +1881,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Smoke GenAIScript" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -2386,6 +2387,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/smoke-opencode.lock.yml b/.github/workflows/smoke-opencode.lock.yml index 6092cb423a..69da38e3f3 100644 --- a/.github/workflows/smoke-opencode.lock.yml +++ b/.github/workflows/smoke-opencode.lock.yml @@ -1865,6 +1865,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Smoke OpenCode" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function sanitizeLabelContent(content) { if (!content || typeof content !== "string") { @@ -2354,6 +2355,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index f57a616711..384f822a7a 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -115,6 +115,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Technical Documentation Writer for GitHub Actions" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -3496,6 +3497,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -4148,6 +4150,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -4386,6 +4389,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Technical Documentation Writer for GitHub Actions" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; @@ -4516,6 +4520,7 @@ jobs: GITHUB_AW_ASSETS_MAX_SIZE_KB: 10240 GITHUB_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const path = require("path"); diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 9c3c2cc6bf..79e28c126f 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -3791,6 +3791,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -4427,6 +4428,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -4671,6 +4673,7 @@ jobs: GITHUB_AW_PUSH_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); async function main() { diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 498c0191c3..31d051d1e3 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -458,6 +458,7 @@ jobs: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} GITHUB_AW_WORKFLOW_NAME: "Documentation Unbloat" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | function generateFooter( workflowName, @@ -3967,6 +3968,7 @@ jobs: GITHUB_AW_PR_IF_NO_CHANGES: "warn" GITHUB_AW_MAX_PATCH_SIZE: 1024 with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const crypto = require("crypto"); @@ -4617,6 +4619,7 @@ jobs: env: GITHUB_AW_AGENT_OUTPUT: ${{ env.GITHUB_AW_AGENT_OUTPUT }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const fs = require("fs"); @@ -4858,6 +4861,7 @@ jobs: GITHUB_AW_WORKFLOW_NAME: "Documentation Unbloat" GITHUB_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | async function main() { const commentId = process.env.GITHUB_AW_COMMENT_ID; @@ -4988,6 +4992,7 @@ jobs: GITHUB_AW_ASSETS_MAX_SIZE_KB: 10240 GITHUB_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg" with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require("fs"); const path = require("path"); diff --git a/docs/src/content/docs/guides/security.md b/docs/src/content/docs/guides/security.md index b76cf23473..08223db850 100644 --- a/docs/src/content/docs/guides/security.md +++ b/docs/src/content/docs/guides/security.md @@ -237,22 +237,56 @@ GitHub Agentic Workflows support flexible token configuration for different exec #### GitHub Token Precedence -By default, workflows use GitHub's standard `GITHUB_TOKEN` for authentication. However, you can override this behavior using environment variables with the following precedence: +Workflows use a hierarchical token precedence system that allows you to configure authentication tokens at different levels. The precedence order from highest to lowest is: -1. **`GH_AW_GITHUB_TOKEN`** - Primary override token (highest priority) -2. **`GITHUB_TOKEN`** - Standard GitHub Actions token (fallback) +1. **Individual safe-output `github-token`** - Token specified for a specific safe-output (e.g., `create-issue.github-token`) +2. **Safe-outputs global `github-token`** - Token specified at the `safe-outputs` level +3. **Top-level `github-token`** - Token specified in workflow frontmatter (new) +4. **Default fallback** - `${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}` + +This allows you to set a default token for the entire workflow while still allowing specific safe-outputs to use different tokens when needed. #### Token Configuration Examples -**Basic override for enhanced permissions:** +**Top-level token for entire workflow:** ```yaml -# Set via repository secrets -env: - GH_AW_GITHUB_TOKEN: ${{ secrets.CUSTOM_PAT }} +--- +on: push +github-token: ${{ secrets.CUSTOM_PAT }} +engine: claude +tools: + github: + allowed: [list_issues] +--- +``` + +This token will be used for: +- Engine authentication (GitHub MCP server, etc.) +- Checkout steps (in trial mode) +- Safe-output operations (unless overridden) + +**Safe outputs with custom tokens:** + +```yaml +--- +on: push +github-token: ${{ secrets.DEFAULT_PAT }} +safe-outputs: + github-token: ${{ secrets.SAFE_OUTPUT_PAT }} + create-issue: + github-token: ${{ secrets.ISSUE_SPECIFIC_PAT }} + create-pull-request: + # Uses safe-outputs global token +--- ``` -**Per-job token configuration:** +In this example: +- `create-issue` uses `ISSUE_SPECIFIC_PAT` (highest precedence) +- `create-pull-request` uses `SAFE_OUTPUT_PAT` (safe-outputs level) +- Engine and other operations use `DEFAULT_PAT` (top-level) + +**Per-job token configuration (legacy):** ```yaml jobs: @@ -264,15 +298,6 @@ jobs: # Workflow steps use the enhanced token ``` -**Safe outputs with custom tokens:** - -```yaml -safe-outputs: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - create-issue: - create-pull-request: -``` - #### Security Considerations When using custom tokens: diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index da1398fc17..9a384d1d98 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2858,6 +2858,10 @@ } }, "additionalProperties": false + }, + "github-token": { + "type": "string", + "description": "GitHub token expression to use for all steps that require GitHub authentication. Typically a secret reference like ${{ secrets.GITHUB_TOKEN }} or ${{ secrets.CUSTOM_PAT }}. If not specified, defaults to ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}. This value can be overridden by safe-outputs github-token or individual safe-output github-token fields." } }, "additionalProperties": false, diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index f39739375c..1390ae9c92 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -630,7 +630,7 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a switch toolName { case "github": githubTool := tools["github"] - e.renderGitHubClaudeMCPConfig(yaml, githubTool, isLast) + e.renderGitHubClaudeMCPConfig(yaml, githubTool, isLast, workflowData) case "playwright": playwrightTool := tools["playwright"] e.renderPlaywrightMCPConfig(yaml, playwrightTool, isLast) @@ -655,7 +655,7 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a // renderGitHubClaudeMCPConfig generates the GitHub MCP server configuration // Supports both local (Docker) and remote (hosted) modes -func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) { +func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, githubTool any, isLast bool, workflowData *WorkflowData) { githubType := getGitHubType(githubTool) customGitHubToken := getGitHubToken(githubTool) readOnly := getGitHubReadOnly(githubTool) @@ -670,12 +670,9 @@ func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, github yaml.WriteString(" \"url\": \"https://api.githubcopilot.com/mcp/\",\n") yaml.WriteString(" \"headers\": {\n") - // Add custom github-token if specified, otherwise use GH_AW_GITHUB_TOKEN - if customGitHubToken != "" { - yaml.WriteString(fmt.Sprintf(" \"Authorization\": \"Bearer %s\"", customGitHubToken)) - } else { - yaml.WriteString(" \"Authorization\": \"Bearer ${{ secrets.GH_AW_GITHUB_TOKEN }}\"") - } + // Use effective token with precedence: custom > top-level > default + effectiveToken := getEffectiveGitHubToken(customGitHubToken, workflowData.GitHubToken) + yaml.WriteString(fmt.Sprintf(" \"Authorization\": \"Bearer %s\"", effectiveToken)) // Add X-MCP-Readonly header if read-only mode is enabled if readOnly { @@ -720,12 +717,9 @@ func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, github yaml.WriteString(" ],\n") yaml.WriteString(" \"env\": {\n") - // Use custom token if specified, otherwise use default - if customGitHubToken != "" { - yaml.WriteString(fmt.Sprintf(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"%s\"", customGitHubToken)) - } else { - yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\"") - } + // Use effective token with precedence: custom > top-level > default + effectiveToken := getEffectiveGitHubToken(customGitHubToken, workflowData.GitHubToken) + yaml.WriteString(fmt.Sprintf(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"%s\"", effectiveToken)) yaml.WriteString("\n") yaml.WriteString(" }\n") diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 5fd383e6b4..fbe8ae6e83 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -125,6 +125,9 @@ INSTRUCTION=$(cat $GITHUB_AW_PROMPT) mkdir -p $CODEX_HOME/logs codex %sexec%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, modelParam, webSearchParam, fullAutoParam, customArgsParam, logFile) + // Get effective GitHub token based on precedence: top-level github-token > default + effectiveGitHubToken := getEffectiveGitHubToken("", workflowData.GitHubToken) + env := map[string]string{ "CODEX_API_KEY": "${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}", "GITHUB_STEP_SUMMARY": "${{ env.GITHUB_STEP_SUMMARY }}", @@ -132,7 +135,7 @@ codex %sexec%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, modelParam, webSearchParam, ful "GITHUB_AW_MCP_CONFIG": "/tmp/gh-aw/mcp-config/config.toml", "CODEX_HOME": "/tmp/gh-aw/mcp-config", "RUST_LOG": "trace,hyper_util=info,mio=info,reqwest=info,os_info=info,codex_otel=warn,codex_core=debug,ocodex_exec=debug", - "GH_AW_GITHUB_TOKEN": "${{ secrets.GH_AW_GITHUB_TOKEN }}", + "GH_AW_GITHUB_TOKEN": effectiveGitHubToken, } // Add GITHUB_AW_SAFE_OUTPUTS if output is needed @@ -580,12 +583,9 @@ func (e *CodexEngine) renderGitHubCodexMCPConfig(yaml *strings.Builder, githubTo yaml.WriteString(" \n") yaml.WriteString(" [mcp_servers.github.env]\n") - // Use custom token if specified, otherwise use default - if customGitHubToken != "" { - yaml.WriteString(" GITHUB_PERSONAL_ACCESS_TOKEN = \"" + customGitHubToken + "\"\n") - } else { - yaml.WriteString(" GITHUB_PERSONAL_ACCESS_TOKEN = \"${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\"\n") - } + // Use effective token with precedence: custom > top-level > default + effectiveToken := getEffectiveGitHubToken(customGitHubToken, workflowData.GitHubToken) + yaml.WriteString(" GITHUB_PERSONAL_ACCESS_TOKEN = \"" + effectiveToken + "\"\n") } } diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 9626cf20ff..d9c209b6f0 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -178,6 +178,7 @@ type WorkflowData struct { SafetyPrompt bool // whether to include XPIA safety prompt (default true) Runtimes map[string]any // runtime version overrides from frontmatter ToolsTimeout int // timeout in seconds for tool/MCP operations (0 = use engine default) + GitHubToken string // top-level github-token expression from frontmatter ToolsStartupTimeout int // timeout in seconds for MCP server startup (0 = use engine default) } @@ -832,6 +833,7 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) ToolsStartupTimeout: toolsStartupTimeout, TrialMode: c.trialMode, TrialLogicalRepo: c.trialLogicalRepoSlug, + GitHubToken: extractStringValue(result.Frontmatter, "github-token"), } // Extract YAML sections from frontmatter - use direct frontmatter map extraction @@ -2415,7 +2417,6 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) ( return job, nil } - // generateMainJobSteps generates the steps section for the main job func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowData) { // Determine if we need to add a checkout step @@ -2435,7 +2436,8 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat // yaml.WriteString(fmt.Sprintf(" path: %s\n", trialTargetRepoName[1])) // } } - yaml.WriteString(" token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\n") + effectiveToken := getEffectiveGitHubToken("", data.GitHubToken) + yaml.WriteString(fmt.Sprintf(" token: %s\n", effectiveToken)) } } diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go index a02ba0195d..b3d11a8d8a 100644 --- a/pkg/workflow/copilot_engine.go +++ b/pkg/workflow/copilot_engine.go @@ -118,11 +118,9 @@ copilot %s 2>&1 | tee %s`, shellJoinArgs(copilotArgs), logFile) if hasGitHubTool(workflowData.Tools) { githubTool := workflowData.Tools["github"] customGitHubToken := getGitHubToken(githubTool) - if customGitHubToken != "" { - env["GITHUB_PERSONAL_ACCESS_TOKEN"] = customGitHubToken - } else { - env["GITHUB_PERSONAL_ACCESS_TOKEN"] = "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" - } + // Use effective token with precedence: custom > top-level > default + effectiveToken := getEffectiveGitHubToken(customGitHubToken, workflowData.GitHubToken) + env["GITHUB_PERSONAL_ACCESS_TOKEN"] = effectiveToken } // Add GITHUB_AW_SAFE_OUTPUTS if output is needed diff --git a/pkg/workflow/custom_engine.go b/pkg/workflow/custom_engine.go index d6d058c96f..b96705d45d 100644 --- a/pkg/workflow/custom_engine.go +++ b/pkg/workflow/custom_engine.go @@ -158,7 +158,7 @@ func (e *CustomEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a switch toolName { case "github": githubTool := tools["github"] - e.renderGitHubMCPConfig(yaml, githubTool, isLast) + e.renderGitHubMCPConfig(yaml, githubTool, isLast, workflowData) case "playwright": playwrightTool := tools["playwright"] e.renderPlaywrightMCPConfig(yaml, playwrightTool, isLast) @@ -182,9 +182,10 @@ func (e *CustomEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a } // renderGitHubMCPConfig generates the GitHub MCP server configuration using shared logic -func (e *CustomEngine) renderGitHubMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) { +func (e *CustomEngine) renderGitHubMCPConfig(yaml *strings.Builder, githubTool any, isLast bool, workflowData *WorkflowData) { githubDockerImageVersion := getGitHubDockerImageVersion(githubTool) customArgs := getGitHubCustomArgs(githubTool) + customGitHubToken := getGitHubToken(githubTool) readOnly := getGitHubReadOnly(githubTool) yaml.WriteString(" \"github\": {\n") @@ -209,7 +210,9 @@ func (e *CustomEngine) renderGitHubMCPConfig(yaml *strings.Builder, githubTool a yaml.WriteString("\n") yaml.WriteString(" ],\n") yaml.WriteString(" \"env\": {\n") - yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\"\n") + // Use effective token with precedence: custom > top-level > default + effectiveToken := getEffectiveGitHubToken(customGitHubToken, workflowData.GitHubToken) + yaml.WriteString(fmt.Sprintf(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"%s\"\n", effectiveToken)) yaml.WriteString(" }\n") if isLast { diff --git a/pkg/workflow/github_remote_mode_test.go b/pkg/workflow/github_remote_mode_test.go index 9c6a62f4f6..c03f7e432a 100644 --- a/pkg/workflow/github_remote_mode_test.go +++ b/pkg/workflow/github_remote_mode_test.go @@ -35,7 +35,7 @@ tools: ---`, expectedType: "remote", expectedURL: "https://api.githubcopilot.com/mcp/", - expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN }}", + expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", engineType: "claude", }, { @@ -65,7 +65,7 @@ tools: ---`, expectedType: "remote", expectedURL: "https://api.githubcopilot.com/mcp/", - expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN }}", + expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", engineType: "claude", }, { @@ -92,7 +92,7 @@ tools: ---`, expectedType: "remote", expectedURL: "https://api.githubcopilot.com/mcp/", - expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN }}", + expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", engineType: "copilot", }, { @@ -107,7 +107,7 @@ tools: ---`, expectedType: "remote", expectedURL: "https://api.githubcopilot.com/mcp/", - expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN }}", + expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", engineType: "copilot", }, { @@ -121,7 +121,7 @@ tools: ---`, expectedType: "remote", expectedURL: "https://api.githubcopilot.com/mcp/", - expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN }}", + expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", engineType: "codex", }, { @@ -151,7 +151,7 @@ tools: ---`, expectedType: "remote", expectedURL: "https://api.githubcopilot.com/mcp-readonly/", - expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN }}", + expectedToken: "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}", engineType: "codex", }, } diff --git a/pkg/workflow/github_token.go b/pkg/workflow/github_token.go new file mode 100644 index 0000000000..45e20c65a3 --- /dev/null +++ b/pkg/workflow/github_token.go @@ -0,0 +1,15 @@ +package workflow + +// getEffectiveGitHubToken returns the GitHub token to use, with precedence: +// 1. Custom token passed as parameter (e.g., from safe-outputs) +// 2. Top-level github-token from frontmatter +// 3. Default fallback: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} +func getEffectiveGitHubToken(customToken, toplevelToken string) string { + if customToken != "" { + return customToken + } + if toplevelToken != "" { + return toplevelToken + } + return "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" +} diff --git a/pkg/workflow/github_token_precedence_integration_test.go b/pkg/workflow/github_token_precedence_integration_test.go new file mode 100644 index 0000000000..fdfe5c4bd3 --- /dev/null +++ b/pkg/workflow/github_token_precedence_integration_test.go @@ -0,0 +1,267 @@ +//go:build integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestTopLevelGitHubTokenPrecedence(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "github-token-precedence-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + t.Run("top-level github-token used when no safe-outputs token", func(t *testing.T) { + testContent := `--- +name: Test Top-Level GitHub Token +on: + issues: + types: [opened] +engine: claude +github-token: ${{ secrets.TOPLEVEL_PAT }} +tools: + github: + mode: remote + allowed: [list_issues] +--- + +# Test Top-Level GitHub Token + +Test that top-level github-token is used in engine configuration. +` + + testFile := filepath.Join(tmpDir, "test-toplevel-token.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow: %v", err) + } + + outputFile := filepath.Join(tmpDir, "test-toplevel-token.lock.yml") + content, err := os.ReadFile(outputFile) + if err != nil { + t.Fatal(err) + } + + yamlContent := string(content) + + // Verify that the top-level token is used in the GitHub MCP config + if !strings.Contains(yamlContent, "Bearer ${{ secrets.TOPLEVEL_PAT }}") { + t.Error("Expected top-level github-token to be used in Authorization header") + t.Logf("Generated YAML:\n%s", yamlContent) + } + }) + + t.Run("safe-outputs github-token overrides top-level", func(t *testing.T) { + testContent := `--- +name: Test Safe-Outputs Override +on: + issues: + types: [opened] +engine: claude +github-token: ${{ secrets.TOPLEVEL_PAT }} +safe-outputs: + github-token: ${{ secrets.SAFE_OUTPUTS_PAT }} + create-issue: +--- + +# Test Safe-Outputs Override + +Test that safe-outputs github-token overrides top-level. +` + + testFile := filepath.Join(tmpDir, "test-safe-outputs-override.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow: %v", err) + } + + outputFile := filepath.Join(tmpDir, "test-safe-outputs-override.lock.yml") + content, err := os.ReadFile(outputFile) + if err != nil { + t.Fatal(err) + } + + yamlContent := string(content) + + // Verify that safe-outputs token is used in the create_issue job + if !strings.Contains(yamlContent, "github-token: ${{ secrets.SAFE_OUTPUTS_PAT }}") { + t.Error("Expected safe-outputs github-token to be used in create_issue job") + t.Logf("Generated YAML:\n%s", yamlContent) + } + + // Verify that top-level token is NOT used in safe-outputs job + if strings.Contains(yamlContent, "github-token: ${{ secrets.TOPLEVEL_PAT }}") { + t.Error("Top-level github-token should not be used when safe-outputs token is present") + } + }) + + t.Run("individual safe-output token overrides both", func(t *testing.T) { + testContent := `--- +name: Test Individual Override +on: + issues: + types: [opened] +engine: claude +github-token: ${{ secrets.TOPLEVEL_PAT }} +safe-outputs: + github-token: ${{ secrets.SAFE_OUTPUTS_PAT }} + create-issue: + github-token: ${{ secrets.INDIVIDUAL_PAT }} +--- + +# Test Individual Override + +Test that individual safe-output github-token has highest precedence. +` + + testFile := filepath.Join(tmpDir, "test-individual-override.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow: %v", err) + } + + outputFile := filepath.Join(tmpDir, "test-individual-override.lock.yml") + content, err := os.ReadFile(outputFile) + if err != nil { + t.Fatal(err) + } + + yamlContent := string(content) + + // Verify that individual token is used in the create_issue job + if !strings.Contains(yamlContent, "github-token: ${{ secrets.INDIVIDUAL_PAT }}") { + t.Error("Expected individual safe-output github-token to be used in create_issue job") + t.Logf("Generated YAML:\n%s", yamlContent) + } + + // Count occurrences of each token to verify precedence + individualCount := strings.Count(yamlContent, "github-token: ${{ secrets.INDIVIDUAL_PAT }}") + safeOutputsCount := strings.Count(yamlContent, "github-token: ${{ secrets.SAFE_OUTPUTS_PAT }}") + toplevelCount := strings.Count(yamlContent, "github-token: ${{ secrets.TOPLEVEL_PAT }}") + + if individualCount == 0 { + t.Error("Individual token should be present at least once") + } + + // Note: safe-outputs global token might appear in other safe-output jobs or contexts + // but should not appear more frequently than the individual token in the create_issue job + // The test is primarily checking that the individual token is used where it should be + if individualCount == 0 && safeOutputsCount > 0 { + t.Error("Individual token should take precedence over safe-outputs token") + } + if individualCount == 0 && toplevelCount > 0 { + t.Error("Individual token should take precedence over top-level token") + } + }) + + t.Run("top-level token used in codex engine", func(t *testing.T) { + testContent := `--- +name: Test Codex Engine Token +on: + workflow_dispatch: +engine: codex +github-token: ${{ secrets.TOPLEVEL_PAT }} +tools: + github: + allowed: [list_issues] +--- + +# Test Codex Engine Token + +Test that top-level github-token is used in Codex engine. +` + + testFile := filepath.Join(tmpDir, "test-codex-token.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow: %v", err) + } + + outputFile := filepath.Join(tmpDir, "test-codex-token.lock.yml") + content, err := os.ReadFile(outputFile) + if err != nil { + t.Fatal(err) + } + + yamlContent := string(content) + + // Verify that the top-level token is used in GH_AW_GITHUB_TOKEN env var + if !strings.Contains(yamlContent, "GH_AW_GITHUB_TOKEN: ${{ secrets.TOPLEVEL_PAT }}") { + t.Error("Expected top-level github-token to be used in GH_AW_GITHUB_TOKEN env var for Codex") + t.Logf("Generated YAML:\n%s", yamlContent) + } + + // Also check in the TOML config + if !strings.Contains(yamlContent, "GITHUB_PERSONAL_ACCESS_TOKEN = \"${{ secrets.TOPLEVEL_PAT }}\"") { + t.Error("Expected top-level github-token to be used in TOML config for Codex") + } + }) + + t.Run("top-level token used in copilot engine", func(t *testing.T) { + testContent := `--- +name: Test Copilot Engine Token +on: + workflow_dispatch: +engine: copilot +github-token: ${{ secrets.TOPLEVEL_PAT }} +tools: + github: + allowed: [list_issues] +--- + +# Test Copilot Engine Token + +Test that top-level github-token is used in Copilot engine. +` + + testFile := filepath.Join(tmpDir, "test-copilot-token.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow: %v", err) + } + + outputFile := filepath.Join(tmpDir, "test-copilot-token.lock.yml") + content, err := os.ReadFile(outputFile) + if err != nil { + t.Fatal(err) + } + + yamlContent := string(content) + + // Verify that the top-level token is used in GITHUB_PERSONAL_ACCESS_TOKEN env var + if !strings.Contains(yamlContent, "GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.TOPLEVEL_PAT }}") { + t.Error("Expected top-level github-token to be used in GITHUB_PERSONAL_ACCESS_TOKEN env var for Copilot") + t.Logf("Generated YAML:\n%s", yamlContent) + } + }) +} diff --git a/pkg/workflow/github_toolset_test.go b/pkg/workflow/github_toolset_test.go index e284b27917..f328de9c6c 100644 --- a/pkg/workflow/github_toolset_test.go +++ b/pkg/workflow/github_toolset_test.go @@ -96,7 +96,8 @@ func TestClaudeEngineGitHubToolsetsRendering(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine := &ClaudeEngine{} var yaml strings.Builder - engine.renderGitHubClaudeMCPConfig(&yaml, tt.githubTool, true) + workflowData := &WorkflowData{} + engine.renderGitHubClaudeMCPConfig(&yaml, tt.githubTool, true, workflowData) result := yaml.String() @@ -266,7 +267,8 @@ func TestGitHubToolsetsWithOtherConfiguration(t *testing.T) { t.Run(tt.name, func(t *testing.T) { engine := &ClaudeEngine{} var yaml strings.Builder - engine.renderGitHubClaudeMCPConfig(&yaml, tt.githubTool, true) + workflowData := &WorkflowData{} + engine.renderGitHubClaudeMCPConfig(&yaml, tt.githubTool, true, workflowData) result := yaml.String() diff --git a/pkg/workflow/mcp_config_test.go b/pkg/workflow/mcp_config_test.go index 3c164aba70..54aabd7580 100644 --- a/pkg/workflow/mcp_config_test.go +++ b/pkg/workflow/mcp_config_test.go @@ -158,7 +158,8 @@ func TestGenerateGitHubMCPConfig(t *testing.T) { // Call the function under test using the Claude engine engine := NewClaudeEngine() - engine.renderGitHubClaudeMCPConfig(&yamlBuilder, tt.githubTool, true) + workflowData := &WorkflowData{} + engine.renderGitHubClaudeMCPConfig(&yamlBuilder, tt.githubTool, true, workflowData) result := yamlBuilder.String() @@ -209,7 +210,8 @@ func TestMCPConfigurationEdgeCases(t *testing.T) { // Call the function under test using the Claude engine engine := NewClaudeEngine() - engine.renderGitHubClaudeMCPConfig(&yamlBuilder, tt.githubTool, tt.isLast) + workflowData := &WorkflowData{} + engine.renderGitHubClaudeMCPConfig(&yamlBuilder, tt.githubTool, tt.isLast, workflowData) result := yamlBuilder.String() diff --git a/pkg/workflow/parse_utils.go b/pkg/workflow/parse_utils.go index c2ca95fc7e..a0721b9e05 100644 --- a/pkg/workflow/parse_utils.go +++ b/pkg/workflow/parse_utils.go @@ -46,23 +46,26 @@ func (c *Compiler) addCustomSafeOutputEnvVars(steps *[]string, data *WorkflowDat } // addSafeOutputGitHubToken adds github-token to the with section of github-script actions +// Uses precedence: safe-outputs global github-token > top-level github-token > default func (c *Compiler) addSafeOutputGitHubToken(steps *[]string, data *WorkflowData) { - if data.SafeOutputs != nil && data.SafeOutputs.GitHubToken != "" { - *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", data.SafeOutputs.GitHubToken)) - } else { - *steps = append(*steps, " github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\n") + var safeOutputsToken string + if data.SafeOutputs != nil { + safeOutputsToken = data.SafeOutputs.GitHubToken } + effectiveToken := getEffectiveGitHubToken(safeOutputsToken, data.GitHubToken) + *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", effectiveToken)) } // addSafeOutputGitHubTokenForConfig adds github-token to the with section, preferring per-config token over global +// Uses precedence: config token > safe-outputs global github-token > top-level github-token > default func (c *Compiler) addSafeOutputGitHubTokenForConfig(steps *[]string, data *WorkflowData, configToken string) { - token := configToken - if token == "" && data.SafeOutputs != nil { - token = data.SafeOutputs.GitHubToken - } - if token != "" { - *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", token)) + var safeOutputsToken string + if data.SafeOutputs != nil { + safeOutputsToken = data.SafeOutputs.GitHubToken } + // Get effective token using double precedence: config > safe-outputs, then > top-level > default + effectiveToken := getEffectiveGitHubToken(configToken, getEffectiveGitHubToken(safeOutputsToken, data.GitHubToken)) + *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", effectiveToken)) } // filterMapKeys creates a new map excluding the specified keys diff --git a/pkg/workflow/safe_outputs_github_token_test.go b/pkg/workflow/safe_outputs_github_token_test.go index 1cde5ba843..651d84e18a 100644 --- a/pkg/workflow/safe_outputs_github_token_test.go +++ b/pkg/workflow/safe_outputs_github_token_test.go @@ -94,8 +94,9 @@ func TestSafeOutputsGitHubTokenIntegration(t *testing.T) { "create-issue": nil, }, }, - expectedInWith: []string{}, - unexpectedInWith: []string{"github-token:"}, + // With top-level github-token support, we now always add the github-token field with the effective value (default in this case) + expectedInWith: []string{"github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}"}, + unexpectedInWith: []string{}, }, { name: "multiple safe outputs with github-token", @@ -441,13 +442,19 @@ func TestIndividualConfigGitHubTokenConfiguration(t *testing.T) { t.Errorf("Expected step to be '%s', got '%s'", expectedStep, steps[0]) } - // Test with no tokens + // Test with no tokens configured - should use default token steps = []string{} // Reset data.SafeOutputs.GitHubToken = "" + data.GitHubToken = "" // Also clear top-level token compiler.addSafeOutputGitHubTokenForConfig(&steps, data, "") - if len(steps) != 0 { - t.Fatalf("Expected 0 steps to be added when no tokens available, got %d", len(steps)) + // With the new implementation, we always add a token (default if none configured) + expectedStep = " github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\n" + if len(steps) != 1 { + t.Fatalf("Expected 1 step to be added with default token, got %d", len(steps)) + } + if steps[0] != expectedStep { + t.Errorf("Expected step to be '%s', got '%s'", expectedStep, steps[0]) } }) } diff --git a/pkg/workflow/trial_mode_test.go b/pkg/workflow/trial_mode_test.go index 322a6ba142..da3af140a2 100644 --- a/pkg/workflow/trial_mode_test.go +++ b/pkg/workflow/trial_mode_test.go @@ -69,8 +69,33 @@ This is a test workflow for trial mode compilation. } // Checkout should not include github-token in normal mode - if strings.Contains(lockContent, "token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") { - t.Error("Did not expect github-token in checkout step in normal mode") + // Check specifically that the checkout step doesn't have a token parameter + lines := strings.Split(lockContent, "\n") + for i, line := range lines { + if strings.Contains(line, "actions/checkout@v5") { + // Check the next few lines for "with:" and "token:" + for j := i + 1; j < len(lines) && j < i+10; j++ { + if strings.TrimSpace(lines[j]) == "with:" { + // Found "with:" section, check for token + for k := j + 1; k < len(lines) && k < j+5; k++ { + if strings.Contains(lines[k], "token:") { + t.Error("Did not expect github-token in checkout step in normal mode") + break + } + // If we hit another step or section, stop checking + if strings.HasPrefix(strings.TrimSpace(lines[k]), "- name:") { + break + } + } + break + } + // If we hit another step, stop checking + if strings.HasPrefix(strings.TrimSpace(lines[j]), "- name:") { + break + } + } + break + } } }) @@ -101,7 +126,36 @@ This is a test workflow for trial mode compilation. } // Checkout should include github-token in trial mode - if !strings.Contains(lockContent, "token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") { + // Check specifically that the checkout step has the token parameter + lines := strings.Split(lockContent, "\n") + foundCheckoutToken := false + for i, line := range lines { + if strings.Contains(line, "actions/checkout@v5") { + // Check the next few lines for "with:" and "token:" + for j := i + 1; j < len(lines) && j < i+10; j++ { + if strings.TrimSpace(lines[j]) == "with:" { + // Found "with:" section, check for token + for k := j + 1; k < len(lines) && k < j+5; k++ { + if strings.Contains(lines[k], "token:") && strings.Contains(lines[k], "${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}") { + foundCheckoutToken = true + break + } + // If we hit another step or section, stop checking + if strings.HasPrefix(strings.TrimSpace(lines[k]), "- name:") { + break + } + } + break + } + // If we hit another step, stop checking + if strings.HasPrefix(strings.TrimSpace(lines[j]), "- name:") { + break + } + } + break + } + } + if !foundCheckoutToken { t.Error("Expected github-token in checkout step in trial mode") }