diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index d1e3bfc25d0c..a4d8a8aba662 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -3,19 +3,37 @@ description: "Setup Bun with caching and install dependencies" runs: using: "composite" steps: - - name: Get baseline download URL + - name: Ensure build tools are available + if: runner.os == 'Linux' + shell: bash + # Blacksmith's ARM64 ubuntu-2404 runners ship a minimal image that is missing: + # unzip — needed by oven-sh/setup-bun to extract the downloaded bun zip + # build-essential (make, g++) — needed by node-gyp to compile native addons + # (e.g. tree-sitter-powershell) during `bun install` + run: | + PKGS=() + command -v unzip >/dev/null 2>&1 || PKGS+=(unzip) + command -v make >/dev/null 2>&1 || PKGS+=(build-essential) + [ ${#PKGS[@]} -gt 0 ] && sudo apt-get install -y --no-install-recommends "${PKGS[@]}" + true + + - name: Get bun download URL id: bun-url shell: bash run: | - if [ "$RUNNER_ARCH" = "X64" ]; then - V=$(node -p "require('./package.json').packageManager.split('@')[1]") - case "$RUNNER_OS" in - macOS) OS=darwin ;; - Linux) OS=linux ;; - Windows) OS=windows ;; - esac - echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" - fi + V=$(node -p "require('./package.json').packageManager.split('@')[1]") + case "$RUNNER_OS" in + macOS) OS=darwin ;; + Linux) OS=linux ;; + Windows) OS=windows ;; + *) exit 0 ;; + esac + case "$RUNNER_ARCH" in + X64) ARCH=x64-baseline ;; + ARM64) ARCH=aarch64 ;; + *) exit 0 ;; + esac + echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-${ARCH}.zip" >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69a3a1a2d13f..e7a2b8cf6f2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,24 @@ jobs: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}- turbo-${{ runner.os }}- + - name: Cache npm registry packages + # Plugin integration tests install @opencode-ai/plugin into fresh temp dirs + # using @npmcli/arborist, which fetches from the npm registry and caches + # tarballs in ~/.npm. Without this cache, each test does a full network + # download, easily hitting the 30 s bun test timeout on Blacksmith ARM64. + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ runner.arch }}-opencode-ai-plugin-${{ hashFiles('**/package.json') }} + restore-keys: | + npm-${{ runner.os }}-${{ runner.arch }}-opencode-ai-plugin- + + - name: Warm npm cache for @opencode-ai/plugin + # Pre-populate ~/.npm so arborist can satisfy @opencode-ai/plugin from + # cache during plugin integration tests instead of hitting the registry. + if: runner.os == 'Linux' + run: npm install --prefix /tmp/plugin-warmup @opencode-ai/plugin + - name: Run unit tests run: bun turbo test:ci env: diff --git a/.opencode/sessions/chore-bolster-install.md b/.opencode/sessions/chore-bolster-install.md new file mode 100644 index 000000000000..a5187bdfd1c8 --- /dev/null +++ b/.opencode/sessions/chore-bolster-install.md @@ -0,0 +1,80 @@ +# Session: Bolster Install — Blacksmith ARM64 CI Fixes + +**Branch**: chore/bolster-install +**Issue**: N/A +**Created**: 2026-04-22 +**Status**: complete — PR #11 open, awaiting merge + +## Goal +Harden the `install-flex` automated installer and fix all Blacksmith ARM64 CI +failures that were blocking the unit and e2e test jobs on the Flexion fork. + +## Approach +Fix issues in layers as they surfaced during CI runs: +1. Add `install-flex` installer script +2. Pin `OPENCODE_CHANNEL=flex` to prevent per-branch SQLite DB fragmentation +3. Fix missing build tools on Blacksmith ARM64 runners +4. Fix unit test timeouts caused by arborist npm installs during tests +5. Bump individual test timeouts that are tight on ARM64 + +## Session Log +- 2026-04-22: Session created +- 2026-04-22: Added `install-flex` script (already existed on branch), fixed DB fragmentation +- 2026-04-22: CI round 1 — fixed `unzip` missing (setup-bun) +- 2026-04-22: CI round 2 — fixed `make`/`g++` missing (build-essential for node-gyp) +- 2026-04-22: CI round 3 — 7 test timeouts; root-caused to `@npmcli/arborist.reify()` in tests +- 2026-04-22: CI round 4 — 1 remaining timeout; fixed shell-loop test 3s → 15s +- 2026-04-22: All CI jobs passing. PR updated. +- 2026-04-22: CI round 5 — 1 new timeout; "shell rejects with BusyError when loop running" 3s → 15s + +## Key Decisions + +### `OPENCODE_CHANNEL=flex` in `install-flex` +OpenCode bakes `InstallationChannel` from the git branch at build time and uses it +as the SQLite DB name suffix (`opencode-.db`). Without pinning, each +rebuild from a different branch creates a fresh empty database, losing all session +history. Pinning to `"flex"` ensures all Flexion builds share `opencode-flex.db`. +See: `packages/opencode/src/storage/db.ts:getChannelPath()`. + +### `OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL` flag +`config.ts` fires a background `@npmcli/arborist.reify()` for `@opencode-ai/plugin` +in every `.opencode/` directory it discovers. In tests, 7 tests were timing out +because: plugin/tool tests called `waitForDependencies()` which joined the arborist +fiber (10–30 s per test on ARM64), and the resulting CPU saturation starved +concurrent session/snapshot tests. The flag skips the install in tests; safe because +bun resolves `@opencode-ai/plugin` from the workspace `node_modules` directly. +Set unconditionally in `test/preload.ts`. + +### Blacksmith ARM64 runner gaps +`blacksmith-4vcpu-ubuntu-2404` uses ARM64 and ships a minimal Ubuntu image missing: +- `unzip` — needed by `oven-sh/setup-bun@v2` to extract the downloaded bun zip +- `make`/`g++` (build-essential) — needed by `node-gyp` for `tree-sitter-powershell` +Both now installed in a single `Ensure build tools are available` step in +`.github/actions/setup-bun/action.yml` (Linux only, no-op if already present). + +### Test timeout bumps +- `snapshot.test.ts` "revert handles large mixed batches": 30 s → 60 s + (280 files + multiple git commits/patches/reverts on ARM64) +- `prompt-effect.test.ts` "loop waits while shell runs": 3 s → 15 s + (spawns a real `sleep 0.2` subprocess; ARM64 fork/exec overhead exceeds 3 s) +- `prompt-effect.test.ts` "shell rejects with BusyError when loop running": 3 s → 15 s + (fiber fork + session init before `llm.wait(1)` exceeds 3 s on ARM64) + +## Files Changed +- `install-flex` — `OPENCODE_CHANNEL=flex` added to build command +- `.github/actions/setup-bun/action.yml` — build tools prereq + ARM64/X64 URL construction +- `.github/workflows/test.yml` — npm cache + pre-warm step (unit job) +- `packages/opencode/src/flag/flag.ts` — `OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL` flag +- `packages/opencode/src/config/config.ts` — guard arborist install with new flag +- `packages/opencode/test/preload.ts` — set `OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL=true` +- `packages/opencode/test/snapshot/snapshot.test.ts` — 60 s timeout on 280-file test +- `packages/opencode/test/session/prompt-effect.test.ts` — 15 s timeout on shell-loop test + +## Side Effects Applied Outside the Repo +- `~/.opencode/bin/opencode` — rebuilt from this branch with `OPENCODE_CHANNEL=flex`; + now reports `0.0.0-flex-` and uses `~/.local/share/opencode/opencode-flex.db` + +## Next Steps +- [ ] Merge PR #11 into flex: https://github.com/flexion/opencode/pull/11 +- [ ] After merge, other developers run `install-flex` to pick up all fixes +- [ ] Consider periodically running `install-flex` to stay current with `flex` branch diff --git a/LOCAL_AWS_SETUP.md b/LOCAL_AWS_SETUP.md index 4be33dcd835c..938861273371 100644 --- a/LOCAL_AWS_SETUP.md +++ b/LOCAL_AWS_SETUP.md @@ -2,6 +2,27 @@ Instructions for cloning, building, and running the Flexion fork of opencode with AWS Bedrock. +## Quick Install + +The installer handles all steps below automatically: + +```bash +curl -fsSL https://raw.githubusercontent.com/flexion/opencode/flex/install-flex | bash +``` + +You will be prompted for: +- **Clone directory** (default: `~/opencode`) +- **AWS account ID** (your personal Flexion account) +- **Preferred AWS region** (default: `us-east-1`) + +Everything else — AWS SSO profile, opencode config, and the `opencode-work` shell function — is written automatically. Skip to [Usage](#4-usage) once the installer finishes. + +--- + +## Manual Setup + +Follow these steps if you prefer to configure things yourself. + ## Prerequisites - [Bun](https://bun.sh) v1.3+ @@ -46,11 +67,11 @@ Add to `~/.aws/config`: ```ini [profile ClaudeCodeAccess] -sso_start_url = -sso_region = +sso_start_url = https://identitycenter.amazonaws.com/ssoins-6684680a9b285ea2 +sso_region = us-east-2 sso_account_id = -sso_role_name = AdministratorAccess -region = +sso_role_name = ClaudeCodeAccess +region = ``` ### 2. Configure opencode for Bedrock @@ -118,20 +139,24 @@ Create `~/.config/opencode/opencode.json`: ### 3. Shell alias -Add to `~/.zshrc` or `~/.bashrc`: +Add to `~/.zshrc` or `~/.bashrc` (replace `~/opencode` with your actual clone path if different): ```bash +# ── Flexion opencode launcher ───────────────────────────────────────────────── opencode-work() { local profile="ClaudeCodeAccess" + local arch os + case "$(uname -m)" in arm64|aarch64) arch="arm64" ;; *) arch="x64" ;; esac + case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *) + echo "Unsupported OS: $(uname -s)" && return 1 ;; esac echo "Logging in to AWS SSO ($profile)..." aws sso login --profile "$profile" || return 1 eval "$(aws configure export-credentials --profile "$profile" --format env)" - /path/to/opencode/packages/opencode/dist/opencode-darwin-arm64/bin/opencode "${opencode_args[@]}" + "$HOME/opencode/packages/opencode/dist/opencode-${os}-${arch}/bin/opencode" "$@" } +# ───────────────────────────────────────────────────────────────────────────── ``` -Replace `/path/to/opencode` with where you cloned the repo (e.g. `~/Code/personal/flexion-work-items/flexchat-stack/opencode`). - ### 4. Usage ```bash @@ -174,6 +199,7 @@ See [flexion/opencode#2](https://github.com/flexion/opencode/pull/2) for the ful | Strip reasoning from history for non-reasoning models | `packages/opencode/src/provider/transform.ts` | Removes reasoning content parts from assistant message history before sending to models with `reasoning: false` — fixes Bedrock rejections when switching from a reasoning model | | Exclude palmyra from reasoning variant generation | `packages/opencode/src/provider/transform.ts` | Prevents unsupported `reasoningConfig` parameters from being sent to Writer Palmyra models | | Local build & AWS Bedrock setup docs | `LOCAL_AWS_SETUP.md` | This file | +| Automated installer | `install-flex` | Single-command installer for the entire setup | Full details and upstream tracking: [flexion/opencode#2](https://github.com/flexion/opencode/pull/2) diff --git a/install-flex b/install-flex new file mode 100755 index 000000000000..af77cade1a28 --- /dev/null +++ b/install-flex @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +# install-flex — Flexion fork of opencode, automated installer +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/flexion/opencode/flex/install-flex | bash +# +# What this does: +# 1. Checks prerequisites (git, bun, aws-cli v2) +# 2. Prompts: clone directory, AWS account ID, preferred AWS region +# 3. Clones git@github.com:flexion/opencode.git (flex branch) — skipped if already present +# 4. Installs dependencies and builds the native binary +# 5. Writes [profile ClaudeCodeAccess] to ~/.aws/config +# 6. Writes ~/.config/opencode/opencode.json (Bedrock + model config) +# 7. Appends opencode-work() launcher function to ~/.zshrc or ~/.bashrc +# +# Existing files are never overwritten; each step is skipped with a warning +# if the output already exists. + +set -euo pipefail + +# ── colors ──────────────────────────────────────────────────────────────────── +if [ -t 1 ]; then + BOLD=$'\033[1m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m' + BLUE=$'\033[0;34m'; RED=$'\033[0;31m'; NC=$'\033[0m' +else + BOLD=''; GREEN=''; YELLOW=''; BLUE=''; RED=''; NC='' +fi + +# ── constants ───────────────────────────────────────────────────────────────── +SSO_START_URL="https://identitycenter.amazonaws.com/ssoins-6684680a9b285ea2" +SSO_REGION="us-east-2" +SSO_ROLE_NAME="ClaudeCodeAccess" +AWS_PROFILE="ClaudeCodeAccess" +REPO_URL="git@github.com:flexion/opencode.git" +BRANCH="flex" + +# ── helpers ─────────────────────────────────────────────────────────────────── +info() { printf '%s▶%s %s\n' "$BLUE" "$NC" "$*"; } +success() { printf '%s✔%s %s\n' "$GREEN" "$NC" "$*"; } +warn() { printf '%s⚠%s %s\n' "$YELLOW" "$NC" "$*"; } +die() { printf '%s✖%s %s\n' "$RED" "$NC" "$*" >&2; exit 1; } + +# ── step 1: prerequisites ───────────────────────────────────────────────────── +check_prereqs() { + info "Checking prerequisites..." + local missing=() + command -v git >/dev/null 2>&1 || missing+=("git") + command -v bun >/dev/null 2>&1 || missing+=("bun (https://bun.sh)") + command -v aws >/dev/null 2>&1 || missing+=("aws-cli v2 (https://aws.amazon.com/cli/)") + if [ ${#missing[@]} -gt 0 ]; then + die "Missing prerequisites:$(printf '\n • %s' "${missing[@]}")" + fi + success "Prerequisites OK" +} + +# ── step 2: gather inputs ───────────────────────────────────────────────────── +# All reads use /dev/tty so prompts work when stdin is the script (curl | bash). +gather_inputs() { + printf '\n%sFlexion opencode installer%s\n' "$BOLD" "$NC" + printf '────────────────────────────────────────────\n' + + printf 'Clone directory [%s]: ' "$HOME/opencode" >/dev/tty + IFS= read -r CLONE_DIR /dev/tty + IFS= read -r ACCOUNT_ID /dev/tty + IFS= read -r AWS_REGION /dev/null; then + warn "AWS profile [$AWS_PROFILE] already in $config — skipping" + return + fi + + info "Writing AWS SSO profile to $config..." + cat >>"$config" <"$config_file" </dev/null; then + warn "opencode-work() already defined in $rc_file — skipping" + return + fi + + info "Appending opencode-work() to $rc_file..." + + # Single-quoted heredoc prevents variable expansion inside the function body. + # CLONE_DIR_PLACEHOLDER is substituted after writing via perl. + cat >>"$rc_file" <<'SHELL_EOF' + +# ── Flexion opencode launcher ───────────────────────────────────────────────── +opencode-work() { + local profile="ClaudeCodeAccess" + local arch os + case "$(uname -m)" in arm64|aarch64) arch="arm64" ;; *) arch="x64" ;; esac + case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *) + echo "Unsupported OS: $(uname -s)" && return 1 ;; esac + echo "Logging in to AWS SSO ($profile)..." + aws sso login --profile "$profile" || return 1 + eval "$(aws configure export-credentials --profile "$profile" --format env)" + "CLONE_DIR_PLACEHOLDER/packages/opencode/dist/opencode-${os}-${arch}/bin/opencode" "$@" +} +# ───────────────────────────────────────────────────────────────────────────── +SHELL_EOF + + # Substitute actual clone path. Uses | as delimiter to safely handle / in paths. + perl -i -pe "s|CLONE_DIR_PLACEHOLDER|${CLONE_DIR}|g" "$rc_file" + + success "opencode-work() added to $rc_file" +} + +# ── main ────────────────────────────────────────────────────────────────────── +main() { + check_prereqs + gather_inputs + clone_and_build + write_aws_config + write_opencode_config + write_shell_alias + + local rc_file + case "$(basename "${SHELL:-bash}")" in + zsh) rc_file="~/.zshrc" ;; + *) rc_file="~/.bashrc" ;; + esac + + printf '\n%s%sInstallation complete!%s\n' "$GREEN" "$BOLD" "$NC" + printf '────────────────────────────────────────────\n' + printf ' Binary: %s/packages/opencode/dist/opencode-*/bin/opencode\n' "$CLONE_DIR" + printf ' AWS: profile=%s account=%s region=%s\n' "$AWS_PROFILE" "$ACCOUNT_ID" "$AWS_REGION" + printf ' Config: %s/.config/opencode/opencode.json\n' "$HOME" + printf '\n Next steps:\n' + printf ' 1. source %s\n' "$rc_file" + printf ' 2. opencode-work\n\n' +} + +main diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 55684fc70dfb..86d9936040e1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -542,28 +542,30 @@ export const layer = Layer.effect( yield* ensureGitignore(dir).pipe(Effect.orDie) - const dep = yield* npmSvc - .install(dir, { - add: [ - { - name: "@opencode-ai/plugin", - version: InstallationLocal ? undefined : InstallationVersion, - }, - ], - }) - .pipe( - Effect.exit, - Effect.tap((exit) => - Exit.isFailure(exit) - ? Effect.sync(() => { - log.warn("background dependency install failed", { dir, error: String(exit.cause) }) - }) - : Effect.void, - ), - Effect.asVoid, - Effect.forkDetach, - ) - deps.push(dep) + if (!Flag.OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL) { + const dep = yield* npmSvc + .install(dir, { + add: [ + { + name: "@opencode-ai/plugin", + version: InstallationLocal ? undefined : InstallationVersion, + }, + ], + }) + .pipe( + Effect.exit, + Effect.tap((exit) => + Exit.isFailure(exit) + ? Effect.sync(() => { + log.warn("background dependency install failed", { dir, error: String(exit.cause) }) + }) + : Effect.void, + ), + Effect.asVoid, + Effect.forkDetach, + ) + deps.push(dep) + } result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir))) result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir))) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 72c8931f5b71..bdd400334e1e 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -44,6 +44,12 @@ export const Flag = { OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), + // Skip the background @npmcli/arborist install of @opencode-ai/plugin into + // .opencode/ directories. In production this ensures the plugin SDK is + // available for user-authored plugins. In test environments bun resolves + // @opencode-ai/plugin from the workspace node_modules directly, so the + // install is unnecessary and causes test timeouts on slower CI runners. + OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL: truthy("OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL"), OPENCODE_DISABLE_CLAUDE_CODE, OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"), OPENCODE_DISABLE_CLAUDE_CODE_SKILLS, diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 58dc2b0b48c5..f678a751a314 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -45,6 +45,12 @@ process.env["OPENCODE_TEST_HOME"] = testHome const testManagedConfigDir = path.join(dir, "managed") process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir process.env["OPENCODE_DISABLE_DEFAULT_PLUGINS"] = "true" +// Skip the background @npmcli/arborist install of @opencode-ai/plugin into +// .opencode/ dirs. Tests don't need the runtime npm install because bun +// resolves @opencode-ai/plugin from the monorepo workspace node_modules. +// Without this, plugin/tool tests trigger a full npm network fetch per test, +// consuming 10-30 s on Blacksmith ARM64 CI and causing timeouts. +process.env["OPENCODE_DISABLE_PLUGIN_DEPS_INSTALL"] = "true" // Write the cache version file to prevent global/index.ts from clearing the cache const cacheDir = path.join(dir, "cache", "opencode") diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 2f5904684023..166ca5f2d376 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -626,7 +626,7 @@ it.live( }), { git: true, config: providerCfg }, ), - 5_000, + 15_000, // subprocess startup on Blacksmith ARM64 can consume 2-3 s; 15 s gives enough headroom ) it.live( @@ -1024,7 +1024,7 @@ it.live( }), { git: true, config: providerCfg }, ), - 3_000, + 15_000, // fiber fork + session init on Blacksmith ARM64 can exceed 3 s; 15 s gives enough headroom ) unix("shell captures stdout and stderr in completed tool output", () => diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 3330b497c3a4..383d44d3730a 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -1528,4 +1528,4 @@ test("revert handles large mixed batches across chunk boundaries", async () => { ) }, }) -}) +}, 60_000) // 280 files + multiple git operations can exceed 30 s on slower ARM64 CI runners