-
Notifications
You must be signed in to change notification settings - Fork 557
Track Actions Status in Issue Comment Responder #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5316592
7b3cc63
5a81c89
bf5933a
2f1f39c
8423f1d
7039cfa
9b4b46f
23cbb7c
42c2a2f
b4b4b44
1337aa8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ permissions: | |||||||||||||||||||||||||||||||||||||
| pull-requests: write | ||||||||||||||||||||||||||||||||||||||
| issues: write | ||||||||||||||||||||||||||||||||||||||
| contents: write | ||||||||||||||||||||||||||||||||||||||
| actions: read | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||
| dispatch: | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 || '') | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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' | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||||||
| 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
AI
Oct 27, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.