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--/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 /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<> "$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<> "$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