Skip to content

Commit e51ab1f

Browse files
authored
Merge branch 'main' into ghi13631-ui-package-upgrades
2 parents 2fa8fd4 + 8b72a16 commit e51ab1f

190 files changed

Lines changed: 6203 additions & 2023 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.asf.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ github:
5757
- jmsperu
5858
- GaOrtiga
5959
- bhouse-nexthop
60+
- Dogface2k
6061

6162
rulesets:
6263
- name: "Default Branch Protection"

.github/COPILOT_TOKENS.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
# Contributing a Copilot token for the agentic workflows
21+
22+
This repo runs scheduled [GitHub Agentic Workflows](https://github.github.com/gh-aw/) (the
23+
`*.lock.yml` files compiled from `*.md` in `.github/workflows/`) that drive the GitHub Copilot
24+
CLI. Each run needs a GitHub token from an account with an active Copilot license. So that no
25+
single person's Copilot quota gets burned through, runs rotate day by day across a pool of
26+
volunteer tokens.
27+
28+
If you have a Copilot license and want to help share the load, add your token to the pool.
29+
30+
## What kind of token
31+
32+
- A fine-grained personal access token. Classic PATs don't work with the Copilot CLI.
33+
- Resource owner: your own personal account.
34+
- Permission: Account permissions > "Copilot Requests" > Read. That's the only permission it
35+
needs, no repo access.
36+
- Your account must have an active Copilot seat.
37+
38+
Create it at <https://github.com/settings/personal-access-tokens/new>. Give it a sensible
39+
expiration; when it lapses the health check (below) will flag it and you can re-add it.
40+
41+
## How to add it
42+
43+
1. Pick a short alias for yourself, e.g. `t1`, `t2`, `vol3`. The alias shows up in workflow logs,
44+
so keep it non-identifying if you prefer.
45+
2. Add your token as a repository secret named `COPILOT_GITHUB_TOKEN_<alias>`
46+
(e.g. `COPILOT_GITHUB_TOKEN_t1`). Repo admins do this via
47+
*Settings > Secrets and variables > Actions > New repository secret*, or:
48+
```
49+
gh secret set COPILOT_GITHUB_TOKEN_t1 --body "github_pat_xxx"
50+
```
51+
3. Ask a repo admin to register the alias by appending it to the repository variable
52+
`GH_AW_COPILOT_TOKEN_NAMES`, which is a JSON array:
53+
```
54+
gh variable set GH_AW_COPILOT_TOKEN_NAMES --body '["t1","t2","t3"]'
55+
```
56+
The workflows can't enumerate secrets, so this variable is the source of truth for the pool.
57+
A token isn't used until its alias is listed there.
58+
59+
## How rotation works (for maintainers)
60+
61+
Each agent workflow (`weekly-repo-status`, `daily-issue-triage`) defines a `pick_copilot_token`
62+
job in its `.md` source. The job has to run outside the agent job because strict mode forbids
63+
reading secrets there. It picks today's alias by day-of-year mod N, checks the token is live
64+
(`GET /user` returns 200, otherwise it moves on to the next candidate) and outputs the chosen
65+
alias. The token value itself never crosses jobs. The two workflows use different
66+
`ROTATION_SLOT`s, which start them half the pool apart so they don't land on the same
67+
volunteer on the same day (with at least two tokens in the pool).
68+
69+
The agent job resolves the secret itself via
70+
`secrets[format('COPILOT_GITHUB_TOKEN_{0}', needs.pick_copilot_token.outputs.name)]` and falls
71+
back to the base `COPILOT_GITHUB_TOKEN` when the pick job outputs an empty name. Keep the base
72+
secret set to one reliable token.
73+
74+
`gh aw compile` doesn't know about this wiring, so after editing the `.md` sources run:
75+
76+
```
77+
gh aw compile && bash .github/scripts/post-compile.sh
78+
```
79+
80+
See the header of `.github/scripts/post-compile.sh` for what it patches.
81+
82+
To check the pool, trigger the "Copilot token health" workflow
83+
(`.github/workflows/copilot-token-health.yml`) from the Actions tab. It prints an HTTP status
84+
code per alias and nothing else, so no account identities end up in logs. Note it can't tell
85+
when a token is live but has used up its monthly Copilot requests.
86+
87+
## Removing a token
88+
89+
Delete the `COPILOT_GITHUB_TOKEN_<alias>` secret and remove `<alias>` from
90+
`GH_AW_COPILOT_TOKEN_NAMES`.

.github/scripts/post-compile.sh

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#!/usr/bin/env bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
# Re-applies the token round-robin wiring to the gh-aw generated .lock.yml files,
20+
# which `gh aw compile` doesn't know about. Run after every compile:
21+
#
22+
# gh aw compile && bash .github/scripts/post-compile.sh
23+
#
24+
# Three edits per lock file (see .github/COPILOT_TOKENS.md for the design):
25+
# - point the agent execute step's COPILOT_GITHUB_TOKEN at the pick_copilot_token
26+
# job's output, falling back to the base secret
27+
# - point the agent job's "Redact secrets in logs" step at the same rotated token,
28+
# so a volunteer token is scrubbed from uploaded artifacts, not just the base one
29+
# - make the agent job depend on pick_copilot_token, and strip the self-reference
30+
# gh-aw sometimes adds to pick_copilot_token's own needs (that would be a cycle)
31+
#
32+
# Safe to re-run; a second run is a no-op.
33+
34+
set -euo pipefail
35+
36+
cd "$(git rev-parse --show-toplevel)"
37+
38+
# Kept in an env var so perl doesn't try to interpolate the ${{ }} bits.
39+
export NEWVAL='${{ needs.pick_copilot_token.outputs.name != '"'"''"'"' && secrets[format('"'"'COPILOT_GITHUB_TOKEN_{0}'"'"', needs.pick_copilot_token.outputs.name)] || secrets.COPILOT_GITHUB_TOKEN }}'
40+
41+
FILES=(
42+
".github/workflows/weekly-repo-status.lock.yml"
43+
".github/workflows/daily-issue-triage.lock.yml"
44+
)
45+
46+
fail() { echo "ERROR: $1" >&2; exit 1; }
47+
48+
# Fixes up `needs:` for the agent and pick_copilot_token jobs only; gh-aw adds
49+
# pick_copilot_token to several other jobs' needs and those must stay as-is.
50+
# Reads stdin, writes stdout. gh-aw emits inline needs (needs: foo) for single
51+
# dependencies and block form for lists; inline is fine unless it needs editing,
52+
# in which case "INLINE_NEEDS:<job>" is printed to stderr and the caller bails.
53+
normalise_needs() {
54+
awk '
55+
function isjob(l){ return (l ~ /^ [A-Za-z0-9_-]+:[ \t]*$/) }
56+
BEGIN { job=""; inneeds=0; agentpick=0 }
57+
{
58+
line=$0
59+
if (isjob(line)) {
60+
if (inneeds && job=="agent" && !agentpick) print " - pick_copilot_token"
61+
inneeds=0; agentpick=0
62+
name=line; sub(/^ /,"",name); sub(/:[ \t]*$/,"",name); job=name
63+
print line; next
64+
}
65+
if (line ~ /^ needs:[ \t]*[^ \t]/) {
66+
if (job=="agent" && line !~ /pick_copilot_token/) print "INLINE_NEEDS:" job > "/dev/stderr"
67+
if (job=="pick_copilot_token" && line ~ /pick_copilot_token/) print "INLINE_NEEDS:" job > "/dev/stderr"
68+
print line; next
69+
}
70+
if (line ~ /^ needs:[ \t]*$/) { inneeds=1; agentpick=0; print line; next }
71+
if (inneeds) {
72+
if (line ~ /^ - /) {
73+
item=line; sub(/^ - /,"",item); gsub(/[ \t\r]/,"",item)
74+
if (job=="pick_copilot_token" && item=="pick_copilot_token") next
75+
if (job=="agent" && item=="pick_copilot_token") agentpick=1
76+
print line; next
77+
} else {
78+
if (job=="agent" && !agentpick) print " - pick_copilot_token"
79+
inneeds=0
80+
print line; next
81+
}
82+
}
83+
print line
84+
}
85+
END { if (inneeds && job=="agent" && !agentpick) print " - pick_copilot_token" }
86+
'
87+
}
88+
89+
for f in "${FILES[@]}"; do
90+
if [ ! -f "$f" ]; then
91+
echo "WARN: $f not found, run 'gh aw compile' first? Skipping" >&2
92+
continue
93+
fi
94+
95+
# Repoint the agent execute step's token. The anchor is the GH_AW_PHASE: agent env
96+
# var further down the same env block; the detection job's block has
97+
# GH_AW_PHASE: detection so it doesn't match and keeps the base token.
98+
before=$(grep -cF 'COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true)
99+
perl -0pi -e \
100+
's/^([ \t]*)COPILOT_GITHUB_TOKEN:[ \t]*\$\{\{[ \t]*secrets\.COPILOT_GITHUB_TOKEN[ \t]*\}\}[ \t]*\n(?=(?:[ \t]+[A-Z][A-Za-z0-9_]*:[^\n]*\n)*?[ \t]+GH_AW_PHASE:[ \t]*agent[ \t]*\n)/$1."COPILOT_GITHUB_TOKEN: ".$ENV{NEWVAL}."\n"/me' \
101+
"$f"
102+
after=$(grep -cF 'COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true)
103+
removed=$(( before - after ))
104+
if [ "$removed" -eq 1 ]; then token_edit="applied"
105+
elif [ "$removed" -eq 0 ] && grep -qE '^[ \t]+COPILOT_GITHUB_TOKEN: \$\{\{ needs\.pick_copilot_token' "$f"; then token_edit="already"
106+
else fail "$f: execute-step token line not patched as expected (removed=$removed), anchor drifted?"
107+
fi
108+
109+
# Repoint the redact step's SECRET_COPILOT_GITHUB_TOKEN the same way; the line is
110+
# unique to the agent job's "Redact secrets in logs" step.
111+
before=$(grep -cF 'SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true)
112+
if [ "$before" -gt 1 ]; then
113+
fail "$f: expected at most one redact-step SECRET_COPILOT_GITHUB_TOKEN line, found $before"
114+
fi
115+
perl -0pi -e \
116+
's/^([ \t]*)SECRET_COPILOT_GITHUB_TOKEN:[ \t]*\$\{\{[ \t]*secrets\.COPILOT_GITHUB_TOKEN[ \t]*\}\}[ \t]*$/$1."SECRET_COPILOT_GITHUB_TOKEN: ".$ENV{NEWVAL}/me' \
117+
"$f"
118+
after=$(grep -cF 'SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true)
119+
if [ "$before" -eq 1 ] && [ "$after" -eq 0 ]; then redact_edit="applied"
120+
elif [ "$before" -eq 0 ] && grep -qF 'SECRET_COPILOT_GITHUB_TOKEN: ${{ needs.pick_copilot_token.outputs.name' "$f"; then redact_edit="already"
121+
else fail "$f: redact-step SECRET_COPILOT_GITHUB_TOKEN line not patched (before=$before after=$after)"
122+
fi
123+
124+
errf="$(mktemp)"
125+
normalise_needs < "$f" > "$f.tmp" 2>"$errf"
126+
if grep -q '^INLINE_NEEDS:' "$errf"; then
127+
rm -f "$f.tmp"; rm -f "$errf"
128+
fail "$f: agent/pick_copilot_token have inline 'needs:' that would need editing, update normalise_needs"
129+
fi
130+
rm -f "$errf"
131+
mv "$f.tmp" "$f"
132+
133+
# sanity checks
134+
self_refs=$(awk '
135+
/^ pick_copilot_token:[ \t]*$/{p=1;next}
136+
/^ [A-Za-z0-9_-]+:[ \t]*$/{p=0}
137+
p && /^ - pick_copilot_token[ \t]*$/{c++}
138+
END{print c+0}' "$f")
139+
[ "$self_refs" -eq 0 ] || fail "$f: pick_copilot_token still self-references (cycle)"
140+
awk '/^ agent:[ \t]*$/{a=1} /^ [A-Za-z0-9_-]+:[ \t]*$/ && !/agent/{if(a&&!seen)exit 3} a && /^ - pick_copilot_token/{seen=1} END{exit (seen?0:3)}' "$f" \
141+
|| fail "$f: agent job does not depend on pick_copilot_token"
142+
grep -qF 'Validate COPILOT_GITHUB_TOKEN secret' "$f" || echo "WARN: validate-secret step missing in $f" >&2
143+
grep -qE '^ pick_copilot_token:$' "$f" || echo "WARN: pick_copilot_token job missing in $f, did compile include the .md jobs: block?" >&2
144+
145+
echo "$f: token-ref=$token_edit, redact-ref=$redact_edit, needs=normalised (self-refs=0, agent->pick ok)"
146+
done
147+
148+
echo "Done."
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# Manual health check for the pool of volunteer Copilot tokens (see .github/COPILOT_TOKENS.md).
19+
# Trigger it from the Actions tab to find dead tokens in GH_AW_COPILOT_TOKEN_NAMES so they can be
20+
# pruned. Only HTTP status codes are printed, never account logins. A 200 just means the token is
21+
# live; there is no endpoint to check whether its monthly Copilot requests are used up.
22+
name: Copilot token health
23+
24+
on:
25+
workflow_dispatch: {}
26+
27+
permissions: {}
28+
29+
jobs:
30+
resolve:
31+
runs-on: ubuntu-latest
32+
outputs:
33+
names: ${{ steps.list.outputs.names }}
34+
steps:
35+
- id: list
36+
env:
37+
NAMES: ${{ vars.GH_AW_COPILOT_TOKEN_NAMES || '[]' }}
38+
run: echo "names=$NAMES" >> "$GITHUB_OUTPUT"
39+
40+
check:
41+
needs: resolve
42+
if: ${{ needs.resolve.outputs.names != '[]' && needs.resolve.outputs.names != '' }}
43+
runs-on: ubuntu-latest
44+
strategy:
45+
fail-fast: false
46+
matrix:
47+
name: ${{ fromJson(needs.resolve.outputs.names) }}
48+
steps:
49+
- name: Check token liveness
50+
env:
51+
TOKEN: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', matrix.name)] }}
52+
run: |
53+
set -euo pipefail
54+
if [ -z "${TOKEN:-}" ]; then
55+
echo "::error::no secret COPILOT_GITHUB_TOKEN_${{ matrix.name }} found for registered alias '${{ matrix.name }}'"
56+
exit 1
57+
fi
58+
code=$(curl -s -o /dev/null -w '%{http_code}' \
59+
-H "Authorization: Bearer $TOKEN" https://api.github.com/user || echo 000)
60+
echo "token '${{ matrix.name }}': HTTP $code"
61+
if [ "$code" != "200" ]; then
62+
echo "::error::token '${{ matrix.name }}' is not live (HTTP $code), consider removing it from GH_AW_COPILOT_TOKEN_NAMES"
63+
exit 1
64+
fi

0 commit comments

Comments
 (0)