diff --git a/.github/actions/select-copilot-pat/README.md b/.github/actions/select-copilot-pat/README.md deleted file mode 100644 index 6d16d6b55d..0000000000 --- a/.github/actions/select-copilot-pat/README.md +++ /dev/null @@ -1,187 +0,0 @@ -# Select Copilot PAT - -Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. - -**This is a stop-gap workaround.** As soon as organization/enterprise billing is offered for agentic workflows, this approach will be removed from our workflows. - -## Repository Onboarding - -To use Agentic Workflows in a dotnet org repository: - -1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. -2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`. -3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. - -> **Optional:** If you plan to manage secrets or workflows from the command line (e.g., `gh aw secrets set`), [install the `gh aw` CLI extension][cli-setup]: -> -> ```sh -> gh extension install github/gh-aw -> ``` - -## PAT Management - -Team members provide PATs into the pools for the repository by adding them as repository secrets with secret names matching the pattern of `_<0-9>`, such as `COPILOT_PAT_0`. - -[Use this link to prefill the PAT creation form with the required settings][create-pat]: - -1. **Resource owner** is your **user account**, not an organization. -2. **Copilot Requests (Read)** must be the only permission granted. -3. **8-day expiration** must be used, which enforces a weekly renewal. -4. **Repository access** set to **Public repositories** only. - -The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. - -Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the repository secrets. With an 8-day expiration, renewal can be done on the same day each week. - -PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets** and matching the `_<0-9>` naming convention. This can also be done using the GitHub CLI. - -```sh -gh aw secrets set "_<0-9>" --value "" --repo dotnet/ -``` - -## Workflow Output Attribution - -Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token. - -## Usage - -Add the following frontmatter at the top-level of an agentic workflow. These elements are not supported through [imports][imports], so they must be copied into all workflows. - -Up to 10 `SECRET_#` environment variables can be passed to the action, numbered 0-9. Different workflows can use different pools of PATs if desired. Change the `secrets.COPILOT_PAT_0` through `secrets.COPILOT_PAT_9` secret names in both the `select-copilot-pat` step `env` values and in the `case` expression under the `engine: env` configuration. - -```yml -on: - # ... your workflow's real triggers go here (schedule, issues, workflow_dispatch, etc.) ... - - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) before - # the activation gate, so its `copilot_pat_number` output is available to the - # activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] - -# Custom job that randomly selects one PAT number from the pool of secrets. -# It MUST be a user-defined (non-built-in) job: because it is referenced in -# `engine: env`, the compiler wires it as a *direct* dependency of the agent -# job, so `needs.select_copilot_pat.outputs.*` resolves at runtime in BOTH the -# activation and agent jobs. (Referencing the built-in `pre_activation` job here -# does NOT work: the agent job only depends on `activation`, so -# `needs.pre_activation.*` evaluates to an empty string in the agent job and the -# rotation silently falls back to the default token.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - # Optional: mirror your workflow's top-level `if:` here so PAT selection is - # gated the same way (e.g. skip scheduled runs on forks). If that condition - # starts with `!`, wrap it in parentheses — see the note after this block. - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression - SECRET_0: ${{ secrets.COPILOT_PAT_0 }} - SECRET_1: ${{ secrets.COPILOT_PAT_1 }} - SECRET_2: ${{ secrets.COPILOT_PAT_2 }} - SECRET_3: ${{ secrets.COPILOT_PAT_3 }} - SECRET_4: ${{ secrets.COPILOT_PAT_4 }} - SECRET_5: ${{ secrets.COPILOT_PAT_5 }} - SECRET_6: ${{ secrets.COPILOT_PAT_6 }} - SECRET_7: ${{ secrets.COPILOT_PAT_7 }} - SECRET_8: ${{ secrets.COPILOT_PAT_8 }} - SECRET_9: ${{ secrets.COPILOT_PAT_9 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. -engine: - id: copilot - env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.select_copilot_pat.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.select_copilot_pat.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} -``` - -> **Why a custom job and `on.needs` (not `pre_activation`)?** The agent job that -> runs the engine only depends on the built-in `activation` job. GitHub Actions' -> `needs` context exposes **only direct dependencies**, so a -> `needs.pre_activation.*` reference inside `engine: env` evaluates to an empty -> string in the agent job — the `case()` silently falls back to the default -> `COPILOT_GITHUB_TOKEN` and rotation never reaches the agent. Declaring a -> user-defined `select_copilot_pat` job and referencing it in `engine: env` -> makes the compiler add it as a **direct** dependency of the agent job, so the -> value resolves correctly. `on.needs` makes it run before the activation gate. - -> **gh-aw note (fork guards / `if:` starting with `!`).** gh-aw renders the -> top-level frontmatter `if:` onto the built-in `pre_activation` job **without** a -> `${{ }}` wrapper. A YAML scalar that starts with `!` is parsed as a tag, so an -> emitted `if: !(...)` is invalid YAML and GitHub rejects the workflow with a -> startup failure. If your top-level `if:` (or the optional `if:` you add to -> `select_copilot_pat`) starts with `!`, wrap it in parentheses so the emitted -> scalar starts with `(`, e.g. -> `if: ${{ (!(github.event_name == 'schedule' && github.event.repository.fork)) }}`. - -## Design / Security - -There are several details of this implementation that keep our workflows and repositories safe. - -1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is - provided to the `select-copilot-pat` action within the `select_copilot_pat` - job, which is a deterministic and trusted portion of the workflow (declared - as an `on.needs` dependency so it runs before activation). No untrusted - context or input is within scope during this job, and on fork pull requests - the pool secrets are simply unavailable. The action step runs within that - job, and the secrets do not get passed across contexts. The - `select-copilot-pat` action only references the secret values to determine - which values are non-empty, filtering the secret numbers to those with - values. -1. **The `select-copilot-pat` action does not require any permissions.** It - merely selects a random number from the pool of non-empty secrets and - returns the _number_ (**not the secret**). The consuming workflow uses the - returned secret number to provide the corresponding PAT to the agent job. -1. **The implementation uses existing extensibility hooks in Agentic - Workflows.** Everything is supported by `gh aw compile` in this approach, - and no hand-editing of the compiled output is required. Custom jobs, - `on.needs` (which sequences a custom job before the activation gate), and the - [secret override][secret-override] capability (which supports using a secret - with a name different from the default `COPILOT_GITHUB_TOKEN`) are all - first-class features. - -Each of the references below contributed to the design and implementation to ensure a secure and reliable design. - -## References - -- [Agentic Workflows CLI Extension][cli-setup] -- [Agentic Authoring][configure-repo] -- [Authentication][authentication] -- [Agentic Workflow Imports][imports] -- [Custom Steps][steps] -- [Custom Jobs][jobs] -- [Job Outputs][job-outputs] -- [Engine Configuration][engine] -- [Engine Environment Variables][engine-vars] -- [Case Function in Workflow Expressions][case-expression] -- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] - -[cli-setup]: https://github.github.com/gh-aw/setup/cli/ -[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository -[authentication]: https://github.github.com/gh-aw/reference/auth/ -[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 -[imports]: https://github.github.com/gh-aw/reference/imports/ -[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps -[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs -[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs -[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine -[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables -[case-expression]: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#case -[secret-override]: https://github.com/github/gh-aw/pull/18017 diff --git a/.github/actions/select-copilot-pat/action.yml b/.github/actions/select-copilot-pat/action.yml deleted file mode 100644 index d3d38a7e50..0000000000 --- a/.github/actions/select-copilot-pat/action.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: "Select Copilot PAT from Pool" -description: >- - Selects a random Copilot PAT from a numbered pool of secrets. Secrets are - passed as environment variables SECRET_0 through SECRET_9 by the calling - workflow step. - -inputs: - random-seed: - description: >- - A seed number to use for the random PAT selection, for deterministic - selection if needed. - required: false - default: "" - -outputs: - copilot_pat_number: - description: >- - The 0-9 secret number selected from the pool of specified secrets - value: ${{ steps.select-pat-number.outputs.copilot_pat_number }} - -runs: - using: composite - steps: - - id: select-pat-number - shell: bash - env: - RANDOM_SEED: ${{ inputs.random-seed }} - run: | - # Collect numbers with non-empty secrets from SECRET_0..SECRET_9. - PAT_NUMBERS=() - for i in $(seq 0 9); do - var="SECRET_${i}" - val="${!var}" - if [ -n "$val" ]; then - PAT_NUMBERS+=(${i}) - fi - done - - # If none of the secrets in the pool have values, emit a warning - # and do not set an output value. The consumer can then fall back - # to using COPILOT_GITHUB_TOKEN. - if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then - warning_message="::warning::None of the specified secrets had values " - warning_message+="(checked SECRET_0 through SECRET_9)" - echo "$warning_message" - exit 0 - fi - - # Select a random index using the seed if specified - if [ -n "$RANDOM_SEED" ]; then - RANDOM=$RANDOM_SEED - fi - - PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) - PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" - selection_message="Selected token ${PAT_NUMBER}" - selection_details="(index: ${PAT_INDEX}; pool size: ${#PAT_NUMBERS[@]})" - echo "${selection_message} ${selection_details}" - - # Set the PAT number as the output - echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/close-stale-prs.agent.lock.yml b/.github/workflows/close-stale-prs.agent.lock.yml index 62f2380ecf..fc98ea28e1 100644 --- a/.github/workflows/close-stale-prs.agent.lock.yml +++ b/.github/workflows/close-stale-prs.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f98108ee5a64f13d7dcfd90563b9816f2f12233a6db6d0c6fb6ca9c2d4dfbd0d","body_hash":"9caea2d16810798c5cdf63dbeb5d3a5866e81a594bfcc608d362984b94d7620f","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c8214e9af2530588ef924a58df3bc8fae6fa76943f7effeace54386211e91c29","body_hash":"4cfe51387f079a2efaadcf0bca4afb2c5ef2d5bfcf44f5c4ba975abcd4aeb5f1","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -24,6 +24,10 @@ # # Automatically warn about and close pull requests that have been open for more than 30 days with no recent activity. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN # - COPILOT_GITHUB_TOKEN_2 @@ -57,7 +61,7 @@ name: "Close Stale Pull Requests" on: # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "6 22 * * 1" # Friendly format: weekly on monday (scattered) @@ -79,8 +83,8 @@ run-name: "Close Stale Pull Requests" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: > needs.pre_activation.outputs.activated == 'true' && ((!(github.event_name == 'schedule' && github.event.repository.fork))) runs-on: ubuntu-slim @@ -143,7 +147,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -204,20 +208,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_c7a73d6fea80656e_EOF' + cat << 'GH_AW_PROMPT_69dad1be80a3178a_EOF' - GH_AW_PROMPT_c7a73d6fea80656e_EOF + GH_AW_PROMPT_69dad1be80a3178a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_c7a73d6fea80656e_EOF' + cat << 'GH_AW_PROMPT_69dad1be80a3178a_EOF' Tools: add_comment(max:30), close_pull_request(max:25), missing_tool, missing_data, noop - GH_AW_PROMPT_c7a73d6fea80656e_EOF + GH_AW_PROMPT_69dad1be80a3178a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_c7a73d6fea80656e_EOF' + cat << 'GH_AW_PROMPT_69dad1be80a3178a_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -246,12 +250,12 @@ jobs: {{/if}} - GH_AW_PROMPT_c7a73d6fea80656e_EOF + GH_AW_PROMPT_69dad1be80a3178a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_c7a73d6fea80656e_EOF' + cat << 'GH_AW_PROMPT_69dad1be80a3178a_EOF' {{#runtime-import .github/workflows/close-stale-prs.agent.md}} - GH_AW_PROMPT_c7a73d6fea80656e_EOF + GH_AW_PROMPT_69dad1be80a3178a_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -333,7 +337,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -465,9 +469,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2631193641b3bf59_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2474665b5cd5f5e6_EOF' {"add_comment":{"max":30},"close_pull_request":{"max":25},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_2631193641b3bf59_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_2474665b5cd5f5e6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -680,7 +684,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_1f18229c83506a73_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_b0d9dd6902a73b81_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -721,7 +725,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_1f18229c83506a73_EOF + GH_AW_MCP_CONFIG_b0d9dd6902a73b81_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -778,7 +782,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -971,8 +975,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true') @@ -1280,7 +1284,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1339,8 +1343,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool if: (!(github.event_name == 'schedule' && github.event.repository.fork)) runs-on: ubuntu-slim outputs: @@ -1473,41 +1548,3 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - diff --git a/.github/workflows/close-stale-prs.agent.md b/.github/workflows/close-stale-prs.agent.md index eaedfe869e..9935757f0a 100644 --- a/.github/workflows/close-stale-prs.agent.md +++ b/.github/workflows/close-stale-prs.agent.md @@ -5,75 +5,29 @@ on: schedule: weekly on monday workflow_dispatch: # Allow manual triggering - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] # Don't run scheduled triggers on forked repositories — forks lack the # secrets and context required, and scheduled runs would consume the # fork owner's minutes. if: ${{ (!(github.event_name == 'schedule' && github.event.repository.fork)) }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} safe-outputs: close-pull-request: diff --git a/.github/workflows/devops-health-check.lock.yml b/.github/workflows/devops-health-check.lock.yml index 1594e92634..bbf80903b9 100644 --- a/.github/workflows/devops-health-check.lock.yml +++ b/.github/workflows/devops-health-check.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"0a2fdda77f695b11fab0a7e471a8f536eac5af24c3723f88b3684f5774371a98","body_hash":"42ff457590a45eb7666d3a96c98289620666e223350359fe081d2c7ad94cc775","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bc5c90dcd7b4f43dea16b6fcbadcad0b718089a033facf4fe33022bd090f26db","body_hash":"be856213a36cd152298ca23be5b2f04410f00f6966556a5b9be5bad9a9029dd8","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -27,6 +27,7 @@ # Resolved workflow manifest: # Imports: # - ../aw/shared/devops-health.lock.md +# - shared/pat_pool.md # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -63,7 +64,7 @@ name: "DevOps Daily Health Check" on: # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "0 3 * * *" workflow_dispatch: @@ -84,8 +85,8 @@ run-name: "DevOps Daily Health Check" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: > needs.pre_activation.outputs.activated == 'true' && ((!(github.event_name == 'schedule' && github.event.repository.fork))) runs-on: ubuntu-slim @@ -148,7 +149,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -209,21 +210,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_05deb90b21215baa_EOF' + cat << 'GH_AW_PROMPT_42fd1a44dc465956_EOF' - GH_AW_PROMPT_05deb90b21215baa_EOF + GH_AW_PROMPT_42fd1a44dc465956_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_05deb90b21215baa_EOF' + cat << 'GH_AW_PROMPT_42fd1a44dc465956_EOF' Tools: add_comment, create_issue, update_issue, dispatch_workflow(max:5), missing_tool, missing_data, noop - GH_AW_PROMPT_05deb90b21215baa_EOF + GH_AW_PROMPT_42fd1a44dc465956_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_05deb90b21215baa_EOF' + cat << 'GH_AW_PROMPT_42fd1a44dc465956_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -252,13 +253,13 @@ jobs: {{/if}} - GH_AW_PROMPT_05deb90b21215baa_EOF + GH_AW_PROMPT_42fd1a44dc465956_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_05deb90b21215baa_EOF' + cat << 'GH_AW_PROMPT_42fd1a44dc465956_EOF' {{#runtime-import .github/aw/shared/devops-health.lock.md}} {{#runtime-import .github/workflows/devops-health-check.md}} - GH_AW_PROMPT_05deb90b21215baa_EOF + GH_AW_PROMPT_42fd1a44dc465956_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -346,7 +347,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: actions: read @@ -495,9 +496,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_488416705133dc4d_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1b6f2104f67637fa_EOF' {"add_comment":{"max":1,"target":"*"},"create_issue":{"max":1},"create_report_incomplete_issue":{},"dispatch_workflow":{"aw_context_workflows":["devops-health-investigate"],"max":5,"workflow_files":{"devops-health-investigate":".lock.yml"},"workflows":["devops-health-investigate"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_488416705133dc4d_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_1b6f2104f67637fa_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -837,7 +838,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_226a912dd104ff93_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_90e4b0ffb01718b2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -878,7 +879,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_226a912dd104ff93_EOF + GH_AW_MCP_CONFIG_90e4b0ffb01718b2_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -955,7 +956,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1166,8 +1167,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat - update_cache_memory if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1478,7 +1479,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1537,8 +1538,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool if: (!(github.event_name == 'schedule' && github.event.repository.fork)) runs-on: ubuntu-slim outputs: @@ -1674,44 +1746,6 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - update_cache_memory: needs: - activation diff --git a/.github/workflows/devops-health-check.md b/.github/workflows/devops-health-check.md index 17a9227260..bc76892255 100644 --- a/.github/workflows/devops-health-check.md +++ b/.github/workflows/devops-health-check.md @@ -14,82 +14,34 @@ on: - cron: "0 3 * * *" # 03:00 UTC daily workflow_dispatch: - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] # Don't run scheduled triggers on forked repositories — forks lack the # secrets and context required, and scheduled runs would consume the # fork owner's minutes. if: ${{ (!(github.event_name == 'schedule' && github.event.repository.fork)) }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read actions: read issues: read +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### imports: + - shared/pat_pool.md - ../aw/shared/devops-health.lock.md tools: diff --git a/.github/workflows/devops-health-groom.lock.yml b/.github/workflows/devops-health-groom.lock.yml index 95539c2022..343f8906ca 100644 --- a/.github/workflows/devops-health-groom.lock.yml +++ b/.github/workflows/devops-health-groom.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"72c1eec69f267d5e6bff33076cdc059aac4c72badcd0c140978bb4a400d0082d","body_hash":"df53a0c6f85e107db36f4a7fd417baacabad2f752a860a58b5d44038ad31077e","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bbbea0ebd573002e1a5febfab0606cce2d5918a8546663e6de8b2fb32b4de6f2","body_hash":"3b85e712d28aa141fe0b5acf7740c6c46d143d83c2be14a940e05c1ef66defbd","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -27,6 +27,7 @@ # Resolved workflow manifest: # Imports: # - ../aw/shared/devops-health.lock.md +# - shared/pat_pool.md # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -60,7 +61,7 @@ name: "DevOps Health — Groom Dashboard" on: # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "0 6 * * *" workflow_dispatch: @@ -81,8 +82,8 @@ run-name: "DevOps Health — Groom Dashboard" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: > needs.pre_activation.outputs.activated == 'true' && ((!(github.event_name == 'schedule' && github.event.repository.fork))) runs-on: ubuntu-slim @@ -145,7 +146,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -206,20 +207,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_1c1b782469b09401_EOF' + cat << 'GH_AW_PROMPT_549dd87e79d272e5_EOF' - GH_AW_PROMPT_1c1b782469b09401_EOF + GH_AW_PROMPT_549dd87e79d272e5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_1c1b782469b09401_EOF' + cat << 'GH_AW_PROMPT_549dd87e79d272e5_EOF' Tools: update_issue, hide_comment(max:50), missing_tool, missing_data, noop - GH_AW_PROMPT_1c1b782469b09401_EOF + GH_AW_PROMPT_549dd87e79d272e5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_1c1b782469b09401_EOF' + cat << 'GH_AW_PROMPT_549dd87e79d272e5_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -248,13 +249,13 @@ jobs: {{/if}} - GH_AW_PROMPT_1c1b782469b09401_EOF + GH_AW_PROMPT_549dd87e79d272e5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_1c1b782469b09401_EOF' + cat << 'GH_AW_PROMPT_549dd87e79d272e5_EOF' {{#runtime-import .github/aw/shared/devops-health.lock.md}} {{#runtime-import .github/workflows/devops-health-groom.md}} - GH_AW_PROMPT_1c1b782469b09401_EOF + GH_AW_PROMPT_549dd87e79d272e5_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -336,7 +337,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: actions: read @@ -467,9 +468,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_4b67bcf64d527c53_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_caf894bd59c1cd6c_EOF' {"create_report_incomplete_issue":{},"hide_comment":{"allowed_reasons":["outdated","resolved"],"max":50},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_4b67bcf64d527c53_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_caf894bd59c1cd6c_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -718,7 +719,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_64a7063e7705979c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_9c465ad8ccded89f_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -762,7 +763,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_64a7063e7705979c_EOF + GH_AW_MCP_CONFIG_9c465ad8ccded89f_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -837,7 +838,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1032,8 +1033,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true') @@ -1340,7 +1341,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1399,8 +1400,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool if: (!(github.event_name == 'schedule' && github.event.repository.fork)) runs-on: ubuntu-slim outputs: @@ -1530,41 +1602,3 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - diff --git a/.github/workflows/devops-health-groom.md b/.github/workflows/devops-health-groom.md index c1a4b5b59b..2fc4b2fcbf 100644 --- a/.github/workflows/devops-health-groom.md +++ b/.github/workflows/devops-health-groom.md @@ -10,82 +10,34 @@ on: - cron: "0 6 * * *" # 06:00 UTC daily (3h after health check) workflow_dispatch: - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] # Don't run scheduled triggers on forked repositories — forks lack the # secrets and context required, and scheduled runs would consume the # fork owner's minutes. if: ${{ (!(github.event_name == 'schedule' && github.event.repository.fork)) }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read actions: read issues: read +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### imports: + - shared/pat_pool.md - ../aw/shared/devops-health.lock.md tools: diff --git a/.github/workflows/devops-health-investigate.lock.yml b/.github/workflows/devops-health-investigate.lock.yml index 32f3434ff9..0e60ad9ebd 100644 --- a/.github/workflows/devops-health-investigate.lock.yml +++ b/.github/workflows/devops-health-investigate.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ee29ca97c2a03f3ba3f332142713417e8a76fc2a91d400daf0eec6c2eda6a970","body_hash":"73a044497f0e054bc45cac64ecbe2e3cdfeb85425ac6ab7de1cfdda007e14fbd","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4fda3bb0d660353cadb6f2dc3f9d968f7599eb4ec2220cbe1d654017129800c4","body_hash":"8a0ee37353425842ad0e7226a6f87139333b3c5d9d78b2ba027424c90487bc55","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -27,6 +27,7 @@ # Resolved workflow manifest: # Imports: # - ../aw/shared/devops-investigate.lock.md +# - shared/pat_pool.md # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -61,7 +62,7 @@ name: "DevOps Health — Deep Investigation" on: # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job workflow_dispatch: inputs: aw_context: @@ -101,8 +102,8 @@ run-name: "DevOps Health — Deep Investigation" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: @@ -164,7 +165,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -232,20 +233,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_86ec03f3567a8c5c_EOF' + cat << 'GH_AW_PROMPT_5ed10bab1f9c457b_EOF' - GH_AW_PROMPT_86ec03f3567a8c5c_EOF + GH_AW_PROMPT_5ed10bab1f9c457b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_86ec03f3567a8c5c_EOF' + cat << 'GH_AW_PROMPT_5ed10bab1f9c457b_EOF' Tools: add_comment, missing_tool, missing_data, noop - GH_AW_PROMPT_86ec03f3567a8c5c_EOF + GH_AW_PROMPT_5ed10bab1f9c457b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_86ec03f3567a8c5c_EOF' + cat << 'GH_AW_PROMPT_5ed10bab1f9c457b_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -274,13 +275,13 @@ jobs: {{/if}} - GH_AW_PROMPT_86ec03f3567a8c5c_EOF + GH_AW_PROMPT_5ed10bab1f9c457b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_86ec03f3567a8c5c_EOF' + cat << 'GH_AW_PROMPT_5ed10bab1f9c457b_EOF' {{#runtime-import .github/aw/shared/devops-investigate.lock.md}} {{#runtime-import .github/workflows/devops-health-investigate.md}} - GH_AW_PROMPT_86ec03f3567a8c5c_EOF + GH_AW_PROMPT_5ed10bab1f9c457b_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -383,7 +384,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: actions: read @@ -515,9 +516,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_5db388f3d239112f_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_afe51687fdbd716b_EOF' {"add_comment":{"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_5db388f3d239112f_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_afe51687fdbd716b_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -711,7 +712,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_179fd96c07103880_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_903f9364ffb9ce0e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -752,7 +753,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_179fd96c07103880_EOF + GH_AW_MCP_CONFIG_903f9364ffb9ce0e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -829,7 +830,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1022,8 +1023,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true') @@ -1331,7 +1332,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1390,8 +1391,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool runs-on: ubuntu-slim outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} @@ -1523,40 +1595,3 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - diff --git a/.github/workflows/devops-health-investigate.md b/.github/workflows/devops-health-investigate.md index 0946c168d1..e606f99c4f 100644 --- a/.github/workflows/devops-health-investigate.md +++ b/.github/workflows/devops-health-investigate.md @@ -30,72 +30,19 @@ on: description: "Unique ID linking this investigation to the health check run" required: true - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] concurrency: group: gh-aw-${{ github.workflow }}-${{ inputs.finding_id }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read @@ -103,7 +50,13 @@ permissions: issues: read pull-requests: read +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### imports: + - shared/pat_pool.md - ../aw/shared/devops-investigate.lock.md tools: diff --git a/.github/workflows/issue-investigate.lock.yml b/.github/workflows/issue-investigate.lock.yml index be7c27c649..35f3c2ddd2 100644 --- a/.github/workflows/issue-investigate.lock.yml +++ b/.github/workflows/issue-investigate.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e8986ca0ac2c20f405c459d449e48b42ab180f11b4b609612f85be80599fa58c","body_hash":"1ee23560e90292f9f579338ccf5e73faa84ea5069da6f09715501f2baaba0713","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7209753f16bd5037fc56d7746c3c47b495e87b7d47555cf28d6b59c85ddc6fac","body_hash":"807aa54507fcc78c86e18f9bd76834876260cccafa806d15e70bb92626e21ef3","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -24,6 +24,10 @@ # # Deep investigation agent triggered when the 'auto-investigate' label is added to an issue. Performs thorough analysis of the issue against the codebase and related issues, suggests optimal next steps, and creates a draft PR with the fix if the solution is clear. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN # - COPILOT_GITHUB_TOKEN_2 @@ -61,7 +65,7 @@ on: types: - labeled # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job permissions: {} @@ -73,8 +77,8 @@ run-name: "Issue Investigate" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: needs.pre_activation.outputs.activated == 'true' && (github.event.label.name == 'auto-investigate') runs-on: ubuntu-slim permissions: @@ -139,7 +143,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -212,23 +216,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_783f1720da50df1e_EOF' + cat << 'GH_AW_PROMPT_fc11a4bddf2e74f1_EOF' - GH_AW_PROMPT_783f1720da50df1e_EOF + GH_AW_PROMPT_fc11a4bddf2e74f1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_783f1720da50df1e_EOF' + cat << 'GH_AW_PROMPT_fc11a4bddf2e74f1_EOF' Tools: add_comment(max:2), create_pull_request, add_labels(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_783f1720da50df1e_EOF + GH_AW_PROMPT_fc11a4bddf2e74f1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_783f1720da50df1e_EOF' + cat << 'GH_AW_PROMPT_fc11a4bddf2e74f1_EOF' - GH_AW_PROMPT_783f1720da50df1e_EOF + GH_AW_PROMPT_fc11a4bddf2e74f1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_783f1720da50df1e_EOF' + cat << 'GH_AW_PROMPT_fc11a4bddf2e74f1_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -257,12 +261,12 @@ jobs: {{/if}} - GH_AW_PROMPT_783f1720da50df1e_EOF + GH_AW_PROMPT_fc11a4bddf2e74f1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_783f1720da50df1e_EOF' + cat << 'GH_AW_PROMPT_fc11a4bddf2e74f1_EOF' {{#runtime-import .github/workflows/issue-investigate.md}} - GH_AW_PROMPT_783f1720da50df1e_EOF + GH_AW_PROMPT_fc11a4bddf2e74f1_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -347,7 +351,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -478,9 +482,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e507d040fa37fec4_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a251ab7dc72bc1dd_EOF' {"add_comment":{"max":2},"add_labels":{"max":3},"create_pull_request":{"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_e507d040fa37fec4_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_a251ab7dc72bc1dd_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -736,7 +740,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_d3c74a7de51824b1_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_cde360f13856dbf9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -777,7 +781,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_d3c74a7de51824b1_EOF + GH_AW_MCP_CONFIG_cde360f13856dbf9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -864,7 +868,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1057,8 +1061,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true') @@ -1368,7 +1372,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1427,8 +1431,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool if: github.event.label.name == 'auto-investigate' runs-on: ubuntu-slim outputs: @@ -1610,41 +1685,3 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - if: ${{ github.event.label.name == 'auto-investigate' }} - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - diff --git a/.github/workflows/issue-investigate.md b/.github/workflows/issue-investigate.md index 326ed74290..f9df7e57ba 100644 --- a/.github/workflows/issue-investigate.md +++ b/.github/workflows/issue-investigate.md @@ -10,20 +10,10 @@ on: issues: types: [labeled] - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] # Only run when the 'auto-investigate' label is applied if: ${{ github.event.label.name == 'auto-investigate' }} @@ -31,55 +21,19 @@ if: ${{ github.event.label.name == 'auto-investigate' }} concurrency: group: gh-aw-${{ github.workflow }}-${{ github.event.issue.number }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - if: ${{ github.event.label.name == 'auto-investigate' }} - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index bdf6b8ecb0..4cfd7848fd 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9a6c3cdd0d1882951fd1d308ec586f0dc2ffe5b20ac945d9179d64b8336551da","body_hash":"2e3be4cf8c7cfff7213a3e668cf3ee77797bf767f71e111caa1b78321b69b853","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3bb4a2c6454907d80c44b59cf9a637fe78edc30c12192c667f9c0047f9e59921","body_hash":"122d19b97455587085bf085e71514edb92fc9a2e61a8c7b87c256416984d8b64","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -24,6 +24,10 @@ # # Intelligent issue triage assistant that processes new and reopened issues. Analyzes issue content, assigns area labels based on the codebase structure, determines appropriate owners from CODEOWNERS, and provides a brief actionable triage summary. Links similar issues when relevant. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN # - COPILOT_GITHUB_TOKEN_2 @@ -61,7 +65,7 @@ on: - opened - reopened # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job # roles: all # Roles processed as role check in pre-activation job workflow_dispatch: inputs: @@ -84,8 +88,8 @@ run-name: "Issue Triage" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: @@ -150,7 +154,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -224,20 +228,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_71987119e42bbc57_EOF' + cat << 'GH_AW_PROMPT_5b0fac42e298c00b_EOF' - GH_AW_PROMPT_71987119e42bbc57_EOF + GH_AW_PROMPT_5b0fac42e298c00b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_71987119e42bbc57_EOF' + cat << 'GH_AW_PROMPT_5b0fac42e298c00b_EOF' Tools: add_comment, update_issue(max:2), add_labels(max:5), missing_tool, missing_data, noop - GH_AW_PROMPT_71987119e42bbc57_EOF + GH_AW_PROMPT_5b0fac42e298c00b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_71987119e42bbc57_EOF' + cat << 'GH_AW_PROMPT_5b0fac42e298c00b_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -266,12 +270,12 @@ jobs: {{/if}} - GH_AW_PROMPT_71987119e42bbc57_EOF + GH_AW_PROMPT_5b0fac42e298c00b_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_71987119e42bbc57_EOF' + cat << 'GH_AW_PROMPT_5b0fac42e298c00b_EOF' {{#runtime-import .github/workflows/issue-triage.md}} - GH_AW_PROMPT_71987119e42bbc57_EOF + GH_AW_PROMPT_5b0fac42e298c00b_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -359,7 +363,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -489,9 +493,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_4ac9217b945a588f_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_fb75621d72d676b1_EOF' {"add_comment":{"max":1},"add_labels":{"max":5},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":2,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_4ac9217b945a588f_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_fb75621d72d676b1_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -760,7 +764,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_bae4d36063c1de1b_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_c0d999eb121b8def_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -801,7 +805,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_bae4d36063c1de1b_EOF + GH_AW_MCP_CONFIG_c0d999eb121b8def_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -877,7 +881,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1070,8 +1074,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true') @@ -1379,7 +1383,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1438,8 +1442,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool runs-on: ubuntu-slim outputs: activated: ${{ 'true' }} @@ -1559,40 +1634,3 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md index 68402999a9..41a247d057 100644 --- a/.github/workflows/issue-triage.md +++ b/.github/workflows/issue-triage.md @@ -21,72 +21,27 @@ on: # [admin, maintainer, write] and silently skip everyone else. roles: all - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] concurrency: group: gh-aw-${{ github.workflow }}-${{ github.event.issue.number || inputs.issue_number }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/markdown-linter.lock.yml b/.github/workflows/markdown-linter.lock.yml index 87681565b3..91bfcb2d56 100644 --- a/.github/workflows/markdown-linter.lock.yml +++ b/.github/workflows/markdown-linter.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"11172ff344e71c0ae5cafe9f1fea88080af906b6767bc342c77e7ef6b46ddf1e","body_hash":"37558638517d0376014f13d06ca0bfe9c88f9d62959269d093c09a5c40523e0c","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"63c4104b50f8532b60ad91989c49a1d69f57ee5ed724d795141e71388d7548d7","body_hash":"8fbb9025178e8d4337b0bceaf6873a612cc1d4e78d5de0998e8cdd77f62dad03","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"},{"repo":"super-linter/super-linter","sha":"61abc07d755095a68f4987d1c2c3d1d64408f1f9","version":"61abc07d755095a68f4987d1c2c3d1d64408f1f9"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -24,6 +24,10 @@ # # Runs Markdown quality checks using Super Linter and creates issues for violations found across the repository. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN # - COPILOT_GITHUB_TOKEN_2 @@ -60,7 +64,7 @@ name: "Markdown Linter" on: # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job schedule: - cron: "0 14 * * 1-5" workflow_dispatch: @@ -81,8 +85,8 @@ run-name: "Markdown Linter" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: > needs.pre_activation.outputs.activated == 'true' && ((!(github.event_name == 'schedule' && github.event.repository.fork))) runs-on: ubuntu-slim @@ -145,7 +149,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -207,21 +211,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_bfe7f3d0a84413d7_EOF' + cat << 'GH_AW_PROMPT_cbcf7a4327dacc31_EOF' - GH_AW_PROMPT_bfe7f3d0a84413d7_EOF + GH_AW_PROMPT_cbcf7a4327dacc31_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_bfe7f3d0a84413d7_EOF' + cat << 'GH_AW_PROMPT_cbcf7a4327dacc31_EOF' Tools: create_issue, missing_tool, missing_data, noop - GH_AW_PROMPT_bfe7f3d0a84413d7_EOF + GH_AW_PROMPT_cbcf7a4327dacc31_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_bfe7f3d0a84413d7_EOF' + cat << 'GH_AW_PROMPT_cbcf7a4327dacc31_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -250,12 +254,12 @@ jobs: {{/if}} - GH_AW_PROMPT_bfe7f3d0a84413d7_EOF + GH_AW_PROMPT_cbcf7a4327dacc31_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_bfe7f3d0a84413d7_EOF' + cat << 'GH_AW_PROMPT_cbcf7a4327dacc31_EOF' {{#runtime-import .github/workflows/markdown-linter.md}} - GH_AW_PROMPT_bfe7f3d0a84413d7_EOF + GH_AW_PROMPT_cbcf7a4327dacc31_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -349,7 +353,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool - super_linter runs-on: ubuntu-latest permissions: @@ -505,9 +509,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_eccf286360fc3189_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b5c69e1cdaddaf77_EOF' {"create_issue":{"expires":48,"labels":["automation","code-quality"],"max":1,"title_prefix":"[linter] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_eccf286360fc3189_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_b5c69e1cdaddaf77_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -715,7 +719,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_466bccd61bda25f8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_a58526ca7067a433_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -756,7 +760,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_466bccd61bda25f8_EOF + GH_AW_MCP_CONFIG_a58526ca7067a433_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -831,7 +835,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1042,8 +1046,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat - super_linter - update_cache_memory if: > @@ -1352,7 +1356,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1411,8 +1415,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool if: (!(github.event_name == 'schedule' && github.event.repository.fork)) runs-on: ubuntu-slim outputs: @@ -1543,44 +1618,6 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - super_linter: needs: activation runs-on: ubuntu-latest diff --git a/.github/workflows/markdown-linter.md b/.github/workflows/markdown-linter.md index df9db2ffef..f2e5bde38b 100644 --- a/.github/workflows/markdown-linter.md +++ b/.github/workflows/markdown-linter.md @@ -9,67 +9,17 @@ on: schedule: - cron: "0 14 * * 1-5" # 2 PM UTC, weekdays only - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] # Don't run scheduled triggers on forked repositories — forks lack the # secrets and context required, and scheduled runs would consume the # fork owner's minutes. if: ${{ (!(github.event_name == 'schedule' && github.event.repository.fork)) }} -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - if: ${{ !(github.event_name == 'schedule' && github.event.repository.fork) }} - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - super_linter: runs-on: ubuntu-latest permissions: @@ -116,14 +66,19 @@ jobs: path: super-linter.log retention-days: 7 -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/pr-malicious-scan.agent.lock.yml b/.github/workflows/pr-malicious-scan.agent.lock.yml index a13b24e21e..90977622ff 100644 --- a/.github/workflows/pr-malicious-scan.agent.lock.yml +++ b/.github/workflows/pr-malicious-scan.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"86a8e273b82a01b21bc0615f83747486c68b3263c9453dbe187897914e37ac91","body_hash":"312ef8e5ac993af34ea4c61c6af483c2ed6a130d95b998d113214818c754ce33","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"991e05d5ef56fe4d642836dd993118d96162ca346927ae545e230add11d1f40c","body_hash":"53ad20adab8620af32df10e5a37760ad5fa707956fafd017d08842ba95b07465","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_GITHUB_TOKEN_2","COPILOT_GITHUB_TOKEN_3","COPILOT_GITHUB_TOKEN_4","COPILOT_GITHUB_TOKEN_5","COPILOT_GITHUB_TOKEN_6","COPILOT_GITHUB_TOKEN_7","COPILOT_GITHUB_TOKEN_8","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/codeql-action/upload-sarif","sha":"7211b7c8077ea37d8641b6271f6a365a22a5fbfa","version":"v4.36.0"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -24,6 +24,10 @@ # # Static diff scan of PRs from external (non-trusted) contributors for suspicious or malicious changes. Surfaces findings as code-scanning alerts and a single maintainer-ping comment per head SHA. Never executes PR head code, never checks out the head with write tokens. # +# Resolved workflow manifest: +# Imports: +# - shared/pat_pool.md +# # Secrets used: # - COPILOT_GITHUB_TOKEN # - COPILOT_GITHUB_TOKEN_2 @@ -57,7 +61,7 @@ name: "PR Malicious Code Scan" on: # needs: # Needs processed as dependency in pre-activation job - # - select_copilot_pat # Needs processed as dependency in pre-activation job + # - pat_pool # Needs processed as dependency in pre-activation job workflow_dispatch: inputs: aw_context: @@ -80,8 +84,8 @@ run-name: "PR Malicious Code Scan" jobs: activation: needs: + - pat_pool - pre_activation - - select_copilot_pat if: needs.pre_activation.outputs.activated == 'true' && ((!github.event.repository.fork)) runs-on: ubuntu-slim permissions: @@ -143,7 +147,7 @@ jobs: id: validate-secret run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -205,20 +209,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_ec143d880143e1a1_EOF' + cat << 'GH_AW_PROMPT_139f8ed6817a67ae_EOF' - GH_AW_PROMPT_ec143d880143e1a1_EOF + GH_AW_PROMPT_139f8ed6817a67ae_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_ec143d880143e1a1_EOF' + cat << 'GH_AW_PROMPT_139f8ed6817a67ae_EOF' Tools: add_comment, add_labels(max:2), create_code_scanning_alert, missing_tool, missing_data, noop - GH_AW_PROMPT_ec143d880143e1a1_EOF + GH_AW_PROMPT_139f8ed6817a67ae_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_ec143d880143e1a1_EOF' + cat << 'GH_AW_PROMPT_139f8ed6817a67ae_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -247,12 +251,12 @@ jobs: {{/if}} - GH_AW_PROMPT_ec143d880143e1a1_EOF + GH_AW_PROMPT_139f8ed6817a67ae_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_ec143d880143e1a1_EOF' + cat << 'GH_AW_PROMPT_139f8ed6817a67ae_EOF' {{#runtime-import .github/workflows/pr-malicious-scan.agent.md}} - GH_AW_PROMPT_ec143d880143e1a1_EOF + GH_AW_PROMPT_139f8ed6817a67ae_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -338,7 +342,7 @@ jobs: agent: needs: - activation - - select_copilot_pat + - pat_pool runs-on: ubuntu-latest permissions: contents: read @@ -465,9 +469,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_d784ab8a1bcedc70_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_60febc5fc0bedbf7_EOF' {"add_comment":{"max":1},"add_labels":{"max":2},"create_code_scanning_alert":{"driver":"PR Malicious Code Scanner"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_d784ab8a1bcedc70_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_60febc5fc0bedbf7_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -720,7 +724,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_b365524ea83b3368_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_fe21e0bbf450824f_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -764,7 +768,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_b365524ea83b3368_EOF + GH_AW_MCP_CONFIG_fe21e0bbf450824f_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -842,7 +846,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent @@ -1037,8 +1041,8 @@ jobs: - activation - agent - detection + - pat_pool - safe_outputs - - select_copilot_pat - upload_code_scanning_sarif if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -1348,7 +1352,7 @@ jobs: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -1407,8 +1411,79 @@ jobs: } } + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Select Copilot token from pool + id: select-pat-number + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + env: + COPILOT_PAT_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + pre_activation: - needs: select_copilot_pat + needs: pat_pool if: (!github.event.repository.fork) runs-on: ubuntu-slim outputs: @@ -1551,44 +1626,6 @@ jobs: /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore - select_copilot_pat: - if: ${{ !github.event.repository.fork }} - runs-on: ubuntu-slim - permissions: - contents: read - - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout the select-copilot-pat action folder - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - - name: Select Copilot token from pool - id: select-copilot-pat - uses: ./.github/actions/select-copilot-pat - env: - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - upload_code_scanning_sarif: needs: safe_outputs if: needs.safe_outputs.outputs.sarif_file != '' diff --git a/.github/workflows/pr-malicious-scan.agent.md b/.github/workflows/pr-malicious-scan.agent.md index 9ce736b882..36a9172a18 100644 --- a/.github/workflows/pr-malicious-scan.agent.md +++ b/.github/workflows/pr-malicious-scan.agent.md @@ -17,20 +17,10 @@ on: description: "PR number to scan" required: true - # ############################################################### - # Override the COPILOT_GITHUB_TOKEN secret usage for the workflow - # with a randomly-selected token from a pool of secrets. - # - # As soon as organization-level billing is offered for Agentic - # Workflows, this stop-gap approach will be removed. - # - # See: /.github/actions/select-copilot-pat/README.md - # ############################################################### - # - # Run the `select_copilot_pat` custom job (defined under `jobs:` below) - # before the activation gate so its `copilot_pat_number` output is available - # to the activation and agent jobs that consume it in `engine: env`. - needs: [select_copilot_pat] + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs (which consume it in + # engine.env). See: shared/pat_pool.README.md. + needs: [pat_pool] # Skip on forks (no secrets, no point). Drafts are filtered out by the # orchestrator before dispatch. @@ -40,55 +30,19 @@ concurrency: group: gh-aw-${{ github.workflow }}-${{ inputs.pr_number }} cancel-in-progress: true -# Custom job that randomly selects one PAT number from the pool of secrets. -# It is declared as an `on.needs` dependency above so it runs before the -# activation gate. Because it is a user-defined (non-built-in) job, the compiler -# wires it as a direct dependency of the agent job, so the -# `needs.select_copilot_pat.outputs.*` reference in `engine: env` resolves -# correctly at runtime in BOTH the activation and agent jobs. (A built-in job -# such as `pre_activation` is not a direct dependency of the agent job, so a -# `needs.pre_activation.*` reference there would silently evaluate to an empty -# string — which is the failure mode this approach avoids.) -jobs: - select_copilot_pat: - runs-on: ubuntu-slim - permissions: - contents: read - if: ${{ !github.event.repository.fork }} - outputs: - copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Checkout the select-copilot-pat action folder - with: - persist-credentials: false - sparse-checkout: .github/actions/select-copilot-pat - sparse-checkout-cone-mode: true - fetch-depth: 1 - - - id: select-copilot-pat - name: Select Copilot token from pool - uses: ./.github/actions/select-copilot-pat - env: - # If the secret names are changed here, they must also be changed - # in the `engine: env` case expression below - SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} - SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} - SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} - SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} - SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} - SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} - SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} - -# Override the COPILOT_GITHUB_TOKEN expression used by the Copilot engine. -# Consume the PAT number from the select_copilot_pat job and select the corresponding secret. +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + engine: id: copilot env: - # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow - # If none of the `COPILOT_GITHUB_TOKEN_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used - COPILOT_GITHUB_TOKEN: ${{ case(needs.select_copilot_pat.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.select_copilot_pat.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.select_copilot_pat.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.select_copilot_pat.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.select_copilot_pat.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.select_copilot_pat.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.select_copilot_pat.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.select_copilot_pat.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} + # If none of the COPILOT_GITHUB_TOKEN[_#] pool secrets were selected, the default COPILOT_GITHUB_TOKEN is used. + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, secrets.COPILOT_GITHUB_TOKEN) }} permissions: contents: read diff --git a/.github/workflows/shared/pat_pool.README.md b/.github/workflows/shared/pat_pool.README.md new file mode 100644 index 0000000000..b9cfbcac01 --- /dev/null +++ b/.github/workflows/shared/pat_pool.README.md @@ -0,0 +1,182 @@ +# PAT Pool + +Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. + +**This is a stop-gap workaround.** As soon as organization/enterprise billing is offered for agentic workflows, this approach will be removed from our workflows. + +## Repository Onboarding + +To use Agentic Workflows in a dotnet org repository: + +1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. Use `gh aw` **v0.71.5 or newer**, which supports the agent job dependencies required for this implementation. +2. Copy the `pat_pool.md` and `pat_pool.README.md` files into the repository under `.github/workflows/shared`. +3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. + +**Install or upgrade the `gh aw` CLI and check the version** + +```sh +gh extension install github/gh-aw --force +gh aw --version +``` + +## PAT Management + +Team members provide PATs into the pool for the repository by adding them as repository secrets. In this repository the pool is backed by the `COPILOT_GITHUB_TOKEN` secret (pool slot `0`) and the numbered `COPILOT_GITHUB_TOKEN_2` through `COPILOT_GITHUB_TOKEN_8` secrets (pool slots `1` through `7`). The first slot reuses the default `COPILOT_GITHUB_TOKEN` so a single-PAT repository still works without any extra secrets. + +[Use this link to prefill the PAT creation form with the required settings][create-pat]: + +1. **Resource owner** is your **user account**, not an organization. +2. **Copilot Requests (Read)** must be the only permission granted. +3. **8-day expiration** must be used, which enforces a weekly renewal. +4. **Repository access** set to **Public repositories** only. + +The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. + +Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the repository secrets. With an 8-day expiration, renewal can be done on the same day each week. + +PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets**. This can also be done using the GitHub CLI. + +```sh +gh aw secrets set "COPILOT_GITHUB_TOKEN_2" --value "" --repo / +``` + +## Workflow Output Attribution + +Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token. + +## Usage + +The [`pat_pool.md`](./pat_pool.md) workflow import defines a custom job with a `pat_number` output. Consuming workflows need three additions to their frontmatter: declare `pat_pool` in `on.needs` so it runs before the activation gate, import the job, and use the PAT number to override the `COPILOT_GITHUB_TOKEN` passed to the workflow's agent job. + +```yml +on: + # ... your workflow's real triggers go here (schedule, issues, etc.) ... + + # Run the imported pat_pool job before the activation gate so its pat_number + # output is available to the activation and agent jobs. + needs: [pat_pool] + +# ############################################################### +# Select a PAT from the pool and override COPILOT_GITHUB_TOKEN. +# When org-level billing is available, this will be removed. +# See `shared/pat_pool.README.md` for more information. +# ############################################################### +imports: + - shared/pat_pool.md + +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: | + ${{ case( + needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, + needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_2, + needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_3, + needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_4, + needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_5, + needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_6, + needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_7, + needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_8, + secrets.COPILOT_GITHUB_TOKEN) + }} +``` + +The expression can be collapsed onto a single line if desired. Declaring `on.needs: [pat_pool]` wires `pat_pool` as a dependency of the built-in `pre_activation` and `activation` jobs (in addition to the agent job, which the compiler wires automatically from the `needs.pat_pool.*` references in `engine.env`). This makes `pat_pool` run first, so the selected PAT is available both to the activation job's secret validation and to the agent's Copilot requests. + +```sh +gh aw compile +``` + +### Customizing the pool + +The import declares 8 optional inputs (`COPILOT_PAT_0` through `COPILOT_PAT_7`), each defaulting to the matching repository secret (`COPILOT_GITHUB_TOKEN`, then `COPILOT_GITHUB_TOKEN_2` through `COPILOT_GITHUB_TOKEN_8`). To point a workflow at a different pool of repository secrets, use the parameterized `uses`/`with` form when importing and pass the substitute secrets as the `COPILOT_PAT_#` inputs: + +```yml +imports: + - uses: shared/pat_pool.md + with: + COPILOT_PAT_0: ${{ secrets.MY_TEAM_PAT_0 }} + COPILOT_PAT_1: ${{ secrets.MY_TEAM_PAT_1 }} + # Unspecified inputs default to the repository's COPILOT_GITHUB_TOKEN[_#] secrets +``` + +The secrets passed via `with:` must match the secrets referenced in the consuming workflow's `case` expression that overrides `COPILOT_GITHUB_TOKEN`--both sides need to agree on which secret backs each `COPILOT_PAT_#` slot. Update the `case` expression accordingly: + +```yml +engine: + id: copilot + env: + COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.MY_TEAM_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.MY_TEAM_PAT_1, ..., secrets.COPILOT_GITHUB_TOKEN) }} +``` + +This approach aligns with GitHub's documented guidance for [passing secrets][passing-secrets] between workflows, where the `pat_pool` job returns a PAT number and the `case` statement acts as a secret store to look the PAT secret up based on the selected number. + +## Design / Security + +There are several details of this implementation that keep our workflows and repositories safe. + +1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is + provided to a dedicated step within the `pat_pool` job. That job runs + before the activation gate and contains only the trusted token-selection + step--no untrusted context or input is within scope. The + `select-pat-number` step only references the secret values to determine + which are non-empty, filtering the secret numbers to those with values. +1. **The `pat_pool` job emits only a number, never a secret.** Its sole output, + `pat_number`, is the 0-7 index of the selected PAT (or empty when the pool + is empty). The actual secret materializes only later, in the activation + job's `engine.env` mapping, where the `case()` expression resolves the + number to the matching secret. This follows GitHub's guidance for + [passing secrets][passing-secrets] between jobs or workflows, with the + `case` statement acting as a very simple secret store. +1. **The `select-pat-number` step does not require any permissions.** It + reads only the `COPILOT_PAT_#` environment variables passed to it and writes + only to `GITHUB_OUTPUT`. The job that hosts it sets `permissions:` to the + workflow defaults (no elevated scopes). +1. **The implementation uses supported Agentic Workflow extensibility hooks.** + Defining a custom job inside an [imported workflow file][imports] is + supported by `gh aw compile`. Declaring `pat_pool` in the consuming + workflow's `on.needs` wires it ahead of the built-in `pre_activation` and + `activation` jobs. The + [secret override][secret-override] capability supplies the `COPILOT_GITHUB_TOKEN` + value via `engine.env` rather than the default secret of the same name. + +Each of the references below contributed to the design and implementation to ensure a secure and reliable design. + +## Design Note: `on.needs` vs. `engine.env`-only wiring + +`gh aw compile` will attach `pat_pool` to the agent job automatically from the `needs.pat_pool.*` references in `engine.env`, but it attaches it _after_ the activation job (as `needs: activation`). That ordering is wrong for the PAT pool: the activation job's secret-validation step also reads the `engine.env` `COPILOT_GITHUB_TOKEN`, and with `pat_pool` running afterwards it would see an empty `pat_number` and validate the default token instead of the selected one. + +Declaring `on.needs: [pat_pool]` fixes the ordering by wiring `pat_pool` as a dependency of the built-in `pre_activation` and `activation` jobs, so `pat_pool` runs first. This also makes the integration work for workflows that use `roles: all` (which otherwise produce no `pre_activation` job at all). + +The `pat_pool` job intentionally declares no `needs:` of its own, so it can be pulled in front of `pre_activation` by `on.needs` without creating a dependency cycle. + +See: [Activation 'needs' does not incorporate jobs in engine.env expressions (github/gh-aw#30790)](https://github.com/github/gh-aw/issues/30790) + +## References + +- [Agentic Workflows CLI Extension][cli-setup] +- [Agentic Authoring][configure-repo] +- [Authentication][authentication] +- [Agentic Workflow Imports][imports] +- [Custom Steps][steps] +- [Custom Jobs][jobs] +- [Job Outputs][job-outputs] +- [Engine Configuration][engine] +- [Engine Environment Variables][engine-vars] +- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] +- [Case Function in Workflow Expressions][case-expression] +- [Passing a secret between jobs or workflows][passing-secrets] + +[cli-setup]: https://github.github.com/gh-aw/setup/cli/ +[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository +[authentication]: https://github.github.com/gh-aw/reference/auth/ +[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 +[imports]: https://github.github.com/gh-aw/reference/imports/ +[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps +[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs +[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs +[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine +[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables +[secret-override]: https://github.com/github/gh-aw/pull/18017 +[case-expression]: https://docs.github.com/actions/reference/workflows-and-actions/expressions#case +[passing-secrets]: https://docs.github.com/actions/reference/workflows-and-actions/workflow-commands#example-masking-and-passing-a-secret-between-jobs-or-workflows diff --git a/.github/workflows/shared/pat_pool.md b/.github/workflows/shared/pat_pool.md new file mode 100644 index 0000000000..afd1ed9468 --- /dev/null +++ b/.github/workflows/shared/pat_pool.md @@ -0,0 +1,106 @@ +--- +description: Agentic workflow import to integrate the Copilot PAT Pool + +jobs: + pat_pool: + runs-on: ubuntu-slim + outputs: + pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + steps: + - id: select-pat-number + name: Select Copilot token from pool + env: + COPILOT_PAT_0: ${{ github.aw.import-inputs.COPILOT_PAT_0 }} + COPILOT_PAT_1: ${{ github.aw.import-inputs.COPILOT_PAT_1 }} + COPILOT_PAT_2: ${{ github.aw.import-inputs.COPILOT_PAT_2 }} + COPILOT_PAT_3: ${{ github.aw.import-inputs.COPILOT_PAT_3 }} + COPILOT_PAT_4: ${{ github.aw.import-inputs.COPILOT_PAT_4 }} + COPILOT_PAT_5: ${{ github.aw.import-inputs.COPILOT_PAT_5 }} + COPILOT_PAT_6: ${{ github.aw.import-inputs.COPILOT_PAT_6 }} + COPILOT_PAT_7: ${{ github.aw.import-inputs.COPILOT_PAT_7 }} + RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} + shell: bash + run: | + # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_7. + PAT_NUMBERS=() + POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) + + for i in $(seq 0 7); do + var="COPILOT_PAT_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + POOL_INDICATORS[${i}]="🟪" + fi + done + + # If none of the entries in the pool have values, emit a warning + # and do not set an output value. The consumer can fall back to + # using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + warning_message="::warning::None of the PAT pool entries had values " + warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_7)" + echo "$warning_message" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + POOL_INDICATORS[${PAT_NUMBER}]="✅" + + echo "Pool size: ${#PAT_NUMBERS[@]}" + echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" + + # Emit a markdown table of the pool entries to the step summary + echo "|0|1|2|3|4|5|6|7|" >> "$GITHUB_STEP_SUMMARY" + echo "|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" + (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" + +import-schema: + COPILOT_PAT_0: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_PAT_1: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + COPILOT_PAT_2: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + COPILOT_PAT_3: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + COPILOT_PAT_4: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + COPILOT_PAT_5: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + COPILOT_PAT_6: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + COPILOT_PAT_7: + type: string + required: false + default: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + random_seed: + type: number + required: false + description: >- + A seed number to use for the random PAT number selection, + for deterministic selection if needed. +--- diff --git a/.github/workflows/validate-pat-pool.yml b/.github/workflows/validate-pat-pool.yml new file mode 100644 index 0000000000..64f24c0aa3 --- /dev/null +++ b/.github/workflows/validate-pat-pool.yml @@ -0,0 +1,208 @@ +name: Validate PAT Pool + +on: + schedule: + - cron: '17 2 * * *' # Daily at ~2:17 AM UTC (off-round to reduce contention) + workflow_dispatch: + +# No GitHub API permissions needed +permissions: {} + +jobs: + validate: + name: Validate Copilot PAT Pool + if: ${{ github.event_name == 'workflow_dispatch' || !github.event.repository.fork }} + runs-on: ubuntu-latest + env: + VALIDATE_PAT: | + if [ -z "$COPILOT_GITHUB_TOKEN" ]; then echo "status=empty" >> "$GITHUB_OUTPUT"; exit 0; fi + set +e; timeout 30 copilot --prompt "Say OK" --available-tools="" --silent --effort=low; rc=$?; set -e + if [ $rc -eq 0 ]; then echo "status=valid" >> "$GITHUB_OUTPUT" + elif [ $rc -eq 124 ]; then echo "status=unknown" >> "$GITHUB_OUTPUT" + else echo "status=invalid" >> "$GITHUB_OUTPUT"; fi + steps: + - name: Setup gh-aw scripts + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + + - name: Install Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55 + + # ----------------------------------------------------------- + # Make a Copilot CLI request with each PAT in the pool. + # Each step sets COPILOT_GITHUB_TOKEN directly from the secret + # via env: so the value never passes through shell variables. + # The pool maps slot 0 -> COPILOT_GITHUB_TOKEN and slots 1-7 -> + # COPILOT_GITHUB_TOKEN_2 .. COPILOT_GITHUB_TOKEN_8. + # ----------------------------------------------------------- + + - name: Validate COPILOT_GITHUB_TOKEN + id: pat0 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_2 + id: pat1 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_3 + id: pat2 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_4 + id: pat3 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_5 + id: pat4 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_6 + id: pat5 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_7 + id: pat6 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + - name: Validate COPILOT_GITHUB_TOKEN_8 + id: pat7 + continue-on-error: true + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }} + shell: bash + run: | + # copilot --prompt "Say OK" + eval "$VALIDATE_PAT" + + # ----------------------------------------------------------- + # Collect results and build the step summary + # ----------------------------------------------------------- + + - name: Build summary + if: always() + env: + S0: ${{ steps.pat0.outputs.status }} + S1: ${{ steps.pat1.outputs.status }} + S2: ${{ steps.pat2.outputs.status }} + S3: ${{ steps.pat3.outputs.status }} + S4: ${{ steps.pat4.outputs.status }} + S5: ${{ steps.pat5.outputs.status }} + S6: ${{ steps.pat6.outputs.status }} + S7: ${{ steps.pat7.outputs.status }} + shell: bash + run: | + # Build summary + statuses=("$S0" "$S1" "$S2" "$S3" "$S4" "$S5" "$S6" "$S7") + names=("COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKEN_2" "COPILOT_GITHUB_TOKEN_3" "COPILOT_GITHUB_TOKEN_4" "COPILOT_GITHUB_TOKEN_5" "COPILOT_GITHUB_TOKEN_6" "COPILOT_GITHUB_TOKEN_7" "COPILOT_GITHUB_TOKEN_8") + + valid=0; empty=0; invalid=0; unknown=0 + for s in "${statuses[@]}"; do + case "$s" in + valid) valid=$((valid + 1)) ;; + empty) empty=$((empty + 1)) ;; + invalid) invalid=$((invalid + 1)) ;; + *) unknown=$((unknown + 1)) ;; + esac + done + + { + if [ $invalid -eq 0 ] && [ $unknown -eq 0 ] && [ $valid -gt 0 ]; then + echo "> [!NOTE]" + echo "> **PAT pool is valid** — no action needed" + echo "" + fi + + if [ $invalid -eq 0 ] && [ $unknown -eq 0 ] && [ $valid -eq 0 ]; then + echo "> [!WARNING]" + echo "> **Empty PAT pool** — agentic workflows will fall back to the default \`COPILOT_GITHUB_TOKEN\`." + echo "" + fi + + if [ $invalid -gt 0 ]; then + echo "> [!CAUTION]" + echo "> **Invalid PAT pool** — agentic workflows selecting an invalid PAT will fail." + echo "" + fi + + if [ $unknown -gt 0 ]; then + echo "> [!WARNING]" + echo "> **PAT pool not verified** due to transient errors — re-run the workflow to retry." + echo "" + fi + + echo "☑️ Valid: ${valid} • ⏹️ Empty: ${empty} • ❌ Invalid: ${invalid} • ❓ Unknown: ${unknown}" + echo "" + + echo "| PAT Secret | Status |" + echo "|:-----------|:-------|" + + for i in $(seq 0 7); do + case "${statuses[$i]}" in + valid) symbol="☑️ Valid" ;; + empty) symbol="⏹️ Empty" ;; + invalid) symbol="❌ Invalid" ;; + *) symbol="❓ Unknown" ;; + esac + echo "| \`${names[$i]}\` | ${symbol} |" + done + } >> "$GITHUB_STEP_SUMMARY" + + if [ $invalid -gt 0 ]; then + echo "::error::${invalid} PAT(s) in the pool are invalid and need to be removed or replaced" + exit 1 + fi + + if [ $unknown -gt 0 ]; then + echo "::error::${unknown} PAT(s) could not be verified due to transient errors — re-run to retry" + exit 1 + fi + + if [ $valid -eq 0 ]; then + echo "::error::The PAT pool is empty — no PATs are available" + exit 1 + fi + + echo "PAT pool validation passed: ${valid} valid PAT(s)"