From 12096532e028e90347af3dd9317b04563aee1d1e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 23:09:50 +0000
Subject: [PATCH 1/5] Initial plan
From e797da34afc498ce9453e78b9367c5b348baf5be Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 23:18:43 +0000
Subject: [PATCH 2/5] Add ignored-roles field to rate-limiting configuration
- Updated RateLimitConfig struct to include IgnoredRoles field
- Updated JSON schema validation to support ignored-roles
- Implemented role check in check_rate_limit.cjs to skip rate limiting for users with ignored roles
- Added comprehensive tests for the new functionality
- Updated documentation with examples and troubleshooting guide
- Created test workflow demonstrating role-based exemptions
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../test-rate-limit-ignored-roles.lock.yml | 518 ++++++++++++++++++
.../test-rate-limit-ignored-roles.md | 55 ++
actions/setup/js/check_rate_limit.cjs | 32 ++
actions/setup/js/check_rate_limit.test.cjs | 108 ++++
docs/RATE_LIMITING.md | 78 ++-
pkg/parser/schemas/main_workflow_schema.json | 14 +
pkg/workflow/frontmatter_types.go | 7 +-
pkg/workflow/role_checks.go | 27 +-
8 files changed, 828 insertions(+), 11 deletions(-)
create mode 100644 .github/workflows/test-rate-limit-ignored-roles.lock.yml
create mode 100644 .github/workflows/test-rate-limit-ignored-roles.md
diff --git a/.github/workflows/test-rate-limit-ignored-roles.lock.yml b/.github/workflows/test-rate-limit-ignored-roles.lock.yml
new file mode 100644
index 0000000000..47619a72be
--- /dev/null
+++ b/.github/workflows/test-rate-limit-ignored-roles.lock.yml
@@ -0,0 +1,518 @@
+#
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw. DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
+#
+#
+# frontmatter-hash: 9c5fc197f53c28f8b3798cfd83a6d7f5bbe5264e1b582fa08cfa40f0d0171ac7
+
+name: "Test Rate Limiting with Ignored Roles"
+"on":
+ issue_comment:
+ types:
+ - created
+ workflow_dispatch:
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
+
+run-name: "Test Rate Limiting with Ignored Roles"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: needs.pre_activation.outputs.activated == 'true'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ comment_id: ""
+ comment_repo: ""
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check workflow file timestamps
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_WORKFLOW_FILE: "test-rate-limit-ignored-roles.lock.yml"
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
+ await main();
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Generate agentic run info
+ id: generate_aw_info
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const fs = require('fs');
+
+ const awInfo = {
+ engine_id: "copilot",
+ engine_name: "GitHub Copilot CLI",
+ model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
+ version: "",
+ agent_version: "0.0.406",
+ workflow_name: "Test Rate Limiting with Ignored Roles",
+ experimental: false,
+ supports_tools_allowlist: true,
+ supports_http_transport: true,
+ run_id: context.runId,
+ run_number: context.runNumber,
+ run_attempt: process.env.GITHUB_RUN_ATTEMPT,
+ repository: context.repo.owner + '/' + context.repo.repo,
+ ref: context.ref,
+ sha: context.sha,
+ actor: context.actor,
+ event_name: context.eventName,
+ staged: false,
+ allowed_domains: ["defaults"],
+ firewall_enabled: true,
+ awf_version: "v0.14.0",
+ awmg_version: "",
+ steps: {
+ firewall: "squid"
+ },
+ created_at: new Date().toISOString()
+ };
+
+ // Write to /tmp/gh-aw directory to avoid inclusion in PR
+ const tmpPath = '/tmp/gh-aw/aw_info.json';
+ fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
+ console.log('Generated aw_info.json at:', tmpPath);
+ console.log(JSON.stringify(awInfo, null, 2));
+
+ // Set model as output for reuse in other steps/jobs
+ core.setOutput('model', awInfo.model);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Install GitHub Copilot CLI
+ run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.406
+ - name: Install awf binary
+ run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.14.0
+ - name: Determine automatic lockdown mode for GitHub MCP server
+ id: determine-automatic-lockdown
+ env:
+ TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ if: env.TOKEN_CHECK != ''
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download container images
+ run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.14.0 ghcr.io/github/gh-aw-firewall/squid:0.14.0 ghcr.io/github/gh-aw-mcpg:v0.1.0 ghcr.io/github/github-mcp-server:v0.30.3
+ - name: Start MCP gateway
+ id: start-mcp-gateway
+ env:
+ GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p /tmp/gh-aw/mcp-config
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="80"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_API_KEY}"
+ export MCP_GATEWAY_API_KEY
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e 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_LOCKDOWN -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 -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
+
+ mkdir -p /home/runner/.copilot
+ cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v0.30.3",
+ "env": {
+ "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_EOF
+ - name: Generate workflow overview
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
+ await generateWorkflowOverview(core);
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ run: |
+ bash /opt/gh-aw/actions/create_prompt_first.sh
+ cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT"
+
+ GH_AW_PROMPT_EOF
+ cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
+ cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
+ cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
+
+ The following GitHub context information is available for this workflow:
+ {{#if __GH_AW_GITHUB_ACTOR__ }}
+ - **actor**: __GH_AW_GITHUB_ACTOR__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_REPOSITORY__ }}
+ - **repository**: __GH_AW_GITHUB_REPOSITORY__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_WORKSPACE__ }}
+ - **workspace**: __GH_AW_GITHUB_WORKSPACE__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
+ - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
+ - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
+ - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
+ - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_RUN_ID__ }}
+ - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
+ {{/if}}
+
+
+ GH_AW_PROMPT_EOF
+ if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
+ cat "/opt/gh-aw/prompts/pr_context_prompt.md" >> "$GH_AW_PROMPT"
+ fi
+ cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
+
+ GH_AW_PROMPT_EOF
+ cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
+ {{#runtime-import .github/workflows/test-rate-limit-ignored-roles.md}}
+ GH_AW_PROMPT_EOF
+ - name: Substitute placeholders
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ with:
+ script: |
+ const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT
+ }
+ });
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
+ await main();
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/print_prompt_summary.sh
+ - name: Clean git credentials
+ run: bash /opt/gh-aw/actions/clean_git_credentials.sh
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.14.0 --skip-pull \
+ -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' \
+ 2>&1 | tee /tmp/gh-aw/agent-stdio.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: |
+ # Copy Copilot session state files to logs folder for artifact collection
+ # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
+ SESSION_STATE_DIR="$HOME/.copilot/session-state"
+ LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
+
+ if [ -d "$SESSION_STATE_DIR" ]; then
+ echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
+ mkdir -p "$LOGS_DIR"
+ cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
+ echo "Session state files copied successfully"
+ else
+ echo "No session-state directory found at $SESSION_STATE_DIR"
+ fi
+ - name: Stop MCP gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload engine output files
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: agent_outputs
+ path: |
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ if-no-files-found: ignore
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
+ await main();
+ - name: Parse MCP gateway logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: agent-artifacts
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/agent/
+ if-no-files-found: ignore
+
+ pre_activation:
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ outputs:
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_REQUIRED_ROLES: admin,maintainer,write
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
+ await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "3"
+ GH_AW_RATE_LIMIT_WINDOW: "30"
+ GH_AW_RATE_LIMIT_EVENTS: "issue_comment,workflow_dispatch"
+ GH_AW_RATE_LIMIT_IGNORED_ROLES: "admin,maintain"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
+
diff --git a/.github/workflows/test-rate-limit-ignored-roles.md b/.github/workflows/test-rate-limit-ignored-roles.md
new file mode 100644
index 0000000000..8c417b8ea8
--- /dev/null
+++ b/.github/workflows/test-rate-limit-ignored-roles.md
@@ -0,0 +1,55 @@
+---
+name: Test Rate Limiting with Ignored Roles
+engine: copilot
+on:
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+rate-limit:
+ max: 3
+ window: 30
+ ignored-roles:
+ - admin
+ - maintain
+ events: [workflow_dispatch, issue_comment]
+---
+
+Test workflow to demonstrate rate limiting with ignored roles.
+
+This workflow:
+- Limits non-admin/non-maintainer users to 3 runs within a 30-minute window
+- Exempts users with "admin" or "maintain" roles from rate limiting
+- Applies to workflow_dispatch and issue_comment events
+
+## Testing
+
+### For Admin/Maintainer Users:
+1. Trigger the workflow multiple times in quick succession (>3 times)
+2. All runs should succeed without rate limiting
+
+### For Other Users (write, triage, read):
+1. Trigger the workflow 4 times in quick succession
+2. The 4th run should be automatically cancelled with a rate limit warning
+3. Wait 30 minutes for the window to reset
+4. Trigger again to confirm the limit resets
+
+## Expected Behavior
+
+**Admin/Maintain users:**
+```
+🔍 Checking rate limit for user 'admin-user' on workflow 'test-rate-limit-ignored-roles.lock.yml'
+ Configuration: max=3 runs per 30 minutes
+ Current event: workflow_dispatch
+ Ignored roles: admin, maintain
+ User 'admin-user' has permission level: admin
+✅ User 'admin-user' has ignored role 'admin'; skipping rate limit check
+```
+
+**Other users (after 3 runs):**
+```
+⚠️ Rate limit exceeded for user 'contributor' on workflow 'test-rate-limit-ignored-roles.lock.yml'
+ User has triggered 3 runs in the last 30 minutes (max: 3)
+ Cancelling current workflow run...
+```
+
+Hello! I'm testing the rate limiting feature with role-based exemptions.
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index 67773b9186..9d00746291 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -35,11 +35,43 @@ async function main() {
const maxRuns = parseInt(process.env.GH_AW_RATE_LIMIT_MAX || "5", 10);
const windowMinutes = parseInt(process.env.GH_AW_RATE_LIMIT_WINDOW || "60", 10);
const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS || "";
+ const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES || "";
core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`);
core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`);
core.info(` Current event: ${eventName}`);
+ // Check if user has an ignored role (exempt from rate limiting)
+ if (ignoredRolesList) {
+ const ignoredRoles = ignoredRolesList.split(",").map(r => r.trim());
+ core.info(` Ignored roles: ${ignoredRoles.join(", ")}`);
+
+ try {
+ // Check user's permission level in the repository
+ const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner,
+ repo,
+ username: actor,
+ });
+
+ const userPermission = permissionData.permission;
+ core.info(` User '${actor}' has permission level: ${userPermission}`);
+
+ // Map GitHub permission levels to role names
+ // GitHub uses: admin, maintain, write, triage, read
+ if (ignoredRoles.includes(userPermission)) {
+ core.info(`✅ User '${actor}' has ignored role '${userPermission}'; skipping rate limit check`);
+ core.setOutput("rate_limit_ok", "true");
+ return;
+ }
+ } catch (error) {
+ // If we can't check permissions, continue with rate limiting (fail-secure)
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ core.warning(`⚠️ Could not check user permissions: ${errorMsg}`);
+ core.warning(` Continuing with rate limit check for user '${actor}'`);
+ }
+ }
+
// Parse events to apply rate limiting to
const limitedEvents = eventsList ? eventsList.split(",").map(e => e.trim()) : [];
diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs
index 765b763ba7..39d74b3f8d 100644
--- a/actions/setup/js/check_rate_limit.test.cjs
+++ b/actions/setup/js/check_rate_limit.test.cjs
@@ -558,4 +558,112 @@ describe("check_rate_limit", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using workflow name: test-workflow (fallback"));
});
+
+ it("should skip rate limiting for users with ignored roles", async () => {
+ process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";
+
+ // Mock the permission check to return admin
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
+ data: {
+ permission: "admin",
+ },
+ }),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin, maintain"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: admin"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'admin'; skipping rate limit check"));
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
+ });
+
+ it("should skip rate limiting for users with maintain permission when in ignored roles", async () => {
+ process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";
+
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
+ data: {
+ permission: "maintain",
+ },
+ }),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'maintain'; skipping rate limit check"));
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
+ });
+
+ it("should apply rate limiting for users without ignored roles", async () => {
+ process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";
+
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
+ data: {
+ permission: "write",
+ },
+ }),
+ };
+
+ mockGithub.rest.actions = {
+ listWorkflowRuns: vi.fn().mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ }),
+ cancelWorkflowRun: vi.fn(),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: write"));
+ expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ });
+
+ it("should continue with rate limiting if permission check fails", async () => {
+ process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin";
+
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockRejectedValue(new Error("API error")),
+ };
+
+ mockGithub.rest.actions = {
+ listWorkflowRuns: vi.fn().mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ }),
+ cancelWorkflowRun: vi.fn(),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Could not check user permissions"));
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Continuing with rate limit check"));
+ expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ });
+
+ it("should handle single ignored role as string", async () => {
+ process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin";
+
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
+ data: {
+ permission: "admin",
+ },
+ }),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'admin'; skipping rate limit check"));
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ });
});
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
index f386312084..6a81c91266 100644
--- a/docs/RATE_LIMITING.md
+++ b/docs/RATE_LIMITING.md
@@ -21,8 +21,11 @@ on:
issue_comment:
types: [created]
rate-limit:
- max: 5 # Required: 1-10 runs
- window: 60 # Optional: minutes (default 60, max 180)
+ max: 5 # Required: 1-10 runs
+ window: 60 # Optional: minutes (default 60, max 180)
+ ignored-roles: # Optional: roles exempt from rate limiting
+ - admin
+ - maintain
# events field is optional - automatically inferred from 'on:' triggers
---
```
@@ -40,6 +43,18 @@ rate-limit:
- Range: 1-180 (up to 3 hours)
- Example: `window: 30` creates a 30-minute window
+### `ignored-roles` (array, optional)
+- List of repository roles that are exempt from rate limiting
+- Users with any of these roles will not be subject to rate limiting checks
+- Supported roles:
+ - `admin` - Repository administrators
+ - `maintain` - Users with maintain permissions
+ - `write` - Users with write access
+ - `triage` - Users with triage access
+ - `read` - Users with read access
+- Example: `ignored-roles: [admin, maintain]` exempts admins and maintainers
+- If not specified, rate limiting applies to all users
+
### `events` (array, optional)
- Specific event types to apply rate limiting to
- **If not specified, automatically inferred from the workflow's `on:` triggers**
@@ -58,17 +73,18 @@ rate-limit:
## How It Works
1. **Pre-Activation Check**: Rate limiting is enforced in the pre-activation job, before the main workflow runs
-2. **Per-User Per-Workflow**: Limits are applied individually for each user and workflow
-3. **Recent Runs Query**: The system queries recent workflow runs from the GitHub API
-4. **Filtering**: Runs are filtered by:
+2. **Role-Based Exemptions**: If `ignored-roles` is configured, the system first checks the user's repository permission level. Users with matching roles skip rate limiting entirely.
+3. **Per-User Per-Workflow**: For non-exempt users, limits are applied individually for each user and workflow
+4. **Recent Runs Query**: The system queries recent workflow runs from the GitHub API
+5. **Filtering**: Runs are filtered by:
- Actor (user who triggered the workflow)
- Time window (only runs within the configured window)
- Event type (if `events` is configured)
- Excludes the current run from the count
- Excludes cancelled runs (cancelled runs don't count toward the limit)
- Excludes runs that completed in less than 15 seconds (treated as failed fast/cancelled)
-5. **Progressive Aggregation**: Uses pagination with short-circuit logic for efficiency
-6. **Automatic Cancellation**: If the limit is exceeded, the current run is automatically cancelled
+6. **Progressive Aggregation**: Uses pagination with short-circuit logic for efficiency
+7. **Automatic Cancellation**: If the limit is exceeded, the current run is automatically cancelled
## Examples
@@ -103,6 +119,26 @@ rate-limit:
```
Explicitly specify events to override inference. Allows only 3 runs per 30 minutes for the specified events.
+### Rate Limiting with Role Exemptions
+```yaml
+rate-limit:
+ max: 5
+ window: 60
+ ignored-roles:
+ - admin
+ - maintain
+```
+Allows 5 runs per hour, but admins and maintainers are exempt from rate limiting entirely.
+
+### Strict Rate Limiting for Contributors
+```yaml
+rate-limit:
+ max: 3
+ window: 30
+ ignored-roles: [admin]
+```
+Strict rate limiting (3 runs per 30 minutes) for all users except repository admins.
+
### Generous Rate Limiting
```yaml
rate-limit:
@@ -127,6 +163,8 @@ The rate limit check provides extensive logging:
🔍 Checking rate limit for user 'username' on workflow 'workflow-name'
Configuration: max=5 runs per 60 minutes
Current event: workflow_dispatch
+ Ignored roles: admin, maintain
+ User 'username' has permission level: write
Time window: runs created after 2026-02-11T11:24:33.098Z
📊 Querying workflow runs for 'workflow-name'...
Fetching page 1 (up to 100 runs per page)...
@@ -144,6 +182,16 @@ The rate limit check provides extensive logging:
Remaining quota: 2 runs
```
+When a user has an ignored role:
+```
+🔍 Checking rate limit for user 'admin-user' on workflow 'workflow-name'
+ Configuration: max=5 runs per 60 minutes
+ Current event: workflow_dispatch
+ Ignored roles: admin, maintain
+ User 'admin-user' has permission level: admin
+✅ User 'admin-user' has ignored role 'admin'; skipping rate limit check
+```
+
### Error Handling
- **Fail-Open**: If the rate limit check fails due to API errors, the workflow is allowed to proceed
- This ensures that temporary API issues don't block legitimate workflow runs
@@ -260,15 +308,23 @@ To test:
- Verify the schema is valid (run `gh aw compile`)
- Check pre-activation job logs for rate limit check output
+### Ignored Roles Not Working
+- Verify the user has the expected repository permission level
+- Check pre-activation job logs for "User 'username' has permission level: X" message
+- Ensure the role name matches exactly (e.g., "maintain" not "maintainer")
+- If permission check fails, rate limiting will still be applied (fail-secure behavior)
+
### Unexpected Cancellations
- Review the rate limit configuration (`max` and `window`)
- Check if other users are triggering the same workflow
- Verify event filters are configured correctly
+- Check if the user has an ignored role that should exempt them
### API Errors
- Rate limit checks fail-open on API errors
- Check GitHub API status if issues persist
- Review workflow run logs for detailed error messages
+- Permission checks fail-secure: if unable to check roles, rate limiting is applied
## Schema Definition
@@ -306,6 +362,14 @@ The rate-limit field is validated against this JSON schema:
]
},
"minItems": 1
+ },
+ "ignored-roles": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["admin", "maintain", "write", "triage", "read"]
+ },
+ "minItems": 1
}
}
}
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index cb39acf026..d1e3d4241d 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -6113,6 +6113,15 @@
"enum": ["workflow_dispatch", "issue_comment", "pull_request_review", "pull_request_review_comment", "issues", "pull_request", "discussion_comment", "discussion"]
},
"minItems": 1
+ },
+ "ignored-roles": {
+ "type": "array",
+ "description": "Optional list of roles that are exempt from rate limiting. Users with any of these roles will not be subject to rate limiting checks. Common roles: 'admin', 'maintain', 'write', 'triage', 'read'.",
+ "items": {
+ "type": "string",
+ "enum": ["admin", "maintain", "write", "triage", "read"]
+ },
+ "minItems": 1
}
},
"additionalProperties": false,
@@ -6125,6 +6134,11 @@
"max": 10,
"window": 30,
"events": ["workflow_dispatch", "issue_comment"]
+ },
+ {
+ "max": 5,
+ "window": 60,
+ "ignored-roles": ["admin", "maintain"]
}
]
},
diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go
index f9bced5aa3..4b53e6caf0 100644
--- a/pkg/workflow/frontmatter_types.go
+++ b/pkg/workflow/frontmatter_types.go
@@ -79,9 +79,10 @@ type PluginsConfig struct {
// RateLimitConfig represents rate limiting configuration for workflow triggers
// Limits how many times a user can trigger a workflow within a time window
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"])
+ 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"])
}
// FrontmatterConfig represents the structured configuration from workflow frontmatter
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index b0de86a174..018d4007f4 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -70,6 +70,15 @@ func (c *Compiler) generateRateLimitCheck(data *WorkflowData, steps []string) []
steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_EVENTS: %q\n", strings.Join(events, ",")))
}
+ // Set ignored roles (if specified)
+ if len(data.RateLimit.IgnoredRoles) > 0 {
+ // Sort roles alphabetically for consistent output
+ ignoredRoles := make([]string, len(data.RateLimit.IgnoredRoles))
+ copy(ignoredRoles, data.RateLimit.IgnoredRoles)
+ sort.Strings(ignoredRoles)
+ steps = append(steps, fmt.Sprintf(" GH_AW_RATE_LIMIT_IGNORED_ROLES: %q\n", strings.Join(ignoredRoles, ",")))
+ }
+
steps = append(steps, " with:\n")
steps = append(steps, " github-token: ${{ secrets.GITHUB_TOKEN }}\n")
steps = append(steps, " script: |\n")
@@ -199,7 +208,23 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
}
}
- roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v", config.Max, config.Window, config.Events)
+ // Extract ignored-roles
+ if ignoredRolesValue, ok := v["ignored-roles"]; ok {
+ switch ignoredRoles := ignoredRolesValue.(type) {
+ case []any:
+ for _, item := range ignoredRoles {
+ if str, ok := item.(string); ok {
+ config.IgnoredRoles = append(config.IgnoredRoles, str)
+ }
+ }
+ case []string:
+ config.IgnoredRoles = ignoredRoles
+ case string:
+ config.IgnoredRoles = []string{ignoredRoles}
+ }
+ }
+
+ roleLog.Printf("Extracted rate-limit config: max=%d, window=%d, events=%v, ignored-roles=%v", config.Max, config.Window, config.Events, config.IgnoredRoles)
return config
}
}
From a7d3081c8a1bd1e4f0d1c9a9424d71598d17e552 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 23:26:29 +0000
Subject: [PATCH 3/5] Complete implementation of ignored-roles field
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
docs/src/content/docs/agent-factory-status.mdx | 1 +
docs/src/content/docs/reference/frontmatter-full.md | 7 +++++++
2 files changed, 8 insertions(+)
diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx
index 398f7ce702..96d79fc6b3 100644
--- a/docs/src/content/docs/agent-factory-status.mdx
+++ b/docs/src/content/docs/agent-factory-status.mdx
@@ -144,6 +144,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Test Create PR Error Handling](https://github.com/github/gh-aw/blob/main/.github/workflows/test-create-pr-error-handling.md) | claude | [](https://github.com/github/gh-aw/actions/workflows/test-create-pr-error-handling.lock.yml) | - | - |
| [Test Dispatcher Workflow](https://github.com/github/gh-aw/blob/main/.github/workflows/test-dispatcher.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-dispatcher.lock.yml) | - | - |
| [Test Project URL Explicit Requirement](https://github.com/github/gh-aw/blob/main/.github/workflows/test-project-url-default.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-project-url-default.lock.yml) | - | - |
+| [Test Rate Limiting with Ignored Roles](https://github.com/github/gh-aw/blob/main/.github/workflows/test-rate-limit-ignored-roles.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-rate-limit-ignored-roles.lock.yml) | - | - |
| [Test Workflow](https://github.com/github/gh-aw/blob/main/.github/workflows/test-workflow.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/test-workflow.lock.yml) | - | - |
| [The Daily Repository Chronicle](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-repo-chronicle.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml) | `0 16 * * 1-5` | - |
| [The Great Escapi](https://github.com/github/gh-aw/blob/main/.github/workflows/firewall-escape.md) | copilot | [](https://github.com/github/gh-aw/actions/workflows/firewall-escape.lock.yml) | - | - |
diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md
index 9d0d3c5851..896e0ec95c 100644
--- a/docs/src/content/docs/reference/frontmatter-full.md
+++ b/docs/src/content/docs/reference/frontmatter-full.md
@@ -3528,6 +3528,13 @@ rate-limit:
events: []
# Array of strings
+ # Optional list of roles that are exempt from rate limiting. Users with any of
+ # these roles will not be subject to rate limiting checks. Common roles: 'admin',
+ # 'maintain', 'write', 'triage', 'read'.
+ # (optional)
+ ignored-roles: []
+ # Array of strings
+
# Enable strict mode validation for enhanced security and compliance. Strict mode
# enforces: (1) Write Permissions - refuses contents:write, issues:write,
# pull-requests:write; requires safe-outputs instead, (2) Network Configuration -
From d1c655d798fc52223005add56a3ba592e027ed7f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 23:39:26 +0000
Subject: [PATCH 4/5] Set default ignored-roles to admin, maintain, write
Default behavior now exempts users with admin, maintain, or write permissions from rate limiting. Only triage and read users are subject to rate limiting by default. Users can override this by explicitly setting ignored-roles in frontmatter.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../test-rate-limit-defaults.lock.yml | 518 ++++++++++++++++++
.github/workflows/test-rate-limit-defaults.md | 18 +
actions/setup/js/check_rate_limit.cjs | 53 +-
actions/setup/js/check_rate_limit.test.cjs | 53 ++
docs/RATE_LIMITING.md | 34 +-
pkg/parser/schemas/main_workflow_schema.json | 4 +-
pkg/workflow/role_checks.go | 4 +
7 files changed, 642 insertions(+), 42 deletions(-)
create mode 100644 .github/workflows/test-rate-limit-defaults.lock.yml
create mode 100644 .github/workflows/test-rate-limit-defaults.md
diff --git a/.github/workflows/test-rate-limit-defaults.lock.yml b/.github/workflows/test-rate-limit-defaults.lock.yml
new file mode 100644
index 0000000000..5595192ae8
--- /dev/null
+++ b/.github/workflows/test-rate-limit-defaults.lock.yml
@@ -0,0 +1,518 @@
+#
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw. DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
+#
+#
+# frontmatter-hash: 252aafefeef415d3213c1a9d3caf249c814f4319a79c8897525436db13509f8b
+
+name: "Test Rate Limiting with Default Ignored Roles"
+"on":
+ issue_comment:
+ types:
+ - created
+ workflow_dispatch:
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
+
+run-name: "Test Rate Limiting with Default Ignored Roles"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: needs.pre_activation.outputs.activated == 'true'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ comment_id: ""
+ comment_repo: ""
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check workflow file timestamps
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_WORKFLOW_FILE: "test-rate-limit-defaults.lock.yml"
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
+ await main();
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Generate agentic run info
+ id: generate_aw_info
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const fs = require('fs');
+
+ const awInfo = {
+ engine_id: "copilot",
+ engine_name: "GitHub Copilot CLI",
+ model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
+ version: "",
+ agent_version: "0.0.406",
+ workflow_name: "Test Rate Limiting with Default Ignored Roles",
+ experimental: false,
+ supports_tools_allowlist: true,
+ supports_http_transport: true,
+ run_id: context.runId,
+ run_number: context.runNumber,
+ run_attempt: process.env.GITHUB_RUN_ATTEMPT,
+ repository: context.repo.owner + '/' + context.repo.repo,
+ ref: context.ref,
+ sha: context.sha,
+ actor: context.actor,
+ event_name: context.eventName,
+ staged: false,
+ allowed_domains: ["defaults"],
+ firewall_enabled: true,
+ awf_version: "v0.14.0",
+ awmg_version: "",
+ steps: {
+ firewall: "squid"
+ },
+ created_at: new Date().toISOString()
+ };
+
+ // Write to /tmp/gh-aw directory to avoid inclusion in PR
+ const tmpPath = '/tmp/gh-aw/aw_info.json';
+ fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
+ console.log('Generated aw_info.json at:', tmpPath);
+ console.log(JSON.stringify(awInfo, null, 2));
+
+ // Set model as output for reuse in other steps/jobs
+ core.setOutput('model', awInfo.model);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Install GitHub Copilot CLI
+ run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.406
+ - name: Install awf binary
+ run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.14.0
+ - name: Determine automatic lockdown mode for GitHub MCP server
+ id: determine-automatic-lockdown
+ env:
+ TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ if: env.TOKEN_CHECK != ''
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download container images
+ run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.14.0 ghcr.io/github/gh-aw-firewall/squid:0.14.0 ghcr.io/github/gh-aw-mcpg:v0.1.0 ghcr.io/github/github-mcp-server:v0.30.3
+ - name: Start MCP gateway
+ id: start-mcp-gateway
+ env:
+ GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p /tmp/gh-aw/mcp-config
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="80"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_API_KEY}"
+ export MCP_GATEWAY_API_KEY
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e 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_LOCKDOWN -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 -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
+
+ mkdir -p /home/runner/.copilot
+ cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v0.30.3",
+ "env": {
+ "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_EOF
+ - name: Generate workflow overview
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
+ await generateWorkflowOverview(core);
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ run: |
+ bash /opt/gh-aw/actions/create_prompt_first.sh
+ cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT"
+
+ GH_AW_PROMPT_EOF
+ cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
+ cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
+ cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
+
+ The following GitHub context information is available for this workflow:
+ {{#if __GH_AW_GITHUB_ACTOR__ }}
+ - **actor**: __GH_AW_GITHUB_ACTOR__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_REPOSITORY__ }}
+ - **repository**: __GH_AW_GITHUB_REPOSITORY__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_WORKSPACE__ }}
+ - **workspace**: __GH_AW_GITHUB_WORKSPACE__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
+ - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
+ - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
+ - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
+ - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_RUN_ID__ }}
+ - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
+ {{/if}}
+
+
+ GH_AW_PROMPT_EOF
+ if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
+ cat "/opt/gh-aw/prompts/pr_context_prompt.md" >> "$GH_AW_PROMPT"
+ fi
+ cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
+
+ GH_AW_PROMPT_EOF
+ cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
+ {{#runtime-import .github/workflows/test-rate-limit-defaults.md}}
+ GH_AW_PROMPT_EOF
+ - name: Substitute placeholders
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
+ with:
+ script: |
+ const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT
+ }
+ });
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
+ await main();
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/print_prompt_summary.sh
+ - name: Clean git credentials
+ run: bash /opt/gh-aw/actions/clean_git_credentials.sh
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.14.0 --skip-pull \
+ -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' \
+ 2>&1 | tee /tmp/gh-aw/agent-stdio.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: |
+ # Copy Copilot session state files to logs folder for artifact collection
+ # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
+ SESSION_STATE_DIR="$HOME/.copilot/session-state"
+ LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
+
+ if [ -d "$SESSION_STATE_DIR" ]; then
+ echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
+ mkdir -p "$LOGS_DIR"
+ cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
+ echo "Session state files copied successfully"
+ else
+ echo "No session-state directory found at $SESSION_STATE_DIR"
+ fi
+ - name: Stop MCP gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload engine output files
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: agent_outputs
+ path: |
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ if-no-files-found: ignore
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
+ await main();
+ - name: Parse MCP gateway logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: agent-artifacts
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/agent/
+ if-no-files-found: ignore
+
+ pre_activation:
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ outputs:
+ activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_REQUIRED_ROLES: admin,maintainer,write
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
+ await main();
+ - name: Check user rate limit
+ id: check_rate_limit
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_RATE_LIMIT_MAX: "5"
+ GH_AW_RATE_LIMIT_WINDOW: "60"
+ GH_AW_RATE_LIMIT_EVENTS: "issue_comment,workflow_dispatch"
+ GH_AW_RATE_LIMIT_IGNORED_ROLES: "admin,maintain,write"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
+ await main();
+
diff --git a/.github/workflows/test-rate-limit-defaults.md b/.github/workflows/test-rate-limit-defaults.md
new file mode 100644
index 0000000000..b2cbbf605b
--- /dev/null
+++ b/.github/workflows/test-rate-limit-defaults.md
@@ -0,0 +1,18 @@
+---
+name: Test Rate Limiting with Default Ignored Roles
+engine: copilot
+on:
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+rate-limit:
+ max: 5
+ window: 60
+---
+
+Test workflow to demonstrate default ignored roles behavior.
+
+By default, admin, maintain, and write users are exempt from rate limiting.
+Only triage and read users will be subject to rate limiting.
+
+Hello! This is a test workflow.
diff --git a/actions/setup/js/check_rate_limit.cjs b/actions/setup/js/check_rate_limit.cjs
index 9d00746291..48f4885322 100644
--- a/actions/setup/js/check_rate_limit.cjs
+++ b/actions/setup/js/check_rate_limit.cjs
@@ -35,41 +35,40 @@ async function main() {
const maxRuns = parseInt(process.env.GH_AW_RATE_LIMIT_MAX || "5", 10);
const windowMinutes = parseInt(process.env.GH_AW_RATE_LIMIT_WINDOW || "60", 10);
const eventsList = process.env.GH_AW_RATE_LIMIT_EVENTS || "";
- const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES || "";
+ // Default: admin, maintain, and write roles are exempt from rate limiting
+ const ignoredRolesList = process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES || "admin,maintain,write";
core.info(`🔍 Checking rate limit for user '${actor}' on workflow '${workflowId}'`);
core.info(` Configuration: max=${maxRuns} runs per ${windowMinutes} minutes`);
core.info(` Current event: ${eventName}`);
// Check if user has an ignored role (exempt from rate limiting)
- if (ignoredRolesList) {
- const ignoredRoles = ignoredRolesList.split(",").map(r => r.trim());
- core.info(` Ignored roles: ${ignoredRoles.join(", ")}`);
+ const ignoredRoles = ignoredRolesList.split(",").map(r => r.trim());
+ core.info(` Ignored roles: ${ignoredRoles.join(", ")}`);
- try {
- // Check user's permission level in the repository
- const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
- owner,
- repo,
- username: actor,
- });
-
- const userPermission = permissionData.permission;
- core.info(` User '${actor}' has permission level: ${userPermission}`);
-
- // Map GitHub permission levels to role names
- // GitHub uses: admin, maintain, write, triage, read
- if (ignoredRoles.includes(userPermission)) {
- core.info(`✅ User '${actor}' has ignored role '${userPermission}'; skipping rate limit check`);
- core.setOutput("rate_limit_ok", "true");
- return;
- }
- } catch (error) {
- // If we can't check permissions, continue with rate limiting (fail-secure)
- const errorMsg = error instanceof Error ? error.message : String(error);
- core.warning(`⚠️ Could not check user permissions: ${errorMsg}`);
- core.warning(` Continuing with rate limit check for user '${actor}'`);
+ try {
+ // Check user's permission level in the repository
+ const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner,
+ repo,
+ username: actor,
+ });
+
+ const userPermission = permissionData.permission;
+ core.info(` User '${actor}' has permission level: ${userPermission}`);
+
+ // Map GitHub permission levels to role names
+ // GitHub uses: admin, maintain, write, triage, read
+ if (ignoredRoles.includes(userPermission)) {
+ core.info(`✅ User '${actor}' has ignored role '${userPermission}'; skipping rate limit check`);
+ core.setOutput("rate_limit_ok", "true");
+ return;
}
+ } catch (error) {
+ // If we can't check permissions, continue with rate limiting (fail-secure)
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ core.warning(`⚠️ Could not check user permissions: ${errorMsg}`);
+ core.warning(` Continuing with rate limit check for user '${actor}'`);
}
// Parse events to apply rate limiting to
diff --git a/actions/setup/js/check_rate_limit.test.cjs b/actions/setup/js/check_rate_limit.test.cjs
index 39d74b3f8d..87d1584656 100644
--- a/actions/setup/js/check_rate_limit.test.cjs
+++ b/actions/setup/js/check_rate_limit.test.cjs
@@ -48,6 +48,10 @@ describe("check_rate_limit", () => {
delete process.env.GH_AW_RATE_LIMIT_MAX;
delete process.env.GH_AW_RATE_LIMIT_WINDOW;
delete process.env.GH_AW_RATE_LIMIT_EVENTS;
+ delete process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES;
+
+ // Reset repos mock
+ mockGithub.rest.repos = undefined;
// Reload the module to get fresh instance
vi.resetModules();
@@ -559,6 +563,55 @@ describe("check_rate_limit", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using workflow name: test-workflow (fallback"));
});
+ it("should use default ignored roles (admin, maintain, write) when not specified", async () => {
+ // Don't set GH_AW_RATE_LIMIT_IGNORED_ROLES, so it uses default
+
+ // Mock the permission check to return write
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
+ data: {
+ permission: "write",
+ },
+ }),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin, maintain, write"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: write"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has ignored role 'write'; skipping rate limit check"));
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ expect(mockGithub.rest.actions.listWorkflowRuns).not.toHaveBeenCalled();
+ });
+
+ it("should apply rate limiting to triage users by default", async () => {
+ // Don't set GH_AW_RATE_LIMIT_IGNORED_ROLES, so it uses default (admin, maintain, write)
+
+ mockGithub.rest.repos = {
+ getCollaboratorPermissionLevel: vi.fn().mockResolvedValue({
+ data: {
+ permission: "triage",
+ },
+ }),
+ };
+
+ mockGithub.rest.actions = {
+ listWorkflowRuns: vi.fn().mockResolvedValue({
+ data: {
+ workflow_runs: [],
+ },
+ }),
+ cancelWorkflowRun: vi.fn(),
+ };
+
+ await checkRateLimit.main();
+
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignored roles: admin, maintain, write"));
+ expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("User 'test-user' has permission level: triage"));
+ expect(mockGithub.rest.actions.listWorkflowRuns).toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("rate_limit_ok", "true");
+ });
+
it("should skip rate limiting for users with ignored roles", async () => {
process.env.GH_AW_RATE_LIMIT_IGNORED_ROLES = "admin,maintain";
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
index 6a81c91266..4e078b5071 100644
--- a/docs/RATE_LIMITING.md
+++ b/docs/RATE_LIMITING.md
@@ -23,10 +23,8 @@ on:
rate-limit:
max: 5 # Required: 1-10 runs
window: 60 # Optional: minutes (default 60, max 180)
- ignored-roles: # Optional: roles exempt from rate limiting
- - admin
- - maintain
- # events field is optional - automatically inferred from 'on:' triggers
+ # ignored-roles is optional and defaults to ["admin", "maintain", "write"]
+ # Only triage and read users are rate limited by default
---
```
@@ -45,6 +43,7 @@ rate-limit:
### `ignored-roles` (array, optional)
- List of repository roles that are exempt from rate limiting
+- **Default**: `["admin", "maintain", "write"]` - by default, users with admin, maintain, or write permissions are exempt from rate limiting
- Users with any of these roles will not be subject to rate limiting checks
- Supported roles:
- `admin` - Repository administrators
@@ -52,8 +51,9 @@ rate-limit:
- `write` - Users with write access
- `triage` - Users with triage access
- `read` - Users with read access
-- Example: `ignored-roles: [admin, maintain]` exempts admins and maintainers
-- If not specified, rate limiting applies to all users
+- Example: `ignored-roles: [admin, maintain]` exempts only admins and maintainers (write users will be rate limited)
+- To apply rate limiting to all users, set to an empty array: `ignored-roles: []`
+- To exempt only admins: `ignored-roles: [admin]`
### `events` (array, optional)
- Specific event types to apply rate limiting to
@@ -102,13 +102,22 @@ rate-limit:
```
Events are automatically inferred from the workflow's triggers. Simplest configuration.
-### Basic Rate Limiting (Default Window)
+### Basic Rate Limiting (Default Ignored Roles)
+```yaml
+rate-limit:
+ max: 5
+ window: 60
+```
+Allows 5 runs per hour. Events inferred from `on:` section. By default, admin, maintain, and write users are exempt; only triage and read users are rate limited.
+
+### Rate Limiting All Users (No Exemptions)
```yaml
rate-limit:
max: 5
window: 60
+ ignored-roles: []
```
-Allows 5 runs per hour. Events inferred from `on:` section.
+Applies rate limiting to all users including admins. Set `ignored-roles` to an empty array to disable the default exemptions.
### Explicit Event Filtering
```yaml
@@ -119,25 +128,24 @@ rate-limit:
```
Explicitly specify events to override inference. Allows only 3 runs per 30 minutes for the specified events.
-### Rate Limiting with Role Exemptions
+### Rate Limiting with Custom Role Exemptions
```yaml
rate-limit:
max: 5
window: 60
ignored-roles:
- admin
- - maintain
```
-Allows 5 runs per hour, but admins and maintainers are exempt from rate limiting entirely.
+Only admins are exempt. Maintainers and write users are rate limited (overrides the default).
### Strict Rate Limiting for Contributors
```yaml
rate-limit:
max: 3
window: 30
- ignored-roles: [admin]
+ ignored-roles: [admin, maintain]
```
-Strict rate limiting (3 runs per 30 minutes) for all users except repository admins.
+Strict rate limiting (3 runs per 30 minutes) for write, triage, and read users. Admins and maintainers are exempt.
### Generous Rate Limiting
```yaml
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index d1e3d4241d..10ec36bd20 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -6116,12 +6116,12 @@
},
"ignored-roles": {
"type": "array",
- "description": "Optional list of roles that are exempt from rate limiting. Users with any of these roles will not be subject to rate limiting checks. Common roles: 'admin', 'maintain', 'write', 'triage', 'read'.",
+ "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: []",
"items": {
"type": "string",
"enum": ["admin", "maintain", "write", "triage", "read"]
},
- "minItems": 1
+ "minItems": 0
}
},
"additionalProperties": false,
diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go
index 018d4007f4..bcd9bf6398 100644
--- a/pkg/workflow/role_checks.go
+++ b/pkg/workflow/role_checks.go
@@ -222,6 +222,10 @@ func (c *Compiler) extractRateLimitConfig(frontmatter map[string]any) *RateLimit
case string:
config.IgnoredRoles = []string{ignoredRoles}
}
+ } else {
+ // Default: admin, maintain, and write roles are exempt from rate limiting
+ config.IgnoredRoles = []string{"admin", "maintain", "write"}
+ 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)
From fc0c1901e475ae44cecbc3c855d8ecdc79c49ea2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 23:48:59 +0000
Subject: [PATCH 5/5] Move test workflows to pkg/cli/workflows and delete
docs/RATE_LIMITING.md
- Moved test-rate-limit-defaults.md to pkg/cli/workflows/
- Moved test-rate-limit-ignored-roles.md to pkg/cli/workflows/
- Deleted docs/RATE_LIMITING.md (documentation exists elsewhere)
- Lock files for test workflows are not tracked per .gitignore
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../test-rate-limit-defaults.lock.yml | 518 ------------------
.../test-rate-limit-ignored-roles.lock.yml | 518 ------------------
docs/RATE_LIMITING.md | 384 -------------
.../workflows/test-rate-limit-defaults.md | 0
.../test-rate-limit-ignored-roles.md | 0
5 files changed, 1420 deletions(-)
delete mode 100644 .github/workflows/test-rate-limit-defaults.lock.yml
delete mode 100644 .github/workflows/test-rate-limit-ignored-roles.lock.yml
delete mode 100644 docs/RATE_LIMITING.md
rename {.github => pkg/cli}/workflows/test-rate-limit-defaults.md (100%)
rename {.github => pkg/cli}/workflows/test-rate-limit-ignored-roles.md (100%)
diff --git a/.github/workflows/test-rate-limit-defaults.lock.yml b/.github/workflows/test-rate-limit-defaults.lock.yml
deleted file mode 100644
index 5595192ae8..0000000000
--- a/.github/workflows/test-rate-limit-defaults.lock.yml
+++ /dev/null
@@ -1,518 +0,0 @@
-#
-# ___ _ _
-# / _ \ | | (_)
-# | |_| | __ _ ___ _ __ | |_ _ ___
-# | _ |/ _` |/ _ \ '_ \| __| |/ __|
-# | | | | (_| | __/ | | | |_| | (__
-# \_| |_/\__, |\___|_| |_|\__|_|\___|
-# __/ |
-# _ _ |___/
-# | | | | / _| |
-# | | | | ___ _ __ _ __| |_| | _____ ____
-# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
-# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
-# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
-#
-# This file was automatically generated by gh-aw. DO NOT EDIT.
-#
-# To update this file, edit the corresponding .md file and run:
-# gh aw compile
-# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
-#
-#
-# frontmatter-hash: 252aafefeef415d3213c1a9d3caf249c814f4319a79c8897525436db13509f8b
-
-name: "Test Rate Limiting with Default Ignored Roles"
-"on":
- issue_comment:
- types:
- - created
- workflow_dispatch:
-
-permissions: {}
-
-concurrency:
- group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
-
-run-name: "Test Rate Limiting with Default Ignored Roles"
-
-jobs:
- activation:
- needs: pre_activation
- if: needs.pre_activation.outputs.activated == 'true'
- runs-on: ubuntu-slim
- permissions:
- contents: read
- outputs:
- comment_id: ""
- comment_repo: ""
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Check workflow file timestamps
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_WORKFLOW_FILE: "test-rate-limit-defaults.lock.yml"
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
- await main();
-
- agent:
- needs: activation
- runs-on: ubuntu-latest
- permissions:
- contents: read
- outputs:
- checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
- model: ${{ steps.generate_aw_info.outputs.model }}
- secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
- - name: Create gh-aw temp directory
- run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- - name: Checkout PR branch
- id: checkout-pr
- if: |
- github.event.pull_request
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- with:
- github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
- await main();
- - name: Generate agentic run info
- id: generate_aw_info
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const fs = require('fs');
-
- const awInfo = {
- engine_id: "copilot",
- engine_name: "GitHub Copilot CLI",
- model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
- version: "",
- agent_version: "0.0.406",
- workflow_name: "Test Rate Limiting with Default Ignored Roles",
- experimental: false,
- supports_tools_allowlist: true,
- supports_http_transport: true,
- run_id: context.runId,
- run_number: context.runNumber,
- run_attempt: process.env.GITHUB_RUN_ATTEMPT,
- repository: context.repo.owner + '/' + context.repo.repo,
- ref: context.ref,
- sha: context.sha,
- actor: context.actor,
- event_name: context.eventName,
- staged: false,
- allowed_domains: ["defaults"],
- firewall_enabled: true,
- awf_version: "v0.14.0",
- awmg_version: "",
- steps: {
- firewall: "squid"
- },
- created_at: new Date().toISOString()
- };
-
- // Write to /tmp/gh-aw directory to avoid inclusion in PR
- const tmpPath = '/tmp/gh-aw/aw_info.json';
- fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
- console.log('Generated aw_info.json at:', tmpPath);
- console.log(JSON.stringify(awInfo, null, 2));
-
- // Set model as output for reuse in other steps/jobs
- core.setOutput('model', awInfo.model);
- - name: Validate COPILOT_GITHUB_TOKEN secret
- id: validate-secret
- run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- - name: Install GitHub Copilot CLI
- run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.406
- - name: Install awf binary
- run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.14.0
- - name: Determine automatic lockdown mode for GitHub MCP server
- id: determine-automatic-lockdown
- env:
- TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- if: env.TOKEN_CHECK != ''
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
- await determineAutomaticLockdown(github, context, core);
- - name: Download container images
- run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.14.0 ghcr.io/github/gh-aw-firewall/squid:0.14.0 ghcr.io/github/gh-aw-mcpg:v0.1.0 ghcr.io/github/github-mcp-server:v0.30.3
- - name: Start MCP gateway
- id: start-mcp-gateway
- env:
- GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
- GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- run: |
- set -eo pipefail
- mkdir -p /tmp/gh-aw/mcp-config
-
- # Export gateway environment variables for MCP config and gateway script
- export MCP_GATEWAY_PORT="80"
- export MCP_GATEWAY_DOMAIN="host.docker.internal"
- MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
- echo "::add-mask::${MCP_GATEWAY_API_KEY}"
- export MCP_GATEWAY_API_KEY
- export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
- mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
- export DEBUG="*"
-
- export GH_AW_ENGINE="copilot"
- export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e 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_LOCKDOWN -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 -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
-
- mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
- {
- "mcpServers": {
- "github": {
- "type": "stdio",
- "container": "ghcr.io/github/github-mcp-server:v0.30.3",
- "env": {
- "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
- "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
- "GITHUB_READ_ONLY": "1",
- "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
- }
- }
- },
- "gateway": {
- "port": $MCP_GATEWAY_PORT,
- "domain": "${MCP_GATEWAY_DOMAIN}",
- "apiKey": "${MCP_GATEWAY_API_KEY}",
- "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
- }
- }
- GH_AW_MCP_CONFIG_EOF
- - name: Generate workflow overview
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
- await generateWorkflowOverview(core);
- - name: Create prompt with built-in context
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
- run: |
- bash /opt/gh-aw/actions/create_prompt_first.sh
- cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT"
-
- GH_AW_PROMPT_EOF
- cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
- cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
- cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
-
- The following GitHub context information is available for this workflow:
- {{#if __GH_AW_GITHUB_ACTOR__ }}
- - **actor**: __GH_AW_GITHUB_ACTOR__
- {{/if}}
- {{#if __GH_AW_GITHUB_REPOSITORY__ }}
- - **repository**: __GH_AW_GITHUB_REPOSITORY__
- {{/if}}
- {{#if __GH_AW_GITHUB_WORKSPACE__ }}
- - **workspace**: __GH_AW_GITHUB_WORKSPACE__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
- - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
- - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
- - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
- - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
- {{/if}}
- {{#if __GH_AW_GITHUB_RUN_ID__ }}
- - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
- {{/if}}
-
-
- GH_AW_PROMPT_EOF
- if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
- cat "/opt/gh-aw/prompts/pr_context_prompt.md" >> "$GH_AW_PROMPT"
- fi
- cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
-
- GH_AW_PROMPT_EOF
- cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
- {{#runtime-import .github/workflows/test-rate-limit-defaults.md}}
- GH_AW_PROMPT_EOF
- - name: Substitute placeholders
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
- with:
- script: |
- const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
-
- // Call the substitution function
- return await substitutePlaceholders({
- file: process.env.GH_AW_PROMPT,
- substitutions: {
- GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
- GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
- GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
- GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
- GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
- GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT
- }
- });
- - name: Interpolate variables and render templates
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
- await main();
- - name: Validate prompt placeholders
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
- - name: Print prompt
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- run: bash /opt/gh-aw/actions/print_prompt_summary.sh
- - name: Clean git credentials
- run: bash /opt/gh-aw/actions/clean_git_credentials.sh
- - name: Execute GitHub Copilot CLI
- id: agentic_execution
- # Copilot CLI tool arguments (sorted):
- timeout-minutes: 20
- run: |
- set -o pipefail
- sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.14.0 --skip-pull \
- -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' \
- 2>&1 | tee /tmp/gh-aw/agent-stdio.log
- env:
- COPILOT_AGENT_RUNNER_TYPE: STANDALONE
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
- GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GITHUB_HEAD_REF: ${{ github.head_ref }}
- GITHUB_REF_NAME: ${{ github.ref_name }}
- GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
- GITHUB_WORKSPACE: ${{ github.workspace }}
- XDG_CONFIG_HOME: /home/runner
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- - name: Copy Copilot session state files to logs
- if: always()
- continue-on-error: true
- run: |
- # Copy Copilot session state files to logs folder for artifact collection
- # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
- SESSION_STATE_DIR="$HOME/.copilot/session-state"
- LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
-
- if [ -d "$SESSION_STATE_DIR" ]; then
- echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
- mkdir -p "$LOGS_DIR"
- cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
- echo "Session state files copied successfully"
- else
- echo "No session-state directory found at $SESSION_STATE_DIR"
- fi
- - name: Stop MCP gateway
- if: always()
- continue-on-error: true
- env:
- MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
- MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
- GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
- run: |
- bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
- - name: Redact secrets in logs
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
- await main();
- env:
- GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
- SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
- SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Upload engine output files
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: agent_outputs
- path: |
- /tmp/gh-aw/sandbox/agent/logs/
- /tmp/gh-aw/redacted-urls.log
- if-no-files-found: ignore
- - name: Parse agent logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
- await main();
- - name: Parse MCP gateway logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
- await main();
- - name: Print firewall logs
- if: always()
- continue-on-error: true
- env:
- AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
- run: |
- # Fix permissions on firewall logs so they can be uploaded as artifacts
- # AWF runs with sudo, creating files owned by root
- sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
- awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
- - name: Upload agent artifacts
- if: always()
- continue-on-error: true
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: agent-artifacts
- path: |
- /tmp/gh-aw/aw-prompts/prompt.txt
- /tmp/gh-aw/aw_info.json
- /tmp/gh-aw/mcp-logs/
- /tmp/gh-aw/sandbox/firewall/logs/
- /tmp/gh-aw/agent-stdio.log
- /tmp/gh-aw/agent/
- if-no-files-found: ignore
-
- pre_activation:
- runs-on: ubuntu-slim
- permissions:
- actions: read
- contents: read
- outputs:
- activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Check team membership for workflow
- id: check_membership
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_REQUIRED_ROLES: admin,maintainer,write
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
- await main();
- - name: Check user rate limit
- id: check_rate_limit
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_RATE_LIMIT_MAX: "5"
- GH_AW_RATE_LIMIT_WINDOW: "60"
- GH_AW_RATE_LIMIT_EVENTS: "issue_comment,workflow_dispatch"
- GH_AW_RATE_LIMIT_IGNORED_ROLES: "admin,maintain,write"
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
- await main();
-
diff --git a/.github/workflows/test-rate-limit-ignored-roles.lock.yml b/.github/workflows/test-rate-limit-ignored-roles.lock.yml
deleted file mode 100644
index 47619a72be..0000000000
--- a/.github/workflows/test-rate-limit-ignored-roles.lock.yml
+++ /dev/null
@@ -1,518 +0,0 @@
-#
-# ___ _ _
-# / _ \ | | (_)
-# | |_| | __ _ ___ _ __ | |_ _ ___
-# | _ |/ _` |/ _ \ '_ \| __| |/ __|
-# | | | | (_| | __/ | | | |_| | (__
-# \_| |_/\__, |\___|_| |_|\__|_|\___|
-# __/ |
-# _ _ |___/
-# | | | | / _| |
-# | | | | ___ _ __ _ __| |_| | _____ ____
-# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
-# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
-# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
-#
-# This file was automatically generated by gh-aw. DO NOT EDIT.
-#
-# To update this file, edit the corresponding .md file and run:
-# gh aw compile
-# For more information: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md
-#
-#
-# frontmatter-hash: 9c5fc197f53c28f8b3798cfd83a6d7f5bbe5264e1b582fa08cfa40f0d0171ac7
-
-name: "Test Rate Limiting with Ignored Roles"
-"on":
- issue_comment:
- types:
- - created
- workflow_dispatch:
-
-permissions: {}
-
-concurrency:
- group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
-
-run-name: "Test Rate Limiting with Ignored Roles"
-
-jobs:
- activation:
- needs: pre_activation
- if: needs.pre_activation.outputs.activated == 'true'
- runs-on: ubuntu-slim
- permissions:
- contents: read
- outputs:
- comment_id: ""
- comment_repo: ""
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Check workflow file timestamps
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_WORKFLOW_FILE: "test-rate-limit-ignored-roles.lock.yml"
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
- await main();
-
- agent:
- needs: activation
- runs-on: ubuntu-latest
- permissions:
- contents: read
- outputs:
- checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
- model: ${{ steps.generate_aw_info.outputs.model }}
- secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
- - name: Create gh-aw temp directory
- run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- - name: Checkout PR branch
- id: checkout-pr
- if: |
- github.event.pull_request
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- with:
- github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
- await main();
- - name: Generate agentic run info
- id: generate_aw_info
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const fs = require('fs');
-
- const awInfo = {
- engine_id: "copilot",
- engine_name: "GitHub Copilot CLI",
- model: process.env.GH_AW_MODEL_AGENT_COPILOT || "",
- version: "",
- agent_version: "0.0.406",
- workflow_name: "Test Rate Limiting with Ignored Roles",
- experimental: false,
- supports_tools_allowlist: true,
- supports_http_transport: true,
- run_id: context.runId,
- run_number: context.runNumber,
- run_attempt: process.env.GITHUB_RUN_ATTEMPT,
- repository: context.repo.owner + '/' + context.repo.repo,
- ref: context.ref,
- sha: context.sha,
- actor: context.actor,
- event_name: context.eventName,
- staged: false,
- allowed_domains: ["defaults"],
- firewall_enabled: true,
- awf_version: "v0.14.0",
- awmg_version: "",
- steps: {
- firewall: "squid"
- },
- created_at: new Date().toISOString()
- };
-
- // Write to /tmp/gh-aw directory to avoid inclusion in PR
- const tmpPath = '/tmp/gh-aw/aw_info.json';
- fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
- console.log('Generated aw_info.json at:', tmpPath);
- console.log(JSON.stringify(awInfo, null, 2));
-
- // Set model as output for reuse in other steps/jobs
- core.setOutput('model', awInfo.model);
- - name: Validate COPILOT_GITHUB_TOKEN secret
- id: validate-secret
- run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
- env:
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- - name: Install GitHub Copilot CLI
- run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.406
- - name: Install awf binary
- run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.14.0
- - name: Determine automatic lockdown mode for GitHub MCP server
- id: determine-automatic-lockdown
- env:
- TOKEN_CHECK: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- if: env.TOKEN_CHECK != ''
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
- await determineAutomaticLockdown(github, context, core);
- - name: Download container images
- run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.14.0 ghcr.io/github/gh-aw-firewall/squid:0.14.0 ghcr.io/github/gh-aw-mcpg:v0.1.0 ghcr.io/github/github-mcp-server:v0.30.3
- - name: Start MCP gateway
- id: start-mcp-gateway
- env:
- GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
- GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- run: |
- set -eo pipefail
- mkdir -p /tmp/gh-aw/mcp-config
-
- # Export gateway environment variables for MCP config and gateway script
- export MCP_GATEWAY_PORT="80"
- export MCP_GATEWAY_DOMAIN="host.docker.internal"
- MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
- echo "::add-mask::${MCP_GATEWAY_API_KEY}"
- export MCP_GATEWAY_API_KEY
- export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
- mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
- export DEBUG="*"
-
- export GH_AW_ENGINE="copilot"
- export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e 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_LOCKDOWN -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 -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.0'
-
- mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
- {
- "mcpServers": {
- "github": {
- "type": "stdio",
- "container": "ghcr.io/github/github-mcp-server:v0.30.3",
- "env": {
- "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
- "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
- "GITHUB_READ_ONLY": "1",
- "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
- }
- }
- },
- "gateway": {
- "port": $MCP_GATEWAY_PORT,
- "domain": "${MCP_GATEWAY_DOMAIN}",
- "apiKey": "${MCP_GATEWAY_API_KEY}",
- "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
- }
- }
- GH_AW_MCP_CONFIG_EOF
- - name: Generate workflow overview
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
- await generateWorkflowOverview(core);
- - name: Create prompt with built-in context
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
- run: |
- bash /opt/gh-aw/actions/create_prompt_first.sh
- cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT"
-
- GH_AW_PROMPT_EOF
- cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
- cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
- cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
-
- The following GitHub context information is available for this workflow:
- {{#if __GH_AW_GITHUB_ACTOR__ }}
- - **actor**: __GH_AW_GITHUB_ACTOR__
- {{/if}}
- {{#if __GH_AW_GITHUB_REPOSITORY__ }}
- - **repository**: __GH_AW_GITHUB_REPOSITORY__
- {{/if}}
- {{#if __GH_AW_GITHUB_WORKSPACE__ }}
- - **workspace**: __GH_AW_GITHUB_WORKSPACE__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
- - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
- - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
- - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
- - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
- {{/if}}
- {{#if __GH_AW_GITHUB_RUN_ID__ }}
- - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
- {{/if}}
-
-
- GH_AW_PROMPT_EOF
- if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then
- cat "/opt/gh-aw/prompts/pr_context_prompt.md" >> "$GH_AW_PROMPT"
- fi
- cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
-
- GH_AW_PROMPT_EOF
- cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
- {{#runtime-import .github/workflows/test-rate-limit-ignored-roles.md}}
- GH_AW_PROMPT_EOF
- - name: Substitute placeholders
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }}
- with:
- script: |
- const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
-
- // Call the substitution function
- return await substitutePlaceholders({
- file: process.env.GH_AW_PROMPT,
- substitutions: {
- GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
- GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
- GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
- GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
- GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
- GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT
- }
- });
- - name: Interpolate variables and render templates
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
- await main();
- - name: Validate prompt placeholders
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
- - name: Print prompt
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- run: bash /opt/gh-aw/actions/print_prompt_summary.sh
- - name: Clean git credentials
- run: bash /opt/gh-aw/actions/clean_git_credentials.sh
- - name: Execute GitHub Copilot CLI
- id: agentic_execution
- # Copilot CLI tool arguments (sorted):
- timeout-minutes: 20
- run: |
- set -o pipefail
- sudo -E awf --enable-chroot --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.14.0 --skip-pull \
- -- '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' \
- 2>&1 | tee /tmp/gh-aw/agent-stdio.log
- env:
- COPILOT_AGENT_RUNNER_TYPE: STANDALONE
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
- GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GITHUB_HEAD_REF: ${{ github.head_ref }}
- GITHUB_REF_NAME: ${{ github.ref_name }}
- GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
- GITHUB_WORKSPACE: ${{ github.workspace }}
- XDG_CONFIG_HOME: /home/runner
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- run: |
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global user.name "github-actions[bot]"
- # Re-authenticate git with GitHub token
- SERVER_URL_STRIPPED="${SERVER_URL#https://}"
- git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
- echo "Git configured with standard GitHub Actions identity"
- - name: Copy Copilot session state files to logs
- if: always()
- continue-on-error: true
- run: |
- # Copy Copilot session state files to logs folder for artifact collection
- # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
- SESSION_STATE_DIR="$HOME/.copilot/session-state"
- LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
-
- if [ -d "$SESSION_STATE_DIR" ]; then
- echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
- mkdir -p "$LOGS_DIR"
- cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
- echo "Session state files copied successfully"
- else
- echo "No session-state directory found at $SESSION_STATE_DIR"
- fi
- - name: Stop MCP gateway
- if: always()
- continue-on-error: true
- env:
- MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
- MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
- GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
- run: |
- bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
- - name: Redact secrets in logs
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
- await main();
- env:
- GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
- SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
- SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Upload engine output files
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: agent_outputs
- path: |
- /tmp/gh-aw/sandbox/agent/logs/
- /tmp/gh-aw/redacted-urls.log
- if-no-files-found: ignore
- - name: Parse agent logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
- await main();
- - name: Parse MCP gateway logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- with:
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
- await main();
- - name: Print firewall logs
- if: always()
- continue-on-error: true
- env:
- AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
- run: |
- # Fix permissions on firewall logs so they can be uploaded as artifacts
- # AWF runs with sudo, creating files owned by root
- sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
- awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
- - name: Upload agent artifacts
- if: always()
- continue-on-error: true
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: agent-artifacts
- path: |
- /tmp/gh-aw/aw-prompts/prompt.txt
- /tmp/gh-aw/aw_info.json
- /tmp/gh-aw/mcp-logs/
- /tmp/gh-aw/sandbox/firewall/logs/
- /tmp/gh-aw/agent-stdio.log
- /tmp/gh-aw/agent/
- if-no-files-found: ignore
-
- pre_activation:
- runs-on: ubuntu-slim
- permissions:
- actions: read
- contents: read
- outputs:
- activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
- steps:
- - name: Checkout actions folder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- sparse-checkout: |
- actions
- persist-credentials: false
- - name: Setup Scripts
- uses: ./actions/setup
- with:
- destination: /opt/gh-aw/actions
- - name: Check team membership for workflow
- id: check_membership
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_REQUIRED_ROLES: admin,maintainer,write
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
- await main();
- - name: Check user rate limit
- id: check_rate_limit
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_RATE_LIMIT_MAX: "3"
- GH_AW_RATE_LIMIT_WINDOW: "30"
- GH_AW_RATE_LIMIT_EVENTS: "issue_comment,workflow_dispatch"
- GH_AW_RATE_LIMIT_IGNORED_ROLES: "admin,maintain"
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
- const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
- await main();
-
diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md
deleted file mode 100644
index 4e078b5071..0000000000
--- a/docs/RATE_LIMITING.md
+++ /dev/null
@@ -1,384 +0,0 @@
-# Rate Limiting for Agentic Workflows
-
-## Overview
-
-The rate limiting feature prevents users from triggering workflows too frequently, helping to:
-- Prevent abuse and resource exhaustion
-- Control costs from programmatic workflow triggers
-- Protect against accidental infinite loops
-- Ensure fair resource allocation across users
-
-## Configuration
-
-Rate limiting is configured in the workflow frontmatter using the `rate-limit` field:
-
-```yaml
----
-name: My Workflow
-engine: copilot
-on:
- workflow_dispatch:
- issue_comment:
- types: [created]
-rate-limit:
- max: 5 # Required: 1-10 runs
- window: 60 # Optional: minutes (default 60, max 180)
- # ignored-roles is optional and defaults to ["admin", "maintain", "write"]
- # Only triage and read users are rate limited by default
----
-```
-
-## Parameters
-
-### `max` (integer, **required**)
-- Maximum number of workflow runs allowed per user within the time window
-- Range: 1-10
-- Example: `max: 5` allows 5 runs per window
-
-### `window` (integer, optional)
-- Time window in minutes for rate limiting
-- Default: 60 (1 hour)
-- Range: 1-180 (up to 3 hours)
-- Example: `window: 30` creates a 30-minute window
-
-### `ignored-roles` (array, optional)
-- List of repository roles that are exempt from rate limiting
-- **Default**: `["admin", "maintain", "write"]` - by default, users with admin, maintain, or write permissions are exempt from rate limiting
-- Users with any of these roles will not be subject to rate limiting checks
-- Supported roles:
- - `admin` - Repository administrators
- - `maintain` - Users with maintain permissions
- - `write` - Users with write access
- - `triage` - Users with triage access
- - `read` - Users with read access
-- Example: `ignored-roles: [admin, maintain]` exempts only admins and maintainers (write users will be rate limited)
-- To apply rate limiting to all users, set to an empty array: `ignored-roles: []`
-- To exempt only admins: `ignored-roles: [admin]`
-
-### `events` (array, optional)
-- Specific event types to apply rate limiting to
-- **If not specified, automatically inferred from the workflow's `on:` triggers**
-- Only programmatic trigger types are included in the inference
-- Can be explicitly set to override the inference
-- Supported events:
- - `workflow_dispatch`
- - `issue_comment`
- - `pull_request_review`
- - `pull_request_review_comment`
- - `issues`
- - `pull_request`
- - `discussion_comment`
- - `discussion`
-
-## How It Works
-
-1. **Pre-Activation Check**: Rate limiting is enforced in the pre-activation job, before the main workflow runs
-2. **Role-Based Exemptions**: If `ignored-roles` is configured, the system first checks the user's repository permission level. Users with matching roles skip rate limiting entirely.
-3. **Per-User Per-Workflow**: For non-exempt users, limits are applied individually for each user and workflow
-4. **Recent Runs Query**: The system queries recent workflow runs from the GitHub API
-5. **Filtering**: Runs are filtered by:
- - Actor (user who triggered the workflow)
- - Time window (only runs within the configured window)
- - Event type (if `events` is configured)
- - Excludes the current run from the count
- - Excludes cancelled runs (cancelled runs don't count toward the limit)
- - Excludes runs that completed in less than 15 seconds (treated as failed fast/cancelled)
-6. **Progressive Aggregation**: Uses pagination with short-circuit logic for efficiency
-7. **Automatic Cancellation**: If the limit is exceeded, the current run is automatically cancelled
-
-## Examples
-
-### Automatic Event Inference (Recommended)
-```yaml
-on:
- issues:
- types: [opened]
- issue_comment:
- types: [created]
-rate-limit:
- max: 5
- window: 60
- # Events automatically inferred: [issues, issue_comment]
-```
-Events are automatically inferred from the workflow's triggers. Simplest configuration.
-
-### Basic Rate Limiting (Default Ignored Roles)
-```yaml
-rate-limit:
- max: 5
- window: 60
-```
-Allows 5 runs per hour. Events inferred from `on:` section. By default, admin, maintain, and write users are exempt; only triage and read users are rate limited.
-
-### Rate Limiting All Users (No Exemptions)
-```yaml
-rate-limit:
- max: 5
- window: 60
- ignored-roles: []
-```
-Applies rate limiting to all users including admins. Set `ignored-roles` to an empty array to disable the default exemptions.
-
-### Explicit Event Filtering
-```yaml
-rate-limit:
- max: 3
- window: 30
- events: [workflow_dispatch, issue_comment]
-```
-Explicitly specify events to override inference. Allows only 3 runs per 30 minutes for the specified events.
-
-### Rate Limiting with Custom Role Exemptions
-```yaml
-rate-limit:
- max: 5
- window: 60
- ignored-roles:
- - admin
-```
-Only admins are exempt. Maintainers and write users are rate limited (overrides the default).
-
-### Strict Rate Limiting for Contributors
-```yaml
-rate-limit:
- max: 3
- window: 30
- ignored-roles: [admin, maintain]
-```
-Strict rate limiting (3 runs per 30 minutes) for write, triage, and read users. Admins and maintainers are exempt.
-
-### Generous Rate Limiting
-```yaml
-rate-limit:
- max: 10
- window: 120
-```
-Allows 10 runs per 2 hours. Events inferred from triggers.
-
-## Behavior Details
-
-### When Rate Limit is Exceeded
-- The workflow run is automatically cancelled
-- A warning message is logged with details:
- - Current run count
- - Maximum allowed
- - Time window
-- The activation output is set to false, preventing the main job from running
-
-### Logging
-The rate limit check provides extensive logging:
-```
-🔍 Checking rate limit for user 'username' on workflow 'workflow-name'
- Configuration: max=5 runs per 60 minutes
- Current event: workflow_dispatch
- Ignored roles: admin, maintain
- User 'username' has permission level: write
- Time window: runs created after 2026-02-11T11:24:33.098Z
-📊 Querying workflow runs for 'workflow-name'...
- Fetching page 1 (up to 100 runs per page)...
- Retrieved 10 runs from page 1
- Skipping run 123457 - cancelled (status: cancelled)
- ✓ Run #5 (123456) by username - event: workflow_dispatch, created: 2026-02-11T11:15:00.000Z, status: completed
-📈 Rate limit summary for user 'username':
- Total recent runs in last 60 minutes: 3
- Maximum allowed: 5
- Breakdown by event type:
- - workflow_dispatch: 2 runs
- - issue_comment: 1 runs
-✅ Rate limit check passed
- User 'username' has 3 runs in the last 60 minutes
- Remaining quota: 2 runs
-```
-
-When a user has an ignored role:
-```
-🔍 Checking rate limit for user 'admin-user' on workflow 'workflow-name'
- Configuration: max=5 runs per 60 minutes
- Current event: workflow_dispatch
- Ignored roles: admin, maintain
- User 'admin-user' has permission level: admin
-✅ User 'admin-user' has ignored role 'admin'; skipping rate limit check
-```
-
-### Error Handling
-- **Fail-Open**: If the rate limit check fails due to API errors, the workflow is allowed to proceed
-- This ensures that temporary API issues don't block legitimate workflow runs
-- Errors are logged with details for troubleshooting
-
-### Performance Optimization
-- **Short-Circuit Logic**: Stops querying additional pages once the limit is reached
-- **Progressive Filtering**: Filters by actor and time window progressively
-- **Pagination**: Efficiently handles workflows with many runs
-
-## Integration with Pre-Activation Job
-
-The rate limit check is automatically added to the pre-activation job when configured:
-
-```yaml
-jobs:
- pre-activation:
- runs-on: ubuntu-latest
- outputs:
- activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
- steps:
- - name: Check team membership
- # ... membership check ...
-
- - name: Check user rate limit
- id: check_rate_limit
- uses: actions/github-script@v8
- env:
- GH_AW_RATE_LIMIT_MAX: "5"
- GH_AW_RATE_LIMIT_WINDOW: "60"
- GH_AW_RATE_LIMIT_EVENTS: workflow_dispatch,issue_comment
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { main } = require('/opt/gh-aw/actions/check_rate_limit.cjs');
- await main();
-```
-
-The activation output combines all pre-activation checks using AND logic, so the workflow only proceeds if all checks pass.
-
-## Use Cases
-
-### Preventing Abuse
-```yaml
-rate-limit:
- max: 3
- window: 60
- events: [workflow_dispatch]
-```
-Limits manual workflow triggers to prevent spam or abuse.
-
-### Cost Control
-```yaml
-rate-limit:
- max: 10
- window: 120
-```
-Controls costs by limiting how often expensive workflows can be triggered.
-
-### Fair Resource Allocation
-```yaml
-rate-limit:
- max: 5
- window: 30
-```
-Ensures fair access to shared resources across multiple users.
-
-### Development vs Production
-Development workflows might have stricter limits:
-```yaml
-# Development
-rate-limit:
- max: 3
- window: 30
-
-# Production
-rate-limit:
- max: 20
- window: 60
-```
-
-## Testing
-
-A test workflow is provided at `.github/workflows/test-rate-limit.md`:
-
-```yaml
----
-name: Test Rate Limiting
-engine: copilot
-on:
- workflow_dispatch:
- issue_comment:
- types: [created]
-rate-limit:
- max: 3
- window: 30
- events: [workflow_dispatch, issue_comment]
----
-
-Test workflow to demonstrate rate limiting functionality.
-This workflow limits each user to 3 runs within a 30-minute window.
-```
-
-To test:
-1. Trigger the workflow manually 4 times in quick succession
-2. The 4th run should be automatically cancelled with a rate limit warning
-3. Wait 30 minutes for the window to reset
-4. Trigger again to confirm the limit resets
-
-## Troubleshooting
-
-### Rate Limit Not Working
-- Check that `rate-limit` is in the workflow frontmatter
-- Verify the schema is valid (run `gh aw compile`)
-- Check pre-activation job logs for rate limit check output
-
-### Ignored Roles Not Working
-- Verify the user has the expected repository permission level
-- Check pre-activation job logs for "User 'username' has permission level: X" message
-- Ensure the role name matches exactly (e.g., "maintain" not "maintainer")
-- If permission check fails, rate limiting will still be applied (fail-secure behavior)
-
-### Unexpected Cancellations
-- Review the rate limit configuration (`max` and `window`)
-- Check if other users are triggering the same workflow
-- Verify event filters are configured correctly
-- Check if the user has an ignored role that should exempt them
-
-### API Errors
-- Rate limit checks fail-open on API errors
-- Check GitHub API status if issues persist
-- Review workflow run logs for detailed error messages
-- Permission checks fail-secure: if unable to check roles, rate limiting is applied
-
-## Schema Definition
-
-The rate-limit field is validated against this JSON schema:
-
-```json
-{
- "type": "object",
- "required": ["max"],
- "properties": {
- "max": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
- },
- "window": {
- "type": "integer",
- "minimum": 1,
- "maximum": 180,
- "default": 60
- },
- "events": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "workflow_dispatch",
- "issue_comment",
- "pull_request_review",
- "pull_request_review_comment",
- "issues",
- "pull_request",
- "discussion_comment",
- "discussion"
- ]
- },
- "minItems": 1
- },
- "ignored-roles": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": ["admin", "maintain", "write", "triage", "read"]
- },
- "minItems": 1
- }
- }
-}
-```
diff --git a/.github/workflows/test-rate-limit-defaults.md b/pkg/cli/workflows/test-rate-limit-defaults.md
similarity index 100%
rename from .github/workflows/test-rate-limit-defaults.md
rename to pkg/cli/workflows/test-rate-limit-defaults.md
diff --git a/.github/workflows/test-rate-limit-ignored-roles.md b/pkg/cli/workflows/test-rate-limit-ignored-roles.md
similarity index 100%
rename from .github/workflows/test-rate-limit-ignored-roles.md
rename to pkg/cli/workflows/test-rate-limit-ignored-roles.md