diff --git a/.github/scripts/colima-stage.sh b/.github/scripts/colima-stage.sh new file mode 100755 index 0000000..41b68d9 --- /dev/null +++ b/.github/scripts/colima-stage.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Print a Colima-visible scratch directory under $HOME. +# Colima's default virtiofs mount is the runner user home, not /opt/github-runners. +set -eu + +if [ -z "${HOME:-}" ]; then + echo "colima-stage.sh: HOME must be set" >&2 + exit 1 +fi + +id="${GITHUB_RUN_ID:-local}-${GITHUB_JOB:-job}-$$" +dir="$HOME/.bloxwap-ci/$id" +mkdir -p "$dir" +printf '%s\n' "$dir" diff --git a/.github/scripts/ensure-colima.sh b/.github/scripts/ensure-colima.sh new file mode 100755 index 0000000..3b786ff --- /dev/null +++ b/.github/scripts/ensure-colima.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Start Colima when needed and point Docker at its socket. No sudo, no apt. +set -eu + +if ! command -v colima >/dev/null 2>&1; then + echo "ensure-colima.sh: colima is required on this runner" >&2 + exit 1 +fi +if ! command -v docker >/dev/null 2>&1; then + echo "ensure-colima.sh: docker CLI is required on this runner" >&2 + exit 1 +fi + +colima status || colima start + +if ! docker info >/dev/null 2>&1; then + sock="${HOME}/.colima/default/docker.sock" + if [ -S "$sock" ]; then + DOCKER_HOST="unix://$sock" + export DOCKER_HOST + if [ -n "${GITHUB_ENV:-}" ]; then + echo "DOCKER_HOST=$DOCKER_HOST" >> "$GITHUB_ENV" + fi + fi +fi + +docker info >/dev/null diff --git a/.github/scripts/ensure-gnu-tar.sh b/.github/scripts/ensure-gnu-tar.sh new file mode 100755 index 0000000..9be84ac --- /dev/null +++ b/.github/scripts/ensure-gnu-tar.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# actions/upload-pages-artifact requires GNU tar as `gtar` on macOS. +set -eu + +add_brew_path() { + if [ -x /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + if [ -n "${GITHUB_PATH:-}" ]; then + echo /opt/homebrew/bin >> "$GITHUB_PATH" + fi + elif [ -x /usr/local/bin/brew ]; then + eval "$(/usr/local/bin/brew shellenv)" + if [ -n "${GITHUB_PATH:-}" ]; then + echo /usr/local/bin >> "$GITHUB_PATH" + fi + fi +} + +add_brew_path + +if command -v gtar >/dev/null 2>&1; then + exit 0 +fi + +if ! command -v brew >/dev/null 2>&1; then + echo "Missing GNU tar (gtar)." >&2 + echo "Install Homebrew on this Mac mini, then: brew install gnu-tar" >&2 + echo "actions/upload-pages-artifact requires gtar on self-hosted macOS." >&2 + exit 1 +fi + +brew list gnu-tar >/dev/null 2>&1 || brew install gnu-tar +add_brew_path + +if ! command -v gtar >/dev/null 2>&1; then + echo "brew install gnu-tar did not put gtar on PATH" >&2 + exit 1 +fi diff --git a/.github/scripts/fetch-linux-bun.sh b/.github/scripts/fetch-linux-bun.sh new file mode 100755 index 0000000..41449ea --- /dev/null +++ b/.github/scripts/fetch-linux-bun.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Download a Linux Bun binary into $RUNNER_TEMP and print its directory. +# Usage: fetch-linux-bun.sh +set -eu + +arch=${1:-} +version=${BUN_VERSION:-1.3.10} + +case "$arch" in + x64 | amd64 | x86_64) asset=bun-linux-x64 ;; + aarch64 | arm64) asset=bun-linux-aarch64 ;; + *) + echo "usage: fetch-linux-bun.sh " >&2 + exit 2 + ;; +esac + +if [ -z "${RUNNER_TEMP:-}" ]; then + echo "fetch-linux-bun.sh: RUNNER_TEMP must be set" >&2 + exit 1 +fi + +dest="$RUNNER_TEMP/$asset" +mkdir -p "$dest" + +if [ ! -x "$dest/bun" ]; then + zip="$RUNNER_TEMP/$asset.zip" + curl -fsSL -o "$zip" \ + "https://github.com/oven-sh/bun/releases/download/bun-v${version}/${asset}.zip" + extract="$RUNNER_TEMP/extract-$asset" + mkdir -p "$extract" + unzip -o "$zip" -d "$extract" >/dev/null + if [ -f "$extract/$asset/bun" ]; then + mv "$extract/$asset/bun" "$dest/bun" + elif [ -f "$extract/bun" ]; then + mv "$extract/bun" "$dest/bun" + else + echo "fetch-linux-bun.sh: bun executable not found in $asset.zip" >&2 + exit 1 + fi + chmod +x "$dest/bun" +fi + +printf '%s\n' "$dest" diff --git a/.github/scripts/linux-run.sh b/.github/scripts/linux-run.sh new file mode 100755 index 0000000..c44f18f --- /dev/null +++ b/.github/scripts/linux-run.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Run a command in a Linux container via Colima. +# Usage: linux-run.sh [args...] +# Mounts LINUX_HOST_DIR (must live under $HOME so Colima virtiofs can see it) at /work. +set -eu + +usage() { + echo "usage: linux-run.sh [args...]" >&2 + exit 2 +} + +[ "$#" -ge 3 ] || usage + +platform=$1 +image=$2 +shift 2 + +if [ -z "${LINUX_HOST_DIR:-}" ]; then + echo "linux-run.sh: LINUX_HOST_DIR must be a Colima-visible host directory" >&2 + exit 1 +fi + +script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +# shellcheck disable=SC1091 +. "$script_dir/ensure-colima.sh" + +mkdir -p "$LINUX_HOST_DIR/tmp" "$LINUX_HOST_DIR/home" + +envfile="$LINUX_HOST_DIR/linux-run.env" +: > "$envfile" +[ -n "${BLOXWAP_SMOKE_BUN:-}" ] && printf 'BLOXWAP_SMOKE_BUN=%s\n' "$BLOXWAP_SMOKE_BUN" >> "$envfile" +[ -n "${BLOXWAP_SMOKE_TARBALL:-}" ] && printf 'BLOXWAP_SMOKE_TARBALL=%s\n' "$BLOXWAP_SMOKE_TARBALL" >> "$envfile" +[ -n "${BLOXWAP_SMOKE_REGISTRY:-}" ] && printf 'BLOXWAP_SMOKE_REGISTRY=%s\n' "$BLOXWAP_SMOKE_REGISTRY" >> "$envfile" + +path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +if [ -x "$LINUX_HOST_DIR/bun/bun" ]; then + path="/work/bun:$path" +fi + +exec docker run --rm \ + --platform "$platform" \ + --env-file "$envfile" \ + -e TMPDIR=/work/tmp \ + -e HOME=/work/home \ + -e RUNNER_TEMP=/work/tmp \ + -e npm_config_cache=/work/tmp/npm-cache \ + -e npm_config_update_notifier=false \ + -e PATH="$path" \ + -v "$LINUX_HOST_DIR:/work" \ + -w /work \ + "$image" \ + "$@" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d242ef9..ac80f6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: pull_request: jobs: check: - runs-on: ubuntu-latest + runs-on: [self-hosted, macOS, ARM64] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9fc7f70..23f1df6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ concurrency: jobs: build: - runs-on: ubuntu-latest + runs-on: [self-hosted, macOS, ARM64] env: NEXT_PUBLIC_BASE_PATH: /agent-cli NEXT_PUBLIC_SITE_URL: https://bloxwap.github.io @@ -42,8 +42,10 @@ jobs: sparse-checkout: scripts/drift.mjs sparse-checkout-cone-mode: false - run: node .hig/scripts/drift.mjs --root . --config hig-drift.json - - run: npx playwright install --with-deps chromium + - run: npx playwright install chromium - run: npm run docs:smoke + - name: Ensure GNU tar + run: .github/scripts/ensure-gnu-tar.sh - uses: actions/upload-pages-artifact@v5 with: path: apps/docs/out @@ -51,7 +53,7 @@ jobs: deploy: if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' needs: build - runs-on: ubuntu-latest + runs-on: [self-hosted, macOS, ARM64] permissions: pages: write id-token: write diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d8bc5f7..3f56b0d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: false jobs: testnet: - runs-on: ubuntu-latest + runs-on: [self-hosted, macOS, ARM64] timeout-minutes: 15 environment: testnet env: @@ -29,7 +29,11 @@ jobs: - run: npm ci - run: npm run build - name: Verify the live testnet lifecycle (Relay mocked) - run: npm run test:e2e + env: + HOME: ${{ runner.temp }}/e2e-home + run: | + mkdir -p "$HOME" + npm run test:e2e - name: Record testnet coverage and outcome if: always() run: | diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 164f5b4..95523e9 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -6,10 +6,29 @@ on: jobs: install: strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] - node: ['22.12.0', '24'] - runs-on: ${{ matrix.os }} + include: + - name: macOS Node 22.12 + node: '22.12.0' + target: macos + - name: macOS Node 24 + node: '24' + target: macos + - name: Linux Node 22.12 + node: '22.12.0' + target: linux + docker_platform: linux/amd64 + bun_arch: x64 + node_image: node:22.12.0-bookworm + - name: Linux Node 24 + node: '24' + target: linux + docker_platform: linux/amd64 + bun_arch: x64 + node_image: node:24-bookworm + name: ${{ matrix.name }} + runs-on: [self-hosted, macOS, ARM64] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -22,9 +41,48 @@ jobs: - run: npm ci - run: npm run build - uses: actions/setup-node@v4 + if: matrix.target == 'macos' with: node-version: ${{ matrix.node }} - name: Install the packed artifact with lifecycle scripts disabled + if: matrix.target == 'macos' run: npm run test:package env: BLOXWAP_SMOKE_BUN: '1' + - name: Pack the install artifact + if: matrix.target == 'linux' + run: | + mkdir -p "$RUNNER_TEMP/pack" + npm pack --workspace @bloxwap/agent-cli --ignore-scripts --pack-destination "$RUNNER_TEMP/pack" + - name: Install the packed artifact in a Linux container + if: matrix.target == 'linux' + env: + BLOXWAP_SMOKE_BUN: '1' + BUN_VERSION: '1.3.10' + DOCKER_PLATFORM: ${{ matrix.docker_platform }} + BUN_ARCH: ${{ matrix.bun_arch }} + NODE_IMAGE: ${{ matrix.node_image }} + run: | + bun_dir=$(.github/scripts/fetch-linux-bun.sh "$BUN_ARCH") + tarball= + for candidate in "$RUNNER_TEMP/pack"/*.tgz; do + tarball=$candidate + done + if [ ! -f "$tarball" ]; then + echo "packed tarball not found in $RUNNER_TEMP/pack" >&2 + exit 1 + fi + host_dir=$(.github/scripts/colima-stage.sh) + cleanup() { rm -rf "$host_dir"; } + trap cleanup EXIT + mkdir -p "$host_dir/pack" "$host_dir/bun" + cp "$bun_dir/bun" "$host_dir/bun/bun" + chmod +x "$host_dir/bun/bun" + cp "$tarball" "$host_dir/pack/" + cp packages/agent-cli/package.json "$host_dir/package.json" + cp packages/agent-cli/scripts/package-smoke.mjs "$host_dir/package-smoke.mjs" + BLOXWAP_SMOKE_TARBALL=/work/pack/$(basename "$tarball") + export BLOXWAP_SMOKE_TARBALL BLOXWAP_SMOKE_BUN + export LINUX_HOST_DIR="$host_dir" + .github/scripts/linux-run.sh "$DOCKER_PLATFORM" "$NODE_IMAGE" \ + node package-smoke.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 497e2f5..8fa998e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,25 +10,9 @@ concurrency: cancel-in-progress: false jobs: build: - name: Candidate (${{ matrix.platform }}) - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - platform: linux-x64 - target: bun-linux-x64-baseline - package: true - - os: ubuntu-24.04-arm - platform: linux-arm64 - target: bun-linux-arm64 - - os: macos-15-intel - platform: darwin-x64 - target: bun-darwin-x64 - - os: macos-latest - platform: darwin-arm64 - target: bun-darwin-arm64 - runs-on: ${{ matrix.os }} + name: Candidate + runs-on: [self-hosted, macOS, ARM64] + timeout-minutes: 45 steps: - uses: actions/checkout@v4 with: @@ -50,25 +34,52 @@ jobs: run: npm run test:package env: BLOXWAP_SMOKE_BUN: '1' - - name: Compile and run the native executable - run: | - npm run build:binaries -- ${{ matrix.target }} - node packages/agent-cli/scripts/binary-smoke.mjs packages/agent-cli/binaries/agent-cli-${{ matrix.platform }} + - name: Cross-compile native executables for every target + run: npm run build:binaries + - name: Ensure Colima + run: .github/scripts/ensure-colima.sh + - name: Smoke-test the linux-x64 binary in linux/amd64 + env: + BLOXWAP_SMOKE_DOCKER_PLATFORM: linux/amd64 + run: node packages/agent-cli/scripts/binary-smoke.mjs packages/agent-cli/binaries/agent-cli-linux-x64 + - name: Smoke-test the linux-arm64 binary in linux/arm64 + env: + BLOXWAP_SMOKE_DOCKER_PLATFORM: linux/arm64 + run: node packages/agent-cli/scripts/binary-smoke.mjs packages/agent-cli/binaries/agent-cli-linux-arm64 + - name: Smoke-test the darwin-x64 binary under Rosetta + env: + BLOXWAP_SMOKE_ROSETTA: '1' + run: node packages/agent-cli/scripts/binary-smoke.mjs packages/agent-cli/binaries/agent-cli-darwin-x64 + - name: Smoke-test the darwin-arm64 binary + run: node packages/agent-cli/scripts/binary-smoke.mjs packages/agent-cli/binaries/agent-cli-darwin-arm64 - name: Ensure generated tracked files match the release commit run: git diff --exit-code HEAD - uses: actions/upload-artifact@v4 with: - name: native-${{ matrix.platform }} - path: packages/agent-cli/binaries/agent-cli-${{ matrix.platform }} + name: native-linux-x64 + path: packages/agent-cli/binaries/agent-cli-linux-x64 + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: native-linux-arm64 + path: packages/agent-cli/binaries/agent-cli-linux-arm64 + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: native-darwin-x64 + path: packages/agent-cli/binaries/agent-cli-darwin-x64 + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: native-darwin-arm64 + path: packages/agent-cli/binaries/agent-cli-darwin-arm64 if-no-files-found: error - name: Pack the tested npm release artifact - if: matrix.package run: | mkdir -p packages/agent-cli/artifacts npm pack --workspace @bloxwap/agent-cli --ignore-scripts --pack-destination packages/agent-cli/artifacts cp packages/agent-cli/SKILL.md packages/agent-cli/CHANGELOG.md packages/agent-cli/LICENSE packages/agent-cli/artifacts/ - name: Verify the exact release tarball - if: matrix.package run: | for release_tarball in "$PWD"/packages/agent-cli/artifacts/*.tgz; do BLOXWAP_SMOKE_TARBALL="$release_tarball" npm run test:package @@ -76,7 +87,6 @@ jobs: env: BLOXWAP_SMOKE_BUN: '1' - uses: actions/upload-artifact@v4 - if: matrix.package with: name: npm-package path: packages/agent-cli/artifacts/* @@ -84,7 +94,7 @@ jobs: publish: needs: build if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && vars.BLOXWAP_PUBLISH == '1' - runs-on: ubuntu-latest + runs-on: [self-hosted, macOS, ARM64] environment: npm-release concurrency: group: agent-cli-npm-publish diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 8f3e503..a0878ce 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -15,7 +15,7 @@ concurrency: jobs: version: if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest + runs-on: [self-hosted, macOS, ARM64] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/docs/ci-runners.md b/docs/ci-runners.md new file mode 100644 index 0000000..9985ac5 --- /dev/null +++ b/docs/ci-runners.md @@ -0,0 +1,105 @@ +# CI runners + +Every GitHub Actions job in this repository runs on the self-hosted Apple +silicon Mac mini fleet. Jobs select that fleet with: + +```yaml +runs-on: [self-hosted, macOS, ARM64] +``` + +The labels are case-sensitive. Register each runner with exactly `self-hosted`, +`macOS`, and `ARM64`. Do not use GitHub-hosted `ubuntu-*` or `macos-*` images. + +Jobs may stay queued when the fleet is busy. That is expected: the Mac minis are +the only runners. + +## Required software + +Install these on the runner account. Workflows never use `sudo` or `apt`. + +| Tool | Why | +| --- | --- | +| macOS on Apple silicon | Native `darwin-arm64` builds and tests | +| Rosetta 2 | `darwin-x64` binary smokes (`arch -x86_64`) and optional `linux/amd64` containers | +| GitHub Actions runner | Must stay logged in as a user that can run Colima and Docker without privilege escalation | +| Xcode Command Line Tools | `git`, `clang`, `tar`, `unzip`, `curl` | +| Homebrew + `gnu-tar` | `actions/upload-pages-artifact` needs `gtar` (`brew install gnu-tar`) | +| Colima + Docker CLI | Linux containers for binary and install smokes | +| Node.js 24 | Workflows also pin Node with `actions/setup-node` | +| Bun 1.3.10 | Native compiles and package smokes; workflows also pin Bun with `oven-sh/setup-bun` | + +One-time Colima VM (persistent; later jobs run `colima status || colima start`): + +```sh +colima start --vm-type vz --vz-rosetta --cpu 4 --memory 8 --disk 100 +``` + +`--vz-rosetta` is required for `linux/amd64` containers. `linux/arm64` runs +natively on the VM. Confirm `docker info` works without `sudo` after Colima is +up. If the default Docker context is not Colima, workflows export +`DOCKER_HOST=unix://$HOME/.colima/default/docker.sock`. + +Colima's default virtiofs mount is the runner user home (`/Users/...`), not +GitHub's work directory under `/opt/github-runners`. Linux jobs copy smoke +inputs into `$HOME/.bloxwap-ci/` before `docker run`. Do not bind-mount the +Actions workspace as `:ro`. + +Install Rosetta 2 once on the machine (this is the only privileged step, and it +is not part of CI): + +```sh +softwareupdate --install-rosetta --agree-to-license +``` + +## Container images + +Jobs pull these images on demand. Pre-pull them to avoid Docker Hub rate limits: + +```sh +docker pull --platform linux/amd64 debian:bookworm-slim +docker pull --platform linux/arm64 debian:bookworm-slim +docker pull --platform linux/amd64 node:22.12.0-bookworm +docker pull --platform linux/amd64 node:24-bookworm +``` + +`debian:bookworm-slim` runs the compiled Linux CLI (glibc). The Node bookworm +images run the Linux install smoke for Node 22.12 and 24. + +## Playwright + +Docs CI installs Chromium with `npx playwright install chromium`. Do **not** +pass `--with-deps`: that path tries to install apt packages. Keep a current +Playwright Chromium build in the runner user cache, or let the workflow download +it. + +## Temporary files + +Host-side scratch space is `$RUNNER_TEMP` (GitHub Actions empties it at the +start and end of each job). Docker bind mounts use `$HOME/.bloxwap-ci/` because +Colima does not see `/opt/github-runners`. The testnet job sets +`HOME=$RUNNER_TEMP/e2e-home` so a persistent Mac mini cannot reuse `~/.bloxwap` +state. + +## Network + +The runner needs outbound access to: + +- GitHub (git, Actions APIs, artifact upload, Pages, Bun releases) +- npm (`registry.npmjs.org`) +- Docker Hub (or a mirror of the images above) +- Playwright CDN +- Hyperliquid testnet and Privy, for the dedicated testnet workflow + +## How jobs use the Mac mini + +- **CI / version / docs deploy / npm publish** run natively on darwin-arm64. +- **Release** cross-compiles `linux-x64`, `linux-arm64`, `darwin-x64`, and + `darwin-arm64` with Bun on the Mac. Linux binaries are smoke-tested in + `linux/amd64` and `linux/arm64` containers. The `darwin-x64` binary is + smoke-tested under Rosetta. +- **Install smoke** still covers macOS and Linux × Node 22.12 and 24. Linux + cases pack on the Mac, then install inside a `linux/amd64` Node container. +- **Docs smoke** uses Playwright Chromium already installed for macOS. + +CI helper scripts live in `.github/scripts/` and are POSIX `/bin/sh` (BSD +userland). They must stay free of GNU-only flags, `apt`, and `sudo`. diff --git a/docs/index.md b/docs/index.md index d9493f2..e25fd9f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,8 @@ for state/env settings, [funding](funding.md) for deposits/signatures, Start with [working with a coding agent](agent-workflow.md) to configure your own Privy app and understand approvals, funding and completion checks. -Development progress is recorded in [PROGRESS](PROGRESS.md). Documentation is +Development progress is recorded in [PROGRESS](PROGRESS.md). GitHub Actions runs +on the self-hosted Mac mini fleet; see [CI runners](ci-runners.md). Documentation is built with `npm run docs:build` into `apps/docs/out`. Use `npm run docs:dev` for port 3903 at `/`, or `npm run docs:preview` for the static export. The command reference and skill MDX are synchronized with `npm run docs:generate`. diff --git a/docs/releases.md b/docs/releases.md index b845a0a..a8e2ef6 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -21,12 +21,15 @@ scoped GitHub App token. The default patch path works without changing that poli Run `npm run build`, `npm run lint`, `npm run typecheck`, `npm test`, and `BLOXWAP_SMOKE_BUN=1 npm run test:package`. The package smoke test installs the packed artifact with lifecycle scripts disabled and exercises npm's global -executable and installed bunx with both Node and Bun. Install CI runs on -macOS/Linux with Node 22.12 and 24. +executable and installed bunx with both Node and Bun. Install CI runs on the +self-hosted Mac mini fleet: native macOS and Linux (Colima `linux/amd64`) with +Node 22.12 and 24. See [CI runners](ci-runners.md). The Release workflow can also be manually dispatched on a branch to build a -candidate. It validates metadata, tests packed installs, compiles and executes -native Linux/macOS x64/arm64 binaries, and uploads reviewable artifacts. A manual +candidate. It validates metadata, tests packed installs, cross-compiles every +native Linux/macOS x64/arm64 binary on the Mac, smoke-tests Linux binaries in +Colima containers and the darwin-x64 binary under Rosetta, and uploads +reviewable artifacts. A manual run **never publishes**, even when `BLOXWAP_PUBLISH=1`. The compiled x64 Linux binary uses Bun's baseline target; binaries do not load incidental `.env` or `bunfig.toml` files from the caller's directory. diff --git a/docs/testnet.md b/docs/testnet.md index aa1fb3b..0248911 100644 --- a/docs/testnet.md +++ b/docs/testnet.md @@ -1,7 +1,9 @@ # Testnet lifecycle verification The `Testnet lifecycle` GitHub workflow runs nightly and on manual dispatch in the -`testnet` environment. It fails when required credentials are absent. A successful +`testnet` environment on the self-hosted Mac mini fleet (see [CI runners](ci-runners.md)). +It isolates `HOME` under `$RUNNER_TEMP` so a persistent runner cannot reuse +`~/.bloxwap`. It fails when required credentials are absent. A successful unit test, skipped integration test or workflow that stops before the lifecycle is not a live testnet pass. diff --git a/packages/agent-cli/scripts/binary-smoke.mjs b/packages/agent-cli/scripts/binary-smoke.mjs index 06d3d16..2d80e66 100644 --- a/packages/agent-cli/scripts/binary-smoke.mjs +++ b/packages/agent-cli/scripts/binary-smoke.mjs @@ -1,22 +1,87 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; -import { resolve } from 'node:path'; +import { chmodSync, copyFileSync, mkdirSync, rmSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -const binary = resolve( - process.argv[2] ?? `binaries/agent-cli-${process.platform}-${process.arch}`, -); -const run = (args) => - JSON.parse( - execFileSync(binary, args, { - input: '', - encoding: 'utf8', - timeout: 30_000, - }), +export function smokeInvocation(binary, args, env = process.env) { + const platform = env.BLOXWAP_SMOKE_DOCKER_PLATFORM; + if (platform) { + const image = env.BLOXWAP_SMOKE_DOCKER_IMAGE || 'debian:bookworm-slim'; + return { + file: 'docker', + argv: [ + 'run', + '--rm', + '--platform', + platform, + '-v', + `${binary}:/opt/agent-cli:ro`, + image, + '/opt/agent-cli', + ...args, + ], + }; + } + if (env.BLOXWAP_SMOKE_ROSETTA === '1') { + return { file: 'arch', argv: ['-x86_64', binary, ...args] }; + } + return { file: binary, argv: args }; +} + +export function stageBinaryForColima(binary, env = process.env) { + const root = join( + env.HOME || homedir(), + '.bloxwap-ci', + `binary-${process.pid}`, + ); + mkdirSync(root, { recursive: true }); + const staged = join(root, 'agent-cli'); + copyFileSync(binary, staged); + chmodSync(staged, 0o755); + return { staged, root }; +} + +export function smokeBinary(binary, env = process.env) { + let target = binary; + let stagedRoot; + if (env.BLOXWAP_SMOKE_DOCKER_PLATFORM) { + ({ staged: target, root: stagedRoot } = stageBinaryForColima(binary, env)); + } + try { + const run = (args) => { + const { file, argv } = smokeInvocation(target, args, env); + return JSON.parse( + execFileSync(file, argv, { + input: '', + encoding: 'utf8', + timeout: 60_000, + }), + ); + }; + assert.equal(run(['--help']).ok, true); + const order = run(['schema', 'order']); + assert.equal(order.ok, true); + assert.ok(order.data.input.properties.coin); + const where = env.BLOXWAP_SMOKE_DOCKER_PLATFORM + ? env.BLOXWAP_SMOKE_DOCKER_PLATFORM + : env.BLOXWAP_SMOKE_ROSETTA === '1' + ? 'darwin/x64 (Rosetta)' + : `${process.platform}/${process.arch}`; + console.log(`PASS: standalone ${where} CLI help and order schema`); + } finally { + if (stagedRoot) rmSync(stagedRoot, { recursive: true, force: true }); + } +} + +const isMain = + Boolean(process.argv[1]) && + resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isMain) { + smokeBinary( + resolve( + process.argv[2] ?? `binaries/agent-cli-${process.platform}-${process.arch}`, + ), ); -assert.equal(run(['--help']).ok, true); -const order = run(['schema', 'order']); -assert.equal(order.ok, true); -assert.ok(order.data.input.properties.coin); -console.log( - `PASS: standalone ${process.platform}/${process.arch} CLI help and order schema`, -); +} diff --git a/packages/agent-cli/scripts/package-smoke.mjs b/packages/agent-cli/scripts/package-smoke.mjs index 00484e4..4726813 100644 --- a/packages/agent-cli/scripts/package-smoke.mjs +++ b/packages/agent-cli/scripts/package-smoke.mjs @@ -11,7 +11,8 @@ import { import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; -const directory = mkdtempSync(join(tmpdir(), 'bloxwap-package-')); +const scratch = process.env.RUNNER_TEMP || tmpdir(); +const directory = mkdtempSync(join(scratch, 'bloxwap-package-')); const jsonCommand = (command, args, cwd = directory) => JSON.parse( execFileSync(command, args, { diff --git a/packages/agent-cli/test/binary-smoke.test.js b/packages/agent-cli/test/binary-smoke.test.js new file mode 100644 index 0000000..84c9bad --- /dev/null +++ b/packages/agent-cli/test/binary-smoke.test.js @@ -0,0 +1,41 @@ +import { expect, test } from 'vitest'; +import { smokeInvocation } from '../scripts/binary-smoke.mjs'; + +test('docker platform wraps the binary for a Colima linux container', () => { + expect( + smokeInvocation('/opt/agent-cli-linux-x64', ['--help'], { + BLOXWAP_SMOKE_DOCKER_PLATFORM: 'linux/amd64', + }), + ).toEqual({ + file: 'docker', + argv: [ + 'run', + '--rm', + '--platform', + 'linux/amd64', + '-v', + '/opt/agent-cli-linux-x64:/opt/agent-cli:ro', + 'debian:bookworm-slim', + '/opt/agent-cli', + '--help', + ], + }); +}); + +test('Rosetta wraps the darwin-x64 binary with arch -x86_64', () => { + expect( + smokeInvocation('/opt/agent-cli-darwin-x64', ['schema', 'order'], { + BLOXWAP_SMOKE_ROSETTA: '1', + }), + ).toEqual({ + file: 'arch', + argv: ['-x86_64', '/opt/agent-cli-darwin-x64', 'schema', 'order'], + }); +}); + +test('native invocation runs the binary directly', () => { + expect(smokeInvocation('/opt/agent-cli-darwin-arm64', ['--help'])).toEqual({ + file: '/opt/agent-cli-darwin-arm64', + argv: ['--help'], + }); +}); diff --git a/packages/agent-cli/test/setup.ts b/packages/agent-cli/test/setup.ts index 7907761..da3911e 100644 --- a/packages/agent-cli/test/setup.ts +++ b/packages/agent-cli/test/setup.ts @@ -1,9 +1,9 @@ import { afterAll, beforeAll, vi } from 'vitest'; import { mkdir, rm } from 'node:fs/promises'; -const testHome = vi.hoisted( - () => - `/tmp/bloxwap-unit-${process.pid}-${Math.random().toString(16).slice(2)}`, -); +const testHome = vi.hoisted(() => { + const root = process.env.RUNNER_TEMP || '/tmp'; + return `${root}/bloxwap-unit-${process.pid}-${Math.random().toString(16).slice(2)}`; +}); vi.mock('node:os', async (importOriginal) => ({ ...(await importOriginal()), homedir: () => testHome,