Skip to content

feat(reporting): add GitHub Checks API reporting #9636

feat(reporting): add GitHub Checks API reporting

feat(reporting): add GitHub Checks API reporting #9636

Workflow file for this run

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');
}