diff --git a/.github/workflows/agentic_commands.yml b/.github/workflows/agentic_commands.yml index dbb43b5ca6a..e9f5699b47d 100644 --- a/.github/workflows/agentic_commands.yml +++ b/.github/workflows/agentic_commands.yml @@ -1,4 +1,4 @@ -# gh-aw-commands: {"payload_version":"v1","schema_version":"v1","compiler_version":"dev","commands":["*","ace","approach-validator","archie","cloclo","craft","dependabot-burner","grumpy","matt","mergefest","nit","plan","poem-bot","ponytail","review","ruflo","scout","security-review","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","souschef","squad-plan","summarize","tidy","unbloat"],"workflows":["ace-editor","approach-validator","archie","ci-doctor","cloclo","craft","dependabot-burner","design-decision-gate","dev","grumpy-reviewer","mattpocock-skills-reviewer","mergefest","necromancer","pdf-summary","plan","poem-bot","ponytail-reviewer","pr-code-quality-reviewer","pr-nitpick-reviewer","pr-sous-chef","ruflo-backed-task","scout","security-review","skillet","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","squad-plan","test-quality-sentinel","tidy","unbloat-docs"]} +# gh-aw-commands: {"payload_version":"v1","schema_version":"v1","compiler_version":"dev","commands":["*","ace","approach-validator","archie","cloclo","craft","dependabot-burner","grumpy","matt","mergefest","nit","plan","poem-bot","ponytail","review","ruflo","scout","security-review","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-drive","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","souschef","squad-plan","summarize","tidy","unbloat"],"workflows":["ace-editor","approach-validator","archie","ci-doctor","cloclo","craft","dependabot-burner","design-decision-gate","dev","grumpy-reviewer","mattpocock-skills-reviewer","mergefest","necromancer","pdf-summary","plan","poem-bot","ponytail-reviewer","pr-code-quality-reviewer","pr-nitpick-reviewer","pr-sous-chef","ruflo-backed-task","scout","security-review","skillet","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-drive","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","squad-plan","test-quality-sentinel","tidy","unbloat-docs"]} # Routing summary (sorted): # slash commands: # /* -> skillet [pull_request_comment,pull_request_review_comment] reaction=eyes @@ -43,6 +43,7 @@ # /smoke-crush -> smoke-crush [issue_comment,issues,pull_request,pull_request_comment] reaction=eyes # /smoke-cursor -> smoke-cursor [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-deepseek-harness -> smoke-deepseek-harness [issue_comment,issues,pull_request,pull_request_comment] reaction=eyes +# /smoke-drive -> smoke-drive [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-gemini -> smoke-gemini [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-github-claude -> smoke-github-claude [pull_request,pull_request_comment] reaction=eyes # /smoke-goose -> smoke-goose [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket @@ -141,9 +142,9 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # runner-guard:ignore RGS-016 -- routing tables below contain emoji variation selectors (U+FE0F) and zero-width joiners (U+200D) used to render standard emoji sequences, not steganographic payloads. env: - GH_AW_SLASH_ROUTING: '{"*":[{"workflow":"skillet","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿณ","status_comment":true}],"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"โœ๏ธ","status_comment":true}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes","emoji":"โœ…","status_comment":true}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ›๏ธ","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ“Š","status_comment":true}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes","emoji":"โœ๏ธ","status_comment":true}],"dependabot-burner":[{"workflow":"dependabot-burner","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”ฅ","status_comment":true}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”€","status_comment":true}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ“‹","status_comment":true}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes","emoji":"๐ŸŽญ","status_comment":true}],"ponytail":[{"workflow":"ponytail-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"โœ‚๏ธ","status_comment":true}],"review":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ—๏ธ","status_comment":true},{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true},{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"ruflo":[{"workflow":"ruflo-backed-task","events":["issue_comment"],"ai_reaction":"eyes","status_comment":true}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”ญ","status_comment":true}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”’","status_comment":true}],"smoke-agent-all-merged":[{"workflow":"smoke-agent-all-merged","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-all-none":[{"workflow":"smoke-agent-all-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-public-approved":[{"workflow":"smoke-agent-public-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-public-none":[{"workflow":"smoke-agent-public-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-scoped-approved":[{"workflow":"smoke-agent-scoped-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-aider":[{"workflow":"smoke-aider","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿง‘โ€โœˆ๏ธ","status_comment":true}],"smoke-call-workflow":[{"workflow":"smoke-call-workflow","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-checkout-pr-dispatch":[{"workflow":"smoke-checkout-pr-dispatch","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-claude":[{"workflow":"smoke-claude","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"heart","emoji":"๐Ÿงช","status_comment":true}],"smoke-claude-on-copilot":[{"workflow":"smoke-claude-on-copilot","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-codex":[{"workflow":"smoke-codex","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"hooray","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot":[{"workflow":"smoke-copilot","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-aoai-apikey":[{"workflow":"smoke-copilot-aoai-apikey","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-aoai-entra":[{"workflow":"smoke-copilot-aoai-entra","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-arm":[{"workflow":"smoke-copilot-arm","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-mai":[{"workflow":"smoke-copilot-mai","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"โšก","status_comment":true}],"smoke-copilot-sdk":[{"workflow":"smoke-copilot-sdk","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”ฌ","status_comment":true}],"smoke-copilot-small":[{"workflow":"smoke-copilot-small","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿชถ","status_comment":true}],"smoke-create-cross-repo-pr":[{"workflow":"smoke-create-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-crush":[{"workflow":"smoke-crush","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-cursor":[{"workflow":"smoke-cursor","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿ–ฑ๏ธ","status_comment":true}],"smoke-deepseek-harness":[{"workflow":"smoke-deepseek-harness","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-gemini":[{"workflow":"smoke-gemini","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงช","status_comment":true}],"smoke-github-claude":[{"workflow":"smoke-github-claude","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-goose":[{"workflow":"smoke-goose","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿชฟ","status_comment":true}],"smoke-kiro":[{"workflow":"smoke-kiro","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงญ","status_comment":true}],"smoke-multi-pr":[{"workflow":"smoke-multi-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-opencode":[{"workflow":"smoke-opencode","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงช","status_comment":true}],"smoke-otel-backends":[{"workflow":"smoke-otel-backends","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-pi":[{"workflow":"smoke-pi","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงช","status_comment":true}],"smoke-project":[{"workflow":"smoke-project","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-pydantic":[{"workflow":"smoke-pydantic","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿ","status_comment":true}],"smoke-service-ports":[{"workflow":"smoke-service-ports","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-temporary-id":[{"workflow":"smoke-temporary-id","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-test-tools":[{"workflow":"smoke-test-tools","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-update-cross-repo-pr":[{"workflow":"smoke-update-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"souschef":[{"workflow":"pr-sous-chef","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ‘จโ€๐Ÿณ","status_comment":true}],"squad-plan":[{"workflow":"squad-plan","events":["issue_comment"],"ai_reaction":"eyes","emoji":"๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘","status_comment":true}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes","emoji":"๐Ÿ“„","status_comment":true}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงน","status_comment":true}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ“","status_comment":true}]}' + GH_AW_SLASH_ROUTING: '{"*":[{"workflow":"skillet","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿณ","status_comment":true}],"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"โœ๏ธ","status_comment":true}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes","emoji":"โœ…","status_comment":true}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ›๏ธ","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ“Š","status_comment":true}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes","emoji":"โœ๏ธ","status_comment":true}],"dependabot-burner":[{"workflow":"dependabot-burner","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”ฅ","status_comment":true}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”€","status_comment":true}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ“‹","status_comment":true}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes","emoji":"๐ŸŽญ","status_comment":true}],"ponytail":[{"workflow":"ponytail-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"โœ‚๏ธ","status_comment":true}],"review":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ—๏ธ","status_comment":true},{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”","status_comment":true},{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"ruflo":[{"workflow":"ruflo-backed-task","events":["issue_comment"],"ai_reaction":"eyes","status_comment":true}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”ญ","status_comment":true}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”’","status_comment":true}],"smoke-agent-all-merged":[{"workflow":"smoke-agent-all-merged","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-all-none":[{"workflow":"smoke-agent-all-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-public-approved":[{"workflow":"smoke-agent-public-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-public-none":[{"workflow":"smoke-agent-public-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-agent-scoped-approved":[{"workflow":"smoke-agent-scoped-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-aider":[{"workflow":"smoke-aider","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿง‘โ€โœˆ๏ธ","status_comment":true}],"smoke-call-workflow":[{"workflow":"smoke-call-workflow","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-checkout-pr-dispatch":[{"workflow":"smoke-checkout-pr-dispatch","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-claude":[{"workflow":"smoke-claude","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"heart","emoji":"๐Ÿงช","status_comment":true}],"smoke-claude-on-copilot":[{"workflow":"smoke-claude-on-copilot","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-codex":[{"workflow":"smoke-codex","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"hooray","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot":[{"workflow":"smoke-copilot","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-aoai-apikey":[{"workflow":"smoke-copilot-aoai-apikey","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-aoai-entra":[{"workflow":"smoke-copilot-aoai-entra","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-arm":[{"workflow":"smoke-copilot-arm","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-copilot-mai":[{"workflow":"smoke-copilot-mai","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"โšก","status_comment":true}],"smoke-copilot-sdk":[{"workflow":"smoke-copilot-sdk","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ”ฌ","status_comment":true}],"smoke-copilot-small":[{"workflow":"smoke-copilot-small","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿชถ","status_comment":true}],"smoke-create-cross-repo-pr":[{"workflow":"smoke-create-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-crush":[{"workflow":"smoke-crush","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-cursor":[{"workflow":"smoke-cursor","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿ–ฑ๏ธ","status_comment":true}],"smoke-deepseek-harness":[{"workflow":"smoke-deepseek-harness","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-drive":[{"workflow":"smoke-drive","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿ’พ","status_comment":true}],"smoke-gemini":[{"workflow":"smoke-gemini","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงช","status_comment":true}],"smoke-github-claude":[{"workflow":"smoke-github-claude","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-goose":[{"workflow":"smoke-goose","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿชฟ","status_comment":true}],"smoke-kiro":[{"workflow":"smoke-kiro","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงญ","status_comment":true}],"smoke-multi-pr":[{"workflow":"smoke-multi-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-opencode":[{"workflow":"smoke-opencode","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงช","status_comment":true}],"smoke-otel-backends":[{"workflow":"smoke-otel-backends","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-pi":[{"workflow":"smoke-pi","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿงช","status_comment":true}],"smoke-project":[{"workflow":"smoke-project","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-pydantic":[{"workflow":"smoke-pydantic","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"๐Ÿ","status_comment":true}],"smoke-service-ports":[{"workflow":"smoke-service-ports","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-temporary-id":[{"workflow":"smoke-temporary-id","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-test-tools":[{"workflow":"smoke-test-tools","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-update-cross-repo-pr":[{"workflow":"smoke-update-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"souschef":[{"workflow":"pr-sous-chef","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ‘จโ€๐Ÿณ","status_comment":true}],"squad-plan":[{"workflow":"squad-plan","events":["issue_comment"],"ai_reaction":"eyes","emoji":"๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘","status_comment":true}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes","emoji":"๐Ÿ“„","status_comment":true}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿงน","status_comment":true}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"๐Ÿ“","status_comment":true}]}' GH_AW_LABEL_ROUTING: '{"approach-proposal":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes","emoji":"โœ…","status_comment":true}],"ci-doctor":[{"workflow":"ci-doctor","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿฅ","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","issues","pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿ“Š","status_comment":true}],"dev":[{"workflow":"dev","events":["discussion","issues","pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿ’ป","status_comment":true}],"necromancer":[{"workflow":"necromancer","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿ’€","status_comment":true}],"needs-design":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes","emoji":"โœ…","status_comment":true}],"smoke":[{"workflow":"smoke-copilot","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true},{"workflow":"smoke-copilot-aoai-apikey","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true},{"workflow":"smoke-copilot-aoai-entra","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true},{"workflow":"smoke-copilot-mai","events":["pull_request"],"ai_reaction":"eyes","emoji":"โšก","status_comment":true},{"workflow":"smoke-copilot-small","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿชถ","status_comment":true},{"workflow":"smoke-otel-backends","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿงช","status_comment":true}],"smoke-sdk":[{"workflow":"smoke-copilot-sdk","events":["pull_request"],"ai_reaction":"eyes","emoji":"๐Ÿ”ฌ","status_comment":true}]}' - GH_AW_HELP_COMMANDS: '[{"command":"*","description":"Reviews pull requests by mapping any slash command to a matching repository skill under .github/skills","centralized":true,"decentralized":false,"source_file":"skillet"},{"command":"ace","description":"Generates an ACE editor session link when invoked with /ace command on pull request comments","centralized":true,"decentralized":false,"source_file":"ace-editor"},{"command":"approach-validator","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":true,"decentralized":false,"source_file":"approach-validator"},{"command":"archie","description":"Generates Mermaid diagrams to visualize issue and pull request relationships when invoked with the /archie command","centralized":true,"decentralized":false,"source_file":"archie"},{"command":"cloclo","centralized":true,"decentralized":false,"source_file":"cloclo"},{"command":"craft","description":"Generates new agentic workflow markdown files based on user requests when invoked with /craft command","centralized":true,"decentralized":false,"source_file":"craft"},{"command":"dependabot-burner","description":"Runs one grouped Dependabot remediation wave from schedule, manual dispatch, or /dependabot-burner on pull requests","centralized":true,"decentralized":false,"source_file":"dependabot-burner"},{"command":"grumpy","description":"โš ๏ธ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Performs critical code review with a focus on edge cases, potential bugs, and code quality issues","centralized":true,"decentralized":false,"source_file":"grumpy-reviewer"},{"command":"matt","description":"Reviews pull requests using Matt Pocock''s engineering skills to provide targeted, high-quality improvement suggestions based on the type of changes","centralized":true,"decentralized":false,"source_file":"mattpocock-skills-reviewer"},{"command":"mergefest","description":"Automatically merges the main branch into pull request branches when invoked with /mergefest command","centralized":true,"decentralized":false,"source_file":"mergefest"},{"command":"nit","description":"โš ๏ธ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Provides detailed nitpicky code review focusing on style, best practices, and minor improvements","centralized":true,"decentralized":false,"source_file":"pr-nitpick-reviewer"},{"command":"plan","description":"Generates project plans and task breakdowns when invoked with /plan command in issues or PRs","centralized":true,"decentralized":false,"source_file":"plan"},{"command":"poem-bot","description":"Generates creative poems on specified themes when invoked with /poem-bot command","centralized":true,"decentralized":false,"source_file":"poem-bot"},{"command":"ponytail","description":"Reviews pull requests for unnecessary complexity using Ponytail","centralized":true,"decentralized":false,"source_file":"ponytail-reviewer"},{"command":"q","description":"Intelligent assistant that answers questions, analyzes repositories, and can create PRs for workflow optimizations","centralized":false,"decentralized":true,"source_file":"q"},{"command":"review","description":"Enforces Architecture Decision Records (ADRs) before implementation work can merge, detecting missing design decisions and generating draft ADRs using AI analysis","centralized":true,"decentralized":false,"source_file":"design-decision-gate"},{"command":"ruflo","description":"Runs a repository task inside GitHub Agentic Workflows while delegating inner planning and coordination to Ruflo","centralized":true,"decentralized":false,"source_file":"ruflo-backed-task"},{"command":"scout","description":"Performs deep research investigations using web search to gather and synthesize comprehensive information on any topic","centralized":true,"decentralized":false,"source_file":"scout"},{"command":"security-review","description":"Security-focused AI agent that reviews pull requests to identify changes that could weaken security posture or extend AWF boundaries","centralized":true,"decentralized":false,"source_file":"security-review"},{"command":"smoke-agent-all-merged","description":"Guard policy smoke test: repos=all, min-integrity=merged (most restrictive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-merged"},{"command":"smoke-agent-all-none","description":"Guard policy smoke test: repos=all, min-integrity=none (most permissive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-none"},{"command":"smoke-agent-public-approved","description":"Smoke test that validates assign-to-agent with the agentic-workflows custom agent","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-approved"},{"command":"smoke-agent-public-none","description":"Guard policy smoke test: repos=public, min-integrity=none","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-none"},{"command":"smoke-agent-scoped-approved","description":"Guard policy smoke test: repos=[github/gh-aw, github/*], min-integrity=approved (scoped patterns)","centralized":true,"decentralized":false,"source_file":"smoke-agent-scoped-approved"},{"command":"smoke-aider","description":"Smoke test workflow that validates Aider engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-aider"},{"command":"smoke-call-workflow","description":"Smoke test for the call-workflow safe output - orchestrator that calls a worker via workflow_call at compile-time fan-out","centralized":true,"decentralized":false,"source_file":"smoke-call-workflow"},{"command":"smoke-checkout-pr-dispatch","description":"Integration test validating that workflow_dispatch events with aw_context.item_type == ''pull_request'' correctly check out the PR branch","centralized":true,"decentralized":false,"source_file":"smoke-checkout-pr-dispatch"},{"command":"smoke-claude","description":"Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-claude"},{"command":"smoke-claude-on-copilot","description":"Smoke test for Claude engine on GitHub Inference that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-claude-on-copilot"},{"command":"smoke-codex","description":"Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-codex"},{"command":"smoke-copilot","description":"Smoke Copilot","centralized":true,"decentralized":false,"source_file":"smoke-copilot"},{"command":"smoke-copilot-aoai-apikey","description":"Smoke Copilot - AOAI (apikey)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-copilot-aoai-entra","description":"Smoke Copilot - AOAI (Entra)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-entra"},{"command":"smoke-copilot-arm","description":"Smoke Copilot ARM64","centralized":true,"decentralized":false,"source_file":"smoke-copilot-arm"},{"command":"smoke-copilot-mai","description":"Smoke test for MAI-Code-1-Flash (mai-code-1-flash-picker) โ€” pricing: $0.75/M input, $0.075/M cached, $4.50/M output","centralized":true,"decentralized":false,"source_file":"smoke-copilot-mai"},{"command":"smoke-copilot-sdk","description":"Smoke Copilot SDK","centralized":true,"decentralized":false,"source_file":"smoke-copilot-sdk"},{"command":"smoke-copilot-small","description":"Smoke Copilot Small","centralized":true,"decentralized":false,"source_file":"smoke-copilot-small"},{"command":"smoke-create-cross-repo-pr","description":"Smoke test validating cross-repo pull request creation in github/gh-aw-side-repo","centralized":true,"decentralized":false,"source_file":"smoke-create-cross-repo-pr"},{"command":"smoke-crush","description":"Smoke test workflow that validates Crush engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-crush"},{"command":"smoke-cursor","description":"Smoke test workflow that validates Cursor engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-cursor"},{"command":"smoke-deepseek-harness","description":"Smoke test workflow that validates DeepSeek Harness engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-deepseek-harness"},{"command":"smoke-gemini","description":"Smoke test workflow that validates Gemini engine functionality twice daily","centralized":true,"decentralized":false,"source_file":"smoke-gemini"},{"command":"smoke-github-claude","description":"Smoke test for Claude engine using GitHub provider that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-github-claude"},{"command":"smoke-goose","description":"Smoke test workflow that validates Goose engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-goose"},{"command":"smoke-kiro","description":"Smoke test workflow that validates Kiro engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-kiro"},{"command":"smoke-multi-pr","description":"Test creating multiple pull requests in a single workflow run","centralized":true,"decentralized":false,"source_file":"smoke-multi-pr"},{"command":"smoke-opencode","description":"Smoke test workflow that validates OpenCode engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-opencode"},{"command":"smoke-otel-backends","description":"Smoke test that validates OTEL span export and query access for Sentry, Grafana, and Datadog","centralized":true,"decentralized":false,"source_file":"smoke-otel-backends"},{"command":"smoke-pi","description":"Smoke test workflow that validates Pi engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pi"},{"command":"smoke-project","description":"Smoke Project - Test project operations","centralized":true,"decentralized":false,"source_file":"smoke-project"},{"command":"smoke-pydantic","description":"Smoke test workflow that validates Pydantic AI engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pydantic"},{"command":"smoke-service-ports","description":"Smoke test to validate --allow-host-service-ports with Redis service container","centralized":true,"decentralized":false,"source_file":"smoke-service-ports"},{"command":"smoke-temporary-id","description":"Test temporary ID functionality for issue chaining and cross-references","centralized":true,"decentralized":false,"source_file":"smoke-temporary-id"},{"command":"smoke-test-tools","description":"Smoke test to validate common development tools are available in the agent container","centralized":true,"decentralized":false,"source_file":"smoke-test-tools"},{"command":"smoke-update-cross-repo-pr","description":"Smoke test validating cross-repo pull request updates in github/gh-aw-side-repo by adding lines from Homer''s Odyssey to the README","centralized":true,"decentralized":false,"source_file":"smoke-update-cross-repo-pr"},{"command":"souschef","description":"Keeps open non-draft PRs moving toward maintainer investigation by posting targeted Copilot nudges","centralized":true,"decentralized":false,"source_file":"pr-sous-chef"},{"command":"squad","description":"Cast, connect, or adopt a Squad AI team for your repository","centralized":false,"decentralized":true,"source_file":"squad"},{"command":"squad-plan","description":"Uses Squad to plan an issue from the /squad-plan slash command and create Copilot-ready sub-issues","centralized":true,"decentralized":false,"source_file":"squad-plan"},{"command":"summarize","description":"pdf summarizer","centralized":true,"decentralized":false,"source_file":"pdf-summary"},{"command":"tidy","description":"Automatically formats and tidies code files (Go, JS, TypeScript) on schedule or command","centralized":true,"decentralized":false,"source_file":"tidy"},{"command":"unbloat","description":"Reviews and simplifies documentation by reducing verbosity while maintaining clarity and completeness","centralized":true,"decentralized":false,"source_file":"unbloat-docs"},{"command":"approach-proposal","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"ci-doctor","description":"Investigates failed CI workflows to identify root causes and patterns, creating issues with diagnostic information; also reviews PR check failures when the ci-doctor label is applied","centralized":false,"decentralized":false,"label":true,"source_file":"ci-doctor"},{"command":"cloclo","centralized":false,"decentralized":false,"label":true,"source_file":"cloclo"},{"command":"dev","description":"Daily status report for gh-aw project","centralized":false,"decentralized":false,"label":true,"source_file":"dev"},{"command":"necromancer","description":"Investigates merge-ready pull requests, traces root-cause issues, and adds regression tests before merge","centralized":false,"decentralized":false,"label":true,"source_file":"necromancer"},{"command":"needs-design","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"smoke","description":"Smoke Copilot - AOAI (apikey)","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-sdk","description":"Smoke Copilot SDK","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-sdk"}]' + GH_AW_HELP_COMMANDS: '[{"command":"*","description":"Reviews pull requests by mapping any slash command to a matching repository skill under .github/skills","centralized":true,"decentralized":false,"source_file":"skillet"},{"command":"ace","description":"Generates an ACE editor session link when invoked with /ace command on pull request comments","centralized":true,"decentralized":false,"source_file":"ace-editor"},{"command":"approach-validator","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":true,"decentralized":false,"source_file":"approach-validator"},{"command":"archie","description":"Generates Mermaid diagrams to visualize issue and pull request relationships when invoked with the /archie command","centralized":true,"decentralized":false,"source_file":"archie"},{"command":"cloclo","centralized":true,"decentralized":false,"source_file":"cloclo"},{"command":"craft","description":"Generates new agentic workflow markdown files based on user requests when invoked with /craft command","centralized":true,"decentralized":false,"source_file":"craft"},{"command":"dependabot-burner","description":"Runs one grouped Dependabot remediation wave from schedule, manual dispatch, or /dependabot-burner on pull requests","centralized":true,"decentralized":false,"source_file":"dependabot-burner"},{"command":"grumpy","description":"โš ๏ธ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Performs critical code review with a focus on edge cases, potential bugs, and code quality issues","centralized":true,"decentralized":false,"source_file":"grumpy-reviewer"},{"command":"matt","description":"Reviews pull requests using Matt Pocock''s engineering skills to provide targeted, high-quality improvement suggestions based on the type of changes","centralized":true,"decentralized":false,"source_file":"mattpocock-skills-reviewer"},{"command":"mergefest","description":"Automatically merges the main branch into pull request branches when invoked with /mergefest command","centralized":true,"decentralized":false,"source_file":"mergefest"},{"command":"nit","description":"โš ๏ธ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Provides detailed nitpicky code review focusing on style, best practices, and minor improvements","centralized":true,"decentralized":false,"source_file":"pr-nitpick-reviewer"},{"command":"plan","description":"Generates project plans and task breakdowns when invoked with /plan command in issues or PRs","centralized":true,"decentralized":false,"source_file":"plan"},{"command":"poem-bot","description":"Generates creative poems on specified themes when invoked with /poem-bot command","centralized":true,"decentralized":false,"source_file":"poem-bot"},{"command":"ponytail","description":"Reviews pull requests for unnecessary complexity using Ponytail","centralized":true,"decentralized":false,"source_file":"ponytail-reviewer"},{"command":"q","description":"Intelligent assistant that answers questions, analyzes repositories, and can create PRs for workflow optimizations","centralized":false,"decentralized":true,"source_file":"q"},{"command":"review","description":"Enforces Architecture Decision Records (ADRs) before implementation work can merge, detecting missing design decisions and generating draft ADRs using AI analysis","centralized":true,"decentralized":false,"source_file":"design-decision-gate"},{"command":"ruflo","description":"Runs a repository task inside GitHub Agentic Workflows while delegating inner planning and coordination to Ruflo","centralized":true,"decentralized":false,"source_file":"ruflo-backed-task"},{"command":"scout","description":"Performs deep research investigations using web search to gather and synthesize comprehensive information on any topic","centralized":true,"decentralized":false,"source_file":"scout"},{"command":"security-review","description":"Security-focused AI agent that reviews pull requests to identify changes that could weaken security posture or extend AWF boundaries","centralized":true,"decentralized":false,"source_file":"security-review"},{"command":"smoke-agent-all-merged","description":"Guard policy smoke test: repos=all, min-integrity=merged (most restrictive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-merged"},{"command":"smoke-agent-all-none","description":"Guard policy smoke test: repos=all, min-integrity=none (most permissive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-none"},{"command":"smoke-agent-public-approved","description":"Smoke test that validates assign-to-agent with the agentic-workflows custom agent","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-approved"},{"command":"smoke-agent-public-none","description":"Guard policy smoke test: repos=public, min-integrity=none","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-none"},{"command":"smoke-agent-scoped-approved","description":"Guard policy smoke test: repos=[github/gh-aw, github/*], min-integrity=approved (scoped patterns)","centralized":true,"decentralized":false,"source_file":"smoke-agent-scoped-approved"},{"command":"smoke-aider","description":"Smoke test workflow that validates Aider engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-aider"},{"command":"smoke-call-workflow","description":"Smoke test for the call-workflow safe output - orchestrator that calls a worker via workflow_call at compile-time fan-out","centralized":true,"decentralized":false,"source_file":"smoke-call-workflow"},{"command":"smoke-checkout-pr-dispatch","description":"Integration test validating that workflow_dispatch events with aw_context.item_type == ''pull_request'' correctly check out the PR branch","centralized":true,"decentralized":false,"source_file":"smoke-checkout-pr-dispatch"},{"command":"smoke-claude","description":"Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-claude"},{"command":"smoke-claude-on-copilot","description":"Smoke test for Claude engine on GitHub Inference that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-claude-on-copilot"},{"command":"smoke-codex","description":"Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-codex"},{"command":"smoke-copilot","description":"Smoke Copilot","centralized":true,"decentralized":false,"source_file":"smoke-copilot"},{"command":"smoke-copilot-aoai-apikey","description":"Smoke Copilot - AOAI (apikey)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-copilot-aoai-entra","description":"Smoke Copilot - AOAI (Entra)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-entra"},{"command":"smoke-copilot-arm","description":"Smoke Copilot ARM64","centralized":true,"decentralized":false,"source_file":"smoke-copilot-arm"},{"command":"smoke-copilot-mai","description":"Smoke test for MAI-Code-1-Flash (mai-code-1-flash-picker) โ€” pricing: $0.75/M input, $0.075/M cached, $4.50/M output","centralized":true,"decentralized":false,"source_file":"smoke-copilot-mai"},{"command":"smoke-copilot-sdk","description":"Smoke Copilot SDK","centralized":true,"decentralized":false,"source_file":"smoke-copilot-sdk"},{"command":"smoke-copilot-small","description":"Smoke Copilot Small","centralized":true,"decentralized":false,"source_file":"smoke-copilot-small"},{"command":"smoke-create-cross-repo-pr","description":"Smoke test validating cross-repo pull request creation in github/gh-aw-side-repo","centralized":true,"decentralized":false,"source_file":"smoke-create-cross-repo-pr"},{"command":"smoke-crush","description":"Smoke test workflow that validates Crush engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-crush"},{"command":"smoke-cursor","description":"Smoke test workflow that validates Cursor engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-cursor"},{"command":"smoke-deepseek-harness","description":"Smoke test workflow that validates DeepSeek Harness engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-deepseek-harness"},{"command":"smoke-drive","description":"Smoke test workflow that validates experimental GitHub Drives memory","centralized":true,"decentralized":false,"source_file":"smoke-drive"},{"command":"smoke-gemini","description":"Smoke test workflow that validates Gemini engine functionality twice daily","centralized":true,"decentralized":false,"source_file":"smoke-gemini"},{"command":"smoke-github-claude","description":"Smoke test for Claude engine using GitHub provider that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-github-claude"},{"command":"smoke-goose","description":"Smoke test workflow that validates Goose engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-goose"},{"command":"smoke-kiro","description":"Smoke test workflow that validates Kiro engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-kiro"},{"command":"smoke-multi-pr","description":"Test creating multiple pull requests in a single workflow run","centralized":true,"decentralized":false,"source_file":"smoke-multi-pr"},{"command":"smoke-opencode","description":"Smoke test workflow that validates OpenCode engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-opencode"},{"command":"smoke-otel-backends","description":"Smoke test that validates OTEL span export and query access for Sentry, Grafana, and Datadog","centralized":true,"decentralized":false,"source_file":"smoke-otel-backends"},{"command":"smoke-pi","description":"Smoke test workflow that validates Pi engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pi"},{"command":"smoke-project","description":"Smoke Project - Test project operations","centralized":true,"decentralized":false,"source_file":"smoke-project"},{"command":"smoke-pydantic","description":"Smoke test workflow that validates Pydantic AI engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pydantic"},{"command":"smoke-service-ports","description":"Smoke test to validate --allow-host-service-ports with Redis service container","centralized":true,"decentralized":false,"source_file":"smoke-service-ports"},{"command":"smoke-temporary-id","description":"Test temporary ID functionality for issue chaining and cross-references","centralized":true,"decentralized":false,"source_file":"smoke-temporary-id"},{"command":"smoke-test-tools","description":"Smoke test to validate common development tools are available in the agent container","centralized":true,"decentralized":false,"source_file":"smoke-test-tools"},{"command":"smoke-update-cross-repo-pr","description":"Smoke test validating cross-repo pull request updates in github/gh-aw-side-repo by adding lines from Homer''s Odyssey to the README","centralized":true,"decentralized":false,"source_file":"smoke-update-cross-repo-pr"},{"command":"souschef","description":"Keeps open non-draft PRs moving toward maintainer investigation by posting targeted Copilot nudges","centralized":true,"decentralized":false,"source_file":"pr-sous-chef"},{"command":"squad","description":"Cast, connect, or adopt a Squad AI team for your repository","centralized":false,"decentralized":true,"source_file":"squad"},{"command":"squad-plan","description":"Uses Squad to plan an issue from the /squad-plan slash command and create Copilot-ready sub-issues","centralized":true,"decentralized":false,"source_file":"squad-plan"},{"command":"summarize","description":"pdf summarizer","centralized":true,"decentralized":false,"source_file":"pdf-summary"},{"command":"tidy","description":"Automatically formats and tidies code files (Go, JS, TypeScript) on schedule or command","centralized":true,"decentralized":false,"source_file":"tidy"},{"command":"unbloat","description":"Reviews and simplifies documentation by reducing verbosity while maintaining clarity and completeness","centralized":true,"decentralized":false,"source_file":"unbloat-docs"},{"command":"approach-proposal","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"ci-doctor","description":"Investigates failed CI workflows to identify root causes and patterns, creating issues with diagnostic information; also reviews PR check failures when the ci-doctor label is applied","centralized":false,"decentralized":false,"label":true,"source_file":"ci-doctor"},{"command":"cloclo","centralized":false,"decentralized":false,"label":true,"source_file":"cloclo"},{"command":"dev","description":"Daily status report for gh-aw project","centralized":false,"decentralized":false,"label":true,"source_file":"dev"},{"command":"necromancer","description":"Investigates merge-ready pull requests, traces root-cause issues, and adds regression tests before merge","centralized":false,"decentralized":false,"label":true,"source_file":"necromancer"},{"command":"needs-design","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"smoke","description":"Smoke Copilot - AOAI (apikey)","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-sdk","description":"Smoke Copilot SDK","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-sdk"}]' GH_AW_HELP_COMMAND_ENABLED: 'true' GH_AW_SLASH_COMMAND_DOCS_URL: 'https://github.github.com/gh-aw/reference/command-triggers/' with: diff --git a/actions/setup/js/add_reaction_and_edit_comment.cjs b/actions/setup/js/add_reaction_and_edit_comment.cjs index d376d83386f..7ab23af7e0d 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.cjs @@ -28,9 +28,24 @@ function expectRestEndpoint(endpoint, endpointName, eventName) { if (!isRestEndpoint(endpoint)) { throw new Error(`${ERR_VALIDATION}: Unexpected ${endpointName} endpoint shape for event: ${eventName}`); } + return endpoint; } +/** + * @param {string} endpoint + * @param {"discussion"|"discussion_comment"} eventName + * @returns {number} + */ +function parseDiscussionEndpoint(endpoint, eventName) { + const match = endpoint.match(eventName === "discussion" ? /^discussion:([1-9]\d*)$/ : /^discussion_comment:([1-9]\d*):[1-9]\d*$/); + const discussionNumber = Number(match?.[1]); + if (!Number.isSafeInteger(discussionNumber)) { + throw new Error(`${ERR_VALIDATION}: Invalid discussion endpoint: ${endpoint}`); + } + return discussionNumber; +} + /** * Resolve the reaction and comment API endpoints for a given event. * Returns null (after calling core.setFailed) when the event or payload is invalid. @@ -262,8 +277,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio if (typeof endpoint !== "string") { throw new Error(`${ERR_VALIDATION}: Unexpected comment endpoint shape for event: ${eventName}`); } - // Parse discussion number from special format: "discussion:NUMBER" or "discussion_comment:NUMBER:COMMENT_ID" - const discussionNumber = parseInt(endpoint.split(":")[1], 10); + const discussionNumber = parseDiscussionEndpoint(endpoint, eventName); const discussionId = await getDiscussionNodeId(eventRepo.owner, eventRepo.repo, discussionNumber); // For discussion_comment events, thread the reply under the triggering comment. // GitHub Discussions only supports two nesting levels, so resolve the top-level parent node ID. @@ -291,4 +305,4 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio } } -module.exports = { main, addCommentWithWorkflowLink, resolveEventEndpoints, VALID_REACTIONS, addReaction, addDiscussionReaction, expectRestEndpoint }; +module.exports = { main, addCommentWithWorkflowLink, resolveEventEndpoints, VALID_REACTIONS, addReaction, addDiscussionReaction, expectRestEndpoint, parseDiscussionEndpoint }; diff --git a/actions/setup/js/add_reaction_and_edit_comment.test.cjs b/actions/setup/js/add_reaction_and_edit_comment.test.cjs index 2a43a4a43b8..d4dc1a19e18 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.test.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.test.cjs @@ -38,8 +38,8 @@ global.context = mockContext; // Helper to import the module fresh (bust module cache) async function loadModule() { - const { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS, expectRestEndpoint } = await import("./add_reaction_and_edit_comment.cjs?" + Date.now()); - return { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS, expectRestEndpoint }; + const { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS, expectRestEndpoint, parseDiscussionEndpoint } = await import("./add_reaction_and_edit_comment.cjs?" + Date.now()); + return { main, addCommentWithWorkflowLink, addReaction, addDiscussionReaction, resolveEventEndpoints, VALID_REACTIONS, expectRestEndpoint, parseDiscussionEndpoint }; } describe("add_reaction_and_edit_comment.cjs", () => { @@ -72,6 +72,17 @@ describe("add_reaction_and_edit_comment.cjs", () => { }); }); + describe("discussion endpoint validation", () => { + it("rejects malformed and non-positive discussion endpoint numbers", async () => { + const { parseDiscussionEndpoint } = await loadModule(); + + for (const endpoint of ["discussion:5junk", "discussion:0", "discussion:5:extra"]) { + expect(() => parseDiscussionEndpoint(endpoint, "discussion")).toThrow("Invalid discussion endpoint"); + } + expect(() => parseDiscussionEndpoint("discussion_comment:5:2junk", "discussion_comment")).toThrow("Invalid discussion endpoint"); + }); + }); + describe("Issue reactions", () => { it("should add reaction to issue successfully", async () => { process.env.GH_AW_REACTION = "eyes"; diff --git a/actions/setup/js/add_workflow_run_comment.cjs b/actions/setup/js/add_workflow_run_comment.cjs index 15125f034c9..101666df84d 100644 --- a/actions/setup/js/add_workflow_run_comment.cjs +++ b/actions/setup/js/add_workflow_run_comment.cjs @@ -53,6 +53,7 @@ function setCommentOutputs(commentId, commentUrl, eventRepo = context.repo, opti } else { core.info(`Successfully created comment with workflow link`); } + core.info(`Comment ID: ${commentId}`); core.info(`Comment URL: ${commentUrl}`); core.info(`Comment Repo: ${eventRepo.owner}/${eventRepo.repo}`); @@ -66,6 +67,20 @@ function setCommentOutputs(commentId, commentUrl, eventRepo = context.repo, opti }; } +/** + * @param {string} endpoint + * @param {"discussion"|"discussion_comment"} eventName + * @returns {number} + */ +function parseDiscussionEndpoint(endpoint, eventName) { + const match = endpoint.match(eventName === "discussion" ? /^discussion:([1-9]\d*)$/ : /^discussion_comment:([1-9]\d*):[1-9]\d*$/); + const discussionNumber = Number(match?.[1]); + if (!Number.isSafeInteger(discussionNumber)) { + throw new Error(`${ERR_VALIDATION}: Invalid discussion endpoint: ${endpoint}`); + } + return discussionNumber; +} + /** * @param {unknown} value * @returns {Record|null} @@ -418,8 +433,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio if (typeof endpoint !== "string") { throw new Error(`${ERR_VALIDATION}: Unexpected comment endpoint shape for event: ${eventName}`); } - // Parse discussion number from special format: "discussion:NUMBER" - const discussionNumber = parseInt(endpoint.split(":")[1], 10); + const discussionNumber = parseDiscussionEndpoint(endpoint, eventName); return postDiscussionComment(discussionNumber, commentBody, null, eventRepo); } @@ -427,8 +441,7 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio if (typeof endpoint !== "string") { throw new Error(`${ERR_VALIDATION}: Unexpected comment endpoint shape for event: ${eventName}`); } - // Parse discussion number from special format: "discussion_comment:NUMBER:COMMENT_ID" - const discussionNumber = parseInt(endpoint.split(":")[1], 10); + const discussionNumber = parseDiscussionEndpoint(endpoint, eventName); // GitHub Discussions only supports two nesting levels, so resolve the top-level parent's node ID const commentNodeId = await resolveTopLevelDiscussionCommentId(github, eventPayload?.comment?.node_id); @@ -449,4 +462,4 @@ async function addCommentWithWorkflowLink(endpoint, runUrl, eventName, invocatio return setCommentOutputs(createResponse.data.id, createResponse.data.html_url, eventRepo); } -module.exports = { main, addCommentWithWorkflowLink, buildCommentBody, postDiscussionComment, createOrReuseStatusComment }; +module.exports = { main, addCommentWithWorkflowLink, buildCommentBody, postDiscussionComment, createOrReuseStatusComment, parseDiscussionEndpoint }; diff --git a/actions/setup/js/add_workflow_run_comment.test.cjs b/actions/setup/js/add_workflow_run_comment.test.cjs index ac45823fd91..cf57a8dffdf 100644 --- a/actions/setup/js/add_workflow_run_comment.test.cjs +++ b/actions/setup/js/add_workflow_run_comment.test.cjs @@ -102,6 +102,17 @@ describe("add_workflow_run_comment", () => { return import("./add_workflow_run_comment.cjs?test=" + importCounter); } + describe("discussion endpoint validation", () => { + it("rejects malformed and non-positive discussion endpoint numbers", async () => { + const { parseDiscussionEndpoint } = await importAddWorkflowRunComment(); + + for (const endpoint of ["discussion:12junk", "discussion:0", "discussion:12:extra"]) { + expect(() => parseDiscussionEndpoint(endpoint, "discussion")).toThrow("Invalid discussion endpoint"); + } + expect(() => parseDiscussionEndpoint("discussion_comment:5junk:2", "discussion_comment")).toThrow("Invalid discussion endpoint"); + }); + }); + // Helper function to run the script async function runScript() { const { main } = await importAddWorkflowRunComment(); diff --git a/actions/setup/js/artifact_client.cjs b/actions/setup/js/artifact_client.cjs index 3ebafb19327..9e44930438c 100644 --- a/actions/setup/js/artifact_client.cjs +++ b/actions/setup/js/artifact_client.cjs @@ -33,6 +33,30 @@ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +async function readResponseText(response, context) { + try { + return await response.text(); + } catch (error) { + throw new Error(`failed to read ${context} response body: ${getErrorMessage(error)}`, { cause: error }); + } +} + +async function readResponseJSON(response, context) { + try { + return await response.json(); + } catch (error) { + throw new Error(`failed to parse ${context} response body: ${getErrorMessage(error)}`, { cause: error }); + } +} + +function makeTempDir(prefix) { + try { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + } catch (error) { + throw new Error(`failed to create temporary directory for ${prefix}: ${getErrorMessage(error)}`, { cause: error }); + } +} + function parseURL(url, base, errorMessage) { try { return base === undefined ? new URL(url) : new URL(url, base); @@ -108,10 +132,10 @@ async function twirpRequest(method, body) { }); if (response.ok) { - return await response.json(); + return await readResponseJSON(response, `artifact twirp ${method}`); } - const responseBody = await response.text(); + const responseBody = await readResponseText(response, `artifact twirp ${method}`); const retryable = response.status >= 500 || response.status === 429; if (!retryable || attempt === DEFAULT_RETRY_ATTEMPTS) { throw new Error(`artifact twirp ${method} failed (${response.status}): ${responseBody || response.statusText}`); @@ -238,7 +262,7 @@ async function uploadFileToSignedURL(filePath, signedUploadURL, contentType) { throw new Error(`artifact blob upload failed: ${getErrorMessage(err)}`, { cause: err }); } if (!response.ok) { - const body = await response.text(); + const body = await readResponseText(response, "artifact blob upload"); throw new Error(`artifact blob upload failed (${response.status}): ${body || response.statusText}`); } return stats.size; @@ -293,10 +317,10 @@ class DefaultArtifactClient { throw new Error(`failed to list artifacts: ${getErrorMessage(err)}`, { cause: err }); } if (!response.ok) { - throw new Error(`failed to list artifacts (${response.status}): ${await response.text()}`); + throw new Error(`failed to list artifacts (${response.status}): ${await readResponseText(response, "list artifacts")}`); } /** @type {any} */ - const payload = await response.json(); + const payload = await readResponseJSON(response, "list artifacts"); const pageArtifacts = Array.isArray(payload?.artifacts) ? payload.artifacts : []; for (const item of pageArtifacts) { artifacts.push({ @@ -372,7 +396,7 @@ class DefaultArtifactClient { const zipLike = isZipResponse(location, contentType); if (zipLike && !options.skipDecompress) { ensureUnzipAvailable(); - const tempDownloadDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-download-")); + const tempDownloadDir = makeTempDir("gh-aw-artifact-download-"); const tempZip = path.join(tempDownloadDir, "artifact.zip"); try { digest = await streamToFile(blobResponse, tempZip); @@ -420,7 +444,7 @@ class DefaultArtifactClient { uploadPath = files[0]; contentType = "application/octet-stream"; } else { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-upload-")); + tmpDir = makeTempDir("gh-aw-artifact-upload-"); uploadPath = path.join(tmpDir, `${artifactName || "artifact"}.zip`); createZipFromFiles(files, rootDirectory, uploadPath); } diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index 14f6d6b9b71..ac48a2274e4 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -748,7 +748,12 @@ function endpointBaseUrl(endpoint) { * @returns {string} */ function deriveBaseUrlFromModelsURL(modelsUrl, env = process.env, readFileSync = fs.readFileSync) { - const parsed = new URL(modelsUrl); + let parsed; + try { + parsed = new URL(modelsUrl); + } catch (error) { + throw new Error(`Invalid models URL: ${modelsUrl}`, { cause: error }); + } const basePath = parsed.pathname.replace(/\/models\/?$/i, ""); return rewriteAPIProxyURLForHostBridge(`${parsed.origin}${basePath}`, env, readFileSync); } diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs index 6fac8a69773..7a1e1bd75c2 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs @@ -277,7 +277,12 @@ async function getRunAIC(artifactClient, runId, token, owner, repo) { artifactId: artifact.id, artifactName: artifact.name, }); - const downloadRoot = fs.mkdtempSync(path.join(os.tmpdir(), `gh-aw-daily-guardrail-${runId}-`)); + let downloadRoot; + try { + downloadRoot = fs.mkdtempSync(path.join(os.tmpdir(), `gh-aw-daily-guardrail-${runId}-`)); + } catch (error) { + throw new Error(`Failed to create temporary artifact directory for run ${runId}: ${getErrorMessage(error)}`, { cause: error }); + } const download = await artifactClient.downloadArtifact(artifact.id, { path: downloadRoot, findBy: { diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs index 8e794667d4e..00003165f12 100644 --- a/actions/setup/js/check_rate_limit.cjs +++ b/actions/setup/js/check_rate_limit.cjs @@ -43,8 +43,11 @@ async function main() { // Get configuration from environment variables // Use .trim() + || so that empty/whitespace-only values also fall back to defaults - const maxRuns = parseInt(process.env.GH_AW_RATE_LIMIT_MAX?.trim() || "5", 10); - const windowMinutes = parseInt(process.env.GH_AW_RATE_LIMIT_WINDOW?.trim() || "60", 10); + const maxRuns = Number(process.env.GH_AW_RATE_LIMIT_MAX?.trim() || "5"); + const windowMinutes = Number(process.env.GH_AW_RATE_LIMIT_WINDOW?.trim() || "60"); + if (!Number.isFinite(maxRuns) || !Number.isSafeInteger(maxRuns) || maxRuns <= 0 || !Number.isFinite(windowMinutes) || !Number.isSafeInteger(windowMinutes) || windowMinutes <= 0) { + throw new Error("Rate limit maximum and window must be positive integers"); + } const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS?.trim() || ""; // Default: admin, maintain, and write roles are exempt from rate limiting const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES?.trim() || "admin,maintain,write"; @@ -109,8 +112,8 @@ async function main() { // Calculate time threshold const windowMs = windowMinutes * 60 * 1000; - const thresholdTime = new Date(Date.now() - windowMs); - const thresholdISO = thresholdTime.toISOString(); + const thresholdTimestamp = Date.now() - windowMs; + const thresholdISO = new Date(thresholdTimestamp).toISOString(); core.info(` Time window: runs created after ${thresholdISO}`); @@ -158,7 +161,11 @@ async function main() { // Stop if run is older than the time window (runs are newest-first) const runCreatedAt = new Date(run.created_at); - if (runCreatedAt < thresholdTime) { + if (Number.isNaN(runCreatedAt.getTime())) { + core.warning(`Skipping run ${run.id} with invalid creation date`); + continue; + } + if (runCreatedAt.getTime() < thresholdTimestamp) { core.info(` Stopping pagination - run ${run.id} created before threshold (${run.created_at})`); hasMore = false; break; diff --git a/actions/setup/js/check_runs_helpers.cjs b/actions/setup/js/check_runs_helpers.cjs index b121691de0b..fd12deebeec 100644 --- a/actions/setup/js/check_runs_helpers.cjs +++ b/actions/setup/js/check_runs_helpers.cjs @@ -40,7 +40,20 @@ function selectLatestRelevantChecks(checkRuns, options = {}) { continue; } const existing = latestByName.get(run.name); - if (!existing || new Date(run.started_at ?? 0) > new Date(existing.started_at ?? 0)) { + if (!existing) { + latestByName.set(run.name, run); + continue; + } + const runStartedAt = Date.parse(run.started_at ?? ""); + const existingStartedAt = Date.parse(existing.started_at ?? ""); + if (!Number.isFinite(runStartedAt)) { + continue; + } + if (!Number.isFinite(existingStartedAt)) { + latestByName.set(run.name, run); + continue; + } + if (runStartedAt > existingStartedAt) { latestByName.set(run.name, run); } } diff --git a/actions/setup/js/check_runs_helpers.test.cjs b/actions/setup/js/check_runs_helpers.test.cjs index 325f7111312..38413fff185 100644 --- a/actions/setup/js/check_runs_helpers.test.cjs +++ b/actions/setup/js/check_runs_helpers.test.cjs @@ -40,6 +40,14 @@ describe("check_runs_helpers", () => { expect(ci?.id).toBe(2); }); + it("replaces an invalid timestamp with a valid later same-name run", () => { + const { relevant } = selectLatestRelevantChecks([ + { id: 1, name: "CI", started_at: null, app: { slug: "github-actions" } }, + { id: 2, name: "CI", started_at: "2024-01-02T00:00:00Z", app: { slug: "github-actions" } }, + ]); + expect(relevant.find(r => r.name === "CI")?.id).toBe(2); + }); + it("excludes deployment checks and reports count", () => { const { relevant, deploymentCheckCount } = selectLatestRelevantChecks(runs); expect(relevant.every(r => r.app?.slug !== "github-deployments")).toBe(true); diff --git a/actions/setup/js/create_prompt.cjs b/actions/setup/js/create_prompt.cjs index 493930a5557..3bca7beb1eb 100644 --- a/actions/setup/js/create_prompt.cjs +++ b/actions/setup/js/create_prompt.cjs @@ -28,7 +28,7 @@ function parseConfig(value) { try { parsed = JSON.parse(value); } catch (error) { - throw new Error(`${ERR_PARSE}: Invalid GH_AW_PROMPT_CONFIG: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Invalid GH_AW_PROMPT_CONFIG: ${getErrorMessage(error)}`, { cause: error }); } if (!parsed || !Array.isArray(parsed.items)) { throw new Error(`${ERR_CONFIG}: GH_AW_PROMPT_CONFIG must contain an items array`); @@ -83,7 +83,11 @@ function writePromptFile(promptPath, content) { const fd = fs.openSync(promptPath, flags, 0o600); try { fs.fchmodSync(fd, 0o600); - fs.writeFileSync(fd, content, "utf8"); + try { + fs.writeFileSync(fd, content, "utf8"); + } catch (error) { + throw new Error(`${ERR_SYSTEM}: Failed to write prompt file ${promptPath}: ${getErrorMessage(error)}`, { cause: error }); + } } finally { fs.closeSync(fd); } @@ -131,7 +135,7 @@ function renderPrompt(config, env, promptsDir) { try { result += fs.readFileSync(promptFile, "utf8"); } catch (error) { - throw new Error(`${ERR_SYSTEM}: Failed to read prompt file: ${getErrorMessage(error)}`); + throw new Error(`${ERR_SYSTEM}: Failed to read prompt file: ${getErrorMessage(error)}`, { cause: error }); } } diff --git a/actions/setup/js/data_schema_normalizer.cjs b/actions/setup/js/data_schema_normalizer.cjs index 4a5891b6c3d..4b662762bc9 100644 --- a/actions/setup/js/data_schema_normalizer.cjs +++ b/actions/setup/js/data_schema_normalizer.cjs @@ -152,7 +152,12 @@ function resolveDataSchema(rawSchema, path) { return normalized; } if (typeof rawSchema === "string") { - const parsed = JSON.parse(rawSchema); + let parsed; + try { + parsed = JSON.parse(rawSchema); + } catch (error) { + throw new Error(`${path}: invalid JSON schema`, { cause: error }); + } if (!isPlainObject(parsed)) { throw new Error(`${path}: string JSON must decode to an object schema`); } diff --git a/actions/setup/js/evaluate_outcomes.cjs b/actions/setup/js/evaluate_outcomes.cjs index c33179cbdb8..a3c2390a14d 100644 --- a/actions/setup/js/evaluate_outcomes.cjs +++ b/actions/setup/js/evaluate_outcomes.cjs @@ -1222,7 +1222,12 @@ function isOnOrAfter(timestamp, threshold) { if (!threshold) return true; const a = Date.parse(timestamp); const b = Date.parse(threshold); - if (!Number.isFinite(a) || !Number.isFinite(b)) return false; + if (!Number.isFinite(a)) { + return false; + } + if (!Number.isFinite(b)) { + return false; + } return a >= b; } diff --git a/actions/setup/js/exchange_otlp_workload_identity.cjs b/actions/setup/js/exchange_otlp_workload_identity.cjs index 4502bc65a22..62ce3c856f4 100644 --- a/actions/setup/js/exchange_otlp_workload_identity.cjs +++ b/actions/setup/js/exchange_otlp_workload_identity.cjs @@ -31,18 +31,24 @@ async function main() { } maskSecret(oidcToken); - const response = await fetch("https://sts.googleapis.com/v1/token", { - method: "POST", - headers: { "content-type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", - requested_token_type: "urn:ietf:params:oauth:token-type:access_token", - subject_token_type: "urn:ietf:params:oauth:token-type:jwt", - subject_token: oidcToken, - audience: process.env.GH_AW_OTLP_WIF_AUDIENCE || "", - scope: CLOUD_PLATFORM_SCOPE, - }), - }); + let response; + try { + response = await fetch("https://sts.googleapis.com/v1/token", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", + requested_token_type: "urn:ietf:params:oauth:token-type:access_token", + subject_token_type: "urn:ietf:params:oauth:token-type:jwt", + subject_token: oidcToken, + audience: process.env.GH_AW_OTLP_WIF_AUDIENCE || "", + scope: CLOUD_PLATFORM_SCOPE, + }), + signal: AbortSignal.timeout(30_000), + }); + } catch (error) { + throw new Error("Google workload identity token exchange request failed", { cause: error }); + } if (!response.ok) { throw new Error( `Google workload identity token exchange failed with HTTP ${response.status} ${response.statusText}. Verify observability.otlp.workload-identity.audience matches the workload identity provider resource and that the provider trusts this repository` @@ -50,7 +56,12 @@ async function main() { } /** @type {any} */ - const stsPayload = await response.json(); + let stsPayload; + try { + stsPayload = await response.json(); + } catch (error) { + throw new Error("Failed to parse Google workload identity token exchange response", { cause: error }); + } let accessToken = stsPayload.access_token; if (!accessToken) { throw new Error("Google workload identity token exchange returned no access token"); @@ -59,18 +70,29 @@ async function main() { const serviceAccount = process.env.GH_AW_OTLP_WIF_SERVICE_ACCOUNT; if (serviceAccount) { - const impersonationResponse = await fetch(`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(serviceAccount)}:generateAccessToken`, { - method: "POST", - headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, - body: JSON.stringify({ scope: [CLOUD_PLATFORM_SCOPE] }), - }); + let impersonationResponse; + try { + impersonationResponse = await fetch(`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(serviceAccount)}:generateAccessToken`, { + method: "POST", + headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, + body: JSON.stringify({ scope: [CLOUD_PLATFORM_SCOPE] }), + signal: AbortSignal.timeout(30_000), + }); + } catch (error) { + throw new Error("Google service account impersonation request failed", { cause: error }); + } if (!impersonationResponse.ok) { throw new Error( `Google service account impersonation failed with HTTP ${impersonationResponse.status} ${impersonationResponse.statusText}. Verify observability.otlp.workload-identity.service-account exists and grants roles/iam.workloadIdentityUser to the federated principal` ); } /** @type {any} */ - const impersonationPayload = await impersonationResponse.json(); + let impersonationPayload; + try { + impersonationPayload = await impersonationResponse.json(); + } catch (error) { + throw new Error("Failed to parse Google service account impersonation response", { cause: error }); + } accessToken = impersonationPayload.accessToken; if (!accessToken) { throw new Error("Google service account impersonation returned no access token"); diff --git a/actions/setup/js/fuzz_template_substitution_harness.cjs b/actions/setup/js/fuzz_template_substitution_harness.cjs index eb3aa5a83c7..65999c1dbb0 100644 --- a/actions/setup/js/fuzz_template_substitution_harness.cjs +++ b/actions/setup/js/fuzz_template_substitution_harness.cjs @@ -63,7 +63,12 @@ function interpolateVariables(content, variables) { * @returns {Promise<{result: string, error: string | null, stages: {afterSubstitution: string, afterInterpolation: string, afterTemplate: string}}>} Test result */ async function testTemplateSubstitution(template, substitutions, variables) { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fuzz-template-")); + let tempDir; + try { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fuzz-template-")); + } catch (error) { + throw new Error("Failed to create fuzz harness temporary directory", { cause: error }); + } const testFile = path.join(tempDir, "test.txt"); try { diff --git a/actions/setup/js/generate_usage_activity_summary.cjs b/actions/setup/js/generate_usage_activity_summary.cjs index fa1a19b7bb9..a34fc768cae 100644 --- a/actions/setup/js/generate_usage_activity_summary.cjs +++ b/actions/setup/js/generate_usage_activity_summary.cjs @@ -489,7 +489,12 @@ function parseSafeOutputsManifest(manifestPath = MANIFEST_FILE_PATH) { // Let read errors propagate so the caller can distinguish "unreadable file" // from "file present but no items" โ€” both previously collapsed to null. - const content = fs.readFileSync(manifestPath, "utf-8"); + let content; + try { + content = fs.readFileSync(manifestPath, "utf-8"); + } catch (error) { + throw new Error(`Failed to read safe output manifest ${manifestPath}`, { cause: error }); + } const itemsByType = {}; let totalItems = 0; diff --git a/actions/setup/js/handle_noop_message.cjs b/actions/setup/js/handle_noop_message.cjs index bd6c59c4b7b..92847c98723 100644 --- a/actions/setup/js/handle_noop_message.cjs +++ b/actions/setup/js/handle_noop_message.cjs @@ -172,9 +172,13 @@ async function main() { return; } - const maxCount = parseInt(process.env.GH_AW_NOOP_MAX || "0", 10); + const maxCount = Number(process.env.GH_AW_NOOP_MAX || "0"); + if (!Number.isFinite(maxCount) || !Number.isSafeInteger(maxCount) || maxCount < 0) { + throw new Error(`${ERR_SYSTEM}: GH_AW_NOOP_MAX must be a non-negative integer`); + } + const limitedMaxCount = maxCount; const allNoopItems = (result.items || []).filter(/** @param {any} item */ item => item.type === "noop"); - const noopItems = maxCount > 0 ? allNoopItems.slice(0, maxCount) : allNoopItems; + const noopItems = limitedMaxCount > 0 ? allNoopItems.slice(0, limitedMaxCount) : allNoopItems; if (noopItems.length === 0) { core.info("No noop items found in agent output"); diff --git a/actions/setup/js/memory_custom_validation.cjs b/actions/setup/js/memory_custom_validation.cjs index 27314441279..a92f4cc67fc 100644 --- a/actions/setup/js/memory_custom_validation.cjs +++ b/actions/setup/js/memory_custom_validation.cjs @@ -11,6 +11,59 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const DEFAULT_VALIDATION_TIMEOUT_SECONDS = 60; const MAX_VALIDATION_OUTPUT_BYTES = 12 * 1024; +function removePath(targetPath, options) { + try { + fs.rmSync(targetPath, options); + } catch (error) { + throw new Error(`Failed to remove ${targetPath}: ${getErrorMessage(error)}`, { cause: error }); + } +} + +function makeDirectory(targetPath) { + try { + fs.mkdirSync(targetPath, { recursive: true }); + } catch (error) { + throw new Error(`Failed to create directory ${targetPath}: ${getErrorMessage(error)}`, { cause: error }); + } +} + +function writeFile(targetPath, content, options) { + try { + fs.writeFileSync(targetPath, content, options); + } catch (error) { + throw new Error(`Failed to write ${targetPath}: ${getErrorMessage(error)}`, { cause: error }); + } +} + +function readDirectory(targetPath) { + try { + return fs.readdirSync(targetPath, { withFileTypes: true }); + } catch (error) { + throw new Error(`Failed to read directory ${targetPath}: ${getErrorMessage(error)}`, { cause: error }); + } +} + +/** + * @param {string} targetPath + * @param {BufferEncoding | undefined} [encoding] + * @returns {any} + */ +function readFile(targetPath, encoding) { + try { + return fs.readFileSync(targetPath, encoding); + } catch (error) { + throw new Error(`Failed to read ${targetPath}: ${getErrorMessage(error)}`, { cause: error }); + } +} + +function makeTempDirectory() { + try { + return fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-memory-validation-")); + } catch (error) { + throw new Error(`Failed to create memory validation temporary directory: ${getErrorMessage(error)}`, { cause: error }); + } +} + /** * @param {string} value */ @@ -31,7 +84,7 @@ function getValidationMarkerPath(kind, memoryId) { * @param {string} memoryId */ function clearValidationMarker(kind, memoryId) { - fs.rmSync(getValidationMarkerPath(kind, memoryId), { force: true }); + removePath(getValidationMarkerPath(kind, memoryId), { force: true }); } /** @@ -40,8 +93,8 @@ function clearValidationMarker(kind, memoryId) { */ function writeValidationMarker(kind, memoryId) { const markerPath = getValidationMarkerPath(kind, memoryId); - fs.mkdirSync(path.dirname(markerPath), { recursive: true }); - fs.writeFileSync(markerPath, "ok\n", "utf8"); + makeDirectory(path.dirname(markerPath)); + writeFile(markerPath, "ok\n", "utf8"); return markerPath; } @@ -71,7 +124,7 @@ function formatJSONFiles(dirPath, maxFileSize) { * @param {string} currentDir */ function visit(currentDir) { - const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + const entries = readDirectory(currentDir); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { @@ -83,7 +136,7 @@ function formatJSONFiles(dirPath, maxFileSize) { if (!entry.isFile() || !entry.name.endsWith(".json")) { continue; } - const raw = fs.readFileSync(fullPath, "utf8"); + const raw = readFile(fullPath, "utf8"); if (!raw.trim()) { continue; } @@ -101,7 +154,7 @@ function formatJSONFiles(dirPath, maxFileSize) { if (formattedSize > maxFileSize) { throw new Error(`Formatted JSON exceeds max file size: ${path.relative(dirPath, fullPath)} (${formattedSize} bytes > ${maxFileSize} bytes)`); } - fs.writeFileSync(fullPath, formatted, "utf8"); + writeFile(fullPath, formatted, "utf8"); formattedFiles.push(path.relative(dirPath, fullPath).replace(/\\/g, "/")); } } @@ -136,7 +189,7 @@ function memoryTreeDigest(dirPath) { * @param {string} currentDir */ function visit(currentDir) { - const entries = fs.readdirSync(currentDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); + const entries = readDirectory(currentDir).sort((a, b) => a.name.localeCompare(b.name)); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); const relativePath = path.relative(dirPath, fullPath).replace(/\\/g, "/"); @@ -145,7 +198,7 @@ function memoryTreeDigest(dirPath) { visit(fullPath); } else if (entry.isFile()) { hash.update(`file\0${relativePath}\0`); - hash.update(fs.readFileSync(fullPath)); + hash.update(readFile(fullPath)); } else if (entry.isSymbolicLink()) { hash.update(`symlink\0${relativePath}\0${fs.readlinkSync(fullPath)}\0`); } else { @@ -199,7 +252,7 @@ function runCustomMemoryValidation(options) { stderr: `Unable to snapshot memory before custom validation: ${getErrorMessage(error)}`, }; } - const validationDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-memory-validation-")); + const validationDir = makeTempDirectory(); const scriptPath = path.join(validationDir, "validator.cjs"); const wrapper = `"use strict"; const fs = require("fs"); @@ -228,7 +281,7 @@ ${script} process.exit(1); }); `; - fs.writeFileSync(scriptPath, wrapper, { encoding: "utf8", mode: 0o600 }); + writeFile(scriptPath, wrapper, { encoding: "utf8", mode: 0o600 }); try { const result = childProcess.spawnSync(process.execPath, [scriptPath], { cwd: options.memoryDir, @@ -256,7 +309,7 @@ ${script} stderr: validationError ? boundedOutput(`${stderr}${stderr ? "\n" : ""}${validationError}`) : stderr, }; } finally { - fs.rmSync(validationDir, { recursive: true, force: true }); + removePath(validationDir, { recursive: true, force: true }); } } diff --git a/actions/setup/js/notify_comment_error.cjs b/actions/setup/js/notify_comment_error.cjs index 2691630f095..2bf52603bf9 100644 --- a/actions/setup/js/notify_comment_error.cjs +++ b/actions/setup/js/notify_comment_error.cjs @@ -117,7 +117,10 @@ async function main() { const agentConclusion = process.env.GH_AW_AGENT_CONCLUSION || "failure"; const detectionConclusion = process.env.GH_AW_DETECTION_CONCLUSION; const detectionReason = process.env.GH_AW_DETECTION_REASON || ""; - const assignToAgentErrorCount = parseInt(process.env.GH_AW_ASSIGNMENT_ERROR_COUNT || "0", 10); + const assignToAgentErrorCount = Number(process.env.GH_AW_ASSIGNMENT_ERROR_COUNT || "0"); + if (!Number.isFinite(assignToAgentErrorCount) || !Number.isSafeInteger(assignToAgentErrorCount) || assignToAgentErrorCount < 0) { + throw new Error(`${ERR_VALIDATION}: GH_AW_ASSIGNMENT_ERROR_COUNT must be a non-negative integer`); + } const safeOutputsResult = process.env.GH_AW_SAFE_OUTPUTS_RESULT; const messagesConfig = getMessages(); diff --git a/actions/setup/js/push_repo_memory.cjs b/actions/setup/js/push_repo_memory.cjs index 6fed6524584..b1297b11f38 100644 --- a/actions/setup/js/push_repo_memory.cjs +++ b/actions/setup/js/push_repo_memory.cjs @@ -47,13 +47,30 @@ async function main() { const memoryId = process.env.MEMORY_ID; const targetRepo = process.env.TARGET_REPO; const branchName = process.env.BRANCH_NAME; - const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || "10240", 10); - const maxFileCount = parseInt(process.env.MAX_FILE_COUNT || "100", 10); - const maxPatchSize = parseInt(process.env.MAX_PATCH_SIZE || "10240", 10); + const maxFileSize = Number(process.env.MAX_FILE_SIZE || "10240"); + const maxFileCount = Number(process.env.MAX_FILE_COUNT || "100"); + const maxPatchSize = Number(process.env.MAX_PATCH_SIZE || "10240"); const fileGlobFilter = process.env.FILE_GLOB_FILTER || ""; const formatJSON = process.env.FORMAT_JSON === "true"; const validationScriptBase64 = process.env.VALIDATION_SCRIPT_B64 || ""; - const validationTimeoutSeconds = parseInt(process.env.VALIDATION_TIMEOUT_SECONDS || "60", 10); + const validationTimeoutSeconds = Number(process.env.VALIDATION_TIMEOUT_SECONDS || "60"); + if ( + !Number.isFinite(maxFileSize) || + !Number.isSafeInteger(maxFileSize) || + maxFileSize <= 0 || + !Number.isFinite(maxFileCount) || + !Number.isSafeInteger(maxFileCount) || + maxFileCount <= 0 || + !Number.isFinite(maxPatchSize) || + !Number.isSafeInteger(maxPatchSize) || + maxPatchSize <= 0 || + !Number.isFinite(validationTimeoutSeconds) || + !Number.isSafeInteger(validationTimeoutSeconds) || + validationTimeoutSeconds <= 0 + ) { + core.setFailed("Memory size, count, patch size, and validation timeout limits must be positive integers"); + return; + } // Parse allowed extensions with error handling let allowedExtensions = [".json", ".jsonl", ".txt", ".md", ".csv"]; diff --git a/actions/setup/js/safe-outputs-mcp-server.cjs b/actions/setup/js/safe-outputs-mcp-server.cjs index 7602bf77326..d44bc70c57e 100644 --- a/actions/setup/js/safe-outputs-mcp-server.cjs +++ b/actions/setup/js/safe-outputs-mcp-server.cjs @@ -27,7 +27,10 @@ logger.debug("Successfully required safe_outputs_mcp_server_http.cjs"); // Log directory is configured via GH_AW_MCP_LOG_DIR environment variable if (require.main === module) { logger.debug("In require.main === module block"); - const port = parseInt(process.env.GH_AW_SAFE_OUTPUTS_PORT || "3001", 10); + const port = Number(process.env.GH_AW_SAFE_OUTPUTS_PORT || "3001"); + if (!Number.isFinite(port) || !Number.isSafeInteger(port) || port <= 0 || port > 65535) { + throw new Error("GH_AW_SAFE_OUTPUTS_PORT must be a valid port number"); + } const logDir = process.env.GH_AW_MCP_LOG_DIR; logger.debug(`Port: ${port}, LogDir: ${logDir}`); logger.debug("Calling startHttpServer..."); diff --git a/pkg/workflow/js/exchange_otlp_workload_identity.cjs b/pkg/workflow/js/exchange_otlp_workload_identity.cjs index 4502bc65a22..62ce3c856f4 100644 --- a/pkg/workflow/js/exchange_otlp_workload_identity.cjs +++ b/pkg/workflow/js/exchange_otlp_workload_identity.cjs @@ -31,18 +31,24 @@ async function main() { } maskSecret(oidcToken); - const response = await fetch("https://sts.googleapis.com/v1/token", { - method: "POST", - headers: { "content-type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", - requested_token_type: "urn:ietf:params:oauth:token-type:access_token", - subject_token_type: "urn:ietf:params:oauth:token-type:jwt", - subject_token: oidcToken, - audience: process.env.GH_AW_OTLP_WIF_AUDIENCE || "", - scope: CLOUD_PLATFORM_SCOPE, - }), - }); + let response; + try { + response = await fetch("https://sts.googleapis.com/v1/token", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", + requested_token_type: "urn:ietf:params:oauth:token-type:access_token", + subject_token_type: "urn:ietf:params:oauth:token-type:jwt", + subject_token: oidcToken, + audience: process.env.GH_AW_OTLP_WIF_AUDIENCE || "", + scope: CLOUD_PLATFORM_SCOPE, + }), + signal: AbortSignal.timeout(30_000), + }); + } catch (error) { + throw new Error("Google workload identity token exchange request failed", { cause: error }); + } if (!response.ok) { throw new Error( `Google workload identity token exchange failed with HTTP ${response.status} ${response.statusText}. Verify observability.otlp.workload-identity.audience matches the workload identity provider resource and that the provider trusts this repository` @@ -50,7 +56,12 @@ async function main() { } /** @type {any} */ - const stsPayload = await response.json(); + let stsPayload; + try { + stsPayload = await response.json(); + } catch (error) { + throw new Error("Failed to parse Google workload identity token exchange response", { cause: error }); + } let accessToken = stsPayload.access_token; if (!accessToken) { throw new Error("Google workload identity token exchange returned no access token"); @@ -59,18 +70,29 @@ async function main() { const serviceAccount = process.env.GH_AW_OTLP_WIF_SERVICE_ACCOUNT; if (serviceAccount) { - const impersonationResponse = await fetch(`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(serviceAccount)}:generateAccessToken`, { - method: "POST", - headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, - body: JSON.stringify({ scope: [CLOUD_PLATFORM_SCOPE] }), - }); + let impersonationResponse; + try { + impersonationResponse = await fetch(`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(serviceAccount)}:generateAccessToken`, { + method: "POST", + headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, + body: JSON.stringify({ scope: [CLOUD_PLATFORM_SCOPE] }), + signal: AbortSignal.timeout(30_000), + }); + } catch (error) { + throw new Error("Google service account impersonation request failed", { cause: error }); + } if (!impersonationResponse.ok) { throw new Error( `Google service account impersonation failed with HTTP ${impersonationResponse.status} ${impersonationResponse.statusText}. Verify observability.otlp.workload-identity.service-account exists and grants roles/iam.workloadIdentityUser to the federated principal` ); } /** @type {any} */ - const impersonationPayload = await impersonationResponse.json(); + let impersonationPayload; + try { + impersonationPayload = await impersonationResponse.json(); + } catch (error) { + throw new Error("Failed to parse Google service account impersonation response", { cause: error }); + } accessToken = impersonationPayload.accessToken; if (!accessToken) { throw new Error("Google service account impersonation returned no access token"); diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index fd902c7129e..d155681f698 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -260,9 +260,6 @@ "discussions": { "$ref": "#/definitions/permissions-level" }, - "drives": { - "$ref": "#/definitions/permissions-level" - }, "id-token": { "$ref": "#/definitions/permissions-level" }, @@ -295,6 +292,9 @@ "type": "string", "enum": ["write", "none"] }, + "drives": { + "$ref": "#/definitions/permissions-level" + }, "vulnerability-alerts": { "type": "string", "enum": ["read", "none"]