mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-25 18:10:37 +00:00
Compare commits
4 Commits
nixos-25.0
...
lib-types-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2513e2f1f0 | ||
|
|
e938d5b77a | ||
|
|
1980e9a444 | ||
|
|
851d4f4f2b |
@@ -23,7 +23,8 @@ insert_final_newline = false
|
||||
|
||||
# see https://nixos.org/nixpkgs/manual/#chap-conventions
|
||||
|
||||
[*.{bash,css,js,json,lock,md,nix,pl,pm,py,rb,sh,xml}]
|
||||
# Match json/lockfiles/markdown/nix/perl/python/ruby/shell/docbook files, set indent to spaces
|
||||
[*.{bash,json,lock,md,nix,pl,pm,py,rb,sh,xml}]
|
||||
indent_style = space
|
||||
|
||||
# Match docbook files, set indent width of one
|
||||
@@ -31,7 +32,7 @@ indent_style = space
|
||||
indent_size = 1
|
||||
|
||||
# Match json/lockfiles/markdown/nix/ruby files, set indent width of two
|
||||
[*.{js,json,lock,md,nix,rb}]
|
||||
[*.{json,lock,md,nix,rb}]
|
||||
indent_size = 2
|
||||
|
||||
# Match all the Bash code in Nix files, set indent width of two
|
||||
|
||||
@@ -193,9 +193,6 @@ cffc27daf06c77c0d76bc35d24b929cb9d68c3c9
|
||||
# nixos/kanidm: inherit lib, nixfmt
|
||||
8f18393d380079904d072007fb19dc64baef0a3a
|
||||
|
||||
# fetchhg: format after refactoring with lib.extendMkDerivation and make overridable (#423539)
|
||||
34a5b1eb23129f8fb62c677e3760903f6d43228f
|
||||
|
||||
# fetchurl: nixfmt-rfc-style
|
||||
ce21e97a1f20dee15da85c084f9d1148d84f853b
|
||||
|
||||
@@ -269,12 +266,3 @@ a034fb50f79816c6738fb48b48503b09ea3b0132
|
||||
|
||||
# treewide: switch instances of lib.teams.*.members to the new meta.teams attribute
|
||||
05580f4b4433fda48fff30f60dfd303d6ee05d21
|
||||
|
||||
# nixfmt 1.0.0
|
||||
a46262ae77e4016fe5a4a390c4a39c0c1b266428 # !autorebase nix-shell --run treefmt
|
||||
aefcb0d50d1124314429a11ed6b7aaaedf2861c5 # !autorebase nix-shell --run treefmt
|
||||
|
||||
# nixf-diagnose
|
||||
6c1e6f90d35932355fdee300b13136a8b7ec851c # !autorebase nix-shell --run treefmt
|
||||
1c6af9ba0a9667e10ad97f53d2cff663abc290c9 # !autorebase nix-shell --run treefmt
|
||||
5cbdb94f3aa2e7de13f3c0a67899ec17427b58ce # !autorebase nix-shell --run treefmt
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/01_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/01_bug_report.yml
vendored
@@ -100,7 +100,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -114,7 +114,7 @@ body:
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
|
||||
If this issue is related to the Darwin packaging architecture as a whole, or is related to the core Darwin frameworks, consider mentioning the `@NixOS/darwin-core` team.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -104,7 +104,7 @@ body:
|
||||
Please note that the maintainer attribute name does not always match the maintainer's GitHub username. If that occurs, try looking in [`maintainers/maintainer-list.nix`](https://github.com/NixOS/nixpkgs/blob/master/maintainers/maintainer-list.nix) for the maintainer attribute name, and checking if the maintainer has a listed GitHub username.
|
||||
|
||||
If in doubt, check `git blame` for whoever last touched the module, or check the associated package's maintainers. Please add the mentions above the `---` characters.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/04_build_failure.yml
vendored
2
.github/ISSUE_TEMPLATE/04_build_failure.yml
vendored
@@ -109,7 +109,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/05_update_request.yml
vendored
2
.github/ISSUE_TEMPLATE/05_update_request.yml
vendored
@@ -84,7 +84,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/06_module_request.yml
vendored
2
.github/ISSUE_TEMPLATE/06_module_request.yml
vendored
@@ -60,7 +60,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -62,7 +62,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -46,7 +46,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +119,7 @@ body:
|
||||
label: "Notify maintainers"
|
||||
description: |
|
||||
Please mention the people who are in the **Maintainers** list of the offending package. This is done by by searching for the package on the [NixOS Package Search](https://search.nixos.org/packages) and mentioning the people listed under **Maintainers** by prefixing their GitHub usernames with an '@' character. Please add the mentions above the `---` characters in the template below.
|
||||
value: |2
|
||||
value: |
|
||||
|
||||
|
||||
---
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/10_package_request.yml
vendored
36
.github/ISSUE_TEMPLATE/10_package_request.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: "Request: Nix Package"
|
||||
description: "Package requests are no longer accepted. Please open a Pull Request with your desired package instead."
|
||||
title: "Package Request"
|
||||
labels: ["0.kind: packaging request", "4.workflow: auto-close"]
|
||||
body:
|
||||
- type: "markdown"
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<a href="https://nixos.org">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/NixOS/nixos-artwork/refs/heads/master/logo/nixos.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NixOS/nixos-artwork/refs/heads/master/logo/nixos-white.svg">
|
||||
<img src="https://raw.githubusercontent.com/NixOS/nixos-artwork/refs/heads/master/logo/nixos.svg" width="400px" alt="NixOS logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Thank you for your interest in packaging new software in Nixpkgs. Unfortunately, to mitigate the unsustainable growth of unmaintained packages, **Nixpkgs is no longer accepting package requests** via Issues.
|
||||
|
||||
As a [volunteer community][community], we are always open to new contributors. If you wish to see this package in Nixpkgs, **we encourage you to [contribute] it yourself**, via a Pull Request. Anyone can [become a package maintainer][maintainers]! You can find language-specific packaging information in the [Nixpkgs Manual][nixpkgs]. Should you need any help, please reach out to the community on [Matrix] or [Discourse].
|
||||
|
||||
[community]: https://nixos.org/community
|
||||
[contribute]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#quick-start-to-adding-a-package
|
||||
[maintainers]: https://github.com/NixOS/nixpkgs/blob/master/maintainers/README.md
|
||||
[nixpkgs]: https://nixos.org/manual/nixpkgs/unstable/
|
||||
[Matrix]: https://matrix.to/#/#dev:nixos.org
|
||||
[Discourse]: https://discourse.nixos.org/c/dev/14
|
||||
|
||||
---
|
||||
- type: "checkboxes"
|
||||
id: "ignored"
|
||||
attributes:
|
||||
label: "Issues for new package requests are not accepted. Please open a Pull Request instead."
|
||||
options:
|
||||
- label: "I didn't read any of that."
|
||||
96
.github/actions/checkout/action.yml
vendored
96
.github/actions/checkout/action.yml
vendored
@@ -1,96 +0,0 @@
|
||||
name: Checkout
|
||||
|
||||
description: 'Checkout into trusted / untrusted / pinned folders consistently.'
|
||||
|
||||
inputs:
|
||||
merged-as-untrusted-at:
|
||||
description: "Whether and which SHA to checkout for the merge commit in the ./nixpkgs/untrusted folder."
|
||||
target-as-trusted-at:
|
||||
description: "Whether and which SHA to checkout for the target commit in the ./nixpkgs/trusted folder."
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
MERGED_SHA: ${{ inputs.merged-as-untrusted-at }}
|
||||
TARGET_SHA: ${{ inputs.target-as-trusted-at }}
|
||||
with:
|
||||
script: |
|
||||
const { spawn } = require('node:child_process')
|
||||
const { join } = require('node:path')
|
||||
|
||||
async function run(cmd, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(cmd, args, {
|
||||
stdio: 'inherit'
|
||||
})
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) resolve()
|
||||
else reject(code)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// These are set automatically by the spare checkout for .github/actions.
|
||||
// Undo them, otherwise git fetch below will not do anything.
|
||||
await run('git', 'config', 'unset', 'remote.origin.promisor')
|
||||
await run('git', 'config', 'unset', 'remote.origin.partialclonefilter')
|
||||
|
||||
// Getting the pinned SHA via API allows us to do one single fetch call for all commits.
|
||||
// Otherwise we would have to fetch merged/target first, read pinned, fetch again.
|
||||
// A single fetch call comes with a lot less overhead. The fetch takes essentially the
|
||||
// same time no matter whether its 1, 2 or 3 commits at once.
|
||||
async function getPinnedSha(ref) {
|
||||
if (!ref) return undefined
|
||||
const { content, encoding } = (await github.rest.repos.getContent({
|
||||
...context.repo,
|
||||
path: 'ci/pinned.json',
|
||||
ref,
|
||||
})).data
|
||||
const pinned = JSON.parse(Buffer.from(content, encoding).toString())
|
||||
return pinned.pins.nixpkgs.revision
|
||||
}
|
||||
|
||||
const commits = [
|
||||
{
|
||||
sha: process.env.MERGED_SHA,
|
||||
path: 'untrusted',
|
||||
},
|
||||
{
|
||||
sha: await getPinnedSha(process.env.MERGED_SHA),
|
||||
path: 'untrusted-pinned'
|
||||
},
|
||||
{
|
||||
sha: process.env.TARGET_SHA,
|
||||
path: 'trusted',
|
||||
},
|
||||
{
|
||||
sha: await getPinnedSha(process.env.TARGET_SHA),
|
||||
path: 'trusted-pinned'
|
||||
}
|
||||
].filter(({ sha }) => Boolean(sha))
|
||||
|
||||
console.log('Checking out the following commits:', commits)
|
||||
|
||||
// Fetching all commits at once is much faster than doing multiple checkouts.
|
||||
// This would fail without --refetch, because the we had a partial clone before, but changed it above.
|
||||
await run('git', 'fetch', '--depth=1', '--refetch', 'origin', ...(commits.map(({ sha }) => sha)))
|
||||
|
||||
// Checking out onto tmpfs takes 1s and is faster by at least factor 10x.
|
||||
await run('mkdir', 'nixpkgs')
|
||||
switch (process.env.RUNNER_OS) {
|
||||
case 'macOS':
|
||||
await run('sudo', 'mount_tmpfs', 'nixpkgs')
|
||||
break
|
||||
case 'Linux':
|
||||
await run('sudo', 'mount', '-t', 'tmpfs', 'tmpfs', 'nixpkgs')
|
||||
break
|
||||
}
|
||||
|
||||
// Create all worktrees in parallel.
|
||||
await Promise.all(commits.map(async ({ sha, path }) => {
|
||||
await run('git', 'worktree', 'add', join('nixpkgs', path), sha, '--no-checkout')
|
||||
await run('git', '-C', join('nixpkgs', path), 'sparse-checkout', 'disable')
|
||||
await run('git', '-C', join('nixpkgs', path), 'checkout', '--progress')
|
||||
}))
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -4,4 +4,4 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels: []
|
||||
labels: [ ]
|
||||
|
||||
20
.github/labeler-development-branches.yml
vendored
20
.github/labeler-development-branches.yml
vendored
@@ -1,23 +1,23 @@
|
||||
# This file is used by .github/workflows/bot.yml
|
||||
# This file is used by .github/workflows/labels.yml
|
||||
# This version is only run for Pull Requests from development branches like staging-next, haskell-updates or python-updates.
|
||||
|
||||
"4.workflow: package set update":
|
||||
- any:
|
||||
- head-branch:
|
||||
- '-updates$'
|
||||
- head-branch:
|
||||
- '-updates$'
|
||||
|
||||
"4.workflow: staging":
|
||||
- any:
|
||||
- head-branch:
|
||||
- '^staging-next$'
|
||||
- '^staging-next-'
|
||||
- head-branch:
|
||||
- '^staging-next$'
|
||||
- '^staging-next-'
|
||||
|
||||
"6.topic: haskell":
|
||||
- any:
|
||||
- head-branch:
|
||||
- '^haskell-updates$'
|
||||
- head-branch:
|
||||
- '^haskell-updates$'
|
||||
|
||||
"6.topic: python":
|
||||
- any:
|
||||
- head-branch:
|
||||
- '^python-updates$'
|
||||
- head-branch:
|
||||
- '^python-updates$'
|
||||
|
||||
35
.github/labeler-no-sync.yml
vendored
35
.github/labeler-no-sync.yml
vendored
@@ -1,25 +1,32 @@
|
||||
# This file is used by .github/workflows/bot.yml
|
||||
# This file is used by .github/workflows/labels.yml
|
||||
# This version uses `sync-labels: false`, meaning that a non-match will NOT remove the label
|
||||
|
||||
# keep-sorted start case=no numeric=yes newline_separated=yes skip_lines=1
|
||||
|
||||
"6.topic: policy discussion":
|
||||
- any:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/**/*
|
||||
- CONTRIBUTING.md
|
||||
- pkgs/README.md
|
||||
- nixos/README.md
|
||||
- maintainers/README.md
|
||||
- lib/README.md
|
||||
- doc/README.md
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/**/*
|
||||
- CONTRIBUTING.md
|
||||
- pkgs/README.md
|
||||
- nixos/README.md
|
||||
- maintainers/README.md
|
||||
- lib/README.md
|
||||
- doc/README.md
|
||||
|
||||
"8.has: documentation":
|
||||
- any:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- doc/**/*
|
||||
- nixos/doc/**/*
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- doc/**/*
|
||||
- nixos/doc/**/*
|
||||
|
||||
"backport release-24.11":
|
||||
- any:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/*
|
||||
- ci/**/*.*
|
||||
|
||||
# keep-sorted end
|
||||
|
||||
799
.github/labeler.yml
vendored
799
.github/labeler.yml
vendored
File diff suppressed because it is too large
Load Diff
9
.github/stale.yml
vendored
Normal file
9
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
daysUntilStale: 180
|
||||
daysUntilClose: false
|
||||
exemptLabels:
|
||||
- "1.severity: security"
|
||||
- "2.status: never-stale"
|
||||
staleLabel: "2.status: stale"
|
||||
markComment: false
|
||||
closeComment: false
|
||||
73
.github/workflows/README.md
vendored
73
.github/workflows/README.md
vendored
@@ -2,76 +2,19 @@
|
||||
|
||||
Some architectural notes about key decisions and concepts in our workflows:
|
||||
|
||||
- Instead of `pull_request` we use [`pull_request_target`](https://docs.github.com/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target) for all PR-related workflows.
|
||||
This has the advantage that those workflows will run without prior approval for external contributors.
|
||||
- Instead of `pull_request` we use [`pull_request_target`](https://docs.github.com/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target) for all PR-related workflows. This has the advantage that those workflows will run without prior approval for external contributors.
|
||||
|
||||
- Running on `pull_request_target` also optionally provides us with a GH_TOKEN with elevated privileges (write access), which we need to do things like adding labels, requesting reviewers or pushing branches.
|
||||
**Note about security:** We need to be careful to limit the scope of elevated privileges as much as possible.
|
||||
Thus they should be lowered to the minimum with `permissions: {}` in every workflow by default.
|
||||
- Running on `pull_request_target` also optionally provides us with a GH_TOKEN with elevated privileges (write access), which we need to do things like adding labels, requesting reviewers or pushing branches. **Note about security:** We need to be careful to limit the scope of elevated privileges as much as possible. Thus they should be lowered to the minimum with `permissions: {}` in every workflow by default.
|
||||
|
||||
- By definition `pull_request_target` runs in the context of the **base** of the pull request.
|
||||
This means, that the workflow files to run will be taken from the base branch, not the PR, and actions/checkout will not checkout the PR, but the base branch, by default.
|
||||
To protect our secrets, we need to make sure to **never execute code** from the pull request and always evaluate or build nix code from the pull request with the **sandbox enabled**.
|
||||
- By definition `pull_request_target` runs in the context of the **base** of the pull request. This means, that the workflow files to run will be taken from the base branch, not the PR, and actions/checkout will not checkout the PR, but the base branch, by default. To protect our secrets, we need to make sure to **never execute code** from the pull request and always evaluate or build nix code from the pull request with the **sandbox enabled**.
|
||||
|
||||
- To test the pull request's contents, we checkout the "test merge commit".
|
||||
This is a temporary commit that GitHub creates automatically as "what would happen, if this PR was merged into the base branch now?".
|
||||
The checkout could be done via the virtual branch `refs/pull/<pr-number>/merge`, but doing so would cause failures when this virtual branch doesn't exist (anymore).
|
||||
This can happen when the PR has conflicts, in which case the virtual branch is not created, or when the PR is getting merged while workflows are still running, in which case the branch won't exist anymore at the time of checkout.
|
||||
Thus, we use the `prepare` job to check whether the PR is mergeable and the test merge commit exists and only then run the relevant jobs.
|
||||
- To test the pull request's contents, we checkout the "test merge commit". This is a temporary commit that GitHub creates automatically as "what would happen, if this PR was merged into the base branch now?". The checkout could be done via the virtual branch `refs/pull/<pr-number>/merge`, but doing so would cause failures when this virtual branch doesn't exist (anymore). This can happen when the PR has conflicts, in which case the virtual branch is not created, or when the PR is getting merged while workflows are still running, in which case the branch won't exist anymore at the time of checkout. Thus, we use the `get-merge-commit.yml` workflow to check whether the PR is mergeable and the test merge commit exists and only then run the relevant jobs.
|
||||
|
||||
- Various workflows need to make comparisons against the base branch.
|
||||
In this case, we checkout the parent of the "test merge commit" for best results.
|
||||
Note, that this is not necessarily the same as the default commit that actions/checkout would use, which is also a commit from the base branch (see above), but might be older.
|
||||
- Various workflows need to make comparisons against the base branch. In this case, we checkout the parent of the "test merge commit" for best results. Note, that this is not necessarily the same as the default commit that actions/checkout would use, which is also a commit from the base branch (see above), but might be older.
|
||||
|
||||
## Terminology
|
||||
|
||||
- **base commit**: The pull_request_target event's context commit, i.e. the base commit given by GitHub Actions.
|
||||
Same as `github.event.pull_request.base.sha`.
|
||||
- **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`.
|
||||
- **base commit**: The pull_request_target event's context commit, i.e. the base commit given by GitHub Actions. Same as `github.event.pull_request.base.sha`.
|
||||
- **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`.
|
||||
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>
|
||||
```
|
||||
|
||||
## Required Status Checks
|
||||
|
||||
The "Required Status Checks" branch ruleset is implemented in two top-level workflows: `pull-request-target.yml` and `merge-group.yml`.
|
||||
|
||||
The PR workflow defines all checks that need to succeed to add a Pull Request to the Merge Queue.
|
||||
If no Merge Queue is set up for a branch, the PR workflow defines the checks required to merge into the target branch.
|
||||
|
||||
The Merge Group workflow defines all checks that are run as part of the Merge Queue.
|
||||
Only when these pass, a Pull Request is finally merged into the target branch.
|
||||
They don't apply when no Merge Queue is set up.
|
||||
|
||||
Both workflows work with the same `no PR failures` status check.
|
||||
This name can never be changed, because it's used in the branch ruleset for these rules.
|
||||
|
||||
57
.github/workflows/backport.yml
vendored
57
.github/workflows/backport.yml
vendored
@@ -9,47 +9,32 @@ on:
|
||||
pull_request_target:
|
||||
types: [closed, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport Pull Request
|
||||
if: vars.NIXPKGS_CI_APP_ID && github.event.pull_request.merged == true && (github.event.action != 'labeled' || startsWith(github.event.label.name, 'backport'))
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
# Use a GitHub App to create the PR so that CI gets triggered
|
||||
# The App is scoped to Repository > Contents and Pull Requests: write for Nixpkgs
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
- uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
permission-workflows: write
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
|
||||
- name: Create backport PRs
|
||||
id: backport
|
||||
uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
|
||||
uses: korthout/backport-action@436145e922f9561fc5ea157ff406f21af2d6b363 # v3.2.0
|
||||
with:
|
||||
# Config README: https://github.com/korthout/backport-action#backport-action
|
||||
copy_labels_pattern: 'severity:\ssecurity'
|
||||
@@ -57,27 +42,17 @@ jobs:
|
||||
pull_description: |-
|
||||
Bot-based backport to `${target_branch}`, triggered by a label in #${pull_number}.
|
||||
|
||||
**Before merging, ensure that this backport is [acceptable for the release](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#changes-acceptable-for-releases).**
|
||||
|
||||
Even as a non-committer, if you find that it is not acceptable, leave a comment.
|
||||
|
||||
> [!TIP]
|
||||
> If you maintain all packages touched by this pull request, and they are all located under `pkgs/by-name/*`, you can comment **`@NixOS/nixpkgs-merge-bot merge`** to automatically merge this PR using the [`nixpkgs-merge-bot`](https://github.com/NixOS/nixpkgs/blob/master/ci/README.md#nixpkgs-merge-bot).
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
* [ ] Before merging, ensure that this backport is [acceptable for the release](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#changes-acceptable-for-releases).
|
||||
* Even as a non-committer, if you find that it is not acceptable, leave a comment.
|
||||
|
||||
- name: "Add 'has: port to stable' label"
|
||||
if: steps.backport.outputs.created_pull_numbers != ''
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
# Not using the app on purpose to avoid triggering another workflow run after adding this label.
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: [ '8.has: port to stable' ]
|
||||
})
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
gh api \
|
||||
--method POST \
|
||||
/repos/"$REPOSITORY"/issues/"$NUMBER"/labels \
|
||||
-f "labels[]=8.has: port to stable"
|
||||
|
||||
130
.github/workflows/bot.yml
vendored
130
.github/workflows/bot.yml
vendored
@@ -1,130 +0,0 @@
|
||||
# WARNING:
|
||||
# When extending this action, be aware that $GITHUB_TOKEN allows some write
|
||||
# access to the GitHub API. This means that it should not evaluate user input in
|
||||
# a way that allows code injection.
|
||||
|
||||
name: Bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 10m
|
||||
# i.e., at each of the listed minutes, every hour
|
||||
- cron: '05,15,25,35,45,55 * * * *'
|
||||
workflow_call:
|
||||
inputs:
|
||||
headBranch:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# This explicitly avoids using `run_id` for the concurrency key to make sure that only
|
||||
# *one* scheduled run can run at a time.
|
||||
group: bot-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number }}
|
||||
# PR-triggered runs will be cancelled, but scheduled runs will be queued.
|
||||
cancel-in-progress: ${{ github.event_name != 'schedule' }}
|
||||
|
||||
# This is used as fallback without app only.
|
||||
# This happens when testing in forks without setting up that app.
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-slim
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'NixOS'
|
||||
env:
|
||||
# TODO: Remove after 2026-03-04, when Node 24 becomes the default.
|
||||
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install @actions/artifact bottleneck
|
||||
|
||||
# Use a GitHub App, because it has much higher rate limits: 12,500 instead of 5,000 req / hour.
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
if: github.event_name != 'pull_request' && vars.NIXPKGS_CI_APP_ID
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
permission-administration: read
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-members: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
|
||||
- name: Run bot
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || github.token }}
|
||||
retries: 3
|
||||
script: |
|
||||
require('./ci/github-script/bot.js')({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
dry: context.eventName == 'pull_request'
|
||||
})
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
name: Labels from touched files
|
||||
if: |
|
||||
github.event_name == 'pull_request_target' &&
|
||||
!contains(fromJSON(inputs.headBranch).type, 'development')
|
||||
with:
|
||||
repo-token: ${{ steps.app-token.outputs.token }}
|
||||
configuration-path: .github/labeler.yml # default
|
||||
sync-labels: true
|
||||
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
name: Labels from touched files (no sync)
|
||||
if: |
|
||||
github.event_name == 'pull_request_target' &&
|
||||
!contains(fromJSON(inputs.headBranch).type, 'development')
|
||||
with:
|
||||
repo-token: ${{ steps.app-token.outputs.token }}
|
||||
configuration-path: .github/labeler-no-sync.yml
|
||||
sync-labels: false
|
||||
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
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 == 'pull_request_target' &&
|
||||
contains(fromJSON(inputs.headBranch).type, 'development')
|
||||
with:
|
||||
repo-token: ${{ steps.app-token.outputs.token }}
|
||||
configuration-path: .github/labeler-development-branches.yml
|
||||
sync-labels: true
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
114
.github/workflows/build.yml
vendored
114
.github/workflows/build.yml
vendored
@@ -1,114 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
artifact-prefix:
|
||||
required: true
|
||||
type: string
|
||||
baseBranch:
|
||||
required: true
|
||||
type: string
|
||||
mergedSha:
|
||||
required: true
|
||||
type: string
|
||||
targetSha:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
# Should only be provided in the merge queue, not in pull requests,
|
||||
# where we're evaluating untrusted code.
|
||||
CACHIX_AUTH_TOKEN_GHA:
|
||||
required: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
name: x86_64-linux
|
||||
systems: x86_64-linux
|
||||
builds: [shell, manual-nixos, lib-tests, tarball]
|
||||
desc: shell, docs, lib, tarball
|
||||
- runner: ubuntu-24.04-arm
|
||||
name: aarch64-linux
|
||||
systems: aarch64-linux
|
||||
builds: [shell, manual-nixos, manual-nixpkgs, manual-nixpkgs-tests]
|
||||
desc: shell, docs
|
||||
- runner: macos-14
|
||||
name: darwin
|
||||
systems: aarch64-darwin x86_64-darwin
|
||||
builds: [shell]
|
||||
desc: shell
|
||||
name: '${{ matrix.name }}: ${{ matrix.desc }}'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Checkout the merge commit
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
target-as-trusted-at: ${{ inputs.targetSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
with:
|
||||
# Sandbox is disabled on MacOS by default.
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
name: ${{ vars.CACHIX_NAME || 'nixpkgs-gha' }}
|
||||
extraPullNames: nixpkgs-gha
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
pushFilter: '(-source$|-nixpkgs-tarball-)'
|
||||
|
||||
- run: nix-env --install -f nixpkgs/trusted-pinned -A nix-build-uncached
|
||||
|
||||
- name: Build shell
|
||||
if: contains(matrix.builds, 'shell')
|
||||
run: echo "${{ matrix.systems }}" | xargs -n1 nix-build-uncached nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A shell --argstr system
|
||||
|
||||
- name: Build NixOS manual
|
||||
if: |
|
||||
contains(matrix.builds, 'manual-nixos') && !cancelled() &&
|
||||
contains(fromJSON(inputs.baseBranch).type, 'primary')
|
||||
run: nix-build-uncached nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A manual-nixos --out-link nixos-manual
|
||||
|
||||
- name: Build Nixpkgs manual
|
||||
if: contains(matrix.builds, 'manual-nixpkgs') && !cancelled()
|
||||
run: nix-build-uncached nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A manual-nixpkgs -A manual-nixpkgs-tests
|
||||
|
||||
- name: Build Nixpkgs manual tests
|
||||
if: contains(matrix.builds, 'manual-nixpkgs-tests') && !cancelled()
|
||||
run: nix-build-uncached nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A manual-nixpkgs-tests
|
||||
|
||||
- name: Build lib tests
|
||||
if: contains(matrix.builds, 'lib-tests') && !cancelled()
|
||||
run: nix-build-uncached nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A lib-tests
|
||||
|
||||
- name: Build tarball
|
||||
if: contains(matrix.builds, 'tarball') && !cancelled()
|
||||
run: nix-build-uncached nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A tarball
|
||||
|
||||
- name: Upload NixOS manual
|
||||
if: |
|
||||
contains(matrix.builds, 'manual-nixos') && !cancelled() &&
|
||||
contains(fromJSON(inputs.baseBranch).type, 'primary')
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: ${{ inputs.artifact-prefix }}nixos-manual-${{ matrix.name }}
|
||||
path: nixos-manual
|
||||
30
.github/workflows/check-cherry-picks.yml
vendored
Normal file
30
.github/workflows/check-cherry-picks.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Check cherry-picks"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/check-cherry-picks.yml
|
||||
pull_request_target:
|
||||
branches:
|
||||
- 'release-**'
|
||||
- 'staging-**'
|
||||
- '!staging-next'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: cherry-pick-check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: blob:none
|
||||
|
||||
- name: Check cherry-picks
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
./maintainers/scripts/check-cherry-picks.sh "$BASE_SHA" "$HEAD_SHA"
|
||||
44
.github/workflows/check-format.yml
vendored
Normal file
44
.github/workflows/check-format.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Check that files are formatted
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/check-format.yml
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
nixos:
|
||||
name: fmt-check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: get-merge-commit
|
||||
if: needs.get-merge-commit.outputs.mergedSha
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- name: Check that files are formatted
|
||||
run: |
|
||||
# Note that it's fine to run this on untrusted code because:
|
||||
# - There's no secrets accessible here
|
||||
# - The build is sandboxed
|
||||
if ! nix-build ci -A fmt.check; then
|
||||
echo "Some files are not properly formatted"
|
||||
echo "Please format them by going to the Nixpkgs root directory and running one of:"
|
||||
echo " nix-shell --run treefmt"
|
||||
echo " nix develop --command treefmt"
|
||||
echo " nix fmt"
|
||||
echo "Make sure your branch is up to date with master; rebase if not."
|
||||
echo "If you're having trouble, please ping @NixOS/nix-formatting"
|
||||
exit 1
|
||||
fi
|
||||
40
.github/workflows/check-shell.yml
vendored
Normal file
40
.github/workflows/check-shell.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: "Check shell"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/check-shell.yml
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'shell.nix'
|
||||
- 'ci/**'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
shell-check:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
system: x86_64-linux
|
||||
- runner: ubuntu-24.04-arm
|
||||
system: aarch64-linux
|
||||
- runner: macos-13
|
||||
system: x86_64-darwin
|
||||
- runner: macos-14
|
||||
system: aarch64-darwin
|
||||
|
||||
name: shell-check-${{ matrix.system }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
|
||||
- name: Build shell
|
||||
run: nix-build ci -A shell
|
||||
111
.github/workflows/check.yml
vendored
111
.github/workflows/check.yml
vendored
@@ -1,111 +0,0 @@
|
||||
name: Check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
baseBranch:
|
||||
required: false
|
||||
type: string
|
||||
headBranch:
|
||||
required: false
|
||||
type: string
|
||||
mergedSha:
|
||||
required: true
|
||||
type: string
|
||||
targetSha:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
# Should only be provided in the merge queue, not in pull requests,
|
||||
# where we're evaluating untrusted code.
|
||||
CACHIX_AUTH_TOKEN_GHA:
|
||||
required: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
commits:
|
||||
if: inputs.baseBranch && inputs.headBranch
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
path: trusted
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install bottleneck
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
|
||||
- name: Check commits
|
||||
id: check
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
TARGETS_STABLE: ${{ fromJSON(inputs.baseBranch).stable && !contains(fromJSON(inputs.headBranch).type, 'development') }}
|
||||
with:
|
||||
script: |
|
||||
const targetsStable = JSON.parse(process.env.TARGETS_STABLE)
|
||||
require('./trusted/ci/github-script/commits.js')({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
dry: context.eventName == 'pull_request',
|
||||
cherryPicks: context.eventName == 'pull_request' || targetsStable,
|
||||
})
|
||||
|
||||
- name: Log current API rate limits
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh api /rate_limit | jq
|
||||
|
||||
owners:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Checkout merge and target commits
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
target-as-trusted-at: ${{ inputs.targetSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
name: ${{ vars.CACHIX_NAME || 'nixpkgs-gha' }}
|
||||
extraPullNames: nixpkgs-gha
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
pushFilter: -source$
|
||||
|
||||
- name: Build codeowners validator
|
||||
run: nix-build nixpkgs/trusted/ci --arg nixpkgs ./nixpkgs/trusted-pinned -A codeownersValidator
|
||||
|
||||
- name: Validate codeowners
|
||||
env:
|
||||
OWNERS_FILE: nixpkgs/untrusted/ci/OWNERS
|
||||
REPOSITORY_PATH: nixpkgs/untrusted
|
||||
# Omits "owners", which checks whether GitHub handles exist, but fails with nested team
|
||||
# structures.
|
||||
CHECKS: "duppatterns,files,syntax"
|
||||
# Set this to "notowned,avoid-shadowing" to check that all files are owned by somebody
|
||||
EXPERIMENTAL_CHECKS: "avoid-shadowing"
|
||||
run: result/bin/codeowners-validator
|
||||
123
.github/workflows/codeowners-v2.yml
vendored
Normal file
123
.github/workflows/codeowners-v2.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
# This workflow depends on two GitHub Apps with the following permissions:
|
||||
# - For checking code owners:
|
||||
# - Permissions:
|
||||
# - Repository > Administration: read-only
|
||||
# - Organization > Members: read-only
|
||||
# - Install App on this repository, setting these variables:
|
||||
# - OWNER_RO_APP_ID (variable)
|
||||
# - OWNER_RO_APP_PRIVATE_KEY (secret)
|
||||
# - For requesting code owners:
|
||||
# - Permissions:
|
||||
# - Repository > Administration: read-only
|
||||
# - Organization > Members: read-only
|
||||
# - Repository > Pull Requests: read-write
|
||||
# - Install App on this repository, setting these variables:
|
||||
# - OWNER_APP_ID (variable)
|
||||
# - OWNER_APP_PRIVATE_KEY (secret)
|
||||
#
|
||||
# This split is done because checking code owners requires handling untrusted PR input,
|
||||
# while requesting code owners requires PR write access, and those shouldn't be mixed.
|
||||
#
|
||||
# Note that the latter is also used for ./eval.yml requesting reviewers.
|
||||
|
||||
name: Codeowners v2
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/codeowners-v2.yml
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review, synchronize, reopened, edited]
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
OWNERS_FILE: ci/OWNERS
|
||||
# Don't do anything on draft PRs
|
||||
DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }}
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
if: github.repository_owner == 'NixOS'
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
# Check that code owners is valid
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: get-merge-commit
|
||||
if: github.repository_owner == 'NixOS' && needs.get-merge-commit.outputs.mergedSha
|
||||
steps:
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
# This cache is for the nixpkgs repo checks and should not be trusted or used elsewhere.
|
||||
name: nixpkgs-ci
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
# Important: Because we use pull_request_target, this checks out the base branch of the PR, not the PR itself.
|
||||
# We later build and run code from the base branch with access to secrets,
|
||||
# so it's important this is not the PRs code.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
path: base
|
||||
|
||||
- name: Build codeowners validator
|
||||
run: nix-build base/ci -A codeownersValidator
|
||||
|
||||
- uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
if: vars.OWNER_RO_APP_ID
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.OWNER_RO_APP_ID }}
|
||||
private-key: ${{ secrets.OWNER_RO_APP_PRIVATE_KEY }}
|
||||
permission-administration: read
|
||||
permission-members: read
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
path: pr
|
||||
|
||||
- name: Validate codeowners
|
||||
if: steps.app-token.outputs.token
|
||||
run: result/bin/codeowners-validator
|
||||
env:
|
||||
OWNERS_FILE: pr/${{ env.OWNERS_FILE }}
|
||||
GITHUB_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
REPOSITORY_PATH: pr
|
||||
OWNER_CHECKER_REPOSITORY: ${{ github.repository }}
|
||||
# Set this to "notowned,avoid-shadowing" to check that all files are owned by somebody
|
||||
EXPERIMENTAL_CHECKS: "avoid-shadowing"
|
||||
|
||||
# Request reviews from code owners
|
||||
request:
|
||||
name: Request
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: github.repository_owner == 'NixOS'
|
||||
steps:
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
|
||||
# Important: Because we use pull_request_target, this checks out the base branch of the PR, not the PR head.
|
||||
# This is intentional, because we need to request the review of owners as declared in the base branch.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
if: vars.OWNER_APP_ID
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.OWNER_APP_ID }}
|
||||
private-key: ${{ secrets.OWNER_APP_PRIVATE_KEY }}
|
||||
permission-administration: read
|
||||
permission-members: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Build review request package
|
||||
run: nix-build ci -A requestReviews
|
||||
|
||||
- name: Request reviews
|
||||
if: steps.app-token.outputs.token
|
||||
run: result/bin/request-code-owner-reviews.sh ${{ github.repository }} ${{ github.event.number }} "$OWNERS_FILE"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
54
.github/workflows/comment.yml
vendored
54
.github/workflows/comment.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# This is used as fallback without app only.
|
||||
# This happens when testing in forks without setting up that app.
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
# The `bot` workflow reacts to comments with @NixOS/nixpkgs-merge-bot references, but might only
|
||||
# pick up a comment after up to 10 minutes. To give the user instant feedback, this job adds
|
||||
# a reaction to these comments.
|
||||
react:
|
||||
name: React with eyes
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 2
|
||||
if: contains(github.event.comment.body, '@NixOS/nixpkgs-merge-bot merge')
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
|
||||
# Use the GitHub App to make sure the reaction happens with the same user who will later merge.
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
if: github.event_name != 'pull_request' && vars.NIXPKGS_CI_APP_ID
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || github.token }}
|
||||
retries: 3
|
||||
script: |
|
||||
const { handleMergeComment } = require('./ci/github-script/merge.js')
|
||||
const { body, node_id } = context.payload.comment
|
||||
|
||||
await handleMergeComment({
|
||||
github,
|
||||
body,
|
||||
node_id,
|
||||
reaction: 'EYES',
|
||||
})
|
||||
59
.github/workflows/edited.yml
vendored
59
.github/workflows/edited.yml
vendored
@@ -1,59 +0,0 @@
|
||||
# Some workflows depend on the base branch of the PR, but changing the base branch is not included in the default trigger events, which would be `opened`, `synchronize` or `reopened`.
|
||||
# Instead it causes an `edited` event.
|
||||
# Since `edited` is also triggered when PR title/body is changed, we use this wrapper workflow, to run the other workflows conditionally only.
|
||||
# There are already feature requests for adding a `base_changed` event:
|
||||
# - https://github.com/orgs/community/discussions/35058
|
||||
# - https://github.com/orgs/community/discussions/64119
|
||||
#
|
||||
# Instead of adding this to each workflow's pull_request_target event, we trigger this in a separate workflow.
|
||||
# This has the advantage, that we can actually skip running those jobs for simple edits like changing the title or description.
|
||||
# The actual trigger happens by closing and re-opening the pull request, which triggers the default pull_request_target events.
|
||||
# This is much simpler and reliable than other approaches.
|
||||
|
||||
name: "Edited base branch"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [edited]
|
||||
|
||||
concurrency:
|
||||
group: edited-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
base:
|
||||
name: Trigger jobs
|
||||
runs-on: ubuntu-slim
|
||||
if: github.event.changes.base.ref.from && github.event.changes.base.ref.from != github.event.pull_request.base.ref
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
# Use a GitHub App to create the PR so that CI gets triggered
|
||||
# The App is scoped to Repository > Contents and Pull Requests: write for Nixpkgs
|
||||
# We only need Pull Requests: write here, but the app is also used for backports.
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
function changeState(state) {
|
||||
return github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
state
|
||||
})
|
||||
}
|
||||
await changeState('closed')
|
||||
await changeState('open')
|
||||
36
.github/workflows/eval-aliases.yml
vendored
Normal file
36
.github/workflows/eval-aliases.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Eval aliases
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/eval-aliases.yml
|
||||
pull_request_target:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
eval-aliases:
|
||||
name: Eval nixpkgs with aliases enabled
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: [ get-merge-commit ]
|
||||
steps:
|
||||
- name: Check out the PR at the test merge commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
path: nixpkgs
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- name: Ensure flake outputs on all systems still evaluate
|
||||
run: nix flake check --all-systems --no-build ./nixpkgs
|
||||
|
||||
- name: Query nixpkgs with aliases enabled to check for basic syntax errors
|
||||
run: |
|
||||
time nix-env -I ./nixpkgs -f ./nixpkgs -qa '*' --option restrict-eval true --option allow-import-from-derivation false >/dev/null
|
||||
516
.github/workflows/eval.yml
vendored
516
.github/workflows/eval.yml
vendored
@@ -1,87 +1,36 @@
|
||||
name: Eval
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
artifact-prefix:
|
||||
required: true
|
||||
type: string
|
||||
mergedSha:
|
||||
required: true
|
||||
type: string
|
||||
targetSha:
|
||||
type: string
|
||||
systems:
|
||||
required: true
|
||||
type: string
|
||||
testVersions:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
# Should only be provided in the merge queue, not in pull requests,
|
||||
# where we're evaluating untrusted code.
|
||||
CACHIX_AUTH_TOKEN_GHA:
|
||||
required: false
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/eval.yml
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review, synchronize, reopened]
|
||||
push:
|
||||
# Keep this synced with ci/request-reviews/dev-branches.txt
|
||||
branches:
|
||||
- master
|
||||
- staging
|
||||
- release-*
|
||||
- staging-*
|
||||
- haskell-updates
|
||||
- python-updates
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
versions:
|
||||
if: inputs.testVersions
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
versions: ${{ steps.versions.outputs.versions }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
path: trusted
|
||||
sparse-checkout: |
|
||||
ci/supportedVersions.nix
|
||||
get-merge-commit:
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
- name: Check out the PR at the test merge commit
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.mergedSha }}
|
||||
path: untrusted
|
||||
sparse-checkout: |
|
||||
ci/pinned.json
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- name: Load supported versions
|
||||
id: versions
|
||||
run: |
|
||||
echo "versions=$(trusted/ci/supportedVersions.nix --arg pinnedJson untrusted/ci/pinned.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
eval:
|
||||
outpaths:
|
||||
name: Outpaths
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: versions
|
||||
if: ${{ !cancelled() && !failure() }}
|
||||
needs: [ get-merge-commit ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system: ${{ fromJSON(inputs.systems) }}
|
||||
version:
|
||||
- "" # Default Eval triggering rebuild labels and such.
|
||||
- ${{ fromJSON(needs.versions.outputs.versions || '[]') }} # Only for ci/pinned.json updates.
|
||||
# Failures for versioned Evals will be collected in a separate job below
|
||||
# to not interrupt main Eval's compare step.
|
||||
continue-on-error: ${{ matrix.version != '' }}
|
||||
name: ${{ matrix.system }}${{ matrix.version && format(' @ {0}', matrix.version) || '' }}
|
||||
timeout-minutes: 15
|
||||
system: ${{ fromJSON(needs.get-merge-commit.outputs.systems) }}
|
||||
steps:
|
||||
# This is not supposed to be used and just acts as a fallback.
|
||||
# Without swap, when Eval runs OOM, it will fail badly with a
|
||||
# job that is sometimes not interruptible anymore.
|
||||
# If Eval starts swapping, decrease chunkSize to keep it fast.
|
||||
- name: Enable swap
|
||||
run: |
|
||||
sudo fallocate -l 10G /swap
|
||||
@@ -89,275 +38,244 @@ jobs:
|
||||
sudo mkswap /swap
|
||||
sudo swapon /swap
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Check out the PR at the test merge commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Check out the PR at merged and target commits
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
target-as-trusted-at: ${{ inputs.targetSha }}
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
path: nixpkgs
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
continue-on-error: true
|
||||
uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
name: ${{ vars.CACHIX_NAME || 'nixpkgs-gha' }}
|
||||
extraPullNames: nixpkgs-gha
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
pushFilter: '(-source|-single-chunk)$'
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- name: Evaluate the ${{ matrix.system }} output paths at the merge commit
|
||||
env:
|
||||
MATRIX_SYSTEM: ${{ matrix.system }}
|
||||
MATRIX_VERSION: ${{ matrix.version || 'nixVersions.latest' }}
|
||||
run: |
|
||||
nix-build nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A eval.singleSystem \
|
||||
--argstr evalSystem "$MATRIX_SYSTEM" \
|
||||
--arg chunkSize 8000 \
|
||||
--argstr nixPath "$MATRIX_VERSION" \
|
||||
--out-link merged
|
||||
# If it uses too much memory, slightly decrease chunkSize.
|
||||
# Note: Keep the same further down in sync!
|
||||
|
||||
- name: Evaluate the ${{ matrix.system }} output paths at the target commit
|
||||
- name: Evaluate the ${{ matrix.system }} output paths for all derivation attributes
|
||||
env:
|
||||
MATRIX_SYSTEM: ${{ matrix.system }}
|
||||
run: |
|
||||
TARGET_DRV=$(nix-instantiate nixpkgs/trusted/ci --arg nixpkgs ./nixpkgs/trusted-pinned -A eval.singleSystem \
|
||||
nix-build nixpkgs/ci -A eval.singleSystem \
|
||||
--argstr evalSystem "$MATRIX_SYSTEM" \
|
||||
--arg chunkSize 8000 \
|
||||
--argstr nixPath "nixVersions.latest")
|
||||
--arg chunkSize 10000
|
||||
# If it uses too much memory, slightly decrease chunkSize
|
||||
|
||||
# Try to fetch this from Cachix a few times, for up to 30 seconds. This avoids running Eval
|
||||
# twice in the Merge Queue, when a later item finishes Eval at the merge commit earlier.
|
||||
for _i in {1..6}; do
|
||||
# Using --max-jobs 0 will cause nix-build to fail if this can't be substituted from cachix.
|
||||
if nix-build "$TARGET_DRV" --max-jobs 0; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Either fetches from Cachix or runs Eval itself. The fallback is required
|
||||
# for pull requests into wip-branches without merge queue.
|
||||
nix-build "$TARGET_DRV" --out-link target
|
||||
|
||||
- name: Compare outpaths against the target branch
|
||||
env:
|
||||
MATRIX_SYSTEM: ${{ matrix.system }}
|
||||
run: |
|
||||
nix-build nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A eval.diff \
|
||||
--arg beforeDir ./target \
|
||||
--arg afterDir ./merged \
|
||||
--argstr evalSystem "$MATRIX_SYSTEM" \
|
||||
--out-link diff
|
||||
|
||||
- name: Upload outpaths diff and stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
- name: Upload the output paths and eval stats
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ inputs.artifact-prefix }}${{ matrix.version && format('{0}-', matrix.version) || '' }}diff-${{ matrix.system }}
|
||||
path: diff/*
|
||||
name: intermediate-${{ matrix.system }}
|
||||
path: result/*
|
||||
|
||||
compare:
|
||||
process:
|
||||
name: Process
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: [eval]
|
||||
if: ${{ !cancelled() && !failure() }}
|
||||
permissions:
|
||||
statuses: write
|
||||
timeout-minutes: 5
|
||||
needs: [ outpaths, get-merge-commit ]
|
||||
outputs:
|
||||
targetRunId: ${{ steps.targetRunId.outputs.targetRunId }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Check out the PR at the target commit
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
target-as-trusted-at: ${{ inputs.targetSha }}
|
||||
|
||||
- name: Download output paths and eval stats for all systems
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
pattern: ${{ inputs.artifact-prefix }}diff-*
|
||||
path: diff
|
||||
merge-multiple: true
|
||||
pattern: intermediate-*
|
||||
path: intermediate
|
||||
|
||||
- name: Check out the PR at the test merge commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
fetch-depth: 2
|
||||
path: nixpkgs
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- name: Combine all output paths and eval stats
|
||||
run: |
|
||||
nix-build nixpkgs/trusted/ci --arg nixpkgs ./nixpkgs/trusted-pinned -A eval.combine \
|
||||
--arg diffDir ./diff \
|
||||
--out-link combined
|
||||
nix-build nixpkgs/ci -A eval.combine \
|
||||
--arg resultsDir ./intermediate \
|
||||
-o prResult
|
||||
|
||||
- name: Upload the maintainer list
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
- name: Upload the combined results
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ inputs.artifact-prefix }}maintainers
|
||||
path: combined/maintainers.json
|
||||
name: result
|
||||
path: prResult/*
|
||||
|
||||
- name: Get target run id
|
||||
if: needs.get-merge-commit.outputs.targetSha
|
||||
id: targetRunId
|
||||
run: |
|
||||
# Get the latest eval.yml workflow run for the PR's target commit
|
||||
if ! run=$(gh api --method GET /repos/"$REPOSITORY"/actions/workflows/eval.yml/runs \
|
||||
-f head_sha="$TARGET_SHA" -f event=push \
|
||||
--jq '.workflow_runs | sort_by(.run_started_at) | .[-1]') \
|
||||
|| [[ -z "$run" ]]; then
|
||||
echo "Could not find an eval.yml workflow run for $TARGET_SHA, cannot make comparison"
|
||||
exit 1
|
||||
fi
|
||||
echo "Comparing against $(jq .html_url <<< "$run")"
|
||||
runId=$(jq .id <<< "$run")
|
||||
conclusion=$(jq -r .conclusion <<< "$run")
|
||||
|
||||
while [[ "$conclusion" == null || "$conclusion" == "" ]]; do
|
||||
echo "Workflow not done, waiting 10 seconds before checking again"
|
||||
sleep 10
|
||||
conclusion=$(gh api /repos/"$REPOSITORY"/actions/runs/"$runId" --jq '.conclusion')
|
||||
done
|
||||
|
||||
if [[ "$conclusion" != "success" ]]; then
|
||||
echo "Workflow was not successful (conclusion: $conclusion), cannot make comparison"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "targetRunId=$runId" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
TARGET_SHA: ${{ needs.get-merge-commit.outputs.targetSha }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
if: steps.targetRunId.outputs.targetRunId
|
||||
with:
|
||||
name: result
|
||||
path: targetResult
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ steps.targetRunId.outputs.targetRunId }}
|
||||
|
||||
- name: Compare against the target branch
|
||||
env:
|
||||
TARGET_SHA: ${{ inputs.mergedSha }}
|
||||
if: steps.targetRunId.outputs.targetRunId
|
||||
run: |
|
||||
git -C nixpkgs/trusted diff --name-only "$TARGET_SHA" \
|
||||
git -C nixpkgs worktree add ../target ${{ needs.get-merge-commit.outputs.targetSha }}
|
||||
git -C nixpkgs diff --name-only ${{ needs.get-merge-commit.outputs.targetSha }} \
|
||||
| jq --raw-input --slurp 'split("\n")[:-1]' > touched-files.json
|
||||
|
||||
# Use the target branch to get accurate maintainer info
|
||||
nix-build nixpkgs/trusted/ci --arg nixpkgs ./nixpkgs/trusted-pinned -A eval.compare \
|
||||
--arg combinedDir ./combined \
|
||||
nix-build target/ci -A eval.compare \
|
||||
--arg beforeResultDir ./targetResult \
|
||||
--arg afterResultDir "$(realpath prResult)" \
|
||||
--arg touchedFilesJson ./touched-files.json \
|
||||
--out-link comparison
|
||||
-o comparison
|
||||
|
||||
cat comparison/step-summary.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload the comparison results
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
- name: Upload the combined results
|
||||
if: steps.targetRunId.outputs.targetRunId
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ inputs.artifact-prefix }}comparison
|
||||
name: comparison
|
||||
path: comparison/*
|
||||
|
||||
- name: Add eval summary to commit statuses
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const { readFile } = require('node:fs/promises')
|
||||
const changed = JSON.parse(await readFile('comparison/changed-paths.json', 'utf-8'))
|
||||
const description =
|
||||
'Package: ' + [
|
||||
`added ${changed.attrdiff.added.length}`,
|
||||
`removed ${changed.attrdiff.removed.length}`,
|
||||
`changed ${changed.attrdiff.changed.length}`
|
||||
].join(', ') +
|
||||
' — Rebuild: ' + [
|
||||
`linux ${changed.rebuildCountByKernel.linux}`,
|
||||
`darwin ${changed.rebuildCountByKernel.darwin}`
|
||||
].join(', ')
|
||||
|
||||
const { serverUrl, repo, runId, payload } = context
|
||||
const target_url =
|
||||
`${serverUrl}/${repo.owner}/${repo.repo}/actions/runs/${runId}?pr=${payload.pull_request.number}`
|
||||
|
||||
await github.rest.repos.createCommitStatus({
|
||||
...repo,
|
||||
sha: payload.pull_request.head.sha,
|
||||
context: 'Eval Summary',
|
||||
state: 'success',
|
||||
description,
|
||||
target_url
|
||||
})
|
||||
|
||||
# Creates a matrix of Eval performance for various versions and systems.
|
||||
report:
|
||||
runs-on: ubuntu-slim
|
||||
needs: [versions, eval]
|
||||
steps:
|
||||
- name: Download output paths and eval stats for all versions
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: "*-diff-*"
|
||||
path: versions
|
||||
|
||||
- name: Add version comparison table to job summary
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
ARTIFACT_PREFIX: ${{ inputs.artifact-prefix }}
|
||||
SYSTEMS: ${{ inputs.systems }}
|
||||
VERSIONS: ${{ needs.versions.outputs.versions }}
|
||||
with:
|
||||
script: |
|
||||
const { readFileSync } = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const prefix = process.env.ARTIFACT_PREFIX
|
||||
const systems = JSON.parse(process.env.SYSTEMS)
|
||||
const versions = JSON.parse(process.env.VERSIONS)
|
||||
|
||||
core.summary.addHeading('Lix/Nix version comparison')
|
||||
core.summary.addTable(
|
||||
[].concat(
|
||||
[
|
||||
[{ data: 'Version', header: true }].concat(
|
||||
systems.map((system) => ({ data: system, header: true })),
|
||||
),
|
||||
],
|
||||
versions.map((version) =>
|
||||
[{ data: version }].concat(
|
||||
systems.map((system) => {
|
||||
try {
|
||||
const artifact = path.join('versions', `${prefix}${version}-diff-${system}`)
|
||||
const time = Math.round(
|
||||
parseFloat(
|
||||
readFileSync(
|
||||
path.join(artifact, 'after', system, 'total-time'),
|
||||
'utf-8',
|
||||
),
|
||||
),
|
||||
)
|
||||
const diff = JSON.parse(
|
||||
readFileSync(path.join(artifact, system, 'diff.json'), 'utf-8'),
|
||||
)
|
||||
const attrs = []
|
||||
.concat(diff.added, diff.removed, diff.changed, diff.rebuilds)
|
||||
// There are some special attributes, which are ignored for rebuilds.
|
||||
// These only have a single path component, because they lack the `.<system>` suffix.
|
||||
.filter((attr) => attr.split('.').length > 1)
|
||||
if (attrs.length > 0) {
|
||||
core.setFailed(
|
||||
`${version} on ${system} has changed outpaths!\nNote: Please make sure to update ci/pinned.json separately from changes to other packages.`,
|
||||
)
|
||||
return { data: ':x:' }
|
||||
}
|
||||
return { data: time }
|
||||
} catch {
|
||||
core.warning(`${version} on ${system} did not produce artifact.`)
|
||||
return { data: ':warning:' }
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
core.summary.addRaw(
|
||||
'\n*Evaluation time in seconds without downloading dependencies.*',
|
||||
true,
|
||||
)
|
||||
core.summary.addRaw('\n*:warning: Job did not report a result.*', true)
|
||||
core.summary.addRaw(
|
||||
'\n*:x: Job produced different outpaths than the target branch.*',
|
||||
true,
|
||||
)
|
||||
core.summary.write()
|
||||
|
||||
misc:
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
# Separate job to have a very tightly scoped PR write token
|
||||
tag:
|
||||
name: Tag
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
needs: [ get-merge-commit, process ]
|
||||
if: needs.process.outputs.targetRunId
|
||||
permissions:
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
# See ./codeowners-v2.yml, reuse the same App because we need the same permissions
|
||||
# Can't use the token received from permissions above, because it can't get enough permissions
|
||||
- uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
if: vars.OWNER_APP_ID
|
||||
id: app-token
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Checkout the merge commit
|
||||
uses: ./.github/actions/checkout
|
||||
app-id: ${{ vars.OWNER_APP_ID }}
|
||||
private-key: ${{ secrets.OWNER_APP_PRIVATE_KEY }}
|
||||
permission-administration: read
|
||||
permission-members: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Download process result
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
name: comparison
|
||||
path: comparison
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
|
||||
- name: Run misc eval tasks in parallel
|
||||
# Important: This workflow job runs with extra permissions,
|
||||
# so we need to make sure to not run untrusted code from PRs
|
||||
- name: Check out Nixpkgs at the base commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.targetSha }}
|
||||
path: base
|
||||
sparse-checkout: ci
|
||||
|
||||
- name: Build the requestReviews derivation
|
||||
run: nix-build base/ci -A requestReviews
|
||||
|
||||
- name: Labelling pull request
|
||||
if: ${{ github.event_name == 'pull_request_target' && github.repository_owner == 'NixOS' }}
|
||||
run: |
|
||||
# Ensure flake outputs on all systems still evaluate
|
||||
nix flake check --all-systems --no-build './nixpkgs/untrusted?shallow=1' &
|
||||
# Query nixpkgs with aliases enabled to check for basic syntax errors
|
||||
nix-env -I ./nixpkgs/untrusted -f ./nixpkgs/untrusted -qa '*' --option restrict-eval true --option allow-import-from-derivation false >/dev/null &
|
||||
wait
|
||||
# Get all currently set rebuild labels
|
||||
gh api \
|
||||
/repos/"$REPOSITORY"/issues/"$NUMBER"/labels \
|
||||
--jq '.[].name | select(startswith("10.rebuild"))' \
|
||||
| sort > before
|
||||
|
||||
# And the labels that should be there
|
||||
jq -r '.labels[]' comparison/changed-paths.json \
|
||||
| sort > after
|
||||
|
||||
# Remove the ones not needed anymore
|
||||
while read -r toRemove; do
|
||||
echo "Removing label $toRemove"
|
||||
gh api \
|
||||
--method DELETE \
|
||||
/repos/"$REPOSITORY"/issues/"$NUMBER"/labels/"$toRemove"
|
||||
done < <(comm -23 before after)
|
||||
|
||||
# And add the ones that aren't set already
|
||||
while read -r toAdd; do
|
||||
echo "Adding label $toAdd"
|
||||
gh api \
|
||||
--method POST \
|
||||
/repos/"$REPOSITORY"/issues/"$NUMBER"/labels \
|
||||
-f "labels[]=$toAdd"
|
||||
done < <(comm -13 before after)
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.number }}
|
||||
|
||||
- name: Add eval summary to commit statuses
|
||||
if: ${{ github.event_name == 'pull_request_target' && github.repository_owner == 'NixOS' }}
|
||||
run: |
|
||||
description=$(jq -r '
|
||||
"Package: added " + (.attrdiff.added | length | tostring) +
|
||||
", removed " + (.attrdiff.removed | length | tostring) +
|
||||
", changed " + (.attrdiff.changed | length | tostring) +
|
||||
", Rebuild: linux " + (.rebuildCountByKernel.linux | tostring) +
|
||||
", darwin " + (.rebuildCountByKernel.darwin | tostring)
|
||||
' <comparison/changed-paths.json)
|
||||
target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID?pr=$NUMBER"
|
||||
gh api --method POST \
|
||||
-H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$GITHUB_REPOSITORY/statuses/$PR_HEAD_SHA" \
|
||||
-f "context=Eval / Summary" -f "state=success" -f "description=$description" -f "target_url=$target_url"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
NUMBER: ${{ github.event.number }}
|
||||
|
||||
- name: Requesting maintainer reviews
|
||||
if: ${{ steps.app-token.outputs.token && github.repository_owner == 'NixOS' }}
|
||||
run: |
|
||||
# maintainers.json contains GitHub IDs. Look up handles to request reviews from.
|
||||
# There appears to be no API to request reviews based on GitHub IDs
|
||||
jq -r 'keys[]' comparison/maintainers.json \
|
||||
| while read -r id; do gh api /user/"$id" --jq .login; done \
|
||||
| GH_TOKEN=${{ steps.app-token.outputs.token }} result/bin/request-reviewers.sh "$REPOSITORY" "$NUMBER" "$AUTHOR"
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.number }}
|
||||
AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
# Don't request reviewers on draft PRs
|
||||
DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }}
|
||||
|
||||
58
.github/workflows/get-merge-commit.yml
vendored
Normal file
58
.github/workflows/get-merge-commit.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Get merge commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/get-merge-commit.yml
|
||||
workflow_call:
|
||||
outputs:
|
||||
mergedSha:
|
||||
description: "The merge commit SHA"
|
||||
value: ${{ jobs.resolve-merge-commit.outputs.mergedSha }}
|
||||
targetSha:
|
||||
description: "The target commit SHA"
|
||||
value: ${{ jobs.resolve-merge-commit.outputs.targetSha }}
|
||||
systems:
|
||||
description: "The supported systems"
|
||||
value: ${{ jobs.resolve-merge-commit.outputs.systems }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
resolve-merge-commit:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
mergedSha: ${{ steps.merged.outputs.mergedSha }}
|
||||
targetSha: ${{ steps.merged.outputs.targetSha }}
|
||||
systems: ${{ steps.systems.outputs.systems }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
path: base
|
||||
sparse-checkout: ci
|
||||
|
||||
- name: Check if the PR can be merged and get the test merge commit
|
||||
id: merged
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_EVENT: ${{ github.event_name }}
|
||||
run: |
|
||||
case "$GH_EVENT" in
|
||||
push)
|
||||
echo "mergedSha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
pull_request*)
|
||||
if commits=$(base/ci/get-merge-commit.sh ${{ github.repository }} ${{ github.event.number }}); then
|
||||
echo -e "Checking the commits:\n$commits"
|
||||
echo "$commits" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Skipping so that no notifications are sent
|
||||
echo "Skipping the rest..."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Load supported systems
|
||||
id: systems
|
||||
run: |
|
||||
echo "systems=$(jq -c <base/ci/supportedSystems.json)" >> "$GITHUB_OUTPUT"
|
||||
60
.github/workflows/labels.yml
vendored
Normal file
60
.github/workflows/labels.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# WARNING:
|
||||
# When extending this action, be aware that $GITHUB_TOKEN allows some write
|
||||
# access to the GitHub API. This means that it should not evaluate user input in
|
||||
# a way that allows code injection.
|
||||
|
||||
name: "Label PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [edited, opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
name: label-pr
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: "github.repository_owner == 'NixOS' && !contains(github.event.pull_request.title, '[skip treewide]')"
|
||||
steps:
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
if: |
|
||||
github.event.pull_request.head.repo.owner.login != 'NixOS' || !(
|
||||
github.head_ref == 'haskell-updates' ||
|
||||
github.head_ref == 'python-updates' ||
|
||||
github.head_ref == 'staging-next' ||
|
||||
startsWith(github.head_ref, 'staging-next-')
|
||||
)
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler.yml # default
|
||||
sync-labels: true
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
if: |
|
||||
github.event.pull_request.head.repo.owner.login != 'NixOS' || !(
|
||||
github.head_ref == 'haskell-updates' ||
|
||||
github.head_ref == 'python-updates' ||
|
||||
github.head_ref == 'staging-next' ||
|
||||
startsWith(github.head_ref, 'staging-next-')
|
||||
)
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler-no-sync.yml
|
||||
sync-labels: false
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
# 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.pull_request.head.repo.owner.login == 'NixOS' && (
|
||||
github.head_ref == 'haskell-updates' ||
|
||||
github.head_ref == 'python-updates' ||
|
||||
github.head_ref == 'staging-next' ||
|
||||
startsWith(github.head_ref, 'staging-next-')
|
||||
)
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler-development-branches.yml
|
||||
sync-labels: true
|
||||
34
.github/workflows/lib-tests.yml
vendored
Normal file
34
.github/workflows/lib-tests.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: "Building Nixpkgs lib-tests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/lib-tests.yml
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'lib/**'
|
||||
- 'maintainers/**'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
nixpkgs-lib-tests:
|
||||
name: nixpkgs-lib-tests
|
||||
runs-on: ubuntu-24.04
|
||||
needs: get-merge-commit
|
||||
if: needs.get-merge-commit.outputs.mergedSha
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- name: Building Nixpkgs lib-tests
|
||||
run: |
|
||||
nix-build ci -A lib-tests
|
||||
153
.github/workflows/lint.yml
vendored
153
.github/workflows/lint.yml
vendored
@@ -1,153 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
mergedSha:
|
||||
required: true
|
||||
type: string
|
||||
targetSha:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
# Should only be provided in the merge queue, not in pull requests,
|
||||
# where we're evaluating untrusted code.
|
||||
CACHIX_AUTH_TOKEN_GHA:
|
||||
required: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
treefmt:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Checkout the merge commit
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
# TODO: Figure out how to best enable caching for the treefmt job. Cachix won't work well,
|
||||
# because the cache would be invalidated on every commit - treefmt checks every file.
|
||||
# Maybe we can cache treefmt's eval-cache somehow.
|
||||
|
||||
- name: Check that files are formatted
|
||||
run: |
|
||||
# Note that it's fine to run this on untrusted code because:
|
||||
# - There's no secrets accessible here
|
||||
# - The build is sandboxed
|
||||
if ! nix-build nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A fmt.check; then
|
||||
echo "Some files are not properly formatted"
|
||||
echo "Please format them by going to the Nixpkgs root directory and running one of:"
|
||||
echo " nix-shell --run treefmt"
|
||||
echo " nix develop --command treefmt"
|
||||
echo " nix fmt"
|
||||
echo "Make sure your branch is up to date with master; rebase if not."
|
||||
echo "If you're having trouble, please ping @NixOS/nix-formatting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
parse:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Checkout the merge commit
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
name: ${{ vars.CACHIX_NAME || 'nixpkgs-gha' }}
|
||||
extraPullNames: nixpkgs-gha
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
pushFilter: -source$
|
||||
|
||||
- name: Parse all nix files
|
||||
run: |
|
||||
# Tests multiple versions at once, let's make sure all of them run, so keep-going.
|
||||
nix-build nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A parse --keep-going
|
||||
|
||||
nixpkgs-vet:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/actions
|
||||
- name: Checkout merge and target commits
|
||||
uses: ./.github/actions/checkout
|
||||
with:
|
||||
merged-as-untrusted-at: ${{ inputs.mergedSha }}
|
||||
target-as-trusted-at: ${{ inputs.targetSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
name: ${{ vars.CACHIX_NAME || 'nixpkgs-gha' }}
|
||||
extraPullNames: nixpkgs-gha
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
pushFilter: -source$
|
||||
|
||||
- name: Running nixpkgs-vet
|
||||
env:
|
||||
# Force terminal colors to be enabled. The library that `nixpkgs-vet` uses respects https://bixense.com/clicolors/
|
||||
CLICOLOR_FORCE: 1
|
||||
run: |
|
||||
if nix-build nixpkgs/untrusted/ci --arg nixpkgs ./nixpkgs/untrusted-pinned -A nixpkgs-vet --arg base "./nixpkgs/trusted" --arg head "./nixpkgs/untrusted"; then
|
||||
exit 0
|
||||
else
|
||||
exitCode=$?
|
||||
echo "To run locally: ./ci/nixpkgs-vet.sh $GITHUB_BASE_REF https://github.com/$GITHUB_REPOSITORY.git"
|
||||
echo "If you're having trouble, ping @NixOS/nixpkgs-vet"
|
||||
exit "$exitCode"
|
||||
fi
|
||||
|
||||
commits:
|
||||
# Only check commits if we have access to the pull_request context.
|
||||
#
|
||||
# Luckily there's no need to lint commit messages in the Merge Queue, because
|
||||
# changes to the target branch can't change commit messages on the base branch.
|
||||
if: ${{ github.event.pull_request.number }}
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
path: trusted
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
- name: Check commit messages
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const checkCommitMessages = require('./trusted/ci/github-script/lint-commits.js')
|
||||
|
||||
checkCommitMessages({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
})
|
||||
58
.github/workflows/manual-nixos-v2.yml
vendored
Normal file
58
.github/workflows/manual-nixos-v2.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: "Build NixOS manual v2"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/manual-nixos-v2.yml
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "nixos/**"
|
||||
# Also build when the nixpkgs doc changed, since we take things like
|
||||
# the release notes and some css and js files from there.
|
||||
# See nixos/doc/manual/default.nix
|
||||
- "doc/**"
|
||||
# Build when something in lib changes
|
||||
# Since the lib functions are used to 'massage' the options before producing the manual
|
||||
- "lib/**"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
nixos:
|
||||
name: nixos-manual-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
system: x86_64-linux
|
||||
- runner: ubuntu-24.04-arm
|
||||
system: aarch64-linux
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
# This cache is for the nixpkgs repo checks and should not be trusted or used elsewhere.
|
||||
name: nixpkgs-ci
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Build NixOS manual
|
||||
id: build-manual
|
||||
run: NIX_PATH=nixpkgs=$(pwd) nix-build --option restrict-eval true ci -A manual-nixos --argstr system ${{ matrix.system }}
|
||||
|
||||
- name: Upload NixOS manual
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: nixos-manual-${{ matrix.system }}
|
||||
path: result/
|
||||
if-no-files-found: error
|
||||
37
.github/workflows/manual-nixpkgs-v2.yml
vendored
Normal file
37
.github/workflows/manual-nixpkgs-v2.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: "Build Nixpkgs manual v2"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/manual-nixpkgs-v2.yml
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'doc/**'
|
||||
- 'lib/**'
|
||||
- 'pkgs/by-name/ni/nixdoc/**'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
nixpkgs:
|
||||
name: nixpkgs-manual-build
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
# This cache is for the nixpkgs repo checks and should not be trusted or used elsewhere.
|
||||
name: nixpkgs-ci
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Building Nixpkgs manual
|
||||
run: NIX_PATH=nixpkgs=$(pwd) nix-build --option restrict-eval true ci -A manual-nixpkgs -A manual-nixpkgs-tests
|
||||
139
.github/workflows/merge-group.yml
vendored
139
.github/workflows/merge-group.yml
vendored
@@ -1,139 +0,0 @@
|
||||
name: Merge Group
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
workflow_call:
|
||||
inputs:
|
||||
artifact-prefix:
|
||||
required: true
|
||||
type: string
|
||||
mergedSha:
|
||||
required: true
|
||||
type: string
|
||||
targetSha:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
baseBranch: ${{ steps.prepare.outputs.base }}
|
||||
mergedSha: ${{ steps.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ steps.prepare.outputs.targetSha }}
|
||||
systems: ${{ steps.prepare.outputs.systems }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
ci/supportedSystems.json
|
||||
|
||||
- id: prepare
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
MERGED_SHA: ${{ inputs.mergedSha }}
|
||||
TARGET_SHA: ${{ inputs.targetSha }}
|
||||
with:
|
||||
script: |
|
||||
const { classify } = require('./ci/supportedBranches.js')
|
||||
const baseBranch = (
|
||||
context.payload.merge_group?.base_ref ??
|
||||
context.payload.pull_request.base.ref
|
||||
).replace(/^refs\/heads\//, '')
|
||||
const baseClassification = classify(baseBranch)
|
||||
core.setOutput('base', baseClassification)
|
||||
core.info('base classification:', baseClassification)
|
||||
|
||||
core.setOutput('mergedSha', context.payload.merge_group?.head_sha ?? process.env.MERGED_SHA)
|
||||
core.info(`mergedSha: ${context.payload.merge_group?.head_sha ?? process.env.MERGED_SHA}`)
|
||||
core.setOutput('targetSha', context.payload.merge_group?.base_sha ?? process.env.TARGET_SHA)
|
||||
core.info(`targetSha: ${context.payload.merge_group?.base_sha ?? process.env.TARGET_SHA}`)
|
||||
core.setOutput('systems', require('./ci/supportedSystems.json'))
|
||||
|
||||
check:
|
||||
name: Check
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/check.yml
|
||||
permissions:
|
||||
# cherry-picks; formality right now, but unused
|
||||
pull-requests: write
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN_GHA: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
with:
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN_GHA: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
with:
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
eval:
|
||||
name: Eval
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/eval.yml
|
||||
# The eval workflow requests these permissions so we must explicitly allow them,
|
||||
# even though they are unused when working with the merge queue.
|
||||
permissions:
|
||||
# compare
|
||||
statuses: write
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN_GHA: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
with:
|
||||
artifact-prefix: ${{ inputs.artifact-prefix }}
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
systems: ${{ needs.prepare.outputs.systems }}
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN_GHA: ${{ secrets.CACHIX_AUTH_TOKEN_GHA }}
|
||||
with:
|
||||
artifact-prefix: ${{ inputs.artifact-prefix }}
|
||||
baseBranch: ${{ needs.prepare.outputs.baseBranch }}
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
# This job's only purpose is to create the target for the "Required Status Checks" branch ruleset.
|
||||
# It "needs" all the jobs that should block the Merge Queue.
|
||||
unlock:
|
||||
if: github.event_name != 'pull_request' && always()
|
||||
# Modify this list to add or remove jobs from required status checks.
|
||||
needs:
|
||||
- check
|
||||
- lint
|
||||
- eval
|
||||
- build
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
RESULTS: ${{ toJSON(needs.*.result) }}
|
||||
with:
|
||||
script: |
|
||||
const { serverUrl, repo, runId, payload } = context
|
||||
const target_url =
|
||||
`${serverUrl}/${repo.owner}/${repo.repo}/actions/runs/${runId}`
|
||||
await github.rest.repos.createCommitStatus({
|
||||
...repo,
|
||||
sha: payload.merge_group.head_sha,
|
||||
// WARNING:
|
||||
// Do NOT change the name of this, otherwise the rule will not catch it anymore.
|
||||
// This would prevent all PRs from merging.
|
||||
context: 'no PR failures',
|
||||
state: JSON.parse(process.env.RESULTS).every(result => result == 'success') ? 'success' : 'error',
|
||||
target_url,
|
||||
})
|
||||
33
.github/workflows/nix-parse-v2.yml
vendored
Normal file
33
.github/workflows/nix-parse-v2.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "Check whether nix files are parseable v2"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/nix-parse-v2.yml
|
||||
pull_request_target:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
tests:
|
||||
name: nix-files-parseable-check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: get-merge-commit
|
||||
if: "needs.get-merge-commit.outputs.mergedSha && !contains(github.event.pull_request.title, '[skip treewide]')"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
nix_path: nixpkgs=channel:nixpkgs-unstable
|
||||
|
||||
- name: Parse all nix files
|
||||
run: |
|
||||
# Tests multiple versions at once, let's make sure all of them run, so keep-going.
|
||||
nix-build ci -A parse --keep-going
|
||||
76
.github/workflows/nixpkgs-vet.yml
vendored
Normal file
76
.github/workflows/nixpkgs-vet.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# `nixpkgs-vet` is a tool to vet Nixpkgs: its architecture, package structure, and more.
|
||||
# Among other checks, it makes sure that `pkgs/by-name` (see `../../pkgs/by-name/README.md`) follows the validity rules outlined in [RFC 140](https://github.com/NixOS/rfcs/pull/140).
|
||||
# When you make changes to this workflow, please also update `ci/nixpkgs-vet.sh` to reflect the impact of your work to the CI.
|
||||
# See https://github.com/NixOS/nixpkgs-vet for details on the tool and its checks.
|
||||
|
||||
name: Vet nixpkgs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/nixpkgs-vet.yml
|
||||
pull_request_target:
|
||||
# This workflow depends on the base branch of the PR, but changing the base branch is not included in the default trigger events, which would be `opened`, `synchronize` or `reopened`.
|
||||
# Instead it causes an `edited` event, so we need to add it explicitly here.
|
||||
# While `edited` is also triggered when the PR title/body is changed, this PR action is fairly quick, and PRs don't get edited **that** often, so it shouldn't be a problem.
|
||||
# There is a feature request for adding a `base_changed` event: https://github.com/orgs/community/discussions/35058
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
permissions: {}
|
||||
|
||||
# We don't use a concurrency group here, because the action is triggered quite often (due to the PR edit trigger), and contributors would get notified on any canceled run.
|
||||
# There is a feature request for suppressing notifications on concurrency-canceled runs: https://github.com/orgs/community/discussions/13015
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
check:
|
||||
name: nixpkgs-vet
|
||||
# This needs to be x86_64-linux, because we depend on the tooling being pre-built in the GitHub releases.
|
||||
runs-on: ubuntu-24.04
|
||||
# This should take 1 minute at most, but let's be generous. The default of 6 hours is definitely too long.
|
||||
timeout-minutes: 10
|
||||
needs: get-merge-commit
|
||||
if: needs.get-merge-commit.outputs.mergedSha
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
# Fetches the merge commit and its parents
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Checking out target branch
|
||||
run: |
|
||||
target=$(mktemp -d)
|
||||
git worktree add "$target" "$(git rev-parse HEAD^1)"
|
||||
echo "target=$target" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
|
||||
|
||||
- name: Fetching the pinned tool
|
||||
# Update the pinned version using ci/nixpkgs-vet/update-pinned-tool.sh
|
||||
run: |
|
||||
# The pinned version of the tooling to use.
|
||||
toolVersion=$(<ci/nixpkgs-vet/pinned-version.txt)
|
||||
|
||||
# Fetch the x86_64-linux-specific release artifact containing the gzipped NAR of the pre-built tool.
|
||||
toolPath=$(curl -sSfL https://github.com/NixOS/nixpkgs-vet/releases/download/"$toolVersion"/x86_64-linux.nar.gz \
|
||||
| gzip -cd | nix-store --import | tail -1)
|
||||
|
||||
# Adds a result symlink as a GC root.
|
||||
nix-store --realise "$toolPath" --add-root result
|
||||
|
||||
- name: Running nixpkgs-vet
|
||||
env:
|
||||
# Force terminal colors to be enabled. The library that `nixpkgs-vet` uses respects https://bixense.com/clicolors/
|
||||
CLICOLOR_FORCE: 1
|
||||
run: |
|
||||
if result/bin/nixpkgs-vet --base "$target" .; then
|
||||
exit 0
|
||||
else
|
||||
exitCode=$?
|
||||
echo "To run locally: ./ci/nixpkgs-vet.sh $GITHUB_BASE_REF https://github.com/$GITHUB_REPOSITORY.git"
|
||||
echo "If you're having trouble, ping @NixOS/nixpkgs-vet"
|
||||
exit "$exitCode"
|
||||
fi
|
||||
28
.github/workflows/no-channel.yml
vendored
Normal file
28
.github/workflows/no-channel.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: "No channel PR"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/no-channel.yml
|
||||
pull_request_target:
|
||||
# Re-run should be triggered when the base branch is updated, instead of silently failing
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
fail:
|
||||
if: |
|
||||
startsWith(github.event.pull_request.base.ref, 'nixos-') ||
|
||||
startsWith(github.event.pull_request.base.ref, 'nixpkgs-')
|
||||
name: "This PR is targeting a channel branch"
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- run: |
|
||||
cat <<EOF
|
||||
The nixos-* and nixpkgs-* branches are pushed to by the channel
|
||||
release script and should not be merged into directly.
|
||||
|
||||
Please target the equivalent release-* branch or master instead.
|
||||
EOF
|
||||
exit 1
|
||||
13
.github/workflows/periodic-merge-24h.yml
vendored
13
.github/workflows/periodic-merge-24h.yml
vendored
@@ -11,15 +11,11 @@ on:
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
# Merge every 24 hours
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
periodic-merge:
|
||||
if: github.repository_owner == 'NixOS'
|
||||
@@ -39,13 +35,10 @@ jobs:
|
||||
into: staging-next-25.05
|
||||
- from: staging-next-25.05
|
||||
into: staging-25.05
|
||||
- name: merge-base(master,staging) → haskell-updates
|
||||
from: master staging
|
||||
- from: master staging
|
||||
into: haskell-updates
|
||||
uses: ./.github/workflows/periodic-merge.yml
|
||||
with:
|
||||
from: ${{ matrix.pairs.from }}
|
||||
into: ${{ matrix.pairs.into }}
|
||||
name: ${{ matrix.pairs.name || format('{0} → {1}', matrix.pairs.from, matrix.pairs.into) }}
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
secrets: inherit
|
||||
|
||||
12
.github/workflows/periodic-merge-6h.yml
vendored
12
.github/workflows/periodic-merge-6h.yml
vendored
@@ -11,15 +11,11 @@ on:
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
# Merge every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
periodic-merge:
|
||||
if: github.repository_owner == 'NixOS'
|
||||
@@ -35,12 +31,8 @@ jobs:
|
||||
into: staging-next
|
||||
- from: staging-next
|
||||
into: staging
|
||||
- from: master
|
||||
into: staging-nixos
|
||||
uses: ./.github/workflows/periodic-merge.yml
|
||||
with:
|
||||
from: ${{ matrix.pairs.from }}
|
||||
into: ${{ matrix.pairs.into }}
|
||||
name: ${{ format('{0} → {1}', matrix.pairs.from, matrix.pairs.into) }}
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
secrets: inherit
|
||||
|
||||
17
.github/workflows/periodic-merge.yml
vendored
17
.github/workflows/periodic-merge.yml
vendored
@@ -11,22 +11,15 @@ on:
|
||||
description: Target branch to merge into.
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY:
|
||||
required: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 5
|
||||
name: ${{ inputs.from }} → ${{ inputs.into }}
|
||||
steps:
|
||||
# Use a GitHub App to create the PR so that CI gets triggered
|
||||
# The App is scoped to Repository > Contents and Pull Requests: write for Nixpkgs
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
- uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
@@ -34,9 +27,7 @@ jobs:
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Find merge base between two branches
|
||||
if: contains(inputs.from, ' ')
|
||||
@@ -60,7 +51,7 @@ jobs:
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Comment on failure
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
issue-number: 105153
|
||||
|
||||
145
.github/workflows/pull-request-target.yml
vendored
145
.github/workflows/pull-request-target.yml
vendored
@@ -1,145 +0,0 @@
|
||||
name: PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
workflow_call:
|
||||
inputs:
|
||||
artifact-prefix:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: pr-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
# wrong branch review comment
|
||||
pull-requests: write
|
||||
outputs:
|
||||
baseBranch: ${{ steps.prepare.outputs.base }}
|
||||
headBranch: ${{ steps.prepare.outputs.head }}
|
||||
mergedSha: ${{ steps.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ steps.prepare.outputs.targetSha }}
|
||||
systems: ${{ steps.prepare.outputs.systems }}
|
||||
touched: ${{ steps.prepare.outputs.touched }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout-cone-mode: true # default, for clarity
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
- id: prepare
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
retries: 10
|
||||
# The default for this includes code 422, which happens regularly for us when comparing commits:
|
||||
# 422 - Server Error: Sorry, this diff is taking too long to generate.
|
||||
# Listing all other values from here to effectively remove 422:
|
||||
# https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14
|
||||
retry-exempt-status-codes: 400,401,403,404
|
||||
script: |
|
||||
require('./ci/github-script/prepare.js')({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
dry: context.eventName == 'pull_request',
|
||||
})
|
||||
|
||||
check:
|
||||
name: Check
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/check.yml
|
||||
permissions:
|
||||
# cherry-picks
|
||||
pull-requests: write
|
||||
with:
|
||||
baseBranch: ${{ needs.prepare.outputs.baseBranch }}
|
||||
headBranch: ${{ needs.prepare.outputs.headBranch }}
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/lint.yml
|
||||
with:
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
eval:
|
||||
name: Eval
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/eval.yml
|
||||
permissions:
|
||||
# compare
|
||||
statuses: write
|
||||
with:
|
||||
artifact-prefix: ${{ inputs.artifact-prefix }}
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
systems: ${{ needs.prepare.outputs.systems }}
|
||||
testVersions: ${{ contains(fromJSON(needs.prepare.outputs.touched), 'pinned') && !contains(fromJSON(needs.prepare.outputs.headBranch).type, 'development') }}
|
||||
|
||||
bot:
|
||||
name: Bot
|
||||
needs: [prepare, eval]
|
||||
uses: ./.github/workflows/bot.yml
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
headBranch: ${{ needs.prepare.outputs.headBranch }}
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
artifact-prefix: ${{ inputs.artifact-prefix }}
|
||||
baseBranch: ${{ needs.prepare.outputs.baseBranch }}
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
# This job's only purpose is to create the target for the "Required Status Checks" branch ruleset.
|
||||
# It "needs" all the jobs that should block merging a PR.
|
||||
unlock:
|
||||
if: github.event_name != 'pull_request' && always()
|
||||
# Modify this list to add or remove jobs from required status checks.
|
||||
needs:
|
||||
- check
|
||||
- lint
|
||||
- eval
|
||||
- build
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
RESULTS: ${{ toJSON(needs.*.result) }}
|
||||
with:
|
||||
script: |
|
||||
const { serverUrl, repo, runId, payload } = context
|
||||
const target_url =
|
||||
`${serverUrl}/${repo.owner}/${repo.repo}/actions/runs/${runId}?pr=${payload.pull_request.number}`
|
||||
await github.rest.repos.createCommitStatus({
|
||||
...repo,
|
||||
sha: payload.pull_request.head.sha,
|
||||
// WARNING:
|
||||
// Do NOT change the name of this, otherwise the rule will not catch it anymore.
|
||||
// This would prevent all PRs from merging.
|
||||
context: 'no PR failures',
|
||||
state: JSON.parse(process.env.RESULTS).every(status => status == 'success') ? 'success' : 'error',
|
||||
target_url,
|
||||
})
|
||||
92
.github/workflows/review.yml
vendored
92
.github/workflows/review.yml
vendored
@@ -1,92 +0,0 @@
|
||||
name: Review
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Reviewed
|
||||
types: [completed]
|
||||
|
||||
# This is used as fallback without app only.
|
||||
# This happens when testing in forks without setting up that app.
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
process:
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
|
||||
# Use the GitHub App to make sure the reaction happens with the same user who will later merge.
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
if: github.event_name != 'pull_request' && vars.NIXPKGS_CI_APP_ID
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || github.token }}
|
||||
retries: 3
|
||||
script: |
|
||||
const { handleMergeComment } = require('./ci/github-script/merge.js')
|
||||
|
||||
// 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
|
||||
})).map(review => {
|
||||
// The `check` 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.
|
||||
if (review.user?.login == 'github-actions[bot]' && review.state == 'DISMISSED')
|
||||
return github.graphql(`
|
||||
mutation($node_id:ID!) {
|
||||
minimizeComment(input: {
|
||||
classifier: RESOLVED,
|
||||
subjectId: $node_id
|
||||
})
|
||||
{ clientMutationId }
|
||||
}`,
|
||||
{ node_id: review.node_id }
|
||||
)
|
||||
|
||||
// The `bot` workflow reacts to comments with @NixOS/nixpkgs-merge-bot references, but might only
|
||||
// pick up a comment after up to 10 minutes. To give the user instant feedback, this job adds
|
||||
// a reaction to these comments.
|
||||
return handleMergeComment({
|
||||
github,
|
||||
body: review.body,
|
||||
node_id: review.node_id,
|
||||
reaction: 'EYES',
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
17
.github/workflows/reviewed.yml
vendored
17
.github/workflows/reviewed.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: Reviewed
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- run: echo This is a no-op only used as a trigger for workflow_run.
|
||||
80
.github/workflows/teams.yml
vendored
80
.github/workflows/teams.yml
vendored
@@ -1,80 +0,0 @@
|
||||
name: Teams
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every Tuesday at 19:42 (randomly chosen)
|
||||
- cron: '42 19 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'NixOS'
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
# Use a GitHub App to create the PR so that CI gets triggered and to
|
||||
# request team member lists.
|
||||
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
permission-administration: read
|
||||
permission-contents: write
|
||||
permission-members: read
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Fetch source
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
maintainers/github-teams.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install bottleneck
|
||||
|
||||
- name: Synchronise teams
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
require('./ci/github-script/get-teams.js')({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
outFile: "maintainers/github-teams.json"
|
||||
})
|
||||
|
||||
- name: Get GitHub App User Git String
|
||||
id: user
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
||||
run: |
|
||||
name="${APP_SLUG}[bot]"
|
||||
userId=$(gh api "/users/$name" --jq .id)
|
||||
email="$userId+$name@users.noreply.github.com"
|
||||
echo "git-string=$name <$email>" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
add-paths: maintainers/github-teams.json
|
||||
author: ${{ steps.user.outputs.git-string }}
|
||||
committer: ${{ steps.user.outputs.git-string }}
|
||||
commit-message: "maintainers/github-teams.json: Automated sync"
|
||||
branch: pr/github-team-sync
|
||||
title: "maintainers/github-teams.json: Automated sync"
|
||||
body: |
|
||||
This is an automated PR to sync the GitHub teams with access to this repository to the `lib.teams` list.
|
||||
|
||||
This PR can be merged without taking any further action.
|
||||
|
||||
105
.github/workflows/test.yml
vendored
105
.github/workflows/test.yml
vendored
@@ -1,105 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: test-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
merge-group: ${{ steps.files.outputs.merge-group }}
|
||||
mergedSha: ${{ steps.prepare.outputs.mergedSha }}
|
||||
pr: ${{ steps.files.outputs.pr }}
|
||||
push: ${{ steps.files.outputs.push }}
|
||||
targetSha: ${{ steps.prepare.outputs.targetSha }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout-cone-mode: true # default, for clarity
|
||||
sparse-checkout: |
|
||||
ci/github-script
|
||||
- id: prepare
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
retries: 10
|
||||
# The default for this includes code 422, which happens regularly for us when comparing commits:
|
||||
# 422 - Server Error: Sorry, this diff is taking too long to generate.
|
||||
# Listing all other values from here to effectively remove 422:
|
||||
# https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14
|
||||
retry-exempt-status-codes: 400,401,403,404
|
||||
script: |
|
||||
require('./ci/github-script/prepare.js')({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
// Review comments will be posted by the main PR workflow on the pull_request_target event.
|
||||
dry: false,
|
||||
})
|
||||
|
||||
- name: Determine changed files
|
||||
id: files
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const files = (await github.paginate(github.rest.pulls.listFiles, {
|
||||
...context.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
})).map(file => file.filename)
|
||||
|
||||
if (files.some(file => [
|
||||
'.github/workflows/eval.yml',
|
||||
'.github/workflows/lint.yml',
|
||||
'.github/workflows/merge-group.yml',
|
||||
'.github/workflows/test.yml',
|
||||
].includes(file))) core.setOutput('merge-group', true)
|
||||
|
||||
if (files.some(file => [
|
||||
'.github/actions/checkout/action.yml',
|
||||
'.github/workflows/bot.yml',
|
||||
'.github/workflows/build.yml',
|
||||
'.github/workflows/check.yml',
|
||||
'.github/workflows/eval.yml',
|
||||
'.github/workflows/lint.yml',
|
||||
'.github/workflows/pull-request-target.yml',
|
||||
'.github/workflows/test.yml',
|
||||
'ci/github-script/bot.js',
|
||||
'ci/github-script/merge.js',
|
||||
'ci/github-script/withRateLimit.js',
|
||||
].includes(file))) core.setOutput('pr', true)
|
||||
|
||||
merge-group:
|
||||
if: needs.prepare.outputs.merge-group
|
||||
name: Merge Group
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/merge-group.yml
|
||||
# Those are actually only used on the merge_group event, but will throw an error if not set.
|
||||
permissions:
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
with:
|
||||
artifact-prefix: mg-
|
||||
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
|
||||
targetSha: ${{ needs.prepare.outputs.targetSha }}
|
||||
|
||||
pr:
|
||||
if: needs.prepare.outputs.pr
|
||||
name: PR
|
||||
needs: [prepare]
|
||||
uses: ./.github/workflows/pull-request-target.yml
|
||||
# Those are actually only used on the pull_request_target event, but will throw an error if not set.
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
secrets:
|
||||
NIXPKGS_CI_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
artifact-prefix: pr-
|
||||
12
.github/zizmor.yml
vendored
12
.github/zizmor.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# This file defines the ignore rules for zizmor.
|
||||
#
|
||||
# For rules that contain a high number of false positives, prefer listing them here
|
||||
# instead of adding ignore comments. Note that zizmor cannot ignore by line-within-a-string, so
|
||||
# there are some ignore items that encompass multiple problems within one `run` block. An issue
|
||||
# tracking this is at https://github.com/woodruffw/zizmor/issues/648.
|
||||
#
|
||||
# For more info, see the documentation: https://woodruffw.github.io/zizmor/usage/#ignoring-results
|
||||
|
||||
rules:
|
||||
dangerous-triggers:
|
||||
disable: true
|
||||
21
.mergify.yml
Normal file
21
.mergify.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
queue_rules:
|
||||
# This rule is for https://docs.mergify.com/commands/queue/
|
||||
# and can be triggered with: @mergifyio queue
|
||||
- name: default
|
||||
merge_conditions:
|
||||
# all github action checks in this list are required to merge a pull request
|
||||
- check-success=Attributes
|
||||
- check-success=Check
|
||||
- check-success=Outpaths (aarch64-darwin)
|
||||
- check-success=Outpaths (aarch64-linux)
|
||||
- check-success=Outpaths (x86_64-darwin)
|
||||
- check-success=Outpaths (x86_64-linux)
|
||||
- check-success=Process
|
||||
- check-success=Request
|
||||
- check-success=editorconfig-check
|
||||
- check-success=label-pr
|
||||
- check-success=nix-files-parseable-check
|
||||
- check-success=nixfmt-check
|
||||
- check-success=nixpkgs-vet
|
||||
# queue up to 5 pull requests at a time
|
||||
batch_size: 5
|
||||
@@ -423,22 +423,20 @@ gitGraph
|
||||
|
||||
Here's an overview of the different branches:
|
||||
|
||||
| branch | `master` | `staging-next` | `staging` | [`staging-nixos`](#test-driver-rebuild) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Used for development | ✔️ | ❌ | ✔️ | ✔️ |
|
||||
| Built by Hydra | ✔️ | ✔️ | ❌ | ❌ |
|
||||
| [Mass rebuilds][mass-rebuild] | ❌ | ⚠️ Only to fix Hydra builds | ✔️ | ❌[^1] |
|
||||
| Critical security fixes | ✔️ for non-mass-rebuilds | ✔️ for mass-rebuilds | ❌ | ✔️ |
|
||||
| Automatically merged into | `staging-next` & `staging-nixos` | `staging` | - | - |
|
||||
| Manually merged into | - | `master` | `staging-next` | `master` |
|
||||
| branch | `master` | `staging-next` | `staging` |
|
||||
| --- | --- | --- | --- |
|
||||
| Used for development | ✔️ | ❌ | ✔️ |
|
||||
| Built by Hydra | ✔️ | ✔️ | ❌ |
|
||||
| [Mass rebuilds][mass-rebuild] | ❌ | ⚠️ Only to fix Hydra builds | ✔️ |
|
||||
| Critical security fixes | ✔️ for non-mass-rebuilds | ✔️ for mass-rebuilds | ❌ |
|
||||
| Automatically merged into | `staging-next` | `staging` | - |
|
||||
| Manually merged into | - | `master` | `staging-next` |
|
||||
|
||||
The staging workflow is used for all main branches, `master` and `release-YY.MM`, with corresponding names:
|
||||
- `master`/`release-YY.MM`
|
||||
- `staging`/`staging-YY.MM`
|
||||
- `staging-next`/`staging-next-YY.MM`
|
||||
|
||||
[^1]: Except changes that cause no more rebuilds than kernel updates
|
||||
|
||||
# Conventions
|
||||
|
||||
## Branch conventions
|
||||
@@ -490,26 +488,6 @@ In order to help the decision, CI automatically assigns [`rebuild` labels](https
|
||||
As a rule of thumb, if the number of rebuilds is **over 500**, it can be considered a mass rebuild.
|
||||
To get a sense for what changes are considered mass rebuilds, see [previously merged pull requests to the staging branches](https://github.com/NixOS/nixpkgs/issues?q=base%3Astaging+-base%3Astaging-next+is%3Amerged).
|
||||
|
||||
Please note that changes to the Linux kernel are an exception to this rule.
|
||||
These PRs go to `staging-nixos`, see [the next section for more context](#changes-rebuilding-all-tests).
|
||||
|
||||
### Changes rebuilding all NixOS tests
|
||||
[test-driver-rebuild]: #changes-rebuilding-all-tests
|
||||
|
||||
Changes causing a rebuild of all NixOS tests get a special [`10.rebuild-nixos-tests`](https://github.com/NixOS/nixpkgs/issues?q=state%3Aopen%20label%3A10.rebuild-nixos-tests) label.
|
||||
These changes pose a significant impact on the build infrastructure.
|
||||
|
||||
Hence, these PRs should either target a `staging`-branch or `staging-nixos`, provided one of following conditions applies:
|
||||
|
||||
* The label `10.rebuild-nixos-tests` is set, or
|
||||
* The PR is a change affecting the Linux kernel.
|
||||
|
||||
The branch gets merged whenever mainline kernel updates or critical security fixes land on the branch.
|
||||
This usually happens on a weekly basis.
|
||||
|
||||
Backports are not handled by such a branch.
|
||||
The relevant PRs from this branch must be backported manually.
|
||||
|
||||
## Commit conventions
|
||||
[commit-conventions]: #commit-conventions
|
||||
|
||||
@@ -581,12 +559,7 @@ If you have any problems with formatting, please ping the
|
||||
- Functions should list their expected arguments as precisely as possible. That is, write
|
||||
|
||||
```nix
|
||||
{
|
||||
stdenv,
|
||||
fetchurl,
|
||||
perl,
|
||||
}:
|
||||
<...>
|
||||
{ stdenv, fetchurl, perl }: <...>
|
||||
```
|
||||
|
||||
instead of
|
||||
@@ -598,25 +571,17 @@ If you have any problems with formatting, please ping the
|
||||
or
|
||||
|
||||
```nix
|
||||
{
|
||||
stdenv,
|
||||
fetchurl,
|
||||
perl,
|
||||
...
|
||||
}:
|
||||
<...>
|
||||
{ stdenv, fetchurl, perl, ... }: <...>
|
||||
```
|
||||
|
||||
For functions that are truly generic in the number of arguments (such as wrappers around `mkDerivation`) that have some required arguments, you should write them using an `@`-pattern:
|
||||
|
||||
```nix
|
||||
{
|
||||
stdenv,
|
||||
doCoverageAnalysis ? false,
|
||||
...
|
||||
}@args:
|
||||
{ stdenv, doCoverageAnalysis ? false, ... } @ args:
|
||||
|
||||
stdenv.mkDerivation (args // { foo = if doCoverageAnalysis then "bla" else ""; })
|
||||
stdenv.mkDerivation (args // {
|
||||
foo = if doCoverageAnalysis then "bla" else "";
|
||||
})
|
||||
```
|
||||
|
||||
instead of
|
||||
@@ -624,36 +589,41 @@ If you have any problems with formatting, please ping the
|
||||
```nix
|
||||
args:
|
||||
|
||||
args.stdenv.mkDerivation (
|
||||
args
|
||||
// {
|
||||
foo = if args ? doCoverageAnalysis && args.doCoverageAnalysis then "bla" else "";
|
||||
}
|
||||
)
|
||||
args.stdenv.mkDerivation (args // {
|
||||
foo = if args ? doCoverageAnalysis && args.doCoverageAnalysis then "bla" else "";
|
||||
})
|
||||
```
|
||||
|
||||
- Unnecessary string conversions should be avoided. Do
|
||||
|
||||
```nix
|
||||
{ rev = version; }
|
||||
{
|
||||
rev = version;
|
||||
}
|
||||
```
|
||||
|
||||
instead of
|
||||
|
||||
```nix
|
||||
{ rev = "${version}"; }
|
||||
{
|
||||
rev = "${version}";
|
||||
}
|
||||
```
|
||||
|
||||
- Building lists conditionally _should_ be done with `lib.optional(s)` instead of using `if cond then [ ... ] else null` or `if cond then [ ... ] else [ ]`.
|
||||
|
||||
```nix
|
||||
{ buildInputs = lib.optional stdenv.hostPlatform.isDarwin iconv; }
|
||||
{
|
||||
buildInputs = lib.optional stdenv.hostPlatform.isDarwin iconv;
|
||||
}
|
||||
```
|
||||
|
||||
instead of
|
||||
|
||||
```nix
|
||||
{ buildInputs = if stdenv.hostPlatform.isDarwin then [ iconv ] else null; }
|
||||
{
|
||||
buildInputs = if stdenv.hostPlatform.isDarwin then [ iconv ] else null;
|
||||
}
|
||||
```
|
||||
|
||||
As an exception, an explicit conditional expression with null can be used when fixing a important bug without triggering a mass rebuild.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://nixos.org">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/NixOS/nixos-artwork/master/logo/nixos.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://nixos.org/logo/nixos-hires.png">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NixOS/nixos-artwork/master/logo/nixos-white.png">
|
||||
<img src="https://raw.githubusercontent.com/NixOS/nixos-artwork/master/logo/nixos.svg" width="500px" alt="NixOS logo">
|
||||
<img src="https://nixos.org/logo/nixos-hires.png" width="500px" alt="NixOS logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
34
ci/OWNERS
34
ci/OWNERS
@@ -15,9 +15,11 @@
|
||||
|
||||
# CI
|
||||
/.github/*_TEMPLATE* @SigmaSquadron
|
||||
/.github/actions @NixOS/nixpkgs-ci
|
||||
/.github/workflows @NixOS/nixpkgs-ci
|
||||
/ci @NixOS/nixpkgs-ci
|
||||
/.github/workflows @NixOS/Security @Mic92 @zowoq @infinisil @azuwis @wolfgangwalther
|
||||
/.github/workflows/check-format.yml @infinisil @wolfgangwalther
|
||||
/.github/workflows/codeowners-v2.yml @infinisil @wolfgangwalther
|
||||
/.github/workflows/nixpkgs-vet.yml @infinisil @philiptaron @wolfgangwalther
|
||||
/ci @infinisil @philiptaron @NixOS/Security @wolfgangwalther
|
||||
/ci/OWNERS @infinisil @philiptaron
|
||||
|
||||
# Development support
|
||||
@@ -26,21 +28,13 @@
|
||||
|
||||
# Libraries
|
||||
/lib @infinisil @hsjobeki
|
||||
/lib/systems @alyssais @ericson2314 @NixOS/stdenv
|
||||
/lib/generators.nix @infinisil @hsjobeki @Profpatsch
|
||||
/lib/cli.nix @infinisil @hsjobeki @Profpatsch
|
||||
/lib/debug.nix @infinisil @hsjobeki @Profpatsch
|
||||
/lib/asserts.nix @infinisil @hsjobeki @Profpatsch
|
||||
/lib/path/* @infinisil @hsjobeki
|
||||
/lib/fileset @infinisil @hsjobeki
|
||||
/maintainers/github-teams.json @infinisil
|
||||
/maintainers/computed-team-list.nix @infinisil
|
||||
## Standard environment–related libraries
|
||||
/lib/customisation.nix @alyssais @NixOS/stdenv
|
||||
/lib/derivations.nix @alyssais @NixOS/stdenv
|
||||
/lib/fetchers.nix @alyssais @NixOS/stdenv
|
||||
/lib/meta.nix @alyssais @NixOS/stdenv
|
||||
/lib/source-types.nix @alyssais @NixOS/stdenv
|
||||
/lib/systems @alyssais @NixOS/stdenv
|
||||
## Libraries / Module system
|
||||
/lib/modules.nix @infinisil @roberth @hsjobeki
|
||||
/lib/types.nix @infinisil @roberth @hsjobeki
|
||||
@@ -64,7 +58,6 @@
|
||||
/pkgs/build-support/cc-wrapper @Ericson2314
|
||||
/pkgs/build-support/bintools-wrapper @Ericson2314
|
||||
/pkgs/build-support/setup-hooks @Ericson2314
|
||||
/pkgs/build-support/setup-hooks/arrayUtilities @ConnorBaker
|
||||
/pkgs/build-support/setup-hooks/auto-patchelf.sh @layus
|
||||
/pkgs/by-name/au/auto-patchelf @layus
|
||||
|
||||
@@ -227,10 +220,7 @@ nixos/modules/installer/tools/nix-fallback-paths.nix @NixOS/nix-team @raitobeza
|
||||
/nixos/tests/snapcast.nix @mweinelt
|
||||
|
||||
# Browsers
|
||||
/pkgs/build-support/build-mozilla-mach @mweinelt
|
||||
/pkgs/applications/networking/browsers/firefox/update.nix
|
||||
/pkgs/applications/networking/browsers/firefox/packages/firefox.nix @mweinelt
|
||||
/pkgs/applications/networking/browsers/firefox/packages/firefox-esr-*.nix @mweinelt
|
||||
/pkgs/applications/networking/browsers/firefox @mweinelt
|
||||
/pkgs/applications/networking/browsers/chromium @emilylange @networkException
|
||||
/nixos/tests/chromium.nix @emilylange @networkException
|
||||
|
||||
@@ -250,7 +240,7 @@ pkgs/development/python-modules/buildcatrust/ @ajs124 @lukegb @mweinelt
|
||||
/pkgs/applications/editors/jetbrains @edwtjo @leona-ya @theCapypara
|
||||
|
||||
# Licenses
|
||||
/lib/licenses.nix @alyssais @emilazy
|
||||
/lib/licenses.nix @alyssais
|
||||
|
||||
# Qt
|
||||
/pkgs/development/libraries/qt-5 @K900 @NickCao @SuperSandro2000 @ttuegel
|
||||
@@ -293,10 +283,6 @@ pkgs/development/python-modules/buildcatrust/ @ajs124 @lukegb @mweinelt
|
||||
/pkgs/servers/home-assistant @mweinelt
|
||||
/pkgs/by-name/es/esphome @mweinelt
|
||||
|
||||
# Linux kernel
|
||||
/pkgs/top-level/linux-kernels.nix @NixOS/linux-kernel
|
||||
/pkgs/os-specific/linux/kernel/ @NixOS/linux-kernel
|
||||
|
||||
# Network Time Daemons
|
||||
/pkgs/by-name/ch/chrony @thoughtpolice
|
||||
/pkgs/by-name/nt/ntp @thoughtpolice
|
||||
@@ -485,7 +471,7 @@ pkgs/development/interpreters/lfe/ @NixOS/beam
|
||||
pkgs/by-name/oc/octodns/ @anthonyroussel
|
||||
|
||||
# Teleport
|
||||
pkgs/by-name/te/teleport* @arianvp @justinas @sigma @tomberek @freezeboy @techknowlogick @JuliusFreudenberger
|
||||
pkgs/servers/teleport @arianvp @justinas @sigma @tomberek @freezeboy @techknowlogick @JuliusFreudenberger
|
||||
|
||||
# Warp-terminal
|
||||
pkgs/by-name/wa/warp-terminal/ @emilytrau @imadnyc @FlameFlag @johnrtitor
|
||||
pkgs/by-name/wa/warp-terminal/ @emilytrau @imadnyc @donteatoreo @johnrtitor
|
||||
|
||||
143
ci/README.md
143
ci/README.md
@@ -6,101 +6,80 @@ This is in contrast with [`maintainers/scripts`](../maintainers/scripts) which i
|
||||
## Pinned Nixpkgs
|
||||
|
||||
CI may need certain packages from Nixpkgs.
|
||||
In order to ensure that the needed packages are generally available without building, [`pinned.json`](./pinned.json) contains a pinned Nixpkgs version tested by Hydra.
|
||||
In order to ensure that the needed packages are generally available without building,
|
||||
[`pinned-nixpkgs.json`](./pinned-nixpkgs.json) contains a pinned Nixpkgs version tested by Hydra.
|
||||
|
||||
Run [`update-pinned.sh`](./update-pinned.sh) to update it.
|
||||
|
||||
## GitHub specific code
|
||||
|
||||
Some of the code is specific to GitHub.
|
||||
This code is currently spread out over multiple places and written in both Bash and JavaScript.
|
||||
The goal is to eventually have all GitHub specific code in `ci/github-script` and written in JavaScript via `actions/github-script`.
|
||||
A lot of code has already been migrated, but some Bash code still remains.
|
||||
New CI features need to be introduced in JavaScript, not Bash.
|
||||
|
||||
## Nixpkgs merge bot
|
||||
|
||||
The Nixpkgs merge bot empowers package maintainers by enabling them to merge PRs related to their own packages.
|
||||
It serves as a bridge for maintainers to quickly respond to user feedback, facilitating a more self-reliant approach.
|
||||
Especially when considering there are roughly 20 maintainers for every committer, this bot is a game-changer.
|
||||
|
||||
Following [RFC 172] the merge bot was originally implemented as a [python webapp](https://github.com/NixOS/nixpkgs-merge-bot), which has now been integrated into [`ci/github-script/bot.js`](./github-script/bot.js) and [`ci/github-script/merge.js`](./github-script/merge.js).
|
||||
|
||||
### Using the merge bot
|
||||
|
||||
To merge a PR, maintainers can simply comment:
|
||||
```gfm
|
||||
@NixOS/nixpkgs-merge-bot merge
|
||||
```
|
||||
|
||||
The next time the bot runs it will verify the below constraints, then (if satisfied) merge the PR.
|
||||
|
||||
The merge bot will reference [#306934](https://github.com/NixOS/nixpkgs/issues/306934) on PRs it merges successfully, [#305350](https://github.com/NixOS/nixpkgs/issues/305350) for unsuccessful attempts, or [#371492](https://github.com/NixOS/nixpkgs/issues/371492) if an error occurs.
|
||||
These issues effectively list PRs the merge bot has interacted with.
|
||||
|
||||
### Merge bot constraints
|
||||
|
||||
To ensure security and a focused utility, the bot adheres to specific limitations:
|
||||
|
||||
- The PR targets one of the [development branches](#branch-classification).
|
||||
- The PR only touches files of packages located under `pkgs/by-name/*`.
|
||||
- The PR is either:
|
||||
- approved by a [committer][@NixOS/nixpkgs-committers].
|
||||
- backported via label.
|
||||
- opened by a [committer][@NixOS/nixpkgs-committers].
|
||||
- opened by [@r-ryantm](https://nix-community.github.io/nixpkgs-update/r-ryantm/).
|
||||
- The user attempting to merge is a member of [@NixOS/nixpkgs-maintainers].
|
||||
- The user attempting to merge is a maintainer of all packages touched by the PR.
|
||||
|
||||
### Approving merge bot changes
|
||||
|
||||
Changes to the bot can usually be approved by the [@NixOS/nixpkgs-ci] team, as with other CI changes.
|
||||
However, additional acknowledgement from the [@NixOS/nixpkgs-core] team is required for changes to what the merge bot will merge, who is eligible to use the merge bot, or similar changes in scope.
|
||||
Run [`update-pinned-nixpkgs.sh`](./update-pinned-nixpkgs.sh) to update it.
|
||||
|
||||
## `ci/nixpkgs-vet.sh BASE_BRANCH [REPOSITORY]`
|
||||
|
||||
Runs the [`nixpkgs-vet` tool](https://github.com/NixOS/nixpkgs-vet) on the HEAD commit, closely matching what CI does.
|
||||
This can't do exactly the same as CI, because CI needs to rely on GitHub's server-side Git history to compute the mergeability of PRs before the check can be started.
|
||||
Runs the [`nixpkgs-vet` tool](https://github.com/NixOS/nixpkgs-vet) on the HEAD commit, closely matching what CI does. This can't do exactly the same as CI, because CI needs to rely on GitHub's server-side Git history to compute the mergeability of PRs before the check can be started.
|
||||
In turn, when contributors are running this tool locally, we don't want to have to push commits to test them, and we can also rely on the local Git history to do the mergeability check.
|
||||
|
||||
Arguments:
|
||||
|
||||
- `BASE_BRANCH`: The base branch to use, e.g. master or release-24.05
|
||||
- `REPOSITORY`: The repository from which to fetch the base branch.
|
||||
Defaults to <https://github.com/NixOS/nixpkgs.git>.
|
||||
- `REPOSITORY`: The repository from which to fetch the base branch. Defaults to <https://github.com/NixOS/nixpkgs.git>.
|
||||
|
||||
# Branch classification
|
||||
## `ci/nixpkgs-vet`
|
||||
|
||||
For the purposes of CI, branches in the NixOS/nixpkgs repository are classified as follows:
|
||||
This directory contains scripts and files used and related to [`nixpkgs-vet`](https://github.com/NixOS/nixpkgs-vet/), which the CI uses to implement `pkgs/by-name` checks, along with many other Nixpkgs architecture rules.
|
||||
See also the [CI GitHub Action](../.github/workflows/nixpkgs-vet.yml).
|
||||
|
||||
- **Channel** branches
|
||||
- `nixos-` or `nixpkgs-` prefix
|
||||
- Are only updated from `master` or `release-` branches, when hydra passes.
|
||||
- Otherwise not worked on, Pull Requests are not allowed.
|
||||
- Long-lived, no deletion, no force push.
|
||||
- **Primary development** branches
|
||||
- `release-` prefix and `master`
|
||||
- Pull Requests required.
|
||||
- Long-lived, no deletion, no force push.
|
||||
- **Secondary development** branches
|
||||
- `staging-` prefix and `haskell-updates`
|
||||
- Pull Requests normally required, except when merging development branches into each other.
|
||||
- Long-lived, no deletion, no force push.
|
||||
- **Work-In-Progress** branches
|
||||
- `backport-`, `revert-` and `wip-` prefixes.
|
||||
- Deprecated: All other branches, not matched by channel/development.
|
||||
- Pull Requests are optional.
|
||||
- Short-lived, force push allowed, deleted after merge.
|
||||
## `ci/nixpkgs-vet/update-pinned-tool.sh`
|
||||
|
||||
Some branches also have a version component, which is either `unstable` or `YY.MM`.
|
||||
Updates the pinned [`nixpkgs-vet` tool](https://github.com/NixOS/nixpkgs-vet) in [`ci/nixpkgs-vet/pinned-version.txt`](./nixpkgs-vet/pinned-version.txt) to the latest [release](https://github.com/NixOS/nixpkgs-vet/releases).
|
||||
|
||||
`ci/supportedBranches.js` is a script imported by CI to classify the base and head branches of a Pull Request.
|
||||
This classification will then be used to skip certain jobs.
|
||||
This script can also be run locally to print basic test cases.
|
||||
Each release contains a pre-built `x86_64-linux` version of the tool which is used by CI.
|
||||
|
||||
This script currently needs to be called manually when the CI tooling needs to be updated.
|
||||
|
||||
[@NixOS/nixpkgs-maintainers]: https://github.com/orgs/NixOS/teams/nixpkgs-maintainers
|
||||
[@NixOS/nixpkgs-committers]: https://github.com/orgs/NixOS/teams/nixpkgs-committers
|
||||
[@NixOS/nixpkgs-ci]: https://github.com/orgs/NixOS/teams/nixpkgs-ci
|
||||
[@NixOS/nixpkgs-core]: https://github.com/orgs/NixOS/teams/nixpkgs-core
|
||||
[RFC 172]: https://github.com/NixOS/rfcs/pull/172
|
||||
Why not just build the tooling right from the PRs Nixpkgs version?
|
||||
|
||||
- Because it allows CI to check all PRs, even if they would break the CI tooling.
|
||||
- Because it makes the CI check very fast, since no Nix builds need to be done, even for mass rebuilds.
|
||||
- Because it improves security, since we don't have to build potentially untrusted code from PRs.
|
||||
The tool only needs a very minimal Nix evaluation at runtime, which can work with [readonly-mode](https://nixos.org/manual/nix/stable/command-ref/opt-common.html#opt-readonly-mode) and [restrict-eval](https://nixos.org/manual/nix/stable/command-ref/conf-file.html#conf-restrict-eval).
|
||||
|
||||
## `get-merge-commit.sh GITHUB_REPO PR_NUMBER`
|
||||
|
||||
Check whether a PR is mergeable and return the test merge commit as
|
||||
[computed by GitHub](https://docs.github.com/en/rest/guides/using-the-rest-api-to-interact-with-your-git-database?apiVersion=2022-11-28#checking-mergeability-of-pull-requests) and its parent.
|
||||
|
||||
Arguments:
|
||||
- `GITHUB_REPO`: The repository of the PR, e.g. `NixOS/nixpkgs`
|
||||
- `PR_NUMBER`: The PR number, e.g. `1234`
|
||||
|
||||
Exit codes:
|
||||
- 0: The PR can be merged, the hashes of the test merge commit and the target commit are returned on stdout
|
||||
- 1: The PR cannot be merged because it's not open anymore
|
||||
- 2: The PR cannot be merged because it has a merge conflict
|
||||
- 3: The merge commit isn't being computed, GitHub is likely having internal issues, unknown if the PR is mergeable
|
||||
|
||||
### Usage
|
||||
|
||||
This script is implemented as a reusable GitHub Actions workflow, and can be used as follows:
|
||||
|
||||
```yaml
|
||||
on: pull_request_target
|
||||
|
||||
# We need a token to query the API, but it doesn't need any special permissions
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get-merge-commit:
|
||||
# use the relative path of the get-merge-commit workflow yaml here
|
||||
uses: ./.github/workflows/get-merge-commit.yml
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
needs: get-merge-commit
|
||||
steps:
|
||||
- uses: actions/checkout@<VERSION>
|
||||
# Add this to _all_ subsequent steps to skip them
|
||||
if: needs.get-merge-commit.outputs.mergedSha
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
- ...
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ buildGoModule {
|
||||
})
|
||||
# Undoes part of the above PR: We don't want to require write access
|
||||
# to the repository, that's only needed for GitHub's native CODEOWNERS.
|
||||
# Furthermore, it removes an unnecessary check from the code
|
||||
# Furthermore, it removes an unneccessary check from the code
|
||||
# that breaks tokens generated for GitHub Apps.
|
||||
./permissions.patch
|
||||
# Allows setting a custom CODEOWNERS path using the OWNERS_FILE env var
|
||||
|
||||
140
ci/default.nix
140
ci/default.nix
@@ -1,34 +1,33 @@
|
||||
let
|
||||
pinned = (builtins.fromJSON (builtins.readFile ./pinned.json)).pins;
|
||||
pinnedNixpkgs = builtins.fromJSON (builtins.readFile ./pinned-nixpkgs.json);
|
||||
in
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
|
||||
nixpkgs ? null,
|
||||
nixPath ? "nixVersions.latest",
|
||||
}:
|
||||
let
|
||||
nixpkgs' =
|
||||
if nixpkgs == null then
|
||||
fetchTarball {
|
||||
inherit (pinned.nixpkgs) url;
|
||||
sha256 = pinned.nixpkgs.hash;
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/${pinnedNixpkgs.rev}.tar.gz";
|
||||
sha256 = pinnedNixpkgs.sha256;
|
||||
}
|
||||
else
|
||||
nixpkgs;
|
||||
|
||||
pkgs = import nixpkgs' {
|
||||
inherit system;
|
||||
# Nixpkgs generally — and CI specifically — do not use aliases,
|
||||
# because we want to ensure they are not load-bearing.
|
||||
allowAliases = false;
|
||||
config = { };
|
||||
overlays = [ ];
|
||||
};
|
||||
|
||||
fmt =
|
||||
let
|
||||
treefmtNixSrc = fetchTarball {
|
||||
inherit (pinned.treefmt-nix) url;
|
||||
sha256 = pinned.treefmt-nix.hash;
|
||||
# Master at 2025-02-12
|
||||
url = "https://github.com/numtide/treefmt-nix/archive/4f09b473c936d41582dd744e19f34ec27592c5fd.tar.gz";
|
||||
sha256 = "051vh6raskrxw5k6jncm8zbk9fhbzgm1gxpq9gm5xw1b6wgbgcna";
|
||||
};
|
||||
treefmtEval = (import treefmtNixSrc).evalModule pkgs {
|
||||
# Important: The auto-rebase script uses `git filter-branch --tree-filter`,
|
||||
@@ -47,102 +46,19 @@ let
|
||||
|
||||
programs.actionlint.enable = true;
|
||||
|
||||
programs.biome = {
|
||||
enable = true;
|
||||
# Disable settings validation because its inputs are liable to hash mismatch
|
||||
validate.enable = false;
|
||||
settings.formatter = {
|
||||
useEditorconfig = true;
|
||||
};
|
||||
settings.javascript.formatter = {
|
||||
quoteStyle = "single";
|
||||
semicolons = "asNeeded";
|
||||
};
|
||||
settings.json.formatter.enabled = false;
|
||||
};
|
||||
settings.formatter.biome.excludes = [
|
||||
"*.min.js"
|
||||
"pkgs/*"
|
||||
];
|
||||
|
||||
programs.keep-sorted.enable = true;
|
||||
|
||||
# This uses nixfmt underneath, the default formatter for Nix code.
|
||||
# This uses nixfmt-rfc-style underneath,
|
||||
# the default formatter for Nix code.
|
||||
# See https://github.com/NixOS/nixfmt
|
||||
programs.nixfmt = {
|
||||
enable = true;
|
||||
package = pkgs.nixfmt;
|
||||
};
|
||||
|
||||
programs.yamlfmt = {
|
||||
enable = true;
|
||||
settings.formatter = {
|
||||
retain_line_breaks = true;
|
||||
};
|
||||
};
|
||||
settings.formatter.yamlfmt.excludes = [
|
||||
# Aligns comments with whitespace
|
||||
"pkgs/development/haskell-modules/configuration-hackage2nix/main.yaml"
|
||||
# TODO: Fix formatting for auto-generated file
|
||||
"pkgs/development/haskell-modules/configuration-hackage2nix/transitive-broken.yaml"
|
||||
];
|
||||
|
||||
programs.nixf-diagnose.enable = true;
|
||||
settings.formatter.nixf-diagnose = {
|
||||
# Ensure nixfmt cleans up after nixf-diagnose.
|
||||
priority = -1;
|
||||
options = [
|
||||
"--auto-fix"
|
||||
# Rule names can currently be looked up here:
|
||||
# https://github.com/nix-community/nixd/blob/main/libnixf/src/Basic/diagnostic.py
|
||||
# TODO: Remove the following and fix things.
|
||||
"--ignore=sema-unused-def-lambda-noarg-formal"
|
||||
"--ignore=sema-unused-def-lambda-witharg-arg"
|
||||
"--ignore=sema-unused-def-lambda-witharg-formal"
|
||||
"--ignore=sema-unused-def-let"
|
||||
# Keep this rule, because we have `lib.or`.
|
||||
"--ignore=or-identifier"
|
||||
];
|
||||
excludes = [
|
||||
# Auto-generated; violates sema-extra-with
|
||||
# Can only sensibly be removed when --auto-fix supports multiple fixes at once:
|
||||
# https://github.com/inclyc/nixf-diagnose/issues/13
|
||||
"pkgs/servers/home-assistant/component-packages.nix"
|
||||
# https://github.com/nix-community/nixd/issues/708
|
||||
"nixos/maintainers/scripts/azure-new/examples/basic/system.nix"
|
||||
];
|
||||
};
|
||||
programs.nixfmt.enable = true;
|
||||
|
||||
settings.formatter.editorconfig-checker = {
|
||||
command = "${pkgs.lib.getExe pkgs.editorconfig-checker}";
|
||||
options = [
|
||||
"-disable-indent-size"
|
||||
# TODO: Remove this once this upstream issue is fixed:
|
||||
# https://github.com/editorconfig-checker/editorconfig-checker/issues/505
|
||||
"-disable-charset"
|
||||
];
|
||||
options = [ "-disable-indent-size" ];
|
||||
includes = [ "*" ];
|
||||
priority = 1;
|
||||
};
|
||||
|
||||
# TODO: Upstream this into treefmt-nix eventually:
|
||||
# https://github.com/numtide/treefmt-nix/issues/387
|
||||
settings.formatter.markdown-code-runner = {
|
||||
command = pkgs.lib.getExe pkgs.markdown-code-runner;
|
||||
options =
|
||||
let
|
||||
config = pkgs.writers.writeTOML "markdown-code-runner-config" {
|
||||
presets.nixfmt = {
|
||||
language = "nix";
|
||||
command = [ (pkgs.lib.getExe pkgs.nixfmt) ];
|
||||
};
|
||||
};
|
||||
in
|
||||
[ "--config=${config}" ];
|
||||
includes = [ "*.md" ];
|
||||
};
|
||||
|
||||
programs.zizmor.enable = true;
|
||||
};
|
||||
fs = pkgs.lib.fileset;
|
||||
nixFilesSrc = fs.toSource {
|
||||
@@ -157,41 +73,21 @@ let
|
||||
};
|
||||
|
||||
in
|
||||
rec {
|
||||
{
|
||||
inherit pkgs fmt;
|
||||
requestReviews = pkgs.callPackage ./request-reviews { };
|
||||
codeownersValidator = pkgs.callPackage ./codeowners-validator { };
|
||||
|
||||
# FIXME(lf-): it might be useful to test other Nix implementations
|
||||
# (nixVersions.stable and Lix) here somehow at some point to ensure we don't
|
||||
# have eval divergence.
|
||||
eval = pkgs.callPackage ./eval {
|
||||
nix = pkgs.lib.getAttrFromPath (pkgs.lib.splitString "." nixPath) pkgs;
|
||||
};
|
||||
eval = pkgs.callPackage ./eval { };
|
||||
|
||||
# CI jobs
|
||||
lib-tests = import ../lib/tests/release.nix { inherit pkgs; };
|
||||
manual-nixos = (import ../nixos/release.nix { }).manual.${system} or null;
|
||||
manual-nixpkgs = (import ../doc { inherit pkgs; });
|
||||
manual-nixpkgs-tests = (import ../doc { inherit pkgs; }).tests;
|
||||
nixpkgs-vet = pkgs.callPackage ./nixpkgs-vet.nix {
|
||||
nix = pkgs.nixVersions.latest;
|
||||
};
|
||||
manual-nixpkgs = (import ../pkgs/top-level/release.nix { }).manual;
|
||||
manual-nixpkgs-tests = (import ../pkgs/top-level/release.nix { }).manual.tests;
|
||||
parse = pkgs.lib.recurseIntoAttrs {
|
||||
latest = pkgs.callPackage ./parse.nix { nix = pkgs.nixVersions.latest; };
|
||||
lix = pkgs.callPackage ./parse.nix { nix = pkgs.lix; };
|
||||
nix_2_28 = pkgs.callPackage ./parse.nix { nix = pkgs.nixVersions.nix_2_28; };
|
||||
minimum = pkgs.callPackage ./parse.nix { nix = pkgs.nixVersions.minimum; };
|
||||
};
|
||||
shell = import ../shell.nix { inherit nixpkgs system; };
|
||||
tarball = import ../pkgs/top-level/make-tarball.nix {
|
||||
# Mirrored from top-level release.nix:
|
||||
nixpkgs = {
|
||||
outPath = pkgs.lib.cleanSource ../.;
|
||||
revCount = 1234;
|
||||
shortRev = "abcdef";
|
||||
revision = "0000000000000000000000000000000000000000";
|
||||
};
|
||||
officialRelease = false;
|
||||
inherit pkgs lib-tests;
|
||||
nix = pkgs.nixVersions.latest;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,47 +2,20 @@
|
||||
|
||||
The code in this directory is used by the [eval.yml](../../.github/workflows/eval.yml) GitHub Actions workflow to evaluate the majority of Nixpkgs for all PRs, effectively making sure that when the development branches are processed by Hydra, no evaluation failures are encountered.
|
||||
|
||||
Furthermore it also allows local evaluation using:
|
||||
|
||||
Furthermore it also allows local evaluation using
|
||||
```
|
||||
nix-build ci -A eval.baseline
|
||||
nix-build ci -A eval.full \
|
||||
--max-jobs 4 \
|
||||
--cores 2 \
|
||||
--arg chunkSize 10000 \
|
||||
--arg evalSystems '["x86_64-linux" "aarch64-darwin"]'
|
||||
```
|
||||
|
||||
The two most important arguments are:
|
||||
- `--arg evalSystems`: The set of systems for which `nixpkgs` should be evaluated.
|
||||
Defaults to the four official platforms (`x86_64-linux`, `aarch64-linux`, `x86_64-darwin` and `aarch64-darwin`).
|
||||
Example: `--arg evalSystems '["x86_64-linux" "aarch64-darwin"]'`
|
||||
- `--arg quickTest`: Enables testing a single chunk of the current system only for quick iteration.
|
||||
Example: `--arg quickTest true`
|
||||
- `--max-jobs`: The maximum number of derivations to run at the same time. Only each [supported system](../supportedSystems.json) gets a separate derivation, so it doesn't make sense to set this higher than that number.
|
||||
- `--cores`: The number of cores to use for each job. Recommended to set this to the amount of cores on your system divided by `--max-jobs`.
|
||||
- `chunkSize`: The number of attributes that are evaluated simultaneously on a single core. Lowering this decreases memory usage at the cost of increased evaluation time. If this is too high, there won't be enough chunks to process them in parallel, and will also increase evaluation time.
|
||||
- `evalSystems`: The set of systems for which `nixpkgs` should be evaluated. Defaults to the four official platforms (`x86_64-linux`, `aarch64-linux`, `x86_64-darwin` and `aarch64-darwin`).
|
||||
|
||||
The following arguments can be used to fine-tune performance:
|
||||
- `--max-jobs`: The maximum number of derivations to run at the same time.
|
||||
Only each [supported system](../supportedSystems.json) gets a separate derivation, so it doesn't make sense to set this higher than that number.
|
||||
- `--cores`: The number of cores to use for each job.
|
||||
Recommended to set this to the amount of cores on your system divided by `--max-jobs`.
|
||||
- `--arg chunkSize`: The number of attributes that are evaluated simultaneously on a single core.
|
||||
Lowering this decreases memory usage at the cost of increased evaluation time.
|
||||
If this is too high, there won't be enough chunks to process them in parallel, and will also increase evaluation time.
|
||||
The default is 5000.
|
||||
Example: `--arg chunkSize 10000`
|
||||
A good default is to set `chunkSize` to 10000, which leads to about 3.6GB max memory usage per core, so suitable for fully utilising machines with 4 cores and 16GB memory, 8 cores and 32GB memory or 16 cores and 64GB memory.
|
||||
|
||||
Note that 16GB memory is the recommended minimum, while with less than 8GB memory evaluation time suffers greatly.
|
||||
|
||||
## Local eval with rebuilds / comparison
|
||||
|
||||
To compare two commits locally, first run the following on the baseline commit:
|
||||
|
||||
```
|
||||
nix-build ci -A eval.baseline --out-link baseline
|
||||
```
|
||||
|
||||
Then, on the commit with your changes:
|
||||
|
||||
```
|
||||
nix-build ci -A eval.full --arg baseline ./baseline
|
||||
```
|
||||
|
||||
Keep in mind to otherwise pass the same set of arguments for both commands (`evalSystems`, `quickTest`, `chunkSize`).
|
||||
Running this command will evaluate the difference between the baseline statistics and the ones at the time of running the command.
|
||||
From that difference, it will produce a human-readable report in `$out/step-summary.md`.
|
||||
If no packages were added or removed, then performance statistics will also be generated as part of this report.
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# This expression will, as efficiently as possible, dump a
|
||||
# *superset* of all attrpaths of derivations which might be
|
||||
# part of a release on *any* platform.
|
||||
#
|
||||
# This expression runs single-threaded under all current Nix
|
||||
# implementations, but much faster and with much less memory
|
||||
# used than ./outpaths.nix itself.
|
||||
#
|
||||
# Once you have the list of attrnames you can split it up into
|
||||
# $NUM_CORES batches and evaluate the outpaths separately for each
|
||||
# batch, in parallel.
|
||||
#
|
||||
# To dump the attrnames:
|
||||
#
|
||||
# nix-instantiate --eval --strict --json ci/eval/attrpaths.nix -A names
|
||||
#
|
||||
{
|
||||
lib ? import (path + "/lib"),
|
||||
trace ? false,
|
||||
path ? ./../..,
|
||||
extraNixpkgsConfigJson ? "{}",
|
||||
}:
|
||||
let
|
||||
|
||||
# TODO: Use mapAttrsToListRecursiveCond when this PR lands:
|
||||
# https://github.com/NixOS/nixpkgs/pull/395160
|
||||
justAttrNames =
|
||||
path: value:
|
||||
let
|
||||
result =
|
||||
if path == [ "AAAAAASomeThingsFailToEvaluate" ] || !(lib.isAttrs value) then
|
||||
[ ]
|
||||
else if lib.isDerivation value then
|
||||
[ path ]
|
||||
else
|
||||
lib.pipe value [
|
||||
(lib.mapAttrsToList (
|
||||
name: value:
|
||||
lib.addErrorContext "while evaluating package set attribute path '${
|
||||
lib.showAttrPath (path ++ [ name ])
|
||||
}'" (justAttrNames (path ++ [ name ]) value)
|
||||
))
|
||||
lib.concatLists
|
||||
];
|
||||
in
|
||||
lib.traceIf trace "** ${lib.showAttrPath path}" result;
|
||||
|
||||
outpaths = import ./outpaths.nix {
|
||||
inherit path;
|
||||
extraNixpkgsConfig = builtins.fromJSON extraNixpkgsConfigJson;
|
||||
attrNamesOnly = true;
|
||||
};
|
||||
|
||||
paths = [
|
||||
# Some of the following are based on variants, which are disabled with `attrNamesOnly = true`.
|
||||
# Until these have been removed from release.nix / hydra, we manually add them to the list.
|
||||
[
|
||||
"pkgsLLVM"
|
||||
"stdenv"
|
||||
]
|
||||
[
|
||||
"pkgsArocc"
|
||||
"stdenv"
|
||||
]
|
||||
[
|
||||
"pkgsZig"
|
||||
"stdenv"
|
||||
]
|
||||
[
|
||||
"pkgsStatic"
|
||||
"stdenv"
|
||||
]
|
||||
[
|
||||
"pkgsMusl"
|
||||
"stdenv"
|
||||
]
|
||||
]
|
||||
++ justAttrNames [ ] outpaths;
|
||||
|
||||
names = map lib.showAttrPath paths;
|
||||
|
||||
in
|
||||
{
|
||||
inherit paths names;
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import argparse
|
||||
import json
|
||||
import numpy as np
|
||||
import os
|
||||
import pandas as pd
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from scipy.stats import ttest_rel
|
||||
from tabulate import tabulate
|
||||
from typing import Final
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
# Define metrics of interest (can be expanded as needed)
|
||||
METRIC_PREFIXES = ("nr", "gc")
|
||||
|
||||
def flatten_data(json_data: dict) -> dict:
|
||||
"""
|
||||
@@ -25,293 +22,133 @@ def flatten_data(json_data: dict) -> dict:
|
||||
"gc.heapSize": 5404549120
|
||||
...
|
||||
|
||||
See https://github.com/NixOS/nix/blob/187520ce88c47e2859064704f9320a2d6c97e56e/src/libexpr/eval.cc#L2846
|
||||
for the ultimate source of this data.
|
||||
|
||||
Args:
|
||||
json_data (dict): JSON data containing metrics.
|
||||
Returns:
|
||||
dict: Flattened metrics with keys as metric names.
|
||||
"""
|
||||
flat_metrics = {}
|
||||
for key, value in json_data.items():
|
||||
# This key is duplicated as `time.cpu`; we keep that copy.
|
||||
if key == "cpuTime":
|
||||
continue
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
flat_metrics[key] = value
|
||||
elif isinstance(value, dict):
|
||||
for subkey, subvalue in value.items():
|
||||
assert isinstance(subvalue, (int, float)), subvalue
|
||||
flat_metrics[f"{key}.{subkey}"] = subvalue
|
||||
else:
|
||||
assert isinstance(value, (float, int, dict)), (
|
||||
f"Value `{value}` has unexpected type"
|
||||
)
|
||||
|
||||
for k, v in json_data.items():
|
||||
if isinstance(v, (int, float)):
|
||||
flat_metrics[k] = v
|
||||
elif isinstance(v, dict):
|
||||
for sub_k, sub_v in v.items():
|
||||
flat_metrics[f"{k}.{sub_k}"] = sub_v
|
||||
return flat_metrics
|
||||
|
||||
|
||||
def load_all_metrics(path: Path) -> dict:
|
||||
|
||||
|
||||
def load_all_metrics(directory: Path) -> dict:
|
||||
"""
|
||||
Loads all stats JSON files in the specified file or directory and extracts metrics.
|
||||
These stats JSON files are created by Nix when the `NIX_SHOW_STATS` environment variable is set.
|
||||
|
||||
If the provided path is a directory, it must have the structure $path/$system/$stats,
|
||||
where $path is the provided path, $system is some system from `lib.systems.doubles.*`,
|
||||
and $stats is a stats JSON file.
|
||||
|
||||
If the provided path is a file, it is a stats JSON file.
|
||||
Loads all stats JSON files in the specified directory and extracts metrics.
|
||||
|
||||
Args:
|
||||
path (Path): Directory containing JSON files or a stats JSON file.
|
||||
|
||||
directory (Path): Directory containing JSON files.
|
||||
Returns:
|
||||
dict: Dictionary with filenames as keys and extracted metrics as values.
|
||||
"""
|
||||
metrics = {}
|
||||
if path.is_dir():
|
||||
for system_dir in path.iterdir():
|
||||
assert system_dir.is_dir()
|
||||
for system_dir in directory.iterdir():
|
||||
assert system_dir.is_dir()
|
||||
|
||||
for chunk_output in system_dir.iterdir():
|
||||
for chunk_output in system_dir.iterdir():
|
||||
with chunk_output.open() as f:
|
||||
data = json.load(f)
|
||||
|
||||
metrics[f"{system_dir.name}/${chunk_output.name}"] = flatten_data(data)
|
||||
else:
|
||||
with path.open() as f:
|
||||
metrics[path.name] = flatten_data(json.load(f))
|
||||
|
||||
return metrics
|
||||
|
||||
def dataframe_to_markdown(df: pd.DataFrame) -> str:
|
||||
df = df.sort_values(by=df.columns[0], ascending=True)
|
||||
markdown_lines = []
|
||||
|
||||
def metric_table_name(name: str, explain: bool) -> str:
|
||||
"""
|
||||
Returns the name of the metric, plus a footnote to explain it if needed.
|
||||
"""
|
||||
return f"{name}[^{name}]" if explain else name
|
||||
# Header (get column names and format them)
|
||||
header = '\n| ' + ' | '.join(df.columns) + ' |'
|
||||
markdown_lines.append(header)
|
||||
markdown_lines.append("| - " * (len(df.columns)) + "|") # Separator line
|
||||
|
||||
# Iterate over rows to build Markdown rows
|
||||
for _, row in df.iterrows():
|
||||
# TODO: define threshold for highlighting
|
||||
highlight = False
|
||||
|
||||
fmt = lambda x: f"**{x}**" if highlight else f"{x}"
|
||||
|
||||
# Check for no change and NaN in p_value/t_stat
|
||||
row_values = []
|
||||
for val in row:
|
||||
if isinstance(val, float) and np.isnan(val): # For NaN values in p-value or t-stat
|
||||
row_values.append("-") # Custom symbol for NaN
|
||||
elif isinstance(val, float) and val == 0: # For no change (mean_diff == 0)
|
||||
row_values.append("-") # Custom symbol for no change
|
||||
else:
|
||||
row_values.append(fmt(f"{val:.4f}" if isinstance(val, float) else str(val)))
|
||||
|
||||
markdown_lines.append('| ' + ' | '.join(row_values) + ' |')
|
||||
|
||||
return '\n'.join(markdown_lines)
|
||||
|
||||
|
||||
METRIC_EXPLANATION_FOOTNOTE: Final[str] = """
|
||||
|
||||
[^time.cpu]: Number of seconds of CPU time accounted by the OS to the Nix evaluator process. On UNIX systems, this comes from [`getrusage(RUSAGE_SELF)`](https://man7.org/linux/man-pages/man2/getrusage.2.html).
|
||||
[^time.gc]: Number of seconds of CPU time accounted by the Boehm garbage collector to performing GC.
|
||||
[^time.gcFraction]: What fraction of the total CPU time is accounted towards performing GC.
|
||||
[^gc.cycles]: Number of times garbage collection has been performed.
|
||||
[^gc.heapSize]: Size in bytes of the garbage collector heap.
|
||||
[^gc.totalBytes]: Size in bytes of all allocations in the garbage collector.
|
||||
[^envs.bytes]: Size in bytes of all `Env` objects allocated by the Nix evaluator. These are almost exclusively created by [`nix-env`](https://nix.dev/manual/nix/stable/command-ref/nix-env.html).
|
||||
[^list.bytes]: Size in bytes of all [lists](https://nix.dev/manual/nix/stable/language/syntax.html#list-literal) allocated by the Nix evaluator.
|
||||
[^sets.bytes]: Size in bytes of all [attrsets](https://nix.dev/manual/nix/stable/language/syntax.html#list-literal) allocated by the Nix evaluator.
|
||||
[^symbols.bytes]: Size in bytes of all items in the Nix evaluator symbol table.
|
||||
[^values.bytes]: Size in bytes of all values allocated by the Nix evaluator.
|
||||
[^envs.number]: The count of all `Env` objects allocated.
|
||||
[^nrAvoided]: The number of thunks avoided being created.
|
||||
[^nrExprs]: The number of expression objects ever created.
|
||||
[^nrFunctionCalls]: The number of function calls ever made.
|
||||
[^nrLookups]: The number of lookups into an attrset ever made.
|
||||
[^nrOpUpdateValuesCopied]: The number of attrset values copied in the process of merging attrsets.
|
||||
[^nrOpUpdates]: The number of attrsets merge operations (`//`) performed.
|
||||
[^nrPrimOpCalls]: The number of function calls to primops (Nix builtins) ever made.
|
||||
[^nrThunks]: The number of [thunks](https://nix.dev/manual/nix/latest/language/evaluation.html#laziness) ever made. A thunk is a delayed computation, represented by an expression reference and a closure.
|
||||
[^sets.number]: The number of attrsets ever made.
|
||||
[^symbols.number]: The number of symbols ever added to the symbol table.
|
||||
[^values.number]: The number of values ever made.
|
||||
[^envs.elements]: The number of values contained within an `Env` object.
|
||||
[^list.concats]: The number of list concatenation operations (`++`) performed.
|
||||
[^list.elements]: The number of values contained within a list.
|
||||
[^sets.elements]: The number of values contained within an attrset.
|
||||
[^sizes.Attr]: Size in bytes of the `Attr` type.
|
||||
[^sizes.Bindings]: Size in bytes of the `Bindings` type.
|
||||
[^sizes.Env]: Size in bytes of the `Env` type.
|
||||
[^sizes.Value]: Size in bytes of the `Value` type.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PairwiseTestResults:
|
||||
updated: pd.DataFrame
|
||||
equivalent: pd.DataFrame
|
||||
|
||||
@staticmethod
|
||||
def tabulate(table, headers) -> str:
|
||||
return tabulate(
|
||||
table, headers, tablefmt="github", floatfmt=".4f", missingval="-"
|
||||
)
|
||||
|
||||
def updated_to_markdown(self, explain: bool) -> str:
|
||||
assert not self.updated.empty
|
||||
# Header (get column names and format them)
|
||||
return self.tabulate(
|
||||
headers=[str(column) for column in self.updated.columns],
|
||||
table=[
|
||||
[
|
||||
# The metric acts as its own footnote name
|
||||
metric_table_name(row["metric"], explain),
|
||||
# Check for no change and NaN in p_value/t_stat
|
||||
*[
|
||||
None if np.isnan(val) or np.allclose(val, 0) else val
|
||||
for val in row[1:]
|
||||
],
|
||||
]
|
||||
for _, row in self.updated.iterrows()
|
||||
],
|
||||
)
|
||||
|
||||
def equivalent_to_markdown(self, explain: bool) -> str:
|
||||
assert not self.equivalent.empty
|
||||
return self.tabulate(
|
||||
headers=[str(column) for column in self.equivalent.columns],
|
||||
table=[
|
||||
[
|
||||
# The metric acts as its own footnote name
|
||||
metric_table_name(row["metric"], explain),
|
||||
row["value"],
|
||||
]
|
||||
for _, row in self.equivalent.iterrows()
|
||||
],
|
||||
)
|
||||
|
||||
def to_markdown(self, explain: bool) -> str:
|
||||
result = ""
|
||||
|
||||
if not self.equivalent.empty:
|
||||
result += "## Unchanged values\n\n"
|
||||
result += self.equivalent_to_markdown(explain)
|
||||
|
||||
if not self.updated.empty:
|
||||
result += ("\n\n" if result else "") + "## Updated values\n\n"
|
||||
result += self.updated_to_markdown(explain)
|
||||
|
||||
if explain:
|
||||
result += METRIC_EXPLANATION_FOOTNOTE
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Equivalent:
|
||||
metric: str
|
||||
value: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Comparison:
|
||||
metric: str
|
||||
mean_before: float
|
||||
mean_after: float
|
||||
mean_diff: float
|
||||
mean_pct_change: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ComparisonWithPValue(Comparison):
|
||||
p_value: float
|
||||
t_stat: float
|
||||
|
||||
|
||||
def metric_sort_key(name: str) -> str:
|
||||
if name in ("time.cpu", "time.gc", "time.gcFraction"):
|
||||
return (1, name)
|
||||
elif name.startswith("gc"):
|
||||
return (2, name)
|
||||
elif name.endswith(("bytes", "Bytes")):
|
||||
return (3, name)
|
||||
elif name.startswith("nr") or name.endswith("number"):
|
||||
return (4, name)
|
||||
else:
|
||||
return (5, name)
|
||||
|
||||
|
||||
def perform_pairwise_tests(
|
||||
before_metrics: dict, after_metrics: dict
|
||||
) -> PairwiseTestResults:
|
||||
def perform_pairwise_tests(before_metrics: dict, after_metrics: dict) -> pd.DataFrame:
|
||||
common_files = sorted(set(before_metrics) & set(after_metrics))
|
||||
all_keys = sorted(
|
||||
{
|
||||
metric_keys
|
||||
for file_metrics in before_metrics.values()
|
||||
for metric_keys in file_metrics.keys()
|
||||
},
|
||||
key=metric_sort_key,
|
||||
)
|
||||
all_keys = sorted({ metric_keys for file_metrics in before_metrics.values() for metric_keys in file_metrics.keys() })
|
||||
|
||||
updated = []
|
||||
equivalent = []
|
||||
results = []
|
||||
|
||||
for key in all_keys:
|
||||
before_vals = []
|
||||
after_vals = []
|
||||
before_vals, after_vals = [], []
|
||||
|
||||
for fname in common_files:
|
||||
if key in before_metrics[fname] and key in after_metrics[fname]:
|
||||
before_vals.append(before_metrics[fname][key])
|
||||
after_vals.append(after_metrics[fname][key])
|
||||
|
||||
if len(before_vals) == 0:
|
||||
continue
|
||||
if len(before_vals) >= 2:
|
||||
before_arr = np.array(before_vals)
|
||||
after_arr = np.array(after_vals)
|
||||
|
||||
before_arr = np.array(before_vals)
|
||||
after_arr = np.array(after_vals)
|
||||
|
||||
diff = after_arr - before_arr
|
||||
|
||||
# If there's no difference, add it all to the equivalent output.
|
||||
if np.allclose(diff, 0):
|
||||
equivalent.append(Equivalent(metric=key, value=before_vals[0]))
|
||||
else:
|
||||
diff = after_arr - before_arr
|
||||
pct_change = 100 * diff / before_arr
|
||||
t_stat, p_val = ttest_rel(after_arr, before_arr)
|
||||
|
||||
result = Comparison(
|
||||
metric=key,
|
||||
mean_before=np.mean(before_arr),
|
||||
mean_after=np.mean(after_arr),
|
||||
mean_diff=np.mean(diff),
|
||||
mean_pct_change=np.mean(pct_change),
|
||||
)
|
||||
results.append({
|
||||
"metric": key,
|
||||
"mean_before": np.mean(before_arr),
|
||||
"mean_after": np.mean(after_arr),
|
||||
"mean_diff": np.mean(diff),
|
||||
"mean_%_change": np.mean(pct_change),
|
||||
"p_value": p_val,
|
||||
"t_stat": t_stat
|
||||
})
|
||||
|
||||
# If there are enough values to perform a t-test, do so.
|
||||
if len(before_vals) > 1:
|
||||
t_stat, p_val = ttest_rel(after_arr, before_arr)
|
||||
result = ComparisonWithPValue(
|
||||
**asdict(result), p_value=p_val, t_stat=t_stat
|
||||
)
|
||||
|
||||
updated.append(result)
|
||||
|
||||
return PairwiseTestResults(
|
||||
updated=pd.DataFrame(map(asdict, updated)),
|
||||
equivalent=pd.DataFrame(map(asdict, equivalent)),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Performance comparison of Nix evaluation statistics"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--explain", action="store_true", help="Explain the evaluation statistics"
|
||||
)
|
||||
parser.add_argument(
|
||||
"before", help="File or directory containing baseline (data before)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"after", help="File or directory containing comparison (data after)"
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
before_stats = Path(options.before)
|
||||
after_stats = Path(options.after)
|
||||
|
||||
before_metrics = load_all_metrics(before_stats)
|
||||
after_metrics = load_all_metrics(after_stats)
|
||||
pairwise_test_results = perform_pairwise_tests(before_metrics, after_metrics)
|
||||
markdown_table = pairwise_test_results.to_markdown(explain=options.explain)
|
||||
print(markdown_table)
|
||||
df = pd.DataFrame(results).sort_values("p_value")
|
||||
return df
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
before_dir = os.environ.get("BEFORE_DIR")
|
||||
after_dir = os.environ.get("AFTER_DIR")
|
||||
|
||||
if not before_dir or not after_dir:
|
||||
print("Error: Environment variables 'BEFORE_DIR' and 'AFTER_DIR' must be set.")
|
||||
exit(1)
|
||||
|
||||
before_stats = Path(before_dir) / "stats"
|
||||
after_stats = Path(after_dir) / "stats"
|
||||
|
||||
# This may happen if the pull request target does not include PR#399720 yet.
|
||||
if not before_stats.exists():
|
||||
print("⚠️ Skipping comparison: stats directory is missing in the target commit.")
|
||||
exit(0)
|
||||
|
||||
# This should never happen, but we're exiting gracefully anyways
|
||||
if not after_stats.exists():
|
||||
print("⚠️ Skipping comparison: stats directory missing in current PR evaluation.")
|
||||
exit(0)
|
||||
|
||||
before_metrics = load_all_metrics(before_stats)
|
||||
after_metrics = load_all_metrics(after_stats)
|
||||
df1 = perform_pairwise_tests(before_metrics, after_metrics)
|
||||
markdown_table = dataframe_to_markdown(df1)
|
||||
print(markdown_table)
|
||||
|
||||
@@ -1,71 +1,25 @@
|
||||
{
|
||||
callPackage,
|
||||
lib,
|
||||
jq,
|
||||
runCommand,
|
||||
writeText,
|
||||
python3,
|
||||
stdenvNoCC,
|
||||
makeWrapper,
|
||||
codeowners,
|
||||
...
|
||||
}:
|
||||
let
|
||||
python = python3.withPackages (ps: [
|
||||
ps.numpy
|
||||
ps.pandas
|
||||
ps.scipy
|
||||
ps.tabulate
|
||||
]);
|
||||
|
||||
cmp-stats = stdenvNoCC.mkDerivation {
|
||||
pname = "cmp-stats";
|
||||
version = lib.trivial.release;
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/share/cmp-stats
|
||||
|
||||
cp ${./cmp-stats.py} "$out/share/cmp-stats/cmp-stats.py"
|
||||
|
||||
makeWrapper ${python.interpreter} "$out/bin/cmp-stats" \
|
||||
--add-flags "$out/share/cmp-stats/cmp-stats.py"
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Performance comparison of Nix evaluation statistics";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "cmp-stats";
|
||||
maintainers = with lib.maintainers; [ philiptaron ];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
combinedDir,
|
||||
beforeResultDir,
|
||||
afterResultDir,
|
||||
touchedFilesJson,
|
||||
ownersFile ? ../../OWNERS,
|
||||
byName ? false,
|
||||
}:
|
||||
let
|
||||
# Usually we expect a derivation, but when evaluating in multiple separate steps, we pass
|
||||
# nix store paths around. These need to be turned into (fake) derivations again to track
|
||||
# dependencies properly.
|
||||
# We use two steps for evaluation, because we compare results from two different checkouts.
|
||||
# CI additionalls spreads evaluation across multiple workers.
|
||||
combined = if lib.isDerivation combinedDir then combinedDir else lib.toDerivation combinedDir;
|
||||
|
||||
/*
|
||||
Derivation that computes which packages are affected (added, changed or removed) between two revisions of nixpkgs.
|
||||
Note: "platforms" are "x86_64-linux", "aarch64-darwin", ...
|
||||
|
||||
---
|
||||
Inputs:
|
||||
- beforeDir, afterDir: The evaluation result from before and after the change.
|
||||
- beforeResultDir, afterResultDir: The evaluation result from before and after the change.
|
||||
They can be obtained by running `nix-build -A ci.eval.full` on both revisions.
|
||||
|
||||
---
|
||||
@@ -77,10 +31,10 @@ let
|
||||
changed: ["package2", "package3"],
|
||||
removed: ["package4"],
|
||||
},
|
||||
labels: {
|
||||
"10.rebuild-darwin: 1-10": true,
|
||||
"10.rebuild-linux: 1-10": true
|
||||
},
|
||||
labels: [
|
||||
"10.rebuild-darwin: 1-10",
|
||||
"10.rebuild-linux: 1-10"
|
||||
],
|
||||
rebuildsByKernel: {
|
||||
darwin: ["package1", "package2"],
|
||||
linux: ["package1", "package2", "package3"]
|
||||
@@ -111,6 +65,7 @@ let
|
||||
Example: { name = "python312Packages.numpy"; platform = "x86_64-linux"; }
|
||||
*/
|
||||
inherit (import ./utils.nix { inherit lib; })
|
||||
diff
|
||||
groupByKernel
|
||||
convertToPackagePlatformAttrs
|
||||
groupByPlatform
|
||||
@@ -118,14 +73,25 @@ let
|
||||
getLabels
|
||||
;
|
||||
|
||||
# Attrs
|
||||
# - keys: "added", "changed", "removed" and "rebuilds"
|
||||
# - values: lists of `packagePlatformPath`s
|
||||
diffAttrs = builtins.fromJSON (builtins.readFile "${combined}/combined-diff.json");
|
||||
getAttrs =
|
||||
dir:
|
||||
let
|
||||
raw = builtins.readFile "${dir}/outpaths.json";
|
||||
# The file contains Nix paths; we need to ignore them for evaluation purposes,
|
||||
# else there will be a "is not allowed to refer to a store path" error.
|
||||
data = builtins.unsafeDiscardStringContext raw;
|
||||
in
|
||||
builtins.fromJSON data;
|
||||
beforeAttrs = getAttrs beforeResultDir;
|
||||
afterAttrs = getAttrs afterResultDir;
|
||||
|
||||
changedPackagePlatformAttrs = convertToPackagePlatformAttrs diffAttrs.changed;
|
||||
rebuildsPackagePlatformAttrs = convertToPackagePlatformAttrs diffAttrs.rebuilds;
|
||||
removedPackagePlatformAttrs = convertToPackagePlatformAttrs diffAttrs.removed;
|
||||
# Attrs
|
||||
# - keys: "added", "changed" and "removed"
|
||||
# - values: lists of `packagePlatformPath`s
|
||||
diffAttrs = diff beforeAttrs afterAttrs;
|
||||
|
||||
rebuilds = diffAttrs.added ++ diffAttrs.changed;
|
||||
rebuildsPackagePlatformAttrs = convertToPackagePlatformAttrs rebuilds;
|
||||
|
||||
changed-paths =
|
||||
let
|
||||
@@ -137,65 +103,52 @@ let
|
||||
in
|
||||
writeText "changed-paths.json" (
|
||||
builtins.toJSON {
|
||||
attrdiff = lib.mapAttrs (_: extractPackageNames) { inherit (diffAttrs) added changed removed; };
|
||||
attrdiff = lib.mapAttrs (_: extractPackageNames) diffAttrs;
|
||||
inherit
|
||||
rebuildsByPlatform
|
||||
rebuildsByKernel
|
||||
rebuildCountByKernel
|
||||
;
|
||||
labels =
|
||||
getLabels rebuildCountByKernel
|
||||
# Sets "10.rebuild-*-stdenv" label to whether the "stdenv" attribute was changed.
|
||||
// lib.mapAttrs' (
|
||||
kernel: rebuilds: lib.nameValuePair "10.rebuild-${kernel}-stdenv" (lib.elem "stdenv" rebuilds)
|
||||
) rebuildsByKernel
|
||||
// {
|
||||
"10.rebuild-nixos-tests" =
|
||||
lib.elem "nixosTests.simple" (extractPackageNames diffAttrs.rebuilds)
|
||||
&&
|
||||
# Only set this label when no other label with indication for staging has been set.
|
||||
# This avoids confusion whether to target staging or batch this with kernel updates.
|
||||
lib.last (lib.sort lib.lessThan (lib.attrValues rebuildCountByKernel)) <= 500;
|
||||
};
|
||||
(getLabels rebuildCountByKernel)
|
||||
# Adds "10.rebuild-*-stdenv" label if the "stdenv" attribute was changed
|
||||
++ lib.mapAttrsToList (kernel: _: "10.rebuild-${kernel}-stdenv") (
|
||||
lib.filterAttrs (_: kernelRebuilds: kernelRebuilds ? "stdenv") rebuildsByKernel
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
inherit
|
||||
(callPackage ./maintainers.nix { } {
|
||||
changedattrs = lib.attrNames (lib.groupBy (a: a.name) changedPackagePlatformAttrs);
|
||||
changedpathsjson = touchedFilesJson;
|
||||
removedattrs = lib.attrNames (lib.groupBy (a: a.name) removedPackagePlatformAttrs);
|
||||
})
|
||||
maintainers
|
||||
packages
|
||||
;
|
||||
maintainers = import ./maintainers.nix {
|
||||
changedattrs = lib.attrNames (lib.groupBy (a: a.name) rebuildsPackagePlatformAttrs);
|
||||
changedpathsjson = touchedFilesJson;
|
||||
inherit byName;
|
||||
};
|
||||
in
|
||||
runCommand "compare"
|
||||
{
|
||||
# Don't depend on -dev outputs to reduce closure size for CI.
|
||||
nativeBuildInputs = map lib.getBin [
|
||||
nativeBuildInputs = [
|
||||
jq
|
||||
cmp-stats
|
||||
codeowners
|
||||
(python3.withPackages (
|
||||
ps: with ps; [
|
||||
numpy
|
||||
pandas
|
||||
scipy
|
||||
]
|
||||
))
|
||||
|
||||
];
|
||||
maintainers = builtins.toJSON maintainers;
|
||||
packages = builtins.toJSON packages;
|
||||
passAsFile = [
|
||||
"maintainers"
|
||||
"packages"
|
||||
];
|
||||
passAsFile = [ "maintainers" ];
|
||||
env = {
|
||||
BEFORE_DIR = "${beforeResultDir}";
|
||||
AFTER_DIR = "${afterResultDir}";
|
||||
};
|
||||
}
|
||||
''
|
||||
mkdir $out
|
||||
|
||||
cp ${changed-paths} $out/changed-paths.json
|
||||
|
||||
{
|
||||
echo
|
||||
echo "# Packages"
|
||||
echo
|
||||
jq -r -f ${./generate-step-summary.jq} < ${changed-paths}
|
||||
} >> $out/step-summary.md
|
||||
|
||||
if jq -e '(.attrdiff.added | length == 0) and (.attrdiff.removed | length == 0)' "${changed-paths}" > /dev/null; then
|
||||
# Chunks have changed between revisions
|
||||
@@ -210,7 +163,7 @@ runCommand "compare"
|
||||
echo
|
||||
} >> $out/step-summary.md
|
||||
|
||||
cmp-stats --explain ${combined}/before/stats ${combined}/after/stats >> $out/step-summary.md
|
||||
python3 ${./cmp-stats.py} >> $out/step-summary.md
|
||||
|
||||
else
|
||||
# Package chunks are the same in both revisions
|
||||
@@ -225,43 +178,7 @@ runCommand "compare"
|
||||
} >> $out/step-summary.md
|
||||
fi
|
||||
|
||||
jq -r '.[]' "${touchedFilesJson}" > ./touched-files
|
||||
readarray -t touchedFiles < ./touched-files
|
||||
echo "This PR touches ''${#touchedFiles[@]} files"
|
||||
|
||||
# TODO: Move ci/OWNERS to Nix and produce owners.json instead of owners.txt.
|
||||
touch "$out/owners.txt"
|
||||
for file in "''${touchedFiles[@]}"; do
|
||||
result=$(codeowners --file "${ownersFile}" "$file")
|
||||
|
||||
# Remove the file prefix and trim the surrounding spaces
|
||||
read -r owners <<< "''${result#"$file"}"
|
||||
if [[ "$owners" == "(unowned)" ]]; then
|
||||
echo "File $file is unowned"
|
||||
continue
|
||||
fi
|
||||
echo "File $file is owned by $owners"
|
||||
|
||||
# Split up multiple owners, separated by arbitrary amounts of spaces
|
||||
IFS=" " read -r -a entries <<< "$owners"
|
||||
|
||||
for entry in "''${entries[@]}"; do
|
||||
# GitHub technically also supports Emails as code owners,
|
||||
# but we can't easily support that, so let's not
|
||||
if [[ ! "$entry" =~ @(.*) ]]; then
|
||||
echo -e "\e[33mCodeowner \"$entry\" for file $file is not valid: Must start with \"@\"\e[0m"
|
||||
# Don't fail, because the PR for which this script runs can't fix it,
|
||||
# it has to be fixed in the base branch
|
||||
continue
|
||||
fi
|
||||
# The first regex match is everything after the @
|
||||
entry=''${BASH_REMATCH[1]}
|
||||
|
||||
echo "$entry" >> "$out/owners.txt"
|
||||
done
|
||||
|
||||
done
|
||||
jq -r -f ${./generate-step-summary.jq} < ${changed-paths} >> $out/step-summary.md
|
||||
|
||||
cp "$maintainersPath" "$out/maintainers.json"
|
||||
cp "$packagesPath" "$out/packages.json"
|
||||
''
|
||||
|
||||
@@ -1,79 +1,78 @@
|
||||
{
|
||||
lib,
|
||||
}:
|
||||
# Almost directly vendored from https://github.com/NixOS/ofborg/blob/5a4e743f192fb151915fcbe8789922fa401ecf48/ofborg/src/maintainers.nix
|
||||
{
|
||||
changedattrs,
|
||||
changedpathsjson,
|
||||
removedattrs,
|
||||
byName ? false,
|
||||
}:
|
||||
let
|
||||
pkgs = import ../../.. {
|
||||
system = "x86_64-linux";
|
||||
# We should never try to ping maintainers through package aliases, this can only lead to errors.
|
||||
# One example case is, where an attribute is a throw alias, but then re-introduced in a PR.
|
||||
# This would trigger the throw. By disabling aliases, we can fallback gracefully below.
|
||||
config.allowAliases = false;
|
||||
config = { };
|
||||
overlays = [ ];
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
|
||||
changedpaths = lib.importJSON changedpathsjson;
|
||||
changedpaths = builtins.fromJSON (builtins.readFile changedpathsjson);
|
||||
|
||||
# Extract attributes that changed from by-name paths.
|
||||
# This allows pinging reviewers for pure refactors.
|
||||
touchedattrs = lib.pipe changedpaths [
|
||||
(lib.filter (changed: lib.hasPrefix "pkgs/by-name/" changed))
|
||||
(map (lib.splitString "/"))
|
||||
(map (path: lib.elemAt path 3))
|
||||
lib.unique
|
||||
];
|
||||
anyMatchingFile =
|
||||
filename: builtins.any (changed: lib.strings.hasSuffix changed filename) changedpaths;
|
||||
|
||||
anyMatchingFile = filename: lib.any (lib.hasPrefix filename) changedpaths;
|
||||
anyMatchingFiles = files: builtins.any anyMatchingFile files;
|
||||
|
||||
anyMatchingFiles = files: lib.any anyMatchingFile files;
|
||||
enrichedAttrs = builtins.map (name: {
|
||||
path = lib.splitString "." name;
|
||||
name = name;
|
||||
}) changedattrs;
|
||||
|
||||
sharded = name: "${lib.substring 0 2 name}/${name}";
|
||||
validPackageAttributes = builtins.filter (
|
||||
pkg:
|
||||
if (lib.attrsets.hasAttrByPath pkg.path pkgs) then
|
||||
(
|
||||
let
|
||||
value = lib.attrsets.attrByPath pkg.path null pkgs;
|
||||
in
|
||||
if (builtins.tryEval value).success then
|
||||
if value != null then true else builtins.trace "${pkg.name} exists but is null" false
|
||||
else
|
||||
builtins.trace "Failed to access ${pkg.name} even though it exists" false
|
||||
)
|
||||
else
|
||||
builtins.trace "Failed to locate ${pkg.name}." false
|
||||
) enrichedAttrs;
|
||||
|
||||
attrsWithMaintainers = lib.pipe (changedattrs ++ removedattrs ++ touchedattrs) [
|
||||
# An attribute can appear in changed/removed *and* touched
|
||||
lib.unique
|
||||
(map (
|
||||
name:
|
||||
let
|
||||
path = lib.splitString "." name;
|
||||
# Some packages might be reported as changed on a different platform, but
|
||||
# not even have an attribute on the platform the maintainers are requested on.
|
||||
# Fallback to `null` for these to filter them out below.
|
||||
package = lib.attrByPath path null pkgs;
|
||||
in
|
||||
{
|
||||
inherit name package;
|
||||
# Adds all files in by-name to each package, no matter whether they are discoverable
|
||||
# via meta attributes below. For example, this allows pinging maintainers for
|
||||
# updates to .json files.
|
||||
# TODO: Support by-name package sets.
|
||||
filenames = lib.optional (lib.length path == 1) "pkgs/by-name/${sharded (lib.head path)}/";
|
||||
# TODO: Refactor this so we can ping entire teams instead of the individual members.
|
||||
# Note that this will require keeping track of GH team IDs in "maintainers/teams.nix".
|
||||
maintainers = package.meta.maintainers or [ ];
|
||||
}
|
||||
))
|
||||
# No need to match up packages without maintainers with their files.
|
||||
# This also filters out attributes where `packge = null`, which is the
|
||||
# case for libintl, for example.
|
||||
(lib.filter (pkg: pkg.maintainers != [ ]))
|
||||
];
|
||||
attrsWithPackages = builtins.map (
|
||||
pkg: pkg // { package = lib.attrsets.attrByPath pkg.path null pkgs; }
|
||||
) validPackageAttributes;
|
||||
|
||||
attrsWithMaintainers = builtins.map (
|
||||
pkg:
|
||||
let
|
||||
meta = pkg.package.meta or { };
|
||||
in
|
||||
pkg
|
||||
// {
|
||||
# TODO: Refactor this so we can ping entire teams instead of the individual members.
|
||||
# Note that this will require keeping track of GH team IDs in "maintainers/teams.nix".
|
||||
maintainers = meta.maintainers or [ ];
|
||||
}
|
||||
) attrsWithPackages;
|
||||
|
||||
relevantFilenames =
|
||||
drv:
|
||||
(lib.unique (
|
||||
map (pos: lib.removePrefix "${toString ../../..}/" pos.file) (
|
||||
lib.filter (x: x != null) [
|
||||
(drv.meta.maintainersPosition or null)
|
||||
(drv.meta.teamsPosition or null)
|
||||
(lib.unsafeGetAttrPos "src" drv)
|
||||
(lib.unsafeGetAttrPos "pname" drv)
|
||||
(lib.unsafeGetAttrPos "version" drv)
|
||||
]
|
||||
++ lib.optionals (drv ? meta.position) [
|
||||
(lib.lists.unique (
|
||||
builtins.map (pos: lib.strings.removePrefix (toString ../..) pos.file) (
|
||||
builtins.filter (x: x != null) [
|
||||
((drv.meta or { }).maintainersPosition or null)
|
||||
((drv.meta or { }).teamsPosition or null)
|
||||
(builtins.unsafeGetAttrPos "src" drv)
|
||||
# broken because name is always set by stdenv:
|
||||
# # A hack to make `nix-env -qa` and `nix search` ignore broken packages.
|
||||
# # TODO(@oxij): remove this assert when something like NixOS/nix#1771 gets merged into nix.
|
||||
# name = assert validity.handled; name + lib.optionalString
|
||||
#(builtins.unsafeGetAttrPos "name" drv)
|
||||
(builtins.unsafeGetAttrPos "pname" drv)
|
||||
(builtins.unsafeGetAttrPos "version" drv)
|
||||
|
||||
# Use ".meta.position" for cases when most of the package is
|
||||
# defined in a "common" section and the only place where
|
||||
# reference to the file with a derivation the "pos"
|
||||
@@ -83,20 +82,20 @@ let
|
||||
# "pkgs/tools/package-management/nix/default.nix:155"
|
||||
# We transform it to the following:
|
||||
# { file = "pkgs/tools/package-management/nix/default.nix"; }
|
||||
{ file = lib.head (lib.splitString ":" drv.meta.position); }
|
||||
{ file = lib.head (lib.splitString ":" (drv.meta.position or "")); }
|
||||
]
|
||||
)
|
||||
));
|
||||
|
||||
attrsWithFilenames = map (
|
||||
pkg: pkg // { filenames = pkg.filenames ++ relevantFilenames pkg.package; }
|
||||
attrsWithFilenames = builtins.map (
|
||||
pkg: pkg // { filenames = relevantFilenames pkg.package; }
|
||||
) attrsWithMaintainers;
|
||||
|
||||
attrsWithModifiedFiles = lib.filter (pkg: anyMatchingFiles pkg.filenames) attrsWithFilenames;
|
||||
attrsWithModifiedFiles = builtins.filter (pkg: anyMatchingFiles pkg.filenames) attrsWithFilenames;
|
||||
|
||||
listToPing = lib.concatMap (
|
||||
pkg:
|
||||
map (maintainer: {
|
||||
builtins.map (maintainer: {
|
||||
id = maintainer.githubId;
|
||||
inherit (maintainer) github;
|
||||
packageName = pkg.name;
|
||||
@@ -104,10 +103,10 @@ let
|
||||
}) pkg.maintainers
|
||||
) attrsWithModifiedFiles;
|
||||
|
||||
byMaintainer = lib.groupBy (ping: toString ping.id) listToPing;
|
||||
in
|
||||
{
|
||||
maintainers = lib.mapAttrs (_: lib.catAttrs "packageName") byMaintainer;
|
||||
byMaintainer = lib.groupBy (ping: toString ping.${if byName then "github" else "id"}) listToPing;
|
||||
|
||||
packages = lib.catAttrs "packageName" listToPing;
|
||||
}
|
||||
packagesPerMaintainer = lib.attrsets.mapAttrs (
|
||||
maintainer: packages: builtins.map (pkg: pkg.packageName) packages
|
||||
) byMaintainer;
|
||||
in
|
||||
packagesPerMaintainer
|
||||
|
||||
@@ -22,7 +22,7 @@ rec {
|
||||
splittedPath = lib.splitString "." packagePlatformPath;
|
||||
|
||||
# ["python312Packages" "numpy" "aarch64-linux"] -> ["python312Packages" "numpy"]
|
||||
packagePath = lib.init splittedPath;
|
||||
packagePath = lib.sublist 0 (lib.length splittedPath - 1) splittedPath;
|
||||
|
||||
# "python312Packages.numpy"
|
||||
name = lib.concatStringsSep "." packagePath;
|
||||
@@ -66,7 +66,7 @@ rec {
|
||||
*/
|
||||
convertToPackagePlatformAttrs =
|
||||
packagePlatformPaths:
|
||||
builtins.filter (x: x != null) (map convertToPackagePlatformAttr packagePlatformPaths);
|
||||
builtins.filter (x: x != null) (builtins.map convertToPackagePlatformAttr packagePlatformPaths);
|
||||
|
||||
/*
|
||||
Converts a list of `packagePlatformPath`s directly to a list of (unique) package names
|
||||
@@ -91,7 +91,33 @@ rec {
|
||||
let
|
||||
packagePlatformAttrs = convertToPackagePlatformAttrs (uniqueStrings packagePlatformPaths);
|
||||
in
|
||||
uniqueStrings (map (p: p.name) packagePlatformAttrs);
|
||||
uniqueStrings (builtins.map (p: p.name) packagePlatformAttrs);
|
||||
|
||||
/*
|
||||
Computes the key difference between two attrs
|
||||
|
||||
{
|
||||
added: [ <keys only in the second object> ],
|
||||
removed: [ <keys only in the first object> ],
|
||||
changed: [ <keys with different values between the two objects> ],
|
||||
}
|
||||
*/
|
||||
diff =
|
||||
let
|
||||
filterKeys = cond: attrs: lib.attrNames (lib.filterAttrs cond attrs);
|
||||
in
|
||||
old: new: {
|
||||
added = filterKeys (n: _: !(old ? ${n})) new;
|
||||
removed = filterKeys (n: _: !(new ? ${n})) old;
|
||||
changed = filterKeys (
|
||||
n: v:
|
||||
# Filter out attributes that don't exist anymore
|
||||
(new ? ${n})
|
||||
|
||||
# Filter out attributes that are the same as the new value
|
||||
&& (v != (new.${n}))
|
||||
) old;
|
||||
};
|
||||
|
||||
/*
|
||||
Group a list of `packagePlatformAttr`s by platforms
|
||||
@@ -151,7 +177,7 @@ rec {
|
||||
lib.genAttrs [ "linux" "darwin" ] filterKernel;
|
||||
|
||||
/*
|
||||
Maps an attrs of `kernel - rebuild counts` mappings to an attrs of labels
|
||||
Maps an attrs of `kernel - rebuild counts` mappings to a list of labels
|
||||
|
||||
Turns
|
||||
{
|
||||
@@ -159,37 +185,54 @@ rec {
|
||||
darwin = 1;
|
||||
}
|
||||
into
|
||||
{
|
||||
"10.rebuild-darwin: 1" = true;
|
||||
"10.rebuild-darwin: 1-10" = true;
|
||||
"10.rebuild-darwin: 11-100" = false;
|
||||
# [...]
|
||||
"10.rebuild-darwin: 1" = false;
|
||||
"10.rebuild-darwin: 1-10" = false;
|
||||
"10.rebuild-linux: 11-100" = true;
|
||||
# [...]
|
||||
}
|
||||
[
|
||||
"10.rebuild-darwin: 1"
|
||||
"10.rebuild-darwin: 1-10"
|
||||
"10.rebuild-linux: 11-100"
|
||||
]
|
||||
*/
|
||||
getLabels =
|
||||
rebuildCountByKernel:
|
||||
lib.mergeAttrsList (
|
||||
lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
kernel: rebuildCount:
|
||||
let
|
||||
range = from: to: from <= rebuildCount && (to == null || rebuildCount <= to);
|
||||
numbers =
|
||||
if rebuildCount == 0 then
|
||||
[ "0" ]
|
||||
else if rebuildCount == 1 then
|
||||
[
|
||||
"1"
|
||||
"1-10"
|
||||
]
|
||||
else if rebuildCount <= 10 then
|
||||
[ "1-10" ]
|
||||
else if rebuildCount <= 100 then
|
||||
[ "11-100" ]
|
||||
else if rebuildCount <= 500 then
|
||||
[ "101-500" ]
|
||||
else if rebuildCount <= 1000 then
|
||||
[
|
||||
"501-1000"
|
||||
"501+"
|
||||
]
|
||||
else if rebuildCount <= 2500 then
|
||||
[
|
||||
"1001-2500"
|
||||
"501+"
|
||||
]
|
||||
else if rebuildCount <= 5000 then
|
||||
[
|
||||
"2501-5000"
|
||||
"501+"
|
||||
]
|
||||
else
|
||||
[
|
||||
"5001+"
|
||||
"501+"
|
||||
];
|
||||
in
|
||||
lib.mapAttrs' (number: lib.nameValuePair "10.rebuild-${kernel}: ${number}") {
|
||||
"0" = range 0 0;
|
||||
"1" = range 1 1;
|
||||
"1-10" = range 1 10;
|
||||
"11-100" = range 11 100;
|
||||
"101-500" = range 101 500;
|
||||
"501-1000" = range 501 1000;
|
||||
"501+" = range 501 null;
|
||||
"1001-2500" = range 1001 2500;
|
||||
"2501-5000" = range 2501 5000;
|
||||
"5001+" = range 5001 null;
|
||||
}
|
||||
lib.forEach numbers (number: "10.rebuild-${kernel}: ${number}")
|
||||
) rebuildCountByKernel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
# Evaluates all the accessible paths in nixpkgs.
|
||||
# *This only builds on Linux* since it requires the Linux sandbox isolation to
|
||||
# be able to write in various places while evaluating inside the sandbox.
|
||||
#
|
||||
# This file is used by nixpkgs CI (see .github/workflows/eval.yml) as well as
|
||||
# being used directly as an entry point in Lix's CI (in `flake.nix` in the Lix
|
||||
# repo).
|
||||
#
|
||||
# If you know you are doing a breaking API change, please ping the nixpkgs CI
|
||||
# maintainers and the Lix maintainers (`nix eval -f . lib.teams.lix`).
|
||||
{
|
||||
callPackage,
|
||||
lib,
|
||||
runCommand,
|
||||
writeShellScript,
|
||||
symlinkJoin,
|
||||
busybox,
|
||||
writeText,
|
||||
linkFarm,
|
||||
time,
|
||||
procps,
|
||||
nixVersions,
|
||||
jq,
|
||||
nix,
|
||||
}:
|
||||
|
||||
{
|
||||
# The number of attributes per chunk, see ./README.md for more info.
|
||||
chunkSize ? 5000,
|
||||
# Whether to just evaluate a single chunk for quick testing
|
||||
quickTest ? false,
|
||||
# Don't try to eval packages marked as broken.
|
||||
includeBroken ? false,
|
||||
# Customize the config used to evaluate nixpkgs
|
||||
extraNixpkgsConfig ? { },
|
||||
sta,
|
||||
python3,
|
||||
}:
|
||||
|
||||
let
|
||||
@@ -37,35 +19,29 @@ let
|
||||
root = ../..;
|
||||
fileset = unions (
|
||||
map (lib.path.append ../..) [
|
||||
".version"
|
||||
"ci/supportedSystems.json"
|
||||
"ci/eval/attrpaths.nix"
|
||||
"ci/eval/chunk.nix"
|
||||
"ci/eval/outpaths.nix"
|
||||
"default.nix"
|
||||
"doc"
|
||||
"lib"
|
||||
"maintainers"
|
||||
"modules"
|
||||
"nixos"
|
||||
"pkgs"
|
||||
".version"
|
||||
"ci/supportedSystems.json"
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
nix = nixVersions.nix_2_24;
|
||||
|
||||
supportedSystems = builtins.fromJSON (builtins.readFile ../supportedSystems.json);
|
||||
|
||||
attrpathsSuperset =
|
||||
{
|
||||
evalSystem,
|
||||
}:
|
||||
runCommand "attrpaths-superset.json"
|
||||
{
|
||||
src = nixpkgs;
|
||||
# Don't depend on -dev outputs to reduce closure size for CI.
|
||||
nativeBuildInputs = map lib.getBin [
|
||||
busybox
|
||||
nativeBuildInputs = [
|
||||
nix
|
||||
time
|
||||
];
|
||||
}
|
||||
''
|
||||
@@ -74,13 +50,12 @@ let
|
||||
export GC_INITIAL_HEAP_SIZE=4g
|
||||
command time -f "Attribute eval done [%MKB max resident, %Es elapsed] %C" \
|
||||
nix-instantiate --eval --strict --json --show-trace \
|
||||
"$src/ci/eval/attrpaths.nix" \
|
||||
"$src/pkgs/top-level/release-attrpaths-superset.nix" \
|
||||
-A paths \
|
||||
-I "$src" \
|
||||
--argstr extraNixpkgsConfigJson ${lib.escapeShellArg (builtins.toJSON extraNixpkgsConfig)} \
|
||||
--option restrict-eval true \
|
||||
--option allow-import-from-derivation false \
|
||||
--option eval-system "${evalSystem}" > $out/paths.json
|
||||
--arg enableWarnings false > $out/paths.json
|
||||
'';
|
||||
|
||||
singleSystem =
|
||||
@@ -88,9 +63,15 @@ let
|
||||
# The system to evaluate.
|
||||
# Note that this is intentionally not called `system`,
|
||||
# because `--argstr system` would only be passed to the ci/default.nix file!
|
||||
evalSystem ? builtins.currentSystem,
|
||||
evalSystem,
|
||||
# The path to the `paths.json` file from `attrpathsSuperset`
|
||||
attrpathFile ? "${attrpathsSuperset { inherit evalSystem; }}/paths.json",
|
||||
attrpathFile ? "${attrpathsSuperset}/paths.json",
|
||||
# The number of attributes per chunk, see ./README.md for more info.
|
||||
chunkSize,
|
||||
checkMeta ? true,
|
||||
includeBroken ? true,
|
||||
# Whether to just evaluate a single chunk for quick testing
|
||||
quickTest ? false,
|
||||
}:
|
||||
let
|
||||
singleChunk = writeShellScript "single-chunk" ''
|
||||
@@ -100,30 +81,25 @@ let
|
||||
system=$3
|
||||
outputDir=$4
|
||||
|
||||
# Default is 5, higher values effectively disable the warning.
|
||||
# This randomly breaks Eval.
|
||||
export GC_LARGE_ALLOC_WARN_INTERVAL=1000
|
||||
|
||||
export NIX_SHOW_STATS=1
|
||||
export NIX_SHOW_STATS_PATH="$outputDir/stats/$myChunk"
|
||||
echo "Chunk $myChunk on $system start"
|
||||
set +e
|
||||
command time -o "$outputDir/timestats/$myChunk" \
|
||||
-f "Chunk $myChunk on $system done [%MKB max resident, %Es elapsed] %C" \
|
||||
nix-env -f "${nixpkgs}/ci/eval/chunk.nix" \
|
||||
nix-env -f "${nixpkgs}/pkgs/top-level/release-attrpaths-parallel.nix" \
|
||||
--eval-system "$system" \
|
||||
--option restrict-eval true \
|
||||
--option allow-import-from-derivation false \
|
||||
--query --available \
|
||||
--out-path --json \
|
||||
--meta \
|
||||
--no-name --attr-path --out-path \
|
||||
--show-trace \
|
||||
--arg chunkSize "$chunkSize" \
|
||||
--arg myChunk "$myChunk" \
|
||||
--arg attrpathFile "${attrpathFile}" \
|
||||
--arg systems "[ \"$system\" ]" \
|
||||
--arg checkMeta ${lib.boolToString checkMeta} \
|
||||
--arg includeBroken ${lib.boolToString includeBroken} \
|
||||
--argstr extraNixpkgsConfigJson ${lib.escapeShellArg (builtins.toJSON extraNixpkgsConfig)} \
|
||||
-I ${nixpkgs} \
|
||||
-I ${attrpathFile} \
|
||||
> "$outputDir/result/$myChunk" \
|
||||
@@ -144,17 +120,15 @@ let
|
||||
in
|
||||
runCommand "nixpkgs-eval-${evalSystem}"
|
||||
{
|
||||
# Don't depend on -dev outputs to reduce closure size for CI.
|
||||
nativeBuildInputs = map lib.getBin [
|
||||
busybox
|
||||
jq
|
||||
nativeBuildInputs = [
|
||||
nix
|
||||
time
|
||||
procps
|
||||
jq
|
||||
];
|
||||
env = {
|
||||
inherit evalSystem chunkSize;
|
||||
};
|
||||
__structuredAttrs = true;
|
||||
unsafeDiscardReferences.out = true;
|
||||
}
|
||||
''
|
||||
export NIX_STATE_DIR=$(mktemp -d)
|
||||
@@ -170,20 +144,20 @@ let
|
||||
chunkCount=$(( (attrCount - 1) / chunkSize + 1 ))
|
||||
echo "Chunk count: $chunkCount"
|
||||
|
||||
mkdir -p $out/${evalSystem}
|
||||
mkdir $out
|
||||
|
||||
# Record and print stats on free memory and swap in the background
|
||||
(
|
||||
while true; do
|
||||
availMemory=$(free -m | grep Mem | awk '{print $7}')
|
||||
freeSwap=$(free -m | grep Swap | awk '{print $4}')
|
||||
echo "Available memory: $(( availMemory )) MiB, free swap: $(( freeSwap )) MiB"
|
||||
availMemory=$(free -b | grep Mem | awk '{print $7}')
|
||||
freeSwap=$(free -b | grep Swap | awk '{print $4}')
|
||||
echo "Available memory: $(( availMemory / 1024 / 1024 )) MiB, free swap: $(( freeSwap / 1024 / 1024 )) MiB"
|
||||
|
||||
if [[ ! -f "$out/${evalSystem}/min-avail-memory" ]] || (( availMemory < $(<$out/${evalSystem}/min-avail-memory) )); then
|
||||
echo "$availMemory" > $out/${evalSystem}/min-avail-memory
|
||||
if [[ ! -f "$out/min-avail-memory" ]] || (( availMemory < $(<$out/min-avail-memory) )); then
|
||||
echo "$availMemory" > $out/min-avail-memory
|
||||
fi
|
||||
if [[ ! -f $out/${evalSystem}/min-free-swap ]] || (( freeSwap < $(<$out/${evalSystem}/min-free-swap) )); then
|
||||
echo "$freeSwap" > $out/${evalSystem}/min-free-swap
|
||||
if [[ ! -f $out/min-free-swap ]] || (( availMemory < $(<$out/min-free-swap) )); then
|
||||
echo "$freeSwap" > $out/min-free-swap
|
||||
fi
|
||||
sleep 4
|
||||
done
|
||||
@@ -199,127 +173,136 @@ let
|
||||
mkdir "$chunkOutputDir"/{result,stats,timestats,stderr}
|
||||
|
||||
seq -w 0 "$seq_end" |
|
||||
command time -f "%e" -o "$out/${evalSystem}/total-time" \
|
||||
command time -f "%e" -o "$out/total-time" \
|
||||
xargs -I{} -P"$cores" \
|
||||
${singleChunk} "$chunkSize" {} "$evalSystem" "$chunkOutputDir"
|
||||
|
||||
cp -r "$chunkOutputDir"/stats $out/${evalSystem}/stats-by-chunk
|
||||
cp -r "$chunkOutputDir"/stats $out/stats-by-chunk
|
||||
|
||||
if (( chunkSize * chunkCount != attrCount )); then
|
||||
# A final incomplete chunk would mess up the stats, don't include it
|
||||
rm "$chunkOutputDir"/stats/"$seq_end"
|
||||
fi
|
||||
|
||||
cat "$chunkOutputDir"/result/* | jq -s 'add | map_values(.outputs)' > $out/${evalSystem}/paths.json
|
||||
cat "$chunkOutputDir"/result/* | jq -s 'add | map_values(.meta)' > $out/${evalSystem}/meta.json
|
||||
# Make sure the glob doesn't break when there's no files
|
||||
shopt -s nullglob
|
||||
cat "$chunkOutputDir"/result/* > $out/paths
|
||||
cat "$chunkOutputDir"/stats/* > $out/stats.jsonstream
|
||||
'';
|
||||
|
||||
diff = callPackage ./diff.nix { };
|
||||
|
||||
combine =
|
||||
{
|
||||
diffDir,
|
||||
resultsDir,
|
||||
}:
|
||||
runCommand "combined-eval"
|
||||
runCommand "combined-result"
|
||||
{
|
||||
# Don't depend on -dev outputs to reduce closure size for CI.
|
||||
nativeBuildInputs = map lib.getBin [
|
||||
nativeBuildInputs = [
|
||||
jq
|
||||
sta
|
||||
];
|
||||
}
|
||||
''
|
||||
mkdir -p $out
|
||||
|
||||
# Combine output paths from all systems
|
||||
cat ${diffDir}/*/diff.json | jq -s '
|
||||
reduce .[] as $item ({}; {
|
||||
added: (.added + $item.added),
|
||||
changed: (.changed + $item.changed),
|
||||
removed: (.removed + $item.removed),
|
||||
rebuilds: (.rebuilds + $item.rebuilds)
|
||||
})
|
||||
' > $out/combined-diff.json
|
||||
# Transform output paths to JSON
|
||||
cat ${resultsDir}/*/paths |
|
||||
jq --sort-keys --raw-input --slurp '
|
||||
split("\n") |
|
||||
map(select(. != "") | split(" ") | map(select(. != ""))) |
|
||||
map(
|
||||
{
|
||||
key: .[0],
|
||||
value: .[1] | split(";") | map(split("=") |
|
||||
if length == 1 then
|
||||
{ key: "out", value: .[0] }
|
||||
else
|
||||
{ key: .[0], value: .[1] }
|
||||
end) | from_entries}
|
||||
) | from_entries
|
||||
' > $out/outpaths.json
|
||||
|
||||
# Combine maintainers from all systems
|
||||
cat ${diffDir}/*/maintainers.json | jq -s '
|
||||
add | group_by(.package) | map({
|
||||
key: .[0].package,
|
||||
value: map(.maintainers) | flatten | unique
|
||||
}) | from_entries
|
||||
' > $out/maintainers.json
|
||||
# Computes min, mean, error, etc. for a list of values and outputs a JSON from that
|
||||
statistics() {
|
||||
local stat=$1
|
||||
sta --transpose |
|
||||
jq --raw-input --argjson stat "$stat" -n '
|
||||
[
|
||||
inputs |
|
||||
split("\t") |
|
||||
{ key: .[0], value: (.[1] | fromjson) }
|
||||
] |
|
||||
from_entries |
|
||||
{
|
||||
key: ($stat | join(".")),
|
||||
value: .
|
||||
}'
|
||||
}
|
||||
|
||||
mkdir -p $out/before/stats
|
||||
for d in ${diffDir}/before/*; do
|
||||
cp -r "$d"/stats-by-chunk $out/before/stats/$(basename "$d")
|
||||
done
|
||||
# Gets all available number stats (without .sizes because those are constant and not interesting)
|
||||
readarray -t stats < <(jq -cs '.[0] | del(.sizes) | paths(type == "number")' ${resultsDir}/*/stats.jsonstream)
|
||||
|
||||
mkdir -p $out/after/stats
|
||||
for d in ${diffDir}/after/*; do
|
||||
cp -r "$d"/stats-by-chunk $out/after/stats/$(basename "$d")
|
||||
# Combines the statistics from all evaluations
|
||||
{
|
||||
echo "{ \"key\": \"minAvailMemory\", \"value\": $(cat ${resultsDir}/*/min-avail-memory | sta --brief --min) }"
|
||||
echo "{ \"key\": \"minFreeSwap\", \"value\": $(cat ${resultsDir}/*/min-free-swap | sta --brief --min) }"
|
||||
cat ${resultsDir}/*/total-time | statistics '["totalTime"]'
|
||||
for stat in "''${stats[@]}"; do
|
||||
cat ${resultsDir}/*/stats.jsonstream |
|
||||
jq --argjson stat "$stat" 'getpath($stat)' |
|
||||
statistics "$stat"
|
||||
done
|
||||
} |
|
||||
jq -s from_entries > $out/stats.json
|
||||
|
||||
mkdir -p $out/stats
|
||||
|
||||
for d in ${resultsDir}/*; do
|
||||
cp -r "$d"/stats-by-chunk $out/stats/$(basename "$d")
|
||||
done
|
||||
'';
|
||||
|
||||
compare = callPackage ./compare { };
|
||||
|
||||
baseline =
|
||||
{
|
||||
# Whether to evaluate on a specific set of systems, by default all are evaluated
|
||||
evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
|
||||
}:
|
||||
symlinkJoin {
|
||||
name = "nixpkgs-eval-baseline";
|
||||
paths = map (
|
||||
evalSystem:
|
||||
singleSystem {
|
||||
inherit evalSystem;
|
||||
}
|
||||
) evalSystems;
|
||||
};
|
||||
compare = import ./compare {
|
||||
inherit
|
||||
lib
|
||||
jq
|
||||
runCommand
|
||||
writeText
|
||||
supportedSystems
|
||||
python3
|
||||
;
|
||||
};
|
||||
|
||||
full =
|
||||
{
|
||||
# Whether to evaluate on a specific set of systems, by default all are evaluated
|
||||
evalSystems ? if quickTest then [ "x86_64-linux" ] else supportedSystems,
|
||||
baseline,
|
||||
# What files have been touched? Defaults to none; use the expression below to calculate it.
|
||||
# ```
|
||||
# git diff --name-only --merge-base master HEAD \
|
||||
# | jq --raw-input --slurp 'split("\n")[:-1]' > touched-files.json
|
||||
# ```
|
||||
touchedFilesJson ? builtins.toFile "touched-files.json" "[ ]",
|
||||
# The number of attributes per chunk, see ./README.md for more info.
|
||||
chunkSize,
|
||||
quickTest ? false,
|
||||
}:
|
||||
let
|
||||
diffs = symlinkJoin {
|
||||
name = "nixpkgs-eval-diffs";
|
||||
paths = map (
|
||||
evalSystem:
|
||||
diff {
|
||||
inherit evalSystem;
|
||||
beforeDir = baseline;
|
||||
afterDir = singleSystem {
|
||||
inherit evalSystem;
|
||||
};
|
||||
}
|
||||
) evalSystems;
|
||||
};
|
||||
comparisonReport = compare {
|
||||
combinedDir = combine { diffDir = diffs; };
|
||||
inherit touchedFilesJson;
|
||||
};
|
||||
results = linkFarm "results" (
|
||||
map (evalSystem: {
|
||||
name = evalSystem;
|
||||
path = singleSystem {
|
||||
inherit quickTest evalSystem chunkSize;
|
||||
};
|
||||
}) evalSystems
|
||||
);
|
||||
in
|
||||
comparisonReport;
|
||||
combine {
|
||||
resultsDir = results;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
inherit
|
||||
attrpathsSuperset
|
||||
singleSystem
|
||||
diff
|
||||
combine
|
||||
compare
|
||||
# The above three are used by separate VMs in a GitHub workflow,
|
||||
# while the below are intended for testing on a single local machine
|
||||
baseline
|
||||
# while the below is intended for testing on a single local machine
|
||||
full
|
||||
;
|
||||
}
|
||||
|
||||
111
ci/eval/diff.nix
111
ci/eval/diff.nix
@@ -1,111 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
runCommand,
|
||||
writeText,
|
||||
}:
|
||||
|
||||
{
|
||||
beforeDir,
|
||||
afterDir,
|
||||
evalSystem,
|
||||
}:
|
||||
|
||||
let
|
||||
# Usually we expect a derivation, but when evaluating in multiple separate steps, we pass
|
||||
# nix store paths around. These need to be turned into (fake) derivations again to track
|
||||
# dependencies properly.
|
||||
# We use two steps for evaluation, because we compare results from two different checkouts.
|
||||
# CI additionalls spreads evaluation across multiple workers.
|
||||
before = if lib.isDerivation beforeDir then beforeDir else lib.toDerivation beforeDir;
|
||||
after = if lib.isDerivation afterDir then afterDir else lib.toDerivation afterDir;
|
||||
|
||||
/*
|
||||
Computes the key difference between two attrs
|
||||
|
||||
{
|
||||
added: [ <keys only in the second object> ],
|
||||
removed: [ <keys only in the first object> ],
|
||||
changed: [ <keys with different values between the two objects> ],
|
||||
rebuilds: [ <keys in the second object with values not present at all in first object> ],
|
||||
}
|
||||
*/
|
||||
diff =
|
||||
old: new:
|
||||
let
|
||||
filterKeys = cond: attrs: lib.attrNames (lib.filterAttrs cond attrs);
|
||||
oldOutputs = lib.pipe old [
|
||||
(lib.mapAttrsToList (_: lib.attrValues))
|
||||
lib.concatLists
|
||||
(lib.flip lib.genAttrs (_: true))
|
||||
];
|
||||
in
|
||||
{
|
||||
added = filterKeys (n: _: !(old ? ${n})) new;
|
||||
removed = filterKeys (n: _: !(new ? ${n})) old;
|
||||
changed = filterKeys (
|
||||
n: v:
|
||||
# Filter out attributes that don't exist anymore
|
||||
(new ? ${n})
|
||||
|
||||
# Filter out attributes that are the same as the new value
|
||||
&& (v != (new.${n}))
|
||||
) old;
|
||||
# A "rebuild" is every attrpath ...
|
||||
rebuilds = filterKeys (
|
||||
_: pkg:
|
||||
# ... that has at least one output ...
|
||||
lib.any (
|
||||
output:
|
||||
# ... which has not been built in "old" already.
|
||||
!(oldOutputs ? ${output})
|
||||
) (lib.attrValues pkg)
|
||||
) new;
|
||||
};
|
||||
|
||||
getAttrs =
|
||||
dir:
|
||||
let
|
||||
raw = builtins.readFile "${dir}/${evalSystem}/paths.json";
|
||||
# The file contains Nix paths; we need to ignore them for evaluation purposes,
|
||||
# else there will be a "is not allowed to refer to a store path" error.
|
||||
data = builtins.unsafeDiscardStringContext raw;
|
||||
in
|
||||
builtins.fromJSON data;
|
||||
|
||||
beforeAttrs = getAttrs before;
|
||||
afterAttrs = getAttrs after;
|
||||
diffAttrs = diff beforeAttrs afterAttrs;
|
||||
diffJson = writeText "diff.json" (builtins.toJSON diffAttrs);
|
||||
|
||||
# The maintainer list is not diffed, but just taken as is, to provide a map
|
||||
# of maintainers on the target branch. A list of GitHub IDs is sufficient for
|
||||
# all our purposes and reduces size massively.
|
||||
meta = lib.importJSON "${after}/${evalSystem}/meta.json";
|
||||
maintainers = lib.pipe meta [
|
||||
(lib.mapAttrsToList (
|
||||
k: v: {
|
||||
# splits off the platform suffix
|
||||
package = lib.pipe k [
|
||||
(lib.splitString ".")
|
||||
lib.init
|
||||
(lib.concatStringsSep ".")
|
||||
];
|
||||
maintainers = map (m: m.githubId) v.maintainers or [ ];
|
||||
}
|
||||
))
|
||||
# Some paths don't have a platform suffix, those will appear with an empty package here.
|
||||
(lib.filter ({ package, maintainers }: package != "" && maintainers != [ ]))
|
||||
];
|
||||
maintainersJson = writeText "maintainers.json" (builtins.toJSON maintainers);
|
||||
in
|
||||
runCommand "diff" { } ''
|
||||
mkdir -p $out/${evalSystem}
|
||||
|
||||
cp -r --no-preserve=mode ${before} $out/before
|
||||
cp -r --no-preserve=mode ${after} $out/after
|
||||
# JSON files will be processed above explicitly, so avoid copying over
|
||||
# the source files to keep the artifacts smaller.
|
||||
find $out/before $out/after -iname '*.json' -delete
|
||||
cp ${diffJson} $out/${evalSystem}/diff.json
|
||||
cp ${maintainersJson} $out/${evalSystem}/maintainers.json
|
||||
''
|
||||
65
ci/get-merge-commit.sh
Executable file
65
ci/get-merge-commit.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# See ./README.md for docs
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
if (( $# < 2 )); then
|
||||
log "Usage: $0 GITHUB_REPO PR_NUMBER"
|
||||
exit 99
|
||||
fi
|
||||
repo=$1
|
||||
prNumber=$2
|
||||
|
||||
# Retry the API query this many times
|
||||
retryCount=5
|
||||
# Start with 5 seconds, but double every retry
|
||||
retryInterval=5
|
||||
|
||||
while true; do
|
||||
log "Checking whether the pull request can be merged"
|
||||
prInfo=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$repo/pulls/$prNumber")
|
||||
|
||||
# Non-open PRs won't have their mergeability computed no matter what
|
||||
state=$(jq -r .state <<< "$prInfo")
|
||||
if [[ "$state" != open ]]; then
|
||||
log "PR is not open anymore"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mergeable=$(jq -r .mergeable <<< "$prInfo")
|
||||
if [[ "$mergeable" == "null" ]]; then
|
||||
if (( retryCount == 0 )); then
|
||||
log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/"
|
||||
exit 3
|
||||
else
|
||||
(( retryCount -= 1 )) || true
|
||||
|
||||
# null indicates that GitHub is still computing whether it's mergeable
|
||||
# Wait a couple seconds before trying again
|
||||
log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)"
|
||||
sleep "$retryInterval"
|
||||
|
||||
(( retryInterval *= 2 )) || true
|
||||
fi
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$mergeable" == "true" ]]; then
|
||||
log "The PR can be merged"
|
||||
mergedSha="$(jq -r .merge_commit_sha <<< "$prInfo")"
|
||||
echo "mergedSha=$mergedSha"
|
||||
targetSha="$(gh api "/repos/$repo/commits/$mergedSha" --jq '.parents[0].sha')"
|
||||
echo "targetSha=$targetSha"
|
||||
else
|
||||
log "The PR has a merge conflict"
|
||||
exit 2
|
||||
fi
|
||||
@@ -1,3 +0,0 @@
|
||||
[run]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
2
ci/github-script/.gitignore
vendored
2
ci/github-script/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
step-summary.md
|
||||
@@ -1,2 +0,0 @@
|
||||
package-lock-only = true
|
||||
save-exact = true
|
||||
@@ -1,17 +0,0 @@
|
||||
# GitHub specific CI scripts
|
||||
|
||||
This folder contains [`actions/github-script`](https://github.com/actions/github-script)-based JavaScript code.
|
||||
It provides a `nix-shell` environment to run and test these actions locally.
|
||||
|
||||
To run any of the scripts locally:
|
||||
|
||||
- Enter `nix-shell` in `./ci/github-script`.
|
||||
- Ensure `gh` is authenticated.
|
||||
|
||||
## Check commits
|
||||
|
||||
Run `./run commits OWNER REPO PR`, where OWNER is your username or "NixOS", REPO is the name of your fork or "nixpkgs" and PR is the number of the pull request to check.
|
||||
|
||||
## Labeler
|
||||
|
||||
Run `./run labels OWNER REPO`, where OWNER is your username or "NixOS" and REPO the name of your fork or "nixpkgs".
|
||||
@@ -1,713 +0,0 @@
|
||||
module.exports = async ({ github, context, core, dry }) => {
|
||||
const path = require('node:path')
|
||||
const { DefaultArtifactClient } = require('@actions/artifact')
|
||||
const { readFile, writeFile } = require('node:fs/promises')
|
||||
const withRateLimit = require('./withRateLimit.js')
|
||||
const { classify } = require('../supportedBranches.js')
|
||||
const { handleMerge } = require('./merge.js')
|
||||
const { handleReviewers } = require('./reviewers.js')
|
||||
|
||||
const artifactClient = new DefaultArtifactClient()
|
||||
|
||||
async function downloadMaintainerMap(branch) {
|
||||
let run
|
||||
|
||||
const commits = (
|
||||
await github.rest.repos.listCommits({
|
||||
...context.repo,
|
||||
sha: branch,
|
||||
// We look at 10 commits to find a maintainer map, but this is an arbitrary number. The
|
||||
// head commit might not have a map, if the queue was bypassed to merge it. This happens
|
||||
// frequently on staging-esque branches. The branch with the highest chance of getting
|
||||
// 10 consecutive bypassing commits is the stable staging-next branch. Luckily, this
|
||||
// also means that the number of PRs open towards that branch is very low, so falling
|
||||
// back to slightly imprecise maintainer data from master only has a marginal effect.
|
||||
per_page: 10,
|
||||
})
|
||||
).data
|
||||
|
||||
for (const commit of commits) {
|
||||
const run = (
|
||||
await github.rest.actions.listWorkflowRuns({
|
||||
...context.repo,
|
||||
workflow_id: 'merge-group.yml',
|
||||
status: 'success',
|
||||
exclude_pull_requests: true,
|
||||
per_page: 1,
|
||||
head_sha: commit.sha,
|
||||
})
|
||||
).data.workflow_runs[0]
|
||||
if (!run) continue
|
||||
|
||||
const artifact = (
|
||||
await github.rest.actions.listWorkflowRunArtifacts({
|
||||
...context.repo,
|
||||
run_id: run.id,
|
||||
name: 'maintainers',
|
||||
})
|
||||
).data.artifacts[0]
|
||||
if (!artifact || artifact.expired) continue
|
||||
|
||||
await artifactClient.downloadArtifact(artifact.id, {
|
||||
findBy: {
|
||||
repositoryName: context.repo.repo,
|
||||
repositoryOwner: context.repo.owner,
|
||||
token: core.getInput('github-token'),
|
||||
},
|
||||
path: path.resolve(path.join('branches', branch)),
|
||||
expectedHash: artifact.digest,
|
||||
})
|
||||
|
||||
return JSON.parse(
|
||||
await readFile(
|
||||
path.resolve(path.join('branches', branch, 'maintainers.json')),
|
||||
'utf-8',
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// We get here when none of the 10 commits we looked at contained a maintainer map.
|
||||
// For the master branch, we don't have any fallback options, so we error out.
|
||||
// For other branches, we select a suitable fallback below.
|
||||
if (branch === 'master') throw new Error('No maintainer map found.')
|
||||
|
||||
const { stable, version } = classify(branch)
|
||||
|
||||
const release = `release-${version}`
|
||||
if (stable && branch !== release) {
|
||||
// Only fallback to the release branch from *other* stable branches.
|
||||
// Explicitly avoids infinite recursion.
|
||||
return await getMaintainerMap(release)
|
||||
} else {
|
||||
// Falling back to master as last resort.
|
||||
// This can either be the case for unstable staging-esque or wip branches,
|
||||
// or for the primary stable branch (release-XX.YY).
|
||||
return await getMaintainerMap('master')
|
||||
}
|
||||
}
|
||||
|
||||
// Simple cache for maintainer maps to avoid downloading the same artifacts
|
||||
// over and over again. Ultimately returns a promise, so the result must be
|
||||
// awaited for.
|
||||
const maintainerMaps = {}
|
||||
function getMaintainerMap(branch) {
|
||||
if (!maintainerMaps[branch]) {
|
||||
maintainerMaps[branch] = downloadMaintainerMap(branch)
|
||||
}
|
||||
return maintainerMaps[branch]
|
||||
}
|
||||
|
||||
// Caching the list of team members saves API requests when running the bot on the schedule and
|
||||
// processing many PRs at once.
|
||||
const members = {}
|
||||
function getTeamMembers(team_slug) {
|
||||
if (context.eventName === 'pull_request') {
|
||||
// We have no chance of getting a token in the pull_request context with the right
|
||||
// permissions to access the members endpoint below. Thus, we're pretending to have
|
||||
// no members. This is OK; because this is only for the Test workflow, not for
|
||||
// real use.
|
||||
return []
|
||||
}
|
||||
|
||||
if (!members[team_slug]) {
|
||||
members[team_slug] = github.paginate(github.rest.teams.listMembersInOrg, {
|
||||
org: context.repo.owner,
|
||||
team_slug,
|
||||
per_page: 100,
|
||||
})
|
||||
}
|
||||
|
||||
return members[team_slug]
|
||||
}
|
||||
|
||||
// Caching users saves API requests when running the bot on the schedule and processing
|
||||
// many PRs at once. It also helps to encapsulate the special logic we need, because
|
||||
// actions/github doesn't support that endpoint fully, yet.
|
||||
const users = {}
|
||||
function getUser(id) {
|
||||
if (!users[id]) {
|
||||
users[id] = github
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: '/user/{id}',
|
||||
id,
|
||||
})
|
||||
.then((resp) => resp.data)
|
||||
}
|
||||
|
||||
return users[id]
|
||||
}
|
||||
|
||||
async function handlePullRequest({ item, stats, events }) {
|
||||
const log = (k, v) => core.info(`PR #${item.number} - ${k}: ${v}`)
|
||||
|
||||
const pull_number = item.number
|
||||
|
||||
// This API request is important for the merge-conflict label, because it triggers the
|
||||
// creation of a new test merge commit. This is needed to actually determine the state of a PR.
|
||||
//
|
||||
// NOTE (2025-12-15): Temporarily skipping mergeability checks here
|
||||
// on GitHub’s request to measure the impact of the resulting ref
|
||||
// writes on their internal metrics; merge conflicts resulting from
|
||||
// changes to target branches will not have labels applied for the
|
||||
// duration. The label should still be updated on pushes.
|
||||
//
|
||||
// TODO: Restore mergeability checks in some form after a few days
|
||||
// or when we hear back from GitHub.
|
||||
const pull_request = (
|
||||
await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
// Undocumented parameter (as of 2025-12-15), added by GitHub
|
||||
// for us; stability unclear.
|
||||
skip_mergeability_checks: true,
|
||||
})
|
||||
).data
|
||||
|
||||
log('author', pull_request.user?.login)
|
||||
|
||||
const maintainers = await getMaintainerMap(pull_request.base.ref)
|
||||
|
||||
const merge_bot_eligible = await handleMerge({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
log,
|
||||
dry,
|
||||
pull_request,
|
||||
events,
|
||||
maintainers,
|
||||
getTeamMembers,
|
||||
getUser,
|
||||
})
|
||||
|
||||
// Check for any human reviews other than the PR author, GitHub actions and other GitHub apps.
|
||||
// Accounts could be deleted as well, so don't count them.
|
||||
const reviews = (
|
||||
await github.paginate(github.rest.pulls.listReviews, {
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
).filter(
|
||||
(r) =>
|
||||
r.user &&
|
||||
!r.user.login.endsWith('[bot]') &&
|
||||
r.user.type !== 'Bot' &&
|
||||
r.user.id !== pull_request.user?.id,
|
||||
)
|
||||
|
||||
const approvals = new Set(
|
||||
reviews
|
||||
.filter((review) => review.state === 'APPROVED')
|
||||
.map((review) => review.user?.id),
|
||||
)
|
||||
|
||||
// After creation of a Pull Request, `merge_commit_sha` will be null initially:
|
||||
// The very first merge commit will only be calculated after a little while.
|
||||
// To avoid labeling the PR as conflicted before that, we wait a few minutes.
|
||||
// This is intentionally less than the time that Eval takes, so that the label job
|
||||
// running after Eval can indeed label the PR as conflicted if that is the case.
|
||||
const merge_commit_sha_valid =
|
||||
Date.now() - new Date(pull_request.created_at) > 3 * 60 * 1000
|
||||
|
||||
const prLabels = {
|
||||
// We intentionally don't use the mergeable or mergeable_state attributes.
|
||||
// Those have an intermediate state while the test merge commit is created.
|
||||
// This doesn't work well for us, because we might have just triggered another
|
||||
// test merge commit creation by request the pull request via API at the start
|
||||
// of this function.
|
||||
// The attribute merge_commit_sha keeps the old value of null or the hash *until*
|
||||
// the new test merge commit has either successfully been created or failed so.
|
||||
// This essentially means we are updating the merge conflict label in two steps:
|
||||
// On the first pass of the day, we just fetch the pull request, which triggers
|
||||
// the creation. At this stage, the label is likely not updated, yet.
|
||||
// The second pass will then read the result from the first pass and set the label.
|
||||
'2.status: merge conflict':
|
||||
merge_commit_sha_valid && !pull_request.merge_commit_sha,
|
||||
'2.status: merge-bot eligible': merge_bot_eligible,
|
||||
'12.approvals: 1': approvals.size === 1,
|
||||
'12.approvals: 2': approvals.size === 2,
|
||||
'12.approvals: 3+': approvals.size >= 3,
|
||||
'12.first-time contribution': [
|
||||
'NONE',
|
||||
'FIRST_TIMER',
|
||||
'FIRST_TIME_CONTRIBUTOR',
|
||||
].includes(pull_request.author_association),
|
||||
}
|
||||
|
||||
const { id: run_id, conclusion } =
|
||||
(
|
||||
await github.rest.actions.listWorkflowRuns({
|
||||
...context.repo,
|
||||
workflow_id: 'pull-request-target.yml',
|
||||
event: 'pull_request_target',
|
||||
exclude_pull_requests: true,
|
||||
head_sha: pull_request.head.sha,
|
||||
})
|
||||
).data.workflow_runs[0] ??
|
||||
// TODO: Remove this after 2026-02-01, at which point all pr.yml artifacts will have expired.
|
||||
(
|
||||
await github.rest.actions.listWorkflowRuns({
|
||||
...context.repo,
|
||||
// In older PRs, we need pr.yml instead of pull-request-target.yml.
|
||||
workflow_id: 'pr.yml',
|
||||
event: 'pull_request_target',
|
||||
exclude_pull_requests: true,
|
||||
head_sha: pull_request.head.sha,
|
||||
})
|
||||
).data.workflow_runs[0] ??
|
||||
{}
|
||||
|
||||
// Newer PRs might not have run Eval to completion, yet.
|
||||
// Older PRs might not have an eval.yml workflow, yet.
|
||||
// In either case we continue without fetching an artifact on a best-effort basis.
|
||||
log('Last eval run', run_id ?? '<n/a>')
|
||||
|
||||
if (conclusion === 'success') {
|
||||
Object.assign(prLabels, {
|
||||
// We only set this label if the latest eval run was successful, because if it was not, it
|
||||
// *could* have requested reviewers. We will let the PR author fix CI first, before "escalating"
|
||||
// this PR to "needs: reviewer".
|
||||
// Since the first Eval run on a PR always sets rebuild labels, the same PR will be "recently
|
||||
// updated" for the next scheduled run. Thus, this label will still be set within a few minutes
|
||||
// after a PR is created, if required.
|
||||
// Note that a "requested reviewer" disappears once they have given a review, so we check
|
||||
// existing reviews, too.
|
||||
'9.needs: reviewer':
|
||||
!pull_request.draft &&
|
||||
pull_request.requested_reviewers.length === 0 &&
|
||||
reviews.length === 0,
|
||||
})
|
||||
}
|
||||
|
||||
const artifact =
|
||||
run_id &&
|
||||
(
|
||||
await github.rest.actions.listWorkflowRunArtifacts({
|
||||
...context.repo,
|
||||
run_id,
|
||||
name: 'comparison',
|
||||
})
|
||||
).data.artifacts[0]
|
||||
|
||||
// Instead of checking the boolean artifact.expired, we will give us a minute to
|
||||
// actually download the artifact in the next step and avoid that race condition.
|
||||
// Older PRs, where the workflow run was already eval.yml, but the artifact was not
|
||||
// called "comparison", yet, will skip the download.
|
||||
const expired =
|
||||
!artifact ||
|
||||
new Date(artifact?.expires_at ?? 0) < new Date(Date.now() + 60 * 1000)
|
||||
log('Artifact expires at', artifact?.expires_at ?? '<n/a>')
|
||||
if (!expired) {
|
||||
stats.artifacts++
|
||||
|
||||
await artifactClient.downloadArtifact(artifact.id, {
|
||||
findBy: {
|
||||
repositoryName: context.repo.repo,
|
||||
repositoryOwner: context.repo.owner,
|
||||
token: core.getInput('github-token'),
|
||||
},
|
||||
path: path.resolve(pull_number.toString()),
|
||||
expectedHash: artifact.digest,
|
||||
})
|
||||
|
||||
const evalLabels = JSON.parse(
|
||||
await readFile(`${pull_number}/changed-paths.json`, 'utf-8'),
|
||||
).labels
|
||||
|
||||
// TODO: Get "changed packages" information from list of changed by-name files
|
||||
// in addition to just the Eval results, to make this work for these packages
|
||||
// when Eval results have expired as well.
|
||||
let packages
|
||||
try {
|
||||
packages = JSON.parse(
|
||||
await readFile(`${pull_number}/packages.json`, 'utf-8'),
|
||||
)
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e
|
||||
// TODO: Remove this fallback code once all old artifacts without packages.json
|
||||
// have expired. This should be the case in ~ February 2026.
|
||||
packages = Array.from(
|
||||
new Set(
|
||||
Object.values(
|
||||
JSON.parse(
|
||||
await readFile(`${pull_number}/maintainers.json`, 'utf-8'),
|
||||
),
|
||||
).flat(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Object.assign(prLabels, evalLabels, {
|
||||
'11.by: package-maintainer':
|
||||
Boolean(packages.length) &&
|
||||
packages.every((pkg) =>
|
||||
maintainers[pkg]?.includes(pull_request.user.id),
|
||||
),
|
||||
'12.approved-by: package-maintainer': packages.some((pkg) =>
|
||||
maintainers[pkg]?.some((m) => approvals.has(m)),
|
||||
),
|
||||
})
|
||||
|
||||
if (!pull_request.draft) {
|
||||
let owners = []
|
||||
try {
|
||||
// TODO: Create owner map similar to maintainer map.
|
||||
owners = (await readFile(`${pull_number}/owners.txt`, 'utf-8')).split(
|
||||
'\n',
|
||||
)
|
||||
} catch (e) {
|
||||
// Older artifacts don't have the owners.txt, yet.
|
||||
if (e.code !== 'ENOENT') throw e
|
||||
}
|
||||
|
||||
// We set this label earlier already, but the current PR state can be very different
|
||||
// after handleReviewers has requested reviews, so update it in this case to prevent
|
||||
// this label from flip-flopping.
|
||||
prLabels['9.needs: reviewer'] = await handleReviewers({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
log,
|
||||
dry,
|
||||
pull_request,
|
||||
reviews,
|
||||
// TODO: Use maintainer map instead of the artifact.
|
||||
maintainers: Object.keys(
|
||||
JSON.parse(
|
||||
await readFile(`${pull_number}/maintainers.json`, 'utf-8'),
|
||||
),
|
||||
).map((id) => parseInt(id)),
|
||||
owners,
|
||||
getTeamMembers,
|
||||
getUser,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return prLabels
|
||||
}
|
||||
|
||||
// Returns true if the issue was closed. In this case, the labeling does not need to
|
||||
// continue for this issue. Returns false if no action was taken.
|
||||
async function handleAutoClose(item) {
|
||||
const issue_number = item.number
|
||||
|
||||
if (item.labels.some(({ name }) => name === '0.kind: packaging request')) {
|
||||
const body = [
|
||||
'Thank you for your interest in packaging new software in Nixpkgs. Unfortunately, to mitigate the unsustainable growth of unmaintained packages, **Nixpkgs is no longer accepting package requests** via Issues.',
|
||||
'',
|
||||
'As a [volunteer community][community], we are always open to new contributors. If you wish to see this package in Nixpkgs, **we encourage you to [contribute] it yourself**, via a Pull Request. Anyone can [become a package maintainer][maintainers]! You can find language-specific packaging information in the [Nixpkgs Manual][nixpkgs]. Should you need any help, please reach out to the community on [Matrix] or [Discourse].',
|
||||
'',
|
||||
'[community]: https://nixos.org/community',
|
||||
'[contribute]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#quick-start-to-adding-a-package',
|
||||
'[maintainers]: https://github.com/NixOS/nixpkgs/blob/master/maintainers/README.md',
|
||||
'[nixpkgs]: https://nixos.org/manual/nixpkgs/unstable/',
|
||||
'[Matrix]: https://matrix.to/#/#dev:nixos.org',
|
||||
'[Discourse]: https://discourse.nixos.org/c/dev/14',
|
||||
].join('\n')
|
||||
|
||||
core.info(`Issue #${item.number}: auto-closed`)
|
||||
|
||||
if (!dry) {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number,
|
||||
body,
|
||||
})
|
||||
|
||||
await github.rest.issues.update({
|
||||
...context.repo,
|
||||
issue_number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function handle({ item, stats }) {
|
||||
try {
|
||||
const log = (k, v, skip) => {
|
||||
core.info(`#${item.number} - ${k}: ${v}${skip ? ' (skipped)' : ''}`)
|
||||
return skip
|
||||
}
|
||||
|
||||
log('Last updated at', item.updated_at)
|
||||
log('URL', item.html_url)
|
||||
|
||||
const issue_number = item.number
|
||||
|
||||
const itemLabels = {}
|
||||
|
||||
const events = await github.paginate(
|
||||
github.rest.issues.listEventsForTimeline,
|
||||
{
|
||||
...context.repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
},
|
||||
)
|
||||
|
||||
const latest_event_at = new Date(
|
||||
events
|
||||
.filter(({ event }) =>
|
||||
[
|
||||
// These events are hand-picked from:
|
||||
// https://docs.github.com/en/rest/using-the-rest-api/issue-event-types?apiVersion=2022-11-28
|
||||
// Each of those causes a PR/issue to *not* be considered as stale anymore.
|
||||
// Most of these use created_at.
|
||||
'assigned',
|
||||
'commented', // uses updated_at, because that could be > created_at
|
||||
'committed', // uses committer.date
|
||||
...(item.labels.some(({ name }) => name === '5.scope: tracking')
|
||||
? ['cross-referenced']
|
||||
: []),
|
||||
'head_ref_force_pushed',
|
||||
'milestoned',
|
||||
'pinned',
|
||||
'ready_for_review',
|
||||
'renamed',
|
||||
'reopened',
|
||||
'review_dismissed',
|
||||
'review_requested',
|
||||
'reviewed', // uses submitted_at
|
||||
'unlocked',
|
||||
'unmarked_as_duplicate',
|
||||
].includes(event),
|
||||
)
|
||||
.map(
|
||||
({ created_at, updated_at, committer, submitted_at }) =>
|
||||
new Date(
|
||||
updated_at ?? created_at ?? submitted_at ?? committer.date,
|
||||
),
|
||||
)
|
||||
// Reverse sort by date value. The default sort() sorts by string representation, which is bad for dates.
|
||||
.sort((a, b) => b - a)
|
||||
.at(0) ?? item.created_at,
|
||||
)
|
||||
log('latest_event_at', latest_event_at.toISOString())
|
||||
|
||||
const stale_at = new Date(new Date().setDate(new Date().getDate() - 180))
|
||||
const is_stale = latest_event_at < stale_at
|
||||
|
||||
if (item.pull_request || context.payload.pull_request) {
|
||||
// No need to compute merge commits for stale PRs over and over again.
|
||||
// This increases the repo size on GitHub's side unnecessarily and wastes
|
||||
// a lot of API requests, too. Any relevant change will result in the
|
||||
// stale status to change and thus pick up the PR again for labeling.
|
||||
if (!is_stale) {
|
||||
stats.prs++
|
||||
Object.assign(
|
||||
itemLabels,
|
||||
await handlePullRequest({ item, stats, events }),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
stats.issues++
|
||||
if (item.labels.some(({ name }) => name === '4.workflow: auto-close')) {
|
||||
// If this returns true, the issue was closed. In this case we return, to not
|
||||
// label the issue anymore. Most importantly this avoids unlabeling stale issues
|
||||
// which are closed via auto-close.
|
||||
if (await handleAutoClose(item)) return
|
||||
}
|
||||
}
|
||||
|
||||
// Create a map (Label -> Boolean) of all currently set labels.
|
||||
// Each label is set to True and can be disabled later.
|
||||
const before = Object.fromEntries(
|
||||
(
|
||||
await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
...context.repo,
|
||||
issue_number,
|
||||
})
|
||||
).map(({ name }) => [name, true]),
|
||||
)
|
||||
|
||||
Object.assign(itemLabels, {
|
||||
'2.status: stale': !before['1.severity: security'] && is_stale,
|
||||
})
|
||||
|
||||
const after = Object.assign({}, before, itemLabels)
|
||||
|
||||
// No need for an API request, if all labels are the same.
|
||||
const hasChanges = Object.keys(after).some(
|
||||
(name) => (before[name] ?? false) !== after[name],
|
||||
)
|
||||
if (log('Has label changes', hasChanges, !hasChanges)) return
|
||||
|
||||
// Skipping labeling on a pull_request event, because we have no privileges.
|
||||
const labels = Object.entries(after)
|
||||
.filter(([, value]) => value)
|
||||
.map(([name]) => name)
|
||||
if (log('Set labels', labels, dry)) return
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
...context.repo,
|
||||
issue_number,
|
||||
labels,
|
||||
})
|
||||
} catch (cause) {
|
||||
throw new Error(`Labeling #${item.number} failed.`, { cause })
|
||||
}
|
||||
}
|
||||
|
||||
// Controls level of parallelism. Applies to both the number of concurrent requests
|
||||
// as well as the number of concurrent workers going through the list of PRs.
|
||||
// We'll only boost concurrency when we're running many PRs in parallel on a schedule,
|
||||
// but not for single PRs. This avoids things going wild, when we accidentally make
|
||||
// too many API requests on treewides.
|
||||
const maxConcurrent = context.payload.pull_request ? 1 : 20
|
||||
|
||||
await withRateLimit({ github, core, maxConcurrent }, async (stats) => {
|
||||
if (context.payload.pull_request) {
|
||||
await handle({ item: context.payload.pull_request, stats })
|
||||
} else {
|
||||
const lastRun = (
|
||||
await github.rest.actions.listWorkflowRuns({
|
||||
...context.repo,
|
||||
workflow_id: 'bot.yml',
|
||||
event: 'schedule',
|
||||
status: 'success',
|
||||
exclude_pull_requests: true,
|
||||
per_page: 1,
|
||||
})
|
||||
).data.workflow_runs[0]
|
||||
|
||||
const cutoff = new Date(
|
||||
Math.max(
|
||||
// Go back as far as the last successful run of this workflow to make sure
|
||||
// we are not leaving anyone behind on GHA failures.
|
||||
// Defaults to go back 1 hour on the first run.
|
||||
new Date(
|
||||
lastRun?.created_at ?? Date.now() - 1 * 60 * 60 * 1000,
|
||||
).getTime(),
|
||||
// Go back max. 1 day to prevent hitting all API rate limits immediately,
|
||||
// when GH API returns a wrong workflow by accident.
|
||||
Date.now() - 24 * 60 * 60 * 1000,
|
||||
),
|
||||
)
|
||||
core.info(`cutoff timestamp: ${cutoff.toISOString()}`)
|
||||
|
||||
const updatedItems = await github.paginate(
|
||||
github.rest.search.issuesAndPullRequests,
|
||||
{
|
||||
q: [
|
||||
`repo:"${context.repo.owner}/${context.repo.repo}"`,
|
||||
'is:open',
|
||||
`updated:>=${cutoff.toISOString()}`,
|
||||
].join(' AND '),
|
||||
per_page: 100,
|
||||
// TODO: Remove after 2025-11-04, when it becomes the default.
|
||||
advanced_search: true,
|
||||
},
|
||||
)
|
||||
|
||||
let cursor
|
||||
|
||||
// No workflow run available the first time.
|
||||
if (lastRun) {
|
||||
// The cursor to iterate through the full list of issues and pull requests
|
||||
// is passed between jobs as an artifact.
|
||||
const artifact = (
|
||||
await github.rest.actions.listWorkflowRunArtifacts({
|
||||
...context.repo,
|
||||
run_id: lastRun.id,
|
||||
name: 'pagination-cursor',
|
||||
})
|
||||
).data.artifacts[0]
|
||||
|
||||
// If the artifact is not available, the next iteration starts at the beginning.
|
||||
if (artifact && !artifact.expired) {
|
||||
stats.artifacts++
|
||||
|
||||
const { downloadPath } = await artifactClient.downloadArtifact(
|
||||
artifact.id,
|
||||
{
|
||||
findBy: {
|
||||
repositoryName: context.repo.repo,
|
||||
repositoryOwner: context.repo.owner,
|
||||
token: core.getInput('github-token'),
|
||||
},
|
||||
expectedHash: artifact.digest,
|
||||
},
|
||||
)
|
||||
|
||||
cursor = await readFile(path.resolve(downloadPath, 'cursor'), 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
// From GitHub's API docs:
|
||||
// GitHub's REST API considers every pull request an issue, but not every issue is a pull request.
|
||||
// For this reason, "Issues" endpoints may return both issues and pull requests in the response.
|
||||
// You can identify pull requests by the pull_request key.
|
||||
const allItems = await github.rest.issues.listForRepo({
|
||||
...context.repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
per_page: 100,
|
||||
after: cursor,
|
||||
})
|
||||
|
||||
// Regex taken and comment adjusted from:
|
||||
// https://github.com/octokit/plugin-paginate-rest.js/blob/8e5da25f975d2f31dda6b8b588d71f2c768a8df2/src/iterator.ts#L36-L41
|
||||
// `allItems.headers.link` format:
|
||||
// <https://api.github.com/repositories/4542716/issues?page=3&per_page=100&after=Y3Vyc29yOnYyOpLPAAABl8qNnYDOvnSJxA%3D%3D>; rel="next",
|
||||
// <https://api.github.com/repositories/4542716/issues?page=1&per_page=100&before=Y3Vyc29yOnYyOpLPAAABl8xFV9DOvoouJg%3D%3D>; rel="prev"
|
||||
// Sets `next` to undefined if "next" URL is not present or `link` header is not set.
|
||||
const next = ((allItems.headers.link ?? '').match(
|
||||
/<([^<>]+)>;\s*rel="next"/,
|
||||
) ?? [])[1]
|
||||
if (next) {
|
||||
cursor = new URL(next).searchParams.get('after')
|
||||
const uploadPath = path.resolve('cursor')
|
||||
await writeFile(uploadPath, cursor, 'utf-8')
|
||||
if (dry) {
|
||||
core.info(`pagination-cursor: ${cursor} (upload skipped)`)
|
||||
} else {
|
||||
// No stats.artifacts++, because this does not allow passing a custom token.
|
||||
// Thus, the upload will not happen with the app token, but the default github.token.
|
||||
await artifactClient.uploadArtifact(
|
||||
'pagination-cursor',
|
||||
[uploadPath],
|
||||
path.resolve('.'),
|
||||
{
|
||||
retentionDays: 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Some items might be in both search results, so filtering out duplicates as well.
|
||||
const items = []
|
||||
.concat(updatedItems, allItems.data)
|
||||
.filter(
|
||||
(thisItem, idx, arr) =>
|
||||
idx ===
|
||||
arr.findIndex((firstItem) => firstItem.number === thisItem.number),
|
||||
)
|
||||
|
||||
// Instead of handling all items in parallel we set up some workers to handle the queue
|
||||
// with more controlled parallelism. This avoids problems with `pull_request` fetched at
|
||||
// the beginning getting out of date towards the end, because it took the whole job 20
|
||||
// minutes or more to go through 100's of PRs.
|
||||
await Promise.all(
|
||||
Array.from({ length: maxConcurrent }, async () => {
|
||||
while (true) {
|
||||
const item = items.pop()
|
||||
if (!item) break
|
||||
try {
|
||||
await handle({ item, stats })
|
||||
} catch (e) {
|
||||
core.setFailed(`${e.message}\n${e.cause.stack}`)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
module.exports = async ({ github, context, core, dry, cherryPicks }) => {
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { classify } = require('../supportedBranches.js')
|
||||
const withRateLimit = require('./withRateLimit.js')
|
||||
const { dismissReviews, postReview } = require('./reviews.js')
|
||||
|
||||
await withRateLimit({ github, core }, async (stats) => {
|
||||
stats.prs = 1
|
||||
|
||||
const pull_number = context.payload.pull_request.number
|
||||
|
||||
const job_url =
|
||||
context.runId &&
|
||||
(
|
||||
await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
|
||||
...context.repo,
|
||||
run_id: context.runId,
|
||||
per_page: 100,
|
||||
})
|
||||
).find(({ name }) => name.endsWith('Check / commits')).html_url +
|
||||
'?pr=' +
|
||||
pull_number
|
||||
|
||||
async function extract({ sha, commit }) {
|
||||
const noCherryPick = Array.from(
|
||||
commit.message.matchAll(/^Not-cherry-picked-because: (.*)$/gm),
|
||||
).at(0)
|
||||
|
||||
if (noCherryPick)
|
||||
return {
|
||||
sha,
|
||||
commit,
|
||||
severity: 'important',
|
||||
message: `${sha} is not a cherry-pick, because: ${noCherryPick[1]}. Please review this commit manually.`,
|
||||
type: 'no-cherry-pick',
|
||||
}
|
||||
|
||||
// Using the last line with "cherry" + hash, because a chained backport
|
||||
// can result in multiple of those lines. Only the last one counts.
|
||||
const cherry = Array.from(
|
||||
commit.message.matchAll(/cherry.*([0-9a-f]{40})/g),
|
||||
).at(-1)
|
||||
|
||||
if (!cherry)
|
||||
return {
|
||||
sha,
|
||||
commit,
|
||||
severity: 'warning',
|
||||
message: `Couldn't locate the cherry-picked commit's hash in the commit message of ${sha}.`,
|
||||
type: 'no-commit-hash',
|
||||
}
|
||||
|
||||
const original_sha = cherry[1]
|
||||
|
||||
let branches
|
||||
try {
|
||||
branches = (
|
||||
await github.request({
|
||||
// This is an undocumented endpoint to fetch the branches a commit is part of.
|
||||
// There is no equivalent in neither the REST nor the GraphQL API.
|
||||
// The endpoint itself is unlikely to go away, because GitHub uses it to display
|
||||
// the list of branches on the detail page of a commit.
|
||||
url: `https://github.com/${context.repo.owner}/${context.repo.repo}/branch_commits/${original_sha}`,
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
})
|
||||
).data.branches
|
||||
.map(({ branch }) => branch)
|
||||
.filter((branch) => classify(branch).type.includes('development'))
|
||||
} catch (e) {
|
||||
// For some unknown reason a 404 error comes back as 500 without any more details in a GitHub Actions runner.
|
||||
// Ignore these to return a regular error message below.
|
||||
if (![404, 500].includes(e.status)) throw e
|
||||
}
|
||||
if (!branches?.length)
|
||||
return {
|
||||
sha,
|
||||
commit,
|
||||
severity: 'error',
|
||||
message: `${original_sha} given in ${sha} not found in any pickable branch.`,
|
||||
}
|
||||
|
||||
return {
|
||||
sha,
|
||||
commit,
|
||||
original_sha,
|
||||
}
|
||||
}
|
||||
|
||||
function diff({ sha, commit, original_sha }) {
|
||||
const diff = execFileSync('git', [
|
||||
'-C',
|
||||
__dirname,
|
||||
'range-diff',
|
||||
'--no-color',
|
||||
'--ignore-all-space',
|
||||
'--no-notes',
|
||||
// 100 means "any change will be reported"; 0 means "no change will be reported"
|
||||
'--creation-factor=100',
|
||||
`${original_sha}~..${original_sha}`,
|
||||
`${sha}~..${sha}`,
|
||||
])
|
||||
.toString()
|
||||
.split('\n')
|
||||
// First line contains commit SHAs, which we'll print separately.
|
||||
.slice(1)
|
||||
// # The output of `git range-diff` is indented with 4 spaces, but we'll control indentation manually.
|
||||
.map((line) => line.replace(/^ {4}/, ''))
|
||||
|
||||
if (!diff.some((line) => line.match(/^[+-]{2}/)))
|
||||
return {
|
||||
sha,
|
||||
commit,
|
||||
severity: 'info',
|
||||
message: `✔ ${original_sha} is highly similar to ${sha}.`,
|
||||
}
|
||||
|
||||
const colored_diff = execFileSync('git', [
|
||||
'-C',
|
||||
__dirname,
|
||||
'range-diff',
|
||||
'--color',
|
||||
'--no-notes',
|
||||
'--creation-factor=100',
|
||||
`${original_sha}~..${original_sha}`,
|
||||
`${sha}~..${sha}`,
|
||||
]).toString()
|
||||
|
||||
return {
|
||||
sha,
|
||||
commit,
|
||||
diff,
|
||||
colored_diff,
|
||||
severity: 'warning',
|
||||
message: `Difference between ${sha} and original ${original_sha} may warrant inspection.`,
|
||||
type: 'diff',
|
||||
}
|
||||
}
|
||||
|
||||
// For now we short-circuit the list of commits when cherryPicks should not be checked.
|
||||
// This will not run any checks, but still trigger the "dismiss reviews" part below.
|
||||
const commits = !cherryPicks
|
||||
? []
|
||||
: await github.paginate(github.rest.pulls.listCommits, {
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
|
||||
const extracted = await Promise.all(commits.map(extract))
|
||||
|
||||
const fetch = extracted
|
||||
.filter(({ severity }) => !severity)
|
||||
.flatMap(({ sha, original_sha }) => [sha, original_sha])
|
||||
|
||||
if (fetch.length > 0) {
|
||||
// Fetching all commits we need for diff at once is much faster than any other method.
|
||||
execFileSync('git', [
|
||||
'-C',
|
||||
__dirname,
|
||||
'fetch',
|
||||
'--depth=2',
|
||||
'origin',
|
||||
...fetch,
|
||||
])
|
||||
}
|
||||
|
||||
const results = extracted.map((result) =>
|
||||
result.severity ? result : diff(result),
|
||||
)
|
||||
|
||||
// Log all results without truncation, with better highlighting and all whitespace changes to the job log.
|
||||
results.forEach(({ sha, commit, severity, message, colored_diff }) => {
|
||||
core.startGroup(`Commit ${sha}`)
|
||||
core.info(`Author: ${commit.author.name} ${commit.author.email}`)
|
||||
core.info(`Date: ${new Date(commit.author.date)}`)
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
core.error(message)
|
||||
break
|
||||
case 'warning':
|
||||
core.warning(message)
|
||||
break
|
||||
default:
|
||||
core.info(message)
|
||||
}
|
||||
core.endGroup()
|
||||
if (colored_diff) core.info(colored_diff)
|
||||
})
|
||||
|
||||
// Only create step summary below in case of warnings or errors.
|
||||
// Also clean up older reviews, when all checks are good now.
|
||||
// An empty results array will always trigger this condition, which is helpful
|
||||
// to clean up reviews created by the prepare step when on the wrong branch.
|
||||
if (results.every(({ severity }) => severity === 'info')) {
|
||||
await dismissReviews({ github, context, dry })
|
||||
return
|
||||
}
|
||||
|
||||
// In the case of "error" severity, we also fail the job.
|
||||
// Those should be considered blocking and not be dismissable via review.
|
||||
if (results.some(({ severity }) => severity === 'error'))
|
||||
process.exitCode = 1
|
||||
|
||||
core.summary.addRaw(
|
||||
'This report is automatically generated by the `PR / Check / cherry-pick` CI workflow.',
|
||||
true,
|
||||
)
|
||||
core.summary.addEOL()
|
||||
core.summary.addRaw(
|
||||
"Some of the commits in this PR require the author's and reviewer's attention.",
|
||||
true,
|
||||
)
|
||||
core.summary.addEOL()
|
||||
|
||||
if (results.some(({ type }) => type === 'no-commit-hash')) {
|
||||
core.summary.addRaw(
|
||||
'Please follow the [backporting guidelines](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#how-to-backport-pull-requests) and cherry-pick with the `-x` flag.',
|
||||
true,
|
||||
)
|
||||
core.summary.addRaw(
|
||||
'This requires changes to the unstable `master` and `staging` branches first, before backporting them.',
|
||||
true,
|
||||
)
|
||||
core.summary.addEOL()
|
||||
core.summary.addRaw(
|
||||
'Occasionally, commits are not cherry-picked at all, for example when updating minor versions of packages which have already advanced to the next major on unstable.',
|
||||
true,
|
||||
)
|
||||
core.summary.addRaw(
|
||||
'These commits can optionally be marked with a `Not-cherry-picked-because: <reason>` footer.',
|
||||
true,
|
||||
)
|
||||
core.summary.addEOL()
|
||||
}
|
||||
|
||||
if (results.some(({ type }) => type === 'diff')) {
|
||||
core.summary.addRaw(
|
||||
'Sometimes it is not possible to cherry-pick exactly the same patch.',
|
||||
true,
|
||||
)
|
||||
core.summary.addRaw(
|
||||
'This most frequently happens when resolving merge conflicts.',
|
||||
true,
|
||||
)
|
||||
core.summary.addRaw(
|
||||
'The range-diff will help to review the resolution of conflicts.',
|
||||
true,
|
||||
)
|
||||
core.summary.addEOL()
|
||||
}
|
||||
|
||||
core.summary.addRaw(
|
||||
'If you need to merge this PR despite the warnings, please [dismiss](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/dismissing-a-pull-request-review) this review shortly before merging.',
|
||||
true,
|
||||
)
|
||||
|
||||
results.forEach(({ severity, message, diff }) => {
|
||||
if (severity === 'info') return
|
||||
|
||||
// The docs for markdown alerts only show examples with markdown blockquote syntax, like this:
|
||||
// > [!WARNING]
|
||||
// > message
|
||||
// However, our testing shows that this also works with a `<blockquote>` html tag, as long as there
|
||||
// is an empty line:
|
||||
// <blockquote>
|
||||
//
|
||||
// [!WARNING]
|
||||
// message
|
||||
// </blockquote>
|
||||
// Whether this is intended or just an implementation detail is unclear.
|
||||
core.summary.addRaw('<blockquote>')
|
||||
core.summary.addRaw(
|
||||
`\n\n[!${{ important: 'IMPORTANT', warning: 'WARNING', error: 'CAUTION' }[severity]}]`,
|
||||
true,
|
||||
)
|
||||
core.summary.addRaw(`${message}`, true)
|
||||
|
||||
if (diff) {
|
||||
// Limit the output to 10k bytes and remove the last, potentially incomplete line, because GitHub
|
||||
// comments are limited in length. The value of 10k is arbitrary with the assumption, that after
|
||||
// the range-diff becomes a certain size, a reviewer is better off reviewing the regular diff in
|
||||
// GitHub's UI anyway, thus treating the commit as "new" and not cherry-picked.
|
||||
// Note: if multiple commits are close to the limit, this approach could still lead to a comment
|
||||
// that's too long. We think this is unlikely to happen, and so don't deal with it explicitly.
|
||||
const truncated = []
|
||||
let total_length = 0
|
||||
for (line of diff) {
|
||||
total_length += line.length
|
||||
if (total_length > 10000) {
|
||||
truncated.push('', '[...truncated...]')
|
||||
break
|
||||
} else {
|
||||
truncated.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
core.summary.addRaw('<details><summary>Show diff</summary>')
|
||||
core.summary.addRaw('\n\n``````````diff', true)
|
||||
core.summary.addRaw(truncated.join('\n'), true)
|
||||
core.summary.addRaw('``````````', true)
|
||||
core.summary.addRaw('</details>')
|
||||
}
|
||||
|
||||
core.summary.addRaw('</blockquote>')
|
||||
})
|
||||
|
||||
if (job_url)
|
||||
core.summary.addRaw(
|
||||
`\n\n_Hint: The full diffs are also available in the [runner logs](${job_url}) with slightly better highlighting._`,
|
||||
)
|
||||
|
||||
const body = core.summary.stringify()
|
||||
core.summary.write()
|
||||
|
||||
// Posting a review could fail for very long comments. This can only happen with
|
||||
// multiple commits all hitting the truncation limit for the diff. If you ever hit
|
||||
// this case, consider just splitting up those commits into multiple PRs.
|
||||
await postReview({ github, context, core, dry, body })
|
||||
})
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
const excludeTeams = [
|
||||
/^voters.*$/,
|
||||
/^nixpkgs-maintainers$/,
|
||||
/^nixpkgs-committers$/,
|
||||
]
|
||||
|
||||
module.exports = async ({ github, context, core, outFile }) => {
|
||||
const withRateLimit = require('./withRateLimit.js')
|
||||
const { writeFileSync } = require('node:fs')
|
||||
|
||||
const org = context.repo.owner
|
||||
|
||||
const result = {}
|
||||
|
||||
await withRateLimit({ github, core }, async () => {
|
||||
// Turn an Array of users into an Object, mapping user.login -> user.id
|
||||
function makeUserSet(users) {
|
||||
// Sort in-place and build result by mutation
|
||||
users.sort((a, b) => (a.login > b.login ? 1 : -1))
|
||||
|
||||
return users.reduce((acc, user) => {
|
||||
acc[user.login] = user.id
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
// Process a list of teams and append to the result variable
|
||||
async function processTeams(teams) {
|
||||
for (const team of teams) {
|
||||
core.notice(`Processing team ${team.slug}`)
|
||||
if (!excludeTeams.some((regex) => team.slug.match(regex))) {
|
||||
const members = makeUserSet(
|
||||
await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||
org,
|
||||
team_slug: team.slug,
|
||||
role: 'member',
|
||||
}),
|
||||
)
|
||||
const maintainers = makeUserSet(
|
||||
await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||
org,
|
||||
team_slug: team.slug,
|
||||
role: 'maintainer',
|
||||
}),
|
||||
)
|
||||
result[team.slug] = {
|
||||
description: team.description,
|
||||
id: team.id,
|
||||
maintainers,
|
||||
members,
|
||||
name: team.name,
|
||||
}
|
||||
}
|
||||
await processTeams(
|
||||
await github.paginate(github.rest.teams.listChildInOrg, {
|
||||
org,
|
||||
team_slug: team.slug,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const teams = await github.paginate(github.rest.repos.listTeams, {
|
||||
...context.repo,
|
||||
})
|
||||
|
||||
await processTeams(teams)
|
||||
})
|
||||
|
||||
// Sort the teams by team name
|
||||
const sorted = Object.keys(result)
|
||||
.sort()
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = result[key]
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const json = `${JSON.stringify(sorted, null, 2)}\n`
|
||||
|
||||
if (outFile) {
|
||||
writeFileSync(outFile, json)
|
||||
} else {
|
||||
console.log(json)
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// @ts-check
|
||||
const { classify } = require('../supportedBranches.js')
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* github: InstanceType<import('@actions/github/lib/utils').GitHub>,
|
||||
* context: import('@actions/github/lib/context').Context
|
||||
* core: import('@actions/core')
|
||||
* }} CheckCommitMessagesProps
|
||||
*/
|
||||
async function checkCommitMessages({ github, context, core }) {
|
||||
// This check should only be run when we have the pull_request context.
|
||||
const pull_number = context.payload.pull_request?.number
|
||||
if (!pull_number) {
|
||||
core.info('This is not a pull request. Skipping checks.')
|
||||
return
|
||||
}
|
||||
|
||||
const pr = (
|
||||
await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
).data
|
||||
|
||||
const baseBranchType = classify(
|
||||
pr.base.ref.replace(/^refs\/heads\//, ''),
|
||||
).type
|
||||
const headBranchType = classify(
|
||||
pr.head.ref.replace(/^refs\/heads\//, ''),
|
||||
).type
|
||||
|
||||
if (
|
||||
baseBranchType.includes('development') &&
|
||||
headBranchType.includes('development')
|
||||
) {
|
||||
// This matches, for example, PRs from staging-next to master, or vice versa.
|
||||
// Ignore them: we should only care about PRs introducing *new* commits.
|
||||
core.info(
|
||||
'This PR is from one development branch to another. Skipping checks.',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const commits = await github.paginate(github.rest.pulls.listCommits, {
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
|
||||
const failures = new Set()
|
||||
|
||||
for (const commit of commits) {
|
||||
const message = commit.commit.message
|
||||
const firstLine = message.split('\n')[0]
|
||||
|
||||
const logMsgStart = `Commit ${commit.sha}'s message's subject ("${firstLine}")`
|
||||
|
||||
if (!firstLine.includes(':')) {
|
||||
core.error(
|
||||
`${logMsgStart} was detected as not meeting our guidelines because ` +
|
||||
'it does not contain a colon. There are likely other issues as well.',
|
||||
)
|
||||
failures.add(commit.sha)
|
||||
}
|
||||
|
||||
if (firstLine.endsWith('.')) {
|
||||
core.error(
|
||||
`${logMsgStart} was detected as not meeting our guidelines because ` +
|
||||
'it ends in a period. There may be other issues as well.',
|
||||
)
|
||||
failures.add(commit.sha)
|
||||
}
|
||||
|
||||
if (!failures.has(commit.sha)) {
|
||||
core.info(`${logMsgStart} passed our automated checks!`)
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.size !== 0) {
|
||||
core.error(
|
||||
'Please review the guidelines at ' +
|
||||
'https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#commit-conventions, ' +
|
||||
'as well as the applicable area-specific guidelines linked there.',
|
||||
)
|
||||
core.setFailed('Committers: merging is discouraged.')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = checkCommitMessages
|
||||
@@ -1,347 +0,0 @@
|
||||
const { classify } = require('../supportedBranches.js')
|
||||
|
||||
function runChecklist({
|
||||
committers,
|
||||
events,
|
||||
files,
|
||||
pull_request,
|
||||
log,
|
||||
maintainers,
|
||||
user,
|
||||
userIsMaintainer,
|
||||
}) {
|
||||
const allByName = files.every(
|
||||
({ filename }) =>
|
||||
filename.startsWith('pkgs/by-name/') && filename.split('/').length > 4,
|
||||
)
|
||||
|
||||
const packages = files
|
||||
.filter(({ filename }) => filename.startsWith('pkgs/by-name/'))
|
||||
.map(({ filename }) => filename.split('/')[3])
|
||||
.filter(Boolean)
|
||||
|
||||
const eligible = !packages.length
|
||||
? new Set()
|
||||
: packages
|
||||
.map((pkg) => new Set(maintainers[pkg]))
|
||||
.reduce((acc, cur) => acc?.intersection(cur) ?? cur)
|
||||
|
||||
const approvals = new Set(
|
||||
events
|
||||
.filter(
|
||||
({ event, state, commit_id }) =>
|
||||
event === 'reviewed' &&
|
||||
state === 'approved' &&
|
||||
// Only approvals for the current head SHA count, otherwise authors could push
|
||||
// bad code between the approval and the merge.
|
||||
commit_id === pull_request.head.sha,
|
||||
)
|
||||
.map(({ user }) => user?.id)
|
||||
// Some users have been deleted, so filter these out.
|
||||
.filter(Boolean),
|
||||
)
|
||||
|
||||
const checklist = {
|
||||
'PR targets a [development branch](https://github.com/NixOS/nixpkgs/blob/-/ci/README.md#branch-classification).':
|
||||
classify(pull_request.base.ref).type.includes('development'),
|
||||
'PR touches only files of packages in `pkgs/by-name/`.': allByName,
|
||||
'PR is at least one of:': {
|
||||
'Approved by a committer.': committers.intersection(approvals).size > 0,
|
||||
'Backported via label.':
|
||||
pull_request.user.login === 'nixpkgs-ci[bot]' &&
|
||||
pull_request.head.ref.startsWith('backport-'),
|
||||
'Opened by a committer.': committers.has(pull_request.user.id),
|
||||
'Opened by r-ryantm.': pull_request.user.login === 'r-ryantm',
|
||||
},
|
||||
}
|
||||
|
||||
if (user) {
|
||||
checklist[
|
||||
`${user.login} is a member of [@NixOS/nixpkgs-maintainers](https://github.com/orgs/NixOS/teams/nixpkgs-maintainers).`
|
||||
] = userIsMaintainer
|
||||
if (allByName) {
|
||||
// We can only determine the below, if all packages are in by-name, since
|
||||
// we can't reliably relate changed files to packages outside by-name.
|
||||
checklist[`${user.login} is a maintainer of all touched packages.`] =
|
||||
eligible.has(user.id)
|
||||
}
|
||||
} else {
|
||||
// This is only used when no user is passed, i.e. for labeling.
|
||||
checklist['PR has maintainers eligible to merge.'] = eligible.size > 0
|
||||
}
|
||||
|
||||
const result = Object.values(checklist).every((v) =>
|
||||
typeof v === 'boolean' ? v : Object.values(v).some(Boolean),
|
||||
)
|
||||
|
||||
log('checklist', JSON.stringify(checklist))
|
||||
log('eligible', JSON.stringify(Array.from(eligible)))
|
||||
log('result', result)
|
||||
|
||||
return {
|
||||
checklist,
|
||||
eligible,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
// The merge command must be on a separate line and not within codeblocks or html comments.
|
||||
// Codeblocks can have any number of ` larger than 3 to open/close. We only look at code
|
||||
// blocks that are not indented, because the later regex wouldn't match those anyway.
|
||||
function hasMergeCommand(body) {
|
||||
return (body ?? '')
|
||||
.replace(/<!--.*?-->/gms, '')
|
||||
.replace(/(^`{3,})[^`].*?\1/gms, '')
|
||||
.match(/^@NixOS\/nixpkgs-merge-bot merge\s*$/m)
|
||||
}
|
||||
|
||||
async function handleMergeComment({ github, body, node_id, reaction }) {
|
||||
if (!hasMergeCommand(body)) return
|
||||
|
||||
await github.graphql(
|
||||
`mutation($node_id: ID!, $reaction: ReactionContent!) {
|
||||
addReaction(input: {
|
||||
content: $reaction,
|
||||
subjectId: $node_id
|
||||
})
|
||||
{ clientMutationId }
|
||||
}`,
|
||||
{ node_id, reaction },
|
||||
)
|
||||
}
|
||||
|
||||
async function handleMerge({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
log,
|
||||
dry,
|
||||
pull_request,
|
||||
events,
|
||||
maintainers,
|
||||
getTeamMembers,
|
||||
getUser,
|
||||
}) {
|
||||
const pull_number = pull_request.number
|
||||
|
||||
const committers = new Set(
|
||||
(await getTeamMembers('nixpkgs-committers')).map(({ id }) => id),
|
||||
)
|
||||
|
||||
const files = (
|
||||
await github.rest.pulls.listFiles({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
per_page: 100,
|
||||
})
|
||||
).data
|
||||
|
||||
// Early exit to prevent treewides from using up a lot of API requests (and time!) to list
|
||||
// all the files in the pull request. For now, the merge-bot will not work when 100 or more
|
||||
// files are touched in a PR - which should be more than fine.
|
||||
// TODO: Find a more efficient way of downloading all the *names* of the touched files,
|
||||
// including an early exit when the first non-by-name file is found.
|
||||
if (files.length >= 100) return false
|
||||
|
||||
// Only look through comments *after* the latest (force) push.
|
||||
const lastPush = events.findLastIndex(
|
||||
({ event, sha, commit_id }) =>
|
||||
['committed', 'head_ref_force_pushed'].includes(event) &&
|
||||
(sha ?? commit_id) === pull_request.head.sha,
|
||||
)
|
||||
|
||||
const comments = events.slice(lastPush + 1).filter(
|
||||
({ event, body, user, node_id }) =>
|
||||
['commented', 'reviewed'].includes(event) &&
|
||||
hasMergeCommand(body) &&
|
||||
// Ignore comments where the user has been deleted already.
|
||||
user &&
|
||||
// Ignore comments which had already been responded to by the bot.
|
||||
(dry ||
|
||||
!events.some(
|
||||
({ event, body }) =>
|
||||
['commented'].includes(event) &&
|
||||
// We're only testing this hidden reference, but not the author of the comment.
|
||||
// We'll just assume that nobody creates comments with this marker on purpose.
|
||||
// Additionally checking the author is quite annoying for local debugging.
|
||||
body.match(new RegExp(`^<!-- comment: ${node_id} -->$`, 'm')),
|
||||
)),
|
||||
)
|
||||
|
||||
async function merge() {
|
||||
if (dry) {
|
||||
core.info(`Merging #${pull_number}... (dry)`)
|
||||
return 'Merge completed (dry)'
|
||||
}
|
||||
|
||||
// Using GraphQL mutations instead of the REST /merge endpoint, because the latter
|
||||
// doesn't work with Merge Queues. We now have merge queues enabled on all development
|
||||
// branches, so we don't need a fallback for regular merges.
|
||||
try {
|
||||
const resp = await github.graphql(
|
||||
`mutation($node_id: ID!, $sha: GitObjectID) {
|
||||
enqueuePullRequest(input: {
|
||||
expectedHeadOid: $sha,
|
||||
pullRequestId: $node_id
|
||||
})
|
||||
{
|
||||
clientMutationId,
|
||||
mergeQueueEntry { mergeQueue { url } }
|
||||
}
|
||||
}`,
|
||||
{ node_id: pull_request.node_id, sha: pull_request.head.sha },
|
||||
)
|
||||
return [
|
||||
`:heavy_check_mark: [Queued](${resp.enqueuePullRequest.mergeQueueEntry.mergeQueue.url}) for merge (#306934)`,
|
||||
]
|
||||
} catch (e) {
|
||||
log('Enqueing failed', e.response.errors[0].message)
|
||||
}
|
||||
|
||||
// If required status checks are not satisfied, yet, the above will fail. In this case
|
||||
// we can enable auto-merge. We could also only use auto-merge, but this often gets
|
||||
// stuck for no apparent reason.
|
||||
try {
|
||||
await github.graphql(
|
||||
`mutation($node_id: ID!, $sha: GitObjectID) {
|
||||
enablePullRequestAutoMerge(input: {
|
||||
expectedHeadOid: $sha,
|
||||
pullRequestId: $node_id
|
||||
})
|
||||
{ clientMutationId }
|
||||
}`,
|
||||
{ node_id: pull_request.node_id, sha: pull_request.head.sha },
|
||||
)
|
||||
return [
|
||||
`:heavy_check_mark: Enabled Auto Merge (#306934)`,
|
||||
'',
|
||||
'> [!TIP]',
|
||||
'> Sometimes GitHub gets stuck after enabling Auto Merge. In this case, leaving another approval should trigger the merge.',
|
||||
]
|
||||
} catch (e) {
|
||||
log('Auto Merge failed', e.response.errors[0].message)
|
||||
throw new Error(e.response.errors[0].message)
|
||||
}
|
||||
}
|
||||
|
||||
for (const comment of comments) {
|
||||
log('comment', comment.node_id)
|
||||
|
||||
async function react(reaction) {
|
||||
if (dry) {
|
||||
core.info(`Reaction ${reaction} on ${comment.node_id} (dry)`)
|
||||
return
|
||||
}
|
||||
|
||||
await handleMergeComment({
|
||||
github,
|
||||
body: comment.body,
|
||||
node_id: comment.node_id,
|
||||
reaction,
|
||||
})
|
||||
}
|
||||
|
||||
async function isMaintainer(username) {
|
||||
try {
|
||||
return (
|
||||
(
|
||||
await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: context.repo.owner,
|
||||
team_slug: 'nixpkgs-maintainers',
|
||||
username,
|
||||
})
|
||||
).data.state === 'active'
|
||||
)
|
||||
} catch (e) {
|
||||
if (e.status === 404) return false
|
||||
else throw e
|
||||
}
|
||||
}
|
||||
|
||||
const { result, eligible, checklist } = runChecklist({
|
||||
committers,
|
||||
events,
|
||||
files,
|
||||
pull_request,
|
||||
log,
|
||||
maintainers,
|
||||
user: comment.user,
|
||||
userIsMaintainer: await isMaintainer(comment.user.login),
|
||||
})
|
||||
|
||||
const body = [
|
||||
`<!-- comment: ${comment.node_id} -->`,
|
||||
`@${comment.user.login} wants to merge this PR.`,
|
||||
'',
|
||||
'Requirements to merge this PR with `@NixOS/nixpkgs-merge-bot merge`:',
|
||||
...Object.entries(checklist).flatMap(([msg, res]) =>
|
||||
typeof res === 'boolean'
|
||||
? `- :${res ? 'white_check_mark' : 'x'}: ${msg}`
|
||||
: [
|
||||
`- :${Object.values(res).some(Boolean) ? 'white_check_mark' : 'x'}: ${msg}`,
|
||||
...Object.entries(res).map(
|
||||
([msg, res]) =>
|
||||
` - ${res ? ':white_check_mark:' : ':white_large_square:'} ${msg}`,
|
||||
),
|
||||
],
|
||||
),
|
||||
'',
|
||||
]
|
||||
|
||||
if (eligible.size > 0 && !eligible.has(comment.user.id)) {
|
||||
const users = await Promise.all(
|
||||
Array.from(eligible, async (id) => (await getUser(id)).login),
|
||||
)
|
||||
body.push(
|
||||
'> [!TIP]',
|
||||
'> Maintainers eligible to merge are:',
|
||||
...users.map((login) => `> - ${login}`),
|
||||
'',
|
||||
)
|
||||
}
|
||||
|
||||
if (result) {
|
||||
await react('ROCKET')
|
||||
try {
|
||||
body.push(...(await merge()))
|
||||
} catch (e) {
|
||||
// Remove the HTML comment with node_id reference to allow retrying this merge on the next run.
|
||||
body.shift()
|
||||
body.push(`:x: Merge failed with: ${e} (#371492)`)
|
||||
}
|
||||
} else {
|
||||
await react('THUMBS_DOWN')
|
||||
body.push(':x: Pull Request could not be merged (#305350)')
|
||||
}
|
||||
|
||||
if (dry) {
|
||||
core.info(body.join('\n'))
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: pull_number,
|
||||
body: body.join('\n'),
|
||||
})
|
||||
}
|
||||
|
||||
if (result) break
|
||||
}
|
||||
|
||||
const { result } = runChecklist({
|
||||
committers,
|
||||
events,
|
||||
files,
|
||||
pull_request,
|
||||
log,
|
||||
maintainers,
|
||||
})
|
||||
|
||||
// Returns a boolean, which indicates whether the PR is merge-bot eligible in principle.
|
||||
// This is used to set the respective label in bot.js.
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleMerge,
|
||||
handleMergeComment,
|
||||
}
|
||||
1908
ci/github-script/package-lock.json
generated
1908
ci/github-script/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@actions/artifact": "2.3.2",
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"bottleneck": "2.19.5",
|
||||
"commander": "14.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
const { classify } = require('../supportedBranches.js')
|
||||
const { postReview } = require('./reviews.js')
|
||||
|
||||
module.exports = async ({ github, context, core, dry }) => {
|
||||
const pull_number = context.payload.pull_request.number
|
||||
|
||||
for (const retryInterval of [5, 10, 20, 40, 80]) {
|
||||
core.info('Checking whether the pull request can be merged...')
|
||||
const prInfo = (
|
||||
await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
).data
|
||||
|
||||
if (prInfo.state !== 'open') throw new Error('PR is not open anymore.')
|
||||
|
||||
if (prInfo.mergeable == null) {
|
||||
core.info(
|
||||
`GitHub is still computing whether this PR can be merged, waiting ${retryInterval} seconds before trying again...`,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000))
|
||||
continue
|
||||
}
|
||||
|
||||
const { base, head } = prInfo
|
||||
|
||||
const baseClassification = classify(base.ref)
|
||||
core.setOutput('base', baseClassification)
|
||||
console.log('base classification:', baseClassification)
|
||||
|
||||
const headClassification =
|
||||
base.repo.full_name === head.repo.full_name
|
||||
? classify(head.ref)
|
||||
: // PRs from forks are always considered WIP.
|
||||
{ type: ['wip'] }
|
||||
core.setOutput('head', headClassification)
|
||||
console.log('head classification:', headClassification)
|
||||
|
||||
if (baseClassification.type.includes('channel')) {
|
||||
const { stable, version } = baseClassification
|
||||
const correctBranch = stable ? `release-${version}` : 'master'
|
||||
const body = [
|
||||
'The `nixos-*` and `nixpkgs-*` branches are pushed to by the channel release script and should not be merged into directly.',
|
||||
'',
|
||||
`Please target \`${correctBranch}\` instead.`,
|
||||
].join('\n')
|
||||
|
||||
await postReview({ github, context, core, dry, body })
|
||||
|
||||
throw new Error('The PR targets a channel branch.')
|
||||
}
|
||||
|
||||
if (headClassification.type.includes('wip')) {
|
||||
// In the following, we look at the git history to determine the base branch that
|
||||
// this Pull Request branched off of. This is *supposed* to be the branch that it
|
||||
// merges into, but humans make mistakes. Once that happens we want to error out as
|
||||
// early as possible.
|
||||
|
||||
// To determine the "real base", we are looking at the merge-base of primary development
|
||||
// branches and the head of the PR. The merge-base which results in the least number of
|
||||
// commits between that base and head is the real base. We can query for this via GitHub's
|
||||
// REST API. There can be multiple candidates for the real base with the same number of
|
||||
// commits. In this case we pick the "best" candidate by a fixed ordering of branches,
|
||||
// as defined in ci/supportedBranches.js.
|
||||
//
|
||||
// These requests take a while, when comparing against the wrong release - they need
|
||||
// to look at way more than 10k commits in that case. Thus, we try to minimize the
|
||||
// number of requests across releases:
|
||||
// - First, we look at the primary development branches only: master and release-xx.yy.
|
||||
// The branch with the fewest commits gives us the release this PR belongs to.
|
||||
// - We then compare this number against the relevant staging branches for this release
|
||||
// to find the exact branch that this belongs to.
|
||||
|
||||
// All potential development branches
|
||||
const branches = (
|
||||
await github.paginate(github.rest.repos.listBranches, {
|
||||
...context.repo,
|
||||
per_page: 100,
|
||||
})
|
||||
).map(({ name }) => classify(name))
|
||||
|
||||
// All stable primary development branches from latest to oldest.
|
||||
const releases = branches
|
||||
.filter(({ stable, type }) => type.includes('primary') && stable)
|
||||
.sort((a, b) => b.version.localeCompare(a.version))
|
||||
|
||||
async function mergeBase({ branch, order, version }) {
|
||||
const { data } = await github.rest.repos.compareCommitsWithBasehead({
|
||||
...context.repo,
|
||||
basehead: `${branch}...${head.sha}`,
|
||||
// Pagination for this endpoint is about the commits listed, which we don't care about.
|
||||
per_page: 1,
|
||||
// Taking the second page skips the list of files of this changeset.
|
||||
page: 2,
|
||||
})
|
||||
return {
|
||||
branch,
|
||||
order,
|
||||
version,
|
||||
commits: data.total_commits,
|
||||
sha: data.merge_base_commit.sha,
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple branches can be OK at the same time, if the PR was created of a merge-base,
|
||||
// thus storing as array.
|
||||
let candidates = [await mergeBase(classify('master'))]
|
||||
for (const release of releases) {
|
||||
const nextCandidate = await mergeBase(release)
|
||||
if (candidates[0].commits === nextCandidate.commits)
|
||||
candidates.push(nextCandidate)
|
||||
if (candidates[0].commits > nextCandidate.commits)
|
||||
candidates = [nextCandidate]
|
||||
// The number 10000 is principally arbitrary, but the GitHub API returns this value
|
||||
// when the number of commits exceeds it in reality. The difference between two stable releases
|
||||
// is certainly more than 10k commits, thus this works for us as well: If we're targeting
|
||||
// a wrong release, the number *will* be 10000.
|
||||
if (candidates[0].commits < 10000) break
|
||||
}
|
||||
|
||||
core.info(`This PR is for NixOS ${candidates[0].version}.`)
|
||||
|
||||
// Secondary development branches for the selected version only.
|
||||
const secondary = branches.filter(
|
||||
({ branch, type, version }) =>
|
||||
type.includes('secondary') && version === candidates[0].version,
|
||||
)
|
||||
|
||||
// Make sure that we always check the current target as well, even if its a WIP branch.
|
||||
secondary.push(classify(base.ref))
|
||||
|
||||
for (const branch of secondary) {
|
||||
const nextCandidate = await mergeBase(branch)
|
||||
if (candidates[0].commits === nextCandidate.commits)
|
||||
candidates.push(nextCandidate)
|
||||
if (candidates[0].commits > nextCandidate.commits)
|
||||
candidates = [nextCandidate]
|
||||
}
|
||||
|
||||
// If the current branch is among the candidates, this is always better than any other,
|
||||
// thus sorting at -1.
|
||||
candidates = candidates
|
||||
.map((candidate) =>
|
||||
candidate.branch === base.ref
|
||||
? { ...candidate, order: -1 }
|
||||
: candidate,
|
||||
)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
|
||||
const best = candidates.at(0)
|
||||
|
||||
core.info('The base branches for this PR are:')
|
||||
core.info(`github: ${base.ref}`)
|
||||
core.info(
|
||||
`candidates: ${candidates.map(({ branch }) => branch).join(',')}`,
|
||||
)
|
||||
core.info(`best candidate: ${best.branch}`)
|
||||
|
||||
if (best.branch !== base.ref) {
|
||||
const current = await mergeBase(classify(base.ref))
|
||||
const body = [
|
||||
`The PR's base branch is set to \`${current.branch}\`, but ${current.commits === 10000 ? 'at least 10000' : current.commits - best.commits} commits from the \`${best.branch}\` branch are included. Make sure you know the [right base branch for your changes](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#branch-conventions), then:`,
|
||||
`- If the changes should go to the \`${best.branch}\` branch, [change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request).`,
|
||||
`- If the changes should go to the \`${current.branch}\` branch, rebase your PR onto the correct merge-base:`,
|
||||
' ```bash',
|
||||
` # git rebase --onto $(git merge-base upstream/${current.branch} HEAD) $(git merge-base upstream/${best.branch} HEAD)`,
|
||||
` git rebase --onto ${current.sha} ${best.sha}`,
|
||||
` git push --force-with-lease`,
|
||||
' ```',
|
||||
].join('\n')
|
||||
|
||||
await postReview({ github, context, core, dry, body })
|
||||
|
||||
throw new Error(`The PR contains commits from a different base.`)
|
||||
}
|
||||
}
|
||||
|
||||
let mergedSha, targetSha
|
||||
|
||||
if (prInfo.mergeable) {
|
||||
core.info('The PR can be merged.')
|
||||
|
||||
mergedSha = prInfo.merge_commit_sha
|
||||
targetSha = (
|
||||
await github.rest.repos.getCommit({
|
||||
...context.repo,
|
||||
ref: prInfo.merge_commit_sha,
|
||||
})
|
||||
).data.parents[0].sha
|
||||
} else {
|
||||
core.warning('The PR has a merge conflict.')
|
||||
|
||||
mergedSha = head.sha
|
||||
targetSha = (
|
||||
await github.rest.repos.compareCommitsWithBasehead({
|
||||
...context.repo,
|
||||
basehead: `${base.sha}...${head.sha}`,
|
||||
})
|
||||
).data.merge_base_commit.sha
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Checking the commits:\nmerged: ${mergedSha}\ntarget: ${targetSha}`,
|
||||
)
|
||||
core.setOutput('mergedSha', mergedSha)
|
||||
core.setOutput('targetSha', targetSha)
|
||||
|
||||
core.setOutput('systems', require('../supportedSystems.json'))
|
||||
|
||||
const files = (
|
||||
await github.paginate(github.rest.pulls.listFiles, {
|
||||
...context.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
})
|
||||
).map((file) => file.filename)
|
||||
|
||||
const touched = []
|
||||
if (files.includes('ci/pinned.json')) touched.push('pinned')
|
||||
core.setOutput('touched', touched)
|
||||
|
||||
return
|
||||
}
|
||||
throw new Error(
|
||||
"Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com.",
|
||||
)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
async function handleReviewers({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
log,
|
||||
dry,
|
||||
pull_request,
|
||||
reviews,
|
||||
maintainers,
|
||||
owners,
|
||||
getTeamMembers,
|
||||
getUser,
|
||||
}) {
|
||||
const pull_number = pull_request.number
|
||||
|
||||
const requested_reviewers = new Set(
|
||||
pull_request.requested_reviewers.map(({ login }) => login.toLowerCase()),
|
||||
)
|
||||
log(
|
||||
'reviewers - requested_reviewers',
|
||||
Array.from(requested_reviewers).join(', '),
|
||||
)
|
||||
|
||||
const existing_reviewers = new Set(
|
||||
reviews.map(({ user }) => user?.login.toLowerCase()).filter(Boolean),
|
||||
)
|
||||
log(
|
||||
'reviewers - existing_reviewers',
|
||||
Array.from(existing_reviewers).join(', '),
|
||||
)
|
||||
|
||||
// Early sanity check, before we start making any API requests. The list of maintainers
|
||||
// does not have duplicates so the only user to filter out from this list would be the
|
||||
// PR author. Therefore, we check for a limit of 15+1, where 15 is the limit we check
|
||||
// further down again.
|
||||
// This is to protect against huge treewides consuming all our API requests for no
|
||||
// reason.
|
||||
if (maintainers.length > 16) {
|
||||
core.warning('Too many potential reviewers, skipping review requests.')
|
||||
// Return a boolean on whether the "needs: reviewers" label should be set.
|
||||
return existing_reviewers.size === 0 && requested_reviewers.size === 0
|
||||
}
|
||||
|
||||
const users = new Set([
|
||||
...(await Promise.all(
|
||||
maintainers.map(async (id) => (await getUser(id)).login.toLowerCase()),
|
||||
)),
|
||||
...owners
|
||||
.filter((handle) => handle && !handle.includes('/'))
|
||||
.map((handle) => handle.toLowerCase()),
|
||||
])
|
||||
log('reviewers - users', Array.from(users).join(', '))
|
||||
|
||||
const teams = new Set(
|
||||
owners
|
||||
.map((handle) => handle.split('/'))
|
||||
.filter(([org, slug]) => org === context.repo.owner && slug)
|
||||
.map(([, slug]) => slug),
|
||||
)
|
||||
log('reviewers - teams', Array.from(teams).join(', '))
|
||||
|
||||
const team_members = new Set(
|
||||
(await Promise.all(Array.from(teams, getTeamMembers)))
|
||||
.flat(1)
|
||||
.map(({ login }) => login.toLowerCase()),
|
||||
)
|
||||
log('reviewers - team_members', Array.from(team_members).join(', '))
|
||||
|
||||
const new_reviewers = users
|
||||
.union(team_members)
|
||||
// We can't request a review from the author.
|
||||
.difference(new Set([pull_request.user?.login.toLowerCase()]))
|
||||
log('reviewers - new_reviewers', Array.from(new_reviewers).join(', '))
|
||||
|
||||
// Filter users to repository collaborators. If they're not, they can't be requested
|
||||
// for review. In that case, they probably missed their invite to the maintainers team.
|
||||
const reviewers = (
|
||||
await Promise.all(
|
||||
Array.from(new_reviewers, async (username) => {
|
||||
// TODO: Restructure this file to only do the collaborator check for those users
|
||||
// who were not already part of a team. Being a member of a team makes them
|
||||
// collaborators by definition.
|
||||
try {
|
||||
await github.rest.repos.checkCollaborator({
|
||||
...context.repo,
|
||||
username,
|
||||
})
|
||||
return username
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e
|
||||
core.warning(
|
||||
`PR #${pull_number}: User ${username} cannot be requested for review because they don't exist or are not a repository collaborator, ignoring. They probably missed the automated invite to the maintainers team (see <https://github.com/NixOS/nixpkgs/issues/234293>).`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter(Boolean)
|
||||
log('reviewers - reviewers', reviewers.join(', '))
|
||||
|
||||
if (reviewers.length > 15) {
|
||||
core.warning(
|
||||
`Too many reviewers (${reviewers.join(', ')}), skipping review requests.`,
|
||||
)
|
||||
// Return a boolean on whether the "needs: reviewers" label should be set.
|
||||
return existing_reviewers.size === 0 && requested_reviewers.size === 0
|
||||
}
|
||||
|
||||
const non_requested_reviewers = new Set(reviewers)
|
||||
.difference(requested_reviewers)
|
||||
// We don't want to rerequest reviews from people who already reviewed.
|
||||
.difference(existing_reviewers)
|
||||
log(
|
||||
'reviewers - non_requested_reviewers',
|
||||
Array.from(non_requested_reviewers).join(', '),
|
||||
)
|
||||
|
||||
if (non_requested_reviewers.size === 0) {
|
||||
log('Has reviewer changes', 'false (skipped)')
|
||||
} else if (dry) {
|
||||
core.info(
|
||||
`Requesting reviewers for #${pull_number}: ${Array.from(non_requested_reviewers).join(', ')} (dry)`,
|
||||
)
|
||||
} else {
|
||||
// We had tried the "request all reviewers at once" thing in the past, but it didn't work out:
|
||||
// https://github.com/NixOS/nixpkgs/commit/034613f860fcd339bd2c20c8f6bc259a2f9dc034
|
||||
// If we're hitting API errors here again, we'll need to investigate - and possibly reverse
|
||||
// course.
|
||||
await github.rest.pulls.requestReviewers({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
reviewers,
|
||||
})
|
||||
}
|
||||
|
||||
// Return a boolean on whether the "needs: reviewers" label should be set.
|
||||
return (
|
||||
non_requested_reviewers.size === 0 &&
|
||||
existing_reviewers.size === 0 &&
|
||||
requested_reviewers.size === 0
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviewers,
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
async function dismissReviews({ github, context, dry }) {
|
||||
const pull_number = context.payload.pull_request.number
|
||||
|
||||
if (dry) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
(
|
||||
await github.paginate(github.rest.pulls.listReviews, {
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
)
|
||||
.filter((review) => review.user?.login === 'github-actions[bot]')
|
||||
.map(async (review) => {
|
||||
if (review.state === 'CHANGES_REQUESTED') {
|
||||
await github.rest.pulls.dismissReview({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
review_id: review.id,
|
||||
message: 'All good now, thank you!',
|
||||
})
|
||||
}
|
||||
await github.graphql(
|
||||
`mutation($node_id:ID!) {
|
||||
minimizeComment(input: {
|
||||
classifier: RESOLVED,
|
||||
subjectId: $node_id
|
||||
})
|
||||
{ clientMutationId }
|
||||
}`,
|
||||
{ node_id: review.node_id },
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function postReview({ github, context, core, dry, body }) {
|
||||
const pull_number = context.payload.pull_request.number
|
||||
|
||||
const pendingReview = (
|
||||
await github.paginate(github.rest.pulls.listReviews, {
|
||||
...context.repo,
|
||||
pull_number,
|
||||
})
|
||||
).find(
|
||||
(review) =>
|
||||
review.user?.login === 'github-actions[bot]' &&
|
||||
// If a review is still pending, we can just update this instead
|
||||
// of posting a new one.
|
||||
(review.state === 'CHANGES_REQUESTED' ||
|
||||
// No need to post a new review, if an older one with the exact
|
||||
// same content had already been dismissed.
|
||||
review.body === body),
|
||||
)
|
||||
|
||||
if (dry) {
|
||||
if (pendingReview)
|
||||
core.info(`pending review found: ${pendingReview.html_url}`)
|
||||
else core.info('no pending review found')
|
||||
core.info(body)
|
||||
} else {
|
||||
if (pendingReview) {
|
||||
await github.rest.pulls.updateReview({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
review_id: pendingReview.id,
|
||||
body,
|
||||
})
|
||||
} else {
|
||||
await github.rest.pulls.createReview({
|
||||
...context.repo,
|
||||
pull_number,
|
||||
event: 'REQUEST_CHANGES',
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dismissReviews,
|
||||
postReview,
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
#!/usr/bin/env -S node --import ./run
|
||||
import { execSync } from 'node:child_process'
|
||||
import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { program } from 'commander'
|
||||
import * as core from '@actions/core'
|
||||
import { getOctokit } from '@actions/github'
|
||||
|
||||
async function run(action, owner, repo, pull_number, options = {}) {
|
||||
const token = execSync('gh auth token', { encoding: 'utf-8' }).trim()
|
||||
|
||||
const github = getOctokit(token)
|
||||
|
||||
const payload = !pull_number ? {} : {
|
||||
pull_request: (await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
})).data
|
||||
}
|
||||
|
||||
process.env['INPUT_GITHUB-TOKEN'] = token
|
||||
|
||||
closeSync(openSync('step-summary.md', 'w'))
|
||||
process.env.GITHUB_STEP_SUMMARY = 'step-summary.md'
|
||||
|
||||
await action({
|
||||
github,
|
||||
context: {
|
||||
payload,
|
||||
repo: {
|
||||
owner,
|
||||
repo,
|
||||
},
|
||||
},
|
||||
core,
|
||||
dry: true,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
program
|
||||
.command('prepare')
|
||||
.description('Prepare relevant information of a pull request.')
|
||||
.argument('<owner>', 'Owner of the GitHub repository to check (Example: NixOS)')
|
||||
.argument('<repo>', 'Name of the GitHub repository to check (Example: nixpkgs)')
|
||||
.argument('<pr>', 'Number of the Pull Request to check')
|
||||
.option('--no-dry', 'Make actual modifications')
|
||||
.action(async (owner, repo, pr, options) => {
|
||||
const prepare = (await import('./prepare.js')).default
|
||||
await run(prepare, owner, repo, pr, options)
|
||||
})
|
||||
|
||||
program
|
||||
.command('commits')
|
||||
.description('Check commit structure of a pull request.')
|
||||
.argument('<owner>', 'Owner of the GitHub repository to check (Example: NixOS)')
|
||||
.argument('<repo>', 'Name of the GitHub repository to check (Example: nixpkgs)')
|
||||
.argument('<pr>', 'Number of the Pull Request to check')
|
||||
.option('--no-cherry-picks', 'Do not expect cherry-picks.')
|
||||
.action(async (owner, repo, pr, options) => {
|
||||
const commits = (await import('./commits.js')).default
|
||||
await run(commits, owner, repo, pr, options)
|
||||
})
|
||||
|
||||
program
|
||||
.command('bot')
|
||||
.description('Run automation on pull requests and issues.')
|
||||
.argument('<owner>', 'Owner of the GitHub repository to label (Example: NixOS)')
|
||||
.argument('<repo>', 'Name of the GitHub repository to label (Example: nixpkgs)')
|
||||
.argument('[pr]', 'Number of the Pull Request to label')
|
||||
.option('--no-dry', 'Make actual modifications')
|
||||
.action(async (owner, repo, pr, options) => {
|
||||
const bot = (await import('./bot.js')).default
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'github-script-'))
|
||||
try {
|
||||
process.env.GITHUB_WORKSPACE = tmp
|
||||
process.chdir(tmp)
|
||||
await run(bot, owner, repo, pr, options)
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
program
|
||||
.command('get-teams')
|
||||
.description('Fetch the list of teams with GitHub and output it to a file')
|
||||
.argument('<owner>', 'Owner of the GitHub repository to label (Example: NixOS)')
|
||||
.argument('<repo>', 'Name of the GitHub repository to label (Example: nixpkgs)')
|
||||
.argument('[outFile]', 'Path to the output file (Example: github-teams.json). If not set, prints to stdout')
|
||||
.action(async (owner, repo, outFile, options) => {
|
||||
const getTeams = (await import('./get-teams.js')).default
|
||||
await run(getTeams, owner, repo, undefined, { ...options, outFile })
|
||||
})
|
||||
|
||||
program
|
||||
.command('lint-commits')
|
||||
.description('Lint for common errors in commit messages')
|
||||
.argument('<owner>', 'Owner of the GitHub repository to run on (Example: NixOS)')
|
||||
.argument('<repo>', 'Name of the GitHub repository to run on (Example: nixpkgs)')
|
||||
.argument('<pr>', 'Number of the Pull Request to run on')
|
||||
.action(async (owner, repo, pr, options) => {
|
||||
const checkCommitMessages = (await import('./lint-commits.js')).default
|
||||
await run(checkCommitMessages, owner, repo, pr, options)
|
||||
})
|
||||
|
||||
await program.parse()
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
system ? builtins.currentSystem,
|
||||
pkgs ? (import ../. { inherit system; }).pkgs,
|
||||
}:
|
||||
|
||||
pkgs.callPackage (
|
||||
{
|
||||
gh,
|
||||
importNpmLock,
|
||||
mkShell,
|
||||
nodejs,
|
||||
}:
|
||||
mkShell {
|
||||
packages = [
|
||||
gh
|
||||
importNpmLock.hooks.linkNodeModulesHook
|
||||
nodejs
|
||||
];
|
||||
|
||||
npmDeps = importNpmLock.buildNodeModules {
|
||||
npmRoot = ./.;
|
||||
inherit nodejs;
|
||||
};
|
||||
}
|
||||
) { }
|
||||
@@ -1,63 +0,0 @@
|
||||
module.exports = async ({ github, core, maxConcurrent = 1 }, callback) => {
|
||||
const Bottleneck = require('bottleneck')
|
||||
|
||||
const stats = {
|
||||
issues: 0,
|
||||
prs: 0,
|
||||
requests: 0,
|
||||
artifacts: 0,
|
||||
}
|
||||
|
||||
// Rate-Limiting and Throttling, see for details:
|
||||
// https://github.com/octokit/octokit.js/issues/1069#throttling
|
||||
// https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api
|
||||
const allLimits = new Bottleneck({
|
||||
// Avoid concurrent requests
|
||||
maxConcurrent,
|
||||
// Will be updated with first `updateReservoir()` call below.
|
||||
reservoir: 0,
|
||||
})
|
||||
// Pause between mutative requests
|
||||
const writeLimits = new Bottleneck({ minTime: 1000 }).chain(allLimits)
|
||||
github.hook.wrap('request', async (request, options) => {
|
||||
// Requests to a different host do not count against the rate limit.
|
||||
if (options.url.startsWith('https://github.com')) return request(options)
|
||||
// Requests to the /rate_limit endpoint do not count against the rate limit.
|
||||
if (options.url === '/rate_limit') return request(options)
|
||||
// Search requests are in a different resource group, which allows 30 requests / minute.
|
||||
// We do less than a handful each run, so not implementing throttling for now.
|
||||
if (options.url.startsWith('/search/')) return request(options)
|
||||
stats.requests++
|
||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method))
|
||||
return writeLimits.schedule(request.bind(null, options))
|
||||
else return allLimits.schedule(request.bind(null, options))
|
||||
})
|
||||
|
||||
async function updateReservoir() {
|
||||
let response
|
||||
try {
|
||||
response = await github.rest.rateLimit.get()
|
||||
} catch (err) {
|
||||
core.error(`Failed updating reservoir:\n${err}`)
|
||||
// Keep retrying on failed rate limit requests instead of exiting the script early.
|
||||
return
|
||||
}
|
||||
// Always keep 1000 spare requests for other jobs to do their regular duty.
|
||||
// They normally use below 100, so 1000 is *plenty* of room to work with.
|
||||
const reservoir = Math.max(0, response.data.resources.core.remaining - 1000)
|
||||
core.info(`Updating reservoir to: ${reservoir}`)
|
||||
allLimits.updateSettings({ reservoir })
|
||||
}
|
||||
await updateReservoir()
|
||||
// Update remaining requests every minute to account for other jobs running in parallel.
|
||||
const reservoirUpdater = setInterval(updateReservoir, 60 * 1000)
|
||||
|
||||
try {
|
||||
await callback(stats)
|
||||
} finally {
|
||||
clearInterval(reservoirUpdater)
|
||||
core.notice(
|
||||
`Processed ${stats.prs} PRs, ${stats.issues} Issues, made ${stats.requests + stats.artifacts} API requests and downloaded ${stats.artifacts} artifacts.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
nix,
|
||||
nixpkgs-vet,
|
||||
runCommand,
|
||||
}:
|
||||
{
|
||||
base ? ../.,
|
||||
head ? ../.,
|
||||
}:
|
||||
let
|
||||
filtered =
|
||||
with lib.fileset;
|
||||
path:
|
||||
toSource {
|
||||
fileset = difference (gitTracked path) (unions [
|
||||
(path + /.github)
|
||||
(path + /ci)
|
||||
]);
|
||||
root = path;
|
||||
};
|
||||
|
||||
filteredBase = filtered base;
|
||||
filteredHead = filtered head;
|
||||
in
|
||||
runCommand "nixpkgs-vet"
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
nixpkgs-vet
|
||||
];
|
||||
env.NIXPKGS_VET_NIX_PACKAGE = nix;
|
||||
}
|
||||
''
|
||||
export NIX_STATE_DIR=$(mktemp -d)
|
||||
$NIXPKGS_VET_NIX_PACKAGE/bin/nix-store --init
|
||||
|
||||
nixpkgs-vet --base ${filteredBase} ${filteredHead}
|
||||
|
||||
# TODO: Upstream into nixpkgs-vet, see:
|
||||
# https://github.com/NixOS/nixpkgs-vet/issues/164
|
||||
badFiles=$(find ${filteredHead}/pkgs -type f -name '*.nix' -print | xargs grep -l '^[^#]*<nixpkgs/' || true)
|
||||
if [[ -n $badFiles ]]; then
|
||||
echo "Nixpkgs is not allowed to use <nixpkgs> to refer to itself."
|
||||
echo "The offending files:"
|
||||
echo "$badFiles"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO: Upstream into nixpkgs-vet, see:
|
||||
# https://github.com/NixOS/nixpkgs-vet/issues/166
|
||||
conflictingPaths=$(find ${filteredHead} | awk '{ print $1 " " tolower($1) }' | sort -k2 | uniq -D -f 1 | cut -d ' ' -f 1)
|
||||
if [[ -n $conflictingPaths ]]; then
|
||||
echo "Files in nixpkgs must not vary only by case."
|
||||
echo "The offending paths:"
|
||||
echo "$conflictingPaths"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch $out
|
||||
''
|
||||
@@ -61,6 +61,11 @@ trace "Done"
|
||||
trace -n "Merging base branch into the HEAD commit in $tmp/merged.. "
|
||||
git -C "$tmp/merged" merge -q --no-edit "$baseSha"
|
||||
trace -e "\e[34m$(git -C "$tmp/merged" rev-parse HEAD)\e[0m"
|
||||
trace -n "Reading pinned nixpkgs-vet version from pinned-version.txt.. "
|
||||
toolVersion=$(<"$tmp/merged/ci/nixpkgs-vet/pinned-version.txt")
|
||||
trace -e "\e[34m$toolVersion\e[0m"
|
||||
|
||||
trace -n "Building tool.. "
|
||||
nix-build https://github.com/NixOS/nixpkgs-vet/tarball/"$toolVersion" -o "$tmp/tool" -A build
|
||||
trace "Running nixpkgs-vet.."
|
||||
nix-build ci -A nixpkgs-vet --arg base "$tmp/base" --arg head "$tmp/merged"
|
||||
"$tmp/tool/bin/nixpkgs-vet" --base "$tmp/base" "$tmp/merged"
|
||||
|
||||
1
ci/nixpkgs-vet/pinned-version.txt
Normal file
1
ci/nixpkgs-vet/pinned-version.txt
Normal file
@@ -0,0 +1 @@
|
||||
0.1.4
|
||||
22
ci/nixpkgs-vet/update-pinned-tool.sh
Executable file
22
ci/nixpkgs-vet/update-pinned-tool.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p jq curl
|
||||
|
||||
set -o pipefail -o errexit -o nounset
|
||||
|
||||
trace() { echo >&2 "$@"; }
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
repository=NixOS/nixpkgs-vet
|
||||
pin_file=$SCRIPT_DIR/pinned-version.txt
|
||||
|
||||
trace -n "Fetching latest release of $repository.. "
|
||||
latestRelease=$(curl -sSfL \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/"$repository"/releases/latest)
|
||||
latestVersion=$(jq .tag_name -r <<< "$latestRelease")
|
||||
trace "$latestVersion"
|
||||
|
||||
trace "Updating $pin_file"
|
||||
echo "$latestVersion" > "$pin_file"
|
||||
21
ci/parse.nix
21
ci/parse.nix
@@ -20,15 +20,24 @@ runCommand "nix-parse-${nix.name}"
|
||||
''
|
||||
export NIX_STORE_DIR=$TMPDIR/store
|
||||
export NIX_STATE_DIR=$TMPDIR/state
|
||||
nix-store --init
|
||||
|
||||
cd "${nixpkgs}"
|
||||
|
||||
# This will only show the first parse error, not all of them. That's fine, because
|
||||
# the other CI jobs will report in more detail. This job is about checking parsing
|
||||
# across different implementations / versions, not about providing the best DX.
|
||||
# Returning all parse errors requires significantly more resources.
|
||||
find . -type f -iname '*.nix' | xargs -P $(nproc) nix-instantiate --parse >/dev/null
|
||||
# Passes all files to nix-instantiate at once.
|
||||
# Much faster, but will only show first error.
|
||||
parse-all() {
|
||||
find . -type f -iname '*.nix' | xargs -P $(nproc) nix-instantiate --parse >/dev/null 2>/dev/null
|
||||
}
|
||||
|
||||
# Passes each file separately to nix-instantiate with -n1.
|
||||
# Much slower, but will show all errors.
|
||||
parse-each() {
|
||||
find . -type f -iname '*.nix' | xargs -n1 -P $(nproc) nix-instantiate --parse >/dev/null
|
||||
}
|
||||
|
||||
if ! parse-all; then
|
||||
parse-each
|
||||
fi
|
||||
|
||||
touch $out
|
||||
''
|
||||
|
||||
4
ci/pinned-nixpkgs.json
Normal file
4
ci/pinned-nixpkgs.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"rev": "eaeed9530c76ce5f1d2d8232e08bec5e26f18ec1",
|
||||
"sha256": "132nimgi1g88fbhddk4b8b1qk68jly494x2mnphyk3xa1d2wy9q7"
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"pins": {
|
||||
"nixpkgs": {
|
||||
"type": "Git",
|
||||
"repository": {
|
||||
"type": "GitHub",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs"
|
||||
},
|
||||
"branch": "nixpkgs-unstable",
|
||||
"submodules": false,
|
||||
"revision": "ee09932cedcef15aaf476f9343d1dea2cb77e261",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/ee09932cedcef15aaf476f9343d1dea2cb77e261.tar.gz",
|
||||
"hash": "1xz5pa6la2fyj5b1cfigmg3nmml11fyf9ah0rnr4zfgmnwimn2gn"
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"type": "Git",
|
||||
"repository": {
|
||||
"type": "GitHub",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix"
|
||||
},
|
||||
"branch": "main",
|
||||
"submodules": false,
|
||||
"revision": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4",
|
||||
"url": "https://github.com/numtide/treefmt-nix/archive/5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4.tar.gz",
|
||||
"hash": "0cr6aj9bk7n3y09lwmfjr7xg1f069332xf4q99z3kj1c1mp0wl82"
|
||||
}
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
44
ci/request-reviews/default.nix
Normal file
44
ci/request-reviews/default.nix
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
makeWrapper,
|
||||
coreutils,
|
||||
codeowners,
|
||||
jq,
|
||||
curl,
|
||||
github-cli,
|
||||
gitMinimal,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation {
|
||||
name = "request-reviews";
|
||||
src = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
./get-code-owners.sh
|
||||
./request-reviewers.sh
|
||||
./request-code-owner-reviews.sh
|
||||
./verify-base-branch.sh
|
||||
./dev-branches.txt
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
dontBuild = true;
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
mv dev-branches.txt $out/bin
|
||||
for bin in *.sh; do
|
||||
mv "$bin" "$out/bin"
|
||||
wrapProgram "$out/bin/$bin" \
|
||||
--set PATH ${
|
||||
lib.makeBinPath [
|
||||
coreutils
|
||||
codeowners
|
||||
jq
|
||||
curl
|
||||
github-cli
|
||||
gitMinimal
|
||||
]
|
||||
}
|
||||
done
|
||||
'';
|
||||
}
|
||||
9
ci/request-reviews/dev-branches.txt
Normal file
9
ci/request-reviews/dev-branches.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# Trusted development branches:
|
||||
# These generally require PRs to update and are built by Hydra.
|
||||
# Keep this synced with the branches in .github/workflows/eval.yml
|
||||
master
|
||||
staging
|
||||
release-*
|
||||
staging-*
|
||||
haskell-updates
|
||||
python-updates
|
||||
97
ci/request-reviews/get-code-owners.sh
Executable file
97
ci/request-reviews/get-code-owners.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Get the code owners of the files changed by a PR, returning one username per line
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
if (( "$#" < 4 )); then
|
||||
log "Usage: $0 GIT_REPO OWNERS_FILE BASE_REF HEAD_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gitRepo=$1
|
||||
ownersFile=$2
|
||||
baseRef=$3
|
||||
headRef=$4
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' exit
|
||||
|
||||
git -C "$gitRepo" diff --name-only --merge-base "$baseRef" "$headRef" > "$tmp/touched-files"
|
||||
readarray -t touchedFiles < "$tmp/touched-files"
|
||||
log "This PR touches ${#touchedFiles[@]} files"
|
||||
|
||||
# Get the owners file from the base, because we don't want to allow PRs to
|
||||
# remove code owners to avoid pinging them
|
||||
git -C "$gitRepo" show "$baseRef":"$ownersFile" > "$tmp"/codeowners
|
||||
|
||||
# Associative array with the user as the key for easy de-duplication
|
||||
# Make sure to always lowercase keys to avoid duplicates with different casings
|
||||
declare -A users=()
|
||||
|
||||
for file in "${touchedFiles[@]}"; do
|
||||
result=$(codeowners --file "$tmp"/codeowners "$file")
|
||||
|
||||
# Remove the file prefix and trim the surrounding spaces
|
||||
read -r owners <<< "${result#"$file"}"
|
||||
if [[ "$owners" == "(unowned)" ]]; then
|
||||
log "File $file is unowned"
|
||||
continue
|
||||
fi
|
||||
log "File $file is owned by $owners"
|
||||
|
||||
# Split up multiple owners, separated by arbitrary amounts of spaces
|
||||
IFS=" " read -r -a entries <<< "$owners"
|
||||
|
||||
for entry in "${entries[@]}"; do
|
||||
# GitHub technically also supports Emails as code owners,
|
||||
# but we can't easily support that, so let's not
|
||||
if [[ ! "$entry" =~ @(.*) ]]; then
|
||||
warn -e "\e[33mCodeowner \"$entry\" for file $file is not valid: Must start with \"@\"\e[0m" >&2
|
||||
# Don't fail, because the PR for which this script runs can't fix it,
|
||||
# it has to be fixed in the base branch
|
||||
continue
|
||||
fi
|
||||
# The first regex match is everything after the @
|
||||
entry=${BASH_REMATCH[1]}
|
||||
|
||||
if [[ "$entry" =~ (.*)/(.*) ]]; then
|
||||
# Teams look like $org/$team
|
||||
org=${BASH_REMATCH[1]}
|
||||
team=${BASH_REMATCH[2]}
|
||||
|
||||
# Instead of requesting a review from the team itself,
|
||||
# we request reviews from the individual users.
|
||||
# This is because once somebody from a team reviewed the PR,
|
||||
# the API doesn't expose that the team was already requested for a review,
|
||||
# so we wouldn't be able to avoid rerequesting reviews
|
||||
# without saving some some extra state somewhere
|
||||
|
||||
# We could also consider implementing a more advanced heuristic
|
||||
# in the future that e.g. only pings one team member,
|
||||
# but escalates to somebody else if that member doesn't respond in time.
|
||||
gh api \
|
||||
--cache=1h \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/orgs/$org/teams/$team/members" \
|
||||
--jq '.[].login' > "$tmp/team-members"
|
||||
readarray -t members < "$tmp/team-members"
|
||||
log "Team $entry has these members: ${members[*]}"
|
||||
|
||||
for user in "${members[@]}"; do
|
||||
users[${user,,}]=
|
||||
done
|
||||
else
|
||||
# Everything else is a user
|
||||
users[${entry,,}]=
|
||||
fi
|
||||
done
|
||||
|
||||
done
|
||||
|
||||
printf "%s\n" "${!users[@]}"
|
||||
82
ci/request-reviews/request-code-owner-reviews.sh
Executable file
82
ci/request-reviews/request-code-owner-reviews.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Requests reviews for a PR after verifying that the base branch is correct
|
||||
|
||||
set -euo pipefail
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' exit
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
effect() {
|
||||
if [[ -n "${DRY_MODE:-}" ]]; then
|
||||
log "Skipping in dry mode:" "${@@Q}"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if (( $# < 3 )); then
|
||||
log "Usage: $0 GITHUB_REPO PR_NUMBER OWNERS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
baseRepo=$1
|
||||
prNumber=$2
|
||||
ownersFile=$3
|
||||
|
||||
log "Fetching PR info"
|
||||
prInfo=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$baseRepo/pulls/$prNumber")
|
||||
|
||||
baseBranch=$(jq -r .base.ref <<< "$prInfo")
|
||||
log "Base branch: $baseBranch"
|
||||
prRepo=$(jq -r .head.repo.full_name <<< "$prInfo")
|
||||
log "PR repo: $prRepo"
|
||||
prBranch=$(jq -r .head.ref <<< "$prInfo")
|
||||
log "PR branch: $prBranch"
|
||||
prAuthor=$(jq -r .user.login <<< "$prInfo")
|
||||
log "PR author: $prAuthor"
|
||||
|
||||
extraArgs=()
|
||||
if pwdRepo=$(git rev-parse --show-toplevel 2>/dev/null); then
|
||||
# Speedup for local runs
|
||||
extraArgs+=(--reference-if-able "$pwdRepo")
|
||||
fi
|
||||
|
||||
log "Fetching Nixpkgs commit history"
|
||||
# We only need the commit history, not the contents, so we can do a tree-less clone using tree:0
|
||||
# https://github.blog/open-source/git/get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-quick-summary
|
||||
git clone --bare --filter=tree:0 --no-tags --origin upstream "${extraArgs[@]}" https://github.com/"$baseRepo".git "$tmp"/nixpkgs.git
|
||||
|
||||
log "Fetching the PR commit history"
|
||||
# Fetch the PR
|
||||
git -C "$tmp/nixpkgs.git" remote add fork https://github.com/"$prRepo".git
|
||||
# This remote config is the same as --filter=tree:0 when cloning
|
||||
git -C "$tmp/nixpkgs.git" config remote.fork.partialclonefilter tree:0
|
||||
git -C "$tmp/nixpkgs.git" config remote.fork.promisor true
|
||||
|
||||
git -C "$tmp/nixpkgs.git" fetch --no-tags fork "$prBranch"
|
||||
headRef=$(git -C "$tmp/nixpkgs.git" rev-parse refs/remotes/fork/"$prBranch")
|
||||
|
||||
log "Checking correctness of the base branch"
|
||||
if ! "$SCRIPT_DIR"/verify-base-branch.sh "$tmp/nixpkgs.git" "$headRef" "$baseRepo" "$baseBranch" "$prRepo" "$prBranch" | tee "$tmp/invalid-base-error" >&2; then
|
||||
log "Posting error as comment"
|
||||
if ! response=$(effect gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$baseRepo/issues/$prNumber/comments" \
|
||||
-F "body=@$tmp/invalid-base-error"); then
|
||||
log "Failed to post the comment: $response"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Requesting reviews from code owners"
|
||||
"$SCRIPT_DIR"/get-code-owners.sh "$tmp/nixpkgs.git" "$ownersFile" "$baseBranch" "$headRef" | \
|
||||
"$SCRIPT_DIR"/request-reviewers.sh "$baseRepo" "$prNumber" "$prAuthor"
|
||||
88
ci/request-reviews/request-reviewers.sh
Executable file
88
ci/request-reviews/request-reviewers.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Request reviewers for a PR, reading line-separated usernames on stdin,
|
||||
# filtering for valid reviewers before using the API endpoint to request reviews:
|
||||
# https://docs.github.com/en/rest/pulls/review-requests?apiVersion=2022-11-28#request-reviewers-for-a-pull-request
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' exit
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
effect() {
|
||||
if [[ -n "${DRY_MODE:-}" ]]; then
|
||||
log "Skipping in dry mode:" "${@@Q}"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if (( "$#" < 3 )); then
|
||||
log "Usage: $0 BASE_REPO PR_NUMBER PR_AUTHOR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
baseRepo=$1
|
||||
prNumber=$2
|
||||
prAuthor=$3
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' exit
|
||||
|
||||
declare -A users=()
|
||||
while read -r handle && [[ -n "$handle" ]]; do
|
||||
users[${handle,,}]=
|
||||
done
|
||||
|
||||
# Cannot request a review from the author
|
||||
if [[ -v users[${prAuthor,,}] ]]; then
|
||||
log "One or more files are owned by the PR author, ignoring"
|
||||
unset 'users[${prAuthor,,}]'
|
||||
fi
|
||||
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$baseRepo/pulls/$prNumber/reviews" \
|
||||
--jq '.[].user.login' > "$tmp/already-reviewed-by"
|
||||
|
||||
# And we don't want to rerequest reviews from people who already reviewed
|
||||
while read -r user; do
|
||||
if [[ -v users[${user,,}] ]]; then
|
||||
log "User $user is a potential reviewer, but has already left a review, ignoring"
|
||||
unset 'users[${user,,}]'
|
||||
fi
|
||||
done < "$tmp/already-reviewed-by"
|
||||
|
||||
for user in "${!users[@]}"; do
|
||||
if ! gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$baseRepo/collaborators/$user" >&2; then
|
||||
log "User $user is not a repository collaborator, probably missed the automated invite to the maintainers team (see <https://github.com/NixOS/nixpkgs/issues/234293>), ignoring"
|
||||
unset 'users[$user]'
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${#users[@]}" -gt 10 ]]; then
|
||||
log "Too many reviewers (${!users[*]}), skipping review requests"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for user in "${!users[@]}"; do
|
||||
log "Requesting review from: $user"
|
||||
|
||||
if ! response=$(jq -n --arg user "$user" '{ reviewers: [ $user ] }' | \
|
||||
effect gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/$baseRepo/pulls/$prNumber/requested_reviewers" \
|
||||
--input -); then
|
||||
log "Failed to request review from $user: $response"
|
||||
fi
|
||||
done
|
||||
104
ci/request-reviews/verify-base-branch.sh
Executable file
104
ci/request-reviews/verify-base-branch.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Check that a PR doesn't include commits from other development branches.
|
||||
# Fails with next steps if it does
|
||||
|
||||
set -euo pipefail
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' exit
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
# Small helper to check whether an element is in a list
|
||||
# Usage: `elementIn foo "${list[@]}"`
|
||||
elementIn() {
|
||||
local e match=$1
|
||||
shift
|
||||
for e; do
|
||||
if [[ "$e" == "$match" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if (( $# < 6 )); then
|
||||
log "Usage: $0 LOCAL_REPO HEAD_REF BASE_REPO BASE_BRANCH PR_REPO PR_BRANCH"
|
||||
exit 1
|
||||
fi
|
||||
localRepo=$1
|
||||
headRef=$2
|
||||
baseRepo=$3
|
||||
baseBranch=$4
|
||||
prRepo=$5
|
||||
prBranch=$6
|
||||
|
||||
# All development branches
|
||||
devBranchPatterns=()
|
||||
while read -r pattern; do
|
||||
if [[ "$pattern" != '#'* ]]; then
|
||||
devBranchPatterns+=("$pattern")
|
||||
fi
|
||||
done < "$SCRIPT_DIR/dev-branches.txt"
|
||||
|
||||
git -C "$localRepo" branch --list --format "%(refname:short)" "${devBranchPatterns[@]}" > "$tmp/dev-branches"
|
||||
readarray -t devBranches < "$tmp/dev-branches"
|
||||
|
||||
if [[ "$baseRepo" == "$prRepo" ]] && elementIn "$prBranch" "${devBranches[@]}"; then
|
||||
log "This PR merges $prBranch into $baseBranch, no commit check necessary"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The current merge base of the PR
|
||||
prMergeBase=$(git -C "$localRepo" merge-base "$baseBranch" "$headRef")
|
||||
log "The PR's merge base with the base branch $baseBranch is $prMergeBase"
|
||||
|
||||
# This is purely for debugging
|
||||
git -C "$localRepo" rev-list --reverse "$baseBranch".."$headRef" > "$tmp/pr-commits"
|
||||
log "The PR includes these $(wc -l < "$tmp/pr-commits") commits:"
|
||||
cat <"$tmp/pr-commits" >&2
|
||||
|
||||
for testBranch in "${devBranches[@]}"; do
|
||||
|
||||
if [[ -z "$(git -C "$localRepo" rev-list -1 --since="1 month ago" "$testBranch")" ]]; then
|
||||
log "Not checking $testBranch, was inactive for the last month"
|
||||
continue
|
||||
fi
|
||||
log "Checking if commits from $testBranch are included in the PR"
|
||||
|
||||
# We need to check for any commits that are in the PR which are also in the test branch.
|
||||
# We could check each commit from the PR individually, but that's unnecessarily slow.
|
||||
#
|
||||
# This does _almost_ what we want: `git rev-list --count headRef testBranch ^baseBranch`,
|
||||
# except that it includes commits that are reachable from _either_ headRef or testBranch,
|
||||
# instead of restricting it to ones reachable by both
|
||||
|
||||
# Easily fixable though, because we can use `git merge-base testBranch headRef`
|
||||
# to get the least common ancestor (aka merge base) commit reachable by both.
|
||||
# If the branch being tested is indeed the right base branch,
|
||||
# this is then also the commit from that branch that the PR is based on top of.
|
||||
testMergeBase=$(git -C "$localRepo" merge-base "$testBranch" "$headRef")
|
||||
|
||||
# And then use the `git rev-list --count`, but replacing the non-working
|
||||
# `headRef testBranch` with the merge base of the two.
|
||||
extraCommits=$(git -C "$localRepo" rev-list --count "$testMergeBase" ^"$baseBranch")
|
||||
|
||||
if (( extraCommits != 0 )); then
|
||||
log -e "\e[33m"
|
||||
echo "The PR's base branch is set to $baseBranch, but $extraCommits commits from the $testBranch branch are included. Make sure you know the [right base branch for your changes](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#branch-conventions), then:"
|
||||
echo "- If the changes should go to the $testBranch branch, [change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request) to $testBranch"
|
||||
echo "- If the changes should go to the $baseBranch branch, rebase your PR onto the merge base with the $baseBranch branch:"
|
||||
echo " \`\`\`bash"
|
||||
echo " # git rebase --onto \$(git merge-base upstream/$baseBranch HEAD) \$(git merge-base upstream/$testBranch HEAD)"
|
||||
echo " git rebase --onto $prMergeBase $testMergeBase"
|
||||
echo " git push --force-with-lease"
|
||||
echo " \`\`\`"
|
||||
log -e "\e[m"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
log "Base branch is correct, no commits from development branches are included"
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
/*
|
||||
#!nix-shell -i node -p nodejs
|
||||
*/
|
||||
|
||||
const typeConfig = {
|
||||
master: ['development', 'primary'],
|
||||
release: ['development', 'primary'],
|
||||
staging: ['development', 'secondary'],
|
||||
'staging-next': ['development', 'secondary'],
|
||||
'staging-nixos': ['development', 'secondary'],
|
||||
'haskell-updates': ['development', 'secondary'],
|
||||
nixos: ['channel'],
|
||||
nixpkgs: ['channel'],
|
||||
}
|
||||
|
||||
// "order" ranks the development branches by how likely they are the intended base branch
|
||||
// when they are an otherwise equally good fit according to ci/github-script/prepare.js.
|
||||
const orderConfig = {
|
||||
master: 0,
|
||||
release: 1,
|
||||
staging: 2,
|
||||
'staging-nixos': 2,
|
||||
'haskell-updates': 3,
|
||||
'staging-next': 4,
|
||||
}
|
||||
|
||||
function split(branch) {
|
||||
return {
|
||||
...branch.match(
|
||||
/(?<prefix>.+?)(-(?<version>\d{2}\.\d{2}|unstable)(?:-(?<suffix>.*))?)?$/,
|
||||
).groups,
|
||||
}
|
||||
}
|
||||
|
||||
function classify(branch) {
|
||||
const { prefix, version } = split(branch)
|
||||
return {
|
||||
branch,
|
||||
order: orderConfig[prefix] ?? Infinity,
|
||||
stable: (version ?? 'unstable') !== 'unstable',
|
||||
type: typeConfig[prefix] ?? ['wip'],
|
||||
version: version ?? 'unstable',
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { classify }
|
||||
|
||||
// If called directly via CLI, runs the following tests:
|
||||
if (!module.parent) {
|
||||
console.log('split(branch)')
|
||||
function testSplit(branch) {
|
||||
console.log(branch, split(branch))
|
||||
}
|
||||
testSplit('master')
|
||||
testSplit('release-25.05')
|
||||
testSplit('staging')
|
||||
testSplit('staging-next')
|
||||
testSplit('staging-25.05')
|
||||
testSplit('staging-next-25.05')
|
||||
testSplit('nixpkgs-25.05-darwin')
|
||||
testSplit('nixpkgs-unstable')
|
||||
testSplit('haskell-updates')
|
||||
testSplit('backport-123-to-release-25.05')
|
||||
|
||||
console.log('')
|
||||
|
||||
console.log('classify(branch)')
|
||||
function testClassify(branch) {
|
||||
console.log(branch, classify(branch))
|
||||
}
|
||||
testClassify('master')
|
||||
testClassify('release-25.05')
|
||||
testClassify('staging')
|
||||
testClassify('staging-next')
|
||||
testClassify('staging-25.05')
|
||||
testClassify('staging-next-25.05')
|
||||
testClassify('nixpkgs-25.05-darwin')
|
||||
testClassify('nixpkgs-unstable')
|
||||
testClassify('haskell-updates')
|
||||
testClassify('backport-123-to-release-25.05')
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env -S nix-instantiate --eval --strict --json --arg unused true
|
||||
# Unused argument to trigger nix-instantiate calling this function with the default arguments.
|
||||
{
|
||||
pinnedJson ? ./pinned.json,
|
||||
}:
|
||||
let
|
||||
pinned = (builtins.fromJSON (builtins.readFile pinnedJson)).pins;
|
||||
nixpkgs = fetchTarball {
|
||||
inherit (pinned.nixpkgs) url;
|
||||
sha256 = pinned.nixpkgs.hash;
|
||||
};
|
||||
pkgs = import nixpkgs {
|
||||
config.allowAliases = false;
|
||||
};
|
||||
|
||||
inherit (pkgs) lib;
|
||||
|
||||
lix = lib.pipe pkgs.lixPackageSets [
|
||||
(lib.filterAttrs (_: set: lib.isDerivation set.lix or null && set.lix.meta.available))
|
||||
lib.attrNames
|
||||
(lib.filter (name: lib.match "lix_[0-9_]+|git" name != null))
|
||||
(map (name: "lixPackageSets.${name}.lix"))
|
||||
];
|
||||
|
||||
nix = lib.pipe pkgs.nixVersions [
|
||||
(lib.filterAttrs (_: drv: lib.isDerivation drv && drv.meta.available))
|
||||
lib.attrNames
|
||||
(lib.filter (name: lib.match "nix_[0-9_]+|git" name != null))
|
||||
(map (name: "nixVersions.${name}"))
|
||||
];
|
||||
in
|
||||
lix ++ nix
|
||||
17
ci/update-pinned-nixpkgs.sh
Executable file
17
ci/update-pinned-nixpkgs.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p jq
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# https://stackoverflow.com/a/246128
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
repo=https://github.com/nixos/nixpkgs
|
||||
branch=nixpkgs-unstable
|
||||
file=$SCRIPT_DIR/pinned-nixpkgs.json
|
||||
|
||||
defaultRev=$(git ls-remote "$repo" refs/heads/"$branch" | cut -f1)
|
||||
rev=${1:-$defaultRev}
|
||||
sha256=$(nix-prefetch-url --unpack "$repo/archive/$rev.tar.gz" --name source)
|
||||
|
||||
jq -n --arg rev "$rev" --arg sha256 "$sha256" '$ARGS.named' | tee /dev/stderr > $file
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p npins
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
npins --lock-file pinned.json update
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user