From 14575b2888abd10d18a515692a46ec8f36a7e35f Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Thu, 25 Jun 2026 18:50:09 +0800 Subject: [PATCH 1/4] test(file_tracker): assert dropped paths order-independently restore_verified builds its dropped-paths Vec by iterating a HashMap, so the order is non-deterministic. The exact-equality assertion passed by luck on the CI runner but failed under a different hash seed (surfaced by the sandboxed Nix build). Sort both sides before comparing; the dropped order is not part of the function's contract. --- crates/oxide-code/src/file_tracker.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/oxide-code/src/file_tracker.rs b/crates/oxide-code/src/file_tracker.rs index 1c5fe985..d95f8c67 100644 --- a/crates/oxide-code/src/file_tracker.rs +++ b/crates/oxide-code/src/file_tracker.rs @@ -1080,10 +1080,12 @@ mod tests { ]; let tracker = FileTracker::default(); - let dropped = tracker.restore_verified(snaps); + let mut dropped = tracker.restore_verified(snaps); + dropped.sort(); + let mut expected = vec![drifted_path, missing_path]; + expected.sort(); assert_eq!( - dropped, - vec![missing_path, drifted_path], + dropped, expected, "both the size-drifted and the missing snapshots must be reported", ); assert!(tracker.lock().contains_key(&kept_path)); From ce127c90f3570e40615a570c5afe2506e1d07230 Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Thu, 25 Jun 2026 18:50:26 +0800 Subject: [PATCH 2/4] ci: unify toolchain through the Nix flake Make the flake dev shell the single source of every tool, mirroring the kiln repo. `nix develop` provisions the toolchain and installs a pre-commit hook (rustfmt, nixfmt, markdownlint, cspell) via git-hooks.nix; `nix flake check` runs the same hooks. CI now runs each job through `nix develop -c`, replacing the dtolnay toolchain and the standalone pnpm setup, and adds a `nix build` job on Linux and macOS. Clippy, tests, and coverage stay as cargo invocations (with cargo caching) rather than flake-check derivations, so incremental builds keep working. The generated .pre-commit-config.yaml is gitignored. --- .cspell/words.txt | 2 + .github/actions/setup-nix/action.yml | 27 +++++++ .github/workflows/ci.yml | 95 ++++++++++++++++++------ .gitignore | 6 ++ CLAUDE.md | 2 + flake.lock | 60 +++++++++++++++ flake.nix | 107 ++++++++++++++++++++++++++- 7 files changed, 274 insertions(+), 25 deletions(-) create mode 100644 .github/actions/setup-nix/action.yml diff --git a/.cspell/words.txt b/.cspell/words.txt index 22978489..7f34b0f1 100644 --- a/.cspell/words.txt +++ b/.cspell/words.txt @@ -9,6 +9,7 @@ APFS arboard atuin cachain +cachix catppuccin claudemd Clawd @@ -24,6 +25,7 @@ dedupe deque deserialize desync +direnv disambiguable dtolnay EACCES diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 00000000..2fc2586b --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,27 @@ +name: Setup Nix +description: Install Nix and configure the Cachix substituter + +inputs: + cachix_name: + description: Cachix cache name (skips Cachix if not provided) + cachix_auth_token: + description: Cachix authentication token + cachix_skip_push: + description: Skip pushing to Cachix + default: 'false' + +runs: + using: composite + steps: + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + install_options: ${{ runner.os == 'Linux' && '--no-daemon' || '' }} + + - name: Setup Cachix + if: ${{ inputs.cachix_name }} + uses: cachix/cachix-action@v17 + with: + name: ${{ inputs.cachix_name }} + authToken: ${{ inputs.cachix_auth_token }} + skipPush: ${{ inputs.cachix_skip_push }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c871a17..802c6fe1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,50 +7,77 @@ on: branches: [main] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: + CACHIX_NAME: hakula CARGO_TERM_COLOR: always jobs: + flake-check: + name: Nix Flake Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Nix + uses: ./.github/actions/setup-nix + with: + cachix_name: ${{ env.CACHIX_NAME }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + cachix_skip_push: ${{ !(github.ref == 'refs/heads/main' || github.actor == 'hakula139') }} + + - name: Run flake check + run: nix flake check + rust-check: + name: Rust Check runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable + - name: Setup Nix + uses: ./.github/actions/setup-nix with: - components: rustfmt, clippy + cachix_name: ${{ env.CACHIX_NAME }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + cachix_skip_push: ${{ !(github.ref == 'refs/heads/main' || github.actor == 'hakula139') }} - - name: Cache dependencies + - name: Cache cargo target uses: Swatinem/rust-cache@v2 - name: Format - run: cargo fmt --all --check + run: nix develop -c cargo fmt --all --check - name: Clippy - run: cargo clippy --all-targets -- -D warnings + run: nix develop -c cargo clippy --all-targets -- -D warnings - name: Test - run: cargo test + run: nix develop -c cargo test coverage: + name: Coverage runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable + - name: Setup Nix + uses: ./.github/actions/setup-nix + with: + cachix_name: ${{ env.CACHIX_NAME }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + cachix_skip_push: ${{ !(github.ref == 'refs/heads/main' || github.actor == 'hakula139') }} - - name: Cache dependencies + - name: Cache cargo target uses: Swatinem/rust-cache@v2 - - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov - - name: Generate coverage - run: cargo llvm-cov --ignore-filename-regex 'main\.rs' --lcov --output-path lcov.info + run: nix develop -c cargo llvm-cov --ignore-filename-regex 'main\.rs' --lcov --output-path lcov.info - name: Upload to Codecov uses: codecov/codecov-action@v6 @@ -59,25 +86,45 @@ jobs: files: lcov.info node-check: + name: Node Check runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - - name: Set up pnpm - uses: pnpm/action-setup@v5 - - - name: Set up Node.js - uses: actions/setup-node@v6 + - name: Setup Nix + uses: ./.github/actions/setup-nix with: - node-version: lts/* - cache: pnpm + cachix_name: ${{ env.CACHIX_NAME }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + cachix_skip_push: ${{ !(github.ref == 'refs/heads/main' || github.actor == 'hakula139') }} - name: Install dependencies - run: pnpm install --frozen-lockfile + run: nix develop -c pnpm install --frozen-lockfile - name: Lint Markdown - run: pnpm lint + run: nix develop -c pnpm lint - name: Spell check - run: pnpm spellcheck + run: nix develop -c pnpm spellcheck + + nix-build: + name: Nix Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Nix + uses: ./.github/actions/setup-nix + with: + cachix_name: ${{ env.CACHIX_NAME }} + cachix_auth_token: ${{ secrets.CACHIX_AUTH_TOKEN }} + cachix_skip_push: ${{ !(github.ref == 'refs/heads/main' || github.actor == 'hakula139') }} + + - name: Build oxide-code + run: nix build .#oxide-code --print-build-logs diff --git a/.gitignore b/.gitignore index 29623de1..8aba0f81 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,12 @@ /target/ /node_modules/ +# Nix +.direnv/ +.pre-commit-config.yaml +result +result-* + # cargo insta pending-review files *.snap.new diff --git a/CLAUDE.md b/CLAUDE.md index 04478dff..c8be53fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -293,6 +293,8 @@ pnpm spellcheck # Spell check The `pnpm` checks gate the `node-check` CI job. `cspell` covers Rust sources too, so a new word in a doc comment fails the same way as one in `README.md`. +`nix develop` provisions the hook toolchain and installs a [pre-commit](https://pre-commit.com) hook (generated by [`git-hooks.nix`](https://github.com/cachix/git-hooks.nix)) that runs the compile-free subset of these checks at commit time: `rustfmt`, `nixfmt`, `markdownlint`, and `cspell`. `nix flake check` runs the same hooks. `clippy`, tests, and coverage stay out of the hook because their build cost would gate every commit. + ### Mutation testing Coverage reports whether a line ran. Mutation testing reports whether a mutation of that line would be caught. Run out-of-band before large-scope changes ship because a full run is slow: diff --git a/flake.lock b/flake.lock index fb4114b0..24130b04 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -18,6 +34,49 @@ "type": "github" } }, + "git-hooks-nix": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781733627, + "narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1778003029, @@ -37,6 +96,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "git-hooks-nix": "git-hooks-nix", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } diff --git a/flake.nix b/flake.nix index a579cac7..86738beb 100644 --- a/flake.nix +++ b/flake.nix @@ -2,6 +2,8 @@ # # nix run github:hakula139/oxide-code # one-shot # nix profile install github:hakula139/oxide-code +# nix develop # dev shell + pre-commit hooks +# nix flake check # run pre-commit hooks { description = "oxide-code — terminal-based AI coding assistant"; @@ -13,6 +15,10 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + git-hooks-nix = { + url = "github:cachix/git-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = @@ -20,6 +26,7 @@ nixpkgs, flake-utils, rust-overlay, + git-hooks-nix, ... }: flake-utils.lib.eachDefaultSystem ( @@ -31,7 +38,14 @@ }; # Track the workspace's MSRV via rust-overlay; nixpkgs' stable rustc may lag. - rustToolchain = pkgs.rust-bin.stable.latest.default; + # llvm-tools-preview backs `cargo llvm-cov`; rust-analyzer / rust-src aid editors. + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ + "llvm-tools-preview" + "rust-analyzer" + "rust-src" + ]; + }; cargoToml = fromTOML (builtins.readFile ./Cargo.toml); @@ -79,13 +93,104 @@ mainProgram = "ox"; }; }; + + # ---------------------------------------------------------------------- + # Node Hook Wrapper + # ---------------------------------------------------------------------- + # `pnpm exec` needs node + pnpm on PATH and the project's `node_modules` + # materialised. The Nix sandbox lacks the latter, so `nix flake check` + # skips these hooks; the equivalent checks run in CI via direct `pnpm` + # scripts (the `node-check` job). + nodeHook = + name: cmd: + let + wrapper = pkgs.writeShellApplication { + inherit name; + runtimeInputs = [ + pkgs.nodejs_24 + pkgs.pnpm + ]; + text = '' + if [ ! -d node_modules ]; then + exit 0 + fi + pnpm exec ${cmd} "$@" + ''; + }; + in + "${wrapper}/bin/${name}"; + + # ---------------------------------------------------------------------- + # Pre-commit Hooks + # ---------------------------------------------------------------------- + # Mirrors the compile-free CI checks. Clippy, tests, and coverage stay + # in CI, where their build cost does not gate every commit. + preCommitCheck = git-hooks-nix.lib.${system}.run { + src = ./.; + hooks = { + nixfmt-rfc-style.enable = true; + + # Clippy stays in CI; the bare hook would recompile on every commit. + rustfmt = { + enable = true; + packageOverrides = { + cargo = rustToolchain; + rustfmt = rustToolchain; + }; + }; + + markdownlint = { + enable = true; + name = "markdownlint-cli2"; + entry = nodeHook "markdownlint" "markdownlint-cli2 --fix"; + files = "\\.md$"; + pass_filenames = true; + }; + + cspell = { + enable = true; + entry = nodeHook "cspell" "cspell --no-must-find-files --no-progress"; + types = [ "text" ]; + pass_filenames = true; + }; + }; + }; in { + # ---------------------------------------------------------------------- + # Dev Shell (`nix develop`) — provisions the hook toolchain and installs + # the git hook via the generated `shellHook`. + # ---------------------------------------------------------------------- + devShells.default = pkgs.mkShell { + name = "oxide-code-dev"; + + packages = + preCommitCheck.enabledPackages + ++ [ rustToolchain ] + ++ (with pkgs; [ + cargo-llvm-cov + git-cliff + nodejs_24 + pnpm + ]); + + shellHook = preCommitCheck.shellHook; + + env.RUST_BACKTRACE = "1"; + }; + packages = { default = oxide-code; inherit oxide-code; }; + # ---------------------------------------------------------------------- + # Checks (`nix flake check`) — runs the same hooks CI gates on. + # ---------------------------------------------------------------------- + checks = { + pre-commit = preCommitCheck; + }; + formatter = pkgs.nixfmt; } ); From d9148f4af66e3dc6f1528648bf920e18f2269a4c Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 26 Jun 2026 10:53:08 +0800 Subject: [PATCH 3/4] chore(flake): bump nixpkgs to nixos-26.05 Aliasing of nixfmt-rfc-style to nixfmt in 26.05 surfaces a deprecation warning; switch the hook to the canonical name. --- flake.lock | 8 ++++---- flake.nix | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 24130b04..86111e13 100644 --- a/flake.lock +++ b/flake.lock @@ -79,16 +79,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778003029, - "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "lastModified": 1782233679, + "narHash": "sha256-QyuGP5+QOtmXpy4i2X4DhBVBaySBdDKQEhqKcphcp34=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "rev": "667d5cf1c59585031d743c78b394b0a647537c35", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.11", + "ref": "nixos-26.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 86738beb..331e2301 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ description = "oxide-code — terminal-based AI coding assistant"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -128,7 +128,7 @@ preCommitCheck = git-hooks-nix.lib.${system}.run { src = ./.; hooks = { - nixfmt-rfc-style.enable = true; + nixfmt.enable = true; # Clippy stays in CI; the bare hook would recompile on every commit. rustfmt = { From 171d020f8a4cde9bf4f5ec92f42ac078b03285e9 Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 26 Jun 2026 10:57:12 +0800 Subject: [PATCH 4/4] style(flake): drop verbose comments --- flake.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 331e2301..ec466d80 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,6 @@ }; # Track the workspace's MSRV via rust-overlay; nixpkgs' stable rustc may lag. - # llvm-tools-preview backs `cargo llvm-cov`; rust-analyzer / rust-src aid editors. rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = [ "llvm-tools-preview" @@ -123,8 +122,6 @@ # ---------------------------------------------------------------------- # Pre-commit Hooks # ---------------------------------------------------------------------- - # Mirrors the compile-free CI checks. Clippy, tests, and coverage stay - # in CI, where their build cost does not gate every commit. preCommitCheck = git-hooks-nix.lib.${system}.run { src = ./.; hooks = { @@ -158,8 +155,7 @@ in { # ---------------------------------------------------------------------- - # Dev Shell (`nix develop`) — provisions the hook toolchain and installs - # the git hook via the generated `shellHook`. + # Dev Shell # ---------------------------------------------------------------------- devShells.default = pkgs.mkShell { name = "oxide-code-dev"; @@ -185,7 +181,7 @@ }; # ---------------------------------------------------------------------- - # Checks (`nix flake check`) — runs the same hooks CI gates on. + # Checks (`nix flake check`) # ---------------------------------------------------------------------- checks = { pre-commit = preCommitCheck;