Skip to content

release

release #139

Workflow file for this run

name: release
permissions:
contents: write
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
tag:
description: 'Existing version tag to build and attach to the release (e.g. v0.1.0)'
required: true
# Cross-platform Phase 7 release pipeline. Builds Companion Emergence.app /
# .deb / .msi by running app/build_python_runtime.sh as part of
# tauri-build's beforeBuildCommand on each runner. The alpha hosted
# runner matrix intentionally omits macOS x86_64: GitHub's Intel macOS
# runner has been stuck queued indefinitely for this private repo. Intel
# Mac users are source-build-only until we have a reliable runner.
#
# Audit 2026-05-07 P3-10: validation job runs Python tests + Ruff +
# frontend tests + frontend build + Rust check before any matrix
# build runs. A release tag should never produce bundles from a
# commit that fails normal validation.
#
# Audit 2026-05-07 P2-6: bundles get attached to a real GitHub
# Release from each platform build job, avoiding an extra publish runner
# that can be blocked by private-repo billing limits after heavy builds.
# Release notes are generated only by macos-arm64 so parallel platform
# uploads do not append duplicate generated notes. Previously the workflow
# only used upload-artifact (14-day retention), which contradicted
# README/INSTALL pointing users at "the Releases page".
#
# Manual dispatch builds the supplied existing tag rather than whatever
# happens to be at the default branch tip, and publishes to that tag's
# GitHub Release. This keeps manual retries equivalent to tag-push runs.
#
# Signing + notarization are NOT included here — those need
# Apple Developer / Microsoft / Linux distro keys, and the manual
# release-checklist walks through the steps.
env:
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
jobs:
validate:
name: validate (tests + lint + frontend + cargo)
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ env.RELEASE_REF }}
- name: Validate manual release tag
if: github.event_name == 'workflow_dispatch'
run: |
case "$RELEASE_TAG" in
v*.*.*) ;;
*) echo "manual release tag must look like vX.Y.Z (got '$RELEASE_TAG')" >&2; exit 1 ;;
esac
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
- name: Set up Python 3.12
run: uv python install 3.12
- name: Install Python deps
run: uv sync --all-extras --locked
- name: Run pytest
run: uv run pytest -q
- name: Lint with ruff
run: uv run ruff check .
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
version: 9
- name: Install Node 22
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: app/pnpm-lock.yaml
- name: Frontend deps
working-directory: app
run: pnpm install --frozen-lockfile
- name: Frontend tests (vitest)
working-directory: app
run: pnpm test
- name: Frontend build (tsc + vite)
working-directory: app
run: pnpm build
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Tauri system deps (validate runs on ubuntu)
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev
- name: cargo check
working-directory: app/src-tauri
run: cargo check
- name: cargo test
working-directory: app/src-tauri
run: cargo test
- name: Wheel/sdist smoke
run: bash scripts/smoke_test_wheel.sh
build:
name: ${{ matrix.platform.label }}
needs: validate
strategy:
fail-fast: false
matrix:
platform:
- { label: macos-arm64, os: macos-14, bundles: 'app,dmg' }
- { label: linux-x86_64, os: ubuntu-22.04, bundles: 'deb,appimage' }
- { label: windows-x86_64, os: windows-2022, bundles: 'msi,nsis' }
runs-on: ${{ matrix.platform.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ env.RELEASE_REF }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
- name: Install Python 3.13 (for `uv build`)
run: uv python install 3.13
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
version: 9
- name: Install Node 22
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: app/pnpm-lock.yaml
- name: Linux — install Tauri system deps
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev
- name: Frontend dependencies
working-directory: app
run: pnpm install --frozen-lockfile
- name: Build Python wheel + sync uv env
run: uv sync --all-extras --locked
- name: Build Tauri bundle (runs build_python_runtime.sh
via beforeBuildCommand)
working-directory: app
run: pnpm tauri build --bundles ${{ matrix.platform.bundles }}
shell: bash
- name: Validate bundled Python runtime
working-directory: app/src-tauri
run: |
if [ "$RUNNER_OS" = "Windows" ]; then
PY="python-runtime/python.exe"
else
PY="python-runtime/bin/python3"
file "$PY" || true
fi
"$PY" -c "import platform, sys; import brain, fastapi, uvicorn; print(sys.executable); print(platform.platform(), platform.machine())"
shell: bash
- name: Smoke bundled brain CLI
working-directory: app/src-tauri
run: |
if [ "$RUNNER_OS" = "Windows" ]; then
PY="python-runtime/python.exe"
# Git Bash on Windows can't exec the bundled nell.bat
# through a Unix-style relative path. Mimic what the .bat
# does at runtime — invoke python.exe directly with the
# same -c incantation. Functionally equivalent for the
# smoke check.
NELL=("$PY" "-c" "from brain.cli import main; main()")
else
PY="python-runtime/bin/python3"
NELL=("python-runtime/bin/nell")
fi
TMP_HOME="$("$PY" -c "import tempfile; print(tempfile.mkdtemp(prefix='nell-smoke-'))")"
cleanup() {
NELLBRAIN_HOME="$TMP_HOME" "$PY" -c "import os, shutil; shutil.rmtree(os.environ['NELLBRAIN_HOME'], ignore_errors=True)"
}
trap cleanup EXIT
"${NELL[@]}" --version
NELLBRAIN_HOME="$TMP_HOME" "${NELL[@]}" init \
--persona smoke_persona \
--user-name Smoke \
--voice-template default \
--fresh
NELLBRAIN_HOME="$TMP_HOME" "$PY" -c "import os; from pathlib import Path; assert (Path(os.environ['NELLBRAIN_HOME']) / 'personas' / 'smoke_persona' / 'persona_config.json').is_file()"
NELLBRAIN_HOME="$TMP_HOME" "${NELL[@]}" status --persona smoke_persona
shell: bash
- name: Prepare release artifacts and checksums
working-directory: app/src-tauri
env:
PLATFORM_LABEL: ${{ matrix.platform.label }}
run: |
if [ "$RUNNER_OS" = "Windows" ]; then
PY="python-runtime/python.exe"
else
PY="python-runtime/bin/python3"
fi
"$PY" - <<'PY'
import hashlib
import os
import shutil
from pathlib import Path
label = os.environ["PLATFORM_LABEL"]
root = Path("target/release/bundle")
out_dir = Path("target/release/release-assets")
out_dir.mkdir(parents=True, exist_ok=True)
suffixes = {".dmg", ".deb", ".AppImage", ".msi", ".exe"}
packages = sorted(
path for path in root.rglob("*")
if path.is_file() and path.suffix in suffixes
)
if not packages:
raise SystemExit(f"no release package files found under {root}")
files = []
for path in packages:
# GitHub Release URLs are easier to document and copy/paste
# when the product-name space is replaced consistently.
target = out_dir / path.name.replace("Companion Emergence", "Companion.Emergence")
shutil.copy2(path, target)
files.append(target)
out = out_dir / f"SHA256SUMS-{label}.txt"
with out.open("w", encoding="utf-8", newline="\n") as handle:
for path in files:
digest = hashlib.sha256(path.read_bytes()).hexdigest()
handle.write(f"{digest} {path.name}\n")
print(out)
print(out.read_text(encoding="utf-8"))
PY
shell: bash
- name: Upload bundle artifacts (workflow retention)
uses: actions/upload-artifact@v7
with:
name: nellface-${{ matrix.platform.label }}
path: |
app/src-tauri/target/release/bundle/macos/*.app
app/src-tauri/target/release/bundle/dmg/*.dmg
app/src-tauri/target/release/bundle/deb/*.deb
app/src-tauri/target/release/bundle/appimage/*.AppImage
app/src-tauri/target/release/bundle/msi/*.msi
app/src-tauri/target/release/bundle/nsis/*.exe
app/src-tauri/target/release/release-assets/*
if-no-files-found: ignore
retention-days: 14
- name: Publish to GitHub Release
if: startsWith(env.RELEASE_TAG, 'v')
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.RELEASE_TAG }}
prerelease: ${{ contains(env.RELEASE_TAG, '-') }}
make_latest: ${{ contains(env.RELEASE_TAG, '-') && 'false' || 'legacy' }}
fail_on_unmatched_files: true
overwrite_files: true
files: |
app/src-tauri/target/release/release-assets/*
generate_release_notes: ${{ matrix.platform.label == 'macos-arm64' }}
append_body: false