Skip to content

Commit b923338

Browse files
author
xiangying
committed
address comments
1 parent d74f9cd commit b923338

File tree

1 file changed

+69
-78
lines changed

1 file changed

+69
-78
lines changed
Lines changed: 69 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,3 @@
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-
201
name: Pulsar Bot
212
on:
223
issue_comment:
@@ -25,6 +6,8 @@ on:
256
permissions:
267
actions: write
278
contents: read
9+
pull-requests: read
10+
issues: read
2811

2912
jobs:
3013
pulsarbot:
@@ -33,21 +16,13 @@ jobs:
3316
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, '/pulsarbot')
3417
steps:
3518
- name: Execute pulsarbot command
36-
id: pulsarbot
37-
uses: actions/github-script@v7
19+
uses: actions/github-script@v8
3820
with:
3921
github-token: ${{ secrets.GITHUB_TOKEN }}
4022
script: |
41-
// Supported commands:
42-
// - /pulsarbot rerun
43-
// Reruns all completed workflows with conclusions of failure/timed_out/skipped/cancelled
44-
// If workflow is still running, cannot rerun whole workflow, just suggest using "/pulsarbot rerun jobname"
45-
// - /pulsarbot rerun jobname
46-
// Matches job.name by keyword, reruns matching jobs (regardless of current state, failures are logged)
47-
// - /pulsarbot stop or /pulsarbot cancel
48-
// Cancels all still running (queued/in_progress) workflow runs associated with the current PR
49-
const commentBody = context.payload.comment.body.trim();
23+
const commentBody = (context.payload.comment?.body || '').trim();
5024
const prefix = '/pulsarbot';
25+
5126
if (!commentBody.startsWith(prefix)) {
5227
console.log('Not a pulsarbot command, skipping ...');
5328
return;
@@ -56,62 +31,71 @@ jobs:
5631
console.error('This comment is not on a Pull Request. pulsarbot only works on PRs.');
5732
return;
5833
}
34+
5935
const parts = commentBody.split(/\s+/);
6036
const sub = (parts[1] || '').toLowerCase();
6137
const arg = parts.length > 2 ? parts.slice(2).join(' ') : '';
38+
6239
const supported = ['rerun', 'stop', 'cancel', 'rerun-failure-checks'];
6340
if (!supported.includes(sub)) {
64-
console.log(`Unsupported command '${sub}'. Supported: '/pulsarbot rerun [jobName?]', '/pulsarbot stop', '/pulsarbot cancel'.`);
41+
console.log(
42+
`Unsupported command '${sub}'. Supported: ${supported
43+
.map(cmd => `'/pulsarbot ${cmd}${cmd === 'rerun' ? ' [jobName?]' : ''}'`)
44+
.join(', ')}.`
45+
);
6546
return;
6647
}
48+
6749
const prNum = context.payload.issue.number;
50+
6851
// Get PR info
6952
let pr;
7053
try {
7154
({ data: pr } = await github.rest.pulls.get({
7255
owner: context.repo.owner,
7356
repo: context.repo.repo,
74-
pull_number: prNum
57+
pull_number: prNum,
7558
}));
7659
} catch (e) {
7760
console.error(`Failed to fetch PR #${prNum}: ${e.message}`);
7861
return;
7962
}
63+
8064
const headSha = pr.head.sha;
8165
const prBranch = pr.head.ref;
8266
const prUser = pr.user.login;
8367
const prUrl = pr.html_url;
68+
8469
console.log(`pulsarbot handling PR #${prNum} ${prUrl}`);
8570
console.log(`PR branch='${prBranch}', headSha='${headSha}', author='${prUser}'`);
8671
console.log(`Command parsed => sub='${sub}', arg='${arg || ''}'`);
87-
// Fetch workflow runs in this repo triggered by this user on this branch, then filter by headSha
88-
let page = 1;
89-
const allRunsRaw = [];
90-
while (true) {
91-
const { data } = await github.rest.actions.listWorkflowRunsForRepo({
72+
73+
// Most reliable: list workflow runs by head_sha (no guessing by actor/branch/event)
74+
const runsAtHeadRaw = await github.paginate(
75+
github.rest.actions.listWorkflowRunsForRepo,
76+
{
9277
owner: context.repo.owner,
9378
repo: context.repo.repo,
94-
actor: prUser,
95-
branch: prBranch,
79+
head_sha: headSha,
9680
per_page: 100,
97-
page
98-
});
99-
const wr = data.workflow_runs || [];
100-
if (wr.length === 0) break;
101-
allRunsRaw.push(...wr);
102-
if (wr.length < 100) break;
103-
page++;
104-
}
105-
const runsAtHead = allRunsRaw.filter(r => r.head_sha === headSha);
81+
},
82+
);
83+
console.log(`DEBUG runs for head_sha=${headSha}: total_count=${runsAtHeadRaw.total_count}, returned=${(runsAtHeadRaw.workflow_runs||[]).length}`);
84+
const runsAtHead = runsAtHeadRaw.filter(r => r && typeof r === 'object');
85+
86+
console.log(`runsAtHead total=${runsAtHead.length} for head_sha=${headSha}`);
87+
10688
if (runsAtHead.length === 0) {
107-
console.error(`No workflow runs found for head SHA ${headSha} on branch ${prBranch}.`);
89+
console.error(`No workflow runs found for head SHA ${headSha} (PR branch ${prBranch}).`);
10890
return;
10991
}
92+
11093
// Only keep the latest run for each workflow_id
11194
runsAtHead.sort((a, b) => {
11295
if (a.workflow_id !== b.workflow_id) return a.workflow_id - b.workflow_id;
11396
return new Date(b.created_at) - new Date(a.created_at);
11497
});
98+
11599
const latestRuns = [];
116100
const seen = new Set();
117101
for (const r of runsAtHead) {
@@ -120,22 +104,24 @@ jobs:
120104
latestRuns.push(r);
121105
}
122106
}
107+
123108
function runKey(r) {
124109
return `[run_id=${r.id}] ${r.name || '(unnamed)'} | status=${r.status} | conclusion=${r.conclusion || '-'} | ${r.html_url}`;
125110
}
111+
126112
console.log('--- Latest workflow runs for this PR headSHA (one per workflow) ---');
127113
for (const r of latestRuns) console.log('- ' + runKey(r));
128-
// Utility: list all jobs in a run
114+
129115
async function listAllJobs(runId) {
130-
let jobs = [];
116+
const jobs = [];
131117
let p = 1;
132118
while (true) {
133119
const { data } = await github.rest.actions.listJobsForWorkflowRun({
134120
owner: context.repo.owner,
135121
repo: context.repo.repo,
136122
run_id: runId,
137123
per_page: 100,
138-
page: p
124+
page: p,
139125
});
140126
const js = data.jobs || [];
141127
if (js.length === 0) break;
@@ -145,36 +131,31 @@ jobs:
145131
}
146132
return jobs;
147133
}
148-
// Utility: rerun a single job
134+
149135
async function rerunJob(job, run) {
150136
try {
151-
if (github.rest.actions.reRunJobForWorkflowRun) {
152-
await github.rest.actions.reRunJobForWorkflowRun({
153-
owner: context.repo.owner,
154-
repo: context.repo.repo,
155-
job_id: job.id
156-
});
157-
} else {
158-
await github.request('POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun', {
159-
owner: context.repo.owner,
160-
repo: context.repo.repo,
161-
job_id: job.id
162-
});
163-
}
137+
await github.rest.actions.reRunJobForWorkflowRun({
138+
owner: context.repo.owner,
139+
repo: context.repo.repo,
140+
job_id: job.id,
141+
});
164142
console.log(`Re-ran job '${job.name}' (job_id=${job.id}) in run '${run.name}' | ${run.html_url}`);
165143
return true;
166144
} catch (e) {
167145
console.log(`Failed to re-run job '${job.name}' (job_id=${job.id}) in run '${run.name}': ${e.message}`);
168146
return false;
169147
}
170148
}
171-
// Command 1: /pulsarbot rerun
149+
150+
// Command 1: /pulsarbot rerun (or rerun-failure-checks)
172151
if ((sub === 'rerun' || sub === 'rerun-failure-checks') && !arg) {
173152
const targetConclusions = new Set(['failure', 'timed_out', 'cancelled', 'skipped']);
174-
let fullRerunCount = 0;
153+
let rerunCount = 0;
175154
let skippedRunning = 0;
176155
let skippedConclusion = 0;
177-
console.log('Mode: full workflow re-run for completed runs with conclusions in [failure,timed_out,cancelled,skipped].');
156+
157+
console.log('Mode: workflow re-run for completed runs with conclusions in [failure,timed_out,cancelled,skipped].');
158+
178159
for (const r of latestRuns) {
179160
if (r.status !== 'completed') {
180161
console.log(`Skip (still running) ${runKey(r)}. Cannot re-run whole workflow. Consider '/pulsarbot rerun <jobName>' for single job.`);
@@ -187,30 +168,34 @@ jobs:
187168
continue;
188169
}
189170
try {
190-
await github.rest.actions.reRunWorkflow({
171+
await github.rest.actions.reRunWorkflowFailedJobs({
191172
owner: context.repo.owner,
192173
repo: context.repo.repo,
193-
run_id: r.id
174+
run_id: r.id,
194175
});
195-
console.log(`Triggered full re-run for ${runKey(r)}`);
196-
fullRerunCount++;
176+
console.log(`Triggered re-run for ${runKey(r)}`);
177+
rerunCount++;
197178
} catch (e) {
198-
console.log(`Failed to trigger full re-run for ${runKey(r)}: ${e.message}`);
179+
console.log(`Failed to trigger re-run for ${runKey(r)}: ${e.message}`);
199180
}
200181
}
201-
if (fullRerunCount === 0) {
182+
183+
if (rerunCount === 0) {
202184
console.error(`No eligible workflow runs to re-run. Skipped running=${skippedRunning}, skipped by conclusion=${skippedConclusion}.`);
203185
} else {
204-
console.log(`Finished. Triggered full re-run for ${fullRerunCount} workflow run(s). Skipped running=${skippedRunning}, skipped by conclusion=${skippedConclusion}.`);
186+
console.log(`Finished. Triggered re-run for ${rerunCount} workflow run(s). Skipped running=${skippedRunning}, skipped by conclusion=${skippedConclusion}.`);
205187
}
206188
return;
207189
}
190+
208191
// Command 2: /pulsarbot rerun jobname
209192
if (sub === 'rerun' && arg) {
210193
const keyword = arg.trim();
211194
console.log(`Mode: job-level re-run. keyword='${keyword}'`);
195+
212196
let matchedJobs = 0;
213197
let successJobs = 0;
198+
214199
for (const r of latestRuns) {
215200
let jobs = [];
216201
try {
@@ -219,6 +204,7 @@ jobs:
219204
console.log(`Failed to list jobs for ${runKey(r)}: ${e.message}`);
220205
continue;
221206
}
207+
222208
for (const j of jobs) {
223209
if (j.name && j.name.includes(keyword)) {
224210
matchedJobs++;
@@ -227,18 +213,22 @@ jobs:
227213
}
228214
}
229215
}
216+
230217
if (matchedJobs === 0) {
231218
console.error(`No jobs matched keyword '${keyword}' among latest runs for this PR head.`);
232219
} else {
233220
console.log(`Finished. Matched ${matchedJobs} job(s); successfully requested re-run for ${successJobs} job(s).`);
234221
}
235222
return;
236223
}
224+
237225
// Command 3: /pulsarbot stop or /pulsarbot cancel
238226
if (sub === 'stop' || sub === 'cancel') {
239227
console.log('Mode: cancel running workflow runs (queued/in_progress).');
228+
240229
let cancelCount = 0;
241230
let alreadyCompleted = 0;
231+
242232
for (const r of latestRuns) {
243233
if (r.status === 'completed') {
244234
console.log(`Skip (already completed) ${runKey(r)}`);
@@ -249,18 +239,19 @@ jobs:
249239
await github.rest.actions.cancelWorkflowRun({
250240
owner: context.repo.owner,
251241
repo: context.repo.repo,
252-
run_id: r.id
242+
run_id: r.id,
253243
});
254244
console.log(`Cancel requested for ${runKey(r)}`);
255245
cancelCount++;
256246
} catch (e) {
257247
console.log(`Failed to cancel ${runKey(r)}: ${e.message}`);
258248
}
259249
}
250+
260251
if (cancelCount === 0) {
261252
console.error(`No running workflow runs to cancel. Already completed: ${alreadyCompleted}.`);
262253
} else {
263254
console.log(`Finished. Requested cancel for ${cancelCount} running workflow run(s). Already completed: ${alreadyCompleted}.`);
264255
}
265256
return;
266-
}
257+
}

0 commit comments

Comments
 (0)