Add taskTemplates to TaskSpawner for per-item pipeline spawning #4730
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] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| comment-label: | |
| if: >- | |
| ( | |
| github.event_name == 'issue_comment' || | |
| github.event_name == 'pull_request_review_comment' || | |
| github.event_name == 'pull_request_review' || | |
| (github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'edited')) || | |
| (github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'edited')) | |
| ) && ( | |
| contains(github.event.comment.body || github.event.review.body || github.event.issue.body || github.event.pull_request.body || '', '/kind') || | |
| contains(github.event.comment.body || github.event.review.body || github.event.issue.body || github.event.pull_request.body || '', '/priority') || | |
| contains(github.event.comment.body || github.event.review.body || github.event.issue.body || github.event.pull_request.body || '', '/actor') || | |
| contains(github.event.comment.body || github.event.review.body || github.event.issue.body || 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 body = | |
| context.payload.comment?.body || | |
| context.payload.review?.body || | |
| context.payload.issue?.body || | |
| context.payload.pull_request?.body || | |
| '' | |
| // Resolve the actor from the author of the text containing the | |
| // label command, not from the event sender. When a bot edits | |
| // a PR description the sender is the bot, but the PR author | |
| // (who wrote /kind, /priority, etc.) is the relevant actor for | |
| // permission checks. | |
| const actor = | |
| context.payload.comment?.user?.login || | |
| context.payload.review?.user?.login || | |
| context.payload.issue?.user?.login || | |
| context.payload.pull_request?.user?.login || | |
| '' | |
| const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number || 0 | |
| core.setOutput('body', body) | |
| core.setOutput('actor', actor) | |
| core.setOutput('issue_number', String(issueNumber)) | |
| if (!actor) { | |
| core.setFailed('Could not determine actor for label command') | |
| } | |
| if (!issueNumber) { | |
| core.setFailed('Could not determine issue number for label command') | |
| } | |
| - name: Check permission | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${{ steps.context.outputs.actor }}/permission" -q .permission) | |
| if [[ "$PERMISSION" != "admin" && "$PERMISSION" != "write" ]]; then | |
| echo "User ${{ steps.context.outputs.actor }} does not have write permission (permission=$PERMISSION)" | |
| exit 1 | |
| fi | |
| - name: Apply labels | |
| 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'); | |
| } |