diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml
index 857c5b2cce..66c89e4a00 100644
--- a/.github/workflows/ai-moderator.lock.yml
+++ b/.github/workflows/ai-moderator.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"15950b65d2a702132a14bc0478af6fc4877e0c5f90d1eba28f6bc53fbb698094","strict":true,"agent_id":"codex"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"af1e2953bedbc8b7a1c59116a75b87e072afbd917fefc234b0901d7608354da8","strict":true,"agent_id":"codex"}
# gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions/cache","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]}
# ___ _ _
# / _ \ | | (_)
@@ -207,15 +207,15 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_1d161161775b0eab_EOF'
+ cat << 'GH_AW_PROMPT_27f6c021899a60c4_EOF'
- GH_AW_PROMPT_1d161161775b0eab_EOF
+ GH_AW_PROMPT_27f6c021899a60c4_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_1d161161775b0eab_EOF'
+ cat << 'GH_AW_PROMPT_27f6c021899a60c4_EOF'
Tools: add_labels, hide_comment(max:5), missing_tool, missing_data, noop
@@ -247,12 +247,12 @@ jobs:
{{/if}}
- GH_AW_PROMPT_1d161161775b0eab_EOF
+ GH_AW_PROMPT_27f6c021899a60c4_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_1d161161775b0eab_EOF'
+ cat << 'GH_AW_PROMPT_27f6c021899a60c4_EOF'
{{#runtime-import .github/workflows/ai-moderator.md}}
- GH_AW_PROMPT_1d161161775b0eab_EOF
+ GH_AW_PROMPT_27f6c021899a60c4_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
@@ -434,9 +434,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ba605b016681ab87_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6b19074e066842da_EOF'
{"add_labels":{"allowed":["spam","ai-generated","link-spam","ai-inspected"],"target":"*"},"create_report_incomplete_issue":{},"hide_comment":{"allowed_reasons":["spam"],"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
- GH_AW_SAFE_OUTPUTS_CONFIG_ba605b016681ab87_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_6b19074e066842da_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -638,7 +638,7 @@ jobs:
export GH_AW_ENGINE="codex"
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19'
- cat > /tmp/gh-aw/mcp-config/config.toml << GH_AW_MCP_CONFIG_e7783151e5530535_EOF
+ cat > /tmp/gh-aw/mcp-config/config.toml << GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF
[history]
persistence = "none"
@@ -665,10 +665,10 @@ jobs:
[mcp_servers.safeoutputs."guard-policies".write-sink]
accept = ["*"]
- GH_AW_MCP_CONFIG_e7783151e5530535_EOF
+ GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF
# Generate JSON config for MCP gateway
- cat << GH_AW_MCP_CONFIG_e7783151e5530535_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
+ cat << GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -711,7 +711,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_e7783151e5530535_EOF
+ GH_AW_MCP_CONFIG_c82d86036dd35b82_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md
index 09486a9c4b..701802157c 100644
--- a/.github/workflows/ai-moderator.md
+++ b/.github/workflows/ai-moderator.md
@@ -14,8 +14,8 @@ on:
skip-roles: [admin, maintainer, write, triage]
skip-bots: [github-actions, copilot, dependabot, renovate, github-copilot-enterprise, copilot-swe-agent]
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"
cancel-in-progress: false
diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml
index 6e87d609ce..47612f6596 100644
--- a/.github/workflows/auto-triage-issues.lock.yml
+++ b/.github/workflows/auto-triage-issues.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"395091b3248ae0bfdd9169fe4e375b45de1a524b5a1c7ac4189864a829171a9e","strict":true,"agent_id":"copilot"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"5862a4bd2aba141ad15e903d7a4a67f1ccded243a92e6572ed65132a859895f5","strict":true,"agent_id":"copilot"}
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]}
# ___ _ _
# / _ \ | | (_)
@@ -177,14 +177,14 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_348e9f22fc6fd7c7_EOF'
+ cat << 'GH_AW_PROMPT_cd6140c31ec529aa_EOF'
- GH_AW_PROMPT_348e9f22fc6fd7c7_EOF
+ GH_AW_PROMPT_cd6140c31ec529aa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_348e9f22fc6fd7c7_EOF'
+ cat << 'GH_AW_PROMPT_cd6140c31ec529aa_EOF'
Tools: create_discussion, add_labels(max:10), missing_tool, missing_data, noop
@@ -216,14 +216,14 @@ jobs:
{{/if}}
- GH_AW_PROMPT_348e9f22fc6fd7c7_EOF
+ GH_AW_PROMPT_cd6140c31ec529aa_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_348e9f22fc6fd7c7_EOF'
+ cat << 'GH_AW_PROMPT_cd6140c31ec529aa_EOF'
{{#runtime-import .github/workflows/shared/github-guard-policy.md}}
{{#runtime-import .github/workflows/shared/reporting.md}}
{{#runtime-import .github/workflows/auto-triage-issues.md}}
- GH_AW_PROMPT_348e9f22fc6fd7c7_EOF
+ GH_AW_PROMPT_cd6140c31ec529aa_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
@@ -398,9 +398,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_d225981bc842f158_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1af373ead35accd7_EOF'
{"add_labels":{"max":10},"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[Auto-Triage] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
- GH_AW_SAFE_OUTPUTS_CONFIG_d225981bc842f158_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_1af373ead35accd7_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -606,7 +606,7 @@ jobs:
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19'
mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_17800b88a17b863b_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
+ cat << GH_AW_MCP_CONFIG_aee730b3c097c24f_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -650,7 +650,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_17800b88a17b863b_EOF
+ GH_AW_MCP_CONFIG_aee730b3c097c24f_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
diff --git a/.github/workflows/auto-triage-issues.md b/.github/workflows/auto-triage-issues.md
index abaabb7bb0..a5a985286e 100644
--- a/.github/workflows/auto-triage-issues.md
+++ b/.github/workflows/auto-triage-issues.md
@@ -7,8 +7,8 @@ on:
schedule: every 6h
workflow_dispatch:
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
permissions:
contents: read
issues: read
diff --git a/.github/workflows/workflow-generator.lock.yml b/.github/workflows/workflow-generator.lock.yml
index 1863d03461..caa79ee1cb 100644
--- a/.github/workflows/workflow-generator.lock.yml
+++ b/.github/workflows/workflow-generator.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"cfd7f6135eab81d11cbd703b3436241bc379da2ede370ecb3285f2186bde6d06","strict":true,"agent_id":"copilot"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7b6b93ab303bfe6cae087f99a41e89b31276e2d728cdf007409fc09d4007b74e","strict":true,"agent_id":"copilot"}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]}
# ___ _ _
# / _ \ | | (_)
@@ -201,14 +201,14 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_81789fea3498ccb0_EOF'
+ cat << 'GH_AW_PROMPT_b025bb242ab3eed8_EOF'
- GH_AW_PROMPT_81789fea3498ccb0_EOF
+ GH_AW_PROMPT_b025bb242ab3eed8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_81789fea3498ccb0_EOF'
+ cat << 'GH_AW_PROMPT_b025bb242ab3eed8_EOF'
Tools: update_issue, assign_to_agent, missing_tool, missing_data, noop
@@ -240,13 +240,13 @@ jobs:
{{/if}}
- GH_AW_PROMPT_81789fea3498ccb0_EOF
+ GH_AW_PROMPT_b025bb242ab3eed8_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_81789fea3498ccb0_EOF'
+ cat << 'GH_AW_PROMPT_b025bb242ab3eed8_EOF'
{{#runtime-import .github/workflows/shared/github-guard-policy.md}}
{{#runtime-import .github/workflows/workflow-generator.md}}
- GH_AW_PROMPT_81789fea3498ccb0_EOF
+ GH_AW_PROMPT_b025bb242ab3eed8_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
@@ -421,9 +421,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
- cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3f58b4200a5b8f57_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c58abdf8d5b2660d_EOF'
{"assign_to_agent":{"allowed":["copilot"],"max":1,"target":"triggering"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_issue":{"allow_body":true,"allow_status":true,"max":1}}
- GH_AW_SAFE_OUTPUTS_CONFIG_3f58b4200a5b8f57_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_c58abdf8d5b2660d_EOF
- name: Write Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -663,7 +663,7 @@ jobs:
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19'
mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_a89a20c6f3ef975e_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
+ cat << GH_AW_MCP_CONFIG_cbd70b665437b402_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -707,7 +707,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_a89a20c6f3ef975e_EOF
+ GH_AW_MCP_CONFIG_cbd70b665437b402_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
diff --git a/.github/workflows/workflow-generator.md b/.github/workflows/workflow-generator.md
index 71011e0d3c..955bb8e71d 100644
--- a/.github/workflows/workflow-generator.md
+++ b/.github/workflows/workflow-generator.md
@@ -6,8 +6,8 @@ on:
lock-for-agent: true
reaction: "eyes"
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
permissions:
contents: read
issues: read
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index 59b9441f11..48bb8036b4 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -48,6 +48,8 @@ async function main() {
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";
+ const maxInProgressStr = process.env.GH_AW_RATE_LIMIT_MAX_IN_PROGRESS?.trim() || "";
+ const maxInProgress = maxInProgressStr ? parseInt(maxInProgressStr, 10) : 0;
core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`);
core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`);
@@ -252,7 +254,6 @@ async function main() {
core.info(`✅ Rate limit check passed`);
core.info(` User '${actor}' has ${totalRecentRuns} runs in the last ${windowMinutes} minutes`);
core.info(` Remaining quota: ${maxRuns - totalRecentRuns} runs`);
- core.setOutput("rate_limit_ok", "true");
} catch (error) {
core.error(`❌ Rate limit check failed: ${getErrorMessage(error)}`);
if (error instanceof Error && error.stack) {
@@ -260,10 +261,64 @@ async function main() {
}
// On error, allow the workflow to proceed (fail-open)
- // This prevents rate limiting from blocking workflows due to API issues
+ // This prevents rate limiting from blocking workflows due to API issues.
+ // Continue to the max-in-progress check rather than returning early.
core.warning(`⚠️ Allowing workflow to proceed due to rate limit check error`);
- core.setOutput("rate_limit_ok", "true");
}
+
+ // Check max-in-progress GitHub agent tasks if configured
+ if (maxInProgress > 0) {
+ core.info(`🔍 Checking max-in-progress agent tasks (max: ${maxInProgress})`);
+ try {
+ // Query in-progress agent tasks using the GitHub agent tasks API
+ // Requires issues: read permission
+ const response = await github.request("GET /repos/{owner}/{repo}/agent/tasks", {
+ owner,
+ repo,
+ status: "in_progress",
+ headers: {
+ "X-GitHub-Api-Version": "2026-03-10",
+ },
+ });
+
+ const inProgressTasks = response.data.tasks ?? response.data ?? [];
+ const inProgressCount = Array.isArray(inProgressTasks) ? inProgressTasks.length : 0;
+
+ core.info(`📊 In-progress agent tasks for '${owner}/${repo}': ${inProgressCount} (max: ${maxInProgress})`);
+
+ if (inProgressCount >= maxInProgress) {
+ core.warning(`⚠️ Max in-progress agent tasks reached for '${owner}/${repo}'`);
+ core.warning(` There are ${inProgressCount} in-progress tasks (max: ${maxInProgress})`);
+ core.warning(` Cancelling current workflow run...`);
+
+ try {
+ await github.rest.actions.cancelWorkflowRun({
+ owner,
+ repo,
+ run_id: runId,
+ });
+ core.warning(`✅ Workflow run ${runId} cancelled successfully`);
+ } catch (cancelError) {
+ core.error(`❌ Failed to cancel workflow run: ${getErrorMessage(cancelError)}`);
+ }
+
+ core.setOutput("rate_limit_ok", "false");
+ return;
+ }
+
+ core.info(`✅ Max-in-progress check passed (${inProgressCount}/${maxInProgress} tasks in progress)`);
+ } catch (error) {
+ core.error(`❌ Max-in-progress check failed: ${getErrorMessage(error)}`);
+ if (error instanceof Error && error.stack) {
+ core.error(` Stack trace: ${error.stack}`);
+ }
+
+ // On error, allow the workflow to proceed (fail-open)
+ core.warning(`⚠️ Allowing workflow to proceed due to max-in-progress check error`);
+ }
+ }
+
+ core.setOutput("rate_limit_ok", "true");
}
module.exports = { main };
diff --git a/pkg/cli/codemod_rate_limit_fields.go b/pkg/cli/codemod_rate_limit_fields.go
new file mode 100644
index 0000000000..4a73a8ce73
--- /dev/null
+++ b/pkg/cli/codemod_rate_limit_fields.go
@@ -0,0 +1,92 @@
+package cli
+
+import (
+ "strings"
+
+ "github.com/github/gh-aw/pkg/logger"
+)
+
+var rateLimitFieldsCodemodLog = logger.New("cli:codemod_rate_limit_fields")
+
+// getRateLimitFieldsCodemod creates a codemod for migrating rate-limit field names:
+// - max -> max-runs-per-user
+// - window -> max-runs-per-user-window
+func getRateLimitFieldsCodemod() Codemod {
+ return Codemod{
+ ID: "rate-limit-fields-migration",
+ Name: "Rename rate-limit fields for clarity",
+ Description: "Renames 'rate-limit.max' to 'rate-limit.max-runs-per-user' and 'rate-limit.window' to 'rate-limit.max-runs-per-user-window'",
+ IntroducedIn: "0.20.0",
+ Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
+ // Check if rate-limit block exists with old field names
+ rateLimitValue, hasRateLimit := frontmatter["rate-limit"]
+ if !hasRateLimit {
+ return content, false, nil
+ }
+ rateLimitMap, ok := rateLimitValue.(map[string]any)
+ if !ok {
+ return content, false, nil
+ }
+
+ _, hasMax := rateLimitMap["max"]
+ _, hasWindow := rateLimitMap["window"]
+ if !hasMax && !hasWindow {
+ return content, false, nil
+ }
+
+ newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) {
+ var modified bool
+ var inRateLimitBlock bool
+ var rateLimitIndent string
+
+ result := make([]string, len(lines))
+ for i, line := range lines {
+ trimmedLine := strings.TrimSpace(line)
+
+ // Detect entering the rate-limit block
+ if strings.HasPrefix(trimmedLine, "rate-limit:") {
+ inRateLimitBlock = true
+ rateLimitIndent = getIndentation(line)
+ result[i] = line
+ continue
+ }
+
+ // Detect leaving the rate-limit block
+ if inRateLimitBlock && len(trimmedLine) > 0 && !strings.HasPrefix(trimmedLine, "#") {
+ if hasExitedBlock(line, rateLimitIndent) {
+ inRateLimitBlock = false
+ }
+ }
+
+ // Apply renames inside the rate-limit block
+ if inRateLimitBlock {
+ if strings.HasPrefix(trimmedLine, "max:") {
+ replacedLine, didReplace := findAndReplaceInLine(line, "max", "max-runs-per-user")
+ if didReplace {
+ result[i] = replacedLine
+ modified = true
+ rateLimitFieldsCodemodLog.Printf("Renamed rate-limit.max to rate-limit.max-runs-per-user on line %d", i+1)
+ continue
+ }
+ } else if strings.HasPrefix(trimmedLine, "window:") {
+ replacedLine, didReplace := findAndReplaceInLine(line, "window", "max-runs-per-user-window")
+ if didReplace {
+ result[i] = replacedLine
+ modified = true
+ rateLimitFieldsCodemodLog.Printf("Renamed rate-limit.window to rate-limit.max-runs-per-user-window on line %d", i+1)
+ continue
+ }
+ }
+ }
+
+ result[i] = line
+ }
+ return result, modified
+ })
+ if applied {
+ rateLimitFieldsCodemodLog.Print("Applied rate-limit fields migration")
+ }
+ return newContent, applied, err
+ },
+ }
+}
diff --git a/pkg/cli/codemod_rate_limit_fields_test.go b/pkg/cli/codemod_rate_limit_fields_test.go
new file mode 100644
index 0000000000..bcc210cd24
--- /dev/null
+++ b/pkg/cli/codemod_rate_limit_fields_test.go
@@ -0,0 +1,240 @@
+//go:build !integration
+
+package cli
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetRateLimitFieldsCodemod(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ assert.Equal(t, "rate-limit-fields-migration", codemod.ID)
+ assert.Equal(t, "Rename rate-limit fields for clarity", codemod.Name)
+ assert.NotEmpty(t, codemod.Description)
+ assert.Equal(t, "0.20.0", codemod.IntroducedIn)
+ require.NotNil(t, codemod.Apply)
+}
+
+func TestRateLimitFieldsCodemod_RenamesMaxAndWindow(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max: 5
+ window: 60
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max": 5,
+ "window": 60,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user: 5")
+ assert.Contains(t, result, "max-runs-per-user-window: 60")
+ assert.NotContains(t, result, " max: 5")
+ assert.NotContains(t, result, " window: 60")
+}
+
+func TestRateLimitFieldsCodemod_RenamesMaxOnly(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max: 3
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max": 3,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user: 3")
+ assert.NotContains(t, result, " max: 3")
+}
+
+func TestRateLimitFieldsCodemod_RenamesWindowOnly(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ window: 30
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "window": 30,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user-window: 30")
+ assert.NotContains(t, result, " window: 30")
+}
+
+func TestRateLimitFieldsCodemod_NoRateLimitBlock(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+permissions:
+ contents: read
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "permissions": map[string]any{
+ "contents": "read",
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.False(t, applied, "Codemod should not be applied")
+ assert.Equal(t, content, result, "Content should be unchanged")
+}
+
+func TestRateLimitFieldsCodemod_AlreadyRenamed(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.False(t, applied, "Codemod should not be applied when fields already renamed")
+ assert.Equal(t, content, result, "Content should be unchanged")
+}
+
+func TestRateLimitFieldsCodemod_PreservesOtherFields(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ content := `---
+on: workflow_dispatch
+rate-limit:
+ max: 5
+ window: 60
+ events:
+ - workflow_dispatch
+ ignored-roles:
+ - admin
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "rate-limit": map[string]any{
+ "max": 5,
+ "window": 60,
+ "events": []any{"workflow_dispatch"},
+ "ignored-roles": []any{"admin"},
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ assert.Contains(t, result, "max-runs-per-user: 5")
+ assert.Contains(t, result, "max-runs-per-user-window: 60")
+ assert.Contains(t, result, "events:")
+ assert.Contains(t, result, "ignored-roles:")
+}
+
+func TestRateLimitFieldsCodemod_PreservesFieldsOutsideRateLimitBlock(t *testing.T) {
+ codemod := getRateLimitFieldsCodemod()
+
+ // "max" outside the rate-limit block should NOT be renamed
+ content := `---
+on: workflow_dispatch
+safe-outputs:
+ create-issue:
+ max: 5
+rate-limit:
+ max: 3
+ window: 60
+---
+
+# Test`
+
+ frontmatter := map[string]any{
+ "on": "workflow_dispatch",
+ "safe-outputs": map[string]any{
+ "create-issue": map[string]any{"max": 5},
+ },
+ "rate-limit": map[string]any{
+ "max": 3,
+ "window": 60,
+ },
+ }
+
+ result, applied, err := codemod.Apply(content, frontmatter)
+
+ require.NoError(t, err, "Should not return an error")
+ assert.True(t, applied, "Codemod should be applied")
+ // max inside safe-outputs.create-issue is preserved
+ assert.Contains(t, result, " max: 5", "max outside rate-limit block should be unchanged")
+ // max inside rate-limit is renamed
+ assert.Contains(t, result, "max-runs-per-user: 3")
+ assert.Contains(t, result, "max-runs-per-user-window: 60")
+}
+
+func TestRateLimitFieldsCodemod_IsRegistered(t *testing.T) {
+ codemods := GetAllCodemods()
+ var found bool
+ for _, c := range codemods {
+ if c.ID == "rate-limit-fields-migration" {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found, "rate-limit-fields-migration codemod should be registered in GetAllCodemods")
+}
diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go
index 9bdb843c2a..28ffacf295 100644
--- a/pkg/cli/fix_codemods.go
+++ b/pkg/cli/fix_codemods.go
@@ -51,6 +51,7 @@ func GetAllCodemods() []Codemod {
getPluginsToDependenciesCodemod(), // Migrate plugins to dependencies (plugins removed in favour of APM)
getGitHubReposToAllowedReposCodemod(), // Rename deprecated tools.github.repos to tools.github.allowed-repos
getDIFCProxyToIntegrityProxyCodemod(), // Migrate deprecated features.difc-proxy to tools.github.integrity-proxy
+ getRateLimitFieldsCodemod(), // Rename rate-limit.max -> max-runs-per-user and rate-limit.window -> max-runs-per-user-window
}
fixCodemodsLog.Printf("Loaded codemod registry: %d codemods available", len(codemods))
return codemods
diff --git a/pkg/cli/fix_codemods_test.go b/pkg/cli/fix_codemods_test.go
index feddc2e0d7..e1799b2073 100644
--- a/pkg/cli/fix_codemods_test.go
+++ b/pkg/cli/fix_codemods_test.go
@@ -43,7 +43,7 @@ func TestGetAllCodemods_ReturnsAllCodemods(t *testing.T) {
codemods := GetAllCodemods()
// Verify we have the expected number of codemods
- expectedCount := 29
+ expectedCount := 30
assert.Len(t, codemods, expectedCount, "Should return all %d codemods", expectedCount)
// Verify all codemods have required fields
@@ -134,6 +134,7 @@ func TestGetAllCodemods_InExpectedOrder(t *testing.T) {
"plugins-to-dependencies",
"github-repos-to-allowed-repos",
"features-difc-proxy-to-tools-github",
+ "rate-limit-fields-migration",
}
require.Len(t, codemods, len(expectedOrder), "Should have expected number of codemods")
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index ec59a0ce13..6398e0dbd6 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -8427,11 +8427,11 @@
},
"rate-limit": {
"type": "object",
- "description": "Rate limiting configuration to restrict how frequently users can trigger the workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events.",
- "required": ["max"],
+ "description": "Per-user workflow rate limit configuration to restrict how frequently individual users can trigger this workflow. Helps prevent abuse and resource exhaustion from programmatically triggered events. Optionally also limits the total number of in-progress GitHub agent tasks for the repository.",
+ "required": ["max-runs-per-user"],
"properties": {
- "max": {
- "description": "Maximum number of workflow runs allowed per user within the time window. Required field. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').",
+ "max-runs-per-user": {
+ "description": "Maximum number of workflow runs allowed per user within the time window. This is a per-user limit: each user is tracked independently. Required field. Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').",
"oneOf": [
{
"type": "integer",
@@ -8445,16 +8445,16 @@
}
]
},
- "window": {
+ "max-runs-per-user-window": {
"type": "integer",
"minimum": 1,
"maximum": 180,
"default": 60,
- "description": "Time window in minutes for rate limiting. Defaults to 60 (1 hour). Maximum: 180 (3 hours)."
+ "description": "Time window in minutes for the per-user rate limit. Defaults to 60 (1 hour). Maximum: 180 (3 hours)."
},
"events": {
"type": "array",
- "description": "Optional list of event types to apply rate limiting to. If not specified, rate limiting applies to all programmatically triggered events (e.g., workflow_dispatch, issue_comment, pull_request_review).",
+ "description": "Optional list of event types to apply the per-user rate limit to. If not specified, rate limiting applies to all programmatically triggered events (e.g., workflow_dispatch, issue_comment, pull_request_review).",
"items": {
"type": "string",
"enum": ["workflow_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "issues", "pull_request", "discussion_comment", "discussion"]
@@ -8463,29 +8463,39 @@
},
"ignored-roles": {
"type": "array",
- "description": "Optional list of roles that are exempt from rate limiting. Defaults to ['admin', 'maintain', 'write'] if not specified. Users with any of these roles will not be subject to rate limiting checks. To apply rate limiting to all users, set to an empty array: []",
+ "description": "Optional list of roles that are exempt from the per-user rate limit. Defaults to ['admin', 'maintain', 'write'] if not specified. Users with any of these roles will not be subject to rate limiting checks. To apply rate limiting to all users, set to an empty array: []",
"items": {
"type": "string",
"enum": ["admin", "maintain", "write", "triage", "read"]
},
"minItems": 0
+ },
+ "max-in-progress": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum number of in-progress GitHub agent tasks allowed for this repository before blocking new workflow runs. If the number of currently in-progress agent tasks meets or exceeds this value, the current run is cancelled. Queries the GitHub agent tasks API (GET /repos/{owner}/{repo}/agent/tasks). Requires issues: read permission. See https://docs.github.com/en/rest/agent-tasks/agent-tasks"
}
},
"additionalProperties": false,
"examples": [
{
- "max": 5,
- "window": 60
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60
},
{
- "max": 10,
- "window": 30,
+ "max-runs-per-user": 10,
+ "max-runs-per-user-window": 30,
"events": ["workflow_dispatch", "issue_comment"]
},
{
- "max": 5,
- "window": 60,
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60,
"ignored-roles": ["admin", "maintain"]
+ },
+ {
+ "max-runs-per-user": 5,
+ "max-runs-per-user-window": 60,
+ "max-in-progress": 3
}
]
},
diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go
index 51069ef63b..3854ace97e 100644
--- a/pkg/workflow/compiler_pre_activation_job.go
+++ b/pkg/workflow/compiler_pre_activation_job.go
@@ -54,6 +54,10 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
perms = NewPermissions()
}
perms.Set(PermissionActions, PermissionRead)
+ // Add issues: read permission if max-in-progress is configured (needed to query agent tasks)
+ if data.RateLimit.MaxInProgress > 0 {
+ perms.Set(PermissionIssues, PermissionRead)
+ }
}
// Merge on.permissions into the pre-activation job permissions.
diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go
index 0bd65c9f2a..41253ce210 100644
--- a/pkg/workflow/frontmatter_types.go
+++ b/pkg/workflow/frontmatter_types.go
@@ -109,13 +109,15 @@ type PermissionsConfig struct {
GitHubAppPermissionsConfig
}
-// RateLimitConfig represents rate limiting configuration for workflow triggers
-// Limits how many times a user can trigger a workflow within a time window
+// RateLimitConfig represents per-user rate limiting configuration for workflow triggers.
+// Controls how many times a user can trigger this workflow within a time window,
+// and optionally limits the maximum number of in-progress GitHub agent tasks for the repo.
type RateLimitConfig struct {
- Max int `json:"max,omitempty"` // Maximum number of runs allowed per time window (default: 5)
- Window int `json:"window,omitempty"` // Time window in minutes (default: 60)
- Events []string `json:"events,omitempty"` // Event types to apply rate limiting to (e.g., ["workflow_dispatch", "issue_comment"])
- IgnoredRoles []string `json:"ignored-roles,omitempty"` // Roles that are exempt from rate limiting (e.g., ["admin", "maintainer"])
+ Max int `json:"max-runs-per-user,omitempty"` // Maximum number of workflow runs allowed per user within the time window (default: 5)
+ Window int `json:"max-runs-per-user-window,omitempty"` // Time window in minutes (default: 60)
+ Events []string `json:"events,omitempty"` // Event types to apply rate limiting to (e.g., ["workflow_dispatch", "issue_comment"])
+ IgnoredRoles []string `json:"ignored-roles,omitempty"` // Roles that are exempt from rate limiting (e.g., ["admin", "maintainer"])
+ MaxInProgress int `json:"max-in-progress,omitempty"` // Maximum number of in-progress GitHub agent tasks allowed for the repo before blocking new runs
}
// OTLPConfig holds configuration for OTLP (OpenTelemetry Protocol) trace export.
diff --git a/pkg/workflow/permissions_factory.go b/pkg/workflow/permissions_factory.go
index 0a7ef8fe1e..3f7c943ef4 100644
--- a/pkg/workflow/permissions_factory.go
+++ b/pkg/workflow/permissions_factory.go
@@ -73,6 +73,14 @@ func NewPermissionsContentsRead() *Permissions {
})
}
+// NewPermissionsContentsReadIssuesRead creates permissions with contents: read and issues: read
+func NewPermissionsContentsReadIssuesRead() *Permissions {
+ return NewPermissionsFromMap(map[PermissionScope]PermissionLevel{
+ PermissionContents: PermissionRead,
+ PermissionIssues: PermissionRead,
+ })
+}
+
// NewPermissionsContentsReadIssuesWrite creates permissions with contents: read and issues: write
func NewPermissionsContentsReadIssuesWrite() *Permissions {
return NewPermissionsFromMap(map[PermissionScope]PermissionLevel{
diff --git a/pkg/workflow/rate_limit_experimental_warning_test.go b/pkg/workflow/rate_limit_experimental_warning_test.go
index 11f295d5e1..b39cb7652e 100644
--- a/pkg/workflow/rate_limit_experimental_warning_test.go
+++ b/pkg/workflow/rate_limit_experimental_warning_test.go
@@ -27,8 +27,8 @@ func TestRateLimitExperimentalWarning(t *testing.T) {
on: workflow_dispatch
engine: copilot
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
permissions:
contents: read
issues: read
@@ -60,8 +60,8 @@ permissions:
on: workflow_dispatch
engine: copilot
rate-limit:
- max: 3
- window: 30
+ max-runs-per-user: 3
+ max-runs-per-user-window: 30
ignored-roles:
- admin
- maintain
@@ -84,8 +84,8 @@ on:
types: [created]
engine: copilot
rate-limit:
- max: 5
- window: 60
+ max-runs-per-user: 5
+ max-runs-per-user-window: 60
events: [workflow_dispatch, issue_comment]
permissions:
contents: read
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index e3354a51cf..a1796229b1 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -83,6 +83,11 @@ func (c *Compiler) generateRateLimitCheck(data *WorkflowData, steps []string) []
steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_IGNORED_ROLES: %q\n", strings.Join(ignoredRoles, ",")))
}
+ // Set max-in-progress (if specified)
+ if data.RateLimit.MaxInProgress > 0 {
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_MAX_IN_PROGRESS: \"%d\"\n", data.RateLimit.MaxInProgress))
+ }
+
steps = append(steps, " with:\n")
steps = append(steps, " github-token: ${{ secrets.GITHUB_TOKEN }}\n")
steps = append(steps, " script: |\n")
@@ -172,8 +177,20 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
case map[string]any:
config := &RateLimitConfig{}
- // Extract max (default: 5)
- if maxValue, ok := v["max"]; ok {
+ // Extract max-runs-per-user (previously named 'max', with backward compat fallback)
+ if maxValue, ok := v["max-runs-per-user"]; ok {
+ switch max := maxValue.(type) {
+ case int:
+ config.Max = max
+ case int64:
+ config.Max = int(max)
+ case uint64:
+ config.Max = int(max)
+ case float64:
+ config.Max = int(max)
+ }
+ } else if maxValue, ok := v["max"]; ok {
+ // Backward compat: accept old 'max' field (codemod renames it)
switch max := maxValue.(type) {
case int:
config.Max = max
@@ -186,8 +203,20 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
}
}
- // Extract window (default: 60 minutes)
- if windowValue, ok := v["window"]; ok {
+ // Extract max-runs-per-user-window (previously named 'window', with backward compat fallback)
+ if windowValue, ok := v["max-runs-per-user-window"]; ok {
+ switch window := windowValue.(type) {
+ case int:
+ config.Window = window
+ case int64:
+ config.Window = int(window)
+ case uint64:
+ config.Window = int(window)
+ case float64:
+ config.Window = int(window)
+ }
+ } else if windowValue, ok := v["window"]; ok {
+ // Backward compat: accept old 'window' field (codemod renames it)
switch window := windowValue.(type) {
case int:
config.Window = window
@@ -242,7 +271,21 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
roleLog.Print("No ignored-roles specified, using defaults: admin, maintain, write")
}
- roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v", config.Max, config.Window, config.Events, config.IgnoredRoles)
+ // Extract max-in-progress
+ if maxInProgressValue, ok := v["max-in-progress"]; ok {
+ switch maxInProgress := maxInProgressValue.(type) {
+ case int:
+ config.MaxInProgress = maxInProgress
+ case int64:
+ config.MaxInProgress = int(maxInProgress)
+ case uint64:
+ config.MaxInProgress = int(maxInProgress)
+ case float64:
+ config.MaxInProgress = int(maxInProgress)
+ }
+ }
+
+ roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v, max-in-progress=%d", config.Max, config.Window, config.Events, config.IgnoredRoles, config.MaxInProgress)
return config
}
}