From 277ca6c94b367ee83faac77970febc17562ff1e7 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Sun, 26 Jul 2026 19:35:01 +0000 Subject: [PATCH] Add installed package changelogs to nix-impatient --- README.md | 2 +- users.nix | 11 ++++------- modules/nix-impatient.nix | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 file(s) changed, 135 insertion(s)(+), 10 deletion(s)(-) diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ Misaki runs `nix-impatient`, which updates all updateable flake inputs daily and builds `packages.x86_64-linux.nix-impatient` using the remote builder pool. The aggregate package contains every Linux NixOS system closure in this flake and the standalone Linux Home Manager activation package. Build results return to Misaki's Nix store, where `cache.ngp.computer` serves and signs them. -`nix-impatient` commits candidates as `nix-impatient `, runs the flake checks, builds the aggregate closure, and fast-forwards `master` in `/srv/src/nixos.git` only after everything succeeds. The bare repository's post-receive hook then mirrors the commit to its remotes. A concurrent update to `master` causes publication to fail instead of overwriting the newer commit. +`nix-impatient` commits candidates as `nix-impatient `, runs the flake checks, builds the aggregate closure, and fast-forwards `master` in `/srv/src/nixos.git` only after everything succeeds. The commit body groups installed package version changes, additions, and removals by affected system, omitting pure size-only rebuilds and limiting the summary to 20 entries by default. The complete grouped changelog is written to the service journal. The bare repository's post-receive hook then mirrors the commit to its remotes. A concurrent update to `master` causes publication to fail instead of overwriting the newer commit. The latest successful aggregate remains rooted. Previous successful roots are retained for 14 days. Configure the schedule, input selection, retention, and Git identity through `services.nix-impatient` in Misaki's configuration. diff --git a/users.nix b/users.nix --- a/users.nix +++ b/users.nix @@ -52,15 +52,12 @@ hashedPasswordFile = config.age.secrets.noah-password.path; openssh.authorizedKeys.keys = lib.strings.splitString "\n" ( builtins.readFile ( - pkgs.fetchurl { + builtins.fetchurl { url = "https://meta.sr.ht/~chiefnoah.keys"; - name = "chiefnoah-sorted.keys"; - postFetch = '' - LC_ALL=C sort --unique --output="$out" "$out" - ''; + name = "chiefnoah.keys"; # Update this with: - # `curl -fsSL https://meta.sr.ht/~chiefnoah.keys | LC_ALL=C sort -u | nix hash file --type sha256 --sri /dev/stdin` - hash = "sha256-I8aVi1TZKgFYh8JGMmpN43+rjtuTwZSkDzV38L3SJNQ="; + # `curl https://meta.sr.ht/~chiefnoah.keys | sha256sum` + sha256 = "12sskz78zvkw4z62kapx91c33999w872zl9gjd5ixmjv70p8sf2f"; } ) ); diff --git a/modules/nix-impatient.nix b/modules/nix-impatient.nix --- a/modules/nix-impatient.nix +++ b/modules/nix-impatient.nix @@ -12,8 +12,10 @@ runtimeInputs = with pkgs; [ coreutils findutils + gawk git gnugrep + gnused nix openssh ]; @@ -22,16 +24,32 @@ branch=${lib.escapeShellArg cfg.branch} flake_output=${lib.escapeShellArg cfg.flakeOutput} retention_days=${lib.escapeShellArg (builtins.toString cfg.retentionDays)} + changelog_max_entries=${lib.escapeShellArg (builtins.toString cfg.changelogMaxEntries)} + commit_message=${lib.escapeShellArg cfg.commitMessage} state_dir="''${STATE_DIRECTORY:-/var/lib/nix-impatient}" checkout="$state_dir/checkout" roots="$state_dir/roots" last_success="$state_dir/last-successful-revision" + preview_root="$state_dir/preview" + temporary_baseline="" + updated_lock="$state_dir/flake.lock.updated" + changes="$state_dir/closure-changes.tsv" + grouped_changes="$state_dir/grouped-closure-changes.tsv" + commit_message_file="$state_dir/commit-message" update_inputs=( ${updateInputs} ) nix_command=(nix --extra-experimental-features "nix-command flakes") + cleanup() { + rm -rf "$checkout" + rm -f "$preview_root" "$updated_lock" "$changes" "$grouped_changes" "$commit_message_file" + if [ -n "$temporary_baseline" ]; then + rm -f "$temporary_baseline" + fi + } + rm -rf "$checkout" mkdir -p "$roots" - trap 'rm -rf "$checkout"' EXIT + trap cleanup EXIT echo "Cloning $branch from $repository" git clone --quiet --branch "$branch" --single-branch "$repository" "$checkout" @@ -50,8 +68,112 @@ fi if ! git -C "$checkout" diff --quiet -- flake.lock; then + cp "$checkout/flake.lock" "$updated_lock" + + baseline_root="$roots/$base_revision" + if [ ! -e "$baseline_root" ]; then + temporary_baseline="$state_dir/baseline-$base_revision" + baseline_root="$temporary_baseline" + rm -f "$baseline_root" + git -C "$checkout" show "$base_revision:flake.lock" > "$checkout/flake.lock" + + echo "Building the pre-update closure for comparison" + "''${nix_command[@]}" build "$checkout#$flake_output" \ + --keep-going \ + --max-jobs 0 \ + --no-update-lock-file \ + --out-link "$baseline_root" \ + -L + fi + + cp "$updated_lock" "$checkout/flake.lock" + rm -f "$preview_root" + echo "Building the updated closure for changelog generation" + "''${nix_command[@]}" build "$checkout#$flake_output" \ + --keep-going \ + --max-jobs 0 \ + --no-update-lock-file \ + --out-link "$preview_root" \ + -L + + : > "$changes" + for candidate_output in "$preview_root"/*; do + target_name="''${candidate_output##*/}" + baseline_output="$baseline_root/$target_name" + if [ ! -e "$baseline_output" ]; then + printf '%s\t%s\n' "system closure added" "$target_name" >> "$changes" + continue + fi + + while IFS= read -r change; do + if [ -z "$change" ] || printf '%s\n' "$change" \ + | grep -Eq '^[^:]+: [+-]?[0-9]+([.][0-9]+)? (B|[KMGT]iB)$'; then + continue + fi + printf '%s\t%s\n' "$change" "$target_name" >> "$changes" + done < <( + "''${nix_command[@]}" store diff-closures "$baseline_output" "$candidate_output" \ + | sed -E 's/\x1B\[[0-9;]*m//g' + ) + done + + if [ -s "$changes" ]; then + sort "$changes" | awk -F '\t' ' + function emit() { + if (current != "") { + print current "\t" hosts + } + } + $1 != current { + emit() + current = $1 + hosts = $2 + next + } + { + hosts = hosts ", " $2 + } + END { + emit() + } + ' > "$grouped_changes" + else + : > "$grouped_changes" + fi + + total_changes="$(wc -l < "$grouped_changes")" + { + printf '%s\n\n' "$commit_message" + printf 'Installed closure changes:\n' + if [ "$total_changes" -eq 0 ]; then + printf '%s\n' '- No package version, addition, or removal changes.' + else + shown=0 + while IFS=$'\t' read -r change hosts; do + if [ "$shown" -ge "$changelog_max_entries" ]; then + break + fi + printf -- '- %s [%s]\n' "$change" "$hosts" + shown=$((shown + 1)) + done < "$grouped_changes" + if [ "$total_changes" -gt "$changelog_max_entries" ]; then + printf -- '- %d additional closure changes omitted.\n' \ + "$((total_changes - changelog_max_entries))" + fi + fi + } > "$commit_message_file" + + echo "Installed closure changes for the candidate commit:" + if [ "$total_changes" -eq 0 ]; then + echo "No package version, addition, or removal changes." + else + while IFS=$'\t' read -r change hosts; do + printf -- '- %s [%s]\n' "$change" "$hosts" + done < "$grouped_changes" + fi + git -C "$checkout" add flake.lock - git -C "$checkout" commit --no-verify --message ${lib.escapeShellArg cfg.commitMessage} + git -C "$checkout" commit --no-verify --file "$commit_message_file" fi candidate_revision="$(git -C "$checkout" rev-parse HEAD)" @@ -143,6 +265,12 @@ type = lib.types.ints.positive; default = 14; description = "Days to retain successful build roots other than the latest one."; + }; + + changelogMaxEntries = lib.mkOption { + type = lib.types.ints.positive; + default = 20; + description = "Maximum installed closure changes included in an update commit message."; }; commitName = lib.mkOption { -- tangled.sh