GitHub releases were named by bare tag because the action got no name. The note's leading heading is the natural title, so publish it as the release name and drop it from the body instead of repeating it.
1223 lines
52 KiB
YAML
1223 lines
52 KiB
YAML
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
|
||
|
||
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 }}
|
||
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
|
||
- 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')
|
||
env:
|
||
OP_VERSION: ${{ needs.version.outputs.version }}
|
||
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- 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
|
||
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
|
||
- 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 }}'
|
||
} >> 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/*
|
||
|
||
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
|