Skip to content

Commit f6535b9

Browse files
jiaenrenclaude
andauthored
Add auto-approver for testbot-respond environment (#835)
Auto-approves environment deployment for NVIDIA/osmo-dev team members and trusted bots (svc-osmo-ci, github-actions[bot], coderabbitai[bot]). Uses workflow_run trigger from main branch so PR authors cannot tamper with the approval logic. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3904aae commit f6535b9

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
# Auto-approves the testbot-respond environment deployment when the
18+
# triggering actor is an NVIDIA/osmo-dev team member or a trusted bot.
19+
# Runs from main via workflow_run, so PR authors cannot tamper
20+
# with this logic.
21+
#
22+
# SVC_OSMO_CI_TOKEN requires:
23+
# - read:org (to check NVIDIA/osmo-dev team membership)
24+
# - repo (to approve environment deployments)
25+
# The token owner (svc-osmo-ci) must be listed as a required
26+
# reviewer on the 'testbot-respond' environment.
27+
name: Testbot - Auto-approve Respond
28+
29+
on:
30+
workflow_run:
31+
# IMPORTANT: must match the 'name:' field in testbot-respond.yaml exactly
32+
workflows: ["Testbot - Respond to Reviews"]
33+
types: [requested]
34+
35+
permissions: {} # All API calls use PAT; GITHUB_TOKEN needs no permissions
36+
37+
jobs:
38+
auto-approve:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- name: Check membership and approve
42+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
43+
with:
44+
github-token: ${{ secrets.SVC_OSMO_CI_TOKEN }}
45+
script: |
46+
const run = context.payload.workflow_run;
47+
const actor = run.triggering_actor.login;
48+
const { owner, repo } = context.repo;
49+
core.info(`Triggering actor: ${actor}`);
50+
51+
// Check if the run was already cancelled (e.g., by cancel-in-progress)
52+
const { data: currentRun } = await github.rest.actions.getWorkflowRun({
53+
owner,
54+
repo,
55+
run_id: run.id,
56+
});
57+
if (currentRun.status === 'completed') {
58+
core.info(`Run ${run.id} already completed (${currentRun.conclusion}) — skipping`);
59+
return;
60+
}
61+
62+
// Trusted bots: approve without org check so their runs complete
63+
// quickly (respond.py will find no /testbot threads and exit).
64+
// This prevents bot-triggered runs from blocking the concurrency
65+
// group — cancel-in-progress could cancel a real /testbot run if
66+
// the bot run stays pending.
67+
const TRUSTED_BOTS = new Set([
68+
'svc-osmo-ci',
69+
'github-actions[bot]',
70+
'coderabbitai[bot]',
71+
]);
72+
const isTrustedBot = TRUSTED_BOTS.has(actor);
73+
74+
if (!isTrustedBot) {
75+
// Check NVIDIA/osmo-dev team membership (200 = active/pending, 404 = not)
76+
try {
77+
const { data: membership } = await github.rest.teams.getMembershipForUserInOrg({
78+
org: 'NVIDIA',
79+
team_slug: 'osmo-dev',
80+
username: actor,
81+
});
82+
if (membership.state !== 'active') {
83+
core.info(`${actor} has osmo-dev state '${membership.state}' (not active) — skipping`);
84+
return;
85+
}
86+
core.info(`${actor} is an active NVIDIA/osmo-dev member`);
87+
} catch (err) {
88+
if (err.status === 404) {
89+
core.info(`${actor} is NOT an NVIDIA/osmo-dev member — skipping`);
90+
return;
91+
}
92+
throw err; // 403/429/5xx = PAT or API issue, fail loudly
93+
}
94+
} else {
95+
core.info(`${actor} is a trusted bot — approving without team check`);
96+
}
97+
98+
// Poll for pending deployments (may take a few seconds to appear).
99+
// Start at 5s — deployments never appear in <3s. Add jitter to
100+
// avoid thundering-herd if multiple runs trigger simultaneously.
101+
let deployments = [];
102+
for (let attempt = 1; attempt <= 12; attempt++) {
103+
const { data } = await github.rest.actions.getPendingDeploymentsForRun({
104+
owner,
105+
repo,
106+
run_id: run.id,
107+
});
108+
deployments = data;
109+
if (deployments.length > 0) break;
110+
const base = Math.min(3 + attempt * 2, 15);
111+
const jitter = Math.random() * 2;
112+
const delay = (base + jitter) * 1000;
113+
core.info(`No pending deployments yet (attempt ${attempt}/12), waiting ${(delay / 1000).toFixed(1)}s...`);
114+
await new Promise(r => setTimeout(r, delay));
115+
}
116+
117+
if (deployments.length === 0) {
118+
core.warning(
119+
`No pending deployments for run ${run.id} — ` +
120+
`run may have been cancelled, already approved, or still queuing`
121+
);
122+
return;
123+
}
124+
125+
// Only approve testbot-respond, not any other environments
126+
const envIds = deployments
127+
.filter(d => d.environment.name === 'testbot-respond')
128+
.map(d => d.environment.id);
129+
130+
if (envIds.length === 0) {
131+
core.info('No pending testbot-respond deployments — skipping');
132+
return;
133+
}
134+
135+
try {
136+
await github.rest.actions.reviewPendingDeploymentsForRun({
137+
owner,
138+
repo,
139+
run_id: run.id,
140+
environment_ids: envIds,
141+
state: 'approved',
142+
comment: isTrustedBot
143+
? `Auto-approved: ${actor} is a trusted bot`
144+
: `Auto-approved: ${actor} is an NVIDIA/osmo-dev member`,
145+
});
146+
core.info(`Approved run ${run.id} for ${actor}`);
147+
} catch (err) {
148+
if (err.status === 422) {
149+
core.info(`Run ${run.id} was already approved — nothing to do`);
150+
return;
151+
}
152+
core.setFailed(
153+
`Failed to approve run ${run.id}: ${err.message} (status: ${err.status}). ` +
154+
`Verify that the PAT owner is listed as a required reviewer ` +
155+
`on the 'testbot-respond' environment.`
156+
);
157+
}

0 commit comments

Comments
 (0)