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