diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 594081924604..20600d9d9a31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 966e0f5c0d52..aa7db2584c60 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 0ca429421891..aaa9fe77ca90 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -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 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8e92b5aed816..3e06ba5265ba 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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 > "$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. diff --git a/ci/.editorconfig b/ci/.editorconfig new file mode 100644 index 000000000000..180d0765eed9 --- /dev/null +++ b/ci/.editorconfig @@ -0,0 +1,4 @@ +# TODO: Move to top-level via staging PR +[*.js] +indent_style = space +indent_size = 2 diff --git a/ci/README.md b/ci/README.md index 66fb707ebec5..7df3b1533e59 100644 --- a/ci/README.md +++ b/ci/README.md @@ -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 . + +# 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. diff --git a/ci/supportedBranches.js b/ci/supportedBranches.js new file mode 100755 index 000000000000..a8579f96df99 --- /dev/null +++ b/ci/supportedBranches.js @@ -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(/(?.+?)(-(?\d{2}\.\d{2}|unstable)(?:-(?.*))?)?$/).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') +}