-
Notifications
You must be signed in to change notification settings - Fork 170
227 lines (213 loc) · 10.1 KB
/
Copy pathpr-test-checker.yml
File metadata and controls
227 lines (213 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
name: "PR Test Checker"
# Grades whether a PR has adequate test coverage and posts a verdict comment.
# Runs only on demand: comment `/pete` (or `/recheck-tests`, `/rePETE`,
# `/re-pete`) on a PR to trigger it. It does NOT run automatically on PR
# open/push (see #14494) -- authors get the same rubric locally via the `pete`
# skill before opening a PR, so the automatic CI pass was redundant with that
# plus the on-demand comment trigger below.
#
# Scope: runs only for PR authors / commenters in the posit-dev `positron-dev`
# team. GitHub Actions `if:` has no team-membership predicate, and the default
# GITHUB_TOKEN cannot read org membership, so a dedicated `gate` job resolves
# membership at runtime via the GitHub API. It mints a short-lived token from
# the "Positron Projects" GitHub App (secrets POSITRON_PROJECTS_CLIENT_ID +
# POSITRON_PROJECTS_PEM, shared with release-generate-sbom.yml) scoped to the
# posit-dev org, which carries the App's `Members: read` permission, and
# exposes an `ok` output the grading job depends on via `needs`. The gate
# FAILS CLOSED: if the token can't be minted or any API call fails (e.g. the
# App is missing `Members: read`), `ok=false` and nothing is graded. Membership
# is always live -- there is no allowlist to maintain. To change who PETE runs
# for, edit the `positron-dev` team on GitHub, not this file.
#
# Security model: the job checks out two trees -- the BASE branch (trusted
# action + skill code, used to run the analyzer with secrets) and the PR HEAD
# (untrusted source code, mounted as read-only data for the agent's
# Read/Grep/Glob tools). The analyzer is invoked from the base checkout via
# `uses: ./base/.github/actions/pr-test-checker` so that PR-head code is never
# executed with secrets in scope. This closes the "pwn request" pattern where
# a fork PR modifies action code, and a positron-dev member comments
# /recheck-tests (an `issue_comment` event that runs in base context with
# full secrets, unlike `pull_request` events which strip secrets for forks).
on:
issue_comment:
types:
- created
jobs:
# Resolve positron-dev team membership at runtime. This is a pure GitHub API
# read with a short-lived App token; it checks out no PR code, so it is safe
# to run in base context. Emits `ok=true` only when BOTH the commenter and
# the PR author are *active* team members, and FAILS CLOSED (`ok=false`) on
# a token/API error.
gate:
name: Check positron-dev membership
# Cheap event-shape pre-filter so we don't spin up a runner (or hit the
# API) for every PR comment -- only trigger-command comments on PRs reach
# the membership check below. Recognized commands: /pete, /recheck-tests,
# /rePETE, /re-pete (contains() is case-insensitive, so casing doesn't
# matter; the /pete substring check does not match /repete or /re-pete,
# since neither contains a "/pete" substring). NOTE: keep this command
# list in sync with the `run` job's `if` below.
if: |
github.event.issue.pull_request != null &&
(contains(github.event.comment.body, '/pete') ||
contains(github.event.comment.body, '/recheck-tests') ||
contains(github.event.comment.body, '/repete') ||
contains(github.event.comment.body, '/re-pete'))
runs-on: ubuntu-latest
# No GITHUB_TOKEN scopes needed: the check uses the App token in GH_TOKEN.
permissions: {}
concurrency:
group: pr-test-checker-gate-${{ github.event.issue.number }}
cancel-in-progress: true
outputs:
ok: ${{ steps.check.outputs.ok }}
steps:
- name: Mint org-scoped App token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.POSITRON_PROJECTS_CLIENT_ID }}
private-key: ${{ secrets.POSITRON_PROJECTS_PEM }}
owner: posit-dev
- name: Check team membership
id: check
env:
# Short-lived token from the Positron Projects GitHub App, scoped to
# the posit-dev org. Reading team membership needs the App's
# `Members: read` permission; without it the gh calls 404 and the gate
# stays ok=false -- fail closed.
GH_TOKEN: ${{ steps.app-token.outputs.token }}
COMMENTER: ${{ github.event.comment.user.login }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
run: |
set -uo pipefail
# Active member -> HTTP 200 {"state":"active"}; non-member -> 404
# (gh exits non-zero, stdout empty). Pending invite -> "pending".
is_member() {
local state
state=$(gh api "orgs/posit-dev/teams/positron-dev/memberships/$1" --jq '.state' 2>/dev/null)
[ "$state" = "active" ]
}
# BOTH the commenter and the PR author must be members, so an
# attacker can't bait a run by commenting the trigger phrase on a
# PR whose author is outside the team.
ok=false
if is_member "$COMMENTER" && is_member "$ISSUE_AUTHOR"; then
ok=true
fi
echo "Membership gate result: ok=$ok"
echo "ok=$ok" >> "$GITHUB_OUTPUT"
run:
name: Grade on command
needs: gate
# Fires when a positron-dev member comments a trigger command
# (/pete, /recheck-tests, /rePETE, /re-pete) on a PR. The issue_comment
# event runs in base-repo context with full secrets even when the PR is
# from a fork, so two layers of defense:
# 1. The `gate` job requires BOTH the commenter AND the PR author to be
# positron-dev members (the PR-author check blocks comments on fork
# PRs by non-members -- attackers can't bait a run on a PR whose
# author is outside the team).
# 2. The job checks out the base branch to RUN the action and the
# PR head only as READ-ONLY DATA -- PR-head code never executes.
# NOTE: keep the command list in sync with the gate job's `if` above.
if: |
github.event.issue.pull_request != null &&
(contains(github.event.comment.body, '/pete') ||
contains(github.event.comment.body, '/recheck-tests') ||
contains(github.event.comment.body, '/repete') ||
contains(github.event.comment.body, '/re-pete')) &&
needs.gate.outputs.ok == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: pr-test-checker-${{ github.event.issue.number }}
cancel-in-progress: true
steps:
# Acknowledge the command with an "eyes" reaction so the commenter gets
# immediate visual confirmation that PETE picked it up and is running.
# First step so it lands before the slower checkout/analysis steps.
# Capture the reaction id so the finalize step can remove it once PETE
# is done.
- name: Acknowledge command
id: ack
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
REACTION_ID=$(gh api --method POST \
"repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}/reactions" \
-f content=eyes --jq '.id' 2>/dev/null) || echo "Failed to add reaction (non-fatal)"
echo "reaction_id=${REACTION_ID}" >> "$GITHUB_OUTPUT"
- name: Resolve PR head SHA
id: pr
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number }}
run: |
HEAD_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.head.sha')
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
- name: Checkout BASE (trusted action + skill code)
uses: actions/checkout@v7
with:
ref: ${{ github.event.repository.default_branch }}
path: base
sparse-checkout: |
.claude/skills/pr-test-checker
.claude/rules
.github/actions/pr-test-checker
- name: Checkout PR HEAD (untrusted source data)
uses: actions/checkout@v7
with:
ref: ${{ steps.pr.outputs.head_sha }}
path: pr-head
sparse-checkout: |
CLAUDE.md
src
extensions
test/e2e
- name: Load Anthropic API key
uses: 1password/load-secrets-action@v4
with:
export-env: true
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
ANTHROPIC_KEY: "op://Positron/Anthropic/credential"
- name: Run pr-test-checker (from base, source from PR head)
uses: ./base/.github/actions/pr-test-checker
with:
pr-number: ${{ github.event.issue.number }}
anthropic-api-key: ${{ env.ANTHROPIC_KEY }}
repo-root: ${{ github.workspace }}/pr-head
# Replace the "eyes" acknowledgement with a terminal reaction once PETE
# finishes: rocket on success, confused on failure. Runs on always() so
# the comment never gets stuck showing "running". Skipped if the ack
# step never recorded a reaction id (e.g. the initial reaction POST
# failed).
- name: Finalize reaction
if: always() && steps.ack.outputs.reaction_id != ''
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
REACTION_ID: ${{ steps.ack.outputs.reaction_id }}
JOB_STATUS: ${{ job.status }}
run: |
# Remove the "eyes" acknowledgement reaction.
gh api --method DELETE \
"repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}/reactions/${REACTION_ID}" \
|| echo "Failed to remove eyes reaction (non-fatal)"
# Signal completion: rocket on success, confused otherwise.
if [ "$JOB_STATUS" = "success" ]; then
FINAL=rocket
else
FINAL=confused
fi
gh api --method POST \
"repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}/reactions" \
-f content="$FINAL" || echo "Failed to add ${FINAL} reaction (non-fatal)"