diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 80f9c44..1065e2c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -38,6 +38,9 @@ jobs: uses: DeterminateSystems/flakehub-cache-action@main - name: Build and cache dev shell for ${{ matrix.systems.nix-system }} on ${{ matrix.systems.runner }} + # We still support this system but caching the dev shell fails due to system support mismatch, + # and we don't really need this cached anyway + if: ${{ matrix.systems.nix-system != 'x86_64-darwin' }} run: | nix build -L ".#devShells.${{ matrix.systems.nix-system }}.default" diff --git a/.github/workflows/ref-statuses.yaml b/.github/workflows/ref-statuses.yaml index c81f489..c92e3db 100644 --- a/.github/workflows/ref-statuses.yaml +++ b/.github/workflows/ref-statuses.yaml @@ -24,11 +24,16 @@ jobs: ref_statuses_json=$(nix develop --command cargo run --features ref-statuses -- --get-ref-statuses | jq --sort-keys .) echo "${ref_statuses_json}" > ref-statuses.json + - name: Update README in light of new list + if: failure() + run: | + nix develop --command update-readme + - name: Create pull request if: failure() uses: peter-evans/create-pull-request@v6 with: - commit-message: Update ref-statuses.json to new valid Git refs list + commit-message: Update ref-statuses.json to new valid Git refs list and update README title: Update ref-statuses.json body: | Nixpkgs has changed its list of maintained references. This PR updates `ref-statuses.json` to reflect that change. diff --git a/README.md b/README.md index 15be669..1a69cdb 100644 --- a/README.md +++ b/README.md @@ -162,3 +162,4 @@ If you'd like to help make the parser more exhaustive, [pull requests][prs] are [rust]: https://rust-lang.org [telemetry]: https://github.com/DeterminateSystems/nix-flake-checker/blob/main/src/telemetry.rs#L29-L43 [val]: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html + diff --git a/flake.lock b/flake.lock index ee4b146..b7c386e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,68 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1745454774, + "narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=", + "rev": "efd36682371678e2b6da3f108fdb5c613b3ec598", + "revCount": 729, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.20.3/01966538-0f80-7363-a573-2ba9fd154399/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/ipetkov/crane/0" + } + }, + "easy-template": { + "inputs": { + "crane": "crane", + "fenix": "fenix", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1750267549, + "narHash": "sha256-febr39vSeQ1H54mIhyIB+c2p/2xdntabf4grEQIj60M=", + "rev": "6fb441f657e98f31024f19ca68b691c5b0c3fdd6", + "revCount": 5, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/easy-template/0.1.5%2Brev-6fb441f657e98f31024f19ca68b691c5b0c3fdd6/01978415-c996-7747-a25c-77ca3a4c5ed9/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/DeterminateSystems/easy-template/0" + } + }, "fenix": { "inputs": { "nixpkgs": [ + "easy-template", "nixpkgs" ], "rust-analyzer-src": "rust-analyzer-src" }, + "locked": { + "lastModified": 1740810935, + "narHash": "sha256-6RzWfxENGlO73jQb3uQNgOvubUFwvveeIg+PZxhAu6s=", + "rev": "f44d7c3596ff028ad9f7fcc31d1941ed585f11b3", + "revCount": 2184, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2184%2Brev-f44d7c3596ff028ad9f7fcc31d1941ed585f11b3/019550c8-7792-7766-8dd2-80fad5595f70/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/nix-community/fenix/0.1" + } + }, + "fenix_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src_2" + }, "locked": { "lastModified": 1754030776, "narHash": "sha256-EA7Qh5OUc3tgYrLHfG7zU6wxltvWsJ0+sFxOcVsbjOY=", @@ -55,12 +111,30 @@ }, "root": { "inputs": { - "fenix": "fenix", + "easy-template": "easy-template", + "fenix": "fenix_2", "naersk": "naersk", "nixpkgs": "nixpkgs" } }, "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1740737930, + "narHash": "sha256-2AW/FJQI/i6bbRB/8HR9l9SjxjuiukJpHdMPgwApPKA=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "fe8444616679f8e50ff9696f4750df1f10e7433d", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "rust-analyzer-src_2": { "flake": false, "locked": { "lastModified": 1753974682, diff --git a/flake.nix b/flake.nix index 8164ad8..6bfc383 100644 --- a/flake.nix +++ b/flake.nix @@ -11,42 +11,64 @@ url = "https://flakehub.com/f/nix-community/naersk/0"; inputs.nixpkgs.follows = "nixpkgs"; }; + + easy-template = { + url = "https://flakehub.com/f/DeterminateSystems/easy-template/0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, ... }@inputs: + outputs = + { self, ... }@inputs: let lastModifiedDate = self.lastModifiedDate or self.lastModified or "19700101"; version = "${builtins.substring 0 8 lastModifiedDate}-${self.shortRev or "dirty"}"; - supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - - forSystems = s: f: inputs.nixpkgs.lib.genAttrs s (system: f rec { - inherit system; - pkgs = import inputs.nixpkgs { - inherit system; - overlays = [ self.overlays.default ]; - }; - }); + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forSystems = + s: f: + inputs.nixpkgs.lib.genAttrs s ( + system: + f rec { + inherit system; + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + } + ); forAllSystems = forSystems supportedSystems; in { - overlays.default = final: prev: + overlays.default = + final: prev: let inherit (final.stdenv.hostPlatform) system; - rustToolchain = with inputs.fenix.packages.${system}; - combine ([ - stable.clippy - stable.rustc - stable.cargo - stable.rustfmt - stable.rust-src - ] ++ inputs.nixpkgs.lib.optionals (system == "x86_64-linux") [ - targets.x86_64-unknown-linux-musl.stable.rust-std - ] ++ inputs.nixpkgs.lib.optionals (system == "aarch64-linux") [ - targets.aarch64-unknown-linux-musl.stable.rust-std - ]); + rustToolchain = + with inputs.fenix.packages.${system}; + combine ( + [ + stable.clippy + stable.rustc + stable.cargo + stable.rustfmt + stable.rust-src + ] + ++ inputs.nixpkgs.lib.optionals (system == "x86_64-linux") [ + targets.x86_64-unknown-linux-musl.stable.rust-std + ] + ++ inputs.nixpkgs.lib.optionals (system == "aarch64-linux") [ + targets.aarch64-unknown-linux-musl.stable.rust-std + ] + ); in { inherit rustToolchain; @@ -57,76 +79,104 @@ }; }; - packages = forAllSystems ({ pkgs, system }: rec { - default = flake-checker; - - flake-checker = pkgs.naerskLib.buildPackage - ({ - name = "flake-checker"; - src = self; - doCheck = true; - nativeBuildInputs = with pkgs; [ ] ++ lib.optionals stdenv.isDarwin [ libiconv ]; - } // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { - CARGO_BUILD_TARGET = - if system == "x86_64-linux" then - "x86_64-unknown-linux-musl" - else if system == "aarch64-linux" then - "aarch64-unknown-linux-musl" - else - throw "Unsupported Linux system: ${system}"; - CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static"; - }); - }); - - devShells = forAllSystems ({ pkgs, ... }: { - default = - let - check-nixpkgs-fmt = pkgs.writeShellApplication { - name = "check-nixpkgs-fmt"; - runtimeInputs = with pkgs; [ git nixpkgs-fmt ]; - text = '' - nixpkgs-fmt --check "$(git ls-files '*.nix')" - ''; - }; - check-rustfmt = pkgs.writeShellApplication { - name = "check-rustfmt"; - runtimeInputs = with pkgs; [ rustToolchain ]; - text = "cargo fmt --check"; - }; - get-ref-statuses = pkgs.writeShellApplication { - name = "get-ref-statuses"; - runtimeInputs = with pkgs; [ rustToolchain ]; - text = "cargo run --features ref-statuses -- --get-ref-statuses"; - }; - in - pkgs.mkShell { - packages = with pkgs; [ - bashInteractive - - # Rust - rustToolchain - cargo-bloat - cargo-edit - cargo-machete - cargo-watch - rust-analyzer - - # Nix - nixpkgs-fmt - - # CI checks - check-nixpkgs-fmt - check-rustfmt - - # Scripts - get-ref-statuses - ]; - - env = { - # Required by rust-analyzer - RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; + packages = forAllSystems ( + { pkgs, system }: + rec { + default = flake-checker; + + flake-checker = pkgs.naerskLib.buildPackage ( + { + name = "flake-checker"; + src = self; + doCheck = true; + nativeBuildInputs = with pkgs; [ ] ++ lib.optionals stdenv.isDarwin [ libiconv ]; + } + // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { + CARGO_BUILD_TARGET = + if system == "x86_64-linux" then + "x86_64-unknown-linux-musl" + else if system == "aarch64-linux" then + "aarch64-unknown-linux-musl" + else + throw "Unsupported Linux system: ${system}"; + CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static"; + } + ); + } + ); + + devShells = forAllSystems ( + { pkgs, system }: + { + default = + let + check-nixpkgs-fmt = pkgs.writeShellApplication { + name = "check-nixpkgs-fmt"; + runtimeInputs = with pkgs; [ + git + nixpkgs-fmt + ]; + text = '' + nixpkgs-fmt --check "$(git ls-files '*.nix')" + ''; + }; + check-rustfmt = pkgs.writeShellApplication { + name = "check-rustfmt"; + runtimeInputs = with pkgs; [ rustToolchain ]; + text = "cargo fmt --check"; + }; + get-ref-statuses = pkgs.writeShellApplication { + name = "get-ref-statuses"; + runtimeInputs = with pkgs; [ rustToolchain ]; + text = "cargo run --features ref-statuses -- --get-ref-statuses"; + }; + update-readme = pkgs.writeShellApplication { + name = "update-readme"; + runtimeInputs = [ + inputs.easy-template.packages.${system}.default + pkgs.jq + ]; + text = '' + tmp=$(mktemp -d) + inputs="''${tmp}/template-inputs.json" + + jq '{supported: .}' ./ref-statuses.json > "''${inputs}" + easy-template ./templates/README.md.handlebars "''${inputs}" > README.md + + rm -rf "''${tmp}" + ''; + }; + in + pkgs.mkShell { + packages = with pkgs; [ + bashInteractive + + # Rust + rustToolchain + cargo-bloat + cargo-edit + cargo-machete + cargo-watch + rust-analyzer + + # Nix + nixpkgs-fmt + + # CI checks + check-nixpkgs-fmt + check-rustfmt + + # Scripts + get-ref-statuses + update-readme + ]; + + env = { + # Required by rust-analyzer + RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; + }; }; - }; - }); + } + ); }; } diff --git a/templates/README.md.handlebars b/templates/README.md.handlebars new file mode 100644 index 0000000..8d99a37 --- /dev/null +++ b/templates/README.md.handlebars @@ -0,0 +1,158 @@ +# Nix Flake Checker + +[![FlakeHub](https://img.shields.io/endpoint?url=https://flakehub.com/f/DeterminateSystems/flake-checker/badge)](https://flakehub.com/flake/DeterminateSystems/flake-checker) + +**Nix Flake Checker** is a tool from [Determinate Systems][detsys] that performs "health" checks on the [`flake.lock`][lockfile] files in your [flake][flakes]-powered Nix projects. +Its goal is to help your Nix projects stay on recent and supported versions of [Nixpkgs]. + +To run the checker in the root of a Nix project: + +```shell +nix run github:DeterminateSystems/flake-checker + +# Or point to an explicit path for flake.lock +nix run github:DeterminateSystems/flake-checker /path/to/flake.lock +``` + +Nix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs. +There are two ways to express flake policies: + +- Via [config parameters](#parameters). +- Via [policy conditions](#policy-conditions) using [Common Expression Language][cel] (CEL). + +If you're running it locally, Nix Flake Checker reports any issues via text output in your terminal. +But you can also use Nix Flake Checker [in CI](#the-flake-checker-action). + +## Supported branches + +At any given time, [Nixpkgs] has a bounded set of branches that are considered _supported_. +The current list, with their statuses: + +{{#each supported}} +- `{{@key}}` +{{/each}} + +## Parameters + +By default, Flake Checker verifies that: + +- Any explicit Nixpkgs Git refs are in the [supported list](#supported-branches). +- Any Nixpkgs dependencies are less than 30 days old. +- Any Nixpkgs dependencies have the [`NixOS`][nixos-org] org as the GitHub owner (and thus that the dependency isn't a fork or non-upstream variant). + +You can adjust this behavior via configuration (all are enabled by default but you can disable them): + +| Flag | Environment variable | Action | Default | +| :------------------ | :---------------------------------- | :--------------------------------------------------------- | :------ | +| `--check-outdated` | `NIX_FLAKE_CHECKER_CHECK_OUTDATED` | Check for outdated Nixpkgs inputs | `true` | +| `--check-owner` | `NIX_FLAKE_CHECKER_CHECK_OWNER` | Check that Nixpkgs inputs have `NixOS` as the GitHub owner | `true` | +| `--check-supported` | `NIX_FLAKE_CHECKER_CHECK_SUPPORTED` | Check that Git refs for Nixpkgs inputs are supported | `true` | + +## Policy conditions + +You can apply a CEL condition to your flake using the `--condition` flag. +Here's an example: + +```shell +flake-checker --condition "has(numDaysOld) && numDaysOld < 365" +``` + +This would check that each Nixpkgs input in your `flake.lock` is less than 365 days old. +These variables are available in each condition: + +| Variable | Description | +| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | +| `gitRef` | The Git reference of the input. | +| `numDaysOld` | The number of days old the input is. | +| `owner` | The input's owner (if a GitHub input). | +| `supportedRefs` | A list of [supported Git refs](#supported-branches) (all are branch names). | +| `refStatuses` | A map. Each key is a branch name. Each value is a branch status (`"rolling"`, `"beta"`, `"stable"`, `"deprecated"` or `"unmaintained"`). | + +We recommend a condition _at least_ this stringent: + +```ruby +supportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 30) && owner == 'NixOS' +``` + +Note that not all Nixpkgs inputs have a `numDaysOld` field, so make sure to ensure that that field exists when checking for the number of days. + +Here are some other example conditions: + +```ruby +# Updated in the last two weeks +supportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 14) && owner == 'NixOS' + +# Check for most recent stable Nixpkgs +gitRef.contains("24.05") +``` + +## The Nix Flake Checker Action + +You can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows: + +```yaml +checks: + steps: + - uses: actions/checkout@v4 + - name: Check Nix flake Nixpkgs inputs + uses: DeterminateSystems/flake-checker-action@main +``` + +When run in GitHub Actions, Nix Flake Checker always exits with a status code of 0 by default—and thus never fails your workflows—and reports its findings as a [Markdown summary][md]. + +## Telemetry + +The goal of Nix Flake Checker is to help teams stay on recent and supported versions of Nixpkgs. +The flake checker collects a little bit of telemetry information to help us make that true. + +To disable diagnostic reporting, set the diagnostics URL to an empty string by passing `--no-telemetry` or setting `FLAKE_CHECKER_NO_TELEMETRY=true`. + +You can read the full privacy policy for [Determinate Systems][detsys], the creators of this tool and the [Determinate Nix Installer][installer], [here][privacy]. + +## Rust library + +The Nix Flake Checker is written in [Rust]. +This repo exposes a [`parse-flake-lock`](./parse-flake-lock) crate that you can use to parse [`flake.lock` files][lockfile] in your own Rust projects. +To add that dependency: + +```toml +[dependencies] +parse-flake-lock = { git = "https://github.com/DeterminateSystems/flake-checker", branch = "main" } +``` + +Here's an example usage: + +```rust +use std::path::Path; + +use parse_flake_lock::{FlakeLock, FlakeLockParseError}; + +fn main() -> Result<(), FlakeLockParseError> { + let flake_lock = FlakeLock::new(Path::new("flake.lock"))?; + println!("flake.lock info:"); + println!("version: {version}", version=flake_lock.version); + println!("root node: {root:?}", root=flake_lock.root); + println!("all nodes: {nodes:?}", nodes=flake_lock.nodes); + + Ok(()) +} +``` + +The `parse-flake-lock` crate doesn't yet exhaustively parse all input node types, instead using a "fallthrough" mechanism that parses input types that don't yet have explicit struct definitions to a [`serde_json::value::Value`][val]. +If you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome. + +[action]: https://github.com/DeterminateSystems/flake-checker-action +[cel]: https://cel.dev +[detsys]: https://determinate.systems +[flakes]: https://zero-to-nix.com/concepts/flakes +[install]: https://zero-to-nix.com/start/install +[installer]: https://github.com/DeterminateSystems/nix-installer +[lockfile]: https://zero-to-nix.com/concepts/flakes#lockfile +[md]: https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries +[nixos-org]: https://github.com/NixOS +[nixpkgs]: https://github.com/NixOS/nixpkgs +[privacy]: https://determinate.systems/policies/privacy +[prs]: /pulls +[rust]: https://rust-lang.org +[telemetry]: https://github.com/DeterminateSystems/nix-flake-checker/blob/main/src/telemetry.rs#L29-L43 +[val]: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html