Skip to content

Release

Release #72

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: write
jobs:
build-sdist:
name: Build sdists
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Build review app frontend
working-directory: web/review-app
run: npm ci && npm run build
- name: Build terminal app frontend
working-directory: web/terminal-app
run: npm ci && npm run build
- name: Build markdown preview app frontend
working-directory: web/markdown-app
run: npm ci && npm run build
- name: Build VNC app frontend
working-directory: web/vnc-app
run: npm ci && npm run build
- name: Build config app frontend
working-directory: web/config-app
run: npm ci && npm run build
- name: Build open-shrimp sdist
run: uv build --sdist
- name: Build moonshine-stt sdist
working-directory: moonshine-stt
run: uv build --sdist
- name: Upload open-shrimp sdist
uses: actions/upload-artifact@v7
with:
name: sdist-openshrimp
path: dist/*.tar.gz
- name: Upload moonshine-stt sdist
uses: actions/upload-artifact@v7
with:
name: sdist-moonshine-stt
path: moonshine-stt/dist/*.tar.gz
build-binary:
name: Build ${{ matrix.artifact }}
needs: build-sdist
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# open-shrimp
- os: ubuntu-latest
artifact: openshrimp-linux-x86_64
sdist_artifact: sdist-openshrimp
exec_code: "from open_shrimp.main import main; main()"
features: "libvirt"
- os: ubuntu-24.04-arm
artifact: openshrimp-linux-aarch64
sdist_artifact: sdist-openshrimp
exec_code: "from open_shrimp.main import main; main()"
features: "libvirt"
- os: macos-latest
artifact: openshrimp-macos-aarch64
sdist_artifact: sdist-openshrimp
exec_code: "from open_shrimp.main import main; main()"
- os: macos-latest
artifact: openshrimp-macos-x86_64
sdist_artifact: sdist-openshrimp
exec_code: "from open_shrimp.main import main; main()"
rust_target: x86_64-apple-darwin
# moonshine-stt
- os: ubuntu-latest
artifact: moonshine-stt-linux-x86_64
sdist_artifact: sdist-moonshine-stt
exec_code: "from moonshine_stt.main import main; main()"
- os: ubuntu-24.04-arm
artifact: moonshine-stt-linux-aarch64
sdist_artifact: sdist-moonshine-stt
exec_code: "from moonshine_stt.main import main; main()"
- os: macos-latest
artifact: moonshine-stt-macos-aarch64
sdist_artifact: sdist-moonshine-stt
exec_code: "from moonshine_stt.main import main; main()"
- os: macos-latest
artifact: moonshine-stt-macos-x86_64
sdist_artifact: sdist-moonshine-stt
exec_code: "from moonshine_stt.main import main; main()"
rust_target: x86_64-apple-darwin
steps:
- uses: actions/checkout@v6
- name: Download sdist
uses: actions/download-artifact@v8
with:
name: ${{ matrix.sdist_artifact }}
path: dist
- name: Install libvirt development headers
if: matrix.features && contains(matrix.features, 'libvirt')
run: sudo apt-get update && sudo apt-get install -y libvirt-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target || '' }}
- name: Cache Cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-pyapp-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-pyapp-
- name: Get sdist filename
id: sdist
run: echo "filename=$(realpath dist/*.tar.gz)" >> "$GITHUB_OUTPUT"
- name: Build with PyApp
env:
PYAPP_PROJECT_PATH: ${{ steps.sdist.outputs.filename }}
PYAPP_PROJECT_FEATURES: ${{ matrix.features || '' }}
PYAPP_PYTHON_VERSION: "3.11"
PYAPP_EXEC_CODE: ${{ matrix.exec_code }}
PYAPP_DISTRIBUTION_EMBED: "1"
PYAPP_PASS_LOCATION: "1"
run: |
if [ -n "${{ matrix.rust_target }}" ]; then
cargo install pyapp --force --root dist/pyapp --target ${{ matrix.rust_target }}
else
cargo install pyapp --force --root dist/pyapp
fi
- name: Rename binary
run: mv dist/pyapp/bin/pyapp dist/${{ matrix.artifact }}
- name: Import signing certificate
if: runner.os == 'macOS'
env:
CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
CERT_PW: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
KC_PW: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
run: |
KC="$RUNNER_TEMP/build.keychain-db"
echo -n "$CERT_B64" | base64 --decode -o "$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KC_PW" "$KC"
security set-keychain-settings -lut 21600 "$KC"
security unlock-keychain -p "$KC_PW" "$KC"
security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
rm "$RUNNER_TEMP/cert.p12"
- name: Sign and notarize binary
if: runner.os == 'macOS'
env:
IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
API_KEY_B64: ${{ secrets.MACOS_NOTARY_API_KEY_P8_BASE64 }}
API_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }}
API_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }}
run: |
set -e
BIN="dist/${{ matrix.artifact }}"
codesign --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "$IDENTITY" "$BIN"
codesign --verify --strict --verbose=2 "$BIN"
KEY="$RUNNER_TEMP/notary_key.p8"
echo -n "$API_KEY_B64" | base64 --decode -o "$KEY"
ZIP="$RUNNER_TEMP/${{ matrix.artifact }}.zip"
ditto -c -k --keepParent "$BIN" "$ZIP"
set +e
SUBMIT_OUT=$(xcrun notarytool submit "$ZIP" \
--key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" \
--wait --timeout 30m 2>&1)
SUBMIT_RC=$?
echo "$SUBMIT_OUT"
SUBMIT_ID=$(printf '%s\n' "$SUBMIT_OUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
if [ "$SUBMIT_RC" -ne 0 ] || ! printf '%s\n' "$SUBMIT_OUT" | grep -q 'status: Accepted'; then
if [ -n "$SUBMIT_ID" ]; then
echo "::group::Notary log for $SUBMIT_ID"
xcrun notarytool log "$SUBMIT_ID" \
--key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" || true
echo "::endgroup::"
fi
rm -f "$ZIP" "$KEY"
exit 1
fi
# Single-file binaries can't be stapled; Gatekeeper checks online.
rm "$ZIP" "$KEY"
- name: Upload binary
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: dist/${{ matrix.artifact }}
build-virtiofsd:
name: Build virtiofsd (${{ matrix.arch }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: x86_64
artifact: virtiofsd-linux-x86_64
- os: ubuntu-24.04-arm
arch: aarch64
artifact: virtiofsd-linux-aarch64
steps:
- name: Install system build dependencies
run: sudo apt-get update && sudo apt-get install -y libseccomp-dev libcap-ng-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-${{ runner.arch }}-cargo-virtiofsd-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-${{ runner.arch }}-cargo-virtiofsd-
- name: Clone virtiofsd
run: git clone --depth 1 --branch v1.13.3 https://gitlab.com/virtio-fs/virtiofsd.git
- name: Build virtiofsd
working-directory: virtiofsd
run: cargo build --release
- name: Rename binary
run: mv virtiofsd/target/release/virtiofsd ${{ matrix.artifact }}
- name: Upload binary
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
build-limactl:
# Builds the bundled `limactl` from upstream Lima at the version
# pinned by `patches/PIN`, with `patches/lima-vznc-display.patch`
# applied. The patch attaches Apple's private `_VZVNCServer` SPI to
# the running `VZVirtualMachine` so OpenShrimp can scrape the
# framebuffer without a `limactl` GUI window. Output tarball name and
# layout match upstream Lima releases so the runtime extractor in
# `lima_helpers.py:_download_lima_sync` works unchanged.
name: Build limactl (${{ matrix.uname_m }})
runs-on: macos-26
defaults:
run:
shell: bash -euo pipefail {0}
strategy:
fail-fast: false
matrix:
include:
- goarch: arm64
uname_m: arm64
- goarch: amd64
uname_m: x86_64
steps:
- uses: actions/checkout@v6
- name: Read pinned LIMA_VERSION
id: pin
run: |
PIN_VER=$(awk -F': *' '$1 == "tag" {sub(/^v/, "", $2); print $2}' patches/PIN)
if [ -z "$PIN_VER" ]; then
echo "::error::no 'tag:' line in patches/PIN"
exit 1
fi
PY_VER=$(awk -F'"' '/^LIMA_VERSION/ {print $2}' \
src/open_shrimp/sandbox/lima_helpers.py)
if [ "$PIN_VER" != "$PY_VER" ]; then
echo "::error::LIMA_VERSION skew: patches/PIN says v$PIN_VER but lima_helpers.py says $PY_VER"
exit 1
fi
echo "version=$PIN_VER" >> "$GITHUB_OUTPUT"
- name: Clone Lima at v${{ steps.pin.outputs.version }}
run: |
git clone --depth 1 \
--branch v${{ steps.pin.outputs.version }} \
https://github.com/lima-vm/lima.git lima-src
- uses: actions/setup-go@v6
with:
go-version: stable
- name: Cache Go modules
# Must run after `git clone lima-src` so hashFiles('lima-src/go.sum')
# actually hashes a file — otherwise the cache key would not
# invalidate on a Lima version bump.
uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/Library/Caches/go-build
key: ${{ runner.os }}-${{ matrix.goarch }}-go-limactl-${{ hashFiles('lima-src/go.sum', 'patches/lima-vznc-display.patch', 'vznc/go.mod') }}
restore-keys: ${{ runner.os }}-${{ matrix.goarch }}-go-limactl-
- name: Apply OpenShrimp patch
run: |
cd lima-src
git apply ../patches/lima-vznc-display.patch
# The patch's `replace` line uses `../../vznc`, valid for the dev
# checkout layout but not for the CI workspace. Rewrite it.
go mod edit -replace=github.com/openshrimp/vznc=${GITHUB_WORKSPACE}/vznc
- name: Build binaries (${{ matrix.goarch }})
working-directory: lima-src
env:
GOOS: darwin
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "1"
# Lima's Makefile pulls VERSION from `git describe`. The shallow
# clone above drops tag annotations, so pass it explicitly.
run: make VERSION=v${{ steps.pin.outputs.version }} binaries
- name: Build manpages (host arch only)
# Lima's manpage build runs the just-built limactl binary, so it
# only works on the host arch (macos-26 = arm64). The amd64
# tarball ships without manpages — the runtime extractor doesn't
# require them.
if: matrix.goarch == 'arm64'
working-directory: lima-src
env:
GOOS: darwin
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "1"
run: make VERSION=v${{ steps.pin.outputs.version }} manpages
- name: Verify Code-Hex/vz field-layout test
# Native arch only: the cross-built amd64 binary can't execute
# on the arm64 host. The test reads struct layout via reflect,
# so running it natively is sufficient coverage.
if: matrix.goarch == 'arm64'
working-directory: lima-src
run: go test -run TestVZVMReflectedFields ./pkg/driver/vz/...
- name: Import signing certificate
env:
CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
CERT_PW: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
KC_PW: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
run: |
KC="$RUNNER_TEMP/build.keychain-db"
echo -n "$CERT_B64" | base64 --decode -o "$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KC_PW" "$KC"
security set-keychain-settings -lut 21600 "$KC"
security unlock-keychain -p "$KC_PW" "$KC"
security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
rm "$RUNNER_TEMP/cert.p12"
- name: Sign and notarize limactl
env:
IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
API_KEY_B64: ${{ secrets.MACOS_NOTARY_API_KEY_P8_BASE64 }}
API_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }}
API_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }}
run: |
BIN="lima-src/_output/bin/limactl"
codesign --force --timestamp --options runtime \
--entitlements vznc/vznc.entitlements \
--sign "$IDENTITY" "$BIN"
codesign --verify --strict --verbose=2 "$BIN"
KEY="$RUNNER_TEMP/notary_key.p8"
echo -n "$API_KEY_B64" | base64 --decode -o "$KEY"
ZIP="$RUNNER_TEMP/limactl-${{ matrix.uname_m }}.zip"
ditto -c -k --keepParent "$BIN" "$ZIP"
set +e
SUBMIT_OUT=$(xcrun notarytool submit "$ZIP" \
--key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" \
--wait --timeout 30m 2>&1)
SUBMIT_RC=$?
echo "$SUBMIT_OUT"
SUBMIT_ID=$(printf '%s\n' "$SUBMIT_OUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
if [ "$SUBMIT_RC" -ne 0 ] || ! printf '%s\n' "$SUBMIT_OUT" | grep -q 'status: Accepted'; then
if [ -n "$SUBMIT_ID" ]; then
echo "::group::Notary log for $SUBMIT_ID"
xcrun notarytool log "$SUBMIT_ID" \
--key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" || true
echo "::endgroup::"
fi
rm -f "$ZIP" "$KEY"
exit 1
fi
# Single-file binaries can't be stapled; Gatekeeper checks online.
rm "$ZIP" "$KEY"
- name: Build tarball
working-directory: lima-src
run: |
mkdir -p _artifacts
OUT="_artifacts/lima-${{ steps.pin.outputs.version }}-Darwin-${{ matrix.uname_m }}.tar.gz"
tar -C _output --no-xattrs -czvf "$OUT" ./
- name: Smoke-test the binary
# Native arch only — confirms codesigning didn't break the
# binary and the entitlement load doesn't reject the helper.
if: matrix.goarch == 'arm64'
run: lima-src/_output/bin/limactl --version
- name: Upload tarball
uses: actions/upload-artifact@v7
with:
name: lima-darwin-${{ matrix.uname_m }}
path: lima-src/_artifacts/lima-${{ steps.pin.outputs.version }}-Darwin-${{ matrix.uname_m }}.tar.gz
build-macos-app:
name: Build macOS .app
needs: build-sdist
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Build review app frontend
working-directory: web/review-app
run: npm ci && npm run build
- name: Build terminal app frontend
working-directory: web/terminal-app
run: npm ci && npm run build
- name: Build markdown preview app frontend
working-directory: web/markdown-app
run: npm ci && npm run build
- name: Build VNC app frontend
working-directory: web/vnc-app
run: npm ci && npm run build
- name: Build config app frontend
working-directory: web/config-app
run: npm ci && npm run build
- name: Install dependencies
run: |
uv pip install --system py2app setuptools
uv pip install --system '.[macos]'
- name: Build .app bundle
run: |
# ruamel is a PEP 420 namespace package (no __init__.py), which
# py2app's imp.find_module cannot resolve. Add a stub so py2app
# can locate the package.
touch "$(python -c 'import ruamel; print(ruamel.__path__[0])')/__init__.py"
mv pyproject.toml pyproject.toml.bak
python setup_app.py py2app
mv pyproject.toml.bak pyproject.toml
- name: Import signing certificate
env:
CERT_B64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
CERT_PW: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
KC_PW: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
run: |
KC="$RUNNER_TEMP/build.keychain-db"
echo -n "$CERT_B64" | base64 --decode -o "$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KC_PW" "$KC"
security set-keychain-settings -lut 21600 "$KC"
security unlock-keychain -p "$KC_PW" "$KC"
security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
rm "$RUNNER_TEMP/cert.p12"
- name: Sign .app bundle
env:
IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
run: |
set -e
APP="dist/OpenShrimp.app"
# Sign every Mach-O file inside the bundle. Extension-based matching
# isn't enough for py2app (the embedded Python interpreter has no
# suffix), so probe each file with `file`.
find "$APP" -type f | while read -r f; do
if file -b "$f" | grep -qE 'Mach-O|dynamically linked shared library'; then
codesign --force --timestamp --options runtime \
--sign "$IDENTITY" "$f"
fi
done
# Sign embedded bundles (frameworks, nested .app) deepest-first so
# each enclosing CodeResources seal covers already-signed contents.
find "$APP/Contents" -type d \( -name "*.framework" -o -name "*.app" \) | \
awk -F/ '{ print NF, $0 }' | sort -k1 -rn | cut -d' ' -f2- | \
while read -r b; do
codesign --force --timestamp --options runtime \
--sign "$IDENTITY" "$b"
done
# Finally sign the outer bundle with entitlements.
codesign --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "$IDENTITY" "$APP"
codesign --verify --strict --deep --verbose=2 "$APP"
- name: Create DMG
run: |
brew install create-dmg
create-dmg \
--volname "OpenShrimp" \
--window-size 600 400 \
--icon "OpenShrimp.app" 150 200 \
--app-drop-link 450 200 \
--no-internet-enable \
"dist/OpenShrimp-$(cat VERSION).dmg" \
"dist/OpenShrimp.app"
- name: Sign, notarize, and staple DMG
env:
IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
API_KEY_B64: ${{ secrets.MACOS_NOTARY_API_KEY_P8_BASE64 }}
API_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }}
API_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }}
run: |
set -e
DMG="dist/OpenShrimp-$(cat VERSION).dmg"
codesign --force --timestamp --sign "$IDENTITY" "$DMG"
KEY="$RUNNER_TEMP/notary_key.p8"
echo -n "$API_KEY_B64" | base64 --decode -o "$KEY"
set +e
SUBMIT_OUT=$(xcrun notarytool submit "$DMG" \
--key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" \
--wait --timeout 30m 2>&1)
SUBMIT_RC=$?
echo "$SUBMIT_OUT"
SUBMIT_ID=$(printf '%s\n' "$SUBMIT_OUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
if [ "$SUBMIT_RC" -ne 0 ] || ! printf '%s\n' "$SUBMIT_OUT" | grep -q 'status: Accepted'; then
if [ -n "$SUBMIT_ID" ]; then
echo "::group::Notary log for $SUBMIT_ID"
xcrun notarytool log "$SUBMIT_ID" \
--key "$KEY" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" || true
echo "::endgroup::"
fi
rm -f "$KEY"
exit 1
fi
rm "$KEY"
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
- name: Upload DMG
uses: actions/upload-artifact@v7
with:
name: OpenShrimp-macos-app
path: dist/OpenShrimp-*.dmg
release:
name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-binary, build-virtiofsd, build-limactl, build-macos-app]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
merge-multiple: true
- name: Create release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
artifacts/openshrimp-*
artifacts/moonshine-stt-*
artifacts/virtiofsd-*
artifacts/OpenShrimp-*.dmg
artifacts/*.tar.gz