From 52eecb0c9bbe6d0df169d31c1c2b602266d5fc6e Mon Sep 17 00:00:00 2001 From: Max Blomstervall Date: Fri, 12 Jun 2026 19:51:30 +0200 Subject: [PATCH 1/5] build(nix): add hermetic workspace test check and darwin support Add nix/workspace-tests.nix, a buildRustPackage derivation that builds the whole workspace and runs the full test suite in checkPhase, installing nothing. Expose it as the flake check checks..tests so the suite runs inside the Nix sandbox instead of an impure dev shell. Add the apple-sdk package to buildInputs on darwin in both the package and the test derivation so the arboard clipboard dependency links against AppKit, following the current nixpkgs Darwin SDK pattern. --- default.nix | 12 +++++++---- flake.nix | 2 ++ nix/workspace-tests.nix | 44 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 nix/workspace-tests.nix diff --git a/default.nix b/default.nix index c36227d81..5f7a39dfe 100644 --- a/default.nix +++ b/default.nix @@ -19,10 +19,14 @@ rustPlatform.buildRustPackage { cargoLock.lockFile = ./Cargo.lock; nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = lib.optionals (pkgs.stdenv.isLinux && withTui) [ - pkgs.wayland - pkgs.xorg.libxcb - ]; + buildInputs = + lib.optionals (pkgs.stdenv.isLinux && withTui) [ + pkgs.wayland + pkgs.xorg.libxcb + ] + ++ lib.optionals (pkgs.stdenv.isDarwin && withTui) [ + pkgs.apple-sdk + ]; cargoBuildFlags = [ "--package" "kanban-cli" ] ++ lib.optionals (!withTui) [ "--no-default-features" ]; diff --git a/flake.nix b/flake.nix index 86170d739..ad57ab2db 100644 --- a/flake.nix +++ b/flake.nix @@ -74,6 +74,8 @@ devShells.demo = import ./demo/shell.nix { inherit pkgs kanban; }; + checks.tests = pkgs.callPackage ./nix/workspace-tests.nix { src = self; }; + packages = let kanban-cli = pkgs.callPackage ./default.nix { src = self; gitRev = self.rev or null; withTui = false; }; in { diff --git a/nix/workspace-tests.nix b/nix/workspace-tests.nix new file mode 100644 index 000000000..24fa406ba --- /dev/null +++ b/nix/workspace-tests.nix @@ -0,0 +1,44 @@ +{ + lib, + pkgs, + rustPlatform, + src, +}: +let + cargoToml = lib.importTOML ../Cargo.toml; +in +rustPlatform.buildRustPackage { + pname = "kanban-tests"; + inherit (cargoToml.workspace.package) version; + + inherit src; + + cargoLock.lockFile = ../Cargo.lock; + + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = + lib.optionals pkgs.stdenv.isLinux [ + pkgs.wayland + pkgs.xorg.libxcb + ] + ++ lib.optionals pkgs.stdenv.isDarwin [ + pkgs.apple-sdk + ]; + + cargoBuildFlags = [ "--workspace" "--all-features" ]; + cargoTestFlags = [ "--workspace" "--all-features" ]; + doCheck = true; + + # This derivation exists solely to exercise the test suite hermetically; + # nothing needs to be installed. + installPhase = '' + runHook preInstall + touch $out + runHook postInstall + ''; + + meta = { + description = "Hermetic test runner for the kanban workspace"; + platforms = lib.platforms.all; + }; +} From 001b12ae90f1a316f48c6c376824944e8538323b Mon Sep 17 00:00:00 2001 From: Max Blomstervall Date: Fri, 12 Jun 2026 19:51:40 +0200 Subject: [PATCH 2/5] ci: run hermetic build and test suite on linux and darwin for master PRs Switch the Linux test job to the hermetic flake check and gate it to develop PRs. Add a master-only matrix job that builds the package and runs the test suite hermetically on both x86_64-linux and aarch64-darwin, with fail-fast disabled so one platform failing does not mask the other. --- ...metic-nix-build-test-suite-linux-darwin.md | 9 +++++ .github/workflows/ci.yml | 38 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 .changeset/kan-682-hermetic-nix-build-test-suite-linux-darwin.md diff --git a/.changeset/kan-682-hermetic-nix-build-test-suite-linux-darwin.md b/.changeset/kan-682-hermetic-nix-build-test-suite-linux-darwin.md new file mode 100644 index 000000000..0f3cd3b4e --- /dev/null +++ b/.changeset/kan-682-hermetic-nix-build-test-suite-linux-darwin.md @@ -0,0 +1,9 @@ +--- +bump: patch +--- + +CI now exercises a fully hermetic Nix build and the entire test suite on both +Linux and Darwin for pull requests into master. The workspace test suite runs +inside the Nix sandbox via a new flake check rather than an impure development +shell, and the package builds on darwin with the modern apple-sdk pattern so the +clipboard integration links correctly. This underpins nixpkgs support on macOS. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bc35973b..5d61272f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,8 @@ jobs: run: nix develop --command cargo clippy --all-targets --all-features -- -D warnings test: - name: Test + name: Test (hermetic) + if: github.base_ref == 'develop' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -49,8 +50,10 @@ jobs: extra_nix_config: | experimental-features = nix-command flakes - - name: Run tests - run: nix develop --command cargo test --all-features --workspace + # Build the workspace and run the full test suite inside the Nix sandbox. + # PRs into master get this on both linux and darwin via the `hermetic` job. + - name: Run test suite hermetically + run: nix build .#checks.x86_64-linux.tests -L test-windows: name: Test (Windows) @@ -65,6 +68,35 @@ jobs: - name: Run tests run: cargo test --all-features --workspace + hermetic: + name: Hermetic Build & Test (${{ matrix.system }}) + if: github.base_ref == 'master' + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + system: x86_64-linux + - os: macos-latest + system: aarch64-darwin + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v27 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + extra_nix_config: | + experimental-features = nix-command flakes + + # Build the release artifact the way nixpkgs would, inside the sandbox. + - name: Build package hermetically + run: nix build .#default -L + + # Build the workspace and run the full test suite inside the sandbox. + - name: Run test suite hermetically + run: nix build .#checks.${{ matrix.system }}.tests -L + build: name: Build runs-on: ubuntu-latest From f4e0dd05829c85eed351c70f041d5a1f7606fbbf Mon Sep 17 00:00:00 2001 From: Max Blomstervall Date: Fri, 12 Jun 2026 21:32:15 +0200 Subject: [PATCH 3/5] build(nix): run hermetic test check via crane with cached deps Replace the rustPlatform test derivation with a crane-based cargoTest check. crane compiles the third-party dependency closure once into cargoArtifacts, keyed on Cargo.lock, and reuses it so only the first-party crates recompile when sources change. A custom source filter keeps schema.sql, which kanban-persistence-sqlite pulls in via include_str! and crane's default source cleaner would otherwise drop. The released package stays on rustPlatform in default.nix for nixpkgs parity. --- flake.lock | 16 +++++++++++++++ flake.nix | 41 +++++++++++++++++++++++++++++++++++++- nix/workspace-tests.nix | 44 ----------------------------------------- 3 files changed, 56 insertions(+), 45 deletions(-) delete mode 100644 nix/workspace-tests.nix diff --git a/flake.lock b/flake.lock index a391334fb..32b876e3a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1780532242, + "narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=", + "owner": "ipetkov", + "repo": "crane", + "rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -52,6 +67,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" diff --git a/flake.nix b/flake.nix index ad57ab2db..b2bcf06a7 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils"; + crane.url = "github:ipetkov/crane"; }; outputs = { @@ -10,6 +11,7 @@ nixpkgs, rust-overlay, flake-utils, + crane, ... }: flake-utils.lib.eachDefaultSystem ( @@ -18,11 +20,48 @@ pkgs = import nixpkgs { inherit system overlays; }; + lib = pkgs.lib; rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = ["rust-src" "rust-analyzer" "clippy" "rustfmt"]; }; + # crane drives the fast, incremental per-PR test check. Dependencies are + # compiled once into cargoArtifacts (keyed on Cargo.lock) and reused, so + # CI only recompiles the first-party crates on each change. The released + # package stays on rustPlatform (default.nix) for nixpkgs parity. + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + # kanban-persistence-sqlite pulls schema.sql in via include_str!, so keep + # .sql files that crane's default source cleaner would otherwise strip. + sqlFilter = path: _type: builtins.match ".*\\.sql$" path != null; + srcFilter = path: type: + (sqlFilter path type) || (craneLib.filterCargoSources path type); + craneSrc = lib.cleanSourceWith { + src = self; + filter = srcFilter; + name = "source"; + }; + + commonArgs = { + src = craneSrc; + strictDeps = true; + pname = "kanban-workspace"; + version = (lib.importTOML ./Cargo.toml).workspace.package.version; + cargoExtraArgs = "--workspace --all-features"; + nativeBuildInputs = [pkgs.pkg-config]; + buildInputs = + lib.optionals pkgs.stdenv.isLinux [pkgs.wayland pkgs.xorg.libxcb] + ++ lib.optionals pkgs.stdenv.isDarwin [pkgs.apple-sdk]; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + workspaceTests = craneLib.cargoTest (commonArgs + // { + inherit cargoArtifacts; + }); + changeset = pkgs.writeShellApplication { name = "changeset"; runtimeInputs = with pkgs; [coreutils]; @@ -74,7 +113,7 @@ devShells.demo = import ./demo/shell.nix { inherit pkgs kanban; }; - checks.tests = pkgs.callPackage ./nix/workspace-tests.nix { src = self; }; + checks.tests = workspaceTests; packages = let kanban-cli = pkgs.callPackage ./default.nix { src = self; gitRev = self.rev or null; withTui = false; }; diff --git a/nix/workspace-tests.nix b/nix/workspace-tests.nix deleted file mode 100644 index 24fa406ba..000000000 --- a/nix/workspace-tests.nix +++ /dev/null @@ -1,44 +0,0 @@ -{ - lib, - pkgs, - rustPlatform, - src, -}: -let - cargoToml = lib.importTOML ../Cargo.toml; -in -rustPlatform.buildRustPackage { - pname = "kanban-tests"; - inherit (cargoToml.workspace.package) version; - - inherit src; - - cargoLock.lockFile = ../Cargo.lock; - - nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = - lib.optionals pkgs.stdenv.isLinux [ - pkgs.wayland - pkgs.xorg.libxcb - ] - ++ lib.optionals pkgs.stdenv.isDarwin [ - pkgs.apple-sdk - ]; - - cargoBuildFlags = [ "--workspace" "--all-features" ]; - cargoTestFlags = [ "--workspace" "--all-features" ]; - doCheck = true; - - # This derivation exists solely to exercise the test suite hermetically; - # nothing needs to be installed. - installPhase = '' - runHook preInstall - touch $out - runHook postInstall - ''; - - meta = { - description = "Hermetic test runner for the kanban workspace"; - platforms = lib.platforms.all; - }; -} From 741131ff1c1fbac38ce3544eeaa10fb9325831f3 Mon Sep 17 00:00:00 2001 From: Max Blomstervall Date: Fri, 12 Jun 2026 21:32:28 +0200 Subject: [PATCH 4/5] ci: cache the nix store to reuse crane artifacts across runs Add cache-nix-action to the hermetic jobs so the nix store, including crane's cargoArtifacts, persists between runs. Keyed on flake.lock and Cargo.lock, a PR restores the compiled dependency closure instead of rebuilding it, cutting the per-PR test build from a full cold compile to recompiling first-party crates only. --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d61272f9..c826a5dcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,19 @@ jobs: extra_nix_config: | experimental-features = nix-command flakes + # Persist /nix/store across runs so crane's cargoArtifacts (the compiled + # dependency closure, keyed on Cargo.lock) is restored and only the + # first-party crates recompile per PR. + - uses: nix-community/cache-nix-action@v7 + with: + primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', '**/Cargo.lock') }} + restore-prefixes-first-match: nix-${{ runner.os }}- + gc-max-store-size-linux: 5G + purge: true + purge-prefixes: nix-${{ runner.os }}- + purge-created: 0 + purge-primary-key: never + # Build the workspace and run the full test suite inside the Nix sandbox. # PRs into master get this on both linux and darwin via the `hermetic` job. - name: Run test suite hermetically @@ -89,6 +102,19 @@ jobs: extra_nix_config: | experimental-features = nix-command flakes + # Persist /nix/store across runs so crane's cargoArtifacts and the package + # dependency closure are restored instead of recompiled on each PR. + - uses: nix-community/cache-nix-action@v7 + with: + primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', '**/Cargo.lock') }} + restore-prefixes-first-match: nix-${{ runner.os }}- + gc-max-store-size-linux: 5G + gc-max-store-size-macos: 5G + purge: true + purge-prefixes: nix-${{ runner.os }}- + purge-created: 0 + purge-primary-key: never + # Build the release artifact the way nixpkgs would, inside the sandbox. - name: Build package hermetically run: nix build .#default -L From e8b616ba7f2934bd082f4a69e4d5e112d7bbc593 Mon Sep 17 00:00:00 2001 From: Max Blomstervall Date: Sat, 13 Jun 2026 13:02:07 +0200 Subject: [PATCH 5/5] ci: drop hermetic test job from develop PRs Hermetic linux+darwin coverage is already provided by the `hermetic` matrix job on PRs into master. Running it on every develop PR too added ~12 min to routine turnaround without extra coverage benefit. Develop PRs retain the impure build job for fast feedback. --- .github/workflows/ci.yml | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c826a5dcb..caf04942e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,37 +37,6 @@ jobs: - name: Run clippy run: nix develop --command cargo clippy --all-targets --all-features -- -D warnings - test: - name: Test (hermetic) - if: github.base_ref == 'develop' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: cachix/install-nix-action@v27 - with: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - extra_nix_config: | - experimental-features = nix-command flakes - - # Persist /nix/store across runs so crane's cargoArtifacts (the compiled - # dependency closure, keyed on Cargo.lock) is restored and only the - # first-party crates recompile per PR. - - uses: nix-community/cache-nix-action@v7 - with: - primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', '**/Cargo.lock') }} - restore-prefixes-first-match: nix-${{ runner.os }}- - gc-max-store-size-linux: 5G - purge: true - purge-prefixes: nix-${{ runner.os }}- - purge-created: 0 - purge-primary-key: never - - # Build the workspace and run the full test suite inside the Nix sandbox. - # PRs into master get this on both linux and darwin via the `hermetic` job. - - name: Run test suite hermetically - run: nix build .#checks.x86_64-linux.tests -L - test-windows: name: Test (Windows) runs-on: windows-latest @@ -102,8 +71,8 @@ jobs: extra_nix_config: | experimental-features = nix-command flakes - # Persist /nix/store across runs so crane's cargoArtifacts and the package - # dependency closure are restored instead of recompiled on each PR. + # Persist /nix/store across runs so crane's cargoArtifacts and the package closure + # are restored instead of recompiled on each PR. - uses: nix-community/cache-nix-action@v7 with: primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', '**/Cargo.lock') }}