diff --git a/overlays/mods/n8n.nix b/overlays/mods/n8n.nix index 510402b..7b985a6 100644 --- a/overlays/mods/n8n.nix +++ b/overlays/mods/n8n.nix @@ -1,33 +1,27 @@ {prev}: let - xlsxTarball = "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"; - xlsxIntegrity = "sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA=="; - patchXlsxLockfile = '' - substituteInPlace pnpm-lock.yaml \ - --replace-fail \ - "resolution: {tarball: ${xlsxTarball}}" \ - "resolution: {integrity: ${xlsxIntegrity}, tarball: ${xlsxTarball}}" - ''; + # n8n >= 2.32 replaced the sheetjs-CDN `xlsx@0.20.2` tarball (which lacked an + # integrity checksum and had to be patched into pnpm-lock.yaml) with the + # regular npm package `@e965/xlsx@0.20.3`, which already carries an integrity + # field. The old lockfile-integrity workaround is therefore obsolete and has + # been removed. in prev.n8n.overrideAttrs (finalAttrs: previousAttrs: { - version = "2.31.6"; + version = "2.32.6"; src = prev.fetchFromGitHub { owner = "n8n-io"; repo = "n8n"; tag = "n8n@${finalAttrs.version}"; - hash = "sha256-OTVfIHnGTaBogEd93Ak2Pu3Jh7VweQNYbUeahq1Z/64="; + hash = "sha256-wWm6vGyJ2I2SBU38gRGaZG2FR5FBxH6V/sWQwn5B4Ac="; }; pnpmDeps = prev.fetchPnpmDeps { inherit (finalAttrs) pname version src; pnpm = prev.pnpm_10; fetcherVersion = 3; - prePnpmInstall = patchXlsxLockfile; - hash = "sha256-Q3HCJg1KjdRJnPZJEUKcaCrh8tBhT9B3gqMz8xbBZVg="; + hash = "sha256-QzUJCF+VIku9/HrmiUR9FNCU3MlYtyKOILZjFNXvN8U="; }; - prePnpmInstall = (previousAttrs.prePnpmInstall or "") + patchXlsxLockfile; - preBuild = (previousAttrs.preBuild or "") + '' diff --git a/overlays/mods/update.sh b/overlays/mods/update.sh index 63d4b8d..f503f1d 100755 --- a/overlays/mods/update.sh +++ b/overlays/mods/update.sh @@ -1,5 +1,6 @@ #!/usr/bin/env nix-shell #!nix-shell --pure -i bash -p bash curl jq nix nix-prefetch cacert git +# shellcheck shell=bash set -euo pipefail # Update the n8n overlay (overlays/mods/n8n.nix) to the latest stable GitHub @@ -15,26 +16,43 @@ set -euo pipefail # 2. Strip the `n8n@` prefix → version # 3. Compare to the version pinned in overlays/mods/n8n.nix # 4. Compute new src hash (fetchFromGitHub tarball) via nix-prefetch-url -# 5. Compute new pnpmDeps hash via the lib.fakeHash trick +# 5. Compute new pnpmDeps hash via the fake-hash trick # 6. Commit SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NIX_FILE="$SCRIPT_DIR/n8n.nix" +NIXPKGS_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true) + +# Snapshot the current n8n.nix so we can restore it if anything goes wrong +# (error, SIGINT, SIGTERM, or a killed build) — otherwise a failed/aborted run +# leaves a stale version bump and/or the fake-hash sentinel behind. Restoring +# from the snapshot (rather than `git checkout`) also preserves any +# pre-existing working-tree edits the user may have made. The trap is disarmed +# once we reach a successful commit. +RESTORE_FILE="$(mktemp)" +cp "$NIX_FILE" "$RESTORE_FILE" +restore_on_failure() { + if [[ -z "${UPDATE_SUCCEEDED:-}" ]]; then + cp -f "$RESTORE_FILE" "$NIX_FILE" 2>/dev/null || true + fi + rm -f "$RESTORE_FILE" +} +trap restore_on_failure EXIT REPO="n8n-io/n8n" TAG_PREFIX="n8n@" # ── 1. Latest stable release ─────────────────────────────────────────────── echo "Fetching latest stable release of $REPO ..." -LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" \ - | jq -r 'select(.prerelease == false) | .tag_name') +LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | + jq -r 'select(.prerelease == false) | .tag_name') if [[ -z "$LATEST_TAG" ]]; then - echo "ERROR: No stable release found for $REPO" >&2 - exit 1 + echo "ERROR: No stable release found for $REPO" >&2 + exit 1 fi -VERSION="${LATEST_TAG#${TAG_PREFIX}}" +VERSION="${LATEST_TAG#"${TAG_PREFIX}"}" echo "Latest version: $VERSION" # ── 2. Current version from the nix file ─────────────────────────────────── @@ -49,8 +67,8 @@ CURRENT=$(awk ' echo "Current version: $CURRENT" if [[ "$VERSION" == "$CURRENT" ]]; then - echo "Already at latest version, nothing to do." - exit 0 + echo "Already at latest version, nothing to do." + exit 0 fi echo "==> Updating n8n $CURRENT -> $VERSION" @@ -74,24 +92,29 @@ awk -v sri="$SRC_SRI" ' src_done = 1 } { print } -' "$NIX_FILE" > "$NIX_FILE.tmp" && mv "$NIX_FILE.tmp" "$NIX_FILE" +' "$NIX_FILE" >"$NIX_FILE.tmp" && mv "$NIX_FILE.tmp" "$NIX_FILE" -# ── 5. Compute pnpmDeps hash via lib.fakeHash ────────────────────────────── +# ── 5. Compute pnpmDeps hash via the fake-hash trick ─────────────────────── echo "==> Computing pnpmDeps hash (this may take a while) ..." -# 5a. Temporarily set the pnpmDeps hash to lib.fakeHash -awk ' +# A literal SRI sentinel (equal to lib.fakeHash). We write the literal rather +# than `lib.fakeHash` so we don't depend on `lib` being in scope inside +# n8n.nix — the overlay is imported with just `{ prev = pkgs; }`, so `lib` +# is not bound there. +FAKE_HASH='sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' + +# 5a. Temporarily set the pnpmDeps hash to the fake sentinel. +awk -v fake="\"$FAKE_HASH\"" ' !pnpm_started && /^[[:space:]]*pnpmDeps = / { pnpm_started = 1 } pnpm_started && !pnpm_done && /^[[:space:]]*hash = "sha256-/ { - sub(/"sha256-[A-Za-z0-9+/=]+"/, "lib.fakeHash") + sub(/"sha256-[A-Za-z0-9+/=]+"/, fake) pnpm_done = 1 } { print } -' "$NIX_FILE" > "$NIX_FILE.tmp" && mv "$NIX_FILE.tmp" "$NIX_FILE" +' "$NIX_FILE" >"$NIX_FILE.tmp" && mv "$NIX_FILE.tmp" "$NIX_FILE" # 5b. Evaluate n8nModified.pnpmDeps — fixed-output derivation with a wrong hash # will fail and print the correct hash in the "got:" line. -SYSTEM=$(nix eval --impure --raw --expr 'builtins.currentSystem' 2>/dev/null || echo "x86_64-linux") NIX_EXPR="(let flake = builtins.getFlake (toString ./.); pkgs = flake.inputs.nixpkgs.legacyPackages.\${builtins.currentSystem}; @@ -100,34 +123,31 @@ NIX_EXPR="(let BUILD_OUTPUT=$(nix build --impure --no-link --expr "$NIX_EXPR" 2>&1 || true) -# Hash mismatch errors list two sha256 values: the wrong "Expected"/"specified" -# one (our lib.fakeHash = all A's) and the correct "Got"/"got" one. Different -# nix versions capitalise the label differently, so we filter out the fakeHash -# sentinel and take whatever real sha256 remains. -PNPM_HASH=$(echo "$BUILD_OUTPUT" \ - | grep -oE 'sha256-[A-Za-z0-9+/=]+' \ - | grep -v 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ - | tail -1) +# Hash mismatch errors list two sha256 values: the fake sentinel (all A's) as +# "specified"/"Expected" and the correct one as "got:"/"Got:". Different nix +# versions label/capitalise differently, so filter out the sentinel and take +# whatever real sha256 remains. +PNPM_HASH=$(echo "$BUILD_OUTPUT" | + grep -oE 'sha256-[A-Za-z0-9+/=]+' | + grep -v 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' | + tail -1) if [[ -z "$PNPM_HASH" ]]; then - echo "ERROR: Failed to compute pnpmDeps hash. nix build output:" >&2 - echo "$BUILD_OUTPUT" >&2 - echo "Restoring original file..." >&2 - git checkout -- "$NIX_FILE" 2>/dev/null || true - exit 1 + echo "ERROR: could not extract a pnpmDeps hash from the build output." >&2 + echo " This usually means fetchPnpmDeps failed for a reason OTHER than" >&2 + echo " a hash mismatch (e.g. a pnpm-lock patch in n8n.nix no longer applies," >&2 + echo " a tarball vanished, or the build was interrupted). Full output:" >&2 + echo "$BUILD_OUTPUT" >&2 + echo "Restoring n8n.nix via the EXIT trap." >&2 + exit 1 fi echo " pnpmDeps hash: $PNPM_HASH" -# 5d. Replace lib.fakeHash with the real hash -awk -v sri="$PNPM_HASH" ' - !pnpm_started && /^[[:space:]]*pnpmDeps = / { pnpm_started = 1 } - pnpm_started && !pnpm_done && /^[[:space:]]*hash = / { - sub(/lib\.fakeHash/, "\"" sri "\"") - pnpm_done = 1 - } - { print } -' "$NIX_FILE" > "$NIX_FILE.tmp" && mv "$NIX_FILE.tmp" "$NIX_FILE" +# 5c. Replace the fake sentinel with the real hash. The sentinel only appears +# in the pnpmDeps block (the src hash was already updated in step 4), so a +# plain literal substitution is sufficient. +sed -i "s|$FAKE_HASH|$PNPM_HASH|" "$NIX_FILE" # ── 6. Verify the file parses and commit ─────────────────────────────────── echo "==> Verifying nix evaluation ..." @@ -137,17 +157,16 @@ if ! nix eval --impure --expr " pkgs = flake.inputs.nixpkgs.legacyPackages.\${builtins.currentSystem}; in (import ./overlays/mods/n8n.nix { prev = pkgs; }).version " >/dev/null 2>&1; then - echo "ERROR: nix evaluation failed after update; restoring original." >&2 - git checkout -- "$NIX_FILE" 2>/dev/null || true - exit 1 + echo "ERROR: nix evaluation failed after update; restoring n8n.nix via the EXIT trap." >&2 + exit 1 fi -NIXPKGS_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true) -if [[ -n "$NIXPKGS_ROOT" ]] && \ - [[ -n "$(git -C "$NIXPKGS_ROOT" status --porcelain "$NIX_FILE")" ]]; then - git -C "$NIXPKGS_ROOT" add "$NIX_FILE" - git -C "$NIXPKGS_ROOT" commit -m "n8n: $CURRENT -> $VERSION" - echo "==> Committed" +if [[ -n "$NIXPKGS_ROOT" ]] && + [[ -n "$(git -C "$NIXPKGS_ROOT" status --porcelain "$NIX_FILE")" ]]; then + git -C "$NIXPKGS_ROOT" add "$NIX_FILE" + git -C "$NIXPKGS_ROOT" commit -m "n8n: $CURRENT -> $VERSION" + echo "==> Committed" fi +UPDATE_SUCCEEDED=1 echo "==> Done" diff --git a/tests/n8n-overlay-test.nix b/tests/n8n-overlay-test.nix index e98535d..563a5e9 100644 --- a/tests/n8n-overlay-test.nix +++ b/tests/n8n-overlay-test.nix @@ -15,12 +15,10 @@ modifiedPkgs = mkPkgs [self.overlays.modifications]; defaultPkgs = mkPkgs [self.overlays.default]; - expectedVersion = "2.31.6"; - expectedSrcHash = "sha256-OTVfIHnGTaBogEd93Ak2Pu3Jh7VweQNYbUeahq1Z/64="; - expectedPnpmHash = "sha256-Q3HCJg1KjdRJnPZJEUKcaCrh8tBhT9B3gqMz8xbBZVg="; + expectedVersion = "2.32.6"; + expectedSrcHash = "sha256-wWm6vGyJ2I2SBU38gRGaZG2FR5FBxH6V/sWQwn5B4Ac="; + expectedPnpmHash = "sha256-QzUJCF+VIku9/HrmiUR9FNCU3MlYtyKOILZjFNXvN8U="; expectedChangelog = "https://github.com/n8n-io/n8n/releases/tag/n8n@${expectedVersion}"; - expectedXlsxTarball = "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"; - expectedXlsxIntegrity = "sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA=="; expectedRootNodeModuleSymlinks = [ "ln -s .pnpm/node_modules/sass-embedded node_modules/sass-embedded" "ln -s .pnpm/node_modules/sqlite3 node_modules/sqlite3" @@ -46,17 +44,6 @@ if !(pkgs.lib.all (symlink: pkgs.lib.hasInfix symlink (package.preBuild or "")) expectedRootNodeModuleSymlinks) then throw "${overlayName} n8n preBuild should create root node_modules symlinks required by the upstream buildPhase" else "ok"; - - checkXlsxIntegrityPatch = overlayName: package: - pkgs.runCommand "${overlayName}-n8n-xlsx-integrity-patch-test" { - inherit expectedXlsxTarball expectedXlsxIntegrity; - } '' - cp ${package.src}/pnpm-lock.yaml pnpm-lock.yaml - ${package.prePnpmInstall or ""} - grep -F "xlsx@$expectedXlsxTarball:" pnpm-lock.yaml - grep -F "resolution: {integrity: $expectedXlsxIntegrity, tarball: $expectedXlsxTarball}" pnpm-lock.yaml - touch $out - ''; in pkgs.runCommand "n8n-overlay-test" { n8nOverlayFile = @@ -73,8 +60,6 @@ in defaultOverlayBuildPhase = checkBuildPhaseInherited "overlays.default" defaultPkgs.n8n; modificationsOverlayRootNodeModuleSymlinks = checkRootNodeModuleSymlinks "overlays.modifications" modifiedPkgs.n8n; defaultOverlayRootNodeModuleSymlinks = checkRootNodeModuleSymlinks "overlays.default" defaultPkgs.n8n; - modificationsOverlayXlsxIntegrityPatch = checkXlsxIntegrityPatch "overlays.modifications" modifiedPkgs.n8n; - defaultOverlayXlsxIntegrityPatch = checkXlsxIntegrityPatch "overlays.default" defaultPkgs.n8n; } '' touch $out ''