lol

workflows/eval: test all available versions

With this change, we start running Eval on all available Lix and Nix
versions. Because this requires a lot of resources, this complete test
is only run when `ci/pinned.json` is updated.

The resulting outpaths are checked for consistency with the target
branch. A difference will cause the `report` job to fail, thus blocking
the merge, ensuring Eval consistency for Nixpkgs across different
versions.

This implements a kind of "ratchet style" check: Since we originally
confirmed that the versions currently in Nixpkgs at the time of this
commit match Eval behavior of Nix 2.3, we can ensure consistency with
Nix 2.3 down the road, even without testing for it explicitly.

There had been one regression in Eval consistency for Nix between 2.18
and 2.24 - two tests in `tests.devShellTools` produce different results
between Lix 2.91+ (which was forked from Nix 2.18) and Nix 2.24+. I
assume it's unlikely that such a change would be "fixed" by now, thus I
added an exception for these.

As a bonus, we also present the total time in seconds it takes for Eval
to complete for every tested version in a summary table. This allows us
to easily see performance improvements for Eval due to version updates.
At this stage, this time only includes the "outpaths" step of Eval, but
not the generation of attrpaths beforehand.

+180 -4
+130 -3
.github/workflows/eval.yml
··· 11 11 systems: 12 12 required: true 13 13 type: string 14 + testVersions: 15 + required: false 16 + default: false 17 + type: boolean 14 18 secrets: 15 19 OWNER_APP_PRIVATE_KEY: 16 20 required: false ··· 22 26 shell: bash 23 27 24 28 jobs: 29 + versions: 30 + if: inputs.testVersions 31 + runs-on: ubuntu-24.04-arm 32 + outputs: 33 + versions: ${{ steps.versions.outputs.versions }} 34 + steps: 35 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 36 + with: 37 + path: trusted 38 + sparse-checkout: | 39 + ci/supportedVersions.nix 40 + 41 + - name: Check out the PR at the test merge commit 42 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 43 + with: 44 + ref: ${{ inputs.mergedSha }} 45 + path: untrusted 46 + sparse-checkout: | 47 + ci/pinned.json 48 + 49 + - name: Install Nix 50 + uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31 51 + 52 + - name: Load supported versions 53 + id: versions 54 + run: | 55 + echo "versions=$(trusted/ci/supportedVersions.nix --arg pinnedJson untrusted/ci/pinned.json)" >> "$GITHUB_OUTPUT" 56 + 25 57 eval: 26 58 runs-on: ubuntu-24.04-arm 59 + needs: versions 60 + if: ${{ !cancelled() }} 27 61 strategy: 28 62 fail-fast: false 29 63 matrix: 30 64 system: ${{ fromJSON(inputs.systems) }} 31 - name: ${{ matrix.system }} 65 + version: 66 + - "" # Default Eval triggering rebuild labels and such. 67 + - ${{ fromJSON(needs.versions.outputs.versions || '[]') }} # Only for ci/pinned.json updates. 68 + # Failures for versioned Evals will be collected in a separate job below 69 + # to not interrupt main Eval's compare step. 70 + continue-on-error: ${{ matrix.version != '' }} 71 + name: ${{ matrix.system }}${{ matrix.version && format(' @ {0}', matrix.version) || '' }} 32 72 outputs: 33 73 targetRunId: ${{ steps.targetRunId.outputs.targetRunId }} 34 74 timeout-minutes: 15 ··· 60 100 - name: Evaluate the ${{ matrix.system }} output paths for all derivation attributes 61 101 env: 62 102 MATRIX_SYSTEM: ${{ matrix.system }} 103 + MATRIX_VERSION: ${{ matrix.version || 'nixVersions.latest' }} 63 104 run: | 64 105 nix-build untrusted/ci --arg nixpkgs ./pinned -A eval.singleSystem \ 65 106 --argstr evalSystem "$MATRIX_SYSTEM" \ 66 107 --arg chunkSize 8000 \ 108 + --argstr nixPath "$MATRIX_VERSION" \ 67 109 --out-link merged 68 110 # If it uses too much memory, slightly decrease chunkSize 69 111 70 112 - name: Upload the output paths and eval stats 71 113 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 72 114 with: 73 - name: merged-${{ matrix.system }} 115 + name: ${{ matrix.version && format('{0}-', matrix.version) || '' }}merged-${{ matrix.system }} 74 116 path: merged/* 75 117 76 118 - name: Log current API rate limits ··· 149 191 if: steps.targetRunId.outputs.targetRunId 150 192 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 151 193 with: 152 - name: diff-${{ matrix.system }} 194 + name: ${{ matrix.version && format('{0}-', matrix.version) || '' }}diff-${{ matrix.system }} 153 195 path: diff/* 154 196 155 197 compare: ··· 239 281 description, 240 282 target_url 241 283 }) 284 + 285 + # Creates a matrix of Eval performance for various versions and systems. 286 + report: 287 + runs-on: ubuntu-24.04-arm 288 + needs: [versions, eval] 289 + steps: 290 + - name: Download output paths and eval stats for all versions 291 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 292 + with: 293 + pattern: "*-diff-*" 294 + path: versions 295 + 296 + - name: Add version comparison table to job summary 297 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 298 + env: 299 + SYSTEMS: ${{ inputs.systems }} 300 + VERSIONS: ${{ needs.versions.outputs.versions }} 301 + with: 302 + script: | 303 + const { readFileSync } = require('node:fs') 304 + const path = require('node:path') 305 + 306 + const systems = JSON.parse(process.env.SYSTEMS) 307 + const versions = JSON.parse(process.env.VERSIONS) 308 + 309 + core.summary.addHeading('Lix/Nix version comparison') 310 + core.summary.addTable( 311 + [].concat( 312 + [ 313 + [{ data: 'Version', header: true }].concat( 314 + systems.map((system) => ({ data: system, header: true })), 315 + ), 316 + ], 317 + versions.map((version) => 318 + [{ data: version }].concat( 319 + systems.map((system) => { 320 + try { 321 + const artifact = path.join('versions', `${version}-diff-${system}`) 322 + const time = Math.round( 323 + parseFloat( 324 + readFileSync( 325 + path.join(artifact, 'after', system, 'total-time'), 326 + 'utf-8', 327 + ), 328 + ), 329 + ) 330 + const diff = JSON.parse( 331 + readFileSync(path.join(artifact, system, 'diff.json'), 'utf-8'), 332 + ) 333 + const attrs = [].concat( 334 + diff.added, 335 + diff.removed, 336 + diff.changed, 337 + diff.rebuilds 338 + ).filter(attr => 339 + // Exceptions related to dev shells, which changed at some time between 2.18 and 2.24. 340 + !attr.startsWith('tests.devShellTools.nixos.') && 341 + !attr.startsWith('tests.devShellTools.unstructuredDerivationInputEnv.') 342 + ) 343 + if (attrs.length > 0) { 344 + core.setFailed( 345 + `${version} on ${system} has changed outpaths!\nNote: Please make sure to update ci/pinned.json separately from changes to other packages.`, 346 + ) 347 + return { data: ':x:' } 348 + } 349 + return { data: time } 350 + } catch { 351 + core.warning(`${version} on ${system} did not produce artifact.`) 352 + return { data: ':warning:' } 353 + } 354 + }), 355 + ), 356 + ), 357 + ), 358 + ) 359 + core.summary.addRaw( 360 + '\n*Evaluation time in seconds without downloading dependencies.*', 361 + true, 362 + ) 363 + core.summary.addRaw('\n*:warning: Job did not report a result.*', true) 364 + core.summary.addRaw( 365 + '\n*:x: Job produced different outpaths than the target branch.*', 366 + true, 367 + ) 368 + core.summary.write() 242 369 243 370 misc: 244 371 if: ${{ github.event_name != 'push' }}
+16
.github/workflows/pr.yml
··· 28 28 mergedSha: ${{ steps.get-merge-commit.outputs.mergedSha }} 29 29 targetSha: ${{ steps.get-merge-commit.outputs.targetSha }} 30 30 systems: ${{ steps.systems.outputs.systems }} 31 + touched: ${{ steps.files.outputs.touched }} 31 32 steps: 32 33 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 33 34 with: ··· 64 65 core.setOutput('head', headClassification) 65 66 core.info('head classification:', headClassification) 66 67 68 + - name: Determine changed files 69 + id: files 70 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 71 + with: 72 + script: | 73 + const files = (await github.paginate(github.rest.pulls.listFiles, { 74 + ...context.repo, 75 + pull_number: context.payload.pull_request.number, 76 + per_page: 100, 77 + })).map(file => file.filename) 78 + 79 + if (files.includes('ci/pinned.json')) core.setOutput('touched', ['pinned']) 80 + else core.setOutput('touched', []) 81 + 67 82 check: 68 83 name: Check 69 84 needs: [prepare] ··· 96 111 mergedSha: ${{ needs.prepare.outputs.mergedSha }} 97 112 targetSha: ${{ needs.prepare.outputs.targetSha }} 98 113 systems: ${{ needs.prepare.outputs.systems }} 114 + testVersions: ${{ contains(fromJSON(needs.prepare.outputs.touched), 'pinned') && !contains(fromJSON(needs.prepare.outputs.headBranch).type, 'development') }} 99 115 100 116 labels: 101 117 name: Labels
+2 -1
ci/default.nix
··· 5 5 system ? builtins.currentSystem, 6 6 7 7 nixpkgs ? null, 8 + nixPath ? "nixVersions.latest", 8 9 }: 9 10 let 10 11 nixpkgs' = ··· 115 116 # (nixVersions.stable and Lix) here somehow at some point to ensure we don't 116 117 # have eval divergence. 117 118 eval = pkgs.callPackage ./eval { 118 - nix = pkgs.nixVersions.latest; 119 + nix = pkgs.lib.getAttrFromPath (pkgs.lib.splitString "." nixPath) pkgs; 119 120 }; 120 121 121 122 # CI jobs
+32
ci/supportedVersions.nix
··· 1 + #!/usr/bin/env -S nix-instantiate --eval --strict --json --arg unused true 2 + # Unused argument to trigger nix-instantiate calling this function with the default arguments. 3 + { 4 + pinnedJson ? ./pinned.json, 5 + }: 6 + let 7 + pinned = (builtins.fromJSON (builtins.readFile pinnedJson)).pins; 8 + nixpkgs = fetchTarball { 9 + inherit (pinned.nixpkgs) url; 10 + sha256 = pinned.nixpkgs.hash; 11 + }; 12 + pkgs = import nixpkgs { 13 + config.allowAliases = false; 14 + }; 15 + 16 + inherit (pkgs) lib; 17 + 18 + lix = lib.pipe pkgs.lixPackageSets [ 19 + (lib.filterAttrs (_: set: lib.isDerivation set.lix or null && set.lix.meta.available)) 20 + lib.attrNames 21 + (lib.filter (name: lib.match "lix_[0-9_]+|git" name != null)) 22 + (map (name: "lixPackageSets.${name}.lix")) 23 + ]; 24 + 25 + nix = lib.pipe pkgs.nixVersions [ 26 + (lib.filterAttrs (_: drv: lib.isDerivation drv && drv.meta.available)) 27 + lib.attrNames 28 + (lib.filter (name: lib.match "nix_[0-9_]+|git" name != null)) 29 + (map (name: "nixVersions.${name}")) 30 + ]; 31 + in 32 + lix ++ nix