Files
openworker/.github/workflows/release.yml
Rohit C Prasad 924358016c Intel Mac builds: link OpenSSL statically into cryptography
The Intel sidecar and program died on their first import; two libssl copies were bundled under one name.
The DMG build now starts the sidecar once (openworker-server --check).
2026-10-05 18:30:54 -07:00

250 lines
11 KiB
YAML

# Desktop release builds — macOS (.dmg, arm64 + Intel) and Windows (.msi + NSIS .exe) — and
# the `openworker` command-line program for Linux (.tar.gz, x86_64 + aarch64;
# packaging/build_linux.sh) and for macOS (.tar.gz, arm64 + x86_64; packaging/build_macos_cli.sh).
#
# CI calls the SAME scripts developers run locally (packaging/build_dmg.sh and
# build_windows.ps1); this file only provisions the toolchain (Node, Rust, a Python venv at
# .venv with PyInstaller) and publishes the results.
#
# Triggers:
# - tag push `v*` → builds all targets and attaches them to a DRAFT GitHub Release
# (review, then publish by hand).
# - manual run → builds and uploads workflow artifacts only (no release).
#
# Each installer is uploaded twice: once with its versioned name (archive) and once with a
# stable name (OpenWorker-macos-arm64.dmg, …) so the website can link to
# github.com/<repo>/releases/latest/download/<stable-name>
# and never need updating.
#
# macOS signing + notarization: Tauri's bundler handles both during `tauri build` when the
# APPLE_* env vars are present (import cert → sign app + sidecar with hardened runtime →
# notarize via notarytool → staple). Driven by repo secrets:
# APPLE_CERTIFICATE base64 .p12 (Developer ID Application cert + key)
# APPLE_CERTIFICATE_PASSWORD the .p12 export password
# APPLE_SIGNING_IDENTITY e.g. "Developer ID Application: Name (TEAMID)"
# APPLE_API_KEY_CONTENT base64 App Store Connect API .p8 (notarytool)
# APPLE_API_KEY the API key id
# APPLE_API_ISSUER the API issuer id
# When the secrets are absent (forks, scratch runs) the build degrades to unsigned —
# installable via `xattr -cr`. Windows remains unsigned (Authenticode is a later step).
name: Release
on:
push:
tags: ["v*", "app-v*"]
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest # Apple Silicon
slug: macos-arm64
# Intel macOS: macos-13 retired in Dec 2025; macos-15-intel replaced it and is
# the LAST x86_64 image Actions will offer (available until Aug 2027). Builds
# natively — the sidecar is a PyInstaller freeze, which cannot cross-compile.
- os: macos-15-intel
slug: macos-x64
- os: windows-latest
slug: windows
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: surfaces/gui/package-lock.json
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: surfaces/gui/src-tauri
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Set up the sidecar venv (.venv)
# The build scripts expect a venv at .venv with the package + PyInstaller.
# typer/tzdata are build-time-only (PyInstaller walks mcp.cli, which needs typer;
# tzdata ships zoneinfo for Windows). aisuite installs like any other dependency
# (git-pinned in pyproject.toml).
run: |
python -m venv .venv
if [ "$RUNNER_OS" = "Windows" ]; then VPY=.venv/Scripts/python; else VPY=.venv/bin/python; fi
"$VPY" -m pip install --upgrade pip
# openshell: grpcio, without which the macOS sidecar cannot use OpenShell
# sandboxes (build_dmg.sh refuses to build without it). Not used on Windows.
if [ "$RUNNER_OS" = "Windows" ]; then EXTRAS=bedrock; else EXTRAS=bedrock,openshell; fi
# Intel Mac: cryptography ships no wheel for it any more, so pip builds it from
# source against Homebrew's OpenSSL, linked dynamically. PyInstaller then bundles
# that libssl AND Python's own older one under the same name, the old one wins,
# and the program dies on import ("Symbol not found: _SSL_get0_group_name"; the
# 0.3.0 and 0.3.1 Intel apps shipped this way). Link OpenSSL in statically.
if [ "$RUNNER_OS" = "macOS" ] && [ "$(uname -m)" = "x86_64" ]; then
export OPENSSL_STATIC=1 OPENSSL_DIR="$(brew --prefix openssl@3)"
fi
"$VPY" -m pip install -e ".[$EXTRAS]" pyinstaller typer tzdata
"$VPY" -c "import aisuite, coworker" # fail fast if either import breaks
- name: npm ci
working-directory: surfaces/gui
run: npm ci
- name: Build .dmg (macOS)
if: runner.os == 'macOS'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
# Auto-update artifact signing (minisign, separate from Apple signing). Absent →
# the build script skips updater artifacts with a warning (fork/scratch runs).
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
# Unset empty APPLE_* vars so runs without secrets stay cleanly unsigned
# (Tauri treats a present-but-empty var as a config error).
for v in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_API_KEY APPLE_API_ISSUER; do
[ -n "$(eval echo "\${$v:-}")" ] || unset "$v"
done
if [ -n "${APPLE_API_KEY_CONTENT:-}" ]; then
echo "$APPLE_API_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/AuthKey.p8"
export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
fi
unset APPLE_API_KEY_CONTENT
bash packaging/build_dmg.sh
# The command-line program for the Mac, from the same venv and identity: signed
# and notarized when the secrets are present.
bash packaging/build_macos_cli.sh
- name: Build .msi + NSIS .exe (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: ./packaging/build_windows.ps1
- name: Stage artifacts (versioned + stable names)
run: |
mkdir -p out
BUNDLE=surfaces/gui/src-tauri/target/release/bundle
if [ "$RUNNER_OS" = "Windows" ]; then
cp "$BUNDLE"/nsis/*.exe out/
cp "$BUNDLE"/nsis/*.exe out/OpenWorker-windows-setup.exe
cp "$BUNDLE"/msi/*.msi out/
cp "$BUNDLE"/msi/*.msi out/OpenWorker-windows.msi
# Updater signature for the NSIS installer (present only when the updater key
# secret is configured). The .sig signs CONTENT, so the stable rename is safe.
SIG=$(ls "$BUNDLE"/nsis/*.exe.sig 2>/dev/null | head -1 || true)
[ -n "$SIG" ] && cp "$SIG" out/OpenWorker-windows-setup.exe.sig
else
cp "$BUNDLE"/dmg/*.dmg out/
cp "$BUNDLE"/dmg/*.dmg out/OpenWorker-${{ matrix.slug }}.dmg
# macOS updater artifact: the signed .app tarball the installed app swaps in.
if [ -f "$BUNDLE"/macos/OpenWorker.app.tar.gz ]; then
cp "$BUNDLE"/macos/OpenWorker.app.tar.gz out/OpenWorker-${{ matrix.slug }}.app.tar.gz
cp "$BUNDLE"/macos/OpenWorker.app.tar.gz.sig out/OpenWorker-${{ matrix.slug }}.app.tar.gz.sig
fi
# The command-line program, named by uname -m (arm64, x86_64) as install.sh asks.
ARCH=$(uname -m)
cp packaging/dist/openworker-*-macos-"$ARCH".tar.gz* out/
cp packaging/dist/openworker-*-macos-"$ARCH".tar.gz out/openworker-macos-"$ARCH".tar.gz
(cd out && shasum -a 256 openworker-macos-"$ARCH".tar.gz > openworker-macos-"$ARCH".tar.gz.sha256)
fi
ls -la out
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.slug }}
path: out/*
if-no-files-found: error
linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: x86_64
- os: ubuntu-24.04-arm
arch: aarch64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
# Builds inside a Debian bullseye container (glibc 2.31), natively on each
# architecture: the program is a PyInstaller freeze, which cannot cross-compile.
- name: Build the Linux program
run: bash packaging/build_linux.sh
- name: Stage artifacts (versioned + stable names)
run: |
mkdir -p out
cp packaging/dist/openworker-*-linux-${{ matrix.arch }}.tar.gz* out/
cp packaging/dist/openworker-*-linux-${{ matrix.arch }}.tar.gz out/openworker-linux-${{ matrix.arch }}.tar.gz
# packaging/install.sh fetches the stable name for "latest" and checks it against this.
(cd out && sha256sum openworker-linux-${{ matrix.arch }}.tar.gz > openworker-linux-${{ matrix.arch }}.tar.gz.sha256)
ls -la out
- uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}
path: out/*
if-no-files-found: error
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [build, linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Compose the auto-update manifest (latest.json)
# Shipped apps poll releases/latest/download/latest.json (via the branded
# download.openworker.com redirect) — publishing this release IS pushing the
# update. The tag must match tauri.conf.json's version or installed apps would
# see a permanent phantom update; fail loudly on drift. Runs only when signed
# updater artifacts exist (i.e. the TAURI_SIGNING_PRIVATE_KEY secret is set).
run: |
TAG="${GITHUB_REF_NAME}"
CONF_VERSION=$(python3 -c "import json; print(json.load(open('surfaces/gui/src-tauri/tauri.conf.json'))['version'])")
if [ "${TAG#v}" != "$CONF_VERSION" ]; then
echo "::error::tag $TAG != tauri.conf.json version $CONF_VERSION — bump the config before tagging"
exit 1
fi
if ls dist/*.sig >/dev/null 2>&1; then
python3 packaging/make_update_manifest.py \
--version "${TAG#v}" --tag "$TAG" --repo "$GITHUB_REPOSITORY" \
--dist dist --out dist/latest.json \
--notes "OpenWorker ${TAG#v}"
else
echo "::warning::no updater signatures in dist/ — release ships WITHOUT auto-update manifest"
fi
- uses: softprops/action-gh-release@v2
with:
draft: true
files: dist/*
generate_release_notes: true