1327 lines
58 KiB
YAML
1327 lines
58 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
|
||
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
|