Skip to content

README: document the real Windows story (x64 native, ARM64 via emulat… #5

README: document the real Windows story (x64 native, ARM64 via emulat…

README: document the real Windows story (x64 native, ARM64 via emulat… #5

name: utmstack-release
# Builds the branded `utmstack` binaries for every platform the installers can
# request, and publishes them with a checksums.txt the installers verify.
#
# The installer downloads "$APP-$target" (e.g. utmstack-linux-x64.tar.gz), so a
# target missing here is a 404 for those users. The -baseline variants matter:
# without them, any x64 machine lacking AVX2 — which includes a lot of cloud
# VMs — cannot install.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "Version to build (without the leading v)"
required: true
permissions:
contents: write
jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# macOS. Built natively so PyInstaller/Bun ad-hoc signing applies —
# an unsigned arm64 binary will not execute on Apple Silicon.
- { runner: macos-14, target: darwin-arm64, ext: zip }
- { runner: macos-15-intel, target: darwin-x64, ext: zip, baseline: true }
# Linux. x64 builds on 22.04 (glibc 2.35) so it runs on Ubuntu 22.04+
# and RHEL 9+. arm64 has no 22.04 ARM runner we can rely on, so it
# builds on 24.04 (glibc 2.39) and needs Ubuntu 24.04+ / Debian 13+.
- { runner: ubuntu-22.04, target: linux-x64, ext: tar.gz, baseline: true }
- { runner: ubuntu-24.04-arm, target: linux-arm64, ext: tar.gz }
# Windows: x64 only. The native ARM64 build cannot start its terminal
# UI (a Bun/OpenTUI FFI limitation that breaks opencode's own
# windows-arm64 binary identically), so we do not ship one — on ARM64
# the installer uses the x64-baseline build under Windows emulation.
# baseline (no-AVX2) is required so the x64-on-ARM emulator can run it.
- { runner: windows-2022, target: windows-x64, ext: zip, baseline: true }
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.13
- name: Resolve version
id: version
shell: bash
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
echo "value=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
fi
- name: Install dependencies
run: bun install
- name: Build
working-directory: packages/opencode
shell: bash
env:
OPENCODE_VERSION: ${{ steps.version.outputs.value }}
run: |
# --baseline additionally emits the no-AVX2 variant for x64 targets,
# which requires Bun to fetch extra cross-compilation artifacts. That
# download is known to be flaky ("Failed to extract executable for
# bun-<target>-baseline"), so retry before giving up.
build() {
if [ "${{ matrix.baseline }}" = "true" ]; then
bun run script/build.ts --single --baseline
else
bun run script/build.ts --single
fi
}
ok=0
for attempt in 1 2 3; do
echo "build attempt $attempt"
if build; then ok=1; break; fi
echo "attempt $attempt failed; retrying in 15s"
sleep 15
done
if [ "$ok" != "1" ] && [ "${{ matrix.baseline }}" = "true" ]; then
# A flaky baseline artifact must not cost us the primary binary for
# this platform. Fall back to the native build alone; the installer
# only requests -baseline on non-AVX2 x64 hardware, and the native
# asset still serves everyone else.
echo "baseline builds failed 3x — falling back to the native build only"
bun run script/build.ts --single && ok=1
fi
[ "$ok" = "1" ] || { echo "build failed after retries"; exit 1; }
ls -la dist/
- name: Verify the binary is branded and self-contained
shell: bash
working-directory: packages/opencode
run: |
set -eu
# Address the exact matrix target. `find | head -1` picked whichever
# directory sorted first, which on x64 runs was the musl variant —
# a musl-linked binary cannot execute on a glibc runner (missing ELF
# interpreter, which surfaces as "No such file or directory" / 127).
DIST="dist/opencode-${{ matrix.target }}"
BIN="$DIST/bin/utmstack"
[ -f "$BIN" ] || BIN="$DIST/bin/utmstack.exe"
[ -f "$BIN" ] || { echo "no binary at $DIST/bin"; ls -R dist | head -40; exit 1; }
echo "binary: $BIN"
got=$("$BIN" --version | tr -d '\r')
[ "$got" = "${{ steps.version.outputs.value }}" ] || {
echo "version mismatch: got '$got'"; exit 1; }
"$BIN" --help 2>&1 | grep -q "utmstack completion" || {
echo "help output is not branded"; exit 1; }
# First run must seed config and extract the embedded skills/agents.
TMPCFG=$(mktemp -d)
XDG_CONFIG_HOME="$TMPCFG" APPDATA="$TMPCFG" "$BIN" --help >/dev/null 2>&1 || true
XDG_CONFIG_HOME="$TMPCFG" APPDATA="$TMPCFG" "$BIN" mcp list >/dev/null 2>&1 || true
test -f "$TMPCFG/utmstack/utmstack.json" || {
echo "default config was not seeded"; exit 1; }
skills=$(ls "$TMPCFG/utmstack/skills" 2>/dev/null | wc -l | tr -d ' ')
agents=$(ls "$TMPCFG/utmstack/agent"/*.md 2>/dev/null | wc -l | tr -d ' ')
echo "seeded config, $skills skills, $agents agents"
[ "$skills" -gt 0 ] || { echo "no skills extracted"; exit 1; }
[ "$agents" -gt 0 ] || { echo "no agents extracted"; exit 1; }
grep -q '"utmstack-mcp"' "$TMPCFG/utmstack/utmstack.json" || {
echo "default config does not reference utmstack-mcp"; exit 1; }
- name: Package
shell: bash
working-directory: packages/opencode
run: |
set -eu
# Absolute output dir. A relative "../.." from inside dist/<name>/bin
# is five levels from the repo root and is trivially miscounted.
OUT="$GITHUB_WORKSPACE/out"
mkdir -p "$OUT"
for dir in dist/*/; do
name=$(basename "$dir")
[ -d "$dir/bin" ] || continue
# The build emits musl variants for free, but they are untested and
# unadvertised, and the installer refuses musl outright. Publishing
# them would promise support we have not verified.
case "$name" in *-musl*) echo "skipping $name (musl, not published)"; continue ;; esac
asset=$(echo "$name" | sed 's/^opencode-/utmstack-/')
if [ "${{ matrix.ext }}" = "tar.gz" ]; then
tar -czf "$OUT/${asset}.tar.gz" -C "$dir/bin" .
else
# COPYFILE_DISABLE keeps macOS AppleDouble "._*" files out of the
# zip; -X drops extended attributes. Both are no-ops on Windows.
(cd "$dir/bin" && env COPYFILE_DISABLE=1 7z a -tzip "$OUT/${asset}.zip" . >/dev/null)
fi
done
ls -lh "$OUT"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: out/*
release:
needs: build
runs-on: ubuntu-22.04
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v4
with: { path: artifacts }
- name: Collect and checksum
run: |
mkdir -p out
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec cp {} out/ \;
cd out && sha256sum * > checksums.txt
echo "Publishing:"; cat checksums.txt
- uses: softprops/action-gh-release@v2
with:
files: out/*
generate_release_notes: true