Skip to content

Release

Release #83

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 PDF app frontend
working-directory: web/pdf-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 }}
# Every step below is a POSIX shell script (realpath, $GITHUB_OUTPUT
# appends, `if [ ... ]`); pwsh is the Windows runner's default, so pin
# bash for all of them — git-bash ships on the Windows image.
defaults:
run:
shell: bash
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
# The `hcs` extra is what makes the Windows build worth shipping:
# win32more, the binding the HCS sandbox backend drives Windows'
# Host Compute Service through. It is Windows-only, so it appears
# on no other entry.
- os: windows-latest
artifact: openshrimp-windows-x86_64.exe
sdist_artifact: sdist-openshrimp
exec_code: "from open_shrimp.main import main; main()"
features: "hcs"
# 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: |
FILE=$(realpath dist/*.tar.gz)
# PyApp is a native program: on Windows it cannot open the
# `/d/a/...` path git-bash's realpath hands back.
if [ "$RUNNER_OS" = "Windows" ]; then FILE=$(cygpath -w "$FILE"); fi
echo "filename=$FILE" >> "$GITHUB_OUTPUT"
- name: Build with PyApp
env:
PYAPP_PROJECT_PATH: ${{ steps.sdist.outputs.filename }}
PYAPP_PROJECT_FEATURES: ${{ matrix.features || '' }}
PYAPP_PYTHON_VERSION: "3.12"
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: |
SRC=dist/pyapp/bin/pyapp
if [ -f "$SRC.exe" ]; then SRC="$SRC.exe"; fi
mv "$SRC" "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:
- uses: actions/checkout@v6
- 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: Apply OpenShrimp virtiofsd patch
working-directory: virtiofsd
run: git apply ../patches/virtiofsd-mountinfo-unescape.patch
- name: Test OpenShrimp virtiofsd patch
working-directory: virtiofsd
run: cargo test mountinfo
- 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-security-key-helper:
name: Build security-key VM helper
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: stable
- name: Build helper
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-trimpath \
-ldflags "-s -w" \
-o openshrimp-security-key-vm-helper-linux-x86_64 \
./src/open_shrimp/security_key/vm_helper_go/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
-trimpath \
-ldflags "-s -w" \
-o openshrimp-security-key-vm-helper-linux-aarch64 \
./src/open_shrimp/security_key/vm_helper_go/main.go
- name: Upload binary
uses: actions/upload-artifact@v7
with:
name: security-key-vm-helper
path: openshrimp-security-key-vm-helper-linux-*
build-hcs-initrd:
# The control initramfs the HCS sandbox backend boots its guests with:
# busybox plus the static vsock control agent. Its build is Linux work
# (gcc -static, mknod, cpio) even though a Windows host consumes it, so
# the operator downloads this asset instead of standing up a WSL build.
name: Build HCS control initramfs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build initramfs
run: |
sudo PUBLISH=1 bash scripts/build_hcs_initrd.sh \
"$PWD/openshrimp-hcs-initrd.img"
- name: Upload initramfs
uses: actions/upload-artifact@v7
with:
name: hcs-initrd
path: |
openshrimp-hcs-initrd.img
openshrimp-hcs-initrd.img.sha256
build-hcs-base-rootfs:
# The guest userland the HCS sandbox backend chroots into: an ext4 volume
# labelled `clauderoot`, in VHDX form, which is what a context's
# `base_image` points at. debootstrap, loop mounts and chroot are Linux
# work even though a Windows host consumes the result, so the operator
# downloads this asset instead of standing up a root shell in WSL to bake
# one by hand.
name: Build HCS base rootfs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build base rootfs
# The ext4 image, the VHDX converted from it and the copy at the
# output path coexist, which the runner's guaranteed free space
# absorbs. Both paths are on the OS disk: the /mnt temp disk is the
# smaller of the two and cannot hold the build.
run: |
sudo PUBLISH=1 WORK="$RUNNER_TEMP/hcs-base" \
bash scripts/build_hcs_base_rootfs.sh \
"$PWD/openshrimp-hcs-base-rootfs.vhdx"
- name: Check the image fits a release asset
# A release rejects any file of 2 GiB or more. Failing here names the
# image that outgrew the limit; failing at upload time would not.
run: |
asset=openshrimp-hcs-base-rootfs.vhdx.zst
size=$(stat -c %s "$asset")
echo "$asset: $((size / 1024 / 1024)) MiB"
if [ "$size" -ge 2147483648 ]; then
echo "::error::$asset is 2 GiB or larger; a release will reject it"
exit 1
fi
- name: Upload base rootfs
# Only the compressed form travels. The raw VHDX is several times
# larger, and both the release asset and the next job take the .zst.
uses: actions/upload-artifact@v7
with:
name: hcs-base-rootfs
# The artifact only has to outlive the jobs that consume it — the
# computer-use build and the release upload, both minutes later.
# The published release asset is the copy that is kept.
retention-days: 7
path: |
openshrimp-hcs-base-rootfs.vhdx.zst
openshrimp-hcs-base-rootfs.vhdx.zst.sha256
build-hcs-gui-rootfs:
# The computer-use variant of the guest rootfs: the base image plus weston
# on its RDP backend, the AF_VSOCK relay and a browser. A context with
# computer use enabled boots this in place of the base image, so the two
# ship together. It builds in its own job rather than after the base in
# one: the images are large enough that each needs a whole runner disk.
name: Build HCS computer-use rootfs
runs-on: ubuntu-latest
needs: build-hcs-base-rootfs
steps:
- uses: actions/checkout@v6
- name: Reclaim runner disk
# The base image, the grown ext4 image holding the desktop stack, the
# VHDX converted from it and the copy at the output path are all live
# at once — several times what the base rootfs build needs, and close
# enough to the free space a runner guarantees to be worth the margin.
# None of the preinstalled language toolchains are used here.
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc \
/usr/local/.ghcup /opt/hostedtoolcache
df -h /
- name: Download base rootfs
uses: actions/download-artifact@v8
with:
name: hcs-base-rootfs
- name: Unpack base rootfs
# The GUI build bakes into a copy of the base image, so it consumes
# exactly the artifact published for operators; verifying the checksum
# here proves the published asset is the one that was built against.
run: |
sha256sum -c openshrimp-hcs-base-rootfs.vhdx.zst.sha256
zstd -d --rm openshrimp-hcs-base-rootfs.vhdx.zst
rm -f openshrimp-hcs-base-rootfs.vhdx.zst.sha256
- name: Build computer-use rootfs
run: |
sudo PUBLISH=1 WORK="$RUNNER_TEMP/hcs-gui" \
bash scripts/build_hcs_gui_rootfs.sh \
"$PWD/openshrimp-hcs-base-rootfs.vhdx" \
"$PWD/openshrimp-hcs-gui-rootfs.vhdx"
rm -f openshrimp-hcs-base-rootfs.vhdx
- name: Check the image fits a release asset
# The desktop stack makes this the largest asset the project ships;
# a release rejects any file of 2 GiB or more.
run: |
asset=openshrimp-hcs-gui-rootfs.vhdx.zst
size=$(stat -c %s "$asset")
echo "$asset: $((size / 1024 / 1024)) MiB"
if [ "$size" -ge 2147483648 ]; then
echo "::error::$asset is 2 GiB or larger; a release will reject it"
exit 1
fi
- name: Upload computer-use rootfs
uses: actions/upload-artifact@v7
with:
name: hcs-gui-rootfs
# Superseded by the published release asset once the release job
# has collected it.
retention-days: 7
path: |
openshrimp-hcs-gui-rootfs.vhdx.zst
openshrimp-hcs-gui-rootfs.vhdx.zst.sha256
build-hcs-rdp-helper:
# The RDP helper for HCS computer use, shipped with every FreeRDP DLL it
# loads. Building it here is what makes `mingw_bin` optional: an operator
# without an MSYS2 toolchain gets this bundle, and the runtime falls back
# to compiling from source only for a source install.
name: Build HCS RDP helper
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v6
- uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
mingw-w64-x86_64-gcc
mingw-w64-x86_64-freerdp
mingw-w64-x86_64-pkgconf
zip
- name: Build helper
# Same compile the runtime fallback performs (sandbox/hcs_rdp.py);
# keep the flags in step with it.
run: |
mkdir -p bundle
gcc src/open_shrimp/sandbox/hcs_rdp_helper.c \
-o bundle/hcs_rdp_helper.exe -D__STDC_NO_THREADS__ -O2 \
$(pkgconf --cflags --libs freerdp-client3 freerdp3 winpr3) \
-lws2_32
- name: Bundle the FreeRDP DLLs
# ldd resolves the PE's imports transitively, so one pass collects the
# whole closure. Only the /mingw64 ones travel; the rest are Windows'
# own and are already on the target machine.
run: |
cd bundle
ldd hcs_rdp_helper.exe | awk '/mingw64/ {print $3}' | sort -u | \
while read -r dll; do cp -n "$dll" .; done
ls -l
- name: Verify the bundle needs no toolchain
shell: pwsh
# The whole point of the bundle is that the DLLs beside the exe are
# enough, so run it with MSYS2 off PATH. With a DLL missing the
# loader kills the process (exit -1073741515) before it prints
# anything; with the bundle complete, argc < 3 prints usage and
# exits 2.
run: |
$env:PATH = "$env:SystemRoot\system32;$env:SystemRoot"
$out = & .\bundle\hcs_rdp_helper.exe 2>&1 | Out-String
Write-Host $out
if ($LASTEXITCODE -ne 2 -or $out -notmatch 'usage:') {
throw "RDP helper bundle is not self-contained (exit $LASTEXITCODE)"
}
- name: Package the bundle
# The runtime unpacks this archive whole and expects the exe at its
# top level, next to the DLLs (sandbox/hcs_rdp.py: HELPER_ASSET).
run: |
cd bundle
zip -r ../openshrimp-hcs-rdp-helper-windows-x86_64.zip .
- name: Upload bundle
uses: actions/upload-artifact@v7
with:
name: hcs-rdp-helper
path: openshrimp-hcs-rdp-helper-windows-x86_64.zip
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.12"
- 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 PDF app frontend
working-directory: web/pdf-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-security-key-helper
- build-hcs-initrd
- build-hcs-base-rootfs
- build-hcs-gui-rootfs
- build-hcs-rdp-helper
- 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/openshrimp-security-key-vm-helper-*
artifacts/openshrimp-hcs-initrd.img*
artifacts/openshrimp-hcs-base-rootfs.vhdx.zst*
artifacts/openshrimp-hcs-gui-rootfs.vhdx.zst*
artifacts/openshrimp-hcs-rdp-helper-*
artifacts/virtiofsd-*
artifacts/OpenShrimp-*.dmg
artifacts/*.tar.gz