Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

702 lines
35 KiB
YAML

name: "CD: Cua Driver (cross-platform)"
# Rust implementation release workflow — cross-platform (Windows/Linux/macOS):
# - macOS: ONE universal binary tarball + bare universal binary
# (lipo arm64 + x86_64 in a single job, no separate per-arch matrix)
# - Windows: cua-driver.exe bare + .zip
# - Linux: cua-driver bare + .tar.gz
#
# Bare binaries let curlable installers do
# `curl -L .../cua-driver-rs-vN-darwin-universal-binary.tar.gz | tar -xz`
# without needing to unpack a directory structure first.
on:
push:
tags:
- "cua-driver-rs-v*"
workflow_dispatch:
inputs:
version:
description: "Version to release (without v prefix)"
required: true
default: "0.1.0"
notarize:
description: "Codesign + notarize the macOS artifacts (set false to skip during iteration)"
required: false
type: boolean
default: true
permissions:
contents: write
jobs:
# ── Linux (x86_64 + arm64) ───────────────────────────────────────────────────
# One matrixed job per Linux target. x86_64 builds on the standard
# ubuntu-latest host; arm64 builds NATIVELY on GitHub's hosted ARM runner
# (ubuntu-24.04-arm) rather than cross-compiling, because the native X11 /
# Wayland C deps (libx11-dev, libwayland-dev, …) would otherwise need their
# arm64 multiarch variants + a cross linker wired up by hand. A native ARM
# runner keeps the build identical to the x86_64 path. install.sh picks the
# matching tarball at install time based on the host's `uname -m`.
build-linux:
name: linux-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
- arch: arm64
target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
# Build INSIDE a Debian 11 (bullseye) container instead of directly on
# the host. ubuntu-latest is now Ubuntu 24.04, whose glibc is 2.39 — a
# binary linked there imports GLIBC_2.39 symbols (e.g. posix_spawn's
# pidfd_spawnp/pidfd_getpid) and refuses to even run `--version` on older
# distros:
# cua-driver: /lib/.../libc.so.6: version 'GLIBC_2.39' not found
# That broke Debian 12 (glibc 2.36), Ubuntu 22.04 (2.35), and RHEL/Rocky
# 9 (2.34). Debian 11's glibc is 2.31, so linking against it lowers the
# floor to GLIBC_2.31 — covering Debian 11+, Ubuntu 20.04+, and RHEL/
# Rocky 8+. A container (rather than the retired ubuntu-20.04 runner
# label) keeps the floor deterministic regardless of GitHub's host image
# churn. The debian:11 image is multi-arch, so the arm64 runner pulls the
# aarch64 variant automatically and the glibc floor holds on both arches.
# The C deps below all exist in bullseye, including libwayland-dev for the
# native Wayland backend (#1910).
container:
image: debian:11
steps:
# actions/checkout needs git inside the container; the bullseye image
# is bare. Install git + the build toolchain before anything else.
- name: Install base tooling (container is bare)
run: |
apt-get update
# PipeWire ScreenCast capture remains Nix/modern-build only because
# bullseye's 0.3.19 headers are too old for libspa-sys 0.8. Portal
# RemoteDesktop/libei input is pure Rust plus libxkbcommon and ships
# in these portable release binaries.
apt-get install -y --no-install-recommends \
git ca-certificates curl build-essential pkg-config \
libx11-dev libxi-dev libxtst-dev libxext-dev libwayland-dev \
libxkbcommon-dev
- uses: actions/checkout@v4
- name: Determine version
id: version
shell: bash
run: |
if [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
else
VERSION="${{ inputs.version }}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
libs/cua-driver/rust/target
key: rust-cua-driver-rs-linux-${{ matrix.arch }}-${{ hashFiles('libs/cua-driver/rust/Cargo.lock', 'libs/cua-driver/rust/**/Cargo.toml') }}
restore-keys: |
rust-cua-driver-rs-linux-${{ matrix.arch }}-
- name: Build (release)
working-directory: libs/cua-driver/rust
run: cargo build -p cua-driver --release --features portal-input --target ${{ matrix.target }}
- name: Package
working-directory: libs/cua-driver/rust
run: |
VERSION="${{ steps.version.outputs.version }}"
LABEL="linux-${{ matrix.arch }}"
TARGET="${{ matrix.target }}"
STAGE="cua-driver-rs-${VERSION}-${LABEL}"
mkdir -p "release/${STAGE}"
cp "target/${TARGET}/release/cua-driver" "release/${STAGE}/"
cp ../../LICENSE.md "release/${STAGE}/LICENSE" 2>/dev/null || true
(cd release && tar -czf "${STAGE}.tar.gz" "${STAGE}")
# Bare binary tarball — single file (cua-driver) at the archive root,
# mirroring the Swift cua-driver `*-binary.tar.gz` convention.
tar -czf "release/${STAGE}-binary.tar.gz" -C "release/${STAGE}" cua-driver
ls -lh "release/${STAGE}.tar.gz" "release/${STAGE}-binary.tar.gz"
- uses: actions/upload-artifact@v4
with:
name: cua-driver-rs-linux-${{ matrix.arch }}
path: libs/cua-driver/rust/release/cua-driver-rs-*-linux-${{ matrix.arch }}*.tar.gz
if-no-files-found: error
# ── Windows (x86_64 + arm64) ─────────────────────────────────────────────────
# One matrixed job per Windows target. The arm64 build cross-compiles from
# the x86_64 windows-latest runner — Rust + MSVC have first-class support
# for that cross, so no separate arm64 runner is needed. install.ps1 picks
# the matching zip at install time based on the host's OS architecture.
build-windows:
name: windows-${{ matrix.arch }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86_64-pc-windows-msvc
- arch: arm64
target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- name: Determine version
id: version
shell: bash
run: |
if [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
else
VERSION="${{ inputs.version }}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
libs/cua-driver/rust/target
key: rust-cua-driver-rs-windows-${{ matrix.arch }}-${{ hashFiles('libs/cua-driver/rust/Cargo.lock', 'libs/cua-driver/rust/**/Cargo.toml') }}
restore-keys: |
rust-cua-driver-rs-windows-${{ matrix.arch }}-
- name: Build (release)
working-directory: libs/cua-driver/rust
# Build both the main CLI/MCP binary AND the uiAccess'd worker.
# `cua-driver-uia.exe` is the Windows-only sibling daemon that runs
# at UIAccess integrity so SendInput / UI Automation can cross UIPI
# into UWP apps — see crates/cua-driver-uia/ and #1602.
run: cargo build -p cua-driver -p cua-driver-uia --release --target ${{ matrix.target }}
- name: Package
working-directory: libs/cua-driver/rust
shell: pwsh
run: |
$version = "${{ steps.version.outputs.version }}"
$label = "windows-${{ matrix.arch }}"
$target = "${{ matrix.target }}"
$stage = "cua-driver-rs-${version}-${label}"
New-Item -ItemType Directory -Force -Path "release/$stage" | Out-Null
Copy-Item "target/$target/release/cua-driver.exe" "release/$stage/"
Copy-Item "target/$target/release/cua-driver-uia.exe" "release/$stage/"
if (Test-Path "../../LICENSE.md") { Copy-Item "../../LICENSE.md" "release/$stage/LICENSE" }
# Zip the directory itself (no trailing /*) so the archive contains
# the top-level "$stage" wrapper dir — install.ps1 walks into
# extracted/cua-driver-rs-<v>-<arch>/cua-driver.exe to find the
# binary. With "release/$stage/*" Compress-Archive strips the
# wrapper and dumps the files at the archive root, which breaks
# the installer's path lookup.
Compress-Archive -Path "release/$stage" -DestinationPath "release/$stage.zip" -Force
# Bare-binaries zip: both exes side-by-side (no wrapper dir).
# `cua-driver-uia.exe` is currently shipped UNSIGNED in this archive
# — until #1602 wires an EV cert into CD, the worker won't elevate
# to UIAccess integrity on a default-policy machine. The main CLI/MCP
# binary stays fully functional regardless; uia is opt-in dead-weight
# until signed.
Compress-Archive -Path "release/$stage/cua-driver.exe","release/$stage/cua-driver-uia.exe" -DestinationPath "release/$stage-binary.zip" -Force
Get-ChildItem "release/$stage*.zip"
- uses: actions/upload-artifact@v4
with:
name: cua-driver-rs-windows-${{ matrix.arch }}
path: libs/cua-driver/rust/release/cua-driver-rs-*-windows-${{ matrix.arch }}*.zip
if-no-files-found: error
# ── macOS universal (arm64 + x86_64 → lipo) ─────────────────────────────────
# Mirrors cd-swift-cua-driver.yml's approach: build both arches in one job
# on macos-26, then combine into a universal binary via `lipo -create`.
# The same universal binary is used in BOTH the arm64 and x86_64 named
# tarballs (so callers that download by arch still get the universal
# binary), plus a third `darwin-universal` tarball and a bare binary.
#
# Runner pinned to macos-26 because `screencapturekit` (added in PR #1720
# for the native ScreenCaptureKit recording backend) pulls in
# `apple-metal v0.8.7`, whose Swift bridge references macOS 26 Metal
# symbols (`MTLSamplerReductionMode`, `MTLSamplerDescriptor.reductionMode`,
# `.lodBias`). The macos-15 runner ships a macOS 15 SDK that doesn't
# expose those symbols, so the transitive Swift bridge fails to compile.
build-macos-universal:
name: darwin-universal
runs-on: macos-26
# Notarize on tag push (real release); on workflow_dispatch honor the
# `notarize` input so we can iterate on the build pipeline without
# touching the Apple notary service.
env:
DO_NOTARIZE: ${{ startsWith(github.ref, 'refs/tags/cua-driver-rs-v') || inputs.notarize == true }}
KEYCHAIN_PASSWORD: temporary-cd-keychain-pwd
steps:
- uses: actions/checkout@v4
- name: Determine version
id: version
shell: bash
run: |
if [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
else
VERSION="${{ inputs.version }}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Import code-signing certificate
if: env.DO_NOTARIZE == 'true'
env:
APPLICATION_CERT_BASE64: ${{ secrets.APPLICATION_CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
run: |
# Create a temporary keychain just for this job. The keychain is torn
# down with the runner when the job exits.
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -l build.keychain
echo "$APPLICATION_CERT_BASE64" | base64 --decode > application.p12
security import application.p12 -k build.keychain -P "$CERT_PASSWORD" \
-T /usr/bin/codesign > /dev/null 2>&1
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain > /dev/null 2>&1
rm application.p12
CERT_COUNT=$(security find-identity -v -p codesigning build.keychain | grep -c "Developer ID Application" || echo 0)
if [ "$CERT_COUNT" -eq 0 ]; then
echo "Error: no Developer ID Application certificate imported" >&2
security find-identity -v -p codesigning build.keychain
exit 1
fi
echo "Imported $CERT_COUNT Developer ID Application certificate(s)"
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin, x86_64-apple-darwin
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
libs/cua-driver/rust/target
key: rust-cua-driver-rs-darwin-universal-${{ hashFiles('libs/cua-driver/rust/Cargo.lock', 'libs/cua-driver/rust/**/Cargo.toml') }}
restore-keys: |
rust-cua-driver-rs-darwin-universal-
- name: Build arm64
working-directory: libs/cua-driver/rust
run: cargo build -p cua-driver --release --target aarch64-apple-darwin
- name: Build x86_64
working-directory: libs/cua-driver/rust
run: cargo build -p cua-driver --release --target x86_64-apple-darwin
- name: Create universal binary (lipo)
working-directory: libs/cua-driver/rust
run: |
ARM64="target/aarch64-apple-darwin/release/cua-driver"
X86="target/x86_64-apple-darwin/release/cua-driver"
mkdir -p release/universal
lipo -create "$ARM64" "$X86" -output release/universal/cua-driver
lipo -info release/universal/cua-driver
- name: Codesign universal binary (hardened runtime)
if: env.DO_NOTARIZE == 'true'
working-directory: libs/cua-driver/rust
env:
DEVELOPER_NAME: ${{ secrets.DEVELOPER_NAME }}
TEAM_ID: ${{ secrets.TEAM_ID }}
run: |
IDENTITY="Developer ID Application: ${DEVELOPER_NAME} (${TEAM_ID})"
codesign --force --timestamp --options runtime \
--entitlements scripts/CuaDriver.entitlements \
--sign "$IDENTITY" release/universal/cua-driver
codesign --verify --strict --verbose=2 release/universal/cua-driver
- name: Assemble CuaDriver.app bundle
working-directory: libs/cua-driver/rust
run: |
# Copy the bundle skeleton (Info.plist) from
# scripts/CuaDriverBundle/ and drop the universal binary into
# Contents/MacOS/cua-driver. The skeleton lives under a non-
# `.app` directory so macOS LaunchServices on developer
# machines doesn't index it as a second installed app with
# the `com.trycua.driver` bundle id (which used to surface as
# a "ghost CuaDriver" entry in System Settings → Privacy &
# Security). The assembled bundle goes into every directory
# tarball so install.sh can `ditto` it to
# /Applications/CuaDriver.app for the TCC auto-relaunch path.
#
# The bundle ships unsigned (notarization happens in the next step).
# TCC keys grants on the bundle identity (com.trycua.driver).
VERSION="${{ steps.version.outputs.version }}"
mkdir -p release/CuaDriver.app
cp -R scripts/CuaDriverBundle/Contents release/CuaDriver.app/Contents
cp release/universal/cua-driver \
release/CuaDriver.app/Contents/MacOS/cua-driver
chmod +x release/CuaDriver.app/Contents/MacOS/cua-driver
# Stamp the release version into Info.plist so the bundle
# version tracks the tag instead of whatever was last
# checked in. Without this the in-tree Info.plist's
# CFBundleShortVersionString drifts from the release tag on
# every cut. CodeRabbit #4.
plutil -replace CFBundleShortVersionString -string "$VERSION" \
release/CuaDriver.app/Contents/Info.plist
plutil -replace CFBundleVersion -string "$VERSION" \
release/CuaDriver.app/Contents/Info.plist
# Remove the .gitkeep we use in source control — it's not
# part of the runtime bundle.
rm -f release/CuaDriver.app/Contents/MacOS/.gitkeep
ls -la release/CuaDriver.app/Contents/MacOS
plutil -p release/CuaDriver.app/Contents/Info.plist | grep -E 'CFBundle(Short)?Version'
- name: Codesign + notarize + staple CuaDriver.app
if: env.DO_NOTARIZE == 'true'
working-directory: libs/cua-driver/rust
env:
DEVELOPER_NAME: ${{ secrets.DEVELOPER_NAME }}
TEAM_ID: ${{ secrets.TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
IDENTITY="Developer ID Application: ${DEVELOPER_NAME} (${TEAM_ID})"
# Sign the .app — `--deep` covers the embedded binary too, but
# since we already pre-signed the binary explicitly above this
# is essentially a no-op on the binary and a fresh signature
# on the bundle wrapper.
codesign --force --deep --timestamp --options runtime \
--entitlements scripts/CuaDriver.entitlements \
--sign "$IDENTITY" release/CuaDriver.app
codesign --verify --strict --verbose=2 release/CuaDriver.app
# notarytool wants a zip (or .dmg / .pkg). Build one next to
# the .app, submit, wait, then staple the bundle in place.
/usr/bin/ditto -c -k --keepParent release/CuaDriver.app \
release/CuaDriver.app.zip
xcrun notarytool submit release/CuaDriver.app.zip \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--wait --timeout 20m
xcrun stapler staple release/CuaDriver.app
spctl -a -vv -t exec release/CuaDriver.app
# The zip we used to submit is throwaway; the packaging step
# below builds the final tarballs from the stapled .app.
rm -f release/CuaDriver.app.zip
- name: Package
working-directory: libs/cua-driver/rust
run: |
VERSION="${{ steps.version.outputs.version }}"
for LABEL in darwin-arm64 darwin-x86_64 darwin-universal; do
STAGE="cua-driver-rs-${VERSION}-${LABEL}"
mkdir -p "release/${STAGE}"
# Same universal binary in all three named tarballs — callers
# that download by arch still get the universal slice
# (matches the Swift `cd-swift-cua-driver.yml` convention).
cp release/universal/cua-driver "release/${STAGE}/"
# Ship the .app bundle in every macOS tarball so install.sh
# can drop it into /Applications/CuaDriver.app for TCC
# attribution (issue #1525). Tarball callers that want only
# the bare binary can grab the *-binary.tar.gz below.
cp -R release/CuaDriver.app "release/${STAGE}/CuaDriver.app"
cp ../../LICENSE.md "release/${STAGE}/LICENSE" 2>/dev/null || true
(cd release && tar -czf "${STAGE}.tar.gz" "${STAGE}")
done
# Bare universal binary — single-file tarball matching Swift's
# `cua-driver-${VERSION}-darwin-universal-binary.tar.gz`. NO
# bundle here: callers that fetch the bare tarball deliberately
# skipped the .app workflow.
tar -czf "release/cua-driver-rs-${VERSION}-darwin-universal-binary.tar.gz" \
-C release/universal cua-driver
ls -lh release/*.tar.gz
- uses: actions/upload-artifact@v4
with:
name: cua-driver-rs-darwin
path: libs/cua-driver/rust/release/cua-driver-rs-*-darwin-*.tar.gz
if-no-files-found: error
# ── Release ──────────────────────────────────────────────────────────────────
release:
needs: [build-linux, build-windows, build-macos-universal]
if: startsWith(github.ref, 'refs/tags/cua-driver-rs-v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine version
id: version
run: |
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
# Bake the new version into the working-tree install scripts BEFORE
# staging them for the release upload, so the per-tag release assets
# carry version N's baked default — not version N-1's. The same sed
# patterns run again later under `Bake version into install scripts
# (commit + push)` to land the change on main; doing the in-tree
# rewrite here is idempotent (sed on an already-baked file is a
# no-op) and ensures the GitHub Release page and main-branch commit
# stay in lockstep.
- name: Bake version into install scripts (in-tree)
if: startsWith(github.ref, 'refs/tags/cua-driver-rs-v')
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# GNU sed (ubuntu-latest). The Swift CD workflow runs on
# macos-15 and uses the BSD form (`sed -i ''`).
# The Rust install logic now lives at
# libs/cua-driver/scripts/_install-rust.sh (a private helper
# invoked by libs/cua-driver/scripts/install.sh by default).
sed -i \
"s/^CUA_DRIVER_RS_BAKED_VERSION=.*/CUA_DRIVER_RS_BAKED_VERSION=\"${VERSION}\"/" \
libs/cua-driver/scripts/_install-rust.sh
# install.ps1 lives at the canonical Swift install dir;
# PowerShell line starts with `$Script:` — escape the `$` for
# sed's BRE so it matches the literal dollar sign.
sed -i \
"s/^\\\$Script:CuaDriverRsBakedVersion = .*/\\\$Script:CuaDriverRsBakedVersion = \"${VERSION}\"/" \
libs/cua-driver/scripts/install.ps1
echo "Baked version ${VERSION} into working-tree install scripts:"
grep -E '^CUA_DRIVER_RS_BAKED_VERSION=' libs/cua-driver/scripts/_install-rust.sh
grep -E '^\$Script:CuaDriverRsBakedVersion = ' libs/cua-driver/scripts/install.ps1
- name: Stage release files
run: |
mkdir -p release-upload
find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" \) \
-exec cp {} release-upload/ \;
# Ship the installer scripts as per-tag release assets too. The
# canonical one-liners both fetch from
# raw.githubusercontent.com/trycua/cua/main/... so they always
# serve the current installer regardless of release flags
# (prerelease excludes us from the /releases/latest/ endpoint).
# The tag-pinned asset URLs below are a convenience for users who
# want to audit the exact installer that shipped with a release.
#
# The freshly-baked scripts (rewritten in the prior step) are
# what gets copied here, so the per-tag release assets carry the
# current version's baked default — not the previous version's.
# Both user-facing installers live under libs/cua-driver/scripts/:
# install.sh is the canonical macOS/Linux installer and delegates
# to the Rust _install-rust.sh helper by default. install.ps1 is
# the canonical Windows installer. The Rust install logic itself
# lives at _install-rust.sh — published as a release asset
# alongside the installers so curl|bash flows can fetch it.
cp libs/cua-driver/scripts/install.sh release-upload/install.sh
cp libs/cua-driver/scripts/install.ps1 release-upload/install.ps1
cp libs/cua-driver/scripts/_install-rust.sh release-upload/_install-rust.sh
# Uninstaller scripts — single canonical file per shell (no
# `_uninstall-rust.sh` helper). uninstall.sh handles Rust (the only
# supported backend). uninstall.ps1 is the Windows uninstaller
# and self-elevates via UAC when an -AutoStart install
# (RunLevel=Highest task) needs to be torn down.
cp libs/cua-driver/scripts/uninstall.sh release-upload/uninstall.sh
cp libs/cua-driver/scripts/uninstall.ps1 release-upload/uninstall.ps1
# Skill pack — single platform-agnostic tarball fetched by
# `cua-driver skills install`. The .md files are identical across
# OS so we publish one asset per release tag. Naming matches the
# versioned binary tarballs.
VERSION="${{ steps.version.outputs.version }}"
# Asset name keeps the `cua-driver-rs-v*` prefix for backward
# compat with the URL skills.rs constructs.
#
# Staging layout = one wrapper directory, flat .md files under
# it. extract_tar_gz strips that single wrapper and lands files
# directly in <HomeDir>/skills/cua-driver/. v0.2.19 and earlier
# had a second `cua-driver/` (or `cua-driver-rs/`) dir inside
# the wrapper — the extractor sees that and strips it too for
# backward compat with already-published releases.
SKILLS_STAGE="cua-driver-rs-v${VERSION}-skills"
if [ -d libs/cua-driver/rust/Skills/cua-driver ]; then
mkdir -p "${SKILLS_STAGE}"
cp -R libs/cua-driver/rust/Skills/cua-driver/. "${SKILLS_STAGE}/"
tar -czf "release-upload/${SKILLS_STAGE}.tar.gz" "${SKILLS_STAGE}"
echo "Packaged skill pack: release-upload/${SKILLS_STAGE}.tar.gz"
else
echo "Note: libs/cua-driver/rust/Skills/cua-driver not present; skipping skill-pack asset."
fi
echo "Release files:"
ls -lh release-upload/
- name: Generate SHA256 checksums
id: checksums
run: |
cd release-upload
{
echo "## SHA256 Checksums"
echo '```'
shasum -a 256 cua-driver-rs-*.{tar.gz,zip} 2>/dev/null | sort
echo '```'
} > checksums.txt
checksums=$(cat checksums.txt)
echo "checksums<<EOF" >> "$GITHUB_OUTPUT"
echo "$checksums" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Generate path-filtered release notes
id: notes
run: |
PREV_TAG=$(git tag -l "cua-driver-rs-v*" --sort=-v:refname \
| grep -v "^${{ github.ref_name }}$" \
| head -n 1 || echo "")
if [ -n "$PREV_TAG" ]; then
NOTES=$(git log ${PREV_TAG}..HEAD --pretty=format:"* %s (%h) by @%an" -- "libs/cua-driver/rust" | head -50)
else
NOTES=$(git log --pretty=format:"* %s (%h) by @%an" -- "libs/cua-driver/rust" | head -50)
fi
[ -z "$NOTES" ] && NOTES="* No path-specific changes found"
{
echo "RELEASE_NOTES<<EOF"
echo "## What's Changed"
echo ""
echo "$NOTES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-upload/*
prerelease: true
body: |
> **cua-driver-rs**
>
> `cua-driver-rs` is the Rust implementation of
> [`cua-driver`](https://github.com/trycua/cua/tree/main/libs/cua-driver),
> shipping the same user-facing `cua-driver` binary for macOS and
> Windows, plus Linux pre-release artifacts for early testing.
>
> The Rust port and the Swift driver publish to the same GitHub repo under
> **distinct tag prefixes** (`cua-driver-rs-v*` vs `cua-driver-v*`), so their
> release artifacts do not collide.
${{ steps.notes.outputs.RELEASE_NOTES }}
${{ steps.checksums.outputs.checksums }}
### Install (macOS / Linux pre-release)
```bash
/bin/bash -c "$(curl -fsSL https://cua.ai/driver/install.sh)"
```
The shell installer covers macOS and the Linux pre-release backend
and installs the Rust implementation by default. Linux artifacts are
published for early testing and are not yet an official supported
release tier.
### Install (Windows)
```powershell
irm https://cua.ai/driver/install.ps1 | iex
```
The installer auto-detects host architecture (x86_64 / arm64) and uses
directory junctions for the install layout, so it runs without admin
and without Developer Mode.
### Artifacts
**macOS (universal — arm64 + x86_64 in one binary, like the Swift cua-driver)**
- `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-universal.tar.gz` — directory tarball with LICENSE + `CuaDriver.app` bundle (install.sh expects this layout)
- `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz` — same payload, named for arm64 callers
- `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-x86_64.tar.gz` — same payload, named for x86_64 callers
- `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-universal-binary.tar.gz` — bare universal binary (single file at archive root; **no** .app — bypasses the TCC auto-relaunch path)
**Linux pre-release**
- `cua-driver-rs-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz` — directory tarball
- `cua-driver-rs-${{ steps.version.outputs.version }}-linux-x86_64-binary.tar.gz` — bare binary
- `cua-driver-rs-${{ steps.version.outputs.version }}-linux-arm64.tar.gz` — directory tarball (Linux on ARM / aarch64)
- `cua-driver-rs-${{ steps.version.outputs.version }}-linux-arm64-binary.tar.gz` — bare binary (Linux on ARM / aarch64)
**Windows**
- `cua-driver-rs-${{ steps.version.outputs.version }}-windows-x86_64.zip` — directory zip
- `cua-driver-rs-${{ steps.version.outputs.version }}-windows-x86_64-binary.zip` — bare `cua-driver.exe`
- `cua-driver-rs-${{ steps.version.outputs.version }}-windows-arm64.zip` — directory zip (Windows on ARM)
- `cua-driver-rs-${{ steps.version.outputs.version }}-windows-arm64-binary.zip` — bare `cua-driver.exe` (Windows on ARM)
**Installer scripts**
- `install.sh` — macOS / Linux pre-release one-liner installer
- `install.ps1` — Windows one-liner installer
- `uninstall.sh` — macOS / Linux pre-release one-liner uninstaller
- `uninstall.ps1` — Windows one-liner uninstaller
generate_release_notes: false
make_latest: false
# The bump-version workflow uses this same GitHub App so its pushes
# bypass the "Changes must be made through a pull request" ruleset
# on main. The default GITHUB_TOKEN (github-actions[bot]) is NOT on
# the bypass list and gets rejected here. Mirror the Swift CD
# workflow's auth setup so the bake-version push lands.
- name: Generate GitHub App token (for bake-version push)
id: app-token
if: startsWith(github.ref, 'refs/tags/cua-driver-rs-v')
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
- name: Bake version into install scripts (commit + push)
if: startsWith(github.ref, 'refs/tags/cua-driver-rs-v')
env:
VERSION: ${{ steps.version.outputs.version }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# Re-authenticate the origin remote with the app token so the
# push uses the bypass-enabled identity, not the default
# github-actions[bot] credentials baked in by actions/checkout.
#
# actions/checkout sets `http.https://github.com/.extraheader`
# with the default GITHUB_TOKEN; that header otherwise overrides
# the URL-embedded App token on the push (two Authorization
# headers sent → server takes the extraheader → push rejected
# by the main-branch ruleset). Drop it so only the App token's
# auth survives.
git config --unset-all "http.https://github.com/.extraheader" || true
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git fetch origin main
git checkout -B bake-version origin/main
# Replay the same sed the earlier in-tree step ran against the
# working tree — but here against origin/main, so the commit we
# push back to main has the new baked version regardless of
# whether the in-tree step ran (it's idempotent and the on-disk
# working-tree edits got discarded by the checkout above).
#
# The release job runs on ubuntu-latest, so this uses GNU sed
# syntax (`sed -i` with no empty-string arg) — the Swift CD
# workflow's bake step runs on macos-15 and uses the BSD form
# (`sed -i ''`).
# Rust install logic lives at libs/cua-driver/scripts/_install-rust.sh
# (private helper invoked by the canonical libs/cua-driver/scripts/install.sh
# by default).
sed -i \
"s/^CUA_DRIVER_RS_BAKED_VERSION=.*/CUA_DRIVER_RS_BAKED_VERSION=\"${VERSION}\"/" \
libs/cua-driver/scripts/_install-rust.sh
# install.ps1 lives at libs/cua-driver/scripts/install.ps1
# (canonical Windows entry point). The PowerShell line starts
# with `$Script:` — escape the `$` for sed's BRE so it matches
# the literal dollar sign.
sed -i \
"s/^\\\$Script:CuaDriverRsBakedVersion = .*/\\\$Script:CuaDriverRsBakedVersion = \"${VERSION}\"/" \
libs/cua-driver/scripts/install.ps1
git config user.name "trycua-release[bot]"
git config user.email "trycua-release[bot]@users.noreply.github.com"
git add libs/cua-driver/scripts/_install-rust.sh libs/cua-driver/scripts/install.ps1
git commit -m "chore(cua-driver-rs): bake version ${VERSION} into install scripts [skip ci]"
git push origin bake-version:main