openpencil/.github/workflows/rust-release.yml
2026-06-20 19:24:59 +08:00

379 lines
18 KiB
YAML

name: Rust release artifacts
# Triggered on tag push (v*) — builds release binaries across desktop targets,
# packages real installers per platform, and assembles a GitHub Release draft.
#
# Artifact matrix (per platform; <v> = tag version, arch token = x64/arm64 to
# match electron-builder's `${productName}-${version}-${arch}-…` convention
# from apps/desktop/electron-builder.yml):
# macOS OpenPencil-<v>-<arch>-mac.dmg .app via scripts/bundle-macos.sh
# (.op/.pen/.fig associations in
# Info.plist, op CLI embedded in
# Contents/MacOS, ad-hoc signed)
# openpencil-desktop-<label>.tar.gz raw binary (kept)
# op-cli-<label>.tar.gz standalone CLI
# Windows OpenPencil-<v>-<arch>-win-setup.exe NSIS installer (Start-Menu +
# desktop shortcuts, HKCR
# .op/.pen ProgID, uninstaller,
# bundles op.exe)
# openpencil-desktop-<label>.zip portable (kept)
# op-cli-<label>.zip standalone CLI
# Linux OpenPencil-<v>-<arch>-linux.AppImage both arches (appimagetool
# 1.9.1 ships native x86_64 AND
# aarch64 builds)
# OpenPencil-<v>-<arch>-linux.deb .desktop + shared-mime-info
# registration of .op/.pen,
# bundles usr/bin/op
# openpencil-desktop-<label>.tar.gz raw binary (kept)
# op-cli-<label>.tar.gz standalone CLI
#
# Branding: productName "OpenPencil" everywhere (parity with
# electron-builder.yml). The macOS bundle id stays com.zseven-w.openpencil
# (the Rust shell identity already registered with LaunchServices via
# scripts/bundle-macos.sh + op-host-desktop bundle metadata) — intentionally
# NOT electron's dev.openpencil.app, so the Electron and Rust apps can
# coexist without fighting over file-association ownership.
#
# ─── Local-verification caveats (2026-06-10, macOS dev machine) ────────────
# Verified locally:
# * bash -n on scripts/bundle-macos.sh, package-deb.sh, package-appimage.sh,
# package-linux-common.sh
# * YAML parse of this file
# * package-deb.sh --layout-only and package-appimage.sh layout dry-runs
# with stub binaries (payload tree, control file, desktop entry, mime XML)
# * appimagetool 1.9.1 pinned release assets (x86_64 + aarch64) exist
# (checked via GitHub API); cargo-bundle `--target` flag confirmed
# against upstream source (present since v0.9.0)
# NOT verifiable locally — needs a real tag-push CI run:
# * makensis compile of scripts/package-windows.nsi (no makensis on macOS;
# NSIS is preinstalled on windows-latest per the runner-images manifest)
# * actual AppImage assembly (appimagetool is a Linux ELF; it also
# downloads its type2-runtime from GitHub at pack time — needs network)
# * dpkg-deb --build (no dpkg locally; only the layout path was dry-run)
# * cargo-bundle bootstrap + `--target` bundling + hdiutil DMG creation on
# the GitHub macOS runner
# * installer smoke tests (NSIS install/uninstall + HKCR assoc, deb
# postinst mime refresh, AppImage launch, DMG mount + Gatekeeper)
#
# The `build` job builds the real `op-host-desktop` crate (winit + skia-safe
# runner that drives the Rust editor); its shipped executable keeps the
# stable `openpencil-desktop` name. The `op` CLI (crates/op-cli, binary name
# `op`) is built in the same cargo invocation. The Rust web bundle is built by
# `.github/workflows/wasm-bundle-build.yml` as a separate deployable CanvasKit
# artifact; this workflow currently keeps the native desktop release matrix
# independent from that web artifact.
on:
push:
tags: ['v*']
workflow_dispatch:
jobs:
build:
name: ${{ matrix.label }}
runs-on: ${{ matrix.runner }}
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
env:
HAS_APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_P12 != '' && 'true' || 'false' }}
HAS_WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERT_BASE64 != '' && 'true' || 'false' }}
strategy:
fail-fast: false
matrix:
include:
- label: macos-aarch64
runner: macos-latest
target: aarch64-apple-darwin
archive: tar.gz
arch: arm64
# macos-13 (Intel) deprecated; cross-compile x86_64-apple-darwin
# from Apple Silicon (cargo supports cross-compile to host's other
# arch out of the box, no `cross` needed).
- label: macos-x86_64
runner: macos-latest
target: x86_64-apple-darwin
archive: tar.gz
arch: x64
- label: linux-x86_64
runner: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive: tar.gz
arch: x64
# Native ARM64 hosted runner — no `cross`. The cross 0.2.x aarch64
# image (Ubuntu 16.04 / GCC 5 / FreeType 2.6) is too old to link the
# modern skia that skia-bindings builds.
- label: linux-aarch64
runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
archive: tar.gz
arch: arm64
- label: windows-x86_64
runner: windows-latest
target: x86_64-pc-windows-msvc
archive: zip
arch: x64
# Windows ARM64 — cargo cross-compile from x86_64 windows runner.
# The NSIS installer stub stays x86 (runs under emulation on
# Windows-on-ARM); the installed payload is native aarch64.
- label: windows-aarch64
runner: windows-latest
target: aarch64-pc-windows-msvc
archive: zip
arch: arm64
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: '1.94'
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- name: Install Linux GL/EGL prereqs
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-dev libxkbcommon-x11-dev \
libwayland-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libegl1-mesa-dev libgles2-mesa-dev libgbm-dev \
libfreetype-dev libfontconfig1-dev
- name: Compute release version
shell: bash
run: |
# Tag pushes (vX.Y.Z) drive the artifact version; workflow_dispatch
# falls back to [workspace.package].version in the root Cargo.toml.
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "OP_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
else
ver="$(awk -F'"' '/^\[workspace\.package\]/{f=1} f && /^version/{print $2; exit}' Cargo.toml)"
echo "OP_VERSION=${ver:-0.0.0}" >> "$GITHUB_ENV"
fi
- name: Build (host)
run: cargo build -p op-host-desktop -p op-cli --target ${{ matrix.target }} --release
- name: Package archives (unix)
if: matrix.archive == 'tar.gz'
shell: bash
run: |
cd target/${{ matrix.target }}/release
tar czf ../../../openpencil-desktop-${{ matrix.label }}.tar.gz openpencil-desktop
tar czf ../../../op-cli-${{ matrix.label }}.tar.gz op
- name: Package archives (windows)
if: matrix.archive == 'zip'
shell: pwsh
run: |
Compress-Archive `
-Path target\${{ matrix.target }}\release\openpencil-desktop.exe `
-DestinationPath openpencil-desktop-${{ matrix.label }}.zip
Compress-Archive `
-Path target\${{ matrix.target }}\release\op.exe `
-DestinationPath op-cli-${{ matrix.label }}.zip
- name: Package DMG (macos)
if: runner.os == 'macOS'
shell: bash
env:
OPENPENCIL_VERSION: ${{ env.OP_VERSION }}
OPENPENCIL_TARGET: ${{ matrix.target }}
# Hand the already-built binaries to the bundle script: it skips its
# own cargo build and overwrites the bundled executable with exactly
# what this workflow built (and embeds the op CLI in Contents/MacOS).
OPENPENCIL_BINARY: ${{ github.workspace }}/target/${{ matrix.target }}/release/openpencil-desktop
OPENPENCIL_CLI_BINARY: ${{ github.workspace }}/target/${{ matrix.target }}/release/op
# Optional real-signing hook. No signing secrets exist today, so
# bundle-macos.sh falls back to ad-hoc ("-") signing. When a
# Developer ID cert lands, uncomment and add keychain import +
# notarization steps:
# MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
run: |
bash scripts/bundle-macos.sh
APP="target/${{ matrix.target }}/release/bundle/osx/OpenPencil.app"
STAGE="$(mktemp -d)"
cp -R "$APP" "$STAGE/OpenPencil.app"
ln -s /Applications "$STAGE/Applications"
# HFS+ instead of the APFS default: mountable on every macOS the
# binary itself supports.
hdiutil create \
-volname "OpenPencil $OP_VERSION" \
-srcfolder "$STAGE" \
-fs HFS+ -format UDZO -ov \
"OpenPencil-$OP_VERSION-${{ matrix.arch }}-mac.dmg"
# ─── Code-signing scaffolding (NO-OP until secrets exist) ───────────
# Until the secrets below are configured in the repo/org settings,
# EVERY signing step is skipped and releases ship ad-hoc-signed
# (macOS, via scripts/bundle-macos.sh) / unsigned (Windows) — the
# current behavior, unchanged. Each step is gated on its first
# required secret being non-empty, so adding the secrets is the only
# action needed to turn signing on.
#
# Required macOS secrets (Developer ID + notarization):
# APPLE_CERTIFICATE_P12 base64 of the Developer ID Application
# .p12 export
# APPLE_CERTIFICATE_PASSWORD password for that .p12
# APPLE_TEAM_ID 10-char Apple Developer Team ID
# APPLE_ID Apple ID email used for notarization
# APPLE_APP_PASSWORD app-specific password for notarytool
#
# Required Windows secrets (Authenticode):
# WINDOWS_CERT_BASE64 base64 of the code-signing .pfx
# WINDOWS_CERT_PASSWORD password for that .pfx
- name: Import codesign certificate (macos)
if: runner.os == 'macOS' && env.HAS_APPLE_CERTIFICATE == 'true'
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Codesign + notarize DMG (macos)
if: runner.os == 'macOS' && env.HAS_APPLE_CERTIFICATE == 'true'
shell: bash
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
DMG="OpenPencil-$OP_VERSION-${{ matrix.arch }}-mac.dmg"
# The .app inside the DMG was ad-hoc signed by bundle-macos.sh;
# re-sign the DMG container with the real Developer ID identity,
# then notarize + staple so Gatekeeper accepts the download.
IDENTITY="Developer ID Application ($APPLE_TEAM_ID)"
codesign --force --timestamp --options runtime \
--sign "$IDENTITY" "$DMG"
xcrun notarytool submit "$DMG" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait
xcrun stapler staple "$DMG"
- name: Package NSIS installer (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
# makensis is preinstalled on windows-latest (runner-images
# manifest). Defines are absolute because NSIS resolves relative
# paths against the .nsi's own directory (scripts/).
makensis `
"/DVERSION=$env:OP_VERSION" `
"/DARCH=${{ matrix.arch }}" `
"/DBIN_DIR=$env:GITHUB_WORKSPACE\target\${{ matrix.target }}\release" `
"/DICON_FILE=$env:GITHUB_WORKSPACE\apps\desktop\build\icon.ico" `
"/DOUT_FILE=$env:GITHUB_WORKSPACE\OpenPencil-$env:OP_VERSION-${{ matrix.arch }}-win-setup.exe" `
scripts\package-windows.nsi
# Authenticode signing scaffolding (NO-OP until WINDOWS_CERT_BASE64
# exists — see the secrets comment block in the macOS section above).
# Until then the NSIS installer ships unsigned (current behavior).
- name: Sign NSIS installer (windows)
if: runner.os == 'Windows' && env.HAS_WINDOWS_CERTIFICATE == 'true'
shell: pwsh
env:
WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: |
# Materialize the .pfx from the base64 secret into a temp file.
$pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_BASE64))
$exe = "OpenPencil-$env:OP_VERSION-${{ matrix.arch }}-win-setup.exe"
# signtool ships with the Windows SDK preinstalled on
# windows-latest. RFC-3161 timestamp so signatures outlive the
# cert's validity window.
$signtool = (Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object FullName -Descending | Select-Object -First 1).FullName
& $signtool sign `
/f $pfx `
/p $env:WINDOWS_CERT_PASSWORD `
/fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
$exe
Remove-Item $pfx -Force
- name: Package .deb (linux)
if: runner.os == 'Linux'
shell: bash
run: |
case "${{ matrix.target }}" in
x86_64-*) DEB_ARCH=amd64 ;;
aarch64-*) DEB_ARCH=arm64 ;;
*) echo "unmapped deb arch for ${{ matrix.target }}" >&2; exit 1 ;;
esac
bash scripts/package-deb.sh \
--desktop-bin "target/${{ matrix.target }}/release/openpencil-desktop" \
--cli-bin "target/${{ matrix.target }}/release/op" \
--icon apps/desktop/build/icon.png \
--version "$OP_VERSION" \
--arch "$DEB_ARCH" \
--out-dir .
- name: Package AppImage (linux)
if: runner.os == 'Linux'
shell: bash
run: |
case "${{ matrix.target }}" in
x86_64-*) AI_ARCH=x86_64 ;;
aarch64-*) AI_ARCH=aarch64 ;;
*) echo "unmapped AppImage arch for ${{ matrix.target }}" >&2; exit 1 ;;
esac
# appimagetool pinned by URL (release 1.9.1 ships native builds for
# both runner arches, so each matrix leg packs natively). NOTE:
# appimagetool 1.9.x downloads the matching static type2-runtime
# from GitHub at pack time — the runner needs network access.
curl -fsSL --retry 3 -o /tmp/appimagetool \
"https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${AI_ARCH}.AppImage"
chmod +x /tmp/appimagetool
bash scripts/package-appimage.sh \
--desktop-bin "target/${{ matrix.target }}/release/openpencil-desktop" \
--cli-bin "target/${{ matrix.target }}/release/op" \
--icon apps/desktop/build/icon.png \
--version "$OP_VERSION" \
--arch "$AI_ARCH" \
--tool /tmp/appimagetool \
--out-dir .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: openpencil-desktop-${{ matrix.label }}
path: |
openpencil-desktop-${{ matrix.label }}.tar.gz
openpencil-desktop-${{ matrix.label }}.zip
op-cli-${{ matrix.label }}.tar.gz
op-cli-${{ matrix.label }}.zip
OpenPencil-*-mac.dmg
OpenPencil-*-win-setup.exe
OpenPencil-*-linux.AppImage
OpenPencil-*-linux.deb
if-no-files-found: ignore
# Web bundle release note:
# The deployable Rust web bundle is produced by
# `.github/workflows/wasm-bundle-build.yml`, which builds the current
# CanvasKit feature set, runs `tools/check-wasm-bundle.sh`, size-gates the
# wasm output, and uploads the `op-web-bundle` artifact. This desktop release
# workflow still ships only native desktop + CLI artifacts; attach or merge the
# `op-web-bundle` artifact here only when tagged releases are meant to publish
# the standalone web bundle from the same draft.
release-draft:
name: Create / update GitHub Release draft
# Native release artifacts only. The deployable Rust web bundle is built by
# wasm-bundle-build.yml and can be attached separately when release policy
# calls for publishing the web bundle from this draft.
needs: [build]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: dist
- name: Flatten artifacts
run: |
mkdir -p release-files
find dist -type f \
\( -name "*.tar.gz" -o -name "*.zip" -o -name "*.dmg" \
-o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" \) \
-exec cp {} release-files/ \;
ls -la release-files
- name: Create / update GitHub Release
uses: softprops/action-gh-release@v2
with:
draft: true
files: release-files/*
generate_release_notes: true