openpencil/.github/workflows/rust-release.yml
2026-08-10 16:43:00 +08:00

1327 lines
58 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: Rust release artifacts
# Triggered on tag push (v*) — builds release binaries across desktop targets,
# packages real installers per platform, builds the web Docker image and SDK
# tarballs, publishes the web SDK packages to npm, publishes a GitHub
# prerelease, then updates the Homebrew tap and Scoop bucket.
#
# 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 notarized final DMG containing
# a notarized .app from
# scripts/bundle-macos.sh
# (.op/.pen/.fig associations in
# Info.plist, op CLI embedded in
# Contents/MacOS; Developer ID
# signed/notarized when secrets
# exist, ad-hoc signed locally)
# 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
# all openpencil-vscode-<v>-<t>.vsix VS Code extension, one per
# vsce target (darwin/linux/
# win32 × x64/arm64), each
# bundling the platform's
# op-host-web-server + the
# wasm/CanvasKit web assets
#
# 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;
# CI installs NSIS on windows-latest before packaging)
# * 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 web Docker job builds the
# Rust web host from `Dockerfile.web-rust`, and the SDK job packages the
# wasm-backed `packages/op-web-sdk*` workspace tarballs.
#
# The standalone `.github/workflows/wasm-bundle-build.yml` still gates the
# CanvasKit web bundle on push/PR. This release workflow only assembles the
# tagged, publishable artifacts.
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write
packages: write
pull-requests: write
jobs:
version:
name: Validate release version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Validate Cargo and tag versions
id: version
shell: bash
run: |
set -euo pipefail
cargo_version="$(scripts/workspace-version.sh)"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
tag_version="${GITHUB_REF_NAME#v}"
if [[ "$tag_version" != "$cargo_version" ]]; then
echo "::error::release tag version $tag_version does not match Cargo workspace version $cargo_version"
exit 1
fi
fi
echo "version=$cargo_version" >> "$GITHUB_OUTPUT"
build:
name: ${{ matrix.label }}
needs: version
runs-on: ${{ matrix.runner }}
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
env:
OP_VERSION: ${{ needs.version.outputs.version }}
# Production collaboration hub URLs live only in repository secrets and
# are baked in at compile time (option_env! in relay_bootstrap_select.rs);
# the source tree deliberately carries no production endpoint. A fork or
# secretless build simply produces a binary without built-in hubs.
OPENPENCIL_BUILD_COLLAB_BOOTSTRAP_URL_CN: ${{ secrets.OPENPENCIL_BUILD_COLLAB_BOOTSTRAP_URL_CN }}
OPENPENCIL_BUILD_COLLAB_BOOTSTRAP_URL_GLOBAL: ${{ secrets.OPENPENCIL_BUILD_COLLAB_BOOTSTRAP_URL_GLOBAL }}
HAS_APPLE_CERTIFICATE: ${{ secrets.CSC_LINK != '' && secrets.CSC_KEY_PASSWORD != '' && secrets.APPLE_ID != '' && secrets.APPLE_TEAM_ID != '' && secrets.APPLE_APP_SPECIFIC_PASSWORD != '' && 'true' || 'false' }}
HAS_WINDOWS_CERTIFICATE: ${{ secrets.WIN_CSC_LINK != '' && secrets.WIN_CSC_KEY_PASSWORD != '' && 'true' || 'false' }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
# ANGLE runtime DLLs (libEGL / libGLESv2 / d3dcompiler_47) shipped
# in the Windows installer so glutin's EGL fallback can drive Skia
# on machines whose native WGL OpenGL is the GDI-generic 1.1
# software renderer (no/old GPU driver, VMs, RDP) — those were the
# startup flash-exits. We don't build ANGLE from source (heavy,
# brittle); we lift the prebuilt, arch-matched DLLs out of an
# Electron release zip (BSD-licensed ANGLE + MS-redistributable
# d3dcompiler, published at the zip root for x64 + arm64).
# Bump freely; verify the tag exists at
# https://github.com/electron/electron/releases before changing.
ANGLE_ELECTRON_VERSION: v33.0.0
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'
shell: bash
run: |
set -euo pipefail
packages=(
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
)
for attempt in 1 2 3 4; do
if sudo apt-get \
-o Acquire::ForceIPv4=true \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
update && \
sudo apt-get \
-o Acquire::ForceIPv4=true \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
install --yes "${packages[@]}"; then
break
fi
if [[ "$attempt" == 4 ]]; then
echo "::error::apt prerequisites failed after $attempt attempts"
exit 1
fi
delay=$((attempt * 20))
echo "::warning::apt prerequisites attempt $attempt failed; retrying in ${delay}s"
sleep "$delay"
done
- name: Build (host)
run: cargo build -p op-host-desktop -p op-cli -p op-host-web-server --target ${{ matrix.target }} --release
- name: Package archives (unix)
if: matrix.archive == 'tar.gz'
shell: bash
run: |
cd target/${{ matrix.target }}/release
if [ "$RUNNER_OS" != "macOS" ]; then
tar czf ../../../openpencil-desktop-${{ matrix.label }}.tar.gz openpencil-desktop
fi
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: 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.CSC_LINK }}
p12-password: ${{ secrets.CSC_KEY_PASSWORD }}
- name: Resolve macOS signing identity
if: runner.os == 'macOS' && env.HAS_APPLE_CERTIFICATE == 'true'
shell: bash
run: |
set -euo pipefail
identity="$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/ {print $2; exit}')"
if [ -z "$identity" ]; then
security find-identity -v -p codesigning
echo "::error::Developer ID Application identity not found in imported keychain"
exit 1
fi
echo "MACOS_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV"
- name: Bundle macOS app
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
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
if [ "$HAS_APPLE_CERTIFICATE" = "true" ]; then
: "${MACOS_SIGN_IDENTITY:?MACOS_SIGN_IDENTITY was not resolved}"
export MACOS_SIGN_IDENTITY
fi
bash scripts/bundle-macos.sh
bash tools/check-macos-bundle-plist.sh \
"target/${{ matrix.target }}/release/bundle/osx/OpenPencil.app/Contents/Info.plist"
- name: Notarize macOS app
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_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
set -euo pipefail
APP="target/${{ matrix.target }}/release/bundle/osx/OpenPencil.app"
APP_ZIP="OpenPencil-$OP_VERSION-${{ matrix.arch }}-mac-app.zip"
ditto -c -k --keepParent "$APP" "$APP_ZIP"
notary_log="$(mktemp)"
set +e
xcrun notarytool submit "$APP_ZIP" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--wait 2>&1 | tee "$notary_log"
notary_status="${PIPESTATUS[0]}"
set -e
submission_id="$(awk '/id: / { print $2; exit }' "$notary_log")"
if [ -n "$submission_id" ]; then
xcrun notarytool log "$submission_id" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" || true
fi
if [ "$notary_status" -ne 0 ] || grep -q "status: Invalid" "$notary_log"; then
echo "::error::macOS app notarization failed"
exit 1
fi
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
- name: Package DMG (macos)
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
APP="target/${{ matrix.target }}/release/bundle/osx/OpenPencil.app"
DMG="OpenPencil-$OP_VERSION-${{ matrix.arch }}-mac.dmg"
STAGE="$(mktemp -d)"
ditto "$APP" "$STAGE/OpenPencil.app"
if [ "$HAS_APPLE_CERTIFICATE" = "true" ]; then
xcrun stapler validate "$STAGE/OpenPencil.app"
fi
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 \
"$DMG"
if [ "$HAS_APPLE_CERTIFICATE" = "true" ]; then
codesign --force --timestamp --sign "$MACOS_SIGN_IDENTITY" "$DMG"
codesign --verify --verbose "$DMG"
fi
- name: 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_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
set -euo pipefail
DMG="OpenPencil-$OP_VERSION-${{ matrix.arch }}-mac.dmg"
notary_log="$(mktemp)"
set +e
xcrun notarytool submit "$DMG" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--wait 2>&1 | tee "$notary_log"
notary_status="${PIPESTATUS[0]}"
set -e
submission_id="$(awk '/id: / { print $2; exit }' "$notary_log")"
if [ -n "$submission_id" ]; then
xcrun notarytool log "$submission_id" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" || true
fi
if [ "$notary_status" -ne 0 ] || grep -q "status: Invalid" "$notary_log"; then
echo "::error::macOS DMG notarization failed"
exit 1
fi
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
# ─── Code-signing secret reference ─────────────────────────────────
# Each platform signing path is gated on its required secrets. Without
# them, releases ship ad-hoc-signed (macOS, via scripts/bundle-macos.sh)
# / unsigned (Windows); with them, CI performs Developer ID
# signing/notarization for macOS and Authenticode signing for Windows.
#
# Required macOS secrets (Developer ID + notarization):
# CSC_LINK base64 of the Developer ID Application
# .p12 export
# CSC_KEY_PASSWORD password for that .p12
# APPLE_TEAM_ID 10-char Apple Developer Team ID
# APPLE_ID Apple ID email used for notarization
# APPLE_APP_SPECIFIC_PASSWORD app-specific password for notarytool
#
# Required Windows secrets (Authenticode):
# WIN_CSC_LINK base64/data URL/URL of the code-signing
# .pfx
# WIN_CSC_KEY_PASSWORD password for that .pfx
#
# Optional downstream publish secrets consumed by release packaging:
# NPM_TOKEN npm registry token for SDK publishing
# TAP_GITHUB_TOKEN GitHub token for tap/formula publishing
- name: Install NSIS (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
choco install nsis -y --no-progress
$nsisRoots = @(
"${env:ProgramFiles(x86)}\NSIS",
"$env:ProgramFiles\NSIS"
)
$makensis = $null
foreach ($root in $nsisRoots) {
$candidate = Join-Path $root "makensis.exe"
if (Test-Path $candidate) {
$makensis = $candidate
break
}
}
if (-not $makensis) {
throw "makensis.exe not found after NSIS install"
}
Split-Path $makensis | Out-File -FilePath $env:GITHUB_PATH -Append
& $makensis /VERSION
- name: Stage ANGLE DLLs (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
# Electron's win32 arch token matches our matrix arch (x64 / arm64).
$arch = "${{ matrix.arch }}"
$ver = "$env:ANGLE_ELECTRON_VERSION"
$url = "https://github.com/electron/electron/releases/download/$ver/electron-$ver-win32-$arch.zip"
$zip = Join-Path $env:RUNNER_TEMP "electron-$arch.zip"
$extract = Join-Path $env:RUNNER_TEMP "electron-$arch"
Write-Host "Fetching ANGLE DLLs from $url"
Invoke-WebRequest -Uri $url -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $extract -Force
$dest = "$env:GITHUB_WORKSPACE\target\${{ matrix.target }}\release"
# d3dcompiler_47.dll ships in Win10/11 System32, but bundling it
# keeps ANGLE self-contained across all environments.
$dlls = @("libEGL.dll", "libGLESv2.dll", "d3dcompiler_47.dll")
foreach ($dll in $dlls) {
$src = Join-Path $extract $dll
if (-not (Test-Path $src)) {
throw "ANGLE DLL '$dll' not found in $url — the fallback would ship broken; failing the release."
}
Copy-Item -Path $src -Destination $dest -Force
Write-Host "Staged $dll -> $dest"
}
- name: Package NSIS installer (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
# 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\crates\op-host-desktop\assets\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:
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
run: |
# Materialize the .pfx from a base64/data-url/url secret into a temp file.
$pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
$link = $env:WIN_CSC_LINK
if ($link -match '^https?://') {
Invoke-WebRequest -Uri $link -OutFile $pfx
} else {
if ($link -match '^data:.*;base64,(.+)$') {
$link = $Matches[1]
}
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($link))
}
$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:WIN_CSC_KEY_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 crates/op-host-desktop/assets/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 crates/op-host-desktop/assets/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
# Raw daemon binary for the platform vsix (the extension's bundled
# runtime). No archive extension, so release-draft's flatten step
# never picks it up as a release file on its own.
- name: Upload daemon binary for the VS Code extension
uses: actions/upload-artifact@v4
with:
name: openpencil-daemon-${{ matrix.label }}
path: |
target/${{ matrix.target }}/release/op-host-web-server
target/${{ matrix.target }}/release/op-host-web-server.exe
if-no-files-found: error
web-docker:
name: Build and push web Docker image
needs: version
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
env:
OP_VERSION: ${{ needs.version.outputs.version }}
outputs:
image: ${{ steps.meta.outputs.image }}
tag: ${{ steps.meta.outputs.tag }}
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Compute image metadata
id: meta
shell: bash
run: |
version="$OP_VERSION"
image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/openpencil-web"
tag="${image}:v${version}"
echo "image=${image}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.web-rust
push: true
tags: ${{ steps.meta.outputs.tag }}
sdk-packages:
name: Build and publish web SDK npm packages
needs: version
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
env:
OP_VERSION: ${{ needs.version.outputs.version }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: '1.94'
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
shared-key: web-sdk-release
- uses: oven-sh/setup-bun@v2
- name: Install wasm-bindgen-cli (pinned to Cargo.lock)
shell: bash
run: |
version="$(awk '/^name = "wasm-bindgen"$/{found=1; next} found && /^version = /{gsub(/[" ]/,"",$3); print $3; exit}' Cargo.lock)"
if [ -z "$version" ]; then
echo "::error::could not resolve wasm-bindgen version from Cargo.lock"
exit 1
fi
cargo install wasm-bindgen-cli --version "$version" --locked
- name: Install binaryen
run: |
BINARYEN_VERSION=version_123
curl -fsSL \
"https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \
| tar -xz
echo "${GITHUB_WORKSPACE}/binaryen-${BINARYEN_VERSION}/bin" >> "$GITHUB_PATH"
- name: Install package workspace dependencies
working-directory: packages
run: bun install --frozen-lockfile
- name: Verify package versions
working-directory: packages
run: bun run sync-version:check
- name: Configure npm auth
shell: bash
run: |
set -euo pipefail
if [ -z "${NPM_TOKEN:-}" ]; then
echo "::error::NPM_TOKEN is required to publish web SDK packages"
exit 1
fi
echo "::notice::NPM_TOKEN must be scoped for @zseven-w packages and have bypass 2FA enabled"
printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN" > ~/.npmrc
npm whoami
- name: Build wasm-backed SDK packages
shell: bash
run: |
set -euo pipefail
cd packages/op-web-sdk
bun run sync-wasm
bun run build
cd ../op-web-sdk-react
bun run build
cd ../op-web-sdk-vue
bun run build
- name: Pack npm tarballs
shell: bash
run: |
set -euo pipefail
mkdir -p sdk-packages
for pkg in op-web-sdk op-web-sdk-react op-web-sdk-vue; do
(cd "packages/${pkg}" && bun pm pack --destination "${GITHUB_WORKSPACE}/sdk-packages")
done
ls -la sdk-packages
- name: Publish npm packages
shell: bash
run: |
set -euo pipefail
version="$OP_VERSION"
publish_pkg() {
local name="$1"
local tarball="$2"
local view_output
if view_output="$(npm view "${name}@${version}" version 2>&1)"; then
echo "::notice::${name}@${version} is already published; skipping"
return
fi
if ! grep -Eq 'E404|404 Not Found|is not in this registry' <<<"$view_output"; then
printf '%s\n' "$view_output"
echo "::error::failed to check npm package ${name}@${version}"
exit 1
fi
npm publish "./$tarball" --access public --tag next
}
publish_pkg "@zseven-w/op-web-sdk" "sdk-packages/zseven-w-op-web-sdk-${version}.tgz"
publish_pkg "@zseven-w/op-web-sdk-react" "sdk-packages/zseven-w-op-web-sdk-react-${version}.tgz"
publish_pkg "@zseven-w/op-web-sdk-vue" "sdk-packages/zseven-w-op-web-sdk-vue-${version}.tgz"
- name: Upload SDK tarballs
uses: actions/upload-artifact@v4
with:
name: openpencil-web-sdk-packages
path: sdk-packages/*.tgz
if-no-files-found: error
vsix:
name: Package VS Code extension (platform vsix with bundled runtime)
needs: [version, build]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
env:
OP_VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: '1.94'
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
shared-key: web-sdk-release
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wasm-bindgen-cli (pinned to Cargo.lock)
shell: bash
run: |
version="$(awk '/^name = "wasm-bindgen"$/{found=1; next} found && /^version = /{gsub(/[" ]/,"",$3); print $3; exit}' Cargo.lock)"
if [ -z "$version" ]; then
echo "::error::could not resolve wasm-bindgen version from Cargo.lock"
exit 1
fi
cargo install wasm-bindgen-cli --version "$version" --locked
- name: Install binaryen
run: |
BINARYEN_VERSION=version_123
curl -fsSL \
"https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \
| tar -xz
echo "${GITHUB_WORKSPACE}/binaryen-${BINARYEN_VERSION}/bin" >> "$GITHUB_PATH"
- name: Build web bundle (served by the bundled daemon)
run: bash tools/check-wasm-bundle.sh
- name: Download daemon binaries
uses: actions/download-artifact@v4
with:
path: daemons
pattern: openpencil-daemon-*
- name: Install package workspace dependencies
working-directory: packages
run: bun install --frozen-lockfile
- name: Check and build extension
working-directory: packages/op-vscode
shell: bash
run: |
set -euo pipefail
bunx tsc -p tsconfig.json --noEmit
bun test src
node build.mjs
# Stamp the release version into the manifest (runtime-only — the
# extension is not in the sync-version managed set yet).
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version=process.env.OP_VERSION;fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
cp ../../LICENSE LICENSE
- name: Package platform vsix (daemon + web assets bundled)
working-directory: packages/op-vscode
shell: bash
run: |
set -euo pipefail
# build-matrix label -> vsce --target. The extension resolves
# bin/op-host-web-server[.exe] and web/{pkg,canvaskit} at runtime
# (restart-source.ts bundledDaemonPath / bundledDaemonEnv).
pairs=(
"macos-aarch64 darwin-arm64"
"macos-x86_64 darwin-x64"
"linux-x86_64 linux-x64"
"linux-aarch64 linux-arm64"
"windows-x86_64 win32-x64"
"windows-aarch64 win32-arm64"
)
for pair in "${pairs[@]}"; do
label="${pair% *}"
vs_target="${pair#* }"
exe="op-host-web-server"
case "$label" in windows-*) exe="op-host-web-server.exe";; esac
src="../../daemons/openpencil-daemon-${label}/${exe}"
if [ ! -f "$src" ]; then
echo "::error::missing daemon binary for ${label}: ${src}"
exit 1
fi
rm -rf bin web
mkdir -p bin web
cp "$src" "bin/${exe}"
chmod +x "bin/${exe}" || true
cp -R ../../crates/op-host-web/pkg web/pkg
# The .opt.wasm is a byte-identical intermediate of the served
# op_host_web_bg.wasm — shipping both doubles the vsix.
rm -f web/pkg/*.opt.wasm web/pkg/*.d.ts
cp -R ../../crates/op-host-web/assets/canvaskit web/canvaskit
bunx @vscode/vsce package --no-dependencies --allow-missing-repository \
--target "$vs_target" \
--out "openpencil-vscode-${OP_VERSION}-${vs_target}.vsix"
done
ls -la ./*.vsix
- name: Upload .vsix artifacts
uses: actions/upload-artifact@v4
with:
name: openpencil-vscode-vsix
path: packages/op-vscode/*.vsix
if-no-files-found: error
# Open VSX is the registry Cursor / VSCodium / Gitpod pull from. The
# token comes from https://open-vsx.org/user-settings/tokens (an
# account owning the `openpencil` namespace) stored as the
# OPENVSX_TOKEN repo secret — mirrors the NPM_TOKEN hard-requirement.
- name: Publish to Open VSX
working-directory: packages/op-vscode
env:
OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [ -z "${OPENVSX_TOKEN:-}" ]; then
echo "::error::OPENVSX_TOKEN is required to publish to open-vsx.org (create it at https://open-vsx.org/user-settings/tokens, add as repo secret)"
exit 1
fi
# First-run namespace claim; harmless "already exists" afterwards.
bunx ovsx create-namespace openpencil -p "$OPENVSX_TOKEN" || true
# Per-file tolerance so a partial failure is retryable: a re-run
# skips targets that already landed ("already published") instead
# of aborting the loop on them, keeps going past a real failure to
# attempt every remaining platform, and fails the job at the end
# if anything genuinely failed.
failed=0
for f in ./openpencil-vscode-"${OP_VERSION}"-*.vsix; do
if out="$(bunx ovsx publish --packagePath "$f" -p "$OPENVSX_TOKEN" 2>&1)"; then
printf '%s\n' "$out"
elif printf '%s' "$out" | grep -qiE "already (exists|published)"; then
echo "skipping ${f}: this target/version is already on open-vsx"
else
printf '%s\n' "$out"
echo "::error::open-vsx publish failed for ${f}"
failed=1
fi
done
exit "$failed"
release-draft:
name: Create / update GitHub prerelease
needs: [version, build, web-docker, sdk-packages, vsix]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
# id-token + attestations power the build-provenance step below; the
# job-level block overrides the workflow defaults, so contents/PR write
# must be restated for the release upload and Nix manifest PR.
permissions:
contents: write
pull-requests: write
id-token: write
attestations: write
env:
OP_VERSION: ${{ needs.version.outputs.version }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
path: dist
pattern: openpencil-*
- name: Flatten artifacts
shell: bash
run: |
set -euo pipefail
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" \
-o -name "*.tgz" -o -name "*.vsix" \) \
-exec cp {} release-files/ \;
ls -la release-files
shopt -s nullglob
native=(release-files/OpenPencil-* release-files/openpencil-desktop-*)
cli=(release-files/op-cli-*)
sdk=(release-files/*.tgz)
vsix=(release-files/*.vsix)
if [ "${#native[@]}" -eq 0 ]; then
echo "::error::missing native desktop artifacts"
exit 1
fi
if [ "${#cli[@]}" -eq 0 ]; then
echo "::error::missing op-cli artifacts"
exit 1
fi
for file in \
release-files/openpencil-desktop-linux-x86_64.tar.gz \
release-files/op-cli-linux-x86_64.tar.gz; do
if [ ! -f "$file" ]; then
echo "::error::missing Linux x86_64 Nix manifest asset: $file"
exit 1
fi
done
if [ "${#sdk[@]}" -ne 3 ]; then
echo "::error::expected 3 SDK tarballs, found ${#sdk[@]}"
exit 1
fi
if [ "${#vsix[@]}" -ne 6 ]; then
echo "::error::expected 6 platform VS Code extension .vsix, found ${#vsix[@]}"
exit 1
fi
- name: Generate CLI install scripts
shell: bash
run: |
set -euo pipefail
version="$OP_VERSION"
sha_file() {
sha256sum "release-files/$1" | awk '{print $1}'
}
cli_mac_arm_sha="$(sha_file "op-cli-macos-aarch64.tar.gz")"
cli_mac_x64_sha="$(sha_file "op-cli-macos-x86_64.tar.gz")"
cli_linux_arm_sha="$(sha_file "op-cli-linux-aarch64.tar.gz")"
cli_linux_x64_sha="$(sha_file "op-cli-linux-x86_64.tar.gz")"
cli_win_arm_sha="$(sha_file "op-cli-windows-aarch64.zip")"
cli_win_x64_sha="$(sha_file "op-cli-windows-x86_64.zip")"
cp scripts/install-op.sh release-files/install-op.sh
cp scripts/install-op.ps1 release-files/install-op.ps1
OP_INSTALLER_VERSION="$version" \
OP_SHA_MACOS_AARCH64="$cli_mac_arm_sha" \
OP_SHA_MACOS_X86_64="$cli_mac_x64_sha" \
OP_SHA_LINUX_AARCH64="$cli_linux_arm_sha" \
OP_SHA_LINUX_X86_64="$cli_linux_x64_sha" \
perl -0pi -e '
s/^DEFAULT_OP_VERSION=.*/DEFAULT_OP_VERSION="$ENV{OP_INSTALLER_VERSION}"/m;
s/^DEFAULT_SHA_MACOS_AARCH64=.*/DEFAULT_SHA_MACOS_AARCH64="$ENV{OP_SHA_MACOS_AARCH64}"/m;
s/^DEFAULT_SHA_MACOS_X86_64=.*/DEFAULT_SHA_MACOS_X86_64="$ENV{OP_SHA_MACOS_X86_64}"/m;
s/^DEFAULT_SHA_LINUX_AARCH64=.*/DEFAULT_SHA_LINUX_AARCH64="$ENV{OP_SHA_LINUX_AARCH64}"/m;
s/^DEFAULT_SHA_LINUX_X86_64=.*/DEFAULT_SHA_LINUX_X86_64="$ENV{OP_SHA_LINUX_X86_64}"/m;
' release-files/install-op.sh
OP_INSTALLER_VERSION="$version" \
OP_SHA_WINDOWS_AARCH64="$cli_win_arm_sha" \
OP_SHA_WINDOWS_X86_64="$cli_win_x64_sha" \
perl -0pi -e '
s/^\$DefaultOpVersion = .*/\$DefaultOpVersion = "$ENV{OP_INSTALLER_VERSION}"/m;
s/^\$DefaultShaWindowsAarch64 = .*/\$DefaultShaWindowsAarch64 = "$ENV{OP_SHA_WINDOWS_AARCH64}"/m;
s/^\$DefaultShaWindowsX86_64 = .*/\$DefaultShaWindowsX86_64 = "$ENV{OP_SHA_WINDOWS_X86_64}"/m;
' release-files/install-op.ps1
chmod +x release-files/install-op.sh
if grep -E 'DEFAULT_OP_VERSION=""|DEFAULT_SHA_(MACOS|LINUX)_[A-Z0-9_]+=""|\$Default(OpVersion|ShaWindows[A-Za-z0-9_]+) = ""' \
release-files/install-op.sh release-files/install-op.ps1; then
echo "::error::CLI install scripts still contain unstamped release metadata"
exit 1
fi
grep -q "$version" release-files/install-op.sh
grep -q "$version" release-files/install-op.ps1
# Downloaders (and antivirus false-positive triage — see issue #198)
# need a way to confirm an asset is the exact file CI produced:
# a checksum manifest plus a signed SLSA provenance attestation that
# links every asset back to this workflow run and source commit
# (verifiable via `gh attestation verify`).
- name: Generate SHA256SUMS
shell: bash
run: |
set -euo pipefail
(cd release-files && sha256sum -- * | sort -k2 > ../SHA256SUMS.txt)
mv SHA256SUMS.txt release-files/
cat release-files/SHA256SUMS.txt
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: release-files/*
- name: Prepare release body
id: body
shell: bash
run: |
set -euo pipefail
version="$OP_VERSION"
note="RELEASE_NOTES/v${version}.md"
if [ ! -f "$note" ]; then
echo "::error::missing release note: $note"
exit 1
fi
# The note's leading `# ` heading is the release title; the rest is
# the body, so the title is not repeated on the release page.
title="$(head -n 1 "$note")"
if [[ "$title" != "# "* ]]; then
echo "::error::release note must start with a '# ' title heading: $note"
exit 1
fi
echo "title=${title#\# }" >> "$GITHUB_OUTPUT"
tail -n +2 "$note" | sed '/./,$!d' > release-body.md
{
printf '\n## Web Docker Image\n\n'
printf -- '- Image: `%s`\n' '${{ needs.web-docker.outputs.tag }}'
printf -- '- Digest: `%s`\n' '${{ needs.web-docker.outputs.digest }}'
printf '\n## Verify Your Download\n\n'
printf 'Every asset is listed in `SHA256SUMS.txt` and carries a signed build-provenance attestation tying it to this repository, commit, and CI run:\n\n'
printf '```bash\ngh attestation verify <downloaded-file> --repo %s\n```\n\n' '${{ github.repository }}'
printf 'Some antivirus engines heuristically flag low-prevalence installers — see [docs/security/antivirus-false-positives.md](https://github.com/%s/blob/main/docs/security/antivirus-false-positives.md).\n' '${{ github.repository }}'
} >> release-body.md
- name: Create / update GitHub prerelease
uses: softprops/action-gh-release@v2
with:
draft: false
prerelease: true
make_latest: false
name: ${{ steps.body.outputs.title }}
body_path: release-body.md
files: release-files/*
- name: Prepare latest published Nix release manifest
id: nix-manifest
shell: bash
run: |
set -euo pipefail
version="$OP_VERSION"
sri_hash() {
printf 'sha256-%s' "$(openssl dgst -sha256 -binary "release-files/$1" | base64 -w0)"
}
manifest="$(jq -n \
--arg version "$version" \
--arg desktopHash "$(sri_hash openpencil-desktop-linux-x86_64.tar.gz)" \
--arg cliHash "$(sri_hash op-cli-linux-x86_64.tar.gz)" \
'{version: $version, desktopHash: $desktopHash, cliHash: $cliHash}')"
# Tags do not retain their source branch. Prefer the matching release
# branch when it contains the tagged commit, then fall back to the
# repository's default branch.
release_branch="v${version}"
default_branch="${{ github.event.repository.default_branch }}"
git fetch --prune origin \
"+refs/heads/*:refs/remotes/origin/*"
if git show-ref --verify --quiet "refs/remotes/origin/${release_branch}" \
&& git merge-base --is-ancestor "$GITHUB_SHA" "origin/${release_branch}"; then
target_branch="$release_branch"
elif git merge-base --is-ancestor "$GITHUB_SHA" "origin/${default_branch}"; then
target_branch="$default_branch"
else
echo "::error::tagged commit is not contained in ${release_branch} or ${default_branch}"
exit 1
fi
git switch --force-create "$target_branch" "origin/${target_branch}"
# Release branches share their name with the release tag. Remove the
# runner-local tag so create-pull-request's `symbolic-ref --short`
# resolves vX.Y.Z instead of heads/vX.Y.Z (and then tries the
# nonexistent origin/heads/vX.Y.Z remote-tracking branch).
git update-ref -d "refs/tags/${target_branch}"
printf '%s\n' "$manifest" > nix/release-manifest.json
echo "target-branch=$target_branch" >> "$GITHUB_OUTPUT"
# The release branches are protected, so publish the generated hash
# update through a reviewable PR instead of trying to push as the
# GitHub Actions bot. Re-running a release updates the same PR branch.
- name: Open Nix release manifest update
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
base: ${{ steps.nix-manifest.outputs.target-branch }}
branch: automation/nix-release-manifest-v${{ env.OP_VERSION }}
delete-branch: true
add-paths: nix/release-manifest.json
commit-message: "chore(release): update Nix release manifest for v${{ env.OP_VERSION }}"
title: "chore(release): update Nix release manifest for v${{ env.OP_VERSION }}"
body: |
Updates the pinned Nix prebuilt release metadata after publishing v${{ env.OP_VERSION }}.
This PR was generated by the Rust release workflow.
package-managers:
name: Update Homebrew tap and Scoop bucket
needs: [version, release-draft]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
env:
OP_VERSION: ${{ needs.version.outputs.version }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
HOMEBREW_TAP_REPOSITORY: ${{ vars.HOMEBREW_TAP_REPOSITORY || 'ZSeven-W/homebrew-openpencil' }}
SCOOP_BUCKET_REPOSITORY: ${{ vars.SCOOP_BUCKET_REPOSITORY || 'ZSeven-W/scoop-openpencil' }}
steps:
- name: Validate package-manager token
shell: bash
run: |
set -euo pipefail
if [ -z "${TAP_GITHUB_TOKEN:-}" ]; then
echo "::error::TAP_GITHUB_TOKEN is required to update Homebrew and Scoop repositories"
exit 1
fi
- name: Download release assets and compute hashes
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
version="$OP_VERSION"
mkdir -p release-assets
gh release download "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--dir release-assets \
--clobber
required=(
"OpenPencil-${version}-arm64-mac.dmg"
"OpenPencil-${version}-x64-mac.dmg"
"openpencil-desktop-windows-x86_64.zip"
"openpencil-desktop-windows-aarch64.zip"
"op-cli-macos-aarch64.tar.gz"
"op-cli-macos-x86_64.tar.gz"
"op-cli-linux-aarch64.tar.gz"
"op-cli-linux-x86_64.tar.gz"
"op-cli-windows-aarch64.zip"
"op-cli-windows-x86_64.zip"
)
for file in "${required[@]}"; do
if [ ! -f "release-assets/$file" ]; then
echo "::error::missing release asset for package-manager update: $file"
exit 1
fi
done
sha_file() {
sha256sum "release-assets/$1" | awk '{print $1}'
}
{
echo "MAC_ARM_DMG_SHA=$(sha_file "OpenPencil-${version}-arm64-mac.dmg")"
echo "MAC_X64_DMG_SHA=$(sha_file "OpenPencil-${version}-x64-mac.dmg")"
echo "CLI_MAC_ARM_SHA=$(sha_file "op-cli-macos-aarch64.tar.gz")"
echo "CLI_MAC_X64_SHA=$(sha_file "op-cli-macos-x86_64.tar.gz")"
echo "CLI_LINUX_ARM_SHA=$(sha_file "op-cli-linux-aarch64.tar.gz")"
echo "CLI_LINUX_X64_SHA=$(sha_file "op-cli-linux-x86_64.tar.gz")"
echo "DESKTOP_WIN_ARM_SHA=$(sha_file "openpencil-desktop-windows-aarch64.zip")"
echo "DESKTOP_WIN_X64_SHA=$(sha_file "openpencil-desktop-windows-x86_64.zip")"
echo "CLI_WIN_ARM_SHA=$(sha_file "op-cli-windows-aarch64.zip")"
echo "CLI_WIN_X64_SHA=$(sha_file "op-cli-windows-x86_64.zip")"
} >> "$GITHUB_ENV"
- name: Checkout Homebrew tap
uses: actions/checkout@v4
with:
repository: ${{ env.HOMEBREW_TAP_REPOSITORY }}
token: ${{ env.TAP_GITHUB_TOKEN }}
path: homebrew-tap
- name: Update Homebrew cask and formula
shell: bash
run: |
set -euo pipefail
mkdir -p homebrew-tap/Casks homebrew-tap/Formula
cat > homebrew-tap/Casks/openpencil.rb <<EOF
cask "openpencil" do
version "${OP_VERSION}"
on_arm do
sha256 "${MAC_ARM_DMG_SHA}"
url "https://github.com/zseven-w/openpencil/releases/download/v#{version}/OpenPencil-#{version}-arm64-mac.dmg"
end
on_intel do
sha256 "${MAC_X64_DMG_SHA}"
url "https://github.com/zseven-w/openpencil/releases/download/v#{version}/OpenPencil-#{version}-x64-mac.dmg"
end
name "OpenPencil"
desc "Open-source AI-native vector design tool"
homepage "https://github.com/zseven-w/openpencil"
app "OpenPencil.app"
zap trash: [
"~/Library/Application Support/OpenPencil",
"~/Library/Preferences/com.zseven-w.openpencil.plist",
"~/Library/Preferences/dev.openpencil.app.plist",
"~/Library/Caches/com.zseven-w.openpencil",
"~/Library/Caches/dev.openpencil.app",
]
end
EOF
cat > homebrew-tap/Formula/op.rb <<EOF
class Op < Formula
desc "OpenPencil command-line tool"
homepage "https://github.com/zseven-w/openpencil"
version "${OP_VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/zseven-w/openpencil/releases/download/v#{version}/op-cli-macos-aarch64.tar.gz"
sha256 "${CLI_MAC_ARM_SHA}"
else
url "https://github.com/zseven-w/openpencil/releases/download/v#{version}/op-cli-macos-x86_64.tar.gz"
sha256 "${CLI_MAC_X64_SHA}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/zseven-w/openpencil/releases/download/v#{version}/op-cli-linux-aarch64.tar.gz"
sha256 "${CLI_LINUX_ARM_SHA}"
else
url "https://github.com/zseven-w/openpencil/releases/download/v#{version}/op-cli-linux-x86_64.tar.gz"
sha256 "${CLI_LINUX_X64_SHA}"
end
end
def install
bin.install "op"
end
test do
assert_match version.to_s, shell_output("#{bin}/op --version")
end
end
EOF
ruby -c homebrew-tap/Casks/openpencil.rb
ruby -c homebrew-tap/Formula/op.rb
- name: Commit Homebrew updates
shell: bash
run: |
set -euo pipefail
cd homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Casks/openpencil.rb Formula/op.rb
if git diff --cached --quiet; then
echo "::notice::Homebrew tap already matches v${OP_VERSION}"
else
git commit -m "Update OpenPencil to v${OP_VERSION}"
git push
fi
- name: Checkout Scoop bucket
uses: actions/checkout@v4
with:
repository: ${{ env.SCOOP_BUCKET_REPOSITORY }}
token: ${{ env.TAP_GITHUB_TOKEN }}
path: scoop-bucket
- name: Update Scoop manifests
shell: bash
run: |
set -euo pipefail
mkdir -p scoop-bucket/bucket
cat > scoop-bucket/bucket/openpencil.json <<EOF
{
"version": "${OP_VERSION}",
"description": "Open-source AI-native vector design tool",
"homepage": "https://github.com/zseven-w/openpencil",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v${OP_VERSION}/openpencil-desktop-windows-x86_64.zip",
"hash": "${DESKTOP_WIN_X64_SHA}"
},
"arm64": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v${OP_VERSION}/openpencil-desktop-windows-aarch64.zip",
"hash": "${DESKTOP_WIN_ARM_SHA}"
}
},
"bin": "openpencil-desktop.exe",
"shortcuts": [["openpencil-desktop.exe", "OpenPencil"]],
"checkver": {
"github": "https://github.com/zseven-w/openpencil"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v\$version/openpencil-desktop-windows-x86_64.zip"
},
"arm64": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v\$version/openpencil-desktop-windows-aarch64.zip"
}
}
}
}
EOF
cat > scoop-bucket/bucket/op.json <<EOF
{
"version": "${OP_VERSION}",
"description": "OpenPencil command-line tool",
"homepage": "https://github.com/zseven-w/openpencil",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v${OP_VERSION}/op-cli-windows-x86_64.zip",
"hash": "${CLI_WIN_X64_SHA}"
},
"arm64": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v${OP_VERSION}/op-cli-windows-aarch64.zip",
"hash": "${CLI_WIN_ARM_SHA}"
}
},
"bin": "op.exe",
"checkver": {
"github": "https://github.com/zseven-w/openpencil"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v\$version/op-cli-windows-x86_64.zip"
},
"arm64": {
"url": "https://github.com/zseven-w/openpencil/releases/download/v\$version/op-cli-windows-aarch64.zip"
}
}
}
}
EOF
jq empty scoop-bucket/bucket/openpencil.json scoop-bucket/bucket/op.json
- name: Commit Scoop updates
shell: bash
run: |
set -euo pipefail
cd scoop-bucket
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add bucket/openpencil.json bucket/op.json
if git diff --cached --quiet; then
echo "::notice::Scoop bucket already matches v${OP_VERSION}"
else
git commit -m "Update OpenPencil to v${OP_VERSION}"
git push
fi