From e1d9ad6b60578171ab525872e0341c5566a8a1a2 Mon Sep 17 00:00:00 2001 From: levonk <277861+levonk@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:06:56 -0700 Subject: [PATCH] feat: add Nix flake and Devbox support Add Nix flake support so Archon can be installed via: nix run github:coleam00/Archon nix profile install github:coleam00/Archon The flake exposes both #prebuilt (prebuilt release binary, also #default) and #source (from-source build via bun build --compile). A daily nix-release.yml workflow auto-bumps version + per-platform sha256 hashes when a new release is detected. CI (nix.yml) validates the flake on every change to Nix files. Adds devbox.json for reproducible development environments (bun only). Updates README, installation docs, landing page, and release guide. Closes #1766 --- .github/workflows/nix-release.yml | 123 ++++++++++ .github/workflows/nix.yml | 81 +++++++ .gitignore | 6 + README.md | 43 ++++ devbox.json | 18 ++ flake.lock | 44 ++++ flake.nix | 221 ++++++++++++++++++ .../content/docs/contributing/releasing.md | 7 + packages/docs-web/src/content/docs/docs.mdx | 9 + .../docs/getting-started/installation.md | 32 +++ 10 files changed, 584 insertions(+) create mode 100644 .github/workflows/nix-release.yml create mode 100644 .github/workflows/nix.yml create mode 100644 devbox.json create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.github/workflows/nix-release.yml b/.github/workflows/nix-release.yml new file mode 100644 index 0000000000..a790cd8b81 --- /dev/null +++ b/.github/workflows/nix-release.yml @@ -0,0 +1,123 @@ +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 (via softprops/action-gh-release), 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 == 'coleam00/Archon' + 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: + # system|asset-substring — one per line. The substring must uniquely + # match the release asset filename for that system. + ASSET_MAP: | + x86_64-linux|archon-linux-x64 + aarch64-linux|archon-linux-arm64 + aarch64-darwin|archon-darwin-arm64 + 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 so a binary substring does not also + # match its companion checksum file. + 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: dev + 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 0000000000..ada5a5f8ce --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,81 @@ +name: Nix flake + +# Validates the flake (flake.nix). For most nixify targets Nix is a side +# concern, so this job is path-filtered to the flake files — it fires only +# when they change, not on every source/docs commit. +# +# Steps, in order of what they catch: +# 1. nix flake check --all-systems — every system's outputs evaluate +# (including darwin on an ubuntu runner). +# 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. +# This is the only step that catches the `let ... in rec` shadowing +# class of bug (passes flake check, fails nix run). Do NOT drop it. +# 4. nix build .#source (if #source output exists) — the from-source +# build path realises for the runner's system. Skip if the flake +# does not expose a #source output. + +on: + push: + branches: [dev] + paths: + - "flake.nix" + - "flake.lock" + - "**/*.nix" + - ".github/workflows/nix.yml" + pull_request: + branches: [dev] + 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 + # DeterminateSystems/nix-installer-action installs Nix natively on the + # runner so `nix build` / `nix run` work directly (a Docker-container + # approach can run `nix flake check` but is awkward for build+smoke). + uses: DeterminateSystems/nix-installer-action@v16 + + - name: nix flake check --all-systems + # --no-build: evaluate every system's outputs (including darwin on + # ubuntu) without realising them. Without --no-build, `nix flake + # check` builds every derivation in `checks`, which fails for + # non-native systems (darwin stdenv can't run on linux). The + # build/run steps below handle realisation for the runner's system. + run: nix flake check --all-systems --no-build + + - name: nix build .#default + run: nix build .#default --print-build-logs + + - name: nix run .#default -- --version + run: nix run .#default -- --version + + - name: nix build .#source (if exists) + # Exercises the from-source build path. Skip if the flake does not + # expose a #source output (source-build-only flakes use #default). + run: | + if nix flake show --json 2>/dev/null | jq -e 'any(.packages[]?; has("source"))' >/dev/null 2>&1; then + nix build .#source --print-build-logs + else + echo "No #source output — skipping" + fi diff --git a/.gitignore b/.gitignore index 0e9038218c..51ef7ddf23 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,9 @@ packages/server/.env skills-lock.json test-results/ .archon/ralph/ + +# Nix build result symlinks +/result +/result-* +# Devbox generated artifacts +.devbox/ diff --git a/README.md b/README.md index c70f3437e4..a53fc99895 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,49 @@ irm https://archon.diy/install.ps1 | iex brew install coleam00/archon/archon ``` +### Nix + +The project provides optional Nix flake outputs for users who already use Nix. The flake exposes the prebuilt release binary as `#prebuilt` (also `#default`) and a from-source build as `#source`. + +```bash +# Run without installing (prebuilt binary, default) +nix run github:coleam00/Archon + +# Install into your profile +nix profile add github:coleam00/Archon + +# Explicitly choose prebuilt or source +nix run github:coleam00/Archon#prebuilt +nix run github:coleam00/Archon#source +``` + +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:coleam00/Archon` +is updated daily when the version-bump PR is merged. (Release tags are cut before +the bump lands, so `github:coleam00/Archon/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 +``` + > **Compiled binaries need a `CLAUDE_BIN_PATH`.** The quick-install binaries > don't bundle Claude Code. Install it separately, then point Archon at it: > diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000000..66a2abf0ce --- /dev/null +++ b/devbox.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json", + "packages": [ + "bun" + ], + "shell": { + "init_hook": [ + "echo 'Welcome to the Archon Devbox environment!'" + ], + "scripts": { + "install": "bun install", + "build": "bun run build", + "test": "bun run test", + "dev": "bun run dev", + "validate": "bun run validate" + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..0568806078 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1784115452, + "narHash": "sha256-BoYPdqk6jlKXy+DyUzyGV/CtRGfAhk2MmIgBhsemTGI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "35d3407a3816f3b341d8cf1d60abaf2b7b8166ac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-darwin": { + "locked": { + "lastModified": 1784103219, + "narHash": "sha256-MyxoglQA10PAmtH4X8XA38Js2pryooWH7oSAfHRKGHU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f8dc94623d99b76cbe2ba5c9d83b465aab073d30", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-26.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "nixpkgs-darwin": "nixpkgs-darwin" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..a4f2af488a --- /dev/null +++ b/flake.nix @@ -0,0 +1,221 @@ +{ + description = "Archon - The first open-source harness builder for AI coding"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + # nixpkgs-unstable (26.11) dropped x86_64-darwin. Use the 26.05 darwin + # stable branch for darwin systems — it still supports Intel Macs until + # end of 2026. Linux systems use nixpkgs-unstable. + nixpkgs-darwin.url = "github:NixOS/nixpkgs/nixpkgs-26.05-darwin"; + }; + + outputs = { self, nixpkgs, nixpkgs-darwin }: let + # Bump version for each release. The nix-release.yml workflow auto-updates + # this line and the per-platform sha256 hashes below when a new release is + # published — no manual editing required. + version = "0.5.0"; + + assets = { + "x86_64-linux" = { + file = "archon-linux-x64"; + sha256 = "sha256-3/FrgQoHNsZRyt/7TwzvjspJHzwhN9ZKojJMBzM/tFU="; + }; + "aarch64-linux" = { + file = "archon-linux-arm64"; + sha256 = "sha256-dMhniBIeOG/nwwncUXhBfqVVeV/JKATX69wfFumNYIA="; + }; + "x86_64-darwin" = { + file = "archon-darwin-x64"; + sha256 = "sha256-gtRvL59SBYXH4DDLSPbX8XTFNIEKlCJ62M6DuzJV7lk="; + }; + "aarch64-darwin" = { + file = "archon-darwin-arm64"; + sha256 = "sha256-MlinhBP2zA64+yFLMpO8y722cMCclzghYz7keMbZG9E="; + }; + }; + + systems = builtins.attrNames assets; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + + # Select the right nixpkgs input per system: darwin uses the 26.05-darwin + # branch (still supports x86_64-darwin), linux uses unstable. + pkgsFor = system: + if nixpkgs.lib.hasSuffix "darwin" system + then nixpkgs-darwin.legacyPackages.${system} + else nixpkgs.legacyPackages.${system}; + + # Prebuilt binary from GitHub release (raw compiled binary, not a tarball) + projectFor = system: let + pkgs = pkgsFor system; + asset = assets.${system}; + in pkgs.stdenv.mkDerivation { + pname = "archon"; + inherit version; + + src = pkgs.fetchurl { + url = "https://github.com/coleam00/Archon/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 ]; + + dontUnpack = true; + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + install -Dm755 $src $out/bin/archon + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Archon - The first open-source harness builder for AI coding"; + homepage = "https://github.com/coleam00/Archon"; + downloadPage = "https://github.com/coleam00/Archon/releases"; + license = licenses.mit; + mainProgram = "archon"; + platforms = systems; + sourceProvenance = [ sourceTypes.binaryNativeCode ]; + }; + }; + + # From-source build — replicates scripts/build-binaries.sh: + # 1. bun install --frozen-lockfile (via deps FOD for sandbox network isolation) + # 2. bun run scripts/generate-bundled-defaults.ts (code generation) + # 3. Rewrite packages/paths/src/bundled-build.ts with version/commit constants + # 4. bun build --compile --minify --target= --outfile=archon packages/cli/src/cli.ts + sourceFor = system: let + pkgs = pkgsFor system; + bunTarget = { + x86_64-linux = "bun-linux-x64"; + aarch64-linux = "bun-linux-arm64"; + x86_64-darwin = "bun-darwin-x64"; + aarch64-darwin = "bun-darwin-arm64"; + }.${system} or (throw "Unsupported platform: ${system}"); + + # Fixed-output derivation: runs `bun install` with network access, + # produces a node_modules store path. The hash must be updated when + # bun.lock changes — the nix-release.yml workflow does not yet automate + # this; run `nix build .#source` after changing bun.lock to get the + # correct hash from the mismatch error. + # ponytail: FOD hash must be updated when bun.lock changes; no automation yet. + deps = pkgs.stdenv.mkDerivation { + pname = "archon-deps"; + inherit version; + src = ./.; + + nativeBuildInputs = [ pkgs.bun ]; + + impureEnvVars = [ "HOME" "XDG_CACHE_HOME" ]; + BUN_INSTALL_CACHE_DIR = "$TMPDIR/bun-cache"; + + dontBuild = true; + dontConfigure = true; + + installPhase = '' + runHook preInstall + bun install --frozen-lockfile + mkdir -p $out + cp -r node_modules $out/node_modules + runHook postInstall + ''; + + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + outputHash = "sha256-+/vRTaUhwDIRj34eKYzucTr5+jRCmgCqbf6/4XyD2fQ="; + }; + in pkgs.stdenv.mkDerivation { + pname = "archon-source"; + inherit version; + src = ./.; + + nativeBuildInputs = [ pkgs.bun ]; + + BUN_INSTALL_CACHE_DIR = "$TMPDIR/bun-cache"; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + + # Use pre-fetched node_modules from the deps FOD + cp -r ${deps}/node_modules ./node_modules + + # Step 1: Regenerate bundled defaults from .archon/{commands,workflows}/defaults/ + bun run scripts/generate-bundled-defaults.ts + + # Step 2: Rewrite build-time constants (replicate scripts/build-binaries.sh) + cat > packages/paths/src/bundled-build.ts << 'BUNDEDEOF' + export const BUNDLED_IS_BINARY = true; + export const BUNDLED_VERSION = '${version}'; + export const BUNDLED_GIT_COMMIT = 'nix-build'; + BUNDEDEOF + + # Step 3: Build standalone binary via bun build --compile + bun build --compile --minify --target ${bunTarget} \ + --outfile archon \ + packages/cli/src/cli.ts + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + install -Dm755 archon $out/bin/archon + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Archon - The first open-source harness builder for AI coding"; + homepage = "https://github.com/coleam00/Archon"; + license = licenses.mit; + mainProgram = "archon"; + platforms = systems; + }; + }; + in { + packages = forAllSystems (system: rec { + archon = projectFor system; + prebuilt = archon; + default = prebuilt; + source = sourceFor system; + }); + + apps = forAllSystems (system: let + # WARNING: do NOT replace this `let` binding with `rec` referencing the + # `packages` attrset above. A `rec { default = { program = "${archon}/bin/..."; }; }` + # that names the binding `archon` shadows the `let`-bound derivation, so + # `${archon}` 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`. + archonPkg = projectFor system; + sourcePkg = sourceFor system; + in { + archon = { + type = "app"; + program = "${archonPkg}/bin/archon"; + }; + prebuilt = { + type = "app"; + program = "${archonPkg}/bin/archon"; + }; + default = { + type = "app"; + program = "${archonPkg}/bin/archon"; + }; + source = { + type = "app"; + program = "${sourcePkg}/bin/archon"; + }; + }); + + checks = forAllSystems (system: { + # CI exercises both the prebuilt and source outputs + prebuilt = projectFor system; + source = sourceFor system; + }); + }; +} diff --git a/packages/docs-web/src/content/docs/contributing/releasing.md b/packages/docs-web/src/content/docs/contributing/releasing.md index f2df7234d2..a650f441e0 100644 --- a/packages/docs-web/src/content/docs/contributing/releasing.md +++ b/packages/docs-web/src/content/docs/contributing/releasing.md @@ -106,6 +106,13 @@ archon version > archon version > ``` +### 5. Update Nix Flake (Automatic) + +The `.github/workflows/nix-release.yml` workflow runs daily and automatically +bumps `version` and per-platform `sha256` hashes in `flake.nix` when a new +release is detected, then opens a PR. No manual action required — just merge +the auto-generated bump PR after each release. + ## Manual Release (When GitHub Actions Unavailable) If GitHub Actions can't run (billing issues, private repo limits), create the release manually: diff --git a/packages/docs-web/src/content/docs/docs.mdx b/packages/docs-web/src/content/docs/docs.mdx index 69b4b4fb2f..8552435554 100644 --- a/packages/docs-web/src/content/docs/docs.mdx +++ b/packages/docs-web/src/content/docs/docs.mdx @@ -45,6 +45,15 @@ brew install coleam00/archon/archon docker run --rm -v "$PWD:/workspace" ghcr.io/coleam00/archon:latest workflow list ``` +```bash [Nix] +# Latest release (auto-bumped daily, prebuilt) +nix run github:coleam00/Archon +nix profile add github:coleam00/Archon + +# Or choose explicitly: #prebuilt (fast) or #source (from source) +nix run github:coleam00/Archon#source +``` + ::: ## What is Archon? diff --git a/packages/docs-web/src/content/docs/getting-started/installation.md b/packages/docs-web/src/content/docs/getting-started/installation.md index 20bf4eb32b..8ef567dc22 100644 --- a/packages/docs-web/src/content/docs/getting-started/installation.md +++ b/packages/docs-web/src/content/docs/getting-started/installation.md @@ -33,6 +33,38 @@ brew install coleam00/archon/archon docker run --rm -v "$PWD:/workspace" ghcr.io/coleam00/archon:latest workflow list ``` +### Nix (Flakes) + +For users who already have Nix with flakes enabled: + +```bash +# Run without installing (prebuilt binary, default) +nix run github:coleam00/Archon + +# Install into your profile +nix profile add github:coleam00/Archon + +# Explicitly choose prebuilt or source +nix run github:coleam00/Archon#prebuilt +nix run github:coleam00/Archon#source +``` + +The flake tracks the default branch and is auto-bumped to the latest release +daily, so `github:coleam00/Archon` is updated daily when the version-bump PR +is merged. For reproducibility, pin to a specific commit SHA or use the nixpkgs +package. + +**Updating:** + +```bash +# For profile installs +nix profile upgrade + +# For flake-based installs (e.g., via flake inputs) +# Run from the consuming flake directory, using the actual input name (e.g. archon) +nix flake update archon +``` + ## From Source ```bash