Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 30 additions & 37 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -325,17 +325,23 @@ jobs:
if ($output -match "Failed to load extension") { exit 1 }
if ($status -ne 0 -and $output -notmatch "No models available|No model selected|No API key found") { exit $status }

# The one leg that proves musl at runtime rather than at link time. Docker is the
# only way to reach a musl userspace from a Blacksmith Ubuntu runner, and the
# native binding is downloaded rather than rebuilt so this job costs an archive
# build instead of a second zig cross-compile.
# Build and run each musl archive on the matching stock Alpine architecture.
# The native binding is downloaded rather than rebuilt, so these legs prove
# the complete release payload without a second Zig cross-compile.
alpine-binary-smoke:
name: Smoke Alpine musl binary
name: Smoke Alpine musl binary (${{ matrix.arch }})
needs: [integrity, native-artifacts]
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 12
runs-on: ${{ matrix.runner }}
timeout-minutes: 14
strategy:
fail-fast: false
matrix:
include:
- { runner: blacksmith-4vcpu-ubuntu-2404, arch: x64, slug: linux-x64-musl, platform: linux-x64-musl, docker_platform: linux/amd64 }
- { runner: blacksmith-4vcpu-ubuntu-2404-arm, arch: arm64, slug: linux-arm64-musl, platform: linux-arm64-musl, docker_platform: linux/arm64 }
steps:
- uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1.1
if: runner.os == 'Linux'
with:
ref: ${{ env.SOURCE_REF }}
persist-credentials: false
Expand All @@ -345,47 +351,30 @@ jobs:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Install musl archive tooling
run: |
sudo apt-get update
sudo apt-get install -y patchelf
- name: Download musl native binding
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: atomic-natives-linux-x64-musl
name: atomic-natives-${{ matrix.slug }}
path: packages/natives/native
- name: Build Linux x64 musl archive
run: ./scripts/build-binaries.sh --skip-deps --platform linux-x64-musl
- name: Smoke musl release archive on Alpine
- name: Build Linux musl archive
run: ./scripts/build-binaries.sh --skip-deps --platform "${{ matrix.platform }}"
- name: Smoke real musl release archive on stock Alpine
run: ./scripts/test-musl-release-archive.sh "packages/coding-agent/binaries/atomic-${{ matrix.platform }}.tar.gz" "${{ matrix.platform }}"
- name: Load the musl native binding under musl libc
shell: bash
run: |
set -euo pipefail
smoke="$RUNNER_TEMP/atomic-alpine-smoke"
smoke="$RUNNER_TEMP/atomic-alpine-native-smoke"
rm -rf "$smoke"
mkdir -p "$smoke"
tar -xzf packages/coding-agent/binaries/atomic-linux-x64-musl.tar.gz -C "$smoke"
cat > "$smoke/smoke.sh" <<'SMOKE'
set -eu
apk add --no-cache libgcc libstdc++
atomic=/smoke/atomic/atomic
"$atomic" --version
mkdir -p /tmp/atomic-alpine-cwd
cd /tmp/atomic-alpine-cwd
set +e
output=$(printf '' | "$atomic" --no-session 2>&1)
status=$?
set -e
echo "$output"
if echo "$output" | grep -q 'Failed to load extension'; then exit 1; fi
if [ "$status" -ne 0 ] && ! echo "$output" | grep -Eq 'No models available|No model selected|No API key found'; then
exit "$status"
fi
SMOKE
docker run --rm -v "$smoke:/smoke" alpine:3.22 /bin/sh /smoke/smoke.sh
- name: Load the musl native binding under musl libc
shell: bash
run: |
set -euo pipefail
smoke="$RUNNER_TEMP/atomic-alpine-smoke"
tar -xzf "packages/coding-agent/binaries/atomic-${{ matrix.platform }}.tar.gz" -C "$smoke"
# `atomic` catches a failed binding load and degrades to the JS search
# paths, so starting is not proof the addon dlopen'd. Require it here.
docker run --rm -v "$smoke:/smoke" node:22-alpine node -e '
docker run --rm --platform "${{ matrix.docker_platform }}" -v "$smoke:/smoke:ro" node:22-alpine node -e '
const binding = require("/smoke/atomic/node_modules/@bastani/atomic-natives");
for (const name of ["glob", "grep"]) {
if (typeof binding[name] !== "function") throw new Error(`musl binding is missing ${name}()`);
Expand Down Expand Up @@ -424,6 +413,10 @@ jobs:
bun run --cwd packages/natives create-npm-dirs
bun run --cwd packages/natives artifacts
bun run --cwd packages/natives prepublish:native -- --skip-optional-publish
- name: Install musl archive tooling
run: |
sudo apt-get update
sudo apt-get install -y patchelf
- name: Build release archives
run: ./scripts/build-binaries.sh --skip-install
- name: Validate package metadata and prepare payload
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ jobs:
# own runner rather than through the workspace suites.
- name: Script tests
run: npm run test:scripts
- name: POSIX installer smoke tests in Alpine and Debian slim
run: ./scripts/test-installers-containers.sh
- name: Deterministic CI and release contracts
run: npm run test:ci-contracts

Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,31 @@

### Prerequisites

- **Node.js 22.19 or newer** — check with `node --version`.
- **A package manager** — use npm, pnpm, Yarn, or Bun. Use Bun 1.3.14+ for Bun installs or workflow-authoring examples.
- **Release archive install:** macOS and Linux need `tar` and either `curl` or `wget`. Windows uses built-in PowerShell commands. This path does not need Node.js or a package manager.
- **Package install:** Node.js 22.19 or newer plus npm, pnpm, Yarn, or Bun. Use Bun 1.3.14+ for Bun installs or workflow-authoring examples.
- **Model-provider access** — use a supported subscription login or API key.

### Install

With npm:
Install the self-contained release archive on macOS or Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/bastani-inc/atomic/main/install.sh | sh
```

On Windows, run this in PowerShell:

```powershell
irm https://raw.githubusercontent.com/bastani-inc/atomic/main/install.ps1 | iex
```

The archive installer verifies `SHA256SUMS`, keeps versioned payloads, and links its launcher from `~/.local/bin/atomic` on macOS/Linux or `%LOCALAPPDATA%\atomic\bin\atomic.cmd` on Windows. It prints PATH guidance when needed. Set `ATOMIC_VERSION` to pin a release, `ATOMIC_INSTALL_DIR` or `ATOMIC_BIN_DIR` to change those locations, and `GITHUB_TOKEN` or `GH_TOKEN` if shared GitHub API limits are a concern.

On macOS/Linux, relative install and bin directories resolve against the physical directory where the installer starts. The install root cannot equal or sit inside the `ATOMIC_BIN_DIR/atomic` launcher path, and `ATOMIC_BIN_DIR` cannot sit inside the install root's `current` or `versions` directories. On Windows, a same-stem launcher that `PATHEXT` resolves before `atomic.cmd`, such as a stale `atomic.exe`, is reported before any download. Exact pins use Atomic release tags in `MAJOR.MINOR.PATCH` or `MAJOR.MINOR.PATCH-alpha.REVISION` form.

The Linux musl archives bundle their C++ runtime libraries and run on stock Alpine without an `apk add` step. Android and Termux use bionic rather than musl and remain unsupported by the release archives.

Package installs still require Node.js and a package manager. With npm:

```bash
npm install -g @bastani/atomic
Expand All @@ -81,7 +99,7 @@ With Bun:
bun add -g @bastani/atomic
```

Atomic does not require package install scripts. Add `--ignore-scripts` to the install command if you want to disable dependency lifecycle scripts during installation.
Atomic does not require package install scripts. Add `--ignore-scripts` to a package install command if you want to disable dependency lifecycle scripts.

### Authenticate and run

Expand Down Expand Up @@ -112,7 +130,7 @@ After authenticating, run `/atomic` for workflow guides, examples, and next step
<details>
<summary><b>Devcontainer, terminal, and SDK references</b></summary>

Atomic runs in a standard devcontainer or VM with Node.js 22.19+ installed. Install it inside the container with a package manager and pass provider credentials through environment variables.
Atomic runs in a standard devcontainer or VM. Use the release-archive installer for an image without Node.js or npm, or install Node.js 22.19+ and use a package manager. Pass provider credentials through environment variables.

See [Terminal setup](./packages/coding-agent/docs/terminal-setup.md), [Security](./packages/coding-agent/docs/security.md), and [Programmatic Usage](./packages/coding-agent/README.md#programmatic-usage) for the SDK and RPC entry points.

Expand Down
9 changes: 6 additions & 3 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Pull request / selected branch push
├─ suites (Linux, Windows): build package -> unit -> integration
├─ agent-suite (Linux, Windows): native bindings -> coding-agent vitest (Node, then Bun)
├─ release-archive (Linux, Windows): build package -> binaries -> smoke
├─ static-checks (Linux): typecheck, docs, Mintlify, contracts
├─ static-checks (Linux): typecheck, docs, installer container smoke, contracts
└─ test (2 legs): result gate carrying both required contexts

Release tag push (`0.9.10` or `0.9.10-alpha.1`)
Expand Down Expand Up @@ -43,7 +43,7 @@ The test workflow runs on pushes to `main`, `release/**`, and `prerelease/**`, a
| `suites` | both | build `@bastani/atomic` -> unit -> integration | 121 s | 195 s |
| `agent-suite` | both | build native bindings -> coding-agent vitest (Node), then its Bun-hosted SQLite selector project | 126 s | 232 s |
| `release-archive` | both | build package -> `scripts/build-binaries.sh` -> archive smoke | 74 s | 149 s |
| `static-checks` | Linux only | typecheck, docs links, Mintlify, CI contracts | 30 s | – |
| `static-checks` | Linux only | typecheck, docs links, Mintlify, Alpine/Debian installer smoke, CI contracts | 30 s | – |
| `test` | 2 gate legs | assert every work-job result is `success` | 15 s | – |

Those are the per-step costs sampled from four sequential-job runs, which put the critical path on the Windows `agent-suite` chain at about 247 s against the 452 s (434–483 s, n=3 healthy) the single sequential job measured. Runner-seconds rise about 35 % (709 s to roughly 957 s); that is the price of the wall-clock cut.
Expand Down Expand Up @@ -114,6 +114,8 @@ Every job that runs a suite through `scripts/run-flaky-test-suite.ts` uploads `.

Archive smoke tests verify bundled builtins, native modules, runtime dependencies, `--version`, and startup far enough to reject extension-load failures.

The static job also runs `scripts/test-installers-containers.sh`. It executes `install.sh` with a restricted PATH and local release fixture inside `alpine:3.22` BusyBox `sh` and `debian:bookworm-slim`, checks the full payload and launcher, and gives the installer no JavaScript runtime or package manager. The Alpine fixture omits `ldd` from `PATH`, proving the `/etc/alpine-release` musl path.

## Direct release trigger and recovery

`.github/workflows/publish.yml` starts directly when an Atomic release tag is pushed. Atomic tags have no `v` prefix:
Expand Down Expand Up @@ -297,7 +299,7 @@ pins are supply-chain hygiene, not a fix for this incident.

Linux and Windows x64 each run `scripts/build-binaries.sh` for their platform, extract the resulting archive, check required bundled files, run `--version`, and start `--no-session` from a clean temporary directory. Expected no-model/no-key exits are accepted; extension-load failures and unexpected exits fail the job.

The `alpine-binary-smoke` job downloads the x64 musl binding, builds `atomic-linux-x64-musl.tar.gz`, and runs it in an `alpine:3.22` Docker container. The container installs `libgcc` and `libstdc++`, runs `--version` and the clean-cwd `--no-session` smoke, and rejects extension-load failures. A separate `node:22-alpine` container directly requires the extracted native package and checks its search exports. This currently exercises the x64 archive; the native matrix builds and publishes both musl architectures.
The `alpine-binary-smoke` matrix downloads each x64/arm64 musl binding, builds the matching archive, and passes it to `scripts/test-musl-release-archive.sh` on a matching runner. That script uses stock `alpine:3.22` with no package installation, checks the full payload and bundled `libgcc`/`libstdc++`, and runs `atomic --version`. A separate matching-architecture `node:22-alpine` container directly requires each extracted native package and checks its search exports.

### Release payload

Expand All @@ -306,6 +308,7 @@ After native and smoke jobs pass, `build`:
1. Installs with `npm ci --ignore-scripts` and runs `npm run check:shrinkwrap`.
2. Generates native platform package directories and the native root manifest.
3. Runs `scripts/build-binaries.sh --skip-install` for all eight archives.
Musl payload assembly downloads pinned Alpine 3.22 `libgcc` and `libstdc++` packages, verifies their SHA256 hashes, copies only the matching runtime libraries under `atomic/lib`, and sets payload-local ELF search paths with `patchelf`.
4. Validates package identity, versions, public/private metadata, binary entrypoint, workspace dependency ranges, build outputs, eight native modules, and eight exact-version native optional dependencies.
5. Packs exactly ten npm tarballs.
6. Extracts release notes from `packages/coding-agent/CHANGELOG.md`.
Expand Down
Loading