feat(reporting): add GitHub Checks API reporting #9636
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Label | |
| on: | |
| issues: | |
| types: [opened, edited, labeled, unlabeled] | |
| pull_request_target: | |
| types: [opened, edited, labeled, unlabeled, synchronize] | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| pull_request_review: | |
| types: [submitted] | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| comment-label: | |
| if: >- | |
| ( | |
| (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && | |
| ( | |
| contains(github.event.comment.body || '', '/kind') || | |
| contains(github.event.comment.body || '', '/priority') || | |
| contains(github.event.comment.body || '', '/actor') || | |
| contains(github.event.comment.body || '', '/triage') | |
| ) | |
| ) || ( | |
| github.event_name == 'pull_request_review' && | |
| ( | |
| contains(github.event.review.body || '', '/kind') || | |
| contains(github.event.review.body || '', '/priority') || | |
| contains(github.event.review.body || '', '/actor') || | |
| contains(github.event.review.body || '', '/triage') | |
| ) | |
| ) || ( | |
| github.event_name == 'issues' && | |
| (github.event.action == 'opened' || github.event.action == 'edited') && | |
| ( | |
| contains(github.event.issue.body || '', '/kind') || | |
| contains(github.event.issue.body || '', '/priority') || | |
| contains(github.event.issue.body || '', '/actor') || | |
| contains(github.event.issue.body || '', '/triage') | |
| ) | |
| ) || ( | |
| github.event_name == 'pull_request_target' && | |
| (github.event.action == 'opened' || github.event.action == 'edited') && | |
| ( | |
| contains(github.event.pull_request.body || '', '/kind') || | |
| contains(github.event.pull_request.body || '', '/priority') || | |
| contains(github.event.pull_request.body || '', '/actor') || | |
| contains(github.event.pull_request.body || '', '/triage') | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve command context | |
| id: context | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const eventName = context.eventName | |
| let body = '' | |
| let issueNumber = 0 | |
| if (eventName === 'issue_comment' || eventName === 'pull_request_review_comment') { | |
| body = context.payload.comment?.body || '' | |
| issueNumber = context.payload.issue?.number || context.payload.pull_request?.number || 0 | |
| } else if (eventName === 'pull_request_review') { | |
| body = context.payload.review?.body || '' | |
| issueNumber = context.payload.pull_request?.number || context.payload.issue?.number || 0 | |
| } else if (eventName === 'issues') { | |
| body = context.payload.issue?.body || '' | |
| issueNumber = context.payload.issue?.number || 0 | |
| } else if (eventName === 'pull_request_target') { | |
| body = context.payload.pull_request?.body || '' | |
| issueNumber = context.payload.pull_request?.number || 0 | |
| } else { | |
| core.setFailed(`Unsupported event for label workflow: ${eventName}`) | |
| return | |
| } | |
| const strippedBody = body.replace(/<!--[\s\S]*?-->/g, '') | |
| const hasCommand = | |
| /^\s*\/(?:remove-)?(?:kind|priority|actor)\s+.+$/m.test(strippedBody) || | |
| /^\s*\/(?:remove-)?triage-accepted\s*$/m.test(strippedBody) | |
| core.setOutput('body', body) | |
| core.setOutput('issue_number', String(issueNumber)) | |
| core.setOutput('has_command', hasCommand ? 'true' : 'false') | |
| if (!hasCommand) { | |
| core.info('No label commands found in selected source body') | |
| return | |
| } | |
| if (!issueNumber) { | |
| core.setFailed('Could not determine issue number for label command') | |
| return | |
| } | |
| - name: Apply labels | |
| if: steps.context.outputs.has_command == 'true' | |
| uses: actions/github-script@v7 | |
| env: | |
| COMMAND_BODY: ${{ steps.context.outputs.body }} | |
| ISSUE_NUMBER: ${{ steps.context.outputs.issue_number }} | |
| with: | |
| script: | | |
| const rawBody = process.env.COMMAND_BODY || ''; | |
| // Strip HTML comments so that example commands in PR | |
| // templates (<!-- /kind bug -->) are not matched. | |
| const body = rawBody.replace(/<!--[\s\S]*?-->/g, ''); | |
| const issueNumber = Number(process.env.ISSUE_NUMBER || '0'); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| if (!issueNumber) { | |
| throw new Error('Issue number is required to apply labels'); | |
| } | |
| const commands = [ | |
| { command: 'kind', prefix: 'kind/' }, | |
| { command: 'priority', prefix: 'priority/' }, | |
| { command: 'actor', prefix: 'actor/' }, | |
| ]; | |
| const toAdd = []; | |
| const toRemove = []; | |
| for (const { command, prefix } of commands) { | |
| const addPattern = new RegExp(`^\\s*/(?:remove-)?${command}\\s+(.+)$`, 'gm'); | |
| let match; | |
| while ((match = addPattern.exec(body)) !== null) { | |
| const line = match[0].trim(); | |
| const value = match[1].trim(); | |
| const label = `${prefix}${value}`; | |
| if (line.startsWith(`/remove-${command}`)) { | |
| toRemove.push(label); | |
| } else { | |
| toAdd.push(label); | |
| } | |
| } | |
| } | |
| // Handle /triage-accepted and /remove-triage-accepted | |
| if (/^\s*\/triage-accepted\s*$/m.test(body)) { | |
| toAdd.push('triage-accepted'); | |
| } | |
| if (/^\s*\/remove-triage-accepted\s*$/m.test(body)) { | |
| toRemove.push('triage-accepted'); | |
| } | |
| if (toAdd.length === 0 && toRemove.length === 0) { | |
| core.info('No label commands found'); | |
| return; | |
| } | |
| if (toAdd.length > 0) { | |
| core.info(`Adding labels: ${toAdd.join(', ')}`); | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| labels: toAdd, | |
| }); | |
| } | |
| for (const label of toRemove) { | |
| core.info(`Removing label: ${label}`); | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| name: label, | |
| }); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| core.warning(`Label ${label} was not present`); | |
| } | |
| } | |
| sync-labels: | |
| if: always() && !cancelled() && (needs.comment-label.result == 'success' || needs.comment-label.result == 'skipped') | |
| needs: comment-label | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Sync needs-* labels | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const item = context.payload.issue || context.payload.pull_request; | |
| const { data: fresh } = await github.rest.issues.listLabelsOnIssue({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: item.number, | |
| }); | |
| const labels = fresh.map(l => l.name); | |
| const rules = [ | |
| { prefix: null, match: 'triage-accepted', needs: 'needs-triage' }, | |
| { prefix: 'kind/', match: null, needs: 'needs-kind' }, | |
| { prefix: 'priority/', match: null, needs: 'needs-priority' }, | |
| { prefix: 'actor/', match: null, needs: 'needs-actor' }, | |
| ]; | |
| const errors = []; | |
| for (const rule of rules) { | |
| try { | |
| const satisfied = rule.match | |
| ? labels.includes(rule.match) | |
| : labels.some(l => l.startsWith(rule.prefix)); | |
| const hasNeeds = labels.includes(rule.needs); | |
| if (!satisfied && !hasNeeds) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: item.number, | |
| labels: [rule.needs], | |
| }); | |
| } else if (satisfied && hasNeeds) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: item.number, | |
| name: rule.needs, | |
| }); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| } catch (e) { | |
| errors.push({ rule: rule.needs, error: e }); | |
| core.warning(`Failed to sync ${rule.needs}: ${e.message}`); | |
| } | |
| } | |
| // For PRs, check whether the body contains a release-note block and | |
| // sync release-note / release-note-none / needs-release-note labels. | |
| const pr = context.payload.pull_request; | |
| if (pr) { | |
| try { | |
| const body = pr.body || ''; | |
| const match = body.match(/```release-note\s*\n([\s\S]*?)```/); | |
| const noteContent = match ? match[1].trim() : ''; | |
| const isNone = noteContent.toLowerCase() === 'none'; | |
| const hasNote = noteContent.length > 0 && !isNone; | |
| const desiredLabel = hasNote ? 'release-note' : isNone ? 'release-note-none' : null; | |
| const releaseLabels = ['release-note', 'release-note-none', 'needs-release-note']; | |
| for (const label of releaseLabels) { | |
| const present = labels.includes(label); | |
| const wanted = label === desiredLabel || (label === 'needs-release-note' && desiredLabel === null); | |
| if (wanted && !present) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: [label], | |
| }); | |
| } else if (!wanted && present) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| name: label, | |
| }); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| errors.push({ rule: 'release-note', error: e }); | |
| core.warning(`Failed to sync release-note labels: ${e.message}`); | |
| } | |
| } | |
| if (errors.length > 0) { | |
| throw new Error(`Failed to sync labels: ${errors.map(e => e.rule).join(', ')}`); | |
| } | |
| check-pr-labels: | |
| runs-on: ubuntu-latest | |
| needs: sync-labels | |
| if: always() && github.event.pull_request | |
| permissions: | |
| issues: read | |
| steps: | |
| - name: Check for blocking labels | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: labels } = await github.rest.issues.listLabelsOnIssue({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| }); | |
| const blocking = ['needs-kind', 'needs-release-note']; | |
| const found = blocking.filter(b => labels.some(l => l.name === b)); | |
| if (found.length > 0) { | |
| core.setFailed(`PR is missing required labels. Blocking labels found: ${found.join(', ')}`); | |
| } else { | |
| core.info('All required labels are present'); | |
| } |