Archon/.github/workflows/release.yml
Rasmus Widing 81859d6842
fix(providers): replace Claude SDK embed with explicit binary-path resolver (#1217)
* feat(providers): replace Claude SDK embed with explicit binary-path resolver

Drop `@anthropic-ai/claude-agent-sdk/embed` and resolve Claude Code via
CLAUDE_BIN_PATH env → assistants.claude.claudeBinaryPath config → throw
with install instructions. The embed's silent failure modes on macOS
(#1210) and Windows (#1087) become actionable errors with a documented
recovery path.

Dev mode (bun run) remains auto-resolved via node_modules. The setup
wizard auto-detects Claude Code by probing the native installer path
(~/.local/bin/claude), npm global cli.js, and PATH, then writes
CLAUDE_BIN_PATH to ~/.archon/.env. Dockerfile pre-sets CLAUDE_BIN_PATH
so extenders using the compiled binary keep working. Release workflow
gets negative and positive resolver smoke tests.

Docs, CHANGELOG, README, .env.example, CLAUDE.md, test-release and
archon skills all updated to reflect the curl-first install story.

Retires #1210, #1087, #1091 (never merged, now obsolete).
Implements #1176.

* fix(providers): only pass --no-env-file when spawning Claude via Bun/Node

`--no-env-file` is a Bun flag that prevents Bun from auto-loading
`.env` from the subprocess cwd. It is only meaningful when the Claude
Code executable is a `cli.js` file — in which case the SDK spawns it
via `bun`/`node` and the flag reaches the runtime.

When `CLAUDE_BIN_PATH` points at a native compiled Claude binary (e.g.
`~/.local/bin/claude` from the curl installer, which is Anthropic's
recommended default), the SDK executes the binary directly. Passing
`--no-env-file` then goes straight to the native binary, which
rejects it with `error: unknown option '--no-env-file'` and the
subprocess exits code 1.

Emit `executableArgs` only when the target is a `.js` file (dev mode
or explicit cli.js path). Caught by end-to-end smoke testing against
the curl-installed native Claude binary.

* docs: record env-leak validation result in provider comment

Verified end-to-end with sentinel `.env` and `.env.local` files in a
workflow CWD that the native Claude binary (curl installer) does not
auto-load `.env` files. With Archon's full spawn pathway and parent
env stripped, the subprocess saw both sentinels as UNSET. The
first-layer protection in `@archon/paths` (#1067) handles the
inheritance leak; `--no-env-file` only matters for the Bun-spawned
cli.js path, where it is still emitted.

* chore(providers): cleanup pass — exports, docs, troubleshooting

Final-sweep cleanup tied to the binary-resolver PR:

- Mirror Codex's package surface for the new Claude resolver: add
  `./claude/binary-resolver` subpath export and re-export
  `resolveClaudeBinaryPath` + `claudeFileExists` from the package
  index. Renames the previously single `fileExists` re-export to
  `codexFileExists` for symmetry; nothing outside the providers
  package was importing it.
- Add a "Claude Code not found" entry to the troubleshooting reference
  doc with platform-specific install snippets and pointers to the
  AI Assistants binary-path section.
- Reframe the example claudeBinaryPath in reference/configuration.md
  away from cli.js-only language; it accepts either the native binary
  or cli.js.

* test+refactor(providers, cli): address PR review feedback

Two test gaps and one doc nit from the PR review (#1217):

- Extract the `--no-env-file` decision into a pure exported helper
  `shouldPassNoEnvFile(cliPath)` so the native-binary branch is unit
  testable without mocking `BUNDLED_IS_BINARY` or running the full
  sendQuery pathway. Six new tests cover undefined, cli.js, native
  binary (Linux + Windows), Homebrew symlink, and suffix-only matching.
  Also adds a `claude.subprocess_env_file_flag` debug log so the
  security-adjacent decision is auditable.

- Extract the three install-location probes in setup.ts into exported
  wrappers (`probeFileExists`, `probeNpmRoot`, `probeWhichClaude`) and
  export `detectClaudeExecutablePath` itself, so the probe order can be
  spied on. Six new tests cover each tier winning, fall-through
  ordering, npm-tier skip when not installed, and the
  which-resolved-but-stale-path edge case.

- CLAUDE.md `claudeBinaryPath` placeholder updated to reflect that the
  field accepts either the native binary or cli.js (the example value
  was previously `/absolute/path/to/cli.js`, slightly misleading now
  that the curl-installer native binary is the default).

Skipped from the review by deliberate scope decision:

- `resolveClaudeBinaryPath` async-with-no-await: matches Codex's
  resolver signature exactly. Changing only Claude breaks symmetry;
  if pursued, do both providers in a separate cleanup PR.
- `isAbsolute()` validation in parseClaudeConfig: Codex doesn't do it
  either. Resolver throws on non-existence already.
- Atomic `.env` writes in setup wizard: pre-existing pattern this PR
  touched only adjacently. File as separate issue if needed.
- classifyError branch in dag-executor for setup errors: scope creep.
- `.env.example` "missing #" claim: false positive (verified all
  CLAUDE_BIN_PATH lines have proper comment prefixes).

* fix(test): use path.join in Windows-compatible probe-order test

The "tier 2 wins (npm cli.js)" test hardcoded forward-slash path
comparisons, but `path.join` produces backslashes on Windows. Caused
the Windows CI leg of the test suite to fail while macOS and Linux
passed. Use `path.join` for both the mock return value and the
expectation so the separator matches whatever the platform produces.
2026-04-14 17:56:37 +03:00

357 lines
13 KiB
YAML

name: Release CLI Binaries
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g., v0.2.0)'
required: true
type: string
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: bun-linux-x64
binary: archon-linux-x64
- os: ubuntu-latest
target: bun-linux-arm64
binary: archon-linux-arm64
- os: ubuntu-latest
target: bun-windows-x64
binary: archon-windows-x64.exe
- os: macos-latest
target: bun-darwin-x64
binary: archon-darwin-x64
- os: macos-latest
target: bun-darwin-arm64
binary: archon-darwin-arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build binary
env:
# On workflow_dispatch, github.ref_name is the branch name (e.g. 'main'),
# not the version tag — fall back to the user-supplied `version` input.
VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
GIT_COMMIT: ${{ github.sha }}
TARGET: ${{ matrix.target }}
OUTFILE: dist/${{ matrix.binary }}
run: |
# Strip 'v' prefix from tag (e.g. v0.3.1 → 0.3.1)
VERSION="${VERSION#v}"
# Short commit (first 8 chars of SHA)
GIT_COMMIT="${GIT_COMMIT::8}"
mkdir -p dist
VERSION="$VERSION" GIT_COMMIT="$GIT_COMMIT" TARGET="$TARGET" OUTFILE="$OUTFILE" bash scripts/build-binaries.sh
- name: Smoke-test built binary
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
env:
RAW_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
run: |
chmod +x dist/${{ matrix.binary }}
if ! VERSION_OUTPUT=$(./dist/${{ matrix.binary }} version 2>&1); then
echo "::error::Binary failed to execute"
echo "$VERSION_OUTPUT"
exit 1
fi
echo "$VERSION_OUTPUT"
# Must not error with "Failed to read version" or similar
if echo "$VERSION_OUTPUT" | grep -qE "Failed to read version|package\.json not found|bad installation"; then
echo "::error::Binary is broken — version command cannot read embedded version"
echo "::error::This means BUNDLED_IS_BINARY was not set to true at build time."
exit 1
fi
# Must report 'Build: binary', not 'Build: source'
if ! echo "$VERSION_OUTPUT" | grep -q "Build: binary"; then
echo "::error::Binary reports wrong build type"
echo "::error::Expected 'Build: binary' in version output"
exit 1
fi
# Must report the (stripped) tag version. Compare against the same
# value that was baked into the binary (VERSION#v), not the raw ref,
# so the check doesn't rely on the CLI re-adding a 'v' prefix.
EXPECTED_VERSION="${RAW_VERSION#v}"
if echo "$VERSION_OUTPUT" | grep -qE "v?${EXPECTED_VERSION}(\s|$)"; then
echo "::notice::Binary correctly reports version ${EXPECTED_VERSION}"
else
echo "::error::Binary does not report version ${EXPECTED_VERSION}"
exit 1
fi
- name: Smoke-test bundled defaults load
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
run: |
# `workflow list` requires running from a git repo
BIN="$PWD/dist/${{ matrix.binary }}"
TMP_REPO=$(mktemp -d)
cd "$TMP_REPO"
git init -q
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init
if ! OUTPUT=$("$BIN" workflow list 2>&1); then
echo "::error::workflow list failed to execute"
echo "$OUTPUT"
exit 1
fi
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q "archon-assist"; then
echo "::notice::Bundled workflows loaded correctly"
else
echo "::error::Bundled workflows did not load — embedded JSON may be missing from the binary"
exit 1
fi
- name: Smoke-test Claude binary-path resolver (negative case)
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
run: |
# With no CLAUDE_BIN_PATH and no config, running a Claude workflow must
# fail with a clear, user-facing error — NOT with "Module not found
# /Users/runner/..." which would indicate the resolver was bypassed.
BIN="$PWD/dist/${{ matrix.binary }}"
TMP_REPO=$(mktemp -d)
cd "$TMP_REPO"
git init -q
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init
# Run without CLAUDE_BIN_PATH set. Expect a clean resolver error.
# Capture both stdout and stderr; we only care that the resolver message is present.
set +e
OUTPUT=$(env -u CLAUDE_BIN_PATH "$BIN" workflow run archon-assist "hello" 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"
if echo "$OUTPUT" | grep -qE 'Module not found.*Users/runner'; then
echo "::error::Resolver was bypassed — SDK hit the import.meta.url fallback (regression of #1210)"
exit 1
fi
if ! echo "$OUTPUT" | grep -q "Claude Code not found"; then
echo "::error::Expected 'Claude Code not found' error when CLAUDE_BIN_PATH is unset"
exit 1
fi
if ! echo "$OUTPUT" | grep -q "CLAUDE_BIN_PATH"; then
echo "::error::Error message does not reference CLAUDE_BIN_PATH remediation"
exit 1
fi
echo "::notice::Resolver error path works (exit code: $EXIT_CODE)"
- name: Smoke-test Claude subprocess spawn (positive case)
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
run: |
# Install Claude Code via the native installer (Anthropic's recommended
# default) and run a workflow with CLAUDE_BIN_PATH set. The subprocess
# must spawn cleanly. We do NOT require the query to succeed (no auth
# in CI — an auth error is fine and expected); we only fail if the SDK
# can't find the executable, which would indicate a resolver regression.
curl -fsSL https://claude.ai/install.sh | bash
CLI_PATH="$HOME/.local/bin/claude"
if [ ! -x "$CLI_PATH" ]; then
echo "::error::Claude Code binary not found after curl install at $CLI_PATH"
ls -la "$HOME/.local/bin/" || true
exit 1
fi
echo "Using CLAUDE_BIN_PATH=$CLI_PATH"
BIN="$PWD/dist/${{ matrix.binary }}"
TMP_REPO=$(mktemp -d)
cd "$TMP_REPO"
git init -q
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init
set +e
OUTPUT=$(CLAUDE_BIN_PATH="$CLI_PATH" "$BIN" workflow run archon-assist "hello" 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"
if echo "$OUTPUT" | grep -qE 'Module not found.*(cli\.js|Users/runner)'; then
echo "::error::Subprocess could not find the executable (resolver regression)"
exit 1
fi
if echo "$OUTPUT" | grep -q "Claude Code not found"; then
echo "::error::Resolver failed even though CLAUDE_BIN_PATH was set to an existing file"
exit 1
fi
# Any of these outcomes are acceptable — they prove the subprocess spawned:
# - auth error ("credit balance", "unauthorized", "authentication")
# - rate-limit / API error
# - successful query (if auth was injected via some other mechanism)
echo "::notice::Claude subprocess spawn path is healthy (exit code: $EXIT_CODE)"
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary }}
path: dist/${{ matrix.binary }}
retention-days: 1
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build web UI
run: bun --filter @archon/web build
- name: Package web dist
run: |
tar czf dist/archon-web.tar.gz -C packages/web/dist .
- name: Generate checksums
run: |
cd dist
sha256sum archon-* archon-web.tar.gz > checksums.txt
cat checksums.txt
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.version }}
name: Archon CLI ${{ steps.version.outputs.version }}
draft: false
prerelease: ${{ contains(steps.version.outputs.version, '-') }}
generate_release_notes: true
files: |
dist/archon-*
dist/archon-web.tar.gz
dist/checksums.txt
body: |
## Installation
### Quick Install (Recommended)
**macOS / Linux**
```bash
curl -fsSL https://archon.diy/install | bash
```
**Windows (PowerShell)**
```powershell
irm https://archon.diy/install.ps1 | iex
```
**Homebrew (macOS / Linux)**
```bash
brew install coleam00/archon/archon
```
**Docker**
```bash
docker run --rm -v "$PWD:/workspace" ghcr.io/coleam00/archon:latest workflow list
```
### Manual Installation
**macOS (Apple Silicon)**
```bash
curl -fsSL https://github.com/coleam00/Archon/releases/latest/download/archon-darwin-arm64 -o /usr/local/bin/archon
chmod +x /usr/local/bin/archon
```
**macOS (Intel)**
```bash
curl -fsSL https://github.com/coleam00/Archon/releases/latest/download/archon-darwin-x64 -o /usr/local/bin/archon
chmod +x /usr/local/bin/archon
```
**Linux (x64)**
```bash
curl -fsSL https://github.com/coleam00/Archon/releases/latest/download/archon-linux-x64 -o /usr/local/bin/archon
chmod +x /usr/local/bin/archon
```
**Linux (ARM64)**
```bash
curl -fsSL https://github.com/coleam00/Archon/releases/latest/download/archon-linux-arm64 -o /usr/local/bin/archon
chmod +x /usr/local/bin/archon
```
**Windows (Manual)**
Download `archon-windows-x64.exe` from the assets below, rename to `archon.exe`, and add to your PATH.
### Verify installation
```bash
archon version
```
update-homebrew:
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: dev
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Wait for release assets
run: sleep 30
- name: Update Homebrew formula
run: bash scripts/update-homebrew.sh ${{ steps.version.outputs.version }}
- name: Commit updated formula
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add homebrew/archon.rb
git diff --cached --quiet || git commit -m "chore: update Homebrew formula for ${{ steps.version.outputs.version }}"
git push origin dev