Skip to content
Merged
Show file tree
Hide file tree
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
10 changes: 10 additions & 0 deletions .github/workflows/examples-apo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ on:
repository_dispatch:
types: [ci-apo, ci-all]

run-name: >-
${{ github.event_name == 'repository_dispatch'
&& format(
'PR #{0} - Label {1} - {2}',
github.event.client_payload.pull_number,
github.event.client_payload.ci_label,
github.event.client_payload.correlation_id
)
|| format('APO - {0}', github.event_name) }}

jobs:
apo:
if: >
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/examples-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ on:
repository_dispatch:
types: [ci-compat, ci-all]

run-name: >-
${{ github.event_name == 'repository_dispatch'
&& format(
'PR #{0} - Label {1} - {2}',
github.event.client_payload.pull_number,
github.event.client_payload.ci_label,
github.event.client_payload.correlation_id
)
|| format('Backward Compatibility - {0}', github.event_name) }}

jobs:
backward-compatibility:
if: >
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/examples-spider.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ on:
repository_dispatch:
types: [ci-spider, ci-all]

run-name: >-
${{ github.event_name == 'repository_dispatch'
&& format(
'PR #{0} - Label {1} - {2}',
github.event.client_payload.pull_number,
github.event.client_payload.ci_label,
github.event.client_payload.correlation_id
)
|| format('Spider - {0}', github.event_name) }}

jobs:
spider:
if: >
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/examples-unsloth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ on:
repository_dispatch:
types: [ci-unsloth, ci-all]

run-name: >-
${{ github.event_name == 'repository_dispatch'
&& format(
'PR #{0} - Label {1} - {2}',
github.event.client_payload.pull_number,
github.event.client_payload.ci_label,
github.event.client_payload.correlation_id
)
|| format('Unsloth - {0}', github.event_name) }}

jobs:
unsloth:
if: >
Expand Down
185 changes: 182 additions & 3 deletions .github/workflows/issue-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ permissions:
pull-requests: write
issues: write
contents: write
actions: read

jobs:
dispatch:
Expand All @@ -16,6 +17,12 @@ jobs:
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/ci')
runs-on: ubuntu-latest
outputs:
dispatched: ${{ steps.dispatch.outputs.dispatched }}
event_types: ${{ steps.dispatch.outputs.event_types }}
correlation_id: ${{ steps.dispatch.outputs.correlation_id }}
trigger_comment_id: ${{ steps.dispatch.outputs.trigger_comment_id }}
ack_comment_id: ${{ steps.ack.outputs.comment_id }}
steps:
- name: Guardrail — allow only members/collaborators
id: guard
Expand Down Expand Up @@ -69,7 +76,10 @@ jobs:
return;
}

const correlation_id = `id-${comment.id}-${Date.now().toString(36)}`;

const clientPayload = {
correlation_id,
pull_number,
pr_ref: `refs/pull/${pull_number}/merge`,
pr_head_ref: pr.head.ref,
Expand All @@ -95,12 +105,16 @@ jobs:

core.setOutput('dispatched', 'true');
core.setOutput('event_types', eventTypes.join(','));
core.setOutput('correlation_id', correlation_id);
core.setOutput('trigger_comment_id', String(comment.id));

- name: Acknowledge in thread (optional)
if: steps.guard.outputs.skip != 'true' && steps.dispatch.outputs.dispatched == 'true'
id: ack
uses: actions/github-script@v8
env:
EVENT_TYPES: ${{ steps.dispatch.outputs.event_types }}
CORRELATION_ID: ${{ steps.dispatch.outputs.correlation_id }}
with:
script: |
const eventTypes = (process.env.EVENT_TYPES || '')
Expand All @@ -110,10 +124,17 @@ jobs:
const formatted = eventTypes.map(label => `\`repository_dispatch:${label}\``).join(', ');
const { owner, repo } = context.repo;
const issue_number = context.payload.issue.number;
await github.rest.issues.createComment({
const body = [
`✅ CI trigger requested by @${context.payload.comment.user.login}.`,
`Fired ${formatted}.`,
'',
`_Collecting run links for correlation \`${process.env.CORRELATION_ID}\`…_`
].join('\n');
const { data: comment } = await github.rest.issues.createComment({
owner, repo, issue_number,
body: `✅ CI retrigger requested by @${context.payload.comment.user.login}. Fired ${formatted}.`
body
});
core.setOutput('comment_id', String(comment.id));

- name: Notify missing ci label
if: steps.guard.outputs.skip != 'true' && steps.dispatch.outputs.dispatched != 'true'
Expand All @@ -126,5 +147,163 @@ jobs:
owner,
repo,
issue_number,
body: `⚠️ CI retrigger ignored because the pull request has no \`ci-*\` labels (e.g. \`ci-apo\`, \`ci-calc-x\`). Add the desired labels and try \`/ci\` again.`
body: `⚠️ CI trigger ignored because the pull request has no \`ci-*\` labels (e.g. \`ci-apo\`, \`ci-calc-x\`). Add the desired labels and try \`/ci\` again.`
});

watch:
needs: dispatch
if: needs.dispatch.outputs.dispatched == 'true'
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Track dispatched runs and update comment
uses: actions/github-script@v8
env:
CORRELATION_ID: ${{ needs.dispatch.outputs.correlation_id }}
ACK_COMMENT_ID: ${{ needs.dispatch.outputs.ack_comment_id }}
TRIGGER_COMMENT_ID: ${{ needs.dispatch.outputs.trigger_comment_id }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const correlationId = process.env.CORRELATION_ID;
if (!correlationId) {
core.warning('No correlation id supplied; nothing to watch.');
return;
}

const ackCommentId = Number(process.env.ACK_COMMENT_ID || 0);
if (!ackCommentId) {
core.warning('No comment id available for updates; skipping watch.');
return;
}
const triggerCommentId = Number(process.env.TRIGGER_COMMENT_ID || 0);
if (!triggerCommentId) {
core.warning('No trigger comment id available; skipping watch.');
return;
}

const prefix = `🚀 CI Watcher for correlation ${correlationId} triggered by comment ${triggerCommentId}`;
core.notice(`Watching workflow runs for correlation '${correlationId}' using comment ${ackCommentId}.`);

function fmt(run) {
const status = run.status;
const conclusion = run.conclusion;
const badge = status === 'completed'
? (conclusion === 'success' ? '🟢' : conclusion === 'failure' ? '🔴' : '🟡')
: (status === 'in_progress' ? '🟣' : '⚪️');
const title = run.display_title || run.name || `run ${run.id}`;
const statusText = status === 'completed' ? `${status}/${conclusion}` : status;
return `- ${badge} [${title}](${run.html_url}) — \`${statusText}\``;
}

const signatureOf = runs =>
runs
.map(run => `${run.id}:${run.status}/${run.conclusion || ''}`)
.sort()
.join('|');

const deadlineMs = Date.now() + 175 * 60 * 1000; // 175 minutes
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The 175-minute deadline is hardcoded but appears arbitrary given the 180-minute job timeout. Consider extracting this as a constant with a clear relationship to the job timeout (e.g., TIMEOUT_MINUTES - 5) to make the intent explicit and easier to maintain if the job timeout changes.

Suggested change
const deadlineMs = Date.now() + 175 * 60 * 1000; // 175 minutes
const TIMEOUT_MINUTES = 180; // Must match 'timeout-minutes' above
const WATCHER_DEADLINE_MINUTES = TIMEOUT_MINUTES - 5; // 5 minute buffer before job timeout
const deadlineMs = Date.now() + WATCHER_DEADLINE_MINUTES * 60 * 1000;

Copilot uses AI. Check for mistakes.
let found = [];

async function searchOnce() {
const runs = await github.paginate(
github.rest.actions.listWorkflowRunsForRepo,
{ owner, repo, event: 'repository_dispatch', per_page: 100 }
);
const cutoff = new Date(Date.now() - 60 * 60 * 1000); // last hour
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The 1-hour lookback window is hardcoded. If workflow dispatch takes longer to create runs or if there are clock skew issues, runs could be missed. Consider extracting this as a configurable constant or making it relative to the comment/trigger timestamp for more reliable run discovery.

Copilot uses AI. Check for mistakes.
return runs.filter(run => {
const createdAt = new Date(run.created_at);
const title = String(run.display_title || run.name || '');
return createdAt >= cutoff && title.includes(correlationId);
});
}

while (Date.now() < deadlineMs) {
found = await searchOnce();
if (found.length > 0) {
core.notice(`Discovered ${found.length} workflow run(s) for correlation '${correlationId}'.`);
break;
}
core.notice(`No runs found yet for correlation '${correlationId}'; retrying shortly.`);
await new Promise(res => setTimeout(res, 10000));
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The 10-second polling interval for initial run discovery is hardcoded. Extract this as a named constant (e.g., INITIAL_POLL_INTERVAL_MS) to improve readability and make it easier to tune if needed.

Copilot uses AI. Check for mistakes.
}

if (found.length === 0) {
core.notice(`Watcher timed out with no runs for correlation '${correlationId}'; notifying thread.`);
await github.rest.issues.updateComment({
owner,
repo,
comment_id: ackCommentId,
body: [
prefix,
`⚠️ I couldn't find any workflow runs for correlation \`${correlationId}\`.`,
`They may be delayed or misconfigured.`
].join('\n')
});
return;
}

const runIds = new Set(found.map(run => run.id));
let lastSignature = '';

async function refreshRuns() {
const ids = Array.from(runIds);
const refreshed = [];
for (const id of ids) {
const { data } = await github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: id
});
refreshed.push(data);
}
return refreshed;
Comment on lines +252 to +261
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The refreshRuns function performs sequential API calls for each workflow run. With multiple runs, this creates unnecessary latency. Consider using Promise.all() to fetch all run statuses concurrently, which would significantly reduce the refresh time.

Suggested change
const refreshed = [];
for (const id of ids) {
const { data } = await github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: id
});
refreshed.push(data);
}
return refreshed;
const promises = ids.map(id =>
github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: id
}).then(({ data }) => data)
);
return Promise.all(promises);

Copilot uses AI. Check for mistakes.
}

async function updateCommentIfChanged(runs, allDone) {
const signature = signatureOf(runs);
if (signature === lastSignature) {
// Run statuses unchanged; skipping comment update.
return;
}
lastSignature = signature;
core.notice(`Updating comment ${ackCommentId} with ${runs.length} run status entries (allDone=${allDone}).`);
await github.rest.issues.updateComment({
owner,
repo,
comment_id: ackCommentId,
body: [
prefix,
`🏃‍♀️ Tracking ${runs.length} workflow run(s):`,
'',
...runs.map(fmt),
'',
allDone ? '✅ All runs completed.' : '_Still running…_'
].join('\n')
});
}

await updateCommentIfChanged(found, found.every(run => run.status === 'completed'));

while (Date.now() < deadlineMs) {
const latest = await searchOnce();
for (const run of latest) {
if (!runIds.has(run.id)) {
runIds.add(run.id);
core.notice(`Detected additional run ${run.id} (${run.name || run.display_title || 'unnamed'}) for correlation '${correlationId}'.`);
}
}
const current = await refreshRuns();
const allDone = current.every(run => run.status === 'completed');
await updateCommentIfChanged(current, allDone);
if (allDone) {
core.notice(`All runs for correlation '${correlationId}' completed; stopping watcher.`);
break;
}
await new Promise(res => setTimeout(res, 60000));
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The 60-second polling interval for status updates is hardcoded. Extract this as a named constant (e.g., STATUS_POLL_INTERVAL_MS) to improve readability and make it easier to adjust polling frequency.

Copilot uses AI. Check for mistakes.
}

if (Date.now() >= deadlineMs) {
core.warning(`Watcher hit the deadline while monitoring correlation '${correlationId}'.`);
}
10 changes: 10 additions & 0 deletions .github/workflows/tests-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ on:
repository_dispatch:
types: [ci-unsloth, ci-all]

run-name: >-
${{ github.event_name == 'repository_dispatch'
&& format(
'PR #{0} - Label {1} - {2}',
github.event.client_payload.pull_number,
github.event.client_payload.ci_label,
github.event.client_payload.correlation_id
)
|| format('GPU Test - {0}', github.event_name) }}

jobs:
tests-full:
if: >
Expand Down