Skip to content

Commit c76c3f7

Browse files
authored
Add PATs rotation to agentic workflow(s) (#13496)
### Context Using a single PAT shared across all agentic workflows can lead to rate-limiting. This PR introduces a PAT-rotation mechanism to randomly select from a pool of Copilot PAT secrets. ### Changes Made - Recompiled the agentic workflows with gh-aw v0.67.1 (cron schedule time may differ as a side effect of recompilation) - Copied the relevant reusable files from https://github.com/dotnet/skills/tree/main/.github/actions/select-copilot-pat - Added the PATs rotation to our agentic workflow(s) using the `COPILOT_GITHUB_TOKEN` / `COPILOT_GITHUB_TOKEN_1..9` naming convention - Updated `README.md` in `.github/actions/select-copilot-pat` to document the `COPILOT_GITHUB_TOKEN(_#)` naming convention used in this repo (replacing the generic `COPILOT_PAT_#` placeholder examples) ### Testing - Verified that the compiled workflow correctly references `COPILOT_GITHUB_TOKEN` and `COPILOT_GITHUB_TOKEN_1` through `COPILOT_GITHUB_TOKEN_9` in both the `select-copilot-pat` step env bindings and the `case(...)` expression ### Notes The PAT pool uses `COPILOT_GITHUB_TOKEN` as the base secret (index 0) and `COPILOT_GITHUB_TOKEN_1` through `COPILOT_GITHUB_TOKEN_9` for additional pool entries. This is a stop-gap workaround until organization/enterprise billing is offered for agentic workflows.
2 parents 20e99d4 + fa13415 commit c76c3f7

File tree

5 files changed

+780
-420
lines changed

5 files changed

+780
-420
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Select Copilot PAT
2+
3+
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.
4+
5+
**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.
6+
7+
## Repository Onboarding
8+
9+
To use Agentic Workflows in a dotnet org repository:
10+
11+
1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo].
12+
2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`.
13+
3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below.
14+
15+
> **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]:
16+
>
17+
> ```sh
18+
> gh extension install github/gh-aw
19+
> ```
20+
21+
## PAT Management
22+
23+
Team members provide PATs into the pools for the repository by adding them as repository secrets. This repository uses the `COPILOT_GITHUB_TOKEN` naming convention: the base secret is named `COPILOT_GITHUB_TOKEN` (used for index 0) and additional pool entries are named `COPILOT_GITHUB_TOKEN_1` through `COPILOT_GITHUB_TOKEN_9`.
24+
25+
[Use this link to prefill the PAT creation form with the required settings][create-pat]:
26+
27+
1. **Resource owner** is your **user account**, not an organization.
28+
2. **Copilot Requests (Read)** must be the only permission granted.
29+
3. **8-day expiration** must be used, which enforces a weekly renewal.
30+
4. **Repository access** set to **Public repositories** only.
31+
32+
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.
33+
34+
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.
35+
36+
PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets** using the `COPILOT_GITHUB_TOKEN(_#)` naming convention. This can also be done using the GitHub CLI.
37+
38+
```sh
39+
gh aw secrets set "COPILOT_GITHUB_TOKEN" --value "<your-github-pat>" --repo dotnet/<repo>
40+
gh aw secrets set "COPILOT_GITHUB_TOKEN_1" --value "<your-github-pat>" --repo dotnet/<repo>
41+
```
42+
43+
## Workflow Output Attribution
44+
45+
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.
46+
47+
## Usage
48+
49+
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.
50+
51+
Up to 10 `SECRET_#` environment variables can be passed to the action, numbered 0-9. This repository uses `COPILOT_GITHUB_TOKEN` (index 0) and `COPILOT_GITHUB_TOKEN_1` through `COPILOT_GITHUB_TOKEN_9` (indices 1-9). If you choose a different `<pool_name>` scheme, update both the `select-copilot-pat` step `env` values and the `case` expression under the `engine: env` configuration to match.
52+
53+
```yml
54+
on:
55+
# Add the pre-activation step of selecting a random PAT from the supplied secrets
56+
steps:
57+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
58+
name: Checkout the select-copilot-pat action folder
59+
with:
60+
persist-credentials: false
61+
sparse-checkout: .github/actions/select-copilot-pat
62+
sparse-checkout-cone-mode: true
63+
fetch-depth: 1
64+
65+
- id: select-copilot-pat
66+
name: Select Copilot token from pool
67+
uses: ./.github/actions/select-copilot-pat
68+
env:
69+
# If the secret names are changed here, they must also be changed
70+
# in the `engine: env` case expression
71+
SECRET_0: ${{ secrets.COPILOT_GITHUB_TOKEN }}
72+
SECRET_1: ${{ secrets.COPILOT_GITHUB_TOKEN_1 }}
73+
SECRET_2: ${{ secrets.COPILOT_GITHUB_TOKEN_2 }}
74+
SECRET_3: ${{ secrets.COPILOT_GITHUB_TOKEN_3 }}
75+
SECRET_4: ${{ secrets.COPILOT_GITHUB_TOKEN_4 }}
76+
SECRET_5: ${{ secrets.COPILOT_GITHUB_TOKEN_5 }}
77+
SECRET_6: ${{ secrets.COPILOT_GITHUB_TOKEN_6 }}
78+
SECRET_7: ${{ secrets.COPILOT_GITHUB_TOKEN_7 }}
79+
SECRET_8: ${{ secrets.COPILOT_GITHUB_TOKEN_8 }}
80+
SECRET_9: ${{ secrets.COPILOT_GITHUB_TOKEN_9 }}
81+
82+
# Add the pre-activation output of the randomly selected PAT
83+
jobs:
84+
pre-activation:
85+
outputs:
86+
copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }}
87+
88+
# Override the COPILOT_GITHUB_TOKEN expression used in the activation job
89+
# Consume the PAT number from the pre-activation step and select the corresponding secret
90+
engine:
91+
id: copilot
92+
env:
93+
# We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow
94+
# If none of the `COPILOT_GITHUB_TOKEN(_#)` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used
95+
COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_GITHUB_TOKEN, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_GITHUB_TOKEN_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_GITHUB_TOKEN_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_GITHUB_TOKEN_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_GITHUB_TOKEN_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_GITHUB_TOKEN_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_GITHUB_TOKEN_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_GITHUB_TOKEN_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_GITHUB_TOKEN_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_GITHUB_TOKEN_9, secrets.COPILOT_GITHUB_TOKEN) }}
96+
```
97+
98+
## Design / Security
99+
100+
There are several details of this implementation that keep our workflows and repositories safe.
101+
102+
1. **Secrets adhere to existing trust boundaries.** The pool of PAT secrets is
103+
provided to the `select-copilot-pat` action within the `pre_activation`
104+
job, which is a deterministic and trusted portion of the workflow. No
105+
untrusted context or input is within scope during this job. The action step
106+
runs within that job, and the secrets do not get passed across contexts. The
107+
`select-copilot-pat` action only references the secret values to determine
108+
which values are non-empty, filtering the secret numbers to those with
109+
values.
110+
1. **The `select-copilot-pat` action does not require any permissions.** It
111+
merely selects a random number from the pool of non-empty secrets and
112+
returns the _number_ (**not the secret**). The consuming workflow uses the
113+
returned secret number to provide the corresponding PAT to the agent job.
114+
1. **The implementation uses existing extensibility hooks in Agentic
115+
Workflows.** Everything is supported by `gh aw compile` in this approach,
116+
and no hand-editing of the compiled output is required. The `pre_activation`
117+
job is designed for this type of extensibility, and the
118+
[secret override][secret-override] capability was added to support using a
119+
secret with a name different from the default `COPILOT_GITHUB_TOKEN`.
120+
121+
Each of the references below contributed to the design and implementation to ensure a secure and reliable design.
122+
123+
## References
124+
125+
- [Agentic Workflows CLI Extension][cli-setup]
126+
- [Agentic Authoring][configure-repo]
127+
- [Authentication][authentication]
128+
- [Agentic Workflow Imports][imports]
129+
- [Custom Steps][steps]
130+
- [Custom Jobs][jobs]
131+
- [Job Outputs][job-outputs]
132+
- [Engine Configuration][engine]
133+
- [Engine Environment Variables][engine-vars]
134+
- [Case Function in Workflow Expressions][case-expression]
135+
- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override]
136+
137+
[cli-setup]: https://github.github.com/gh-aw/setup/cli/
138+
[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository
139+
[authentication]: https://github.github.com/gh-aw/reference/auth/
140+
[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
141+
[imports]: https://github.github.com/gh-aw/reference/imports/
142+
[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps
143+
[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs
144+
[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs
145+
[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine
146+
[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables
147+
[case-expression]: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#case
148+
[secret-override]: https://github.com/github/gh-aw/pull/18017
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
name: "Select Copilot PAT from Pool"
3+
description: >-
4+
Selects a random Copilot PAT from a numbered pool of secrets. Secrets are
5+
passed as environment variables SECRET_0 through SECRET_9 by the calling
6+
workflow step.
7+
8+
inputs:
9+
random-seed:
10+
description: >-
11+
A seed number to use for the random PAT selection, for deterministic
12+
selection if needed.
13+
required: false
14+
default: ""
15+
16+
outputs:
17+
copilot_pat_number:
18+
description: >-
19+
The 0-9 secret number selected from the pool of specified secrets
20+
value: ${{ steps.select-pat-number.outputs.copilot_pat_number }}
21+
22+
runs:
23+
using: composite
24+
steps:
25+
- id: select-pat-number
26+
shell: bash
27+
env:
28+
RANDOM_SEED: ${{ inputs.random-seed }}
29+
run: |
30+
# Collect numbers with non-empty secrets from SECRET_0..SECRET_9.
31+
PAT_NUMBERS=()
32+
for i in $(seq 0 9); do
33+
var="SECRET_${i}"
34+
val="${!var}"
35+
if [ -n "$val" ]; then
36+
PAT_NUMBERS+=(${i})
37+
fi
38+
done
39+
40+
# If none of the secrets in the pool have values, emit a warning
41+
# and do not set an output value. The consumer can then fall back
42+
# to using COPILOT_GITHUB_TOKEN.
43+
if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then
44+
warning_message="::warning::None of the specified secrets had values "
45+
warning_message+="(checked SECRET_0 through SECRET_9)"
46+
echo "$warning_message"
47+
exit 0
48+
fi
49+
50+
# Select a random index using the seed if specified
51+
if [ -n "$RANDOM_SEED" ]; then
52+
RANDOM=$RANDOM_SEED
53+
fi
54+
55+
PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} ))
56+
PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}"
57+
selection_message="Selected token ${PAT_NUMBER}"
58+
selection_details="(index: ${PAT_INDEX}; pool size: ${#PAT_NUMBERS[@]})"
59+
echo "${selection_message} ${selection_details}"
60+
61+
# Set the PAT number as the output
62+
echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT"

.github/aw/actions-lock.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"repo": "github/gh-aw-actions/setup",
1010
"version": "v0.59.0",
1111
"sha": "066087f607f52664010289ddd52198f33044c38a"
12+
},
13+
"github/gh-aw-actions/setup@v0.67.1": {
14+
"repo": "github/gh-aw-actions/setup",
15+
"version": "v0.67.1",
16+
"sha": "80471a493be8c528dd27daf73cd644242a7965e0"
1217
}
1318
}
1419
}

0 commit comments

Comments
 (0)