Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,25 @@ Some architectural notes about key decisions and concepts in our workflows:
- **head commit**: The HEAD commit in the pull request's branch. Same as `github.event.pull_request.head.sha`.
- **merge commit**: The temporary "test merge commit" that GitHub Actions creates and updates for the pull request. Same as `refs/pull/${{ github.event.pull_request.number }}/merge`.
- **target commit**: The base branch's parent of the "test merge commit" to compare against.

## Concurrency Groups

We use [GitHub's Concurrency Groups](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs) to cancel older jobs on pushes to Pull Requests.
When two workflows are in the same group, a newer workflow cancels an older workflow.
Thus, it is important how to construct the group keys:

- Because we want to run jobs for different events at same time, we add `github.event_name` to the key. This is the case for the `pull_request` which runs on changes to the workflow files to test the new files and the same workflow from the base branch run via `pull_request_event`.

- We don't want workflows of different Pull Requests to cancel each other, so we include `github.event.pull_request.number`. The [GitHub docs](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs#example-using-a-fallback-value) show using `github.head_ref` for this purpose, but this doesn't work well with forks: Different users could have the same head branch name in their forks and run CI for their PRs at the same time.

- Sometimes, there is no `pull_request.number`. That's the case for `push` or `workflow_run` events. To ensure non-PR runs are never cancelled, we add a fallback of `github.run_id`. This is a unique value for each workflow run.

- Of course, we run multiple workflows at the same time, so we add `github.workflow` to the key. Otherwise workflows would cancel each other.

- There is a special case for reusable workflows called via `workflow_call` - they will have `github.workflow` set to their parent workflow's name. Thus, they would cancel each other. That's why we additionally hardcode the name of the workflow as well.

This results in a key with the following semantics:

```
<running-workflow>-<triggering-workflow>-<triggered-event>-<pull-request/fallback>
```
2 changes: 1 addition & 1 deletion .github/workflows/backport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
types: [closed, labeled]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: backport-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
pull_request_target:

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: build-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
pull_request_target:

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: check-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeowners-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ on:
types: [opened, ready_for_review, synchronize, reopened]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: codeowners-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/dismissed-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Dismissed review

on:
workflow_run:
workflows:
- Review dismissed
types: [completed]

concurrency:
group: dismissed-review-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions:
pull-requests: write

defaults:
run:
shell: bash

jobs:
# The `check-cherry-picks` workflow creates review comments which reviewers
# are encouraged to manually dismiss if they're not relevant.
# When a CI-generated review is dismissed, this job automatically minimizes
# it, preventing it from cluttering the PR.
minimize:
name: Minimize as resolved
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
// 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
await Promise.all(
(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)
.map(async (pull_request) =>
Promise.all(
(await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull_request.number
})).filter(review =>
review.user.login == 'github-actions[bot]' &&
review.state == 'DISMISSED'
).map(review => github.graphql(`
mutation($node_id:ID!) {
minimizeComment(input: {
classifier: RESOLVED,
subjectId: $node_id
})
{ clientMutationId }
}`,
{ node_id: review.node_id }
))
)
)
)
2 changes: 1 addition & 1 deletion .github/workflows/edited.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
types: [edited]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: edited-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/eval-aliases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
pull_request_target:

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: eval-aliases-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down
52 changes: 9 additions & 43 deletions .github/workflows/eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
- python-updates

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: eval-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down Expand Up @@ -217,46 +217,6 @@ jobs:
name: comparison
path: comparison/*

- name: Labelling pull request
if: ${{ github.event_name == 'pull_request_target' }}
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
}

// 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
}))
)

// 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
})
}

- name: Add eval summary to commit statuses
if: ${{ github.event_name == 'pull_request_target' }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
Expand Down Expand Up @@ -289,6 +249,14 @@ jobs:
target_url
})

labels:
name: Labels
needs: [ compare ]
uses: ./.github/workflows/labels.yml
permissions:
issues: write
pull-requests: write

reviewers:
name: Reviewers
# No dependency on "compare", so that it can start at the same time.
Expand All @@ -298,5 +266,3 @@ jobs:
if: needs.prepare.outputs.targetSha
uses: ./.github/workflows/reviewers.yml
secrets: inherit
with:
caller: ${{ github.workflow }}
122 changes: 119 additions & 3 deletions .github/workflows/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
name: "Label PR"

on:
pull_request_target:
workflow_call:
workflow_run:
workflows:
- Review dismissed
- Review submitted
types: [completed]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: labels-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions:
contents: read
issues: write # needed to create *new* labels
pull-requests: write

Expand All @@ -27,8 +31,114 @@ 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: 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')

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 ]
}

await Promise.all(
pull_requests.map(async (pull_request) => {
const pr = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull_request.number
}

// 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 = new Set(
(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 = new Set(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.size > 0) after.push(`12.approvals: ${approvals.size > 2 ? '3+' : approvals.size}`)
if (Array.from(maintainers).some(m => approvals.has(m))) 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' ||
Expand All @@ -39,8 +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' ||
Expand All @@ -51,11 +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' ||
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
pull_request_target:

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
group: lint-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions: {}
Expand Down
Loading