From 99ad087158aee976b4380a2e946ed7a1dc4d8880 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 e286015cca..07c76b3530 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -278,3 +278,224 @@ jobs: run: | jq -rn --argjson needs '${{ toJSON(needs) }}' '$needs|to_entries[]|select(.value.result!="success")|"::error::"+.key+": "+.value.result' exit 1 + + 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 951f303631c83853e581e91261e8c8150a7f9bb3 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 07c76b3530..ed3023cffa 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -328,17 +328,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: @@ -352,7 +353,7 @@ jobs: [ { name = prefix + n; - path = v; + drvPath = v.drvPath; } ] else if builtins.isAttrs v then @@ -362,40 +363,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 a2979a2fd9fc709d4d8e42b739e18a6ace1ccc8e 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 ed3023cffa..725853085e 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -493,7 +493,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.