fix tests #162
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: Status Labels | |
| on: | |
| issues: | |
| types: [opened, reopened, assigned, unassigned, labeled, unlabeled, closed] | |
| pull_request: | |
| types: [opened, reopened, synchronize, ready_for_review, converted_to_draft, review_requested, review_request_removed, closed] | |
| pull_request_review: | |
| types: [submitted, dismissed] | |
| permissions: | |
| issues: write | |
| pull-requests: read | |
| jobs: | |
| issue-status-labels: | |
| if: github.event_name == 'issues' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Sync issue status labels | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issue = context.payload.issue; | |
| const issueNumber = issue.number; | |
| const state = issue.state; | |
| const assignees = issue.assignees || []; | |
| const labels = (issue.labels || []).map(l => l.name.toLowerCase()); | |
| const workingLabel = 'in progress'; | |
| const blockedLabels = ['blocked', 'needs-triage', 'do-not-assign']; | |
| const STATUS_TRIAGE = 'status: triage'; | |
| const STATUS_IN_PROGRESS = 'status: in-progress'; | |
| const STATUS_BLOCKED = 'status: blocked'; | |
| const STATUS_CLOSED = 'status: closed'; | |
| const statusLabels = [STATUS_TRIAGE, STATUS_IN_PROGRESS, STATUS_BLOCKED, STATUS_CLOSED]; | |
| async function safeWrite(fn, op) { | |
| try { | |
| return await fn(); | |
| } catch (e) { | |
| if (e.status === 403) { | |
| core.warning(`Skipping ${op}: token cannot write in this context (403).`); | |
| return null; | |
| } | |
| throw e; | |
| } | |
| } | |
| async function ensureLabel(name) { | |
| const existing = await github.rest.issues.getLabel({ owner, repo, name }).catch(e => { | |
| if (e.status === 404) return null; | |
| throw e; | |
| }); | |
| if (existing) return; | |
| await safeWrite( | |
| () => github.rest.issues.createLabel({ | |
| owner, | |
| repo, | |
| name, | |
| color: 'BFDADC', | |
| description: `Issue status label: ${name}` | |
| }), | |
| `create label ${name}` | |
| ); | |
| } | |
| for (const label of statusLabels) await ensureLabel(label); | |
| const shouldBe = new Set(); | |
| const isBlocked = labels.some(l => blockedLabels.includes(l)); | |
| const hasAssignee = assignees.length > 0; | |
| const hasWorking = labels.includes(workingLabel); | |
| if (state === 'closed') { | |
| shouldBe.add(STATUS_CLOSED); | |
| } else if (isBlocked) { | |
| shouldBe.add(STATUS_BLOCKED); | |
| } else if (hasAssignee || hasWorking) { | |
| shouldBe.add(STATUS_IN_PROGRESS); | |
| } else { | |
| shouldBe.add(STATUS_TRIAGE); | |
| } | |
| const remove = statusLabels.filter(l => !shouldBe.has(l) && labels.includes(l.toLowerCase())); | |
| if (remove.length > 0) { | |
| for (const name of remove) { | |
| try { | |
| await safeWrite( | |
| () => github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name }), | |
| `remove label ${name} from issue #${issueNumber}` | |
| ); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| } | |
| const add = [...shouldBe].filter(l => !labels.includes(l.toLowerCase())); | |
| if (add.length > 0) { | |
| await safeWrite( | |
| () => github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| labels: add | |
| }), | |
| `add labels to issue #${issueNumber}` | |
| ); | |
| } | |
| pr-status-labels: | |
| if: github.event_name == 'pull_request' || github.event_name == 'pull_request_review' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Sync PR status labels | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const pullNumber = context.payload.pull_request.number; | |
| const prPayload = context.payload.pull_request; | |
| const fromFork = !!prPayload.head?.repo?.fork || prPayload.head?.repo?.full_name !== `${owner}/${repo}`; | |
| const pr = await github.rest.pulls.get({ | |
| owner, | |
| repo, | |
| pull_number: pullNumber | |
| }); | |
| const STATUS_DRAFT = 'status: draft'; | |
| const STATUS_REVIEW = 'status: in-review'; | |
| const STATUS_APPROVED = 'status: approved'; | |
| const STATUS_CHANGES = 'status: changes-requested'; | |
| const STATUS_MERGED = 'status: merged'; | |
| const STATUS_CLOSED = 'status: closed'; | |
| const statusLabels = [STATUS_DRAFT, STATUS_REVIEW, STATUS_APPROVED, STATUS_CHANGES, STATUS_MERGED, STATUS_CLOSED]; | |
| async function safeWrite(fn, op) { | |
| try { | |
| return await fn(); | |
| } catch (e) { | |
| if (e.status === 403) { | |
| core.warning(`Skipping ${op}: token cannot write in this context (403).`); | |
| return null; | |
| } | |
| throw e; | |
| } | |
| } | |
| if (fromFork) { | |
| core.warning('Skipping PR status label writes for fork-origin PR context.'); | |
| return; | |
| } | |
| async function ensureLabel(name) { | |
| const existing = await github.rest.issues.getLabel({ owner, repo, name }).catch(e => { | |
| if (e.status === 404) return null; | |
| throw e; | |
| }); | |
| if (existing) return; | |
| await safeWrite( | |
| () => github.rest.issues.createLabel({ | |
| owner, | |
| repo, | |
| name, | |
| color: 'D4C5F9', | |
| description: `PR status label: ${name}` | |
| }), | |
| `create label ${name}` | |
| ); | |
| } | |
| for (const label of statusLabels) await ensureLabel(label); | |
| const currentLabels = (pr.data.labels || []).map(l => l.name.toLowerCase()); | |
| const shouldBe = new Set(); | |
| if (pr.data.state === 'closed') { | |
| if (pr.data.merged_at) shouldBe.add(STATUS_MERGED); | |
| else shouldBe.add(STATUS_CLOSED); | |
| } else if (pr.data.draft) { | |
| shouldBe.add(STATUS_DRAFT); | |
| } else { | |
| const reviews = await github.paginate(github.rest.pulls.listReviews, { | |
| owner, | |
| repo, | |
| pull_number: pullNumber, | |
| per_page: 100 | |
| }); | |
| const latestByUser = new Map(); | |
| for (const r of reviews) { | |
| const login = r.user?.login; | |
| if (!login) continue; | |
| latestByUser.set(login, r.state); | |
| } | |
| const states = [...latestByUser.values()]; | |
| if (states.includes('CHANGES_REQUESTED')) shouldBe.add(STATUS_CHANGES); | |
| else if (states.includes('APPROVED')) shouldBe.add(STATUS_APPROVED); | |
| else shouldBe.add(STATUS_REVIEW); | |
| } | |
| const remove = statusLabels.filter(l => !shouldBe.has(l) && currentLabels.includes(l.toLowerCase())); | |
| for (const name of remove) { | |
| try { | |
| await safeWrite( | |
| () => github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number: pullNumber, | |
| name | |
| }), | |
| `remove label ${name} from PR #${pullNumber}` | |
| ); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| const add = [...shouldBe].filter(l => !currentLabels.includes(l.toLowerCase())); | |
| if (add.length > 0) { | |
| await safeWrite( | |
| () => github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: pullNumber, | |
| labels: add | |
| }), | |
| `add labels to PR #${pullNumber}` | |
| ); | |
| } |