From 452f642d9fb0b2689762a5bd76770195e20973a3 Mon Sep 17 00:00:00 2001 From: jcardozo-eth <131674798+jcardozo-eth@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:09:22 +0200 Subject: [PATCH 1/2] ci(github): add environment diff comment workflow for flake.lock PRs On every pull request that changes flake.lock, build the pinned dev-shell environment at the PR head and at the merge base, diff the two realized closures with nix store diff-closures, and upsert a single PR comment with the package-level delta. Later pushes edit that same comment in place; an unchanged environment posts nothing. Runs least-privilege (contents: read, pull-requests: write) with the built-in token and no repository secrets, so it also annotates Dependabot flake.lock bumps that otherwise render as an opaque hash change. --- .github/workflows/environment-diff.yml | 97 ++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/environment-diff.yml diff --git a/.github/workflows/environment-diff.yml b/.github/workflows/environment-diff.yml new file mode 100644 index 0000000..e68042e --- /dev/null +++ b/.github/workflows/environment-diff.yml @@ -0,0 +1,97 @@ +--- +# Environment diff workflow: when a PR changes flake.lock, build the pinned +# environment (the Nix shell CI and contributors enter) at the PR head and at +# the merge base, diff the two realized closures, and upsert the package-level +# delta as a single PR comment. No secrets; builds are mostly binary-cache +# downloads. +name: Nix flake environment package diff + +on: + pull_request: + paths: + - flake.lock + - .github/workflows/environment-diff.yml + +# Cancel a superseded run when a new push lands on the same PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Dependabot-triggered runs get a read-only GITHUB_TOKEN by default; the +# explicit permissions key is respected and restores comment access. +permissions: + contents: read + pull-requests: write + +jobs: + environment-diff: + name: Environment closure diff + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # The pull_request checkout is the PR merge commit; full history so + # the base side's flake.lock can be read from its first parent. + fetch-depth: 0 + + - name: Install Nix + # flakes + nix-command are enabled by default in install-nix-action v31, + # so no extra_nix_config is needed. + uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0 + + - name: Build both environments and diff their closures + run: | + set -euo pipefail + # The runner is x86_64-linux; hardcode the system rather than + # evaluating builtins.currentSystem impurely. + attr='devShells.x86_64-linux.default' + # HEAD is the PR merge commit, so HEAD^1 is the exact base tip this PR + # merges onto. Building the checkout against the base lock + # (--reference-lock-file) pairs head flake.nix with base pins: + # identical to the true base on the lock-only bumps this workflow + # targets. + git show 'HEAD^1:flake.lock' > /tmp/base.lock + nix build ".#${attr}" --out-link result-head + nix build ".#${attr}" --reference-lock-file /tmp/base.lock \ + --no-write-lock-file --out-link result-base + if [ "$(readlink result-base)" = "$(readlink result-head)" ]; then + # Empty file: the closures are identical (no effective change). + : > diff.txt + else + nix store diff-closures ./result-base ./result-head > diff.txt + fi + + - name: Upsert the PR comment + # --edit-last targets the last comment of the workflow token's own + # identity (github-actions[bot]); this is the only workflow that comments + # as that bot, so the marker line is informational, not the match key. + # The unchanged case edits an existing comment but never creates one: + # no --create-if-none, and the edit's failure when none exists is the + # intended silence. + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + if [ -s diff.txt ]; then + { + echo '' + echo '### Environment diff (`flake.lock` update)' + echo + echo '```text' + cat diff.txt + echo '```' + } > comment.md + gh pr comment "$PR_NUMBER" --edit-last --create-if-none \ + --body-file comment.md + else + { + echo '' + echo '### Environment diff (`flake.lock` update)' + echo + echo 'No effective change to the environment closure.' + } > comment.md + gh pr comment "$PR_NUMBER" --edit-last --body-file comment.md || true + fi From 103d9761003618624fff38144cc200b6d30a3e8d Mon Sep 17 00:00:00 2001 From: jcardozo-eth <131674798+jcardozo-eth@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:09:36 +0200 Subject: [PATCH 2/2] docs(contributing): document the environment diff workflow Add a CI/CD paragraph describing the flake.lock environment diff comment: what a reviewer gets from it and why it matters for Dependabot's weekly lock bumps, which otherwise land as an opaque hash change. --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ca4383..e0d5beb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -465,6 +465,14 @@ auto-deploy described in the README's [Applying the configuration](README.md#applying-the-configuration), safe: a red check is the last cheap place to catch a bad change before a host converges to it. +A per-pull-request workflow, `.github/workflows/environment-diff.yml`, lets a reviewer +see what a `flake.lock` bump actually does before approving it. Such a bump, most often +one of Dependabot's weekly ones, arrives as a single opaque hash change that can stand for +hundreds of upstream package updates, with nothing in the PR showing which tools of the +pinned environment move. The workflow builds the environment before and after the bump and +posts the package and version differences as one plain-language PR comment. It updates that +same comment on later pushes and says nothing when the environment is effectively unchanged. + A separate workflow, `.github/workflows/release.yml`, runs at release time rather than on every change: a pushed `v*` tag triggers it to guard the tag against `metadata.json` and the CHANGELOG, reuse the validation workflow above as its gate, and draft a GitHub Release for