workflows/labels: manage approval labels

The category 12 labels for number of approvals and approved by package
maintainer are now automatically managed by the labels workflow. The
logic is slightly different from the "by: package-maintainer" label. For
approval, it's enough if *any one* maintainer approves the PR to have
the label added, even if the PR touches multiple packages.

The workflow only counts approved reviews, no matter whether there had
been a push in the meantime or not. To achieve the currently used logic
of "expiring approvals after push", we will have to set up a branch
protection rule, which actually dismissed those reviews automatically.
This commit is contained in:
Wolfgang Walther
2025-06-10 22:15:29 +02:00
parent 2d0bcd7165
commit 5f09e16f00
2 changed files with 111 additions and 28 deletions
+94 -28
View File
@@ -12,6 +12,11 @@ on:
description: Name of the calling workflow.
required: true
type: string
workflow_run:
workflows:
- Review dismissed
- Review submitted
types: [completed]
concurrency:
group: ${{ inputs.caller }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
@@ -27,56 +32,113 @@ jobs:
runs-on: ubuntu-24.04-arm
if: "!contains(github.event.pull_request.title, '[skip treewide]')"
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
id: eval
with:
script: |
const run_id = (await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'eval.yml',
event: 'pull_request_target',
head_sha: context.payload.pull_request?.head.sha ?? context.payload.workflow_run.head_sha
})).data.workflow_runs[0]?.id
core.setOutput('run-id', run_id)
- name: Download the comparison results
if: steps.eval.outputs.run-id
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
run-id: ${{ steps.eval.outputs.run-id }}
github-token: ${{ github.token }}
pattern: comparison
path: comparison
merge-multiple: true
- name: Labels from eval
if: ${{ github.event_name != 'pull_request' }}
if: steps.eval.outputs.run-id && github.event_name != 'pull_request'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { readFile } = require('node:fs/promises')
const pr = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number
let pull_requests
if (context.payload.workflow_run) {
// PRs from forks don't have any PRs associated by default.
// Thus, we request the PR number with an API call *to* the fork's repo.
// Multiple pull requests can be open from the same head commit, either via
// different base branches or head branches.
const { head_repository, head_sha, repository } = context.payload.workflow_run
pull_requests = (await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {
owner: head_repository.owner.login,
repo: head_repository.name,
commit_sha: head_sha
})).filter(pull_request => pull_request.base.repo.id == repository.id)
} else {
pull_requests = [ context.payload.pull_request ]
}
// Get all currently set labels that we manage
const before =
(await github.paginate(github.rest.issues.listLabelsOnIssue, pr))
.map(({ name }) => name)
.filter(name => name.startsWith('10.rebuild') || name == '11.by: package-maintainer')
// And the labels that should be there
const after = JSON.parse(await readFile('comparison/changed-paths.json', 'utf-8')).labels
// Remove the ones not needed anymore
await Promise.all(
before.filter(name => !after.includes(name))
.map(name => github.rest.issues.removeLabel({
...pr,
name
}))
)
pull_requests.map(async (pull_request) => {
const pr = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull_request.number
}
// And add the ones that aren't set already
const added = after.filter(name => !before.includes(name))
if (added.length > 0) {
await github.rest.issues.addLabels({
...pr,
labels: added
// Get all currently set labels that we manage
const before =
(await github.paginate(github.rest.issues.listLabelsOnIssue, pr))
.map(({ name }) => name)
.filter(name =>
name.startsWith('10.rebuild') ||
name == '11.by: package-maintainer' ||
name.startsWith('12.approvals:') ||
name == '12.approved-by: package-maintainer'
)
const approvals =
(await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull_request.number
}))
.filter(review => review.state == 'APPROVED')
.map(review => review.user.id)
const maintainers = Object.keys(
JSON.parse(await readFile('comparison/maintainers.json', 'utf-8'))
)
// And the labels that should be there
const after = JSON.parse(await readFile('comparison/changed-paths.json', 'utf-8')).labels
if (approvals.length > 0) after.push(`12.approvals: ${approvals.length > 2 ? '3+' : approvals.length}`)
if (maintainers.some(id => approvals.includes(id))) after.push('12.approved-by: package-maintainer')
// Remove the ones not needed anymore
await Promise.all(
before.filter(name => !after.includes(name))
.map(name => github.rest.issues.removeLabel({
...pr,
name
}))
)
// And add the ones that aren't set already
const added = after.filter(name => !before.includes(name))
if (added.length > 0) {
await github.rest.issues.addLabels({
...pr,
labels: added
})
}
})
}
)
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
name: Labels from touched files
if: |
github.event_name != 'workflow_run' &&
github.event.pull_request.head.repo.owner.login != 'NixOS' || !(
github.head_ref == 'haskell-updates' ||
github.head_ref == 'python-updates' ||
@@ -87,9 +149,11 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler.yml # default
sync-labels: true
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
name: Labels from touched files (no sync)
if: |
github.event_name != 'workflow_run' &&
github.event.pull_request.head.repo.owner.login != 'NixOS' || !(
github.head_ref == 'haskell-updates' ||
github.head_ref == 'python-updates' ||
@@ -100,12 +164,14 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler-no-sync.yml
sync-labels: false
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
name: Labels from touched files (development branches)
# Development branches like staging-next, haskell-updates and python-updates get special labels.
# This is to avoid the mass of labels there, which is mostly useless - and really annoying for
# the backport labels.
if: |
github.event_name != 'workflow_run' &&
github.event.pull_request.head.repo.owner.login == 'NixOS' && (
github.head_ref == 'haskell-updates' ||
github.head_ref == 'python-updates' ||
+17
View File
@@ -0,0 +1,17 @@
name: Review submitted
on:
pull_request_review:
types: [submitted]
permissions: {}
defaults:
run:
shell: bash
jobs:
trigger:
runs-on: ubuntu-24.04-arm
steps:
- run: echo This is a no-op only used as a trigger for workflow_run.