Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 230 additions & 3 deletions .github/workflows/ci-pulsarbot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,233 @@ jobs:
steps:
- name: Execute pulsarbot command
id: pulsarbot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: apache/pulsar-test-infra/pulsarbot@master
uses: actions/github-script@v7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use most recent version, v8:

Suggested change
uses: actions/github-script@v7
uses: actions/github-script@v8

with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Supported commands:
// - /pulsarbot rerun
// Reruns all completed workflows with conclusions of failure/timed_out/skipped/cancelled
// If workflow is still running, cannot rerun whole workflow, just suggest using "/pulsarbot rerun jobname"
// - /pulsarbot rerun jobname
// Matches job.name by keyword, reruns matching jobs (regardless of current state, failures are logged)
// - /pulsarbot stop or /pulsarbot cancel
// Cancels all still running (queued/in_progress) workflow runs associated with the current PR
const commentBody = context.payload.comment.body.trim();
const prefix = '/pulsarbot';
if (!commentBody.startsWith(prefix)) {
console.log('Not a pulsarbot command, skipping ...');
return;
}
if (!context.payload.issue || !context.payload.issue.pull_request) {
console.error('This comment is not on a Pull Request. pulsarbot only works on PRs.');
return;
}
const parts = commentBody.split(/\s+/);
const sub = (parts[1] || '').toLowerCase();
const arg = parts.length > 2 ? parts.slice(2).join(' ') : '';
const supported = ['rerun', 'stop', 'cancel', 'rerun-failure-checks'];
if (!supported.includes(sub)) {
console.log(`Unsupported command '${sub}'. Supported: '/pulsarbot rerun [jobName?]', '/pulsarbot stop', '/pulsarbot cancel'.`);
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message lists supported commands but omits 'rerun-failure-checks' which is included in the supported array on line 62. The message should include all supported commands or reference them generically.

Suggested change
console.log(`Unsupported command '${sub}'. Supported: '/pulsarbot rerun [jobName?]', '/pulsarbot stop', '/pulsarbot cancel'.`);
console.log(`Unsupported command '${sub}'. Supported: ${supported.map(cmd => `'/pulsarbot ${cmd}'`).join(', ')}.`);

Copilot uses AI. Check for mistakes.
return;
}
const prNum = context.payload.issue.number;
// Get PR info
let pr;
try {
({ data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNum
}));
} catch (e) {
console.error(`Failed to fetch PR #${prNum}: ${e.message}`);
return;
}
const headSha = pr.head.sha;
const prBranch = pr.head.ref;
const prUser = pr.user.login;
const prUrl = pr.html_url;
console.log(`pulsarbot handling PR #${prNum} ${prUrl}`);
console.log(`PR branch='${prBranch}', headSha='${headSha}', author='${prUser}'`);
console.log(`Command parsed => sub='${sub}', arg='${arg || ''}'`);
// Fetch workflow runs in this repo triggered by this user on this branch, then filter by headSha
let page = 1;
const allRunsRaw = [];
while (true) {
const { data } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
actor: prUser,
branch: prBranch,
per_page: 100,
page
});
const wr = data.workflow_runs || [];
if (wr.length === 0) break;
allRunsRaw.push(...wr);
if (wr.length < 100) break;
page++;
}
Comment on lines +88 to +104
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Octokit has built-in support for pagination and retrieving all results in a single call: https://github.com/octokit/octokit.js?tab=readme-ov-file#pagination (check the last example for the single call)

const runsAtHead = allRunsRaw.filter(r => r.head_sha === headSha);
if (runsAtHead.length === 0) {
console.error(`No workflow runs found for head SHA ${headSha} on branch ${prBranch}.`);
return;
}
// Only keep the latest run for each workflow_id
runsAtHead.sort((a, b) => {
if (a.workflow_id !== b.workflow_id) return a.workflow_id - b.workflow_id;
return new Date(b.created_at) - new Date(a.created_at);
});
const latestRuns = [];
const seen = new Set();
for (const r of runsAtHead) {
if (!seen.has(r.workflow_id)) {
seen.add(r.workflow_id);
latestRuns.push(r);
}
}
function runKey(r) {
return `[run_id=${r.id}] ${r.name || '(unnamed)'} | status=${r.status} | conclusion=${r.conclusion || '-'} | ${r.html_url}`;
}
console.log('--- Latest workflow runs for this PR headSHA (one per workflow) ---');
for (const r of latestRuns) console.log('- ' + runKey(r));
// Utility: list all jobs in a run
async function listAllJobs(runId) {
let jobs = [];
let p = 1;
while (true) {
const { data } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
per_page: 100,
page: p
});
const js = data.jobs || [];
if (js.length === 0) break;
jobs.push(...js);
if (js.length < 100) break;
p++;
}
return jobs;
}
// Utility: rerun a single job
async function rerunJob(job, run) {
try {
if (github.rest.actions.reRunJobForWorkflowRun) {
await github.rest.actions.reRunJobForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
job_id: job.id
});
} else {
await github.request('POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun', {
owner: context.repo.owner,
repo: context.repo.repo,
job_id: job.id
});
}
Comment on lines +151 to +163
Copy link

Copilot AI Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic checks if reRunJobForWorkflowRun exists at runtime, but this adds unnecessary complexity. The GitHub REST API for rerunning jobs should be consistently available in the @v7 version of actions/github-script. Consider using only the primary method or documenting why the fallback is necessary.

Suggested change
if (github.rest.actions.reRunJobForWorkflowRun) {
await github.rest.actions.reRunJobForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
job_id: job.id
});
} else {
await github.request('POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun', {
owner: context.repo.owner,
repo: context.repo.repo,
job_id: job.id
});
}
await github.rest.actions.reRunJobForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
job_id: job.id
});

Copilot uses AI. Check for mistakes.
console.log(`Re-ran job '${job.name}' (job_id=${job.id}) in run '${run.name}' | ${run.html_url}`);
return true;
} catch (e) {
console.log(`Failed to re-run job '${job.name}' (job_id=${job.id}) in run '${run.name}': ${e.message}`);
return false;
}
}
// Command 1: /pulsarbot rerun
if ((sub === 'rerun' || sub === 'rerun-failure-checks') && !arg) {
const targetConclusions = new Set(['failure', 'timed_out', 'cancelled', 'skipped']);
let fullRerunCount = 0;
let skippedRunning = 0;
let skippedConclusion = 0;
console.log('Mode: full workflow re-run for completed runs with conclusions in [failure,timed_out,cancelled,skipped].');
for (const r of latestRuns) {
if (r.status !== 'completed') {
console.log(`Skip (still running) ${runKey(r)}. Cannot re-run whole workflow. Consider '/pulsarbot rerun <jobName>' for single job.`);
skippedRunning++;
continue;
}
if (!targetConclusions.has(r.conclusion)) {
console.log(`Skip (conclusion not eligible) ${runKey(r)}`);
skippedConclusion++;
continue;
}
try {
await github.rest.actions.reRunWorkflow({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to LLM, this should use reRunWorkflowFailedJobs instead of reRunWorkflow to rerun only the failed jobs.

Suggested change
await github.rest.actions.reRunWorkflow({
await github.rest.actions.reRunWorkflowFailedJobs({

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

owner: context.repo.owner,
repo: context.repo.repo,
run_id: r.id
});
console.log(`Triggered full re-run for ${runKey(r)}`);
fullRerunCount++;
} catch (e) {
console.log(`Failed to trigger full re-run for ${runKey(r)}: ${e.message}`);
}
}
if (fullRerunCount === 0) {
console.error(`No eligible workflow runs to re-run. Skipped running=${skippedRunning}, skipped by conclusion=${skippedConclusion}.`);
} else {
console.log(`Finished. Triggered full re-run for ${fullRerunCount} workflow run(s). Skipped running=${skippedRunning}, skipped by conclusion=${skippedConclusion}.`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no intention to do a "full re-run". Please update this comment to be consistent with the intention about re-running failed jobs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
return;
}
// Command 2: /pulsarbot rerun jobname
if (sub === 'rerun' && arg) {
const keyword = arg.trim();
console.log(`Mode: job-level re-run. keyword='${keyword}'`);
let matchedJobs = 0;
let successJobs = 0;
for (const r of latestRuns) {
let jobs = [];
try {
jobs = await listAllJobs(r.id);
} catch (e) {
console.log(`Failed to list jobs for ${runKey(r)}: ${e.message}`);
continue;
}
for (const j of jobs) {
if (j.name && j.name.includes(keyword)) {
matchedJobs++;
const ok = await rerunJob(j, r);
if (ok) successJobs++;
}
}
}
if (matchedJobs === 0) {
console.error(`No jobs matched keyword '${keyword}' among latest runs for this PR head.`);
} else {
console.log(`Finished. Matched ${matchedJobs} job(s); successfully requested re-run for ${successJobs} job(s).`);
}
return;
}
// Command 3: /pulsarbot stop or /pulsarbot cancel
if (sub === 'stop' || sub === 'cancel') {
console.log('Mode: cancel running workflow runs (queued/in_progress).');
let cancelCount = 0;
let alreadyCompleted = 0;
for (const r of latestRuns) {
if (r.status === 'completed') {
console.log(`Skip (already completed) ${runKey(r)}`);
alreadyCompleted++;
continue;
}
try {
await github.rest.actions.cancelWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: r.id
});
console.log(`Cancel requested for ${runKey(r)}`);
cancelCount++;
} catch (e) {
console.log(`Failed to cancel ${runKey(r)}: ${e.message}`);
}
}
if (cancelCount === 0) {
console.error(`No running workflow runs to cancel. Already completed: ${alreadyCompleted}.`);
} else {
console.log(`Finished. Requested cancel for ${cancelCount} running workflow run(s). Already completed: ${alreadyCompleted}.`);
}
return;
}
Loading