workflows/pr: refactor base/head branch decision making

Some jobs purposefully only run on certain base or head branches. By
centralizing the logic, parts of it can easily be re-used later. Also,
this gives them an explicit name and thus makes them easier to
understand.
This commit is contained in:
Wolfgang Walther 2025-06-20 12:41:29 +02:00
parent caf4ced100
commit 7763be5a80
No known key found for this signature in database
GPG Key ID: B39893FA5F65CAE1
7 changed files with 143 additions and 26 deletions

View File

@ -3,6 +3,9 @@ name: Build
on:
workflow_call:
inputs:
baseBranch:
required: true
type: string
mergedSha:
required: true
type: string
@ -63,7 +66,7 @@ jobs:
- name: Build NixOS manual
if: |
contains(matrix.builds, 'manual-nixos') && !cancelled() &&
(github.base_ref == 'master' || startsWith(github.base_ref, 'release-'))
contains(fromJSON(inputs.baseBranch).type, 'primary')
run: nix-build untrusted/ci -A manual-nixos --argstr system ${{ matrix.system }} --out-link nixos-manual
- name: Build Nixpkgs manual
@ -81,7 +84,7 @@ jobs:
- name: Upload NixOS manual
if: |
contains(matrix.builds, 'manual-nixos') && !cancelled() &&
(github.base_ref == 'master' || startsWith(github.base_ref, 'release-'))
contains(fromJSON(inputs.baseBranch).type, 'primary')
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: nixos-manual-${{ matrix.system }}

View File

@ -2,6 +2,10 @@ name: Check
on:
workflow_call:
inputs:
baseBranch:
required: true
type: string
permissions: {}
@ -12,9 +16,7 @@ defaults:
jobs:
no-channel-base:
name: no channel base
if: |
startsWith(github.base_ref, 'nixos-') ||
startsWith(github.base_ref, 'nixpkgs-')
if: contains(fromJSON(inputs.baseBranch).type, 'channel')
runs-on: ubuntu-24.04-arm
steps:
- run: |
@ -29,8 +31,7 @@ jobs:
cherry-pick:
if: |
github.event_name == 'pull_request' ||
startsWith(github.base_ref, 'release-') ||
(startsWith(github.base_ref, 'staging-') && github.base_ref != 'staging-next')
fromJSON(inputs.baseBranch).stable
permissions:
pull-requests: write
runs-on: ubuntu-24.04-arm

View File

@ -9,6 +9,10 @@ on:
schedule:
- cron: '07,17,27,37,47,57 * * * *'
workflow_call:
inputs:
headBranch:
required: true
type: string
workflow_dispatch:
inputs:
updatedWithin:
@ -252,12 +256,7 @@ jobs:
name: Labels from touched files
if: |
github.event_name == 'pull_request_target' &&
(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-')
))
!contains(fromJSON(inputs.headBranch).type, 'development')
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler.yml # default
@ -267,12 +266,7 @@ jobs:
name: Labels from touched files (no sync)
if: |
github.event_name == 'pull_request_target' &&
(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-')
))
!contains(fromJSON(inputs.headBranch).type, 'development')
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler-no-sync.yml
@ -285,12 +279,7 @@ jobs:
# the backport labels.
if: |
github.event_name == 'pull_request_target' &&
(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-')
))
contains(fromJSON(inputs.headBranch).type, 'development')
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler-development-branches.yml

View File

@ -21,6 +21,8 @@ jobs:
prepare:
runs-on: ubuntu-24.04-arm
outputs:
baseBranch: ${{ steps.branches.outputs.base }}
headBranch: ${{ steps.branches.outputs.head }}
mergedSha: ${{ steps.get-merge-commit.outputs.mergedSha }}
targetSha: ${{ steps.get-merge-commit.outputs.targetSha }}
systems: ${{ steps.systems.outputs.systems }}
@ -29,6 +31,7 @@ jobs:
with:
sparse-checkout: |
.github/actions
ci/supportedBranches.js
ci/supportedSystems.json
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
@ -39,12 +42,35 @@ jobs:
run: |
echo "systems=$(jq -c <ci/supportedSystems.json)" >> "$GITHUB_OUTPUT"
- name: Determine branch type
id: branches
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { classify } = require('./ci/supportedBranches.js')
const { base, head } = context.payload.pull_request
const baseClassification = classify(base.ref)
core.setOutput('base', baseClassification)
core.info('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)
core.info('head classification:', headClassification)
check:
name: Check
needs: [prepare]
uses: ./.github/workflows/check.yml
permissions:
# cherry-picks
pull-requests: write
with:
baseBranch: ${{ needs.prepare.outputs.baseBranch }}
lint:
name: Lint
@ -70,11 +96,13 @@ jobs:
labels:
name: Labels
needs: [eval]
needs: [prepare, eval]
uses: ./.github/workflows/labels.yml
permissions:
issues: write
pull-requests: write
with:
headBranch: ${{ needs.prepare.outputs.headBranch }}
reviewers:
name: Reviewers
@ -91,6 +119,7 @@ jobs:
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
with:
baseBranch: ${{ needs.prepare.outputs.baseBranch }}
mergedSha: ${{ needs.prepare.outputs.mergedSha }}
# This job's only purpose is to serve as a target for the "Required Status Checks" branch ruleset.

4
ci/.editorconfig Normal file
View File

@ -0,0 +1,4 @@
# TODO: Move to top-level via staging PR
[*.js]
indent_style = space
indent_size = 2

View File

@ -20,3 +20,32 @@ 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>.
# Branch classification
For the purposes of CI, branches in the NixOS/nixpkgs repository are classified as follows:
- **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, `haskell-updates` and `python-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.
Some branches also have a version component, which is either `unstable` or `YY.MM`.
`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.

62
ci/supportedBranches.js Executable file
View File

@ -0,0 +1,62 @@
#!/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'],
'haskell-updates': ['development', 'secondary'],
'python-updates': ['development', 'secondary'],
nixos: ['channel'],
nixpkgs: ['channel'],
}
function split(branch) {
return { ...branch.match(/(?<prefix>.+?)(-(?<version>\d{2}\.\d{2}|unstable)(?:-(?<suffix>.*))?)?$/).groups }
}
function classify(branch) {
const { prefix, version } = split(branch)
return {
stable: (version ?? 'unstable') !== 'unstable',
type: typeConfig[prefix] ?? [ 'wip' ]
}
}
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-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-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')
}