diff --git a/.github/workflows/nix-release.yml b/.github/workflows/nix-release.yml new file mode 100644 index 0000000..ed6879c --- /dev/null +++ b/.github/workflows/nix-release.yml @@ -0,0 +1,119 @@ +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 == 'clidey/deptrust' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Check for lag and rewrite flake.nix + env: + ASSET_MAP: | + x86_64-linux|linux_amd64.tar.gz + aarch64-linux|linux_arm64.tar.gz + x86_64-darwin|darwin_amd64.tar.gz + aarch64-darwin|darwin_arm64.tar.gz + 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) + 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${{ env.VERSION }}" + title: "chore(nix): bump flake to v${{ env.VERSION }}" + branch: chore/nix-flake-v${{ env.VERSION }} + base: main + body: | + Auto-generated by the `Update Nix flake` workflow (daily lag-check). + The latest GitHub release is v${{ 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/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..5fc81bd --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,56 @@ +name: Nix flake + +# Validates the flake (flake.nix). Nix is a side concern, so this job is +# path-filtered to the flake files — it fires only when they change. +# +# Three steps, in order of what they catch: +# 1. nix flake check --all-systems — every system's outputs evaluate +# 2. nix build .#default — fetchurl + autoPatchelf + install +# layout actually realises for the runner's system. +# 3. nix run .#default -- --version — the patched binary actually execs. + +on: + push: + branches: [main] + paths: + - "flake.nix" + - "flake.lock" + - "**/*.nix" + - ".github/workflows/nix.yml" + pull_request: + branches: [main] + paths: + - "flake.nix" + - "flake.lock" + - "**/*.nix" + - ".github/workflows/nix.yml" + +permissions: + contents: read + +concurrency: + group: nix-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check: + name: nix flake check + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v16 + + - name: nix flake check --all-systems + run: nix flake check --all-systems --print-build-logs + + - name: nix build .#default + run: nix build .#default --print-build-logs + + - name: nix run .#default -- version + run: nix run .#default -- version diff --git a/.gitignore b/.gitignore index 24d5f6c..1a06f69 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,10 @@ go.work.sum # Local sandbox/tool caches .cache/ +# Nix build results +/result +/result-* + # Editor/IDE .idea/ .vscode/ diff --git a/README.md b/README.md index 9aa23e9..aa0e9a9 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,45 @@ Go users can install directly: go install github.com/clidey/deptrust/cmd/deptrust@latest ``` +### 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:clidey/deptrust + +# Install into your profile +nix profile install github:clidey/deptrust +``` + +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:clidey/deptrust` +always serves the current release. (Release tags are cut before the bump lands, +so `github:clidey/deptrust/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 +``` + ## Agent Setup To install deptrust and register everything the installer can configure without the guided prompts: diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..7eba074 --- /dev/null +++ b/devbox.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json", + "packages": [ + "go", + "gopls" + ], + "shell": { + "init_hook": [ + "echo 'Welcome to the Devbox environment!'" + ], + "scripts": { + "build": "go build ./cmd/deptrust", + "test": "go test ./...", + "run": "go run ./cmd/deptrust" + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b4f5c4c --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1782948114, + "narHash": "sha256-AXmz9ho4Lud5CsbrZsuSVwpQZ4o5FgZ1chxBn5cJ8+0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9e92285f211dad236540fd617d7e30e0b99bc0e1", + "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..3faffd0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,85 @@ +{ + description = "deptrust — local package vulnerability checker and MCP server for AI agents"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { self, nixpkgs }: let + version = "0.9.0"; + + assets = { + "x86_64-linux" = { + file = "deptrust_v${version}_linux_amd64.tar.gz"; + sha256 = "sha256-qL/N68QDrzGB7wLsy0X3sk4bpgzKXexNKZQ/bEvZ0RA="; + }; + "aarch64-linux" = { + file = "deptrust_v${version}_linux_arm64.tar.gz"; + sha256 = "sha256-g6f5GH37PLP0A1zWb5mzlSVGk963ZkJztXkW0ZU8+iY="; + }; + "x86_64-darwin" = { + file = "deptrust_v${version}_darwin_amd64.tar.gz"; + sha256 = "sha256-vRch3/CkvkgKEB8sqi2WK4MlL/+ucDI9DgA9hqVi4VU="; + }; + "aarch64-darwin" = { + file = "deptrust_v${version}_darwin_arm64.tar.gz"; + sha256 = "sha256-yvSrmO55FFTyQAFDTyFAri4Xcp8Z/dUY7UYXtF6wjag="; + }; + }; + + systems = builtins.attrNames assets; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + + deptrustFor = system: let + pkgs = nixpkgs.legacyPackages.${system}; + asset = assets.${system}; + in pkgs.stdenv.mkDerivation { + pname = "deptrust"; + inherit version; + + src = pkgs.fetchurl { + url = "https://github.com/clidey/deptrust/releases/download/v${version}/${asset.file}"; + sha256 = asset.sha256; + }; + + 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 + mkdir -p $out/bin + install -Dm755 deptrust $out/bin/deptrust + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Local package vulnerability checker and MCP server for AI agents"; + homepage = "https://github.com/clidey/deptrust"; + downloadPage = "https://github.com/clidey/deptrust/releases"; + license = licenses.mit; + mainProgram = "deptrust"; + platforms = systems; + sourceProvenance = [ sourceTypes.binaryNativeCode ]; + }; + }; + in { + packages = forAllSystems (system: rec { + deptrust = deptrustFor system; + default = deptrust; + }); + + apps = forAllSystems (system: let + deptrustPkg = deptrustFor system; + in { + deptrust = { + type = "app"; + program = "${deptrustPkg}/bin/deptrust"; + }; + default = { + type = "app"; + program = "${deptrustPkg}/bin/deptrust"; + }; + }); + }; +}