Skip to content

Release

Release #36

Workflow file for this run

name: Release
# First-release pipeline (M1): Rust crates → crates.io, Node → npm,
# Python → PyPI (trusted publishing), Go .a artifacts + C ABI artifacts →
# GitHub Release. Swift / Kotlin / Flutter land in later milestones.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
rust:
description: 'Publish Rust crates to crates.io'
type: boolean
default: true
node:
description: 'Build + publish Node binding to npm'
type: boolean
default: false
python:
description: 'Publish Python wheels to PyPI'
type: boolean
default: false
jvm:
description: 'Publish Java + Kotlin to Maven Central'
type: boolean
default: false
flutter:
description: 'Publish Flutter plugin to pub.dev (embeds mobile FFI libs)'
type: boolean
default: false
artifacts:
description: 'Build Go .a + C ABI libs and create GitHub Release'
type: boolean
default: false
permissions:
contents: write
id-token: write # npm provenance + PyPI trusted publishing
env:
CARGO_TERM_COLOR: always
jobs:
# ── Rust core → crates.io (dependency order) ─────────────────────────────
rust-publish:
name: Publish crates to crates.io
runs-on: ubuntu-latest
environment: release
if: github.event_name == 'push' || github.event.inputs.rust == 'true'
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# Idempotent publish: skips a crate whose version already exists on
# crates.io, so re-running the workflow after a partial failure is safe.
# The version is read from the workspace Cargo.toml so this never goes
# stale across releases.
- name: Publish aimux-stream
run: |
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
if curl -sf -A "aimux-ci" "https://crates.io/api/v1/crates/aimux-stream/${VERSION}" > /dev/null 2>&1; then
echo "aimux-stream@${VERSION} already published, skipping"
else
cargo publish -p aimux-stream
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish aimux-core
run: |
sleep 30
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
if curl -sf -A "aimux-ci" "https://crates.io/api/v1/crates/aimux-core/${VERSION}" > /dev/null 2>&1; then
echo "aimux-core@${VERSION} already published, skipping"
else
cargo publish -p aimux-core
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish aimux-provider-utils
run: |
sleep 30
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
if curl -sf -A "aimux-ci" "https://crates.io/api/v1/crates/aimux-provider-utils/${VERSION}" > /dev/null 2>&1; then
echo "aimux-provider-utils@${VERSION} already published, skipping"
else
cargo publish -p aimux-provider-utils
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish aimux-providers
run: |
sleep 30
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
if curl -sf -A "aimux-ci" "https://crates.io/api/v1/crates/aimux-providers/${VERSION}" > /dev/null 2>&1; then
echo "aimux-providers@${VERSION} already published, skipping"
else
cargo publish -p aimux-providers
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish aimux-ffi
run: |
sleep 30
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
if curl -sf -A "aimux-ci" "https://crates.io/api/v1/crates/aimux-ffi/${VERSION}" > /dev/null 2>&1; then
echo "aimux-ffi@${VERSION} already published, skipping"
else
cargo publish -p aimux-ffi
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# ── Node binding: build per-platform .node, then publish to npm ──────────
node-build:
name: Node build (${{ matrix.settings.target }})
runs-on: ${{ matrix.settings.host }}
env:
MACOSX_DEPLOYMENT_TARGET: '10.13'
if: github.event_name == 'push' || github.event.inputs.node == 'true'
strategy:
fail-fast: false
matrix:
settings:
- host: windows-latest
target: x86_64-pc-windows-msvc
binary: aimux.win32-x64-msvc.node
- host: windows-latest
target: aarch64-pc-windows-msvc
binary: aimux.win32-arm64-msvc.node
- host: macos-15-intel
target: x86_64-apple-darwin
binary: aimux.darwin-x64.node
- host: macos-latest
target: aarch64-apple-darwin
binary: aimux.darwin-arm64.node
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary: aimux.linux-x64-gnu.node
- host: ubuntu-latest
target: aarch64-unknown-linux-gnu
binary: aimux.linux-arm64-gnu.node
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
working-directory: bindings/node
run: npm ci
- name: Build Linux (glibc 2.17 baseline)
if: contains(matrix.settings.target, 'unknown-linux-gnu')
working-directory: bindings/node
shell: bash
env:
TARGET_CC: clang
TARGET_CXX: clang++
run: npx napi build --platform --release --target ${{ matrix.settings.target }} --use-napi-cross
- name: Build
if: ${{ !contains(matrix.settings.target, 'unknown-linux-gnu') }}
working-directory: bindings/node
shell: bash
run: npx napi build --platform --release --target ${{ matrix.settings.target }}
- name: Verify artifact
working-directory: bindings/node
shell: bash
run: test -f '${{ matrix.settings.binary }}'
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: node-${{ matrix.settings.target }}
path: bindings/node/*.node
node-publish:
name: Publish Node to npm
needs: node-build
runs-on: ubuntu-latest
environment: release
if: github.event_name == 'push' || github.event.inputs.node == 'true'
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Install dependencies
working-directory: bindings/node
run: npm ci
- name: Build napi index.js + d.ts (regenerate from current lib.rs)
# index.js / index.d.ts are napi build artifacts; the committed
# copies may be stale (e.g. initLogging added in RFC-0014 but not
# re-exported in the committed index.js). Rebuild on the host to
# regenerate them before tsc + prepublish.
working-directory: bindings/node
run: npm run build
- name: Create platform packages
working-directory: bindings/node
run: npx napi create-npm-dirs
- name: Download platform binaries
uses: actions/download-artifact@v4
with:
path: bindings/node/artifacts
pattern: node-*
- name: Collect and verify platform packages
working-directory: bindings/node
shell: bash
run: |
npx napi artifacts --output-dir artifacts --npm-dir npm
expected=(
npm/win32-x64-msvc/aimux.win32-x64-msvc.node
npm/win32-arm64-msvc/aimux.win32-arm64-msvc.node
npm/darwin-x64/aimux.darwin-x64.node
npm/darwin-arm64/aimux.darwin-arm64.node
npm/linux-x64-gnu/aimux.linux-x64-gnu.node
npm/linux-arm64-gnu/aimux.linux-arm64-gnu.node
)
for binary in "${expected[@]}"; do
test -f "$binary" || { echo "Missing $binary" >&2; exit 1; }
done
test "$(find npm -type f -name 'aimux.*.node' | wc -l | tr -d ' ')" = 6
- name: Publish
working-directory: bindings/node
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"
NPM_CONFIG_ACCESS: public
# ── Python binding: build + publish platform wheels (OIDC trusted) ───────
python-release:
name: Publish Python (${{ matrix.os }})
runs-on: ${{ matrix.os }}
if: github.event_name == 'push' || github.event.inputs.python == 'true'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build and publish wheel
uses: PyO3/maturin-action@v1
with:
command: publish
args: --no-sdist
working-directory: bindings/python
# ── Go binding: build libaimux_ffi.a per platform, ship on GitHub Release
# (users download the archive for their platform via `go generate`).
# NOTE: Windows uses the gnu target — Go's cgo links with mingw gcc,
# which cannot consume MSVC-format .a archives. ─────────────────────────
go-build:
name: Go binding (${{ matrix.settings.target }})
runs-on: ${{ matrix.settings.host }}
if: github.event_name == 'push' || github.event.inputs.artifacts == 'true'
strategy:
fail-fast: false
matrix:
settings:
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
- host: macos-15-intel
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
- host: windows-latest
target: x86_64-pc-windows-gnu
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
# macos-13 runners do not preinstall Go (ubuntu/windows do) — pin it
# explicitly so every host has the same toolchain.
- uses: actions/setup-go@v5
with:
go-version: "1.24.x"
- name: Build static archive
run: cargo build -p aimux-ffi --release --target ${{ matrix.settings.target }}
- name: Test Go binding
working-directory: bindings/go
shell: bash
run: |
export CGO_LDFLAGS="-L${{ github.workspace }}/target/${{ matrix.settings.target }}/release"
go vet ./...
go test ./...
- name: Stage archive with platform name
shell: bash
run: |
mkdir -p staging
cp target/${{ matrix.settings.target }}/release/libaimux_ffi.a staging/libaimux_ffi-${{ matrix.settings.target }}.a
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: libaimux_ffi-${{ matrix.settings.target }}.a
path: staging/libaimux_ffi-${{ matrix.settings.target }}.a
console-build-test:
name: CONSOLE-TEST (${{ matrix.settings.target }})
runs-on: ${{ matrix.settings.host }}
if: github.event_name == 'push' || github.event.inputs.artifacts == 'true'
strategy:
fail-fast: false
matrix:
include:
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
suffix: linux-x64
- host: macos-latest
target: aarch64-apple-darwin
suffix: macos-arm64
- host: windows-latest
target: x86_64-pc-windows-msvc
suffix: windows-x64.exe
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
# macos-13 runners do not preinstall Go (ubuntu/windows do) — pin it
# explicitly so every host has the same toolchain.
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build frontend
working-directory: tools/aimux-web/web
run: |
npm ci
npm run build
- name: Build console (embedded frontend)
run: cargo build --release --target ${{ matrix.settings.target }} -p aimux-web --features embed-frontend
- name: Build CLI tools
run: cargo build --release --target ${{ matrix.settings.target }} -p aimux-cli -p aimux-replay
- name: Test Go binding
working-directory: bindings/go
shell: bash
run: |
export CGO_LDFLAGS="-L${{ github.workspace }}/target/${{ matrix.settings.target }}/release"
go vet ./...
go test ./...
- name: Stage binaries with platform suffix
shell: bash
run: |
mkdir -p staging
EXT=""; [ "$RUNNER_OS" = "Windows" ] && EXT=".exe"
cp "target/${{ matrix.settings.target }}/release/aimux-web$EXT" "staging/aimux-web-${{ matrix.settings.suffix }}"
for tool in aimux-cli aimux-replay; do
if [ -f "target/${{ matrix.settings.target }}/release/$tool$EXT" ]; then
cp "target/${{ matrix.settings.target }}/release/$tool$EXT" "staging/$tool-${{ matrix.settings.suffix }}"
fi
done
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: aimux-web-test-${{ matrix.settings.target }}
path: staging/*
# ── C ABI shared libraries (C/C++ + reference for other bindings) ────────
ffi-build:
name: aimux-ffi (${{ matrix.target }})
runs-on: ${{ matrix.host }}
if: github.event_name == 'push' || github.event.inputs.artifacts == 'true'
strategy:
fail-fast: false
matrix:
include:
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: libaimux_ffi-linux-x64.so
- host: macos-latest
target: aarch64-apple-darwin
artifact: libaimux_ffi-macos-arm64.dylib
- host: windows-latest
target: x86_64-pc-windows-msvc
artifact: aimux_ffi-windows-x64.dll
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Build cdylib
run: cargo build -p aimux-ffi --release --target ${{ matrix.target }}
- name: Stage library with platform name
shell: bash
run: |
mkdir -p staging
case "${{ matrix.target }}" in
*linux*) cp target/${{ matrix.target }}/release/libaimux_ffi.so staging/${{ matrix.artifact }} ;;
*darwin*) cp target/${{ matrix.target }}/release/libaimux_ffi.dylib staging/${{ matrix.artifact }} ;;
*windows*) cp target/${{ matrix.target }}/release/aimux_ffi.dll staging/${{ matrix.artifact }} ;;
esac
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: staging/${{ matrix.artifact }}
# ── aimux-web console + CLI: self-contained binaries with the embedded
# frontend, shipped on the GitHub Release so users download one file and
# run it (no cargo / npm step). The CLI tools ride along on the same
# matrix — they share the heavy provider dependency tree. ───────────────
console-build:
name: aimux-web (${{ matrix.settings.target }})
runs-on: ${{ matrix.settings.host }}
if: github.event_name == 'push' || github.event.inputs.artifacts == 'true'
strategy:
fail-fast: false
matrix:
include:
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
suffix: -linux-x64
- host: macos-latest
target: aarch64-apple-darwin
suffix: -macos-arm64
- host: windows-latest
target: x86_64-pc-windows-msvc
suffix: -windows-x64.exe
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build frontend
working-directory: tools/aimux-web/web
run: |
npm ci
npm run build
- name: Build console (embedded frontend)
run: cargo build --release --target ${{ matrix.settings.target }} -p aimux-web --features embed-frontend
- name: Build CLI tools
run: cargo build --release --target ${{ matrix.settings.target }} -p aimux-cli -p aimux-replay
- name: Stage binaries with platform suffix
shell: bash
run: |
mkdir -p staging
EXT=""; [ "$RUNNER_OS" = "Windows" ] && EXT=".exe"
cp "target/${{ matrix.settings.target }}/release/aimux-web$EXT" "staging/aimux-web${{ matrix.settings.suffix }}"
for tool in aimux-cli aimux-replay; do
if [ -f "target/${{ matrix.settings.target }}/release/$tool$EXT" ]; then
cp "target/${{ matrix.settings.target }}/release/$tool$EXT" "staging/$tool${{ matrix.settings.suffix }}"
fi
done
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: aimux-web-${{ matrix.settings.target }}
path: staging/*
# ── Flutter mobile artifacts: Android .so per ABI + iOS .xcframework ──────
# Built at release time and embedded into the Flutter plugin package by the
# flutter-publish job, so the pub.dev artifact ships with its native core.
flutter-ffi-mobile:
name: Flutter mobile FFI (${{ matrix.platform }})
runs-on: ${{ matrix.host }}
# Built for both the GitHub Release (manual integration) and the
# pub.dev plugin package (flutter input).
if: github.event_name == 'push' || github.event.inputs.artifacts == 'true' || github.event.inputs.flutter == 'true'
strategy:
fail-fast: false
matrix:
include:
- platform: android
host: ubuntu-latest
targets: aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
- platform: ios
host: macos-latest
targets: aarch64-apple-ios aarch64-apple-ios-sim
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.targets }}
- uses: Swatinem/rust-cache@v2
- name: Build Android .so (cargo-ndk)
if: matrix.platform == 'android'
run: |
cargo install cargo-ndk
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o staging/android \
build -p aimux-ffi --release
for abi in arm64-v8a armeabi-v7a x86_64; do
test -f "staging/android/$abi/libaimux_ffi.so" || { echo "Missing $abi" >&2; exit 1; }
done
- name: Build iOS xcframework
if: matrix.platform == 'ios'
run: |
mkdir -p staging
bash scripts/build-ios-xcframework.sh staging/aimux_ffi.xcframework
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: flutter-${{ matrix.platform }}
path: staging/
# ── JVM bindings: publish aimux-java + aimux-kotlin to Maven Central ──────
jvm-publish:
name: Publish Java+Kotlin to Maven Central
runs-on: ubuntu-latest
environment: release
if: github.event_name == 'push' || github.event.inputs.jvm == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: gradle/actions/setup-gradle@v4
- name: Publish aimux-java
working-directory: bindings/java
run: gradle publish
env:
ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.OSSRH_USERNAME }}
ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.OSSRH_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
- name: Publish aimux-kotlin
working-directory: bindings/kotlin
run: gradle publish
env:
ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.OSSRH_USERNAME }}
ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.OSSRH_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
- name: Submit deployments to Central Portal
# The OSSRH Staging API compatibility service only stages uploads;
# this POST (same IP, same runner) moves them into the Portal for
# the ai.arcships namespace.
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
run: |
curl -sS -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" \
-X POST "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/ai.arcships" \
-w '\nHTTP %{http_code}\n'
# 201/202 = accepted; the deployment then appears in the Portal
# (central.sonatype.com/publishing) for release.
# ── Flutter plugin: embed mobile FFI libs, test, publish to pub.dev ───────
# Publisher: arcships.ai (pub.dev). The native core (Android .so per ABI +
# iOS xcframework) is built by flutter-ffi-mobile and embedded into the
# package so consumers get a working plugin from `flutter pub add aimux`.
flutter-publish:
name: Publish Flutter to pub.dev
needs: flutter-ffi-mobile
runs-on: ubuntu-latest
environment: release
if: github.event_name == 'push' || github.event.inputs.flutter == 'true'
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: subosito/flutter-action@v2
with:
channel: stable
- name: Download mobile FFI artifacts
uses: actions/download-artifact@v4
with:
path: mobile-artifacts
pattern: flutter-*
merge-multiple: true
- name: Embed Android .so per ABI
run: |
for abi in arm64-v8a armeabi-v7a x86_64; do
test -f "mobile-artifacts/android/$abi/libaimux_ffi.so" || { echo "Missing $abi" >&2; exit 1; }
cp "mobile-artifacts/android/$abi/libaimux_ffi.so" \
"bindings/flutter/android/src/main/jniLibs/$abi/libaimux_ffi.so"
done
- name: Embed iOS xcframework
run: |
# flutter-ffi-mobile (ios) uploads staging/ contents, so the
# artifact lands at the root (no ios/ prefix).
test -f mobile-artifacts/aimux_ffi.xcframework/Info.plist
cp -r mobile-artifacts/aimux_ffi.xcframework bindings/flutter/ios/
- name: Build host FFI library (for tests)
run: cargo build -p aimux-ffi --release
- name: Test
working-directory: bindings/flutter
env:
LD_LIBRARY_PATH: ${{ github.workspace }}/target/release
run: flutter test
- name: Analyze
working-directory: bindings/flutter
run: flutter analyze
- name: Publish to pub.dev
working-directory: bindings/flutter
env:
# OAuth credentials from `dart pub login` (pub-credentials.json),
# stored as a single JSON secret.
PUB_DEV_TOKEN: ${{ secrets.PUB_DEV_TOKEN }}
run: |
mkdir -p "$HOME/.config/dart"
printf '%s' "$PUB_DEV_TOKEN" > "$HOME/.config/dart/pub-credentials.json"
flutter pub publish --force
# ── GitHub Release: aggregate all binary artifacts (C ABI libs + Go .a) ──
github-release:
name: Create GitHub Release
needs: [go-build, ffi-build, console-build, flutter-ffi-mobile]
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.inputs.artifacts == 'true'
steps:
- uses: actions/checkout@v4
# workflow_dispatch runs have no tag ref; resolve it from Cargo.toml so
# softprops/action-gh-release can target the right tag either way.
- name: Resolve version
id: version
run: |
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: List assets
run: ls -la release-assets/
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
files: release-assets/**
generate_release_notes: true