diff --git a/.github/workflows/nix-release.yml b/.github/workflows/nix-release.yml new file mode 100644 index 0000000..842b040 --- /dev/null +++ b/.github/workflows/nix-release.yml @@ -0,0 +1,124 @@ +name: Update Nix flake + +# Checks whether flake.nix lags behind the latest GitHub release. If it does, +# prefetches the new release's per-platform SRI hashes, rewrites flake.nix, +# and opens a PR. +# +# Runs on a schedule instead of release: published because releases are created +# with GITHUB_TOKEN, which does not start new workflow runs. A daily lag-check +# is fully decoupled from how releases are created and needs no PAT. + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: nix-flake-release + cancel-in-progress: true + +jobs: + update-flake: + name: Bump flake version + hashes if lagging + runs-on: ubuntu-latest + if: github.repository == 'modiqo/cliare' + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Check for lag and rewrite flake.nix + env: + # system|asset-substring — one per line. The substring must uniquely + # match the release asset filename for that system (including the + # .tar.gz suffix so it does not match the sibling .sha256 files). + ASSET_MAP: | + x86_64-linux|x86_64-unknown-linux-gnu + aarch64-linux|aarch64-unknown-linux-gnu + x86_64-darwin|x86_64-apple-darwin + aarch64-darwin|aarch64-apple-darwin + run: | + set -euo pipefail + tag=$(curl -fsSL -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])') + latest="${tag#v}" + current=$(python3 -c 'import re; s=open("flake.nix").read(); m=re.search(r"version = \"([^\"]*)\";", s); print(m.group(1))') + echo "flake.nix version: $current | latest release: $latest (tag $tag)" + if [ "$current" = "$latest" ]; then + echo "flake.nix is up to date; nothing to do." + echo "LAGGING=no" >> "$GITHUB_ENV" + exit 0 + fi + echo "LAGGING=yes" >> "$GITHUB_ENV" + echo "VERSION=$latest" >> "$GITHUB_ENV" + export TAG="$tag" + python3 <<'PYEOF' + import json, os, re, subprocess, urllib.request + tag = os.environ["TAG"] + version = tag.lstrip("v") + repo = os.environ["GITHUB_REPOSITORY"] + with urllib.request.urlopen( + f"https://api.github.com/repos/{repo}/releases/latest") as r: + release = json.load(r) + # Drop sibling checksum files (.sha256) so a tarball substring does + # not also match its ".sha256" companion (cargo-dist etc.). + names = {a["name"] for a in release["assets"] + if not a["name"].endswith(".sha256")} + asset_map = {} + for line in os.environ["ASSET_MAP"].splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + sys_, sub = line.split("|", 1) + asset_map[sys_.strip()] = sub.strip() + src = open("flake.nix").read() + src, n = re.subn(r'version = "[^"]*";', f'version = "{version}";', src, count=1) + if n != 1: + raise SystemExit('could not find version = "..." in flake.nix') + for sys_, sub in asset_map.items(): + match = next((n for n in names if sub in n), None) + if not match: + raise SystemExit(f"no asset for {sys_} ({sub}) in {tag}; have: {sorted(names)}") + url = f"https://github.com/{repo}/releases/download/{tag}/{match}" + out = json.loads(subprocess.check_output( + ["nix", "store", "prefetch-file", "--json", "--hash-type", "sha256", url])) + sri = out["hash"] + pat = re.compile(r'("' + re.escape(sys_) + r'" = \{[^}]*\})', re.S) + def repl(m): + b = m.group(1) + b = re.sub(r'file = "[^"]*";', f'file = "{match}";', b, count=1) + b = re.sub(r'sha256 = "[^"]*";', f'sha256 = "{sri}";', b, count=1) + return b + src, n = pat.subn(repl, src, count=1) + if n != 1: + raise SystemExit(f"could not find assets block for {sys_} in flake.nix") + open("flake.nix", "w").write(src) + print(f"bumped flake.nix to {version}: {list(asset_map)}") + PYEOF + + - name: Open PR + if: env.LAGGING == 'yes' + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "chore(nix): bump flake to v{{ printf "%s" "${{ env.VERSION }}" }}" + title: "chore(nix): bump flake to v{{ printf "%s" "${{ env.VERSION }}" }}" + branch: chore/nix-flake-v{{ printf "%s" "${{ env.VERSION }}" }} + base: main + body: | + Auto-generated by the `Update Nix flake` workflow (daily lag-check). + The latest GitHub release is v{{ printf "%s" "${{ env.VERSION }}" }} but `flake.nix` was + pinned to an older version. This PR bumps `version` and refreshes the per-platform SRI + hashes by prefetching the new release assets. + + Note: PRs opened by `GITHUB_TOKEN` do not trigger downstream workflow runs (e.g. CI), + so this PR will show no checks. The diff is a 5-line hash bump with no source changes — + safe to merge as-is. diff --git a/.gitignore b/.gitignore index b4fdb0b..7eb0689 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /target .cliare/ .cliare-bench/ + +# Nix build result symlinks +/result +/result-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 270e9b8..4084497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project are documented here. ## [Unreleased] +### Added +- Added optional Nix flake support for running and installing cliare with Nix, plus a Devbox environment for reproducible development. + ## [0.1.9] - 2026-07-02 Crates.io packaging reissue. diff --git a/README.md b/README.md index 09b1280..c73dbde 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,45 @@ cargo install --path . cliare metadata --format json ``` +### Nix + +The project provides optional Nix flake outputs for users who already use Nix. The flake wraps the prebuilt release binary. + +```bash +# Run without installing +nix run github:modiqo/cliare + +# Install into your profile +nix profile install github:modiqo/cliare +``` + +The flake tracks the default branch and is auto-bumped to the latest release by a +daily [workflow](.github/workflows/nix-release.yml), so `github:modiqo/cliare` +always serves the current release. (Release tags are cut before the bump lands, +so `github:modiqo/cliare/vX.Y.Z` is not a valid pin — use the +nixpkgs package or a specific commit SHA if you need reproducibility.) + +### Devbox + +For reproducible development environments, use Devbox: + +```bash +# Install Devbox first (if not already installed) +curl -fsSL https://get.jetify.dev/devbox | bash + +# Initialize the environment +devbox shell + +# Build the project +devbox run build +``` + +Or install Devbox via Homebrew: + +```bash +brew install jetify-com/devbox/devbox +``` + For local development: ```sh diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..5e2d6a3 --- /dev/null +++ b/devbox.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json", + "packages": [ + "rustc", + "cargo", + "rust-analyzer" + ], + "shell": { + "init_hook": [ + "echo 'Welcome to the cliare Devbox environment!'" + ], + "scripts": { + "build": "cargo build --release", + "test": "cargo test --workspace --all-features", + "run": "cargo run --", + "check": "cargo fmt --all -- --check && env RUSTC_WRAPPER= cargo clippy --workspace --all-targets --all-features -- -D warnings" + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d38172b --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1783279667, + "narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c4c31c1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,93 @@ +{ + description = "CLI agent-readiness measurement, command-shape inference, and CI scorecards"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { self, nixpkgs }: let + version = "0.1.9"; + + assets = { + x86_64-linux = { + file = "cliare-x86_64-unknown-linux-gnu.tar.gz"; + sha256 = "sha256-Z0E8ss/NcOTWHtVXxNZ0oeUdYAbCt11bRIHognmUxcY="; + }; + aarch64-linux = { + file = "cliare-aarch64-unknown-linux-gnu.tar.gz"; + sha256 = "sha256-Y0C1rN34V9SvW1CfUd3sADv6X7zpEVr1bNiYrKAbGyI="; + }; + x86_64-darwin = { + file = "cliare-x86_64-apple-darwin.tar.gz"; + sha256 = "sha256-wzp+vk/rwHaNWfKcPQzPwfcrdj1MvNhsJlsMYQZVZ68="; + }; + aarch64-darwin = { + file = "cliare-aarch64-apple-darwin.tar.gz"; + sha256 = "sha256-L5fToCamXIreMmKYmW3K+4Pz8Mw40eYLXJhwj/Mz9e4="; + }; + }; + + systems = builtins.attrNames assets; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + + projectFor = system: let + pkgs = nixpkgs.legacyPackages.${system}; + asset = assets.${system}; + in pkgs.stdenv.mkDerivation { + pname = "cliare"; + inherit version; + + src = pkgs.fetchurl { + url = "https://github.com/modiqo/cliare/releases/download/v${version}/${asset.file}"; + sha256 = asset.sha256; + }; + + sourceRoot = "."; + + nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.autoPatchelfHook ]; + buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.stdenv.cc.cc.lib ]; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + install -Dm755 cliare "$out/bin/cliare" + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "CLI agent-readiness measurement, command-shape inference, and CI scorecards"; + homepage = "https://github.com/modiqo/cliare"; + downloadPage = "https://github.com/modiqo/cliare/releases"; + license = licenses.asl20; + mainProgram = "cliare"; + platforms = systems; + sourceProvenance = [ sourceTypes.binaryNativeCode ]; + }; + }; + in { + packages = forAllSystems (system: rec { + cliare = projectFor system; + default = cliare; + }); + + apps = forAllSystems (system: let + # WARNING: do NOT replace this `let` binding with `rec` referencing the + # `packages` attrset above. A `rec { default = { program = "${cliare}/bin/..."; }; }` + # that names the binding `cliare` shadows the `let`-bound derivation, so + # `${cliare}` interpolates the app attrset (a set, not a store path) and + # throws "cannot coerce a set to a string" at `nix run` / `nix flake check`. + # The separate `let cliarePkg = projectFor system;` binding keeps the + # derivation in scope as a store path. + cliarePkg = projectFor system; + in { + cliare = { + type = "app"; + program = "${cliarePkg}/bin/cliare"; + }; + default = { + type = "app"; + program = "${cliarePkg}/bin/cliare"; + }; + }); + }; +}