mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(cua): enable native cursor and live macOS previews * fix(cua): preserve focus in standalone UI hosts * feat(cua): polish floating preview cards * fix(cua): stabilize PiP hover and modal interactions * test(node-repl): use a lint-compatible status assertion * fix(cua): retain the preview while a newer frame is pending * fix(cua): use an auxiliary fullscreen role for the preview * fix(cua): preserve worker lifetime and preview controls
1203 lines
57 KiB
YAML
1203 lines
57 KiB
YAML
name: 'CD: Qwen CUA Driver'
|
|
# Cross-platform release for the vendored cua-driver fork under
|
|
# packages/cua-driver. Adapted from upstream trycua/cua
|
|
# cd-rust-cua-driver.yml, with two changes for this repo:
|
|
# - paths point at packages/cua-driver/rust (not libs/cua-driver/rust)
|
|
# - macOS signing/notarization uses qwen-code's existing secrets:
|
|
# cert : MAC_CSC_LINK (base64 .p12) + MAC_CSC_KEY_PASSWORD
|
|
# notarize : App Store Connect API key
|
|
# (APPLE_NOTARY_API_KEY_P8_BASE64 / _KEY_ID / _ISSUER_ID)
|
|
# The Developer ID Application identity is auto-discovered from the
|
|
# imported cert (no DEVELOPER_NAME secret needed).
|
|
# Windows archives carry the unsigned UIAccess worker. Deployments that need
|
|
# UIAccess must sign and trust that worker on the target machine before placing
|
|
# it under the secure Program Files path required by Windows. Branch dry runs
|
|
# test-sign the worker through the SignPath API to exercise the signed install.
|
|
#
|
|
# Artifact convention used by the @qwen-code/cua-sdk native installer:
|
|
# macOS : cua-driver-rs-<v>-darwin-{arm64,x86_64,universal}.tar.gz
|
|
# (each contains the Qwen binary, SDK payload, and signed
|
|
# QwenCuaDriver.app) + *-binary.tar.gz (runtime + SDK payload)
|
|
# Linux : cua-driver-rs-<v>-linux-{x86_64,arm64}.tar.gz + *-binary.tar.gz
|
|
# Windows: cua-driver-rs-<v>-windows-{x86_64,arm64}.zip + *-binary.zip
|
|
# npm : @qwen-code/cua-sdk downloads and verifies the matching *-binary
|
|
# archive above. SDK, browser SDK and Node REPL share one version
|
|
# and ship from the same verified workflow.
|
|
on:
|
|
push:
|
|
tags: ['cua-driver-rs-v*']
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Version to release (without v prefix)'
|
|
required: true
|
|
default: '0.27.0'
|
|
notarize:
|
|
description:
|
|
'Codesign + notarize the macOS artifacts (false to skip during
|
|
iteration)'
|
|
required: false
|
|
type: 'boolean'
|
|
default: true
|
|
dry_run:
|
|
description:
|
|
'Build release assets and clean-install all three npm packages without
|
|
creating a GitHub Release, npm version, or tag.'
|
|
required: false
|
|
type: 'boolean'
|
|
default: true
|
|
permissions:
|
|
contents: 'write'
|
|
jobs:
|
|
validate-version:
|
|
name: 'validate-release-version'
|
|
runs-on: 'ubuntu-latest'
|
|
outputs:
|
|
version: '${{ steps.release.outputs.version }}'
|
|
node_repl_version: '${{ steps.release.outputs.node_repl_version }}'
|
|
steps:
|
|
- uses: 'actions/checkout@v4'
|
|
with:
|
|
fetch-depth: 0
|
|
- name: 'Validate release version'
|
|
id: 'release'
|
|
shell: 'bash'
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
|
|
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
|
|
else
|
|
VERSION="${{ inputs.version }}"
|
|
fi
|
|
|
|
CARGO_VERSION=$(sed -nE 's/^version = "([^"]+)"$/\1/p' packages/cua-driver/rust/Cargo.toml | head -1)
|
|
if [[ "$CARGO_VERSION" != "$VERSION" ]]; then
|
|
echo "::error::Release version $VERSION does not match Cargo version $CARGO_VERSION"
|
|
exit 1
|
|
fi
|
|
|
|
DRIVER_VERSION=$(tr -d '[:space:]' < packages/cua-driver/rust/VERSION)
|
|
if [[ "$DRIVER_VERSION" != "$VERSION" ]]; then
|
|
echo "::error::Release version $VERSION does not match driver version $DRIVER_VERSION"
|
|
exit 1
|
|
fi
|
|
|
|
SDK_NAME=$(node -p "require('./packages/cua-driver/typescript/package.json').name")
|
|
SDK_VERSION=$(node -p "require('./packages/cua-driver/typescript/package.json').version")
|
|
if [[ "$SDK_NAME" != "@qwen-code/cua-sdk" ]]; then
|
|
echo "::error::Expected @qwen-code/cua-sdk, found $SDK_NAME"
|
|
exit 1
|
|
fi
|
|
if [[ "$SDK_VERSION" != "$VERSION" ]]; then
|
|
echo "::error::SDK version $SDK_VERSION does not match release version $VERSION"
|
|
exit 1
|
|
fi
|
|
|
|
INSTALL_SH_VERSION=$(sed -nE 's/^CUA_DRIVER_RS_BAKED_VERSION="([^"]+)"$/\1/p' packages/cua-driver/scripts/_install-rust.sh)
|
|
# shellcheck disable=SC2016
|
|
INSTALL_PS1_VERSION=$(sed -nE 's/^\$Script:CuaDriverRsBakedVersion = "([^"]+)"$/\1/p' packages/cua-driver/scripts/install.ps1)
|
|
INSTALL_ENTRYPOINT_RS_VERSION=$(sed -nE 's/^# CUA_DRIVER_RS_VERSION=([^ ]+) pin a specific Rust release tag$/\1/p' packages/cua-driver/scripts/install.sh)
|
|
INSTALL_ENTRYPOINT_LEGACY_VERSION=$(sed -nE 's/^# CUA_DRIVER_VERSION=([^ ]+) legacy alias for CUA_DRIVER_RS_VERSION$/\1/p' packages/cua-driver/scripts/install.sh)
|
|
DRIVER_SKILL_VERSION=$(sed -nE 's/^version: ([^ ]+) # x-release-please-version$/\1/p' packages/cua-driver/rust/Skills/cua-driver/SKILL.md)
|
|
README_SH_VERSION=$(sed -nE 's/^CUA_DRIVER_RS_VERSION=([^ ]+) \\$/\1/p' packages/cua-driver/README.md)
|
|
# shellcheck disable=SC2016
|
|
README_PS1_VERSION=$(sed -nE 's/^\$env:CUA_DRIVER_RS_VERSION = "([^"]+)"$/\1/p' packages/cua-driver/README.md)
|
|
README_EXPECTED_VERSION=$(sed -nE 's/^Expected: qwen-cua-driver ([0-9A-Za-z.-]+)\.$/\1/p' packages/cua-driver/README.md)
|
|
for PAIR in \
|
|
"shell installer:$INSTALL_SH_VERSION" \
|
|
"PowerShell installer:$INSTALL_PS1_VERSION" \
|
|
"installer entrypoint Rust pin:$INSTALL_ENTRYPOINT_RS_VERSION" \
|
|
"installer entrypoint legacy pin:$INSTALL_ENTRYPOINT_LEGACY_VERSION" \
|
|
"driver skill:$DRIVER_SKILL_VERSION" \
|
|
"README shell command:$README_SH_VERSION" \
|
|
"README PowerShell command:$README_PS1_VERSION" \
|
|
"README expected output:$README_EXPECTED_VERSION"; do
|
|
LABEL=${PAIR%%:*}
|
|
VALUE=${PAIR#*:}
|
|
if [[ "$VALUE" != "$VERSION" ]]; then
|
|
echo "::error::$LABEL version $VALUE does not match release version $VERSION"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
NODE_REPL_NAME=$(node -p "require('./packages/node-repl/package.json').name")
|
|
NODE_REPL_VERSION=$(node -p "require('./packages/node-repl/package.json').version")
|
|
if [[ "$NODE_REPL_NAME" != "@qwen-code/node-repl-mcp" ]]; then
|
|
echo "::error::Expected @qwen-code/node-repl-mcp, found $NODE_REPL_NAME"
|
|
exit 1
|
|
fi
|
|
if [[ ! "$NODE_REPL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
|
echo "::error::Invalid Node REPL package version $NODE_REPL_VERSION"
|
|
exit 1
|
|
fi
|
|
BROWSER_VERSION=$(node -p "require('./packages/browser-use/package.json').version")
|
|
SDK_BROWSER_VERSION=$(node -p "require('./packages/cua-driver/typescript/package.json').dependencies['@qwen-code/browser-use']")
|
|
NODE_SDK_VERSION=$(node -p "require('./packages/node-repl/package.json').dependencies['@qwen-code/cua-sdk']")
|
|
for VALUE in "$NODE_REPL_VERSION" "$BROWSER_VERSION" "$SDK_BROWSER_VERSION" "$NODE_SDK_VERSION"; do
|
|
if [[ "$VALUE" != "$VERSION" ]]; then
|
|
echo "::error::SDK, browser SDK, Node REPL and native release must use the same version $VERSION; found $VALUE"
|
|
exit 1
|
|
fi
|
|
done
|
|
USER_GUIDE_NODE_REPL_VERSION=$(sed -nE 's/^qwen mcp add --scope user node-repl npx -y @qwen-code\/node-repl-mcp@([^ ]+)$/\1/p' docs/users/features/computer-use.md)
|
|
if [[ "$USER_GUIDE_NODE_REPL_VERSION" != "$NODE_REPL_VERSION" ]]; then
|
|
echo "::error::Computer Use user guide Node REPL version $USER_GUIDE_NODE_REPL_VERSION does not match Node REPL version $NODE_REPL_VERSION"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.dry_run }}" != "true" && "$GITHUB_REF" != "refs/heads/main" ]]; then
|
|
echo "::error::A production dispatch must run from protected main"
|
|
exit 1
|
|
fi
|
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.dry_run }}" != "true" && "${{ inputs.notarize }}" != "true" ]]; then
|
|
echo "::error::A production dispatch must enable macOS codesigning and notarization"
|
|
exit 1
|
|
fi
|
|
if [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
|
|
git fetch origin main
|
|
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
echo "::error::Release tag must point to a commit reachable from main"
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "node_repl_version=$NODE_REPL_VERSION" >> "$GITHUB_OUTPUT"
|
|
- uses: 'dtolnay/[email protected]'
|
|
- name: 'Test model payload filtering'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
run: 'cargo test -p cua-driver-core --locked model_payload'
|
|
|
|
# ── Linux (x86_64 + arm64) ───────────────────────────────────────────────
|
|
# Built inside debian:11 (glibc 2.31) so binaries run on Debian 11+,
|
|
# Ubuntu 20.04+, RHEL/Rocky 8+. arm64 builds natively on a hosted ARM
|
|
# runner to avoid hand-wiring a cross linker for the X11 C deps.
|
|
build-linux:
|
|
name: 'linux-${{ matrix.arch }}'
|
|
needs: ['validate-version']
|
|
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'
|
|
container:
|
|
image: 'debian:11'
|
|
steps:
|
|
- name: 'Install base tooling (container is bare)'
|
|
run: |
|
|
# Debian 11 is intentionally retained for glibc 2.31 compatibility.
|
|
# Pin all repositories to one coherent end-of-LTS snapshot: the live
|
|
# security mirror can retain an expired index after removing its .debs.
|
|
printf '%s\n' \
|
|
'deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20260831T000000Z bullseye main' \
|
|
'deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20260831T000000Z bullseye-updates main' \
|
|
'deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/20260831T000000Z bullseye-security main' \
|
|
> /etc/apt/sources.list
|
|
apt-get -o Acquire::Retries=3 update
|
|
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
|
|
git ca-certificates curl build-essential pkg-config \
|
|
libx11-dev libxi-dev libxtst-dev libxext-dev
|
|
- uses: 'actions/checkout@v4'
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
- 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/[email protected]'
|
|
with:
|
|
targets: '${{ matrix.target }}'
|
|
- uses: 'actions/cache@v4'
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
packages/cua-driver/rust/target
|
|
key:
|
|
"cua-driver-linux-${{ matrix.arch }}-${{ hashFiles('packages/cua-driver/rust/Cargo.lock')
|
|
}}"
|
|
restore-keys: |
|
|
cua-driver-linux-${{ matrix.arch }}-
|
|
- name: 'Build (release)'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
run: 'cargo build -p cua-driver -p cursor-theme-cli -p cua-driver-sdk --release --locked --target ${{ matrix.target }}'
|
|
- name: 'Build Electron-compatible Node runtime'
|
|
working-directory: 'packages/cua-driver'
|
|
run: |
|
|
corepack pnpm -C ../.. install --frozen-lockfile --ignore-scripts --prefer-offline
|
|
node scripts/build-node-runtime.mjs \
|
|
--target "${{ matrix.target }}" \
|
|
--output "rust/target/${{ matrix.target }}/release/cua_driver_node_runtime.node"
|
|
- name: 'Package'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
run: |
|
|
VERSION="${{ steps.version.outputs.version }}"
|
|
TARGET="${{ matrix.target }}"
|
|
STAGE="cua-driver-rs-${VERSION}-linux-${{ matrix.arch }}"
|
|
mkdir -p "release/${STAGE}"
|
|
cp "target/${TARGET}/release/qwen-cua-driver" "release/${STAGE}/"
|
|
cp "target/${TARGET}/release/cua-cursor-theme" "release/${STAGE}/"
|
|
cp "target/${TARGET}/release/libcua_driver_sdk.so" "release/${STAGE}/"
|
|
cp "target/${TARGET}/release/cua_driver_node_runtime.node" "release/${STAGE}/"
|
|
cp include/cua_driver_abi.h "release/${STAGE}/"
|
|
cp ../../../LICENSE "release/${STAGE}/LICENSE"
|
|
(cd release && tar -czf "${STAGE}.tar.gz" "${STAGE}")
|
|
tar -czf "release/${STAGE}-binary.tar.gz" -C "release/${STAGE}" \
|
|
qwen-cua-driver cua-cursor-theme libcua_driver_sdk.so \
|
|
cua_driver_node_runtime.node cua_driver_abi.h
|
|
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: 'packages/cua-driver/rust/release/cua-driver-rs-*-linux-${{ matrix.arch }}*.tar.gz'
|
|
if-no-files-found: 'error'
|
|
|
|
# ── Windows (x86_64 + arm64) — unsigned UIAccess worker ────────────────────
|
|
build-windows:
|
|
name: 'windows-${{ matrix.arch }}'
|
|
needs: ['validate-version', 'verify-browser-package']
|
|
runs-on: 'windows-latest'
|
|
permissions:
|
|
contents: 'read'
|
|
env:
|
|
CUA_TEST_SIGNING:
|
|
"${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true
|
|
&& !startsWith(github.ref, 'refs/tags/') }}"
|
|
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'
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
- 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/[email protected]'
|
|
with:
|
|
targets: '${{ matrix.target }}'
|
|
- uses: 'actions/cache@v4'
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
packages/cua-driver/rust/target
|
|
key:
|
|
"cua-driver-windows-${{ matrix.arch }}-${{ hashFiles('packages/cua-driver/rust/Cargo.lock')
|
|
}}"
|
|
restore-keys: |
|
|
cua-driver-windows-${{ matrix.arch }}-
|
|
- name: 'Build (release)'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
shell: 'pwsh'
|
|
run: |
|
|
# A cached target may contain a worker mutated by an earlier signing
|
|
# job. Remove the final PE so Cargo always relinks an unsigned worker.
|
|
Remove-Item "target/${{ matrix.target }}/release/cua-driver-uia.exe" -Force -ErrorAction SilentlyContinue
|
|
cargo build -p cua-driver -p cursor-theme-cli -p cua-driver-uia -p cua-driver-sdk --release --locked --target ${{ matrix.target }}
|
|
- name: 'Build Electron-compatible Node runtime'
|
|
working-directory: 'packages/cua-driver'
|
|
shell: 'bash'
|
|
run: |
|
|
corepack pnpm -C ../.. install --frozen-lockfile --ignore-scripts --prefer-offline
|
|
node scripts/build-node-runtime.mjs \
|
|
--target "${{ matrix.target }}" \
|
|
--output "rust/target/${{ matrix.target }}/release/cua_driver_node_runtime.node"
|
|
- name: 'Verify unsigned UIAccess worker'
|
|
shell: 'pwsh'
|
|
run: |
|
|
$worker = "packages/cua-driver/rust/target/${{ matrix.target }}/release/cua-driver-uia.exe"
|
|
if (-not (Test-Path -LiteralPath $worker -PathType Leaf)) {
|
|
throw 'Windows build did not produce cua-driver-uia.exe'
|
|
}
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $worker
|
|
if ($signature.Status -ne 'NotSigned') {
|
|
throw "Release worker must be unsigned for target-machine signing; status is $($signature.Status)"
|
|
}
|
|
- name: 'Test-sign UIAccess worker (dry run)'
|
|
if: "env.CUA_TEST_SIGNING == 'true'"
|
|
shell: 'pwsh'
|
|
env:
|
|
SIGNPATH_API_TOKEN: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
|
run: >-
|
|
./.github/scripts/test-sign-cua-windows-worker.ps1
|
|
-Worker "packages/cua-driver/rust/target/${{ matrix.target }}/release/cua-driver-uia.exe"
|
|
- name: 'Package'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
shell: 'pwsh'
|
|
run: |
|
|
$version = "${{ steps.version.outputs.version }}"
|
|
$target = "${{ matrix.target }}"
|
|
$stage = "cua-driver-rs-$version-windows-${{ matrix.arch }}"
|
|
New-Item -ItemType Directory -Force -Path "release/$stage" | Out-Null
|
|
Copy-Item "target/$target/release/qwen-cua-driver.exe" "release/$stage/"
|
|
Copy-Item "target/$target/release/cua-cursor-theme.exe" "release/$stage/"
|
|
Copy-Item "target/$target/release/cua-driver-uia.exe" "release/$stage/qwen-cua-driver-uia.exe"
|
|
Copy-Item "target/$target/release/cua_driver_sdk.dll" "release/$stage/"
|
|
Copy-Item "target/$target/release/cua_driver_node_runtime.node" "release/$stage/"
|
|
Copy-Item "include/cua_driver_abi.h" "release/$stage/"
|
|
Copy-Item "../../../LICENSE" "release/$stage/LICENSE"
|
|
Compress-Archive -Path "release/$stage" -DestinationPath "release/$stage.zip" -Force
|
|
Compress-Archive -Path "release/$stage/qwen-cua-driver.exe","release/$stage/cua-cursor-theme.exe","release/$stage/qwen-cua-driver-uia.exe","release/$stage/cua_driver_sdk.dll","release/$stage/cua_driver_node_runtime.node","release/$stage/cua_driver_abi.h" -DestinationPath "release/$stage-binary.zip" -Force
|
|
Get-ChildItem "release/$stage*.zip"
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'browser-use-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/browser-use-package'
|
|
- name: 'Clean-install Windows SDK from packaged payload'
|
|
shell: 'pwsh'
|
|
env:
|
|
CUA_DRIVER_USE_UIACCESS: "${{ env.CUA_TEST_SIGNING == 'true' && '1' || '0' }}"
|
|
run: |
|
|
$sdk = Join-Path $env:GITHUB_WORKSPACE 'packages/cua-driver/typescript'
|
|
$stage = "cua-driver-rs-${{ steps.version.outputs.version }}-windows-${{ matrix.arch }}"
|
|
$archive = Join-Path $env:GITHUB_WORKSPACE "packages/cua-driver/rust/release/$stage-binary.zip"
|
|
$native = Join-Path $env:RUNNER_TEMP 'cua-sdk-native'
|
|
Expand-Archive -LiteralPath $archive -DestinationPath $native
|
|
$env:QWEN_CUA_SDK_NATIVE_DIR = $native
|
|
Push-Location $sdk
|
|
try {
|
|
npm pack --pack-destination $env:RUNNER_TEMP
|
|
if ($LASTEXITCODE -ne 0) { throw 'SDK packaging failed' }
|
|
} finally { Pop-Location }
|
|
$consumer = Join-Path $env:RUNNER_TEMP 'cua-sdk-consumer'
|
|
New-Item -ItemType Directory -Path $consumer | Out-Null
|
|
Push-Location $consumer
|
|
try {
|
|
# @qwen-code/browser-use@<version> is published only by publish-browser,
|
|
# downstream of this job, so its tarball must be supplied locally.
|
|
$browserPkg = Join-Path $env:RUNNER_TEMP 'browser-use-package/qwen-code-browser-use-${{ steps.version.outputs.version }}.tgz'
|
|
npm install (Join-Path $env:RUNNER_TEMP 'qwen-code-cua-sdk-${{ steps.version.outputs.version }}.tgz') $browserPkg --ignore-scripts=false --no-audit --no-fund
|
|
if ($LASTEXITCODE -ne 0) { throw 'Windows SDK postinstall failed' }
|
|
$worker = Join-Path $env:ProgramFiles 'Qwen/CuaDriver/${{ steps.version.outputs.version }}/qwen-cua-driver-uia.exe'
|
|
if ($env:CUA_DRIVER_USE_UIACCESS -eq '1') {
|
|
if ((Get-AuthenticodeSignature -LiteralPath $worker).Status -ne 'Valid') {
|
|
throw 'Installed UIAccess worker must have a valid signature'
|
|
}
|
|
} elseif (Test-Path -LiteralPath $worker) {
|
|
throw 'Ordinary installation must not deploy the UIAccess worker'
|
|
}
|
|
} finally { Pop-Location }
|
|
- uses: 'actions/upload-artifact@v4'
|
|
with:
|
|
name: 'cua-driver-rs-windows-${{ matrix.arch }}'
|
|
path: 'packages/cua-driver/rust/release/cua-driver-rs-*-windows-${{ matrix.arch }}*.zip'
|
|
if-no-files-found: 'error'
|
|
|
|
# ── macOS (universal: lipo arm64 + x86_64) — SIGNED + NOTARIZED ──────────
|
|
build-macos:
|
|
name: 'macos-universal'
|
|
needs: ['validate-version']
|
|
runs-on: 'macos-26'
|
|
env:
|
|
# Sign+notarize on tag push (real release); on manual dispatch honor the
|
|
# `notarize` input so the pipeline can be iterated without Apple's notary.
|
|
DO_NOTARIZE: "${{ startsWith(github.ref, 'refs/tags/cua-driver-rs-v') || inputs.notarize == true }}"
|
|
steps:
|
|
- uses: 'actions/checkout@v4'
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
- 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/[email protected]'
|
|
with:
|
|
targets: 'aarch64-apple-darwin,x86_64-apple-darwin'
|
|
- uses: 'actions/cache@v4'
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
packages/cua-driver/rust/target
|
|
key: "cua-driver-darwin-universal-${{ hashFiles('packages/cua-driver/rust/Cargo.lock') }}"
|
|
restore-keys: |
|
|
cua-driver-darwin-universal-
|
|
- name: 'Build arm64'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
env:
|
|
MACOSX_DEPLOYMENT_TARGET: '13.0'
|
|
run: 'cargo build -p cua-driver -p cursor-theme-cli -p cua-driver-sdk --release --locked --target aarch64-apple-darwin'
|
|
- name: 'Build x86_64'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
env:
|
|
MACOSX_DEPLOYMENT_TARGET: '13.0'
|
|
run: 'cargo build -p cua-driver -p cursor-theme-cli -p cua-driver-sdk --release --locked --target x86_64-apple-darwin'
|
|
- name: 'Build Electron-compatible Node runtimes'
|
|
working-directory: 'packages/cua-driver'
|
|
run: |
|
|
corepack pnpm -C ../.. install --frozen-lockfile --ignore-scripts --prefer-offline
|
|
node scripts/build-node-runtime.mjs \
|
|
--target aarch64-apple-darwin \
|
|
--output rust/target/aarch64-apple-darwin/release/cua_driver_node_runtime.node
|
|
node scripts/build-node-runtime.mjs \
|
|
--target x86_64-apple-darwin \
|
|
--output rust/target/x86_64-apple-darwin/release/cua_driver_node_runtime.node
|
|
- name: 'Create universal binary (lipo)'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
run: |
|
|
mkdir -p release/universal
|
|
lipo -create \
|
|
target/aarch64-apple-darwin/release/qwen-cua-driver \
|
|
target/x86_64-apple-darwin/release/qwen-cua-driver \
|
|
-output release/universal/qwen-cua-driver
|
|
lipo -create \
|
|
target/aarch64-apple-darwin/release/cua-cursor-theme \
|
|
target/x86_64-apple-darwin/release/cua-cursor-theme \
|
|
-output release/universal/cua-cursor-theme
|
|
lipo -create \
|
|
target/aarch64-apple-darwin/release/libcua_driver_sdk.dylib \
|
|
target/x86_64-apple-darwin/release/libcua_driver_sdk.dylib \
|
|
-output release/universal/libcua_driver_sdk.dylib
|
|
lipo -create \
|
|
target/aarch64-apple-darwin/release/cua_driver_node_runtime.node \
|
|
target/x86_64-apple-darwin/release/cua_driver_node_runtime.node \
|
|
-output release/universal/cua_driver_node_runtime.node
|
|
lipo -info release/universal/qwen-cua-driver
|
|
lipo -info release/universal/cua-cursor-theme
|
|
lipo -info release/universal/libcua_driver_sdk.dylib
|
|
lipo -info release/universal/cua_driver_node_runtime.node
|
|
test "$(otool -D release/universal/libcua_driver_sdk.dylib | tail -n 1)" = \
|
|
'@rpath/libcua_driver_sdk.dylib'
|
|
otool -l release/universal/libcua_driver_sdk.dylib | \
|
|
grep -A2 LC_RPATH | grep -Fq 'path /usr/lib/swift'
|
|
otool -l release/universal/libcua_driver_sdk.dylib | \
|
|
grep -A2 LC_RPATH | grep -Fq 'path @loader_path'
|
|
- name: 'Import signing certificate + discover identity'
|
|
if: "env.DO_NOTARIZE == 'true'"
|
|
env:
|
|
MAC_CSC_LINK: '${{ secrets.MAC_CSC_LINK }}'
|
|
MAC_CSC_KEY_PASSWORD: '${{ secrets.MAC_CSC_KEY_PASSWORD }}'
|
|
run: |
|
|
KEYCHAIN_PASSWORD="temp-cd-keychain-pwd"
|
|
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 "$MAC_CSC_LINK" | base64 --decode > certificate.p12
|
|
security import certificate.p12 -k build.keychain -P "$MAC_CSC_KEY_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
|
|
# Auto-discover the Developer ID Application identity from the cert
|
|
# (avoids needing a separate DEVELOPER_NAME secret).
|
|
IDENTITY=$(security find-identity -v -p codesigning build.keychain \
|
|
| grep "Developer ID Application" | head -1 | sed -E 's/.*"(.*)"/\1/')
|
|
if [ -z "$IDENTITY" ]; then
|
|
echo "::error::No 'Developer ID Application' identity in MAC_CSC_LINK"
|
|
security find-identity -v -p codesigning build.keychain
|
|
exit 1
|
|
fi
|
|
echo "SIGN_IDENTITY=$IDENTITY" >> "$GITHUB_ENV"
|
|
echo "Signing identity: $IDENTITY"
|
|
- name: 'Codesign universal binary (hardened runtime)'
|
|
if: "env.DO_NOTARIZE == 'true'"
|
|
working-directory: 'packages/cua-driver/rust'
|
|
run: |
|
|
codesign --force --timestamp --options runtime \
|
|
--entitlements scripts/CuaDriver.entitlements \
|
|
--sign "$SIGN_IDENTITY" release/universal/qwen-cua-driver
|
|
codesign --force --timestamp --options runtime \
|
|
--sign "$SIGN_IDENTITY" release/universal/cua-cursor-theme
|
|
codesign --force --timestamp --options runtime \
|
|
--sign "$SIGN_IDENTITY" release/universal/libcua_driver_sdk.dylib
|
|
codesign --verify --strict --verbose=2 release/universal/qwen-cua-driver
|
|
codesign --verify --strict --verbose=2 release/universal/cua-cursor-theme
|
|
codesign --verify --strict --verbose=2 release/universal/libcua_driver_sdk.dylib
|
|
- name: 'Assemble QwenCuaDriver.app bundle'
|
|
working-directory: 'packages/cua-driver/rust'
|
|
run: |
|
|
VERSION="${{ steps.version.outputs.version }}"
|
|
mkdir -p release/QwenCuaDriver.app
|
|
cp -R scripts/CuaDriverBundle/Contents release/QwenCuaDriver.app/Contents
|
|
cp release/universal/qwen-cua-driver release/QwenCuaDriver.app/Contents/MacOS/qwen-cua-driver
|
|
cp release/universal/cua-cursor-theme release/QwenCuaDriver.app/Contents/MacOS/cua-cursor-theme
|
|
chmod +x release/QwenCuaDriver.app/Contents/MacOS/qwen-cua-driver
|
|
chmod +x release/QwenCuaDriver.app/Contents/MacOS/cua-cursor-theme
|
|
BUNDLE_VERSION="${VERSION%%-*}"
|
|
plutil -replace CFBundleShortVersionString -string "$BUNDLE_VERSION" release/QwenCuaDriver.app/Contents/Info.plist
|
|
plutil -replace CFBundleVersion -string "$BUNDLE_VERSION" release/QwenCuaDriver.app/Contents/Info.plist
|
|
rm -f release/QwenCuaDriver.app/Contents/MacOS/.gitkeep
|
|
plutil -p release/QwenCuaDriver.app/Contents/Info.plist | grep -E 'CFBundle(Short)?Version'
|
|
- name:
|
|
'Codesign + notarize + staple QwenCuaDriver.app (App Store Connect API
|
|
key)'
|
|
if: "env.DO_NOTARIZE == 'true'"
|
|
working-directory: 'packages/cua-driver/rust'
|
|
env:
|
|
APPLE_NOTARY_API_KEY_P8_BASE64: '${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}'
|
|
APPLE_NOTARY_KEY_ID: '${{ secrets.APPLE_NOTARY_KEY_ID }}'
|
|
APPLE_NOTARY_ISSUER_ID: '${{ secrets.APPLE_NOTARY_ISSUER_ID }}'
|
|
run: |
|
|
codesign --force --deep --timestamp --options runtime \
|
|
--entitlements scripts/CuaDriver.entitlements \
|
|
--sign "$SIGN_IDENTITY" release/QwenCuaDriver.app
|
|
codesign --verify --strict --verbose=2 release/QwenCuaDriver.app
|
|
|
|
# notarytool with App Store Connect API key (.p8). ditto a zip,
|
|
# submit, wait, then staple the ticket into the bundle.
|
|
echo "$APPLE_NOTARY_API_KEY_P8_BASE64" | base64 --decode > notary_key.p8
|
|
/usr/bin/ditto -c -k --keepParent release/QwenCuaDriver.app release/QwenCuaDriver.app.zip
|
|
xcrun notarytool submit release/QwenCuaDriver.app.zip \
|
|
--key notary_key.p8 \
|
|
--key-id "$APPLE_NOTARY_KEY_ID" \
|
|
--issuer "$APPLE_NOTARY_ISSUER_ID" \
|
|
--wait --timeout 20m
|
|
xcrun stapler staple release/QwenCuaDriver.app
|
|
spctl -a -vv -t exec release/QwenCuaDriver.app
|
|
rm -f release/QwenCuaDriver.app.zip notary_key.p8
|
|
- name: 'Package'
|
|
working-directory: 'packages/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 slice in all three named tarballs (matches the
|
|
# upstream convention so arch-keyed downloaders still work).
|
|
cp release/universal/qwen-cua-driver "release/${STAGE}/"
|
|
cp release/universal/cua-cursor-theme "release/${STAGE}/"
|
|
cp release/universal/libcua_driver_sdk.dylib "release/${STAGE}/"
|
|
cp release/universal/cua_driver_node_runtime.node "release/${STAGE}/"
|
|
cp include/cua_driver_abi.h "release/${STAGE}/"
|
|
cp -R release/QwenCuaDriver.app "release/${STAGE}/QwenCuaDriver.app"
|
|
cp ../../../LICENSE "release/${STAGE}/LICENSE"
|
|
(cd release && tar -czf "${STAGE}.tar.gz" "${STAGE}")
|
|
done
|
|
tar -czf "release/cua-driver-rs-${VERSION}-darwin-universal-binary.tar.gz" \
|
|
-C release/universal qwen-cua-driver cua-cursor-theme \
|
|
libcua_driver_sdk.dylib cua_driver_node_runtime.node \
|
|
-C ../../include cua_driver_abi.h
|
|
ls -lh release/cua-driver-rs-*-darwin-*.tar.gz
|
|
- uses: 'actions/upload-artifact@v4'
|
|
with:
|
|
name: 'cua-driver-rs-macos'
|
|
path: 'packages/cua-driver/rust/release/cua-driver-rs-*-darwin-*.tar.gz'
|
|
if-no-files-found: 'error'
|
|
|
|
verify-release-artifacts:
|
|
name: 'release artifact contract'
|
|
needs: ['build-linux', 'build-windows', 'build-macos']
|
|
runs-on: 'ubuntu-latest'
|
|
steps:
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
path: 'artifacts'
|
|
- name: 'Verify installer and SDK payloads'
|
|
shell: 'bash'
|
|
run: |
|
|
set -euo pipefail
|
|
mapfile -t archives < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' \) | sort)
|
|
[[ ${#archives[@]} -eq 12 ]] || {
|
|
printf 'expected 12 release archives, found %s\n' "${#archives[@]}" >&2
|
|
printf '%s\n' "${archives[@]}" >&2
|
|
exit 1
|
|
}
|
|
for archive in "${archives[@]}"; do
|
|
case "$archive" in
|
|
*.tar.gz) members=$(tar -tzf "$archive") ;;
|
|
*.zip) members=$(unzip -Z1 "$archive") ;;
|
|
esac
|
|
grep -Eq '(^|/)qwen-cua-driver(\.exe)?$' <<<"$members"
|
|
grep -Eq '(^|/)cua-cursor-theme(\.exe)?$' <<<"$members"
|
|
grep -Eq '(^|/)cua_driver_abi\.h$' <<<"$members"
|
|
grep -Eq '(^|/)cua_driver_node_runtime\.node$' <<<"$members"
|
|
case "$archive" in
|
|
*windows*)
|
|
grep -Eq '(^|/)qwen-cua-driver-uia\.exe$' <<<"$members"
|
|
grep -Eq '(^|/)cua_driver_sdk\.dll$' <<<"$members"
|
|
;;
|
|
*darwin*) grep -Eq '(^|/)libcua_driver_sdk\.dylib$' <<<"$members" ;;
|
|
*linux*) grep -Eq '(^|/)libcua_driver_sdk\.so$' <<<"$members" ;;
|
|
esac
|
|
done
|
|
|
|
verify-sdk-package:
|
|
name: '@qwen-code/cua-sdk package dry-run'
|
|
needs:
|
|
[
|
|
'validate-version',
|
|
'build-linux',
|
|
'verify-release-artifacts',
|
|
'verify-browser-package',
|
|
]
|
|
runs-on: 'ubuntu-latest'
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
- uses: 'actions/checkout@v4'
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
scope: '@qwen-code'
|
|
- name: 'Install npm 11'
|
|
run: 'npm install --global [email protected]'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'cua-driver-rs-linux-x86_64'
|
|
path: '${{ runner.temp }}/cua-driver-linux'
|
|
- name: 'Extract verified native payload'
|
|
id: 'native'
|
|
shell: 'bash'
|
|
run: |
|
|
set -euo pipefail
|
|
ARCHIVE=$(find "${RUNNER_TEMP}/cua-driver-linux" -type f -name '*-binary.tar.gz' -print -quit)
|
|
if [[ -z "$ARCHIVE" ]]; then
|
|
echo '::error::Linux x86_64 binary archive is missing'
|
|
exit 1
|
|
fi
|
|
NATIVE_DIR="${RUNNER_TEMP}/cua-sdk-native"
|
|
RELEASE_DIR="${RUNNER_TEMP}/cua-sdk-release-fixture"
|
|
mkdir -p "$NATIVE_DIR"
|
|
mkdir -p "$RELEASE_DIR"
|
|
tar -xzf "$ARCHIVE" -C "$NATIVE_DIR"
|
|
test -f "$NATIVE_DIR/libcua_driver_sdk.so"
|
|
test -f "$NATIVE_DIR/cua_driver_node_runtime.node"
|
|
cp "$ARCHIVE" "$RELEASE_DIR/"
|
|
(cd "$RELEASE_DIR" && sha256sum "$(basename "$ARCHIVE")" > checksums.txt)
|
|
echo "directory=$NATIVE_DIR" >> "$GITHUB_OUTPUT"
|
|
echo "release_directory=$RELEASE_DIR" >> "$GITHUB_OUTPUT"
|
|
- name: 'Install SDK workspace dependencies'
|
|
run: |
|
|
corepack pnpm install --frozen-lockfile --ignore-scripts --prefer-offline
|
|
npm run build --workspace @qwen-code/browser-use
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'browser-use-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/browser-use-package'
|
|
- name: 'Test SDK and native loader'
|
|
working-directory: 'packages/cua-driver/typescript'
|
|
env:
|
|
CUA_DRIVER_REQUIRE_UNIFFI: '1'
|
|
QWEN_CUA_SDK_NATIVE_DIR: '${{ steps.native.outputs.directory }}'
|
|
run: 'npm test'
|
|
- name: 'Pack, publish-dry-run, and clean-install one SDK tarball'
|
|
env:
|
|
QWEN_CUA_SDK_NATIVE_DIR: '${{ steps.native.outputs.directory }}'
|
|
run: |
|
|
node packages/cua-driver/scripts/verify-cua-sdk-package.mjs \
|
|
--browser-tarball "${RUNNER_TEMP}/browser-use-package/qwen-code-browser-use-${{ needs.validate-version.outputs.version }}.tgz" \
|
|
--native-dir "${QWEN_CUA_SDK_NATIVE_DIR}" \
|
|
--release-dir "${{ steps.native.outputs.release_directory }}" \
|
|
--output-dir "${RUNNER_TEMP}/cua-sdk-package"
|
|
- uses: 'actions/upload-artifact@v4'
|
|
with:
|
|
name: 'cua-sdk-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/cua-sdk-package/*.tgz'
|
|
if-no-files-found: 'error'
|
|
retention-days: 7
|
|
|
|
verify-browser-package:
|
|
name: '@qwen-code/browser-use package dry-run'
|
|
needs: ['validate-version']
|
|
runs-on: 'ubuntu-latest'
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
- uses: 'actions/checkout@v4'
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
- name: 'Install workspace dependencies'
|
|
run: 'corepack pnpm install --frozen-lockfile --ignore-scripts --prefer-offline'
|
|
- name: 'Test and pack existing browser SDK'
|
|
run: |
|
|
npm run test --workspace @qwen-code/browser-use
|
|
node packages/browser-use/scripts/verify-package.mjs --output-dir "${RUNNER_TEMP}/browser-use-package"
|
|
- uses: 'actions/upload-artifact@v4'
|
|
with:
|
|
name: 'browser-use-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/browser-use-package/*.tgz'
|
|
if-no-files-found: 'error'
|
|
retention-days: 7
|
|
|
|
verify-node-repl-package:
|
|
name: '@qwen-code/node-repl-mcp package dry-run'
|
|
needs:
|
|
[
|
|
'validate-version',
|
|
'verify-sdk-package',
|
|
'verify-browser-package',
|
|
'build-linux',
|
|
]
|
|
runs-on: 'ubuntu-latest'
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
- uses: 'actions/checkout@v4'
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
scope: '@qwen-code'
|
|
- name: 'Install npm 11'
|
|
run: 'npm install --global [email protected]'
|
|
- name: 'Install workspace dependencies'
|
|
run: 'corepack pnpm install --frozen-lockfile --ignore-scripts --prefer-offline --reporter=append-only'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'cua-sdk-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/cua-sdk-package'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'browser-use-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/browser-use-package'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'cua-driver-rs-linux-x86_64'
|
|
path: '${{ runner.temp }}/cua-driver-linux'
|
|
- name: 'Prepare matching native payload and SDK declarations'
|
|
run: |
|
|
mkdir -p "${RUNNER_TEMP}/native"
|
|
tar -xzf "${RUNNER_TEMP}/cua-driver-linux/cua-driver-rs-${{ needs.validate-version.outputs.version }}-linux-x86_64-binary.tar.gz" -C "${RUNNER_TEMP}/native"
|
|
npm run build --workspace @qwen-code/browser-use
|
|
npm run build --workspace @qwen-code/cua-sdk
|
|
- name: 'Test standalone Node REPL package'
|
|
working-directory: 'packages/node-repl'
|
|
run: |
|
|
npm run build
|
|
npm run typecheck
|
|
npm test
|
|
npm run smoke
|
|
npm run smoke:mcp
|
|
npm run smoke:lifecycle
|
|
- name: 'Pack, publish-dry-run, and clean-install Node REPL tarball'
|
|
env:
|
|
QWEN_CUA_SDK_NATIVE_DIR: '${{ runner.temp }}/native'
|
|
run: |
|
|
node packages/node-repl/scripts/verify-package.mjs \
|
|
--sdk-tarball "${RUNNER_TEMP}/cua-sdk-package/qwen-code-cua-sdk-${{ needs.validate-version.outputs.version }}.tgz" \
|
|
--browser-tarball "${RUNNER_TEMP}/browser-use-package/qwen-code-browser-use-${{ needs.validate-version.outputs.version }}.tgz" \
|
|
--output-dir "${RUNNER_TEMP}/node-repl-package"
|
|
- uses: 'actions/upload-artifact@v4'
|
|
with:
|
|
name: 'node-repl-mcp-npm-${{
|
|
needs.validate-version.outputs.node_repl_version }}'
|
|
path: '${{ runner.temp }}/node-repl-package/*.tgz'
|
|
if-no-files-found: 'error'
|
|
retention-days: 7
|
|
|
|
# ── Release ──────────────────────────────────────────────────────────────
|
|
release:
|
|
name: 'Create GitHub Release'
|
|
needs:
|
|
[
|
|
'build-linux',
|
|
'build-windows',
|
|
'build-macos',
|
|
'verify-release-artifacts',
|
|
'verify-sdk-package',
|
|
'verify-browser-package',
|
|
'verify-node-repl-package',
|
|
]
|
|
runs-on: 'ubuntu-latest'
|
|
outputs:
|
|
version: '${{ steps.version.outputs.version }}'
|
|
if:
|
|
"startsWith(github.ref, 'refs/tags/cua-driver-rs-v') || (github.event_name
|
|
== 'workflow_dispatch' && inputs.dry_run == false)"
|
|
steps:
|
|
- uses: 'actions/checkout@v4'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
path: 'release-upload'
|
|
pattern: 'cua-driver-rs-*'
|
|
merge-multiple: true
|
|
- name: 'Determine version + tag'
|
|
id: 'version'
|
|
shell: 'bash'
|
|
run: |
|
|
if [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
|
|
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
|
|
TAG="${GITHUB_REF#refs/tags/}"
|
|
else
|
|
VERSION="${{ inputs.version }}"
|
|
TAG="cua-driver-rs-v${VERSION}"
|
|
fi
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
- name: 'Stage installers and skill pack'
|
|
shell: 'bash'
|
|
run: |
|
|
VERSION="${{ steps.version.outputs.version }}"
|
|
cp packages/cua-driver/scripts/install.sh release-upload/install.sh
|
|
cp packages/cua-driver/scripts/install.ps1 release-upload/install.ps1
|
|
cp packages/cua-driver/scripts/_install-rust.sh release-upload/_install-rust.sh
|
|
cp packages/cua-driver/scripts/uninstall.sh release-upload/uninstall.sh
|
|
cp packages/cua-driver/scripts/uninstall.ps1 release-upload/uninstall.ps1
|
|
SKILLS_STAGE="cua-driver-rs-v${VERSION}-skills"
|
|
mkdir -p "$SKILLS_STAGE"
|
|
cp -R packages/cua-driver/rust/Skills/cua-driver/. "$SKILLS_STAGE/"
|
|
tar -czf "release-upload/${SKILLS_STAGE}.tar.gz" "$SKILLS_STAGE"
|
|
(
|
|
cd release-upload
|
|
: > checksums.txt
|
|
for file in *; do
|
|
[[ -f "$file" && "$file" != checksums.txt ]] || continue
|
|
sha256sum "$file" >> checksums.txt
|
|
done
|
|
)
|
|
- name: 'Refuse to replace an existing release'
|
|
id: 'release-state'
|
|
env:
|
|
GH_TOKEN: '${{ github.token }}'
|
|
TAG: '${{ steps.version.outputs.tag }}'
|
|
shell: 'bash'
|
|
run: |
|
|
set -euo pipefail
|
|
if ! gh release view "$TAG" >/dev/null 2>&1; then
|
|
echo 'exists=false' >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
git fetch origin "refs/tags/${TAG}:refs/tags/${TAG}"
|
|
TAG_COMMIT=$(git rev-list -n 1 "$TAG")
|
|
if [[ "$TAG_COMMIT" != "$GITHUB_SHA" ]]; then
|
|
echo "::error::Existing $TAG points to $TAG_COMMIT, not $GITHUB_SHA"
|
|
exit 1
|
|
fi
|
|
|
|
EXISTING=$(mktemp -d)
|
|
trap 'rm -rf "$EXISTING"' EXIT
|
|
gh release download "$TAG" --dir "$EXISTING"
|
|
find release-upload -maxdepth 1 -type f -exec basename {} \; | sort > "$EXISTING/expected-assets.txt"
|
|
gh release view "$TAG" --json assets --jq '.assets[].name' | sort > "$EXISTING/actual-assets.txt"
|
|
if ! diff -u "$EXISTING/expected-assets.txt" "$EXISTING/actual-assets.txt"; then
|
|
echo "::error::Existing $TAG asset set is incomplete or unexpected"
|
|
exit 1
|
|
fi
|
|
grep -v '^checksums.txt$' "$EXISTING/expected-assets.txt" > "$EXISTING/expected-checksums.txt"
|
|
sed -nE 's/^[0-9a-f]{64} [ *](.+)$/\1/p' "$EXISTING/checksums.txt" | sort > "$EXISTING/actual-checksums.txt"
|
|
if ! diff -u "$EXISTING/expected-checksums.txt" "$EXISTING/actual-checksums.txt"; then
|
|
echo "::error::Existing $TAG checksum manifest is incomplete or unexpected"
|
|
exit 1
|
|
fi
|
|
(cd "$EXISTING" && sha256sum --check checksums.txt)
|
|
echo "Existing $TAG is complete and checksum-valid; reusing it without replacement"
|
|
echo 'exists=true' >> "$GITHUB_OUTPUT"
|
|
- name: 'Create GitHub Release'
|
|
if: "steps.release-state.outputs.exists != 'true'"
|
|
uses: 'softprops/action-gh-release@v2'
|
|
with:
|
|
tag_name: '${{ steps.version.outputs.tag }}'
|
|
target_commitish: '${{ github.sha }}'
|
|
name: 'cua-driver-rs v${{ steps.version.outputs.version }}'
|
|
files: 'release-upload/**/*'
|
|
prerelease: true
|
|
generate_release_notes: true
|
|
body: |
|
|
Qwen CUA Driver prebuilt binaries
|
|
(vendored under `packages/cua-driver`).
|
|
|
|
- **macOS**: codesigned + notarized universal binary + `QwenCuaDriver.app`
|
|
- **Linux**: unsigned (x86_64 + arm64, glibc 2.31 floor)
|
|
- **Windows**: unsigned UIAccess worker + native SDK payload
|
|
(x86_64 + arm64); deployments must sign and trust the worker
|
|
on the target machine before UIAccess activation
|
|
- **Node.js**: the same workflow publishes `@qwen-code/cua-sdk`
|
|
after a clean install against these release assets and publishes
|
|
the same-version browser SDK and `@qwen-code/node-repl-mcp` package
|
|
after its MCP protocol smoke tests
|
|
|
|
Enable relative coordinates: `CUA_DRIVER_RS_COORDINATE_SPACE=1`
|
|
(default `0` = off; optional `CUA_DRIVER_RS_COORDINATE_SCALE=1000`).
|
|
|
|
Enable textual MCP payload filtering for affected model API routes:
|
|
`MCP_MODEL_PAYLOAD_FILTER=1` (default off).
|
|
|
|
publish-sdk:
|
|
name: 'Publish @qwen-code/cua-sdk'
|
|
needs:
|
|
['validate-version', 'verify-sdk-package', 'release', 'publish-browser']
|
|
if:
|
|
"startsWith(github.ref, 'refs/tags/cua-driver-rs-v') || (github.event_name
|
|
== 'workflow_dispatch' && inputs.dry_run == false)"
|
|
runs-on: 'ubuntu-latest'
|
|
environment:
|
|
name: 'production-release'
|
|
url: 'https://www.npmjs.com/package/@qwen-code/cua-sdk/v/${{ needs.validate-version.outputs.version }}'
|
|
permissions:
|
|
contents: 'read'
|
|
id-token: 'write'
|
|
steps:
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
scope: '@qwen-code'
|
|
- name: 'Install npm 11'
|
|
run: 'npm install --global [email protected]'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'cua-sdk-npm-${{ needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/cua-sdk-package'
|
|
- name: 'Clean-install against the published GitHub Release'
|
|
env:
|
|
QWEN_CUA_SDK_CACHE_DIR: '${{ runner.temp }}/public-release-cache'
|
|
VERSION: '${{ needs.validate-version.outputs.version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/cua-sdk-package" -type f -name '*.tgz' -print -quit)
|
|
if [[ -z "$TARBALL" ]]; then
|
|
echo '::error::Packed @qwen-code/cua-sdk tarball is missing'
|
|
exit 1
|
|
fi
|
|
CHECKSUMS_URL="https://github.com/QwenLM/qwen-code/releases/download/cua-driver-rs-v${VERSION}/checksums.txt"
|
|
for attempt in {1..12}; do
|
|
if curl -fsSL --connect-timeout 15 --max-time 60 "$CHECKSUMS_URL" -o "${RUNNER_TEMP}/checksums.txt"; then
|
|
break
|
|
fi
|
|
if [[ "$attempt" == 12 ]]; then
|
|
echo '::error::Published release checksums did not become available'
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|
|
CONSUMER=$(mktemp -d)
|
|
trap 'rm -rf "$CONSUMER"' EXIT
|
|
cd "$CONSUMER"
|
|
npm init --yes >/dev/null
|
|
npm install "$TARBALL" --no-audit --no-fund
|
|
node --input-type=module --eval '
|
|
import { CuaDriver } from "@qwen-code/cua-sdk";
|
|
import { ComputerUse } from "@qwen-code/cua-sdk/computer-use";
|
|
const driver = CuaDriver.create(undefined);
|
|
const computer = await ComputerUse.create({ session: "release-registry-smoke" });
|
|
try {
|
|
const listing = JSON.parse(await driver.listToolsJson());
|
|
if (!Array.isArray(listing.tools) || listing.tools.length === 0) {
|
|
throw new Error("native SDK returned no tools");
|
|
}
|
|
let safeRefusal = false;
|
|
try {
|
|
await computer.appCall("click", { scope: "desktop", x: 1, y: 1 });
|
|
} catch (error) {
|
|
if (error.code !== "screenshot_required" || error.sent !== false) throw error;
|
|
safeRefusal = true;
|
|
}
|
|
if (!safeRefusal || listing.tools.length !== 19) throw new Error("Computer Use session/manifest contract mismatch");
|
|
console.log(JSON.stringify({
|
|
toolCount: listing.tools.length,
|
|
safeRefusal,
|
|
}));
|
|
} finally {
|
|
await computer.close();
|
|
await driver.shutdown();
|
|
driver.uniffiDestroy();
|
|
}
|
|
'
|
|
- name: 'Publish immutable SDK tarball'
|
|
env:
|
|
VERSION: '${{ needs.validate-version.outputs.version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/cua-sdk-package" -type f -name '*.tgz' -print -quit)
|
|
LOCAL_INTEGRITY="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
|
|
REMOTE_INTEGRITY=$(npm view "@qwen-code/cua-sdk@${VERSION}" dist.integrity 2>/dev/null || true)
|
|
if [[ -n "$REMOTE_INTEGRITY" ]]; then
|
|
if [[ "$REMOTE_INTEGRITY" != "$LOCAL_INTEGRITY" ]]; then
|
|
echo "::error::@qwen-code/cua-sdk@${VERSION} already exists with different integrity"
|
|
exit 1
|
|
fi
|
|
echo "@qwen-code/cua-sdk@${VERSION} already contains this exact tarball"
|
|
exit 0
|
|
fi
|
|
NPM_TAG=latest
|
|
[[ "$VERSION" == *-* ]] && NPM_TAG=next
|
|
npm publish "$TARBALL" --provenance --access public --tag "$NPM_TAG"
|
|
- name: 'Verify npm registry integrity'
|
|
env:
|
|
VERSION: '${{ needs.validate-version.outputs.version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/cua-sdk-package" -type f -name '*.tgz' -print -quit)
|
|
LOCAL_INTEGRITY="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
|
|
for attempt in {1..20}; do
|
|
REMOTE_INTEGRITY=$(npm view "@qwen-code/cua-sdk@${VERSION}" dist.integrity 2>/dev/null || true)
|
|
if [[ "$REMOTE_INTEGRITY" == "$LOCAL_INTEGRITY" ]]; then
|
|
echo "Verified npm registry integrity on attempt $attempt"
|
|
exit 0
|
|
fi
|
|
if [[ -n "$REMOTE_INTEGRITY" ]]; then
|
|
echo '::error::Published npm artifact integrity does not match the verified tarball'
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|
|
echo '::error::Published npm version did not become visible'
|
|
exit 1
|
|
|
|
publish-browser:
|
|
name: 'Publish @qwen-code/browser-use'
|
|
needs: ['validate-version', 'verify-browser-package', 'release']
|
|
if: "startsWith(github.ref, 'refs/tags/cua-driver-rs-v') || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false)"
|
|
runs-on: 'ubuntu-latest'
|
|
environment:
|
|
name: 'production-release'
|
|
url: 'https://www.npmjs.com/package/@qwen-code/browser-use/v/${{
|
|
needs.validate-version.outputs.version }}'
|
|
permissions:
|
|
contents: 'read'
|
|
id-token: 'write'
|
|
steps:
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
scope: '@qwen-code'
|
|
- name: 'Install npm 11'
|
|
run: 'npm install --global [email protected]'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'browser-use-npm-${{
|
|
needs.validate-version.outputs.version }}'
|
|
path: '${{ runner.temp }}/browser-use-package'
|
|
- name: 'Publish immutable Browser SDK tarball'
|
|
env:
|
|
VERSION: '${{ needs.validate-version.outputs.version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/browser-use-package" -type f -name '*.tgz' -print -quit)
|
|
if [[ -z "$TARBALL" ]]; then
|
|
echo '::error::Packed @qwen-code/browser-use tarball is missing'
|
|
exit 1
|
|
fi
|
|
LOCAL_INTEGRITY="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
|
|
REMOTE_INTEGRITY=$(npm view "@qwen-code/browser-use@${VERSION}" dist.integrity 2>/dev/null || true)
|
|
if [[ -n "$REMOTE_INTEGRITY" ]]; then
|
|
if [[ "$REMOTE_INTEGRITY" != "$LOCAL_INTEGRITY" ]]; then
|
|
echo "::error::@qwen-code/browser-use@${VERSION} already exists with different integrity"
|
|
exit 1
|
|
fi
|
|
echo "@qwen-code/browser-use@${VERSION} already contains this exact tarball"
|
|
exit 0
|
|
fi
|
|
NPM_TAG=latest
|
|
[[ "$VERSION" == *-* ]] && NPM_TAG=next
|
|
npm publish "$TARBALL" --provenance --access public --tag "$NPM_TAG"
|
|
- name: 'Verify npm registry integrity'
|
|
env:
|
|
VERSION: '${{ needs.validate-version.outputs.version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/browser-use-package" -type f -name '*.tgz' -print -quit)
|
|
LOCAL_INTEGRITY="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
|
|
for attempt in {1..20}; do
|
|
REMOTE_INTEGRITY=$(npm view "@qwen-code/browser-use@${VERSION}" dist.integrity 2>/dev/null || true)
|
|
if [[ "$REMOTE_INTEGRITY" == "$LOCAL_INTEGRITY" ]]; then
|
|
echo "Verified npm registry integrity on attempt $attempt"
|
|
exit 0
|
|
fi
|
|
if [[ -n "$REMOTE_INTEGRITY" ]]; then
|
|
echo '::error::Published npm artifact integrity does not match the verified tarball'
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|
|
echo '::error::Published npm version did not become visible'
|
|
exit 1
|
|
|
|
publish-node-repl:
|
|
name: 'Publish @qwen-code/node-repl-mcp'
|
|
needs:
|
|
['validate-version', 'verify-node-repl-package', 'release', 'publish-sdk']
|
|
if: "startsWith(github.ref, 'refs/tags/cua-driver-rs-v') || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false)"
|
|
runs-on: 'ubuntu-latest'
|
|
environment:
|
|
name: 'production-release'
|
|
url: 'https://www.npmjs.com/package/@qwen-code/node-repl-mcp/v/${{
|
|
needs.validate-version.outputs.node_repl_version }}'
|
|
permissions:
|
|
contents: 'read'
|
|
id-token: 'write'
|
|
steps:
|
|
- uses: 'actions/setup-node@v4'
|
|
with:
|
|
node-version: '22'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
scope: '@qwen-code'
|
|
- name: 'Install npm 11'
|
|
run: 'npm install --global [email protected]'
|
|
- uses: 'actions/download-artifact@v4'
|
|
with:
|
|
name: 'node-repl-mcp-npm-${{
|
|
needs.validate-version.outputs.node_repl_version }}'
|
|
path: '${{ runner.temp }}/node-repl-package'
|
|
- name: 'Publish immutable Node REPL tarball'
|
|
env:
|
|
VERSION: '${{ needs.validate-version.outputs.node_repl_version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/node-repl-package" -type f -name '*.tgz' -print -quit)
|
|
if [[ -z "$TARBALL" ]]; then
|
|
echo '::error::Packed @qwen-code/node-repl-mcp tarball is missing'
|
|
exit 1
|
|
fi
|
|
LOCAL_INTEGRITY="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
|
|
REMOTE_INTEGRITY=$(npm view "@qwen-code/node-repl-mcp@${VERSION}" dist.integrity 2>/dev/null || true)
|
|
if [[ -n "$REMOTE_INTEGRITY" ]]; then
|
|
if [[ "$REMOTE_INTEGRITY" != "$LOCAL_INTEGRITY" ]]; then
|
|
echo "::error::@qwen-code/node-repl-mcp@${VERSION} already exists with different integrity"
|
|
exit 1
|
|
fi
|
|
echo "@qwen-code/node-repl-mcp@${VERSION} already contains this exact tarball"
|
|
exit 0
|
|
fi
|
|
NPM_TAG=latest
|
|
[[ "$VERSION" == *-* ]] && NPM_TAG=next
|
|
npm publish "$TARBALL" --provenance --access public --tag "$NPM_TAG"
|
|
- name: 'Verify npm registry integrity'
|
|
env:
|
|
VERSION: '${{ needs.validate-version.outputs.node_repl_version }}'
|
|
run: |
|
|
set -euo pipefail
|
|
TARBALL=$(find "${RUNNER_TEMP}/node-repl-package" -type f -name '*.tgz' -print -quit)
|
|
LOCAL_INTEGRITY="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
|
|
for attempt in {1..20}; do
|
|
REMOTE_INTEGRITY=$(npm view "@qwen-code/node-repl-mcp@${VERSION}" dist.integrity 2>/dev/null || true)
|
|
if [[ "$REMOTE_INTEGRITY" == "$LOCAL_INTEGRITY" ]]; then
|
|
echo "Verified npm registry integrity on attempt $attempt"
|
|
exit 0
|
|
fi
|
|
if [[ -n "$REMOTE_INTEGRITY" ]]; then
|
|
echo '::error::Published npm artifact integrity does not match the verified tarball'
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|
|
echo '::error::Published npm version did not become visible'
|
|
exit 1
|