Skip to content

Commit dc6fcce

Browse files
authored
ci: add /retest PR comment workflow to re-run CI jobs (#1640)
Add .github/workflows/retest.yml to allow PR authors to trigger re-runs of failed, all, or specific CI jobs by commenting /retest on their pull requests. Note: Workflows triggered on issue_comment only take effect once this PR is merged into the repository default branch (main), as GitHub Actions strictly executes comment workflows from the default branch. Features: - Validates that only the PR author can trigger re-runs. - Supports /retest to re-run only failed jobs for the latest commit. - Supports /retest all to re-run all jobs from scratch (including passed tests). - Supports /retest <job-name> (e.g. /retest tsan, /retest test_gcc) to re-run a specific completed job (including passed tests, even while other jobs in the workflow run are in progress). - Provides immediate feedback via comment reactions (eyes, rocket, confused) and informative status comments. - Updates CONTRIBUTING.md with instructions on how to use the command. - Removes legacy Azure Pipelines retest alias from repokitteh.star. - Pins actions to Git commit SHA for OpenSSF Scorecard supply-chain security compliance. Signed-off-by: Jason Ye <jiajunye@google.com>
1 parent c08a488 commit dc6fcce

3 files changed

Lines changed: 243 additions & 4 deletions

File tree

‎.github/workflows/retest.yml‎

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
name: Retest CI
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
issue_comment:
8+
types:
9+
- created
10+
11+
jobs:
12+
retest:
13+
name: Retest CI
14+
if: >-
15+
${{
16+
github.event.issue.pull_request
17+
&& startsWith(github.event.comment.body, '/retest')
18+
&& !contains(github.event.comment.user.login, '[bot]')
19+
}}
20+
runs-on: ubuntu-22.04
21+
permissions:
22+
actions: write
23+
pull-requests: write
24+
issues: write
25+
contents: read
26+
27+
steps:
28+
- name: Retest CI Check
29+
# Pinned to full Git commit SHA for OpenSSF Scorecard supply-chain security compliance.
30+
# Dependabot automatically keeps this SHA and the version tag comment up to date via .github/dependabot.yml.
31+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
32+
with:
33+
script: |
34+
const commenter = context.payload.comment.user.login;
35+
const prAuthor = context.payload.issue.user.login;
36+
const commentBody = context.payload.comment.body.trim();
37+
38+
// Acknowledge receipt of the command with eyes reaction
39+
await github.rest.reactions.createForIssueComment({
40+
owner: context.repo.owner,
41+
repo: context.repo.repo,
42+
comment_id: context.payload.comment.id,
43+
content: 'eyes',
44+
});
45+
46+
// Check permissions: only allow the PR author to trigger re-runs
47+
if (commenter !== prAuthor) {
48+
await github.rest.issues.createComment({
49+
owner: context.repo.owner,
50+
repo: context.repo.repo,
51+
issue_number: context.payload.issue.number,
52+
body: `@${commenter}: You do not have permission to trigger CI re-runs on this PR. Only the PR author (@${prAuthor}) can re-run workflows via \`/retest\`.`,
53+
});
54+
await github.rest.reactions.createForIssueComment({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
comment_id: context.payload.comment.id,
58+
content: '-1',
59+
});
60+
return;
61+
}
62+
63+
// Fetch the PR to verify state and get head commit SHA
64+
const { data: pr } = await github.rest.pulls.get({
65+
owner: context.repo.owner,
66+
repo: context.repo.repo,
67+
pull_number: context.payload.issue.number,
68+
});
69+
70+
if (pr.state === 'closed') {
71+
await github.rest.issues.createComment({
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
issue_number: context.payload.issue.number,
75+
body: `@${commenter}: Cannot re-run CI on a closed pull request.`,
76+
});
77+
await github.rest.reactions.createForIssueComment({
78+
owner: context.repo.owner,
79+
repo: context.repo.repo,
80+
comment_id: context.payload.comment.id,
81+
content: 'confused',
82+
});
83+
return;
84+
}
85+
86+
// Parse optional argument from the command line only
87+
const lines = commentBody.split('\n').map(l => l.trim());
88+
const commandLine = lines.find(l => l.startsWith('/retest')) || '';
89+
const tokens = commandLine.split(/\s+/);
90+
const targetFilter = tokens.length > 1 ? tokens.slice(1).join(' ').toLowerCase() : null;
91+
92+
const headSha = pr.head.sha;
93+
94+
// Fetch workflow runs for this repository matching the PR's head SHA
95+
const { data: runsData } = await github.rest.actions.listWorkflowRunsForRepo({
96+
owner: context.repo.owner,
97+
repo: context.repo.repo,
98+
head_sha: headSha,
99+
});
100+
101+
let rerunJobsCount = 0;
102+
let rerunWorkflowsCount = 0;
103+
const inProgressRuns = [];
104+
const errors = [];
105+
106+
for (const run of runsData.workflow_runs) {
107+
// If "/retest all": re-run the entire workflow from scratch (including passed tests)
108+
if (targetFilter === 'all') {
109+
if (run.status !== 'completed') {
110+
inProgressRuns.push(run.name);
111+
continue;
112+
}
113+
try {
114+
console.log(`Re-running all jobs for workflow ${run.name} (run id: ${run.id})`);
115+
try {
116+
await github.rest.actions.reRunWorkflow({
117+
owner: context.repo.owner,
118+
repo: context.repo.repo,
119+
run_id: run.id,
120+
});
121+
} catch (e) {
122+
await github.request('POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun', {
123+
owner: context.repo.owner,
124+
repo: context.repo.repo,
125+
run_id: run.id,
126+
});
127+
}
128+
rerunWorkflowsCount++;
129+
} catch (err) {
130+
console.error(`Error re-running workflow ${run.id}:`, err);
131+
errors.push(err.message);
132+
}
133+
} else if (targetFilter) {
134+
// If a specific job target filter was provided:
135+
// Re-run matching completed jobs (supports re-running both passed and failed tests)
136+
try {
137+
const { data: jobsData } = await github.rest.actions.listJobsForWorkflowRun({
138+
owner: context.repo.owner,
139+
repo: context.repo.repo,
140+
run_id: run.id,
141+
});
142+
143+
for (const job of jobsData.jobs) {
144+
if (job.name.toLowerCase().includes(targetFilter)) {
145+
if (job.status !== 'completed') {
146+
inProgressRuns.push(`${job.name} (in ${run.name})`);
147+
continue;
148+
}
149+
console.log(`Re-running job ${job.name} (id: ${job.id}, conclusion: ${job.conclusion}) in workflow ${run.name} (run id: ${run.id})`);
150+
await github.rest.actions.reRunJobForWorkflowRun({
151+
owner: context.repo.owner,
152+
repo: context.repo.repo,
153+
job_id: job.id,
154+
});
155+
rerunJobsCount++;
156+
}
157+
}
158+
} catch (err) {
159+
console.error(`Error re-running matching job in run ${run.id}:`, err);
160+
errors.push(err.message);
161+
}
162+
} else {
163+
// No target filter provided: re-run ONLY failed/cancelled/timed_out jobs
164+
if (run.status !== 'completed') {
165+
inProgressRuns.push(run.name);
166+
continue;
167+
}
168+
if (['failure', 'cancelled', 'timed_out'].includes(run.conclusion)) {
169+
try {
170+
console.log(`Re-running failed jobs for workflow ${run.name} (run id: ${run.id})`);
171+
await github.rest.actions.reRunWorkflowFailedJobs({
172+
owner: context.repo.owner,
173+
repo: context.repo.repo,
174+
run_id: run.id,
175+
});
176+
rerunWorkflowsCount++;
177+
} catch (err) {
178+
console.error(`Error re-running failed jobs for run ${run.id}:`, err);
179+
errors.push(err.message);
180+
}
181+
}
182+
}
183+
}
184+
185+
if (rerunJobsCount > 0 || rerunWorkflowsCount > 0) {
186+
await github.rest.reactions.createForIssueComment({
187+
owner: context.repo.owner,
188+
repo: context.repo.repo,
189+
comment_id: context.payload.comment.id,
190+
content: 'rocket',
191+
});
192+
} else if (errors.length > 0) {
193+
await github.rest.issues.createComment({
194+
owner: context.repo.owner,
195+
repo: context.repo.repo,
196+
issue_number: context.payload.issue.number,
197+
body: `@${commenter}: Encountered error(s) while attempting to re-run CI for commit \`${headSha.substring(0, 7)}\`:\n- ${errors.join('\n- ')}`,
198+
});
199+
await github.rest.reactions.createForIssueComment({
200+
owner: context.repo.owner,
201+
repo: context.repo.repo,
202+
comment_id: context.payload.comment.id,
203+
content: 'confused',
204+
});
205+
} else if (inProgressRuns.length > 0) {
206+
await github.rest.issues.createComment({
207+
owner: context.repo.owner,
208+
repo: context.repo.repo,
209+
issue_number: context.payload.issue.number,
210+
body: `@${commenter}: Workflow(s) \`${inProgressRuns.join(', ')}\` are currently in progress for commit \`${headSha.substring(0, 7)}\`. Please wait for them to finish before retrying.`,
211+
});
212+
await github.rest.reactions.createForIssueComment({
213+
owner: context.repo.owner,
214+
repo: context.repo.repo,
215+
comment_id: context.payload.comment.id,
216+
content: 'eyes',
217+
});
218+
} else {
219+
const msg = (targetFilter === 'all')
220+
? `No completed CI workflow runs found to re-run for commit \`${headSha.substring(0, 7)}\`.`
221+
: targetFilter
222+
? `No completed jobs matching \`${targetFilter}\` found to re-run for commit \`${headSha.substring(0, 7)}\`.`
223+
: `No failed or cancelled CI workflow runs found to re-run for commit \`${headSha.substring(0, 7)}\`.`;
224+
await github.rest.issues.createComment({
225+
owner: context.repo.owner,
226+
repo: context.repo.repo,
227+
issue_number: context.payload.issue.number,
228+
body: `@${commenter}: ${msg}`,
229+
});
230+
await github.rest.reactions.createForIssueComment({
231+
owner: context.repo.owner,
232+
repo: context.repo.repo,
233+
comment_id: context.payload.comment.id,
234+
content: 'confused',
235+
});
236+
}

‎CONTRIBUTING.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,10 @@ Commits need to be signed off. See [here](https://github.com/envoyproxy/envoy/bl
3434

3535
## Triggering CI re-run without making changes
3636

37-
See [here](https://github.com/envoyproxy/envoy/blob/main/CONTRIBUTING.md#triggering-ci-re-run-without-making-changes).
37+
PR authors can trigger a re-run of CI jobs by commenting on their Pull Request:
38+
39+
* Comment `/retest` to re-run only failed jobs for the latest commit.
40+
* Comment `/retest all` to re-run all jobs from scratch (including passed ones).
41+
* Comment `/retest <job-name>` (e.g. `/retest tsan`, `/retest test_gcc`) to re-run a specific completed job.
42+
43+
See also [Envoy's policy](https://github.com/envoyproxy/envoy/blob/main/CONTRIBUTING.md#triggering-ci-re-run-without-making-changes).

‎repokitteh.star‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
use("github.com/repokitteh/modules/assign.star")
22
use("github.com/repokitteh/modules/review.star")
33
use("github.com/repokitteh/modules/wait.star")
4-
use("github.com/envoyproxy/envoy/ci/repokitteh/modules/azure_pipelines.star", secret_token = get_secret("azp_token"))
5-
6-
alias("retest", "retry-azp")

0 commit comments

Comments
 (0)