From c9a2c800cc866b27251651b1f5a00158cebd8728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Wed, 22 Jul 2026 09:38:11 +0300 Subject: [PATCH 1/3] ci: add package version diff comment on PRs Adds a version-diff job that compares every legacyPackages. derivation between a PR's head and base commit and posts the result as a collapsed comment (one
per system, folded into its summary line), using nvd for the actual version/added/removed diff. Enumerates legacyPackages directly (via a small linkFarm collector expression) rather than relying on nix-eval's packages_matrix, since that matrix only lists not-yet-cached packages and goes stale/empty once a PR's packages are built and cached -- the wrong signal for "did anything change vs base". Uses a GitHub App token via actions/create-github-app-token so the comment step isn't blocked by the org's "Actions can't create/approve PRs" policy, and grants pull-requests: write only to the comment job. --- .github/workflows/nix-build.yml | 221 ++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 23466caf1e..ee9b568b7e 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -233,3 +233,224 @@ jobs: secrets: DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + version-diff: + name: Package version diff + if: github.event_name == 'pull_request' + needs: + - nix-build-packages-x86_64-linux + - nix-build-packages-aarch64-linux + - nix-build-packages-aarch64-darwin + strategy: + fail-fast: false + matrix: + include: + - system: x86_64-linux + runs_on: blacksmith-32vcpu-ubuntu-2404 + installer: ephemeral + - system: aarch64-linux + runs_on: blacksmith-4vcpu-ubuntu-2404-arm + installer: ephemeral + - system: aarch64-darwin + runs_on: + group: self-hosted-runners-nix + labels: [aarch64-darwin] + installer: self-hosted + runs-on: ${{ matrix.runs_on }} + steps: + - name: Checkout PR head + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + path: head + + - name: Checkout PR base + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: base + + - name: Install nix (ephemeral) + if: matrix.installer == 'ephemeral' + uses: ./head/.github/actions/nix-install-ephemeral + with: + push-to-cache: 'true' + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Install nix (self-hosted) + if: matrix.installer == 'self-hosted' + uses: ./head/.github/actions/nix-install-self-hosted + + # nix-eval's packages_matrix only lists *uncached* packages, so it + # cannot be used to decide what to diff. Instead, collect every + # derivation under legacyPackages. (recursing into attrsets + # like nix-eval-jobs --force-recurse does) into one linkFarm root, + # whose closure nvd can diff against the other side's root. + - name: Write root collector expression + run: | + cat > /tmp/version-diff-root.nix <<'EOF' + { dir, system }: + let + flake = builtins.getFlake dir; + pkgs = flake.inputs.nixpkgs.legacyPackages.${system}; + isDrv = v: (v.type or "") == "derivation"; + collect = + prefix: set: + builtins.concatLists ( + map ( + n: + let + v = set.${n}; + in + if isDrv v then + [ + { + name = prefix + n; + path = v; + } + ] + else if builtins.isAttrs v then + collect (prefix + n + ".") v + else + [ ] + ) (builtins.attrNames set) + ); + in + pkgs.linkFarm "version-diff-root" (collect "" flake.legacyPackages.${system}) + EOF + + # Evaluation only -- no store paths are realized yet. + - name: Evaluate head root + id: head + working-directory: head + run: | + path=$(nix eval --impure --raw --accept-flake-config \ + --expr '(import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }).outPath') + echo "path=$path" >> "$GITHUB_OUTPUT" + + - name: Evaluate base root + id: base + working-directory: base + run: | + path=$(nix eval --impure --raw --accept-flake-config \ + --expr '(import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }).outPath') + echo "path=$path" >> "$GITHUB_OUTPUT" + + # Both roots' closures were already built and pushed to the shared + # cache (head by nix-build-packages-* above, base by CI on the base + # branch), so building them only substitutes -- no rebuild. + - name: Diff versions with nvd + run: | + if [ "${{ steps.head.outputs.path }}" = "${{ steps.base.outputs.path }}" ]; then + echo "No \`legacyPackages.${{ matrix.system }}\` version changes in this PR." > /tmp/diff.txt + else + (cd head && nix build --impure --accept-flake-config --no-link \ + --expr 'import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }') + (cd base && nix build --impure --accept-flake-config --no-link \ + --expr 'import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }') + nix run --accept-flake-config nixpkgs#nvd -- diff \ + "${{ steps.base.outputs.path }}" "${{ steps.head.outputs.path }}" > /tmp/diff.txt + fi + cat /tmp/diff.txt + + - name: Upload diff artifact + uses: actions/upload-artifact@v4 + with: + name: diff-${{ matrix.system }} + path: /tmp/diff.txt + retention-days: 7 + + version-diff-comment: + name: Post package version diff comment + if: always() && github.event_name == 'pull_request' + needs: version-diff + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Download diff artifacts + uses: actions/download-artifact@v4 + with: + pattern: diff-* + path: diffs + merge-multiple: false + + - name: Assemble collapsed comment body + run: | + all_no_diff=true + for dir in diffs/diff-*; do + file="$dir/diff.txt" + if [ ! -f "$file" ] || ! grep -q '^No `legacyPackages' "$file"; then + all_no_diff=false + break + fi + done + + if [ "$all_no_diff" = "true" ]; then + systems=$(for dir in diffs/diff-*; do echo "\`${dir#diffs/diff-}\`"; done | paste -sd', ') + { + echo "" + echo "## Package version diff: none" + echo "Compares built package versions in \`legacyPackages\` between this PR and its base commit, per system." + echo + echo "### No Package Differences" + echo "All packages are hash-identical on all systems (${systems})." + } > /tmp/comment.md + else + { + echo "" + echo "## Package version diff" + echo "Compares built package versions in \`legacyPackages\` between this PR and its base commit, per system." + echo + for dir in diffs/diff-*; do + system="${dir#diffs/diff-}" + file="$dir/diff.txt" + if [ ! -f "$file" ]; then + echo "
" + echo "${system}: diff unavailable (job failed or skipped)" + echo "
" + echo + continue + fi + if grep -q '^No `legacyPackages' "$file"; then + # Short, no-op case: summary alone, nothing to expand. + echo "
" + echo "${system}: $(cat "$file")" + echo "
" + echo + continue + fi + summary=$(grep '^Closure size:' "$file" | tail -1) + echo "
" + echo "${system}: ${summary:-changed (see below)}" + echo + echo '```' + # Cap the visible diff so a single arch can't blow the comment size limit. + head -c 20000 "$file" + if [ "$(wc -c < "$file")" -gt 20000 ]; then + echo + echo "... truncated, see workflow run for full output ..." + fi + echo '```' + echo "
" + echo + done + } > /tmp/comment.md + fi + + - name: Find existing version diff comment + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Comment on PR + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: /tmp/comment.md + edit-mode: replace From 371cc35a7ef6421585ab6c557b4c5195a3ff5599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Thu, 23 Jul 2026 01:28:28 +0300 Subject: [PATCH 2/3] ci: scope version-diff to changed attrs via drvPath comparison Building the entire legacyPackages closure on both sides just to nvd-diff two roots meant realizing ~700+ store paths per side per arch even when only one package changed -- most of that time was substituter narinfo lookups and downloads, not building anything. Evaluate each attr's drvPath (pure evaluation, no network) on both sides first. Equal maps mean no changes, decided without touching the store at all. Otherwise only the attrs whose drvPath actually differs get built and nvd-diffed; added/removed attrs are reported directly. --- .github/workflows/nix-build.yml | 85 +++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index ee9b568b7e..0c7386517d 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -283,17 +283,18 @@ jobs: uses: ./head/.github/actions/nix-install-self-hosted # nix-eval's packages_matrix only lists *uncached* packages, so it - # cannot be used to decide what to diff. Instead, collect every - # derivation under legacyPackages. (recursing into attrsets - # like nix-eval-jobs --force-recurse does) into one linkFarm root, - # whose closure nvd can diff against the other side's root. - - name: Write root collector expression + # cannot be used to decide what to diff. Instead, evaluate every + # derivation's drvPath under legacyPackages. (recursing into + # attrsets like nix-eval-jobs --force-recurse does) on both sides. + # drvPath captures any input change (source, patches, deps, build + # steps), not just a bumped version string, and evaluating it is pure + # -- no substituter queries, no downloads, no builds. + - name: Write drvPath collector expression run: | - cat > /tmp/version-diff-root.nix <<'EOF' + cat > /tmp/version-diff-drvpaths.nix <<'EOF' { dir, system }: let flake = builtins.getFlake dir; - pkgs = flake.inputs.nixpkgs.legacyPackages.${system}; isDrv = v: (v.type or "") == "derivation"; collect = prefix: set: @@ -307,7 +308,7 @@ jobs: [ { name = prefix + n; - path = v; + drvPath = v.drvPath; } ] else if builtins.isAttrs v then @@ -317,40 +318,64 @@ jobs: ) (builtins.attrNames set) ); in - pkgs.linkFarm "version-diff-root" (collect "" flake.legacyPackages.${system}) + builtins.listToAttrs ( + map (e: { + inherit (e) name; + value = e.drvPath; + }) (collect "" flake.legacyPackages.${system}) + ) EOF - # Evaluation only -- no store paths are realized yet. - - name: Evaluate head root - id: head + - name: Evaluate head drvPaths working-directory: head run: | - path=$(nix eval --impure --raw --accept-flake-config \ - --expr '(import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }).outPath') - echo "path=$path" >> "$GITHUB_OUTPUT" + nix eval --impure --json --accept-flake-config \ + --expr '(import /tmp/version-diff-drvpaths.nix { dir = toString ./.; system = "${{ matrix.system }}"; })' \ + > /tmp/head-drvpaths.json - - name: Evaluate base root - id: base + - name: Evaluate base drvPaths working-directory: base run: | - path=$(nix eval --impure --raw --accept-flake-config \ - --expr '(import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }).outPath') - echo "path=$path" >> "$GITHUB_OUTPUT" + nix eval --impure --json --accept-flake-config \ + --expr '(import /tmp/version-diff-drvpaths.nix { dir = toString ./.; system = "${{ matrix.system }}"; })' \ + > /tmp/base-drvpaths.json - # Both roots' closures were already built and pushed to the shared - # cache (head by nix-build-packages-* above, base by CI on the base - # branch), so building them only substitutes -- no rebuild. + # Only realize/diff attrs whose drvPath actually differs, instead of + # the whole legacyPackages closure -- most PRs change a handful of + # packages, not all ~700+ of them. - name: Diff versions with nvd run: | - if [ "${{ steps.head.outputs.path }}" = "${{ steps.base.outputs.path }}" ]; then + if diff -q <(jq -S . /tmp/head-drvpaths.json) <(jq -S . /tmp/base-drvpaths.json) > /dev/null; then echo "No \`legacyPackages.${{ matrix.system }}\` version changes in this PR." > /tmp/diff.txt else - (cd head && nix build --impure --accept-flake-config --no-link \ - --expr 'import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }') - (cd base && nix build --impure --accept-flake-config --no-link \ - --expr 'import /tmp/version-diff-root.nix { dir = toString ./.; system = "${{ matrix.system }}"; }') - nix run --accept-flake-config nixpkgs#nvd -- diff \ - "${{ steps.base.outputs.path }}" "${{ steps.head.outputs.path }}" > /tmp/diff.txt + changed=$(jq -r -n --slurpfile h /tmp/head-drvpaths.json --slurpfile b /tmp/base-drvpaths.json ' + ($h[0]) as $H | ($b[0]) as $B | + ($H | keys) as $hk | ($B | keys) as $bk | + ($hk - ($hk - $bk))[] | select($H[.] != $B[.]) + ') + added=$(jq -r -n --slurpfile h /tmp/head-drvpaths.json --slurpfile b /tmp/base-drvpaths.json ' + (($h[0] | keys) - ($b[0] | keys))[] + ') + removed=$(jq -r -n --slurpfile h /tmp/head-drvpaths.json --slurpfile b /tmp/base-drvpaths.json ' + (($b[0] | keys) - ($h[0] | keys))[] + ') + + : > /tmp/diff.txt + for attr in $changed; do + head_path=$(cd head && nix build --accept-flake-config ".#legacyPackages.${{ matrix.system }}.$attr" --no-link --print-out-paths) + base_path=$(cd base && nix build --accept-flake-config ".#legacyPackages.${{ matrix.system }}.$attr" --no-link --print-out-paths) + nix run --accept-flake-config nixpkgs#nvd -- diff "$base_path" "$head_path" >> /tmp/diff.txt + done + for attr in $added; do + echo "+ ${attr} added" >> /tmp/diff.txt + done + for attr in $removed; do + echo "- ${attr} removed" >> /tmp/diff.txt + done + + if [ ! -s /tmp/diff.txt ]; then + echo "No \`legacyPackages.${{ matrix.system }}\` version changes in this PR." > /tmp/diff.txt + fi fi cat /tmp/diff.txt From f8af2a20f727be870510590b4edd344f0ea83b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Thu, 23 Jul 2026 04:52:08 +0300 Subject: [PATCH 3/3] ci: drop redundant "(see below)" from version-diff summary fallback --- .github/workflows/nix-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 0c7386517d..d713d9891d 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -448,7 +448,7 @@ jobs: fi summary=$(grep '^Closure size:' "$file" | tail -1) echo "
" - echo "${system}: ${summary:-changed (see below)}" + echo "${system}: ${summary:-changed}" echo echo '```' # Cap the visible diff so a single arch can't blow the comment size limit.