chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
!libs/lume/scripts/build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Scripts
server/scripts/
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Conda
.conda/
# Local environment
.env.local
# macOS DS_Store
.DS_Store
weights/
weights/icon_detect/
weights/icon_detect/model.pt
weights/icon_detect/model.pt.zip
weights/icon_detect/model.pt.zip.part*
libs/python/omniparser/weights/icon_detect/model.pt
/screenshots/
/experiments/
/logs/
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm/
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Ignore folder
ignore
# .release
.release/
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = true
[*.{js,ts,jsx,tsx,json,css,scss,html,md}]
indent_size = 2
+2
View File
@@ -0,0 +1,2 @@
* text=auto
*.sh text eol=lf
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: trycua
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Verifies that the version in pyproject.toml matches the expected version.
Usage:
python get_pyproject_version.py <pyproject_path> <expected_version>
Exit codes:
0 - Versions match
1 - Versions don't match or error occurred
"""
import sys
try:
import tomllib
except ImportError:
# Fallback for Python < 3.11
import toml as tomllib
def main():
if len(sys.argv) != 3:
print(
"Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
file=sys.stderr,
)
sys.exit(1)
pyproject_path = sys.argv[1]
expected_version = sys.argv[2]
# tomllib requires binary mode
try:
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
except FileNotFoundError:
print(f"❌ ERROR: File not found: {pyproject_path}", file=sys.stderr)
sys.exit(1)
except Exception as e:
# Fallback to toml if using the old library or handle other errors
try:
import toml
data = toml.load(pyproject_path)
except FileNotFoundError:
print(f"❌ ERROR: File not found: {pyproject_path}", file=sys.stderr)
sys.exit(1)
except Exception as toml_err:
print(f"❌ ERROR: Failed to parse TOML file: {e}", file=sys.stderr)
sys.exit(1)
actual_version = data.get("project", {}).get("version")
if not actual_version:
print("❌ ERROR: No version found in pyproject.toml", file=sys.stderr)
sys.exit(1)
if actual_version != expected_version:
print("❌ Version mismatch detected!", file=sys.stderr)
print(f" pyproject.toml version: {actual_version}", file=sys.stderr)
print(f" Expected version: {expected_version}", file=sys.stderr)
print("", file=sys.stderr)
print(
"The version in pyproject.toml must match the version being published.", file=sys.stderr
)
print(
f"Please update pyproject.toml to version {expected_version} or use the correct tag.",
file=sys.stderr,
)
sys.exit(1)
print(f"✅ Version consistency check passed: {actual_version}")
sys.exit(0)
if __name__ == "__main__":
main()
+137
View File
@@ -0,0 +1,137 @@
# Tests for .github/scripts
This directory contains comprehensive tests for the GitHub workflow scripts using Python's built-in testing framework.
## Requirements
**No external dependencies required!**
This test suite uses:
- `unittest` - Python's built-in testing framework
- `tomllib` - Python 3.11+ built-in TOML parser
For Python < 3.11, the `toml` package is used as a fallback.
## Running Tests
### Run all tests
```bash
cd .github/scripts/tests
python3 -m unittest discover -v
```
### Run a specific test file
```bash
python3 -m unittest test_get_pyproject_version -v
```
### Run a specific test class
```bash
python3 -m unittest test_get_pyproject_version.TestGetPyprojectVersion -v
```
### Run a specific test method
```bash
python3 -m unittest test_get_pyproject_version.TestGetPyprojectVersion.test_matching_versions -v
```
### Run tests directly from the test file
```bash
python3 test_get_pyproject_version.py
```
## Test Structure
### test_get_pyproject_version.py
Comprehensive tests for `get_pyproject_version.py` covering:
-**Version matching**: Tests successful version validation
-**Version mismatch**: Tests error handling when versions don't match
-**Missing version**: Tests handling of pyproject.toml without version field
-**Missing project section**: Tests handling of pyproject.toml without project section
-**File not found**: Tests handling of non-existent files
-**Malformed TOML**: Tests handling of invalid TOML syntax
-**Argument validation**: Tests proper argument count validation
-**Semantic versioning**: Tests various semantic version formats
-**Pre-release tags**: Tests versions with alpha, beta, rc tags
-**Build metadata**: Tests versions with build metadata
-**Edge cases**: Tests empty versions and other edge cases
**Total Tests**: 17+ test cases covering all functionality
## Best Practices Implemented
1. **Fixture Management**: Uses `setUp()` and `tearDown()` for clean test isolation
2. **Helper Methods**: Provides reusable helpers for creating test fixtures
3. **Temporary Files**: Uses `tempfile` for file creation with proper cleanup
4. **Comprehensive Coverage**: Tests happy paths, error conditions, and edge cases
5. **Clear Documentation**: Each test has a descriptive docstring
6. **Output Capture**: Uses `unittest.mock.patch` and `StringIO` to test stdout/stderr
7. **Exit Code Validation**: Properly tests script exit codes with `assertRaises(SystemExit)`
8. **Type Hints**: Uses type hints in helper methods for clarity
9. **PEP 8 Compliance**: Follows Python style guidelines
10. **Zero External Dependencies**: Uses only Python standard library
## Continuous Integration
These tests can be integrated into GitHub Actions workflows with no additional dependencies:
```yaml
- name: Run .github scripts tests
run: |
cd .github/scripts/tests
python3 -m unittest discover -v
```
## Test Output Example
```
test_empty_version_string (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of empty version string. ... ok
test_file_not_found (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of non-existent pyproject.toml file. ... ok
test_malformed_toml (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of malformed TOML file. ... ok
test_matching_versions (test_get_pyproject_version.TestGetPyprojectVersion)
Test that matching versions result in success. ... ok
test_missing_project_section (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of pyproject.toml without a project section. ... ok
test_missing_version_field (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of pyproject.toml without a version field. ... ok
test_no_arguments (test_get_pyproject_version.TestGetPyprojectVersion)
Test that providing no arguments results in usage error. ... ok
test_semantic_version_0_0_1 (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version 0.0.1. ... ok
test_semantic_version_1_0_0 (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version 1.0.0. ... ok
test_semantic_version_10_20_30 (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version 10.20.30. ... ok
test_semantic_version_alpha (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version with alpha tag. ... ok
test_semantic_version_beta (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version with beta tag. ... ok
test_semantic_version_rc_with_build (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version with rc and build metadata. ... ok
test_too_few_arguments (test_get_pyproject_version.TestGetPyprojectVersion)
Test that providing too few arguments results in usage error. ... ok
test_too_many_arguments (test_get_pyproject_version.TestGetPyprojectVersion)
Test that providing too many arguments results in usage error. ... ok
test_version_mismatch (test_get_pyproject_version.TestGetPyprojectVersion)
Test that mismatched versions result in failure with appropriate error message. ... ok
test_version_with_build_metadata (test_get_pyproject_version.TestGetPyprojectVersion)
Test matching versions with build metadata. ... ok
test_version_with_prerelease_tags (test_get_pyproject_version.TestGetPyprojectVersion)
Test matching versions with pre-release tags like alpha, beta, rc. ... ok
----------------------------------------------------------------------
Ran 18 tests in 0.XXXs
OK
```
+1
View File
@@ -0,0 +1 @@
"""Tests for .github/scripts."""
@@ -0,0 +1,39 @@
"""Regression tests for cua-cloud release automation wiring."""
from pathlib import Path
import unittest
REPO_ROOT = Path(__file__).resolve().parents[3]
class TestCuaCloudReleaseWiring(unittest.TestCase):
"""Verify cua-cloud is wired into the release automation."""
def read(self, relative_path: str) -> str:
return (REPO_ROOT / relative_path).read_text()
def test_release_on_merge_tracks_cua_cloud(self) -> None:
workflow = self.read(".github/workflows/release-on-merge.yml")
self.assertIn('["libs/python/cua-cloud/"]="pypi/cloud"', workflow)
def test_release_bump_version_supports_cua_cloud(self) -> None:
workflow = self.read(".github/workflows/release-bump-version.yml")
self.assertIn("- pypi/cloud", workflow)
self.assertIn('"pypi/cloud")', workflow)
self.assertIn('echo "directory=libs/python/cua-cloud" >> $GITHUB_OUTPUT', workflow)
def test_unreleased_digest_tracks_cua_cloud(self) -> None:
workflow = self.read(".github/workflows/release-unreleased-digest.yml")
self.assertIn('SERVICE_TAG_DIR["pypi/cloud"]="cloud-v|libs/python/cua-cloud/"', workflow)
def test_package_release_files_exist(self) -> None:
self.assertTrue((REPO_ROOT / ".github/workflows/cd-py-cloud.yml").exists())
self.assertTrue((REPO_ROOT / "libs/python/cua-cloud/.bumpversion.cfg").exists())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,68 @@
"""Regression tests for cua-driver-rs release and PyPI wiring."""
from pathlib import Path
import unittest
REPO_ROOT = Path(__file__).resolve().parents[3]
class TestCuaDriverReleaseWiring(unittest.TestCase):
"""Verify cua-driver-rs releases feed the Python cua-driver publisher."""
def read(self, relative_path: str) -> str:
return (REPO_ROOT / relative_path).read_text()
def test_python_publish_follows_rust_workflow_run(self) -> None:
workflow = self.read(".github/workflows/cd-py-cua-driver.yml")
self.assertIn('workflows: ["CD: Cua Driver (cross-platform)"]', workflow)
self.assertNotIn("branches:\n - main", workflow)
self.assertIn('github.event.workflow_run.conclusion != \'cancelled\'', workflow)
self.assertIn('gh release view "$TAG" --repo "$GITHUB_REPOSITORY"', workflow)
def test_python_publish_defaults_to_current_rust_version(self) -> None:
workflow = self.read(".github/workflows/cd-py-cua-driver.yml")
self.assertIn("required: false", workflow)
self.assertIn('default: ""', workflow)
self.assertIn("libs/cua-driver/rust/Cargo.toml", workflow)
def test_python_publish_builds_linux_arm64_wheel(self) -> None:
workflow = self.read(".github/workflows/cd-py-cua-driver.yml")
self.assertIn("os: ubuntu-24.04-arm", workflow)
self.assertIn("arch: arm64", workflow)
def test_release_on_merge_tracks_rust_driver(self) -> None:
workflow = self.read(".github/workflows/release-on-merge.yml")
self.assertIn('["libs/cua-driver/rust/"]="cua-driver-rs"', workflow)
def test_release_reminder_tracks_rust_driver(self) -> None:
workflow = self.read(".github/workflows/ci-release-reminder.yml")
self.assertIn('["libs/cua-driver/rust/"]="cua-driver-rs"', workflow)
self.assertIn("cua-driver desktop release validation", workflow)
self.assertIn("e2e-rust-windows.yml", workflow)
self.assertIn("scripts/ci/macos/run-rust-e2e.sh", workflow)
self.assertIn("e2e-rust-linux.yml", workflow)
self.assertIn("e2e-rust-linux-wayland.yml", workflow)
def test_unreleased_digest_tracks_rust_driver(self) -> None:
workflow = self.read(".github/workflows/release-unreleased-digest.yml")
self.assertIn(
'SERVICE_TAG_DIR["cua-driver-rs"]="cua-driver-rs-v|libs/cua-driver/rust/"',
workflow,
)
def test_rust_driver_bump_keeps_python_wrapper_version_synced(self) -> None:
config = self.read("libs/cua-driver/rust/.bumpversion.cfg")
self.assertIn("[bumpversion:file:../python/pyproject.toml]", config)
self.assertIn("[bumpversion:file:../python/src/cua_driver/__init__.py]", config)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,360 @@
"""
Comprehensive tests for get_pyproject_version.py script using unittest.
This test suite covers:
- Version matching validation
- Error handling for missing versions
- Invalid input handling
- File not found scenarios
- Malformed TOML handling
"""
import sys
import tempfile
import unittest
from io import StringIO
from pathlib import Path
from unittest.mock import patch
# Add parent directory to path to import the module
sys.path.insert(0, str(Path(__file__).parent.parent))
# Import after path is modified
import get_pyproject_version
class TestGetPyprojectVersion(unittest.TestCase):
"""Test suite for get_pyproject_version.py functionality."""
def setUp(self):
"""Reset sys.argv before each test."""
self.original_argv = sys.argv.copy()
def tearDown(self):
"""Restore sys.argv after each test."""
sys.argv = self.original_argv
def create_pyproject_toml(self, version: str) -> Path:
"""Helper to create a temporary pyproject.toml file with a given version."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False)
temp_file.write(
f"""
[project]
name = "test-project"
version = "{version}"
description = "A test project"
"""
)
temp_file.close()
return Path(temp_file.name)
def create_pyproject_toml_no_version(self) -> Path:
"""Helper to create a pyproject.toml without a version field."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False)
temp_file.write(
"""
[project]
name = "test-project"
description = "A test project without version"
"""
)
temp_file.close()
return Path(temp_file.name)
def create_pyproject_toml_no_project(self) -> Path:
"""Helper to create a pyproject.toml without a project section."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False)
temp_file.write(
"""
[tool.poetry]
name = "test-project"
version = "1.0.0"
"""
)
temp_file.close()
return Path(temp_file.name)
def create_malformed_toml(self) -> Path:
"""Helper to create a malformed TOML file."""
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False)
temp_file.write(
"""
[project
name = "test-project
version = "1.0.0"
"""
)
temp_file.close()
return Path(temp_file.name)
# Test: Successful version match
def test_matching_versions(self):
"""Test that matching versions result in success."""
pyproject_file = self.create_pyproject_toml("1.2.3")
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.2.3"]
# Capture stdout
captured_output = StringIO()
with patch("sys.stdout", captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn("✅ Version consistency check passed: 1.2.3", captured_output.getvalue())
finally:
pyproject_file.unlink()
# Test: Version mismatch
def test_version_mismatch(self):
"""Test that mismatched versions result in failure with appropriate error message."""
pyproject_file = self.create_pyproject_toml("1.2.3")
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.2.4"]
# Capture stderr
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
error_output = captured_error.getvalue()
self.assertIn("❌ Version mismatch detected!", error_output)
self.assertIn("pyproject.toml version: 1.2.3", error_output)
self.assertIn("Expected version: 1.2.4", error_output)
self.assertIn("Please update pyproject.toml to version 1.2.4", error_output)
finally:
pyproject_file.unlink()
# Test: Missing version in pyproject.toml
def test_missing_version_field(self):
"""Test handling of pyproject.toml without a version field."""
pyproject_file = self.create_pyproject_toml_no_version()
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.0.0"]
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("❌ ERROR: No version found in pyproject.toml", captured_error.getvalue())
finally:
pyproject_file.unlink()
# Test: Missing project section
def test_missing_project_section(self):
"""Test handling of pyproject.toml without a project section."""
pyproject_file = self.create_pyproject_toml_no_project()
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.0.0"]
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("❌ ERROR: No version found in pyproject.toml", captured_error.getvalue())
finally:
pyproject_file.unlink()
# Test: File not found
def test_file_not_found(self):
"""Test handling of non-existent pyproject.toml file."""
sys.argv = ["get_pyproject_version.py", "/nonexistent/pyproject.toml", "1.0.0"]
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
# Test: Malformed TOML
def test_malformed_toml(self):
"""Test handling of malformed TOML file."""
pyproject_file = self.create_malformed_toml()
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.0.0"]
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
finally:
pyproject_file.unlink()
# Test: Incorrect number of arguments - too few
def test_too_few_arguments(self):
"""Test that providing too few arguments results in usage error."""
sys.argv = ["get_pyproject_version.py", "pyproject.toml"]
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn(
"Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
captured_error.getvalue(),
)
# Test: Incorrect number of arguments - too many
def test_too_many_arguments(self):
"""Test that providing too many arguments results in usage error."""
sys.argv = ["get_pyproject_version.py", "pyproject.toml", "1.0.0", "extra"]
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn(
"Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
captured_error.getvalue(),
)
# Test: No arguments
def test_no_arguments(self):
"""Test that providing no arguments results in usage error."""
sys.argv = ["get_pyproject_version.py"]
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn(
"Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
captured_error.getvalue(),
)
# Test: Version with pre-release tags
def test_version_with_prerelease_tags(self):
"""Test matching versions with pre-release tags like alpha, beta, rc."""
pyproject_file = self.create_pyproject_toml("1.2.3-rc.1")
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.2.3-rc.1"]
captured_output = StringIO()
with patch("sys.stdout", captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn(
"✅ Version consistency check passed: 1.2.3-rc.1", captured_output.getvalue()
)
finally:
pyproject_file.unlink()
# Test: Version with build metadata
def test_version_with_build_metadata(self):
"""Test matching versions with build metadata."""
pyproject_file = self.create_pyproject_toml("1.2.3+build.123")
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.2.3+build.123"]
captured_output = StringIO()
with patch("sys.stdout", captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn(
"✅ Version consistency check passed: 1.2.3+build.123", captured_output.getvalue()
)
finally:
pyproject_file.unlink()
# Test: Various semantic version formats
def test_semantic_version_0_0_1(self):
"""Test semantic version 0.0.1."""
self._test_version_format("0.0.1")
def test_semantic_version_1_0_0(self):
"""Test semantic version 1.0.0."""
self._test_version_format("1.0.0")
def test_semantic_version_10_20_30(self):
"""Test semantic version 10.20.30."""
self._test_version_format("10.20.30")
def test_semantic_version_alpha(self):
"""Test semantic version with alpha tag."""
self._test_version_format("1.2.3-alpha")
def test_semantic_version_beta(self):
"""Test semantic version with beta tag."""
self._test_version_format("1.2.3-beta.1")
def test_semantic_version_rc_with_build(self):
"""Test semantic version with rc and build metadata."""
self._test_version_format("1.2.3-rc.1+build.456")
def _test_version_format(self, version: str):
"""Helper method to test various semantic version formats."""
pyproject_file = self.create_pyproject_toml(version)
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), version]
captured_output = StringIO()
with patch("sys.stdout", captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn(
f"✅ Version consistency check passed: {version}", captured_output.getvalue()
)
finally:
pyproject_file.unlink()
# Test: Empty version string
def test_empty_version_string(self):
"""Test handling of empty version string."""
pyproject_file = self.create_pyproject_toml("")
try:
sys.argv = ["get_pyproject_version.py", str(pyproject_file), "1.0.0"]
captured_error = StringIO()
with patch("sys.stderr", captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
# Empty string is falsy, so it should trigger error
self.assertIn("", captured_error.getvalue())
finally:
pyproject_file.unlink()
class TestSuiteInfo(unittest.TestCase):
"""Test suite metadata."""
def test_suite_info(self):
"""Display test suite information."""
print("\n" + "=" * 70)
print("Test Suite: get_pyproject_version.py")
print("Framework: unittest (Python built-in)")
print("TOML Library: tomllib (Python 3.11+ built-in)")
print("=" * 70)
self.assertTrue(True)
if __name__ == "__main__":
# Run tests with verbose output
unittest.main(verbosity=2)
+25
View File
@@ -0,0 +1,25 @@
{
"network": {
"allowedDomains": [
"github.com",
"*.github.com",
"api.github.com",
"*.githubusercontent.com",
"bedrock-runtime.us-west-2.amazonaws.com",
"bedrock.us-west-2.amazonaws.com",
"sts.amazonaws.com",
"sts.us-west-2.amazonaws.com",
"otel.cua.ai",
"files.pythonhosted.org",
"pypi.org",
"registry.npmjs.org"
],
"deniedDomains": [],
"allowAllUnixSockets": true
},
"filesystem": {
"denyRead": ["~/.ssh", "~/.gnupg", "~/.aws/credentials"],
"allowWrite": [".", "/tmp", "/home/runner", "/home/runner/work"],
"denyWrite": [".env", ".env.local", "*.pem", "*.key", "secrets/"]
}
}
+18
View File
@@ -0,0 +1,18 @@
name: "CD: Container CuaBot"
on:
push:
tags:
- "docker-cuabot-v*.*.*"
workflow_dispatch:
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: cuabot
context_dir: libs/cuabot
tag_prefix: docker-cuabot-v
docker_hub_org: trycua
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
+17
View File
@@ -0,0 +1,17 @@
name: "CD: Container Kasm"
on:
push:
tags:
- "docker-kasm-v*.*.*"
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: cua-ubuntu
context_dir: libs/kasm
tag_prefix: docker-kasm-v
docker_hub_org: trycua
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
+17
View File
@@ -0,0 +1,17 @@
name: "CD: Container Lumier"
on:
push:
tags:
- "docker-lumier-v*.*.*"
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: lumier
context_dir: libs/lumier
tag_prefix: docker-lumier-v
docker_hub_org: trycua
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
@@ -0,0 +1,18 @@
name: "CD: Container QEMU Android"
on:
push:
tags:
- "docker-cua-qemu-android-v*.*.*"
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: cua-qemu-android
context_dir: libs/qemu-docker/android
tag_prefix: docker-cua-qemu-android-v
docker_hub_org: trycua
skip_arm64: true # Android QEMU image is not buildable on arm64
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
@@ -0,0 +1,17 @@
name: "CD: Container QEMU Linux"
on:
push:
tags:
- "docker-cua-qemu-linux-v*.*.*"
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: cua-qemu-linux
context_dir: libs/qemu-docker/linux
tag_prefix: docker-cua-qemu-linux-v
docker_hub_org: trycua
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
@@ -0,0 +1,17 @@
name: "CD: Container QEMU Windows"
on:
push:
tags:
- "docker-cua-qemu-windows-v*.*.*"
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: cua-qemu-windows
context_dir: libs/qemu-docker/windows
tag_prefix: docker-cua-qemu-windows-v
docker_hub_org: trycua
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
+18
View File
@@ -0,0 +1,18 @@
name: "CD: Container XFCE"
on:
push:
tags:
- "docker-xfce-v*.*.*"
workflow_dispatch:
jobs:
publish:
uses: ./.github/workflows/docker-reusable-publish.yml
with:
image_name: cua-xfce
context_dir: libs/xfce
tag_prefix: docker-xfce-v
docker_hub_org: trycua
secrets:
DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
+92
View File
@@ -0,0 +1,92 @@
name: "CD: Cua Driver Reference Docs"
# On a cua-driver release tag, regenerate the auto-generated reference MDX
# (cli-reference.mdx + mcp-tools.mdx) from the released binary's `dump-docs`
# output and open a PR with the refresh. This keeps the shipped reference in
# lockstep with the released binary's CLI/MCP surface without a human having
# to remember to run `pnpm docs:generate:cua-driver` after every cut.
#
# A PR (rather than a direct push to main) keeps a human in the loop for the
# generated-content diff. The App token mirrors the identity the version-bake
# step in cd-rust-cua-driver.yml already uses for release-time repo writes.
on:
push:
tags:
- "cua-driver-rs-v*"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
regenerate-reference-docs:
name: Regenerate cua-driver reference
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for git-tag version discovery
persist-credentials: false # re-auth with the App token before pushing
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install docs dependencies
run: pnpm install
working-directory: docs
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build cua-driver (release)
run: cargo build -p cua-driver --release
working-directory: libs/cua-driver/rust
- name: Regenerate reference docs
run: npx tsx scripts/docs-generators/runner.ts --library cua-driver
- name: Generate GitHub App token
id: app-token
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: Open PR with refreshed reference docs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
if git diff --quiet -- docs/content/docs/reference/cua-driver; then
echo "Reference docs already up to date; nothing to open."
exit 0
fi
BRANCH="docs/cua-driver-reference-${GITHUB_REF_NAME}"
# Re-authenticate origin with the App token so the push uses the
# bypass-enabled identity, not the default github-actions[bot]
# credentials baked in by actions/checkout.
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 config user.name "trycua-release[bot]"
git config user.email "trycua-release[bot]@users.noreply.github.com"
git checkout -B "$BRANCH"
git add docs/content/docs/reference/cua-driver
git commit -m "docs(cua-driver): regenerate reference for ${GITHUB_REF_NAME}"
git push -u origin "$BRANCH" --force-with-lease
gh pr create --base main --head "$BRANCH" \
--title "docs(cua-driver): regenerate reference for ${GITHUB_REF_NAME}" \
--body "Auto-generated from \`cua-driver dump-docs\` at \`${GITHUB_REF_NAME}\` by the cua-driver reference-docs workflow. Only the auto-generated \`cli-reference.mdx\` and \`mcp-tools.mdx\` change; the hand-maintained sibling files are untouched." \
|| echo "PR already exists for ${BRANCH}; the push updated it."
+184
View File
@@ -0,0 +1,184 @@
name: "CD: cua-agent (PyPI)"
on:
push:
tags:
- "agent-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
computer_version: ${{ steps.update-deps.outputs.computer_version }}
som_version: ${{ steps.update-deps.outputs.som_version }}
core_version: ${{ steps.update-deps.outputs.core_version }}
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Ensure latest main branch
run: |
git fetch origin main
git reset --hard origin/main
echo "Current HEAD commit:"
git log -1 --oneline
- name: Determine version
id: get-version
run: |
# Check inputs.version first (works for workflow_call regardless of event_name)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/agent-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "ERROR: Invalid tag format for agent"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
echo "ERROR: No version found (inputs.version, event.inputs.version, and tag all empty)"
exit 1
fi
echo "Agent version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Update dependencies to latest versions
id: update-deps
run: |
cd libs/python/agent
# Install required package for PyPI API access
pip install requests
# Create a more robust Python script for PyPI version checking
cat > get_latest_versions.py << 'EOF'
import requests
import json
import sys
def get_package_version(package_name, fallback="0.1.0"):
try:
response = requests.get(f'https://pypi.org/pypi/{package_name}/json')
print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr)
if response.status_code != 200:
print(f"API request failed for {package_name}, using fallback version", file=sys.stderr)
return fallback
data = json.loads(response.text)
if 'info' not in data:
print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr)
return fallback
return data['info']['version']
except Exception as e:
print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr)
return fallback
# Get latest versions
print(get_package_version('cua-computer'))
print(get_package_version('cua-som'))
print(get_package_version('cua-core'))
EOF
# Execute the script to get the versions
VERSIONS=($(python get_latest_versions.py))
LATEST_COMPUTER=${VERSIONS[0]}
LATEST_SOM=${VERSIONS[1]}
LATEST_CORE=${VERSIONS[2]}
echo "Latest cua-computer version: $LATEST_COMPUTER"
echo "Latest cua-som version: $LATEST_SOM"
echo "Latest cua-core version: $LATEST_CORE"
# Output the versions for the next job
echo "computer_version=$LATEST_COMPUTER" >> $GITHUB_OUTPUT
echo "som_version=$LATEST_SOM" >> $GITHUB_OUTPUT
echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT
# Determine major version for version constraint
COMPUTER_MAJOR=$(echo $LATEST_COMPUTER | cut -d. -f1)
SOM_MAJOR=$(echo $LATEST_SOM | cut -d. -f1)
CORE_MAJOR=$(echo $LATEST_CORE | cut -d. -f1)
NEXT_COMPUTER_MAJOR=$((COMPUTER_MAJOR + 1))
NEXT_SOM_MAJOR=$((SOM_MAJOR + 1))
NEXT_CORE_MAJOR=$((CORE_MAJOR + 1))
# Update dependencies in pyproject.toml
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS version of sed needs an empty string for -i
sed -i '' "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml
sed -i '' "s/\"cua-som>=.*,<.*\"/\"cua-som>=$LATEST_SOM,<$NEXT_SOM_MAJOR.0.0\"/" pyproject.toml
sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml
else
# Linux version
sed -i "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml
sed -i "s/\"cua-som>=.*,<.*\"/\"cua-som>=$LATEST_SOM,<$NEXT_SOM_MAJOR.0.0\"/" pyproject.toml
sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml
fi
# Display the updated dependencies
echo "Updated dependencies in pyproject.toml:"
grep -E "cua-computer|cua-som|cua-core" pyproject.toml
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "agent"
package_dir: "libs/python/agent"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-agent"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
set-env-variables:
needs: [prepare, publish]
runs-on: macos-latest
steps:
- name: Set environment variables for use in other jobs
run: |
echo "COMPUTER_VERSION=${{ needs.prepare.outputs.computer_version }}" >> $GITHUB_ENV
echo "SOM_VERSION=${{ needs.prepare.outputs.som_version }}" >> $GITHUB_ENV
echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "agent-v${{ needs.prepare.outputs.version }}"
release_name: "cua-agent v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/agent"
body: |
**Dependencies:** cua-computer ${{ needs.prepare.outputs.computer_version }}, cua-som ${{ needs.prepare.outputs.som_version }}
+74
View File
@@ -0,0 +1,74 @@
name: "CD: cua-auto (PyPI)"
on:
push:
tags:
- "auto-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (set by workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/auto-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for auto"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch
VERSION=${{ github.event.inputs.version }}
else
echo "No version provided"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "auto"
package_dir: "libs/python/cua-auto"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-auto"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "auto-v${{ needs.prepare.outputs.version }}"
release_name: "cua-auto v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua-auto"
body: ""
+73
View File
@@ -0,0 +1,73 @@
name: "CD: cua-bench-ui (PyPI)"
on:
push:
tags:
- "bench-ui-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check if version was provided via workflow_call first
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/bench-ui-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for bench-ui"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch (direct trigger)
VERSION=${{ github.event.inputs.version }}
else
echo "Unable to determine version"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "bench-ui"
package_dir: "libs/python/bench-ui"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-bench-ui"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "bench-ui-v${{ needs.prepare.outputs.version }}"
release_name: "cua-bench-ui v${{ needs.prepare.outputs.version }}"
body: ""
+73
View File
@@ -0,0 +1,73 @@
name: "CD: cua-bench (PyPI)"
on:
push:
tags:
- "bench-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check if version was provided via workflow_call first
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/bench-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for bench"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch (direct trigger)
VERSION=${{ github.event.inputs.version }}
else
echo "Unable to determine version"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "bench"
package_dir: "libs/cua-bench"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-bench"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "bench-v${{ needs.prepare.outputs.version }}"
release_name: "cua-bench v${{ needs.prepare.outputs.version }}"
body: ""
+74
View File
@@ -0,0 +1,74 @@
name: "CD: cua-cli (PyPI)"
on:
push:
tags:
- "cli-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (set by workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/cli-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for cli"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch
VERSION=${{ github.event.inputs.version }}
else
echo "No version provided"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "cli"
package_dir: "libs/python/cua-cli"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-cli"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "cli-v${{ needs.prepare.outputs.version }}"
release_name: "cua-cli v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua-cli"
body: ""
+69
View File
@@ -0,0 +1,69 @@
name: "CD: cua-cloud (PyPI)"
on:
push:
tags:
- "cloud-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/cloud-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for cloud"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
echo "ERROR: No version found!"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "cloud"
package_dir: "libs/python/cua-cloud"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-cloud"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "cloud-v${{ needs.prepare.outputs.version }}"
release_name: "cua-cloud v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua-cloud"
body: ""
+142
View File
@@ -0,0 +1,142 @@
name: "CD: cua-computer-server (PyPI)"
on:
push:
tags:
- "computer-server-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
outputs:
version:
description: "The version that was published"
value: ${{ jobs.prepare.outputs.version }}
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (set by workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/computer-server-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for computer-server"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch
VERSION=${{ github.event.inputs.version }}
else
echo "No version provided"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "computer-server"
package_dir: "libs/python/computer-server"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-computer-server"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
build-binaries:
needs: prepare
strategy:
matrix:
include:
- os: ubuntu-latest
artifact_name: cua-computer-server-linux-x86_64
zip_name: cua-computer-server-linux-x86_64.tar.gz
dir_name: cua-computer-server
- os: windows-latest
artifact_name: cua-computer-server-windows-x86_64
zip_name: cua-computer-server-windows-x86_64.zip
dir_name: cua-computer-server
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install pyinstaller
pip install -e libs/python/computer-server
- name: Build binary with PyInstaller (onedir — fast startup)
run: |
cd libs/python/computer-server
pyinstaller --onedir --name "${{ matrix.dir_name }}" --console --noconfirm --clean --collect-all computer_server --hidden-import uvicorn.logging --hidden-import uvicorn.protocols.http --hidden-import uvicorn.protocols.http.auto --hidden-import uvicorn.protocols.http.h11_impl --hidden-import uvicorn.protocols.websockets --hidden-import uvicorn.protocols.websockets.auto --hidden-import uvicorn.lifespan --hidden-import uvicorn.lifespan.on --hidden-import PIL._tkinter_finder run_server.py
- name: Create archive (Linux)
if: runner.os == 'Linux'
run: |
cd libs/python/computer-server/dist
tar czf "${{ matrix.zip_name }}" "${{ matrix.dir_name }}"/
- name: Create archive (Windows)
if: runner.os == 'Windows'
run: |
cd libs/python/computer-server/dist
7z a "${{ matrix.zip_name }}" "${{ matrix.dir_name }}/"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: libs/python/computer-server/dist/${{ matrix.zip_name }}
set-env-variables:
needs: [prepare, publish]
runs-on: ubuntu-latest
steps:
- name: Set environment variables for use in other jobs
run: |
echo "COMPUTER_VERSION=${{ needs.prepare.outputs.version }}" >> $GITHUB_ENV
create-release:
needs: [prepare, publish, build-binaries]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "computer-server-v${{ needs.prepare.outputs.version }}"
release_name: "cua-computer-server v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/computer-server"
body: ""
attach_artifacts: true
+166
View File
@@ -0,0 +1,166 @@
name: "CD: cua-computer (PyPI)"
on:
push:
tags:
- "computer-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
core_version: ${{ steps.update-deps.outputs.core_version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
echo "=== Version Detection Debug ==="
echo "Event name: ${{ github.event_name }}"
echo "Workflow call version: ${{ inputs.version }}"
echo "Workflow dispatch version: ${{ github.event.inputs.version }}"
echo "GitHub ref: ${{ github.ref }}"
# Check inputs.version first (works for workflow_call regardless of event_name)
if [ -n "${{ inputs.version }}" ]; then
# Version provided via workflow_call or workflow_dispatch with version input
VERSION=${{ inputs.version }}
echo "Using inputs.version: $VERSION"
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/computer-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
echo "Extracted from tag: $VERSION"
else
echo "Invalid tag format for computer"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
# Use version from workflow_dispatch event inputs
VERSION=${{ github.event.inputs.version }}
echo "Using event.inputs.version: $VERSION"
else
echo "ERROR: No version found!"
echo " - inputs.version is empty"
echo " - event.inputs.version is empty"
echo " - Not a tag push event"
exit 1
fi
echo "=== Final Version ==="
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Update dependencies to latest versions
id: update-deps
run: |
cd libs/python/computer
# Install required package for PyPI API access
pip install requests
# Create a more robust Python script for PyPI version checking
cat > get_latest_versions.py << 'EOF'
import requests
import json
import sys
def get_package_version(package_name, fallback="0.1.0"):
try:
response = requests.get(f'https://pypi.org/pypi/{package_name}/json')
print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr)
if response.status_code != 200:
print(f"API request failed for {package_name}, using fallback version", file=sys.stderr)
return fallback
data = json.loads(response.text)
if 'info' not in data:
print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr)
return fallback
return data['info']['version']
except Exception as e:
print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr)
return fallback
# Get latest versions
print(get_package_version('cua-core'))
EOF
# Execute the script to get the versions
VERSIONS=($(python get_latest_versions.py))
LATEST_CORE=${VERSIONS[0]}
echo "Latest cua-core version: $LATEST_CORE"
# Output the versions for the next job
echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT
# Determine major version for version constraint
CORE_MAJOR=$(echo $LATEST_CORE | cut -d. -f1)
NEXT_CORE_MAJOR=$((CORE_MAJOR + 1))
# Update dependencies in pyproject.toml
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS version of sed needs an empty string for -i
sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml
else
# Linux version
sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml
fi
# Display the updated dependencies
echo "Updated dependencies in pyproject.toml:"
grep -E "cua-core" pyproject.toml
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "computer"
package_dir: "libs/python/computer"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-computer"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
set-env-variables:
needs: [prepare, publish]
runs-on: macos-latest
steps:
- name: Set environment variables for use in other jobs
run: |
echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "computer-v${{ needs.prepare.outputs.version }}"
release_name: "cua-computer v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/computer"
body: ""
+74
View File
@@ -0,0 +1,74 @@
name: "CD: cua-core (PyPI)"
on:
push:
tags:
- "core-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (set by workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/core-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for core"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch
VERSION=${{ github.event.inputs.version }}
else
echo "No version provided"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "core"
package_dir: "libs/python/core"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-core"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "core-v${{ needs.prepare.outputs.version }}"
release_name: "cua-core v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/core"
body: ""
+171
View File
@@ -0,0 +1,171 @@
name: "CD: Python cua-driver"
# Triggered after cua-driver-rs releases to build platform-specific wheels
# with bundled binaries and publish to PyPI.
on:
workflow_dispatch:
inputs:
version:
description: "cua-driver-rs version to package (without v prefix)"
required: false
default: ""
# Auto-trigger when cua-driver-rs releases (after CD workflow completes)
workflow_run:
workflows: ["CD: Cua Driver (cross-platform)"]
types:
- completed
permissions:
contents: read
jobs:
# Extract version from the triggering workflow or input
get-version:
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion != 'cancelled'
outputs:
version: ${{ steps.version.outputs.version }}
should_publish: ${{ steps.version.outputs.should_publish }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Determine version
id: version
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
if [ -z "$VERSION" ]; then
VERSION=$(sed -n -E 's/^version = "([^"]+)"/\1/p' libs/cua-driver/rust/Cargo.toml | head -1)
fi
SHOULD_PUBLISH="true"
else
# Extract from the tag that triggered the workflow_run
# Validate SHA format to prevent injection
HEAD_SHA="${{ github.event.workflow_run.head_sha }}"
if ! echo "$HEAD_SHA" | grep -qE '^[0-9a-f]{40}$'; then
echo "Invalid SHA format: $HEAD_SHA"
VERSION="0.0.0"
SHOULD_PUBLISH="false"
else
git fetch --tags
TAG=$(git tag --points-at "$HEAD_SHA" | grep '^cua-driver-rs-v' | head -1 || echo "")
if [ -n "$TAG" ]; then
VERSION="${TAG#cua-driver-rs-v}"
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
SHOULD_PUBLISH="true"
else
echo "GitHub Release $TAG is not available; skipping PyPI publish."
SHOULD_PUBLISH="false"
fi
else
# No matching tag, skip
VERSION="0.0.0"
SHOULD_PUBLISH="false"
fi
fi
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "should_publish=$SHOULD_PUBLISH" >> "$GITHUB_OUTPUT"
echo "Detected version: $VERSION (publish: $SHOULD_PUBLISH)"
# Build wheels for each platform (with bundled binaries)
build-wheels:
needs: get-version
if: needs.get-version.outputs.should_publish == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: macos
arch: universal
- os: ubuntu-latest
platform: linux
arch: x86_64
- os: ubuntu-24.04-arm
platform: linux
arch: arm64
- os: windows-latest
platform: windows
arch: x86_64
- os: windows-latest
platform: windows
arch: arm64
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build hatchling
- name: Update version in pyproject.toml and __init__.py
shell: bash
run: |
VERSION="${{ needs.get-version.outputs.version }}"
cd libs/cua-driver/python
# Portable sed: use temp file approach for all platforms
sed "s/^version = .*/version = \"$VERSION\"/" pyproject.toml > pyproject.toml.tmp
mv pyproject.toml.tmp pyproject.toml
sed "s/^__version__ = .*/__version__ = \"$VERSION\"/" src/cua_driver/__init__.py > __init__.py.tmp
mv __init__.py.tmp src/cua_driver/__init__.py
echo "Updated versions:"
grep "^version = " pyproject.toml
grep "^__version__ = " src/cua_driver/__init__.py
- name: Build wheel with bundled binary
shell: bash
run: |
cd libs/cua-driver/python
python build_wheel.py --version "${{ needs.get-version.outputs.version }}" --arch "${{ matrix.arch }}"
- name: List built artifacts
shell: bash
run: |
ls -lh libs/cua-driver/python/dist/
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: wheel-${{ matrix.platform }}-${{ matrix.arch }}
path: libs/cua-driver/python/dist/*.whl
if-no-files-found: error
# Publish all wheels to PyPI
publish-pypi:
needs: [get-version, build-wheels]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Download all wheels
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
pattern: wheel-*
path: dist
merge-multiple: true
- name: List wheels to publish
run: |
ls -lh dist/
- name: Install twine
run: |
python -m pip install --upgrade pip
pip install twine
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
python -m twine upload --skip-existing --verbose dist/*
+69
View File
@@ -0,0 +1,69 @@
name: "CD: cua (PyPI)"
on:
push:
tags:
- "cua-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/cua-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for cua"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
echo "ERROR: No version found!"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "cua"
package_dir: "libs/python/cua"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "cua-v${{ needs.prepare.outputs.version }}"
release_name: "cua v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua"
body: ""
+168
View File
@@ -0,0 +1,168 @@
name: "CD: cua-mcp-server (PyPI)"
on:
push:
tags:
- "mcp-server-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
outputs:
version:
description: "The version that was published"
value: ${{ jobs.prepare.outputs.version }}
# Adding permissions at workflow level
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
agent_version: ${{ steps.update-deps.outputs.agent_version }}
computer_version: ${{ steps.update-deps.outputs.computer_version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (set by workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/mcp-server-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for mcp-server"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch
VERSION=${{ github.event.inputs.version }}
else
echo "No version provided"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Update dependencies to latest versions
id: update-deps
run: |
cd libs/python/mcp-server
# Install required package for PyPI API access
pip install requests
# Create a Python script for PyPI version checking
cat > get_latest_versions.py << 'EOF'
import requests
import json
import sys
def get_package_version(package_name, fallback="0.1.0"):
try:
response = requests.get(f'https://pypi.org/pypi/{package_name}/json')
print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr)
if response.status_code != 200:
print(f"API request failed for {package_name}, using fallback version", file=sys.stderr)
return fallback
data = json.loads(response.text)
if 'info' not in data:
print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr)
return fallback
return data['info']['version']
except Exception as e:
print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr)
return fallback
# Get latest versions
print(get_package_version('cua-agent'))
print(get_package_version('cua-computer'))
EOF
# Execute the script to get the versions
VERSIONS=($(python get_latest_versions.py))
LATEST_AGENT=${VERSIONS[0]}
LATEST_COMPUTER=${VERSIONS[1]}
echo "Latest cua-agent version: $LATEST_AGENT"
echo "Latest cua-computer version: $LATEST_COMPUTER"
# Output the versions for the next job
echo "agent_version=$LATEST_AGENT" >> $GITHUB_OUTPUT
echo "computer_version=$LATEST_COMPUTER" >> $GITHUB_OUTPUT
# Determine major version for version constraint
AGENT_MAJOR=$(echo $LATEST_AGENT | cut -d. -f1)
COMPUTER_MAJOR=$(echo $LATEST_COMPUTER | cut -d. -f1)
NEXT_AGENT_MAJOR=$((AGENT_MAJOR + 1))
NEXT_COMPUTER_MAJOR=$((COMPUTER_MAJOR + 1))
# Update dependencies in pyproject.toml
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS version of sed needs an empty string for -i
# Update cua-agent with all extras
sed -i '' "s/\"cua-agent\[all\]>=.*,<.*\"/\"cua-agent[all]>=$LATEST_AGENT,<$NEXT_AGENT_MAJOR.0.0\"/" pyproject.toml
sed -i '' "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml
else
# Linux version
sed -i "s/\"cua-agent\[all\]>=.*,<.*\"/\"cua-agent[all]>=$LATEST_AGENT,<$NEXT_AGENT_MAJOR.0.0\"/" pyproject.toml
sed -i "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml
fi
# Display the updated dependencies
echo "Updated dependencies in pyproject.toml:"
grep -E "cua-agent|cua-computer" pyproject.toml
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "mcp-server"
package_dir: "libs/python/mcp-server"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-mcp-server"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
set-env-variables:
needs: [prepare, publish]
runs-on: macos-latest
steps:
- name: Set environment variables for use in other jobs
run: |
echo "AGENT_VERSION=${{ needs.prepare.outputs.agent_version }}" >> $GITHUB_ENV
echo "COMPUTER_VERSION=${{ needs.prepare.outputs.computer_version }}" >> $GITHUB_ENV
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "mcp-server-v${{ needs.prepare.outputs.version }}"
release_name: "cua-mcp-server v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/mcp-server"
body: ""
+69
View File
@@ -0,0 +1,69 @@
name: "CD: cua-sandbox-apps (PyPI)"
on:
push:
tags:
- "sandbox-apps-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/sandbox-apps-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for sandbox-apps"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
echo "ERROR: No version found!"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "sandbox-apps"
package_dir: "libs/python/cua-sandbox-apps"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-sandbox-apps"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "sandbox-apps-v${{ needs.prepare.outputs.version }}"
release_name: "cua-sandbox-apps v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua-sandbox-apps"
body: ""
+69
View File
@@ -0,0 +1,69 @@
name: "CD: cua-sandbox (PyPI)"
on:
push:
tags:
- "sandbox-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/sandbox-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for sandbox"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
echo "ERROR: No version found!"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "sandbox"
package_dir: "libs/python/cua-sandbox"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-sandbox"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "sandbox-v${{ needs.prepare.outputs.version }}"
release_name: "cua-sandbox v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua-sandbox"
body: ""
+78
View File
@@ -0,0 +1,78 @@
name: "CD: cua-som (PyPI)"
on:
push:
tags:
- "som-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
outputs:
version:
description: "The version that was published"
value: ${{ jobs.determine-version.outputs.version }}
# Adding permissions at workflow level
permissions:
contents: write
jobs:
determine-version:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (set by workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag (for package-specific tags)
if [[ "${{ github.ref }}" =~ ^refs/tags/som-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for som"
exit 1
fi
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Use version from workflow dispatch
VERSION=${{ github.event.inputs.version }}
else
echo "No version provided"
exit 1
fi
echo "VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: determine-version
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "som"
package_dir: "libs/python/som"
version: ${{ needs.determine-version.outputs.version }}
base_package_name: "cua-som"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [determine-version, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "som-v${{ needs.determine-version.outputs.version }}"
release_name: "cua-som v${{ needs.determine-version.outputs.version }}"
module_path: "libs/python/som"
body: ""
+69
View File
@@ -0,0 +1,69 @@
name: "CD: cua-train (PyPI)"
on:
push:
tags:
- "train-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to publish"
required: true
type: string
permissions:
contents: write
jobs:
prepare:
runs-on: macos-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION=${{ inputs.version }}
elif [ "${{ github.event_name }}" == "push" ]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/train-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "Invalid tag format for train"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
else
echo "ERROR: No version found!"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/py-reusable-publish.yml
with:
package_name: "train"
package_dir: "libs/python/cua-train"
version: ${{ needs.prepare.outputs.version }}
base_package_name: "cua-train"
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: "train-v${{ needs.prepare.outputs.version }}"
release_name: "cua-train v${{ needs.prepare.outputs.version }}"
module_path: "libs/python/cua-train"
body: ""
+701
View File
@@ -0,0 +1,701 @@
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
+356
View File
@@ -0,0 +1,356 @@
name: "CD: Lume (macOS)"
on:
push:
tags:
- "lume-v*"
workflow_dispatch:
inputs:
version:
description: "Version to notarize (without v prefix)"
required: true
default: "0.1.0"
workflow_call:
inputs:
version:
description: "Version to notarize"
required: true
type: string
secrets:
APPLICATION_CERT_BASE64:
required: true
INSTALLER_CERT_BASE64:
required: true
CERT_PASSWORD:
required: true
APPLE_ID:
required: true
TEAM_ID:
required: true
APP_SPECIFIC_PASSWORD:
required: true
DEVELOPER_NAME:
required: true
PROVISIONING_PROFILE_BASE64:
required: true
RELEASE_APP_ID:
required: false
RELEASE_APP_PRIVATE_KEY:
required: false
permissions:
contents: write
env:
APPLICATION_CERT_BASE64: ${{ secrets.APPLICATION_CERT_BASE64 }}
INSTALLER_CERT_BASE64: ${{ secrets.INSTALLER_CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
DEVELOPER_NAME: ${{ secrets.DEVELOPER_NAME }}
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
jobs:
notarize:
runs-on: macos-15
outputs:
sha256_checksums: ${{ steps.generate_checksums.outputs.checksums }}
version: ${{ steps.set_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Select Xcode 16.3
run: |
sudo xcode-select -s /Applications/Xcode_16.3.app
xcodebuild -version
- name: Install dependencies
run: |
brew install cpio
- name: Create .release directory
run: mkdir -p .release
- name: Set version
id: set_version
run: |
# Determine version from tag or input
if [[ "$GITHUB_REF" == refs/tags/lume-v* ]]; then
VERSION="${GITHUB_REF#refs/tags/lume-v}"
echo "Using version from tag: $VERSION"
elif [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
echo "Using version from input: $VERSION"
elif [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
echo "Using version from workflow_call input: $VERSION"
else
echo "Error: No version found in tag or input"
exit 1
fi
# Update version in Main.swift
echo "Updating version in Main.swift to $VERSION"
sed -i '' "s/static let current: String = \".*\"/static let current: String = \"$VERSION\"/" libs/lume/src/Main.swift
# Set output for later steps
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Import Certificates
env:
APPLICATION_CERT_BASE64: ${{ secrets.APPLICATION_CERT_BASE64 }}
INSTALLER_CERT_BASE64: ${{ secrets.INSTALLER_CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
KEYCHAIN_PASSWORD: "temp_password"
run: |
# Create a temporary keychain
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
# Import certificates
echo $APPLICATION_CERT_BASE64 | base64 --decode > application.p12
echo $INSTALLER_CERT_BASE64 | base64 --decode > installer.p12
# Import certificates silently (minimize output)
security import application.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/pkgbuild > /dev/null 2>&1
security import installer.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/pkgbuild > /dev/null 2>&1
# Allow codesign to access the certificates (minimal output)
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain > /dev/null 2>&1
# Verify certificates were imported
echo "Verifying signing identities..."
CERT_COUNT=$(security find-identity -v -p codesigning build.keychain | grep -c "Developer ID Application" || echo "0")
INSTALLER_COUNT=$(security find-identity -v build.keychain | grep -c "Developer ID Installer" || echo "0")
if [ "$CERT_COUNT" -eq 0 ]; then
echo "Error: No Developer ID Application certificate found"
security find-identity -v -p codesigning build.keychain
exit 1
fi
if [ "$INSTALLER_COUNT" -eq 0 ]; then
echo "Error: No Developer ID Installer certificate found"
security find-identity -v build.keychain
exit 1
fi
echo "Found $CERT_COUNT Developer ID Application certificate(s) and $INSTALLER_COUNT Developer ID Installer certificate(s)"
echo "All required certificates verified successfully"
# Clean up certificate files
rm application.p12 installer.p12
- name: Install Provisioning Profile
env:
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
run: |
echo "Installing provisioning profile..."
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > libs/lume/resources/embedded.provisionprofile
echo "Provisioning profile installed successfully"
- name: Build and Notarize
id: build_notarize
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
# These will now reference the imported certificates
CERT_APPLICATION_NAME: "Developer ID Application: ${{ secrets.DEVELOPER_NAME }} (${{ secrets.TEAM_ID }})"
CERT_INSTALLER_NAME: "Developer ID Installer: ${{ secrets.DEVELOPER_NAME }} (${{ secrets.TEAM_ID }})"
VERSION: ${{ steps.set_version.outputs.version }}
working-directory: ./libs/lume
run: |
# Minimal debug information
echo "Starting build process..."
echo "Swift version: $(swift --version | head -n 1)"
echo "Building version: $VERSION"
# Ensure .release directory exists
mkdir -p .release
chmod 755 .release
# Build the project first (redirect verbose output)
echo "Building project..."
swift build --configuration release > build.log 2>&1
echo "Build completed."
# Run the notarization script with LOG_LEVEL env var
chmod +x scripts/build/build-release-notarized.sh
cd scripts/build
LOG_LEVEL=minimal ./build-release-notarized.sh
# Return to the lume directory
cd ../..
# Debug: List what files were actually created
echo "Files in .release directory:"
find .release -type f -name "*.tar.gz" -o -name "*.pkg.tar.gz"
# Get architecture for output filename
ARCH=$(uname -m)
OS_IDENTIFIER="darwin-${ARCH}"
# Output paths for later use
echo "tarball_path=.release/lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" >> $GITHUB_OUTPUT
echo "pkg_path=.release/lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" >> $GITHUB_OUTPUT
- name: Upload build log on failure
if: failure() && steps.build_notarize.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: swift-build-log
path: ./libs/lume/build.log
retention-days: 7
- name: Generate SHA256 Checksums
id: generate_checksums
working-directory: ./libs/lume/.release
run: |
# Use existing checksums file if it exists, otherwise generate one
if [ -f "checksums.txt" ]; then
echo "Using existing checksums file"
cat checksums.txt
else
echo "## SHA256 Checksums" > checksums.txt
echo '```' >> checksums.txt
shasum -a 256 lume-*.tar.gz >> checksums.txt
echo '```' >> checksums.txt
fi
checksums=$(cat checksums.txt)
echo "checksums<<EOF" >> $GITHUB_OUTPUT
echo "$checksums" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Debug: Show all files in the release directory
echo "All files in release directory:"
ls -la
- name: Create Standard Version Releases
working-directory: ./libs/lume/.release
run: |
VERSION=${{ steps.set_version.outputs.version }}
ARCH=$(uname -m)
OS_IDENTIFIER="darwin-${ARCH}"
# Create OS-tagged symlinks
ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" "lume-darwin.tar.gz"
ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" "lume-darwin.pkg.tar.gz"
# Create simple symlinks
ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" "lume.tar.gz"
ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" "lume.pkg.tar.gz"
# List all files (including symlinks)
echo "Files with symlinks in release directory:"
ls -la
- name: Upload Notarized Package (Tarball)
uses: actions/upload-artifact@v4
with:
name: lume-notarized-tarball
path: ./libs/lume/${{ steps.build_notarize.outputs.tarball_path }}
if-no-files-found: error
- name: Upload Notarized Package (Installer)
uses: actions/upload-artifact@v4
with:
name: lume-notarized-installer
path: ./libs/lume/${{ steps.build_notarize.outputs.pkg_path }}
if-no-files-found: error
- name: Generate path-filtered release notes
if: startsWith(github.ref, 'refs/tags/lume-v')
id: release-notes
run: |
# Find previous lume tag
PREV_TAG=$(git tag -l "lume-v*" --sort=-v:refname | grep -v "^${{ github.ref_name }}$" | head -n 1 || echo "")
echo "Current tag: ${{ github.ref_name }}"
echo "Previous tag: $PREV_TAG"
# Generate release notes filtered by libs/lume path
if [ -n "$PREV_TAG" ]; then
echo "Generating notes for commits between $PREV_TAG and HEAD in libs/lume"
NOTES=$(git log ${PREV_TAG}..HEAD --pretty=format:"* %s (%h) by @%an" -- "libs/lume" | head -50)
else
echo "No previous tag found, generating notes for recent commits in libs/lume"
NOTES=$(git log --pretty=format:"* %s (%h) by @%an" -- "libs/lume" | head -50)
fi
if [ -z "$NOTES" ]; then
NOTES="* Initial release or no path-specific changes found"
fi
# Store notes in output
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
echo "## What's Changed" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
if: startsWith(github.ref, 'refs/tags/lume-v')
uses: softprops/action-gh-release@v1
with:
files: |
./libs/lume/${{ steps.build_notarize.outputs.tarball_path }}
./libs/lume/${{ steps.build_notarize.outputs.pkg_path }}
./libs/lume/.release/lume-darwin.tar.gz
./libs/lume/.release/lume-darwin.pkg.tar.gz
./libs/lume/.release/lume.tar.gz
./libs/lume/.release/lume.pkg.tar.gz
body: |
${{ steps.release-notes.outputs.RELEASE_NOTES }}
${{ steps.generate_checksums.outputs.checksums }}
### Installation with script
```bash
/bin/bash -c "$(curl -fsSL https://cua.ai/lume/install.sh)"
```
generate_release_notes: false
make_latest: true
- name: Generate GitHub App token (for bake-version push)
id: app-token
if: startsWith(github.ref, 'refs/tags/lume-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 script (commit + push)
if: startsWith(github.ref, 'refs/tags/lume-v')
env:
VERSION: ${{ steps.set_version.outputs.version }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
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-lume-version origin/main
sed -i '' \
"s/^LUME_BAKED_VERSION=.*/LUME_BAKED_VERSION=\"${VERSION}\"/" \
libs/lume/scripts/install.sh
if git diff --quiet -- libs/lume/scripts/install.sh; then
echo "LUME_BAKED_VERSION already ${VERSION}; nothing to commit."
exit 0
fi
git config user.name "trycua-release[bot]"
git config user.email "trycua-release[bot]@users.noreply.github.com"
git add libs/lume/scripts/install.sh
git commit -m "chore(lume): bake version ${VERSION} into install script [skip ci]"
git push origin bake-lume-version:main
+163
View File
@@ -0,0 +1,163 @@
name: "CD: @trycua/cli (npm)"
on:
push:
tags:
- "npm-cli-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
default: ""
workflow_call:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
type: string
default: ""
permissions:
id-token: write
contents: write
packages: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (works for workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag
if [[ "${{ github.ref }}" =~ ^refs/tags/npm-cli-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "ERROR: Invalid tag format for npm-cli"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Read from package.json
VERSION=$(node -p "require('./libs/typescript/cua-cli/package.json').version")
fi
echo "CLI version: $VERSION"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=npm-cli-v${VERSION}" >> $GITHUB_OUTPUT
build-binaries:
strategy:
matrix:
include:
- target: bun-linux-x64
ext: ""
binary_name: cua-linux-x64
- target: bun-darwin-x64
ext: ""
binary_name: cua-darwin-x64
- target: bun-darwin-arm64
ext: ""
binary_name: cua-darwin-arm64
- target: bun-windows-x64
ext: ".exe"
binary_name: cua-windows-x64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
working-directory: ./libs/typescript/cua-cli
run: bun install --frozen-lockfile
- name: Build binary
working-directory: ./libs/typescript/cua-cli
run: |
bun build --compile --minify --sourcemap --target=${{ matrix.target }} index.ts --outfile ${{ matrix.binary_name }}${{ matrix.ext }}
mkdir -p ../../../dist
mv ${{ matrix.binary_name }}${{ matrix.ext }}* ../../../dist/
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: cua-binary-${{ matrix.target }}
path: dist/
if-no-files-found: error
retention-days: 1
publish-npm:
needs: prepare
uses: ./.github/workflows/ts-reusable-publish.yml
with:
package_name: "cli"
package_dir: "libs/typescript/cua-cli"
package_manager: "bun"
version: ${{ needs.prepare.outputs.version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
needs: [prepare, publish-npm, build-binaries]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: ${{ needs.prepare.outputs.tag }}
release_name: "cua-cli v${{ needs.prepare.outputs.version }}"
module_path: "libs/typescript/cua-cli"
attach_artifacts: true
body: |
## Installation
### Using install script (recommended)
```bash
# For Linux/macOS
curl -fsSL https://cua.ai/cli/install.sh | sh
# For Windows (PowerShell)
irm https://cua.ai/cli/install.ps1 | iex
```
### Using npm/bun
```bash
# Using bun
bun add -g @trycua/cli
# Or using npm
npm install -g @trycua/cli
```
### From source
```bash
git clone -b ${{ needs.prepare.outputs.tag }} https://github.com/trycua/cua.git
cd cua/libs/typescript/cua-cli
bun install
bun link
bun link cua-cli
```
## Release Assets
- `cua-darwin-arm64`: macOS (Apple Silicon)
- `cua-darwin-x64`: macOS (Intel)
- `cua-linux-x64`: Linux (x86_64)
- `cua-windows-x64.exe`: Windows (x86_64)
+83
View File
@@ -0,0 +1,83 @@
name: "CD: @trycua/computer (npm)"
on:
push:
tags:
- "npm-computer-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
default: ""
workflow_call:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
type: string
default: ""
permissions:
id-token: write
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (works for workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag
if [[ "${{ github.ref }}" =~ ^refs/tags/npm-computer-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "ERROR: Invalid tag format for npm-computer"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Read from package.json
VERSION=$(node -p "require('./libs/typescript/computer/package.json').version")
fi
echo "Computer version: $VERSION"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=npm-computer-v${VERSION}" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/ts-reusable-publish.yml
with:
package_name: "computer"
package_dir: "libs/typescript/computer"
package_manager: "pnpm"
version: ${{ needs.prepare.outputs.version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: ${{ needs.prepare.outputs.tag }}
release_name: "@trycua/computer v${{ needs.prepare.outputs.version }}"
module_path: "libs/typescript/computer"
body: |
## Installation
```bash
npm install @trycua/computer
```
+83
View File
@@ -0,0 +1,83 @@
name: "CD: @trycua/core (npm)"
on:
push:
tags:
- "npm-core-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
default: ""
workflow_call:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
type: string
default: ""
permissions:
id-token: write
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (works for workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag
if [[ "${{ github.ref }}" =~ ^refs/tags/npm-core-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "ERROR: Invalid tag format for npm-core"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Read from package.json
VERSION=$(node -p "require('./libs/typescript/core/package.json').version")
fi
echo "Core version: $VERSION"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=npm-core-v${VERSION}" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/ts-reusable-publish.yml
with:
package_name: "core"
package_dir: "libs/typescript/core"
package_manager: "pnpm"
version: ${{ needs.prepare.outputs.version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: ${{ needs.prepare.outputs.tag }}
release_name: "@trycua/core v${{ needs.prepare.outputs.version }}"
module_path: "libs/typescript/core"
body: |
## Installation
```bash
npm install @trycua/core
```
+102
View File
@@ -0,0 +1,102 @@
name: "CD: cuabot (npm)"
on:
push:
tags:
- "cuabot-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
default: ""
workflow_call:
inputs:
version:
description: "Version to publish"
required: false
type: string
default: ""
permissions:
id-token: write
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [ "${{ github.event_name }}" == "push" ]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/cuabot-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "ERROR: Invalid tag format"
exit 1
fi
else
VERSION=$(node -p "require('./libs/cuabot/package.json').version")
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=cuabot-v${VERSION}" >> $GITHUB_OUTPUT
publish-npm:
needs: prepare
uses: ./.github/workflows/ts-reusable-publish.yml
with:
package_name: "cuabot"
package_dir: "libs/cuabot"
package_manager: "pnpm"
version: ${{ needs.prepare.outputs.version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
needs: [prepare, publish-npm]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: ${{ needs.prepare.outputs.tag }}
release_name: "cuabot v${{ needs.prepare.outputs.version }}"
module_path: "libs/cuabot"
body: |
## Quick Start
```bash
npx cuabot
```
Or install globally:
```bash
npm install -g cuabot
cuabot
```
## Usage
```bash
cuabot # Run default agent (or setup)
cuabot claude # Run Claude Code in sandbox
cuabot gemini # Run Gemini CLI in sandbox
cuabot codex # Run Codex CLI in sandbox
cuabot chromium # Open sandboxed Chromium
cuabot --screenshot # Take screenshot
cuabot --type "hello" # Type text
cuabot --click 100 200 # Click at coordinates
```
## Documentation
- [Getting Started](https://cua.ai/docs/cuabot/cuabot)
- [Installation Guide](https://cua.ai/docs/cuabot/install)
+83
View File
@@ -0,0 +1,83 @@
name: "CD: @trycua/playground (npm)"
on:
push:
tags:
- "npm-playground-v*"
workflow_dispatch:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
default: ""
workflow_call:
inputs:
version:
description: "Version to publish (default: from package.json)"
required: false
type: string
default: ""
permissions:
id-token: write
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine version
id: get-version
run: |
# Check inputs.version first (works for workflow_call)
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [ "${{ github.event_name }}" == "push" ]; then
# Extract version from tag
if [[ "${{ github.ref }}" =~ ^refs/tags/npm-playground-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
VERSION=${BASH_REMATCH[1]}
else
echo "ERROR: Invalid tag format for npm-playground"
exit 1
fi
elif [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Read from package.json
VERSION=$(node -p "require('./libs/typescript/playground/package.json').version")
fi
echo "Playground version: $VERSION"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=npm-playground-v${VERSION}" >> $GITHUB_OUTPUT
publish:
needs: prepare
uses: ./.github/workflows/ts-reusable-publish.yml
with:
package_name: "playground"
package_dir: "libs/typescript/playground"
package_manager: "pnpm"
version: ${{ needs.prepare.outputs.version }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
needs: [prepare, publish]
uses: ./.github/workflows/release-github-reusable.yml
with:
tag_name: ${{ needs.prepare.outputs.tag }}
release_name: "@trycua/playground v${{ needs.prepare.outputs.version }}"
module_path: "libs/typescript/playground"
body: |
## Installation
```bash
npm install @trycua/playground
```
+80
View File
@@ -0,0 +1,80 @@
name: "CI: Check Docs Links"
on:
pull_request:
paths:
- "docs/content/**"
- "docs/src/**"
- "docs/package.json"
- "docs/scripts/check-links.ts"
- "docs/scripts/check-hygiene.ts"
jobs:
check-internal-links:
name: Check Internal Links (next-validate-link)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install
working-directory: docs
- name: Check internal links
run: pnpm docs:check-links
working-directory: docs
- name: Check docs hygiene
run: pnpm docs:check-hygiene
working-directory: docs
- name: Show help if check failed
if: failure()
run: |
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Broken Documentation Links! ║"
echo "╠══════════════════════════════════════════════════════════════════╣"
echo "║ ║"
echo "║ One or more internal links in the documentation are broken. ║"
echo "║ ║"
echo "║ To find broken links locally, run from the docs/ directory: ║"
echo "║ ║"
echo "║ pnpm docs:check-links ║"
echo "║ ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
check-external-links:
name: Check External Links (lychee)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check external links
uses: lycheeverse/lychee-action@v2
with:
args: >-
--verbose
--no-progress
--accept 100..=399
--scheme https
--scheme http
--root-dir ${{ github.workspace }}/docs/content/docs
--exclude 'localhost'
--exclude '127\.0\.0\.1'
--exclude 'example\.com'
--exclude 'cua\.ai'
--exclude 'platform\.openai\.com'
'./docs/content/**/*.mdx'
token: ${{ secrets.GITHUB_TOKEN }}
fail: true
+174
View File
@@ -0,0 +1,174 @@
name: "CI: Check Docs"
on:
pull_request:
paths:
# Cua Driver (Rust)
- "libs/cua-driver/rust/crates/**"
- "libs/cua-driver/rust/Cargo.toml"
- "libs/cua-driver/rust/Cargo.lock"
# Lume (Swift)
- "libs/lume/src/**"
# Cua CLI (TypeScript)
- "libs/typescript/cua-cli/src/**"
# MCP Server (Python)
- "libs/python/mcp-server/src/**"
# Computer SDK
- "libs/python/computer/computer/**"
- "libs/typescript/computer/src/**"
# Agent SDK
- "libs/python/agent/agent/**"
- "libs/typescript/agent/src/**"
# Cua-Bot
- "libs/cuabot/src/**"
# Documentation files themselves
- "docs/content/docs/reference/cua-driver/**"
- "docs/content/docs/cua/reference/**"
- "docs/content/docs/cuabot/reference/**"
# Generator scripts
- "scripts/docs-generators/**"
jobs:
check-docs-sync:
name: Check Documentation Sync
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for git tags (version discovery) and changed file detection
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node dependencies
run: pnpm install
working-directory: docs
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Python doc dependencies
run: pip install -r scripts/docs-generators/requirements.txt
- name: Determine changed generators
id: changed
run: |
# Diff all commits in this PR against the base branch
BASE_SHA="${{ github.event.pull_request.base.sha }}"
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
# Check which generators need to run
GENERATORS=""
# Source changes
if echo "$CHANGED_FILES" | grep -q "^libs/cua-driver/rust/crates/\|^libs/cua-driver/rust/Cargo.toml\|^libs/cua-driver/rust/Cargo.lock\|^docs/content/docs/reference/cua-driver/"; then
GENERATORS="$GENERATORS cua-driver"
fi
if echo "$CHANGED_FILES" | grep -q "^libs/lume/src/"; then
GENERATORS="$GENERATORS lume"
fi
if echo "$CHANGED_FILES" | grep -q "^libs/typescript/cua-cli/src/"; then
GENERATORS="$GENERATORS cua-cli"
fi
if echo "$CHANGED_FILES" | grep -q "^libs/python/mcp-server/src/"; then
GENERATORS="$GENERATORS mcp-server"
fi
if echo "$CHANGED_FILES" | grep -q "^libs/python/computer/computer/\|^libs/python/agent/agent/"; then
GENERATORS="$GENERATORS python-sdk"
fi
if echo "$CHANGED_FILES" | grep -q "^libs/cuabot/src/"; then
GENERATORS="$GENERATORS cuabot"
fi
# Individual generator script changes — only trigger their own generator
if echo "$CHANGED_FILES" | grep -q "^scripts/docs-generators/cua-driver\.ts"; then
GENERATORS="$GENERATORS cua-driver"
fi
if echo "$CHANGED_FILES" | grep -q "^scripts/docs-generators/lume\.ts"; then
GENERATORS="$GENERATORS lume"
fi
if echo "$CHANGED_FILES" | grep -q "^scripts/docs-generators/python-sdk\.ts"; then
GENERATORS="$GENERATORS python-sdk"
fi
if echo "$CHANGED_FILES" | grep -q "^scripts/docs-generators/cuabot\.ts"; then
GENERATORS="$GENERATORS cuabot"
fi
# Only runner.ts changes trigger all generators (it's the orchestration layer)
# config.json changes only affect whichever generator scripts were also changed
if echo "$CHANGED_FILES" | grep -qE "^scripts/docs-generators/runner\.ts$"; then
GENERATORS="all"
fi
# Deduplicate
GENERATORS=$(echo "$GENERATORS" | tr ' ' '\n' | sort -u | tr '\n' ' ' | xargs)
echo "generators=$GENERATORS" >> $GITHUB_OUTPUT
- name: Build cua-driver (if needed)
if: contains(steps.changed.outputs.generators, 'cua-driver') || steps.changed.outputs.generators == 'all'
run: cargo build -p cua-driver --release
working-directory: libs/cua-driver/rust
- name: Build Lume (if needed)
if: contains(steps.changed.outputs.generators, 'lume') || steps.changed.outputs.generators == 'all'
run: swift build -c release
working-directory: libs/lume
- name: Run documentation check
run: |
GENERATORS="${{ steps.changed.outputs.generators }}"
if [ -z "$GENERATORS" ]; then
echo "No documentation-related changes detected."
exit 0
fi
if [ "$GENERATORS" = "all" ]; then
npx tsx scripts/docs-generators/runner.ts --check
else
for gen in $GENERATORS; do
echo "Checking generator: $gen"
npx tsx scripts/docs-generators/runner.ts --library "$gen" --check
done
fi
working-directory: .
- name: Show help if check failed
if: failure()
run: |
echo ""
echo "╔═══════════════════════════════════════════════════════════════════════╗"
echo "║ Documentation Out of Sync! ║"
echo "╠═══════════════════════════════════════════════════════════════════════╣"
echo "║ ║"
echo "║ The documentation has drifted from the source code. ║"
echo "║ ║"
echo "║ To regenerate all docs, run from the repository root: ║"
echo "║ ║"
echo "║ npx tsx scripts/docs-generators/runner.ts ║"
echo "║ ║"
echo "║ Or for a specific library: ║"
echo "║ ║"
echo "║ npx tsx scripts/docs-generators/runner.ts --library lume ║"
echo "║ npx tsx scripts/docs-generators/runner.ts --library python-sdk ║"
echo "║ npx tsx scripts/docs-generators/runner.ts --library cuabot ║"
echo "║ ║"
echo "║ For versioned docs and changelogs (after tagging a new release): ║"
echo "║ ║"
echo "║ npx tsx scripts/docs-generators/generate-versioned-docs.ts ║"
echo "║ npx tsx scripts/docs-generators/generate-changelog.ts ║"
echo "║ ║"
echo "║ Then commit the updated documentation files. ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════════════╝"
@@ -0,0 +1,41 @@
name: "CI: Cold Start Benchmark"
on:
workflow_dispatch:
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up uv and Python
uses: astral-sh/setup-uv@v4
with:
python-version: "3.12"
- name: Install dependencies
run: |
rm -rf .venv
uv venv --python 3.12
uv pip install -e libs/python/core -e libs/python/computer -e libs/python/cua-sandbox
- name: Run cold start benchmark
id: benchmark
timeout-minutes: 10
run: uv run python tests/cold_start_benchmark.py
env:
CUA_API_KEY: ${{ secrets.CUA_API_KEY }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
- name: Send results to Slack
if: always()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: ${{ vars.SLACK_CHANNEL }}
SLACK_TITLE: "Cloud Sandbox Cold Start Benchmark"
SLACK_COLOR: ${{ env.SLACK_COLOR }}
SLACK_MESSAGE: |
${{ env.SLACK_MESSAGE }}
+16
View File
@@ -0,0 +1,16 @@
name: "CI: Container CuaBot"
on:
pull_request:
paths:
- "libs/cuabot/Dockerfile"
- "libs/cuabot/src/**"
- ".github/workflows/ci-container-cuabot.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: cuabot
context_dir: libs/cuabot
+15
View File
@@ -0,0 +1,15 @@
name: "CI: Container Kasm"
on:
pull_request:
paths:
- "libs/kasm/**"
- ".github/workflows/ci-container-kasm.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: cua-ubuntu
context_dir: libs/kasm
+15
View File
@@ -0,0 +1,15 @@
name: "CI: Container Lumier"
on:
pull_request:
paths:
- "libs/lumier/**"
- ".github/workflows/ci-container-lumier.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: lumier
context_dir: libs/lumier
@@ -0,0 +1,16 @@
name: "CI: Container QEMU Android"
on:
pull_request:
paths:
- "libs/qemu-docker/android/**"
- ".github/workflows/ci-container-qemu-android.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: cua-qemu-android
context_dir: libs/qemu-docker/android
skip_arm64: true # Android QEMU image is not buildable on arm64
@@ -0,0 +1,15 @@
name: "CI: Container QEMU Linux"
on:
pull_request:
paths:
- "libs/qemu-docker/linux/**"
- ".github/workflows/ci-container-qemu-linux.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: cua-qemu-linux
context_dir: libs/qemu-docker/linux
@@ -0,0 +1,15 @@
name: "CI: Container QEMU Windows"
on:
pull_request:
paths:
- "libs/qemu-docker/windows/**"
- ".github/workflows/ci-container-qemu-windows.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: cua-qemu-windows
context_dir: libs/qemu-docker/windows
+15
View File
@@ -0,0 +1,15 @@
name: "CI: Container XFCE"
on:
pull_request:
paths:
- "libs/xfce/**"
- ".github/workflows/ci-container-xfce.yml"
- ".github/workflows/docker-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/docker-reusable-build.yml
with:
image_name: cua-xfce
context_dir: libs/xfce
@@ -0,0 +1,266 @@
name: "CI: cua-driver distro-compat matrix"
# Smoke-tests the *released* cua-driver binary inside real distro containers
# to catch bugs that the NixOS test suite cannot: glibc ABI floor issues and
# distro-packaging gaps (e.g. Qt5 AT-SPI bridge absent on Ubuntu).
#
# What this catches (the NixOS suite CANNOT catch):
# 1. glibc ABI floor — the released binary is built in a debian:11
# container (glibc 2.31) so it should run on all of the matrix distros.
# If someone accidentally bumps the build container to a newer distro the
# --version smoke-test below will fail on the older glibc distros.
# 2. Runtime library gaps — `doctor` queries the OS for required capabilities
# (X11, AT-SPI, etc.). If a distro is missing a package the doctor command
# exits non-zero and prints a human-readable error.
#
# Design principles:
# - Run the RELEASED binary (downloaded from GitHub Releases), not a freshly
# built one. This is the only way to catch ABI mismatches because the NixOS
# CI builds its own binary against NixOS's own glibc.
# - Keep this fast and cheap: containers + --version/doctor only. Full GUI
# behavior stays in the canonical Rust X11 and Wayland E2E workflows.
# - Non-blocking by default (continue-on-error: true) — the released binary
# may not exist yet on a fresh branch; the job is informational until the
# first linux release tag exists.
#
# Trigger: any PR that touches the Rust cua-driver or this workflow.
# Also runs on push to main and on workflow_dispatch so it works as a
# post-release regression guard.
#
# Companion to the Rust desktop E2E workflows; it validates released binaries,
# not source-built harness behavior.
on:
pull_request:
paths:
- "libs/cua-driver/rust/**"
- ".github/workflows/ci-distro-compat-cua-driver.yml"
push:
# Run on main (path-filtered) AND on release tags so a newly-published
# binary is immediately validated against the distro matrix.
branches: [main]
tags:
- "cua-driver-rs-v*"
paths:
- "libs/cua-driver/rust/**"
- ".github/workflows/ci-distro-compat-cua-driver.yml"
workflow_dispatch:
inputs:
version:
description: "cua-driver-rs version to test (without leading v). Leave blank to auto-detect latest."
required: false
default: ""
permissions:
contents: read
jobs:
# ── Resolve the binary version to test ───────────────────────────────────────
# Fetch the latest published cua-driver-rs release tag so individual matrix
# jobs don't each hit the GitHub API. If workflow_dispatch supplied a version
# we use that instead.
resolve-version:
name: Resolve release version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.pick.outputs.version }}
binary_url: ${{ steps.pick.outputs.binary_url }}
steps:
- name: Pick version
id: pick
env:
GH_TOKEN: ${{ github.token }}
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then
VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}"
else
# Fetch the latest release that matches the cua-driver-rs-v* pattern.
# The releases are marked prerelease=true so we use /releases instead
# of /releases/latest (which skips pre-releases).
VERSION=$(gh api repos/trycua/cua/releases \
--jq '[.[] | select(.tag_name | startswith("cua-driver-rs-v"))] | first | .tag_name | ltrimstr("cua-driver-rs-v")' \
2>/dev/null || echo "")
fi
if [[ -z "$VERSION" ]]; then
echo "No cua-driver-rs release found yet — this is expected on a fresh branch."
echo "version=none" >> "$GITHUB_OUTPUT"
echo "binary_url=none" >> "$GITHUB_OUTPUT"
else
BINARY_URL="https://github.com/trycua/cua/releases/download/cua-driver-rs-v${VERSION}/cua-driver-rs-${VERSION}-linux-x86_64-binary.tar.gz"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "binary_url=$BINARY_URL" >> "$GITHUB_OUTPUT"
echo "Will test version: $VERSION"
echo "Binary URL: $BINARY_URL"
fi
# ── Per-distro smoke-test matrix ─────────────────────────────────────────────
# Each job runs in a real distro container, downloads the released binary,
# and asserts that:
# 1. The binary executes at all (catches glibc ABI floor regressions).
# 2. `--version` prints a version string.
# 3. `doctor` exits 0 (or exits non-zero with a parseable diagnostic —
# the doctor command reports capabilities, some may be absent in a
# headless container; we treat a clean exit or a known-missing-display
# exit as success for the ABI test).
#
# Why these distros?
# debian:12 — glibc 2.36, representative of Debian stable users
# ubuntu:22.04 — glibc 2.35, Ubuntu LTS most widely deployed
# ubuntu:24.04 — glibc 2.39, also tests Qt5 AT-SPI bridge gap (see CUA-599)
# rockylinux:9 — glibc 2.34, RHEL/Rocky/AlmaLinux users
# fedora:41 — glibc 2.40, leading-edge RPM users
distro-smoke:
name: "${{ matrix.distro }} (glibc ${{ matrix.glibc_version }})"
needs: resolve-version
# Don't block the PR if no release binary exists yet.
continue-on-error: true
runs-on: ubuntu-latest
container:
image: ${{ matrix.image }}
strategy:
fail-fast: false
matrix:
include:
# Debian family
# X11 runtime libs (libx11-6 libxi6 libxtst6 libxext6) and the
# Wayland client lib (libwayland-client0) are required because
# cua-driver is dynamically linked against the X11 input stack and
# the native Wayland backend (added in #1910). They are the runtime
# counterparts of the build-time deps
# (libx11-dev libxi-dev libxtst-dev libxext-dev libwayland-dev) used
# in the CD workflow. Without them the dynamic linker fails before
# main() and --version exits 127, which the smoke-test correctly
# treats as an ABI error. Installing only curl+ca-certificates is not
# enough.
- distro: "debian:12"
image: "debian:12"
glibc_version: "2.36"
pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libwayland-client0"
- distro: "ubuntu:22.04"
image: "ubuntu:22.04"
glibc_version: "2.35"
pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libwayland-client0"
- distro: "ubuntu:24.04"
image: "ubuntu:24.04"
glibc_version: "2.39"
pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libwayland-client0"
# RPM family
# Rocky Linux 9 ships curl-minimal in the base image which conflicts
# with the full curl package. Use --allowerasing to let dnf replace
# curl-minimal with curl, or skip curl and use curl-minimal (already
# present). We use --allowerasing so the install is explicit and
# consistent with what a user would do on a fresh Rocky install.
- distro: "rockylinux:9"
image: "rockylinux:9"
glibc_version: "2.34"
pkg_install: "dnf install -y --setopt=install_weak_deps=False --allowerasing curl ca-certificates libX11 libXi libXtst libXext libwayland-client"
- distro: "fedora:41"
image: "fedora:41"
glibc_version: "2.40"
pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates libX11 libXi libXtst libXext libwayland-client"
steps:
- name: Skip if no release binary
if: needs.resolve-version.outputs.version == 'none'
run: |
echo "No cua-driver-rs release binary exists yet. Skipping distro smoke-test."
echo "This is expected on branches before the first release tag."
exit 0
- name: Install runtime deps (curl, ca-certificates, X11 libs)
if: needs.resolve-version.outputs.version != 'none'
run: ${{ matrix.pkg_install }}
- name: Download released binary
if: needs.resolve-version.outputs.version != 'none'
env:
BINARY_URL: ${{ needs.resolve-version.outputs.binary_url }}
run: |
echo "Downloading: $BINARY_URL"
curl -fsSL "$BINARY_URL" -o cua-driver.tar.gz
tar -xzf cua-driver.tar.gz
chmod +x cua-driver
ls -lh cua-driver
- name: Verify glibc floor (ldd)
if: needs.resolve-version.outputs.version != 'none'
run: |
# Print glibc version on this host and the minimum version the binary
# requires. This makes CI logs self-explanatory if the binary fails.
echo "=== Host glibc ==="
ldd --version | head -1 || true
echo "=== Binary glibc requirements ==="
# objdump / readelf may not be installed in minimal containers;
# strings is more universally available.
strings cua-driver | grep -E "^GLIBC_[0-9]" | sort -V | tail -5 || true
echo "=== Binary shared-library dependencies (ldd) ==="
# ldd shows all dynamic deps and flags any missing ones. This is
# purely informational — failures here are diagnosed at smoke-test time.
ldd ./cua-driver 2>&1 || true
- name: Smoke-test --version
if: needs.resolve-version.outputs.version != 'none'
run: |
echo "=== cua-driver --version ==="
# This is the primary ABI-floor gate: if the binary can't even print
# its version the glibc requirement is too high for this distro.
VERSION_OUT=$(./cua-driver --version)
echo "Output: $VERSION_OUT"
# Sanity-check that the output contains a version number.
if ! echo "$VERSION_OUT" | grep -qE "[0-9]+\.[0-9]+\.[0-9]+"; then
echo "ERROR: --version output does not contain a semver string"
exit 1
fi
echo "PASS: --version"
- name: Smoke-test doctor
if: needs.resolve-version.outputs.version != 'none'
run: |
echo "=== cua-driver doctor ==="
# doctor checks for runtime capabilities (display, AT-SPI, etc.).
# In a headless container many capabilities will be absent — that's
# expected and NOT a failure. What we test here is that:
# a. The binary loads and runs the doctor subcommand at all.
# b. It exits with a parseable status (not a SIGILL / glibc symbol
# error which would manifest as exit code 127 or similar).
set +e
./cua-driver doctor 2>&1
EXIT_CODE=$?
set -e
echo "doctor exit code: $EXIT_CODE"
# Exit codes that indicate glibc/ABI failure (command not found / bad ELF):
if [[ $EXIT_CODE -eq 127 || $EXIT_CODE -eq 126 ]]; then
echo "ERROR: cua-driver failed to execute (exit $EXIT_CODE) — likely glibc ABI mismatch"
exit 1
fi
# Treat 0 (all capabilities present) or 1 (capabilities missing but
# doctor ran) as success — both mean the binary loaded correctly.
echo "PASS: doctor ran without ABI error"
# ── Summary job ──────────────────────────────────────────────────────────────
# A single job that other status checks can require. Marks green when all
# distro-smoke jobs pass (or when the binary doesn't exist yet and all
# continue-on-error jobs skipped).
distro-compat-summary:
name: "Distro compat summary"
needs: [resolve-version, distro-smoke]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check results
run: |
echo "resolve-version result: ${{ needs.resolve-version.result }}"
echo "distro-smoke result: ${{ needs.distro-smoke.result }}"
# If resolve-version failed (API error etc.) that's a real failure.
if [[ "${{ needs.resolve-version.result }}" == "failure" ]]; then
echo "ERROR: resolve-version job failed"
exit 1
fi
# distro-smoke is continue-on-error so its result is always
# 'success' even when individual jobs fail. The individual job
# logs are the source of truth; this summary job just gates
# the overall workflow status.
echo "All distro compat checks completed."
+35
View File
@@ -0,0 +1,35 @@
name: "CI: Lint Python"
on:
pull_request:
paths:
- "libs/python/**"
- "pyproject.toml"
- ".github/workflows/ci-lint-python.yml"
jobs:
lint:
name: Python Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install Python dependencies
run: |
pip install uv
uv sync
- name: Python lint & typecheck
run: |
uv run isort --check-only libs/python tests
uv run black --check libs/python tests
uv run ruff check libs/python tests
# Temporarily disabled due to untyped codebase
# uv run mypy .
+35
View File
@@ -0,0 +1,35 @@
name: "CI: Lint TypeScript"
on:
pull_request:
paths:
- "libs/typescript/**"
- ".github/workflows/ci-lint-typescript.yml"
jobs:
lint:
name: TypeScript Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: libs/typescript/package.json
- name: Install Node dependencies
run: pnpm -C libs/typescript install --frozen-lockfile
- name: TypeScript typecheck
run: pnpm -C libs/typescript typecheck
- name: Prettier check
run: pnpm -C libs/typescript format:check
+56
View File
@@ -0,0 +1,56 @@
name: "CI: Nix Linux Rust source"
# Nix owns reproducible Linux package and Rust-source builds. Interactive
# desktop behavior is owned by the Rust E2E workflows.
on:
pull_request:
paths:
- "flake.nix"
- "flake.lock"
- "nix/cua-driver/**"
- "libs/cua-driver/rust/**"
- ".github/workflows/ci-nix-linux.yml"
push:
branches: [main]
paths:
- "flake.nix"
- "flake.lock"
- "nix/cua-driver/**"
- "libs/cua-driver/rust/**"
- ".github/workflows/ci-nix-linux.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-nix-linux-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
rust-source:
name: Nix / ${{ matrix.name }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- name: compositor build
attribute: cua-compositor-build
- name: driver package
attribute: cua-driver-build
- name: Rust unit tests
attribute: cua-driver-linux-rust-unit
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Build ${{ matrix.name }}
run: >-
nix build
.#checks.x86_64-linux.${{ matrix.attribute }}
--no-link --print-build-logs --show-trace
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-agent"
on:
pull_request:
paths:
- "libs/python/agent/**"
- ".github/workflows/ci-py-agent.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "agent"
package_dir: "libs/python/agent"
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-bench-ui"
on:
pull_request:
paths:
- "libs/python/bench-ui/**"
- ".github/workflows/ci-py-bench-ui.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "bench-ui"
package_dir: "libs/python/bench-ui"
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-bench"
on:
pull_request:
paths:
- "libs/cua-bench/**"
- ".github/workflows/ci-py-bench.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "bench"
package_dir: "libs/cua-bench"
@@ -0,0 +1,15 @@
name: "CI: cua-computer-server"
on:
pull_request:
paths:
- "libs/python/computer-server/**"
- ".github/workflows/ci-py-computer-server.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "computer-server"
package_dir: "libs/python/computer-server"
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-computer"
on:
pull_request:
paths:
- "libs/python/computer/**"
- ".github/workflows/ci-py-computer.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "computer"
package_dir: "libs/python/computer"
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-core"
on:
pull_request:
paths:
- "libs/python/core/**"
- ".github/workflows/ci-py-core.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "core"
package_dir: "libs/python/core"
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-mcp-server"
on:
pull_request:
paths:
- "libs/python/mcp-server/**"
- ".github/workflows/ci-py-mcp-server.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "mcp-server"
package_dir: "libs/python/mcp-server"
+15
View File
@@ -0,0 +1,15 @@
name: "CI: cua-som"
on:
pull_request:
paths:
- "libs/python/som/**"
- ".github/workflows/ci-py-som.yml"
- ".github/workflows/py-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/py-reusable-build.yml
with:
package_name: "som"
package_dir: "libs/python/som"
+132
View File
@@ -0,0 +1,132 @@
name: "CI: Release Reminder"
on:
pull_request:
types: [opened, synchronize, labeled, unlabeled]
jobs:
release-reminder:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check for publishable changes and remind
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
# Get changed files in this PR
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only 2>/dev/null || true)
if [ -z "$CHANGED_FILES" ]; then
exit 0
fi
# Map paths to publishable services
declare -A PATH_TO_SERVICE=(
["libs/python/cua-cli/"]="pypi/cli"
["libs/python/cua-auto/"]="pypi/auto"
["libs/python/agent/"]="pypi/agent"
["libs/python/computer/"]="pypi/computer"
["libs/python/core/"]="pypi/core"
["libs/python/som/"]="pypi/som"
["libs/python/bench-ui/"]="pypi/bench-ui"
["libs/cua-bench/"]="pypi/bench"
["libs/python/computer-server/"]="pypi/computer-server"
["libs/python/mcp-server/"]="pypi/mcp-server"
["libs/typescript/cua-cli/"]="npm/cli"
["libs/typescript/computer/"]="npm/computer"
["libs/typescript/core/"]="npm/core"
["libs/typescript/playground/"]="npm/playground"
["libs/cuabot/"]="npm/cuabot"
["libs/xfce/"]="docker/xfce"
["libs/kasm/"]="docker/kasm"
["libs/lumier/"]="docker/lumier"
["libs/qemu-docker/android/"]="docker/qemu-android"
["libs/qemu-docker/linux/"]="docker/qemu-linux"
["libs/qemu-docker/windows/"]="docker/qemu-windows"
["libs/lume/"]="lume"
["libs/cua-driver/rust/"]="cua-driver-rs"
)
# Find affected services
AFFECTED=()
for path_prefix in "${!PATH_TO_SERVICE[@]}"; do
if echo "$CHANGED_FILES" | grep -q "^${path_prefix}"; then
AFFECTED+=("${PATH_TO_SERVICE[$path_prefix]}")
fi
done
# Deduplicate and sort
IFS=$'\n' AFFECTED=($(printf '%s\n' "${AFFECTED[@]}" | sort -u)); unset IFS
if [ ${#AFFECTED[@]} -eq 0 ]; then
echo "No publishable packages affected."
exit 0
fi
# Check which ones already have release labels
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
HAS_NO_RELEASE=false
if echo "$LABELS" | grep -q '"no-release"'; then
HAS_NO_RELEASE=true
fi
LABELED=()
UNLABELED=()
for svc in "${AFFECTED[@]}"; do
if echo "$LABELS" | grep -q "\"release:${svc}\""; then
LABELED+=("$svc")
else
UNLABELED+=("$svc")
fi
done
# Build comment body
COMMENT_MARKER="<!-- release-reminder -->"
BODY="${COMMENT_MARKER}\n"
BODY+="### 📦 Publishable packages changed\n\n"
if [ ${#LABELED[@]} -gt 0 ]; then
for svc in "${LABELED[@]}"; do
BODY+="- [x] \`${svc}\` — will auto-release on merge\n"
done
fi
if [ ${#UNLABELED[@]} -gt 0 ]; then
for svc in "${UNLABELED[@]}"; do
if [ "$HAS_NO_RELEASE" = "true" ]; then
BODY+="- [x] ~\`${svc}\`~ — skipped (no-release)\n"
else
BODY+="- [ ] \`${svc}\`\n"
fi
done
fi
if [ "$HAS_NO_RELEASE" = "false" ] && [ ${#UNLABELED[@]} -gt 0 ]; then
BODY+="\nAdd \`release:<service>\` labels to auto-release on merge"
BODY+=" (+ optional \`bump:minor\` or \`bump:major\`, default is patch)."
BODY+="\nOr add \`no-release\` to skip."
fi
if printf '%s\n' "${AFFECTED[@]}" | grep -qx 'cua-driver-rs'; then
BODY+="\n\n### cua-driver desktop release validation\n\n"
BODY+="Before adding the release label, a maintainer records green runs on the exact release SHA:\n"
BODY+="- [ ] [Windows canonical matrix](https://github.com/${REPO}/actions/workflows/e2e-rust-windows.yml)\n"
BODY+="- [ ] macOS logged-in host: \`scripts/ci/macos/run-rust-e2e.sh\`, with the typed summary attached to the PR\n"
BODY+="- [ ] [Linux X11 canonical matrix](https://github.com/${REPO}/actions/workflows/e2e-rust-linux.yml)\n"
BODY+="- [ ] [Linux Sway canonical matrix](https://github.com/${REPO}/actions/workflows/e2e-rust-linux-wayland.yml) with environment \`sway\`\n"
BODY+="- [ ] Result links and remaining experimental environment gaps are recorded in the PR\n"
fi
# Delete previous reminder comment if exists, then post new one
EXISTING_COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id" 2>/dev/null | head -1)
if [ -n "$EXISTING_COMMENT_ID" ]; then
gh api -X DELETE "repos/${REPO}/issues/${PR_NUMBER}/comments/${EXISTING_COMMENT_ID}" 2>/dev/null || true
fi
echo -e "$BODY" | gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file -
+57
View File
@@ -0,0 +1,57 @@
name: "CI: Rust Linux unit"
on:
pull_request:
paths:
- "libs/cua-driver/rust/Cargo.toml"
- "libs/cua-driver/rust/Cargo.lock"
- "libs/cua-driver/rust/crates/cua-driver/**"
- "libs/cua-driver/rust/crates/cua-driver-core/**"
- "libs/cua-driver/rust/crates/cua-driver-testkit/**"
- "libs/cua-driver/rust/crates/platform-linux/**"
- "libs/cua-driver/tests/fixtures/**"
- "nix/**"
- "flake.nix"
- "flake.lock"
- ".github/workflows/ci-rust-linux.yml"
push:
branches: [main]
paths:
- "libs/cua-driver/rust/**"
- "libs/cua-driver/tests/fixtures/**"
- "nix/**"
- "flake.nix"
- "flake.lock"
- ".github/workflows/ci-rust-linux.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-rust-linux-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
unit:
name: Rust Linux unit and compile
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
workspaces: "libs/cua-driver/rust -> target"
- name: Install Linux build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang pkg-config libdbus-1-dev libpipewire-0.3-dev libspa-0.2-dev \
libei-dev libxkbcommon-dev libx11-dev libxi-dev libxtst-dev libxext-dev
- name: Run Linux Rust tests
working-directory: libs/cua-driver/rust
run: cargo test -p cua-driver -p cua-driver-core -p cua-driver-testkit -p platform-linux --all-targets --locked
- name: Compile portable GNOME/KDE portal input
working-directory: libs/cua-driver/rust
run: cargo check -p cua-driver --features portal-input --locked
+45
View File
@@ -0,0 +1,45 @@
name: "CI: Rust Windows unit"
on:
pull_request:
paths:
- "libs/cua-driver/rust/Cargo.toml"
- "libs/cua-driver/rust/Cargo.lock"
- "libs/cua-driver/rust/crates/cua-driver/**"
- "libs/cua-driver/rust/crates/cua-driver-core/**"
- "libs/cua-driver/rust/crates/cua-driver-testkit/**"
- "libs/cua-driver/rust/crates/platform-windows/**"
- "libs/cua-driver/rust/crates/cua-driver-uia/**"
- "libs/cua-driver/tests/fixtures/**"
- ".github/workflows/ci-rust-windows.yml"
push:
branches: [main]
paths:
- "libs/cua-driver/rust/**"
- "libs/cua-driver/tests/fixtures/**"
- ".github/workflows/ci-rust-windows.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-rust-windows-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
unit:
name: Rust Windows unit and compile
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
workspaces: "libs/cua-driver/rust -> target"
- name: Run Windows Rust tests
working-directory: libs/cua-driver/rust
# Compile every Rust target without executing desktop-dependent integration
# tests; interactive behavior belongs in e2e-rust-windows.yml.
run: cargo test -p cua-driver -p cua-driver-core -p cua-driver-testkit -p platform-windows -p cua-driver-uia --all-targets --no-run --locked
+54
View File
@@ -0,0 +1,54 @@
name: "CI: SPDX Headers"
# Verifies that every cua-driver source file carries an SPDX-License-Identifier
# header. Currently warn-only (continue-on-error: true) while we stamp the
# existing tree in a separate sweep PR; will be flipped to enforcing once the
# tree is clean. See scripts/spdx-headers.py for the applier and the runbook.
on:
pull_request:
paths:
- "libs/cua-driver/**/*.swift"
- "libs/cua-driver/**/*.rs"
- "scripts/spdx-headers.py"
- ".github/workflows/ci-spdx-headers.yml"
push:
branches: [main]
paths:
- "libs/cua-driver/**/*.swift"
- "libs/cua-driver/**/*.rs"
- "scripts/spdx-headers.py"
jobs:
check:
name: Check SPDX headers (warn-only)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check SPDX headers
id: spdx
continue-on-error: true
run: python3 scripts/spdx-headers.py --check libs/cua-driver
- name: Show help if check failed
if: steps.spdx.outcome == 'failure'
run: |
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Missing SPDX-License-Identifier headers ║"
echo "╠══════════════════════════════════════════════════════════════════╣"
echo "║ ║"
echo "║ This check is WARN-ONLY today; it will become enforcing once ║"
echo "║ the existing tree has been stamped in a follow-up sweep PR. ║"
echo "║ ║"
echo "║ To stamp new files locally: ║"
echo "║ ║"
echo "║ python3 scripts/spdx-headers.py --apply libs/cua-driver ║"
echo "║ ║"
echo "║ To preview without writing: ║"
echo "║ ║"
echo "║ python3 scripts/spdx-headers.py --dry-run libs/cua-driver ║"
echo "║ ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
+32
View File
@@ -0,0 +1,32 @@
name: "CI: Lume"
on:
pull_request:
paths:
- "libs/lume/**"
- ".github/workflows/ci-swift-lume.yml"
concurrency:
group: lume-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Runner images: https://github.com/actions/runner-images
jobs:
test:
name: Test
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- run: uname -a
- run: sudo xcode-select -s /Applications/Xcode_16.3.app # Swift 6.1
- run: swift test
working-directory: ./libs/lume
build:
name: Release build
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- run: uname -a
- run: sudo xcode-select -s /Applications/Xcode_16.3.app # Swift 6.1
- run: swift build --configuration release
working-directory: ./libs/lume
+386
View File
@@ -0,0 +1,386 @@
name: "CI: Test Models"
# This workflow tests all supported Cua models with API keys
# Runs weekly on Monday or manually via workflow_dispatch
on:
workflow_dispatch:
inputs:
test_models:
description: "Test all supported models (requires API keys)"
required: false
default: true
type: boolean
schedule:
- cron: '0 9 * * 1' # Every Monday at 9:00 UTC
jobs:
# Test all Cua models - runs on PRs, pushes to main, or when manually triggered
test-all-models:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
model:
# Claude Sonnet/Haiku
- anthropic/claude-sonnet-4-6
- anthropic/claude-sonnet-4-5-20250929
- anthropic/claude-haiku-4-5-20251001
- anthropic/claude-opus-4-6
- anthropic/claude-opus-4-5-20251101
- anthropic/claude-opus-4-1-20250805
# OpenAI CU Preview
- openai/computer-use-preview
# GLM-V
- openrouter/z-ai/glm-4.5v
# - huggingface-local/zai-org/GLM-4.5V # Requires local model setup
# Gemini CU Preview
- gemini-2.5-computer-use-preview-10-2025
# Cua VLM Router Models (via inference.cua.ai)
- cua/anthropic/claude-haiku-4.5
- cua/anthropic/claude-sonnet-4.5
- cua/anthropic/claude-opus-4.5
- cua/microsoft/fara-7b
- cua/google/gemini-3-flash-preview
- cua/google/gemini-3-pro-preview
# InternVL
# - huggingface-local/OpenGVLab/InternVL3_5-1B
# - huggingface-local/OpenGVLab/InternVL3_5-2B
# - huggingface-local/OpenGVLab/InternVL3_5-4B
# - huggingface-local/OpenGVLab/InternVL3_5-8B
# UI-TARS (supports full computer-use, can run standalone)
# - huggingface-local/ByteDance-Seed/UI-TARS-1.5-7B
# Note: OpenCUA, GTA, and Holo are grounding-only models
# They only support predict_click(), not agent.run()
# See composed agents section below for testing them
# Moondream (typically used in composed agents)
# Format: moondream3+{any-llm-with-tools}
# - moondream3+anthropic/claude-sonnet-4-5-20250929 # Claude has VLM + Tools
# - moondream3+openai/gpt-4o # GPT-4o has VLM + Tools
# OmniParser (typically used in composed agents)
# Format: omniparser+{any-vlm-with-tools}
- omniparser+anthropic/claude-sonnet-4-5-20250929 # Claude has VLM + Tools
# - omniparser+openai/gpt-4o # GPT-4o has VLM + Tools
# Other grounding models + VLM with tools
# Format: {grounding-model}+{any-vlm-with-tools}
# These grounding-only models (OpenCUA, GTA, Holo) must be used in composed form
# since they only support predict_click(), not full agent.run()
# - huggingface-local/HelloKKMe/GTA1-7B+anthropic/claude-sonnet-4-5-20250929
# - huggingface-local/xlangai/OpenCUA-7B+anthropic/claude-sonnet-4-5-20250929
# - huggingface-local/Hcompany/Holo1.5-3B+anthropic/claude-sonnet-4-5-20250929
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up uv and Python
uses: astral-sh/setup-uv@v4
with:
python-version: "3.12"
- name: Cache system packages
uses: actions/cache@v4
with:
path: /var/cache/apt
key: ${{ runner.os }}-apt-${{ hashFiles('**/Dockerfile') }}
restore-keys: |
${{ runner.os }}-apt-
- name: Install system dependencies
timeout-minutes: 20
run: |
sudo apt-get update
sudo apt-get install -y libgl1-mesa-dri libglib2.0-0
- name: Cache Python dependencies (uv)
uses: actions/cache@v4
with:
path: |
~/.cache/uv
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml', 'uv.lock', 'libs/python/**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-uv-
- name: Install Cua dependencies (uv)
run: |
# Remove existing venv if it exists (from cache restore) to avoid interactive prompt
rm -rf .venv
uv venv --python 3.12
uv pip install -e libs/python/agent -e libs/python/computer
uv pip install -e libs/python/core
uv pip install "cua-agent[uitars-hf,internvl-hf,opencua-hf,moondream3,omni]"
uv pip install pytest
- name: Cache HuggingFace models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: ${{ runner.os }}-hf-models-v1
restore-keys: |
${{ runner.os }}-hf-models-
# Large cache - models can be several GB each and are reused across runs
- name: Record test start time
run: echo "TEST_START_TIME=$(date +%s)" >> $GITHUB_ENV
env:
# Ensure HuggingFace uses consistent cache location
HF_HOME: ~/.cache/huggingface
- name: Test model with agent loop
id: test_model
timeout-minutes: 20
continue-on-error: true
run: |
cd tests/agent_loop_testing
uv run python agent_test.py --model "${{ matrix.model }}"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
CUA_API_KEY: ${{ secrets.CUA_API_KEY }}
- name: Calculate test duration and prepare message
if: always()
run: |
TEST_END_TIME=$(date +%s)
# Handle case where TEST_START_TIME might not be set
if [ -z "$TEST_START_TIME" ]; then
TEST_START_TIME=$TEST_END_TIME
fi
TEST_DURATION=$((TEST_END_TIME - TEST_START_TIME))
# Convert seconds to minutes and seconds
MINUTES=$((TEST_DURATION / 60))
SECONDS=$((TEST_DURATION % 60))
# Format duration
if [ $MINUTES -gt 0 ]; then
DURATION_STR="${MINUTES}m ${SECONDS}s"
else
DURATION_STR="${SECONDS}s"
fi
# Determine status icon based on test step outcome
if [ "${{ steps.test_model.outcome }}" == "success" ]; then
STATUS_ICON="✅"
STATUS_TEXT="PASSED"
SLACK_COLOR="#36a64f"
else
STATUS_ICON="❌"
STATUS_TEXT="FAILED"
SLACK_COLOR="#dc3545"
fi
# Prepare Slack message
echo "TESTS_CONTENT<<EOF" >> $GITHUB_ENV
echo "*Cua Model Test Results*" >> $GITHUB_ENV
echo "" >> $GITHUB_ENV
echo "*Model:* ${{ matrix.model }}" >> $GITHUB_ENV
echo "*Status:* ${STATUS_ICON} ${STATUS_TEXT}" >> $GITHUB_ENV
echo "*Duration:* ${DURATION_STR}" >> $GITHUB_ENV
echo "*Run:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# Set color based on outcome
echo "SLACK_COLOR=${SLACK_COLOR}" >> $GITHUB_ENV
# Save result to JSON file for summary
mkdir -p test_summary
MODEL_NAME="${{ matrix.model }}"
# Sanitize model name for filename
SAFE_MODEL_NAME=$(echo "$MODEL_NAME" | sed 's/[^a-zA-Z0-9]/_/g')
# Determine pass status
if [ "${{ steps.test_model.outcome }}" == "success" ]; then
PASSED_VAL="true"
else
PASSED_VAL="false"
fi
# Create JSON file using printf to avoid YAML parsing issues
printf '{\n "model": "%s",\n "status": "%s",\n "status_icon": "%s",\n "duration": "%s",\n "duration_seconds": %d,\n "passed": %s\n}' \
"${MODEL_NAME}" "${STATUS_TEXT}" "${STATUS_ICON}" "${DURATION_STR}" "${TEST_DURATION}" "${PASSED_VAL}" \
> "test_summary/${SAFE_MODEL_NAME}.json"
# Expose safe model name for subsequent steps (artifact naming)
echo "SAFE_MODEL_NAME=${SAFE_MODEL_NAME}" >> $GITHUB_ENV
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.model }}
path: |
tests/agent_loop_testing/test_images/
*.log
if-no-files-found: ignore
retention-days: 7
- name: Upload test summary data
if: always()
uses: actions/upload-artifact@v4
with:
# Unique, slash-free artifact name per matrix entry
name: test-summary-${{ env.SAFE_MODEL_NAME }}
path: test_summary/
if-no-files-found: ignore
retention-days: 1
- name: Set default Slack color
if: always() && env.SLACK_COLOR == ''
run: echo "SLACK_COLOR=#36a64f" >> $GITHUB_ENV
# Individual model notifications disabled - only summary is sent
# - name: Notify Slack with test results
# if: always()
# uses: rtCamp/action-slack-notify@v2
# env:
# SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
# SLACK_CHANNEL: ${{ vars.SLACK_CHANNEL }}
# SLACK_TITLE: Cua Model Test Update
# SLACK_COLOR: ${{ env.SLACK_COLOR }}
# SLACK_MESSAGE: |
# ${{ env.TESTS_CONTENT }}
# Summary job that aggregates all model test results
test-summary:
if: ${{ always() }}
needs: test-all-models
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Download all test summary artifacts
continue-on-error: true
uses: actions/download-artifact@v4
with:
pattern: test-summary-*
merge-multiple: true
path: all_summaries
- name: Generate and send summary
if: always()
shell: bash
run: |
# Create directory if it doesn't exist
mkdir -p all_summaries
# Get list of models being tested in this run from the matrix
# This helps filter out artifacts from previous runs when testing locally
EXPECTED_MODELS="${{ join(matrix.model, ' ') }}"
# Aggregate all results
PASSED_COUNT=0
FAILED_COUNT=0
TOTAL_DURATION=0
SUMMARY_MESSAGE="*🚀 Model Summaries*\n\n"
# Process each JSON file (find all JSON files recursively)
# Save to temp file first to avoid subshell issues
find all_summaries -name "*.json" -type f 2>/dev/null > /tmp/json_files.txt || true
# Use associative array to deduplicate by model name
declare -A processed_models
while IFS= read -r json_file; do
if [ -f "$json_file" ]; then
MODEL=$(jq -r '.model' "$json_file")
# Skip if we've already processed this model
if [ "${processed_models[$MODEL]}" = "1" ]; then
echo "Skipping duplicate model: $MODEL"
continue
fi
# Filter: Only include models that are in the current matrix
# This prevents including artifacts from previous workflow runs
if [ -n "$EXPECTED_MODELS" ]; then
if ! echo "$EXPECTED_MODELS" | grep -q "$MODEL"; then
echo "Skipping model from previous run: $MODEL"
continue
fi
fi
# Mark as processed
processed_models[$MODEL]="1"
STATUS_ICON=$(jq -r '.status_icon' "$json_file")
STATUS=$(jq -r '.status' "$json_file")
DURATION=$(jq -r '.duration' "$json_file")
DURATION_SEC=$(jq -r '.duration_seconds' "$json_file")
PASSED=$(jq -r '.passed' "$json_file")
# Add to summary as clean line format
SUMMARY_MESSAGE="${SUMMARY_MESSAGE}${STATUS_ICON} ${STATUS} - \`${MODEL}\` - ${DURATION}\n"
if [ "$PASSED" = "true" ]; then
PASSED_COUNT=$((PASSED_COUNT + 1))
else
FAILED_COUNT=$((FAILED_COUNT + 1))
fi
TOTAL_DURATION=$((TOTAL_DURATION + DURATION_SEC))
fi
done < /tmp/json_files.txt
# Check if we found any results
TOTAL_COUNT=$((PASSED_COUNT + FAILED_COUNT))
if [ $TOTAL_COUNT -eq 0 ]; then
SUMMARY_MESSAGE="${SUMMARY_MESSAGE}⚠️ No test results found (workflow may have been canceled)\n"
SLACK_COLOR="#ffa500"
else
# Add summary stats
SUMMARY_MESSAGE="${SUMMARY_MESSAGE}\n*Results:* ${PASSED_COUNT} passed, ${FAILED_COUNT} failed out of ${TOTAL_COUNT} models\n"
# Calculate total duration
TOTAL_MIN=$((TOTAL_DURATION / 60))
TOTAL_SEC=$((TOTAL_DURATION % 60))
if [ $TOTAL_MIN -gt 0 ]; then
TOTAL_DURATION_STR="${TOTAL_MIN}m ${TOTAL_SEC}s"
else
TOTAL_DURATION_STR="${TOTAL_SEC}s"
fi
SUMMARY_MESSAGE="${SUMMARY_MESSAGE}*Total Duration:* ${TOTAL_DURATION_STR}\n"
# Determine color based on results
if [ $FAILED_COUNT -eq 0 ]; then
SLACK_COLOR="#36a64f"
elif [ $PASSED_COUNT -eq 0 ]; then
SLACK_COLOR="#dc3545"
else
SLACK_COLOR="#ffa500"
fi
fi
SUMMARY_MESSAGE="${SUMMARY_MESSAGE}*Run:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# Export for use in next step
echo "SUMMARY_MESSAGE<<EOF" >> $GITHUB_ENV
echo -e "${SUMMARY_MESSAGE}" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "SLACK_COLOR=${SLACK_COLOR}" >> $GITHUB_ENV
- name: Send summary to Slack
if: always()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: ${{ vars.SLACK_CHANNEL }}
SLACK_TITLE: Cua Models Test Summary
SLACK_COLOR: ${{ env.SLACK_COLOR }}
SLACK_MESSAGE: |
${{ env.SUMMARY_MESSAGE }}
+94
View File
@@ -0,0 +1,94 @@
name: "CI: Test Python"
on:
pull_request:
paths:
- "libs/python/**"
- ".github/workflows/ci-test-python.yml"
workflow_dispatch: # Allow manual trigger
jobs:
test:
name: Test ${{ matrix.package }}
runs-on: ubuntu-latest
strategy:
fail-fast: false # Test all packages even if one fails
matrix:
package:
- core
- agent
- computer
- computer-server
- mcp-server
- som
- cua-auto
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
run: |
pip install uv
- name: Install package and dependencies
run: |
cd libs/python/${{ matrix.package }}
# Install the package in editable mode with dev dependencies
if [ -f pyproject.toml ]; then
uv pip install --system -e .
fi
shell: bash
- name: Install test dependencies
run: |
# Install test dependencies from root pyproject.toml if tests directory exists
# The root pyproject.toml has package=false, so we install just the dependency group
if [ -d "libs/python/${{ matrix.package }}/tests" ]; then
uv pip install --system --group test
fi
shell: bash
- name: Run tests
run: |
cd libs/python/${{ matrix.package }}
if [ -d tests ]; then
python -m pytest tests/ -v --tb=short --cov --cov-report=term --cov-report=xml
else
echo "No tests directory found, skipping tests"
fi
shell: bash
env:
CUA_TELEMETRY_ENABLED: "false" # Disable telemetry during tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
if: always()
with:
file: ./libs/python/${{ matrix.package }}/coverage.xml
flags: ${{ matrix.package }}
name: codecov-${{ matrix.package }}
fail_ci_if_error: false
continue-on-error: true
summary:
name: Test Summary
runs-on: ubuntu-latest
needs: test
if: always()
steps:
- name: Check test results
run: |
if [ "${{ needs.test.result }}" == "failure" ]; then
echo "❌ Some tests failed. Please check the logs above."
exit 1
else
echo "✅ All tests passed!"
fi
+30
View File
@@ -0,0 +1,30 @@
name: "CI: Test Scripts"
on:
pull_request:
paths:
- ".github/scripts/**"
- ".github/workflows/ci-test-scripts.yml"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest toml
- name: Run tests
run: |
cd .github/scripts
pytest tests/ -v
+16
View File
@@ -0,0 +1,16 @@
name: "CI: @trycua/cli"
on:
pull_request:
paths:
- "libs/typescript/cua-cli/**"
- ".github/workflows/ci-ts-cli.yml"
- ".github/workflows/ts-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/ts-reusable-build.yml
with:
package_name: "cli"
package_dir: "libs/typescript/cua-cli"
package_manager: "bun"
+16
View File
@@ -0,0 +1,16 @@
name: "CI: @trycua/computer"
on:
pull_request:
paths:
- "libs/typescript/computer/**"
- ".github/workflows/ci-ts-computer.yml"
- ".github/workflows/ts-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/ts-reusable-build.yml
with:
package_name: "computer"
package_dir: "libs/typescript/computer"
package_manager: "pnpm"
+16
View File
@@ -0,0 +1,16 @@
name: "CI: @trycua/core"
on:
pull_request:
paths:
- "libs/typescript/core/**"
- ".github/workflows/ci-ts-core.yml"
- ".github/workflows/ts-reusable-build.yml"
jobs:
build:
uses: ./.github/workflows/ts-reusable-build.yml
with:
package_name: "core"
package_dir: "libs/typescript/core"
package_manager: "pnpm"
+509
View File
@@ -0,0 +1,509 @@
name: Claude Auto Fix
on:
pull_request:
types: [labeled]
permissions:
actions: read
contents: write
pull-requests: write
id-token: write
concurrency:
group: auto-fix-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
auto-fix:
name: Auto-fix CI Failure
# Use ubuntu-22.04 to avoid AppArmor restrictions on bubblewrap in 24.04
runs-on: ubuntu-22.04
if: github.event.label.name == 'auto-fix'
steps:
- name: Determine context
id: context
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const prNumber = pr.number.toString();
const targetBranch = pr.head.ref;
let failedRunId = '';
let failedRunTimestamp = null;
let failedWorkflowName = '';
const workflowFileMap = {
"CI: Lint Python": "ci-lint-python.yml",
"CI: Lint TypeScript": "ci-lint-typescript.yml",
"CI: Test Python": "ci-test-python.yml",
"CI: Test Scripts": "ci-test-scripts.yml",
"CI: cua-agent": "ci-py-agent.yml",
"CI: cua-core": "ci-py-core.yml",
"CI: cua-computer": "ci-py-computer.yml",
"CI: cua-computer-server": "ci-py-computer-server.yml",
"CI: cua-mcp-server": "ci-py-mcp-server.yml",
"CI: cua-som": "ci-py-som.yml",
"CI: cua-bench": "ci-py-bench.yml",
"CI: cua-bench-ui": "ci-py-bench-ui.yml",
"CI: @trycua/cli": "ci-ts-cli.yml",
"CI: @trycua/computer": "ci-ts-computer.yml",
"CI: @trycua/core": "ci-ts-core.yml"
};
console.log(`Looking for failed runs on branch: ${targetBranch}`);
for (const [workflowName, workflowFile] of Object.entries(workflowFileMap)) {
try {
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowFile,
branch: targetBranch,
status: 'failure',
per_page: 1
});
if (runs.workflow_runs.length > 0) {
const run = runs.workflow_runs[0];
const runTimestamp = new Date(run.created_at);
if (!failedRunTimestamp || runTimestamp > failedRunTimestamp) {
failedRunId = run.id.toString();
failedRunTimestamp = runTimestamp;
failedWorkflowName = run.name;
console.log(`Found failed run: ${run.name} (${run.id})`);
}
}
} catch (e) {
console.log(`Could not check workflow ${workflowName}: ${e.message}`);
}
}
if (!failedRunId) {
console.log('No failed workflow runs found for this PR');
}
console.log(`target_branch: ${targetBranch}`);
console.log(`pr_number: ${prNumber}`);
console.log(`failed_run_id: ${failedRunId}`);
console.log(`failed_workflow_name: ${failedWorkflowName}`);
core.setOutput('target_branch', targetBranch);
core.setOutput('pr_number', prNumber);
core.setOutput('failed_run_id', failedRunId);
core.setOutput('failed_workflow_name', failedWorkflowName);
core.setOutput('has_failed_run', failedRunId ? 'true' : 'false');
- name: Skip - no failed run found
if: steps.context.outputs.has_failed_run != 'true'
run: |
echo "::notice::No failed CI run found for this PR. The auto-fix label was added but there's nothing to fix."
- name: Record start time
if: steps.context.outputs.has_failed_run == 'true'
id: start-time
run: |
echo "start_time=$(date +%s)" >> "$GITHUB_OUTPUT"
echo "start_time_nano=$(date +%s%N)" >> "$GITHUB_OUTPUT"
- name: Comment on PR (starting)
if: steps.context.outputs.has_failed_run == 'true' && steps.context.outputs.pr_number != ''
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
WORKFLOW_NAME: ${{ steps.context.outputs.failed_workflow_name }}
FAILED_RUN_ID: ${{ steps.context.outputs.failed_run_id }}
with:
script: |
const prNumber = parseInt(process.env.PR_NUMBER, 10);
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}`;
const failedRunUrl = `https://github.com/${{ github.repository }}/actions/runs/${process.env.FAILED_RUN_ID}`;
const workflowName = process.env.WORKFLOW_NAME || 'Unknown';
const body = [
'## Claude Auto-Fix Started',
'',
'Failed workflow: ' + workflowName + ' - ' + failedRunUrl,
'Auto-fix run: ' + runUrl,
'',
'Analyzing the CI failure and attempting to create a fix...'
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
- name: Checkout code
if: steps.context.outputs.has_failed_run == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.context.outputs.target_branch }}
- name: Get failed workflow logs
if: steps.context.outputs.has_failed_run == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
RUN_ID="${{ steps.context.outputs.failed_run_id }}"
echo "Fetching logs for run $RUN_ID..."
gh run view "$RUN_ID" --log-failed > /tmp/failed_logs.txt 2>&1 || true
head -c 50000 /tmp/failed_logs.txt > /tmp/logs_summary.txt
echo "Log files created:"
echo " - /tmp/failed_logs.txt ($(wc -c < /tmp/failed_logs.txt) bytes)"
echo " - /tmp/logs_summary.txt ($(wc -c < /tmp/logs_summary.txt) bytes)"
- name: Configure AWS Credentials (OIDC)
if: steps.context.outputs.has_failed_run == 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::296062593712:role/claude-code-bedrock-role
aws-region: us-west-2
- name: Set up Node.js
if: steps.context.outputs.has_failed_run == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Set up Python
if: steps.context.outputs.has_failed_run == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install sandbox dependencies
if: steps.context.outputs.has_failed_run == 'true'
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap socat ripgrep
- name: Install Claude Code and Sandbox Runtime
if: steps.context.outputs.has_failed_run == 'true'
run: |
npm install -g @anthropic-ai/claude-code @anthropic-ai/sandbox-runtime
which srt
- name: Install Python toolchain
if: steps.context.outputs.has_failed_run == 'true'
run: |
pip install uv
uv sync
- name: Install pnpm
if: steps.context.outputs.has_failed_run == 'true'
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install superpowers skills
if: steps.context.outputs.has_failed_run == 'true'
run: |
git clone --depth 1 https://github.com/obra/superpowers.git /tmp/superpowers
mkdir -p ~/.claude/skills
cp -r /tmp/superpowers/skills/* ~/.claude/skills/
rm -rf /tmp/superpowers
echo "Installed skills:"
ls -la ~/.claude/skills/
- name: Create sandbox settings
if: steps.context.outputs.has_failed_run == 'true'
run: cp .github/srt-settings.json ~/.srt-settings.json
- name: Configure git
if: steps.context.outputs.has_failed_run == 'true'
run: |
git config --global user.name "claude[bot]"
git config --global user.email "claude[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
- name: Build auto-fix prompt
if: steps.context.outputs.has_failed_run == 'true'
run: |
cat > /tmp/claude-prompt.txt << 'PROMPT_EOF'
You are Claude, an AI assistant that fixes CI failures in the Cua (Computer Use Agent) monorepo.
CONTEXT:
- Repository: ${{ github.repository }}
- Failed Workflow: ${{ steps.context.outputs.failed_workflow_name }}
- Run ID: ${{ steps.context.outputs.failed_run_id }}
- Target Branch: ${{ steps.context.outputs.target_branch }}
- Fix Branch: fix/auto-fix-${{ steps.context.outputs.failed_run_id }}
AVAILABLE LOG FILES:
- /tmp/logs_summary.txt - First 50KB for quick overview
- /tmp/failed_logs.txt - Full failure logs (use grep to search if needed)
REPOSITORY STRUCTURE:
This is a Python/TypeScript monorepo managed with uv (Python) and pnpm (TypeScript).
- libs/python/ - Python packages: agent, core, computer, computer-server, som, mcp-server, bench-ui
- libs/typescript/ - TypeScript packages: core, computer, agent, cua-cli, playground
- Python requires: >=3.12,<3.14
- Package manager: uv (Python), pnpm@10 (TypeScript)
- Build backend: pdm-backend (Python)
PYTHON TOOLS (configured in root pyproject.toml):
- black: code formatter (line-length: 100, target: py312)
- isort: import sorter (profile: black)
- ruff: linter (rules: E, F, B, I; has --fix flag)
- pytest: test runner (asyncio_mode: auto)
- mypy: type checker (currently disabled in CI)
TYPESCRIPT TOOLS:
- prettier: code formatter
- TypeScript: type checking via pnpm -C libs/typescript typecheck
- pnpm: package manager
TASK:
1. Read /tmp/logs_summary.txt first to understand what failed
2. Search /tmp/failed_logs.txt if you need more detail
3. Apply systematic debugging: investigate the codebase to find the ROOT CAUSE before attempting any fix
Use analysis of competing hypotheses. Create two or more competing hypotheses for the
root cause, and create tests or measurements to prove them wrong.
FIXING LINT FAILURES:
For Python lint failures (isort, black, ruff), you can often auto-fix:
- Import order: uv run isort .
- Formatting: uv run black .
- Linting: uv run ruff check . --fix
For TypeScript lint failures:
- Formatting: pnpm -C libs/typescript format
After auto-fixing, verify the fix passes:
- Python: uv run isort --check-only . && uv run black --check . && uv run ruff check .
- TypeScript: pnpm -C libs/typescript format:check
FIXING TEST FAILURES:
For Python test failures:
- Run the specific failing test: uv run python -m pytest libs/python/<package>/tests/ -v --tb=long
- The test environment uses CUA_TELEMETRY_ENABLED=false
- Tests use pytest-asyncio with asyncio_mode=auto
For TypeScript build/type failures:
- Check types: pnpm -C libs/typescript typecheck
- Build: cd libs/typescript && pnpm install && pnpm build
IMPORTANT:
- DO NOT create a PR - just push to the fix branch. The workflow will comment a PR creation link.
- Never commit directly to ${{ steps.context.outputs.target_branch }} - always use the fix branch
- Only proceed if you have a high-confidence fix with verified root cause
- If you cannot determine a fix, explain why and do not push
- Output "FIX_BRANCH_PUSHED=true" after successful push so the workflow knows to comment the PR link
VALIDATION AND RECURSIVE FIXING:
1. NO SHORTCUTS: Always run the full validation command - never assume a partial fix is sufficient.
2. RECURSIVE FIXING: If your fix causes a new failure, keep fixing until the command passes completely.
3. PERSISTENCE: A fix is only complete when the validation command exits with success (exit code 0).
4. AVOID NEAR-SIGHTED FIXES: Consider whether similar issues exist elsewhere in the codebase.
ANTI-PATTERN - WHAT NOT TO DO:
- NEVER push code without verifying it works locally first
- NEVER assume a fix works without running the validation command
- NEVER push a broken fix hoping CI will catch issues
- ALWAYS wait for your local build/test to complete before pushing
PROMPT_EOF
- name: Run Claude Auto Fix with sandbox
if: steps.context.outputs.has_failed_run == 'true'
id: claude-fix
env:
CLAUDE_CODE_USE_BEDROCK: "1"
ANTHROPIC_BEDROCK_BASE_URL: "https://bedrock-runtime.us-west-2.amazonaws.com"
AWS_REGION: us-west-2
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUA_TELEMETRY_ENABLED: "false"
CLAUDE_CODE_ENABLE_TELEMETRY: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT: https://otel.cua.ai
run: |
srt claude \
--output-format=stream-json \
--dangerously-skip-permissions \
< /tmp/claude-prompt.txt 2>&1 | \
tee /tmp/claude-output.json
CLAUDE_EXIT=${PIPESTATUS[0]}
if [ "$CLAUDE_EXIT" -ne 0 ]; then
echo "::error::Claude Code failed with exit code $CLAUDE_EXIT"
echo "branch_pushed=false" >> "$GITHUB_OUTPUT"
exit "$CLAUDE_EXIT"
fi
if grep -q "FIX_BRANCH_PUSHED=true" /tmp/claude-output.json; then
echo "branch_pushed=true" >> "$GITHUB_OUTPUT"
echo "Claude indicated fix branch was pushed"
else
echo "branch_pushed=false" >> "$GITHUB_OUTPUT"
echo "Claude did not indicate branch was pushed"
fi
- name: Verify fix branch exists
id: verify-branch
if: always() && steps.context.outputs.has_failed_run == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
FIX_BRANCH="fix/auto-fix-${{ steps.context.outputs.failed_run_id }}"
if git ls-remote --exit-code --heads origin "$FIX_BRANCH" > /dev/null 2>&1; then
echo "Fix branch '$FIX_BRANCH' exists on remote"
echo "fix_branch=$FIX_BRANCH" >> "$GITHUB_OUTPUT"
echo "branch_exists=true" >> "$GITHUB_OUTPUT"
else
echo "Fix branch '$FIX_BRANCH' does not exist on remote"
echo "fix_branch=" >> "$GITHUB_OUTPUT"
echo "branch_exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Comment on PR (completed)
if: always() && steps.context.outputs.has_failed_run == 'true' && steps.context.outputs.pr_number != ''
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
FIX_BRANCH: ${{ steps.verify-branch.outputs.fix_branch }}
BRANCH_EXISTS: ${{ steps.verify-branch.outputs.branch_exists }}
TARGET_BRANCH: ${{ steps.context.outputs.target_branch }}
WORKFLOW_NAME: ${{ steps.context.outputs.failed_workflow_name }}
FAILED_RUN_ID: ${{ steps.context.outputs.failed_run_id }}
with:
script: |
const prNumber = parseInt(process.env.PR_NUMBER, 10);
const runUrl = 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}';
const fixBranch = process.env.FIX_BRANCH || '';
const branchExists = process.env.BRANCH_EXISTS === 'true';
const targetBranch = process.env.TARGET_BRANCH || '';
const workflowName = process.env.WORKFLOW_NAME || 'Unknown';
const failedRunUrl = `https://github.com/${{ github.repository }}/actions/runs/${process.env.FAILED_RUN_ID}`;
let body;
if (branchExists && fixBranch) {
const createPrUrl = `https://github.com/${{ github.repository }}/compare/${targetBranch}...${fixBranch}?expand=1`;
body = [
'## Claude Auto-Fix Complete',
'',
`Claude has pushed a fix to branch \`${fixBranch}\`.`,
'',
`**[Click here to create the PR](${createPrUrl})**`,
'',
`Failed workflow: [${workflowName}](${failedRunUrl})`,
`Auto-fix run: ${runUrl}`
].join('\n');
} else {
body = [
'## Claude Auto-Fix Could Not Fix',
'',
'Unable to automatically fix this CI failure. Please review the logs.',
'',
`Failed workflow: [${workflowName}](${failedRunUrl})`,
`Auto-fix run: ${runUrl}`
].join('\n');
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
- name: Send autofix metrics to OTEL
if: always() && steps.context.outputs.has_failed_run == 'true'
env:
OTEL_EXPORTER_OTLP_ENDPOINT: https://otel.cua.ai
START_TIME: ${{ steps.start-time.outputs.start_time }}
START_TIME_NANO: ${{ steps.start-time.outputs.start_time_nano }}
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
TARGET_BRANCH: ${{ steps.context.outputs.target_branch }}
BRANCH_EXISTS: ${{ steps.verify-branch.outputs.branch_exists }}
TRIGGERED_BY: ${{ steps.context.outputs.failed_workflow_name }}
run: |
END_TIME=$(date +%s)
END_TIME_NANO=$(date +%s%N)
DURATION_SECONDS=$((END_TIME - START_TIME))
if [ "$BRANCH_EXISTS" = "true" ]; then
SUCCESS=1
else
SUCCESS=0
fi
echo "Sending autofix metrics to OTEL:"
echo " PR: #${PR_NUMBER}"
echo " Duration: ${DURATION_SECONDS}s"
echo " Success: ${SUCCESS}"
METRICS_PAYLOAD=$(cat <<EOF
{
"resourceMetrics": [{
"resource": {
"attributes": [
{"key": "service.name", "value": {"stringValue": "claude-autofix"}},
{"key": "service.version", "value": {"stringValue": "1.0.0"}}
]
},
"scopeMetrics": [{
"scope": {"name": "claude-autofix"},
"metrics": [
{
"name": "claude_autofix_attempt",
"description": "Autofix attempt counter",
"sum": {
"dataPoints": [{
"asInt": "1",
"startTimeUnixNano": "${START_TIME_NANO}",
"timeUnixNano": "${END_TIME_NANO}",
"attributes": [
{"key": "pr_number", "value": {"stringValue": "${PR_NUMBER}"}},
{"key": "repository", "value": {"stringValue": "${{ github.repository }}"}},
{"key": "triggered_by", "value": {"stringValue": "${TRIGGERED_BY}"}},
{"key": "success", "value": {"stringValue": "${SUCCESS}"}}
]
}],
"aggregationTemporality": 2,
"isMonotonic": true
}
},
{
"name": "claude_autofix_duration_seconds",
"description": "Duration of autofix run in seconds",
"gauge": {
"dataPoints": [{
"asDouble": ${DURATION_SECONDS},
"timeUnixNano": "${END_TIME_NANO}",
"attributes": [
{"key": "pr_number", "value": {"stringValue": "${PR_NUMBER}"}},
{"key": "repository", "value": {"stringValue": "${{ github.repository }}"}},
{"key": "triggered_by", "value": {"stringValue": "${TRIGGERED_BY}"}},
{"key": "success", "value": {"stringValue": "${SUCCESS}"}}
]
}]
}
},
{
"name": "claude_autofix_success_total",
"description": "Total number of successful autofix runs",
"sum": {
"dataPoints": [{
"asInt": "${SUCCESS}",
"startTimeUnixNano": "${START_TIME_NANO}",
"timeUnixNano": "${END_TIME_NANO}",
"attributes": [
{"key": "pr_number", "value": {"stringValue": "${PR_NUMBER}"}},
{"key": "repository", "value": {"stringValue": "${{ github.repository }}"}},
{"key": "triggered_by", "value": {"stringValue": "${TRIGGERED_BY}"}}
]
}],
"aggregationTemporality": 2,
"isMonotonic": true
}
}
]
}]
}]
}
EOF
)
curl -s -X POST "${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics" \
-H "Content-Type: application/json" \
-d "$METRICS_PAYLOAD" || echo "Warning: Failed to send metrics"
@@ -0,0 +1,70 @@
name: Reusable Docker Build Workflow
on:
workflow_dispatch: # Prevents phantom runs on push, allows manual testing
workflow_call:
inputs:
image_name:
description: "Name of the Docker image (e.g. cua-ubuntu, cua-xfce)"
required: true
type: string
context_dir:
description: "Directory containing the Dockerfile relative to workspace root (e.g. libs/kasm, libs/xfce)"
required: true
type: string
dockerfile_path:
description: "Path to Dockerfile relative to context_dir (e.g. Dockerfile)"
required: false
type: string
default: "Dockerfile"
skip_arm64:
description: "Skip arm64 build (for images that don't support arm64)"
required: false
type: boolean
default: false
jobs:
setup:
runs-on: ubuntu-latest
outputs:
platforms: ${{ steps.set-platforms.outputs.platforms }}
steps:
- id: set-platforms
run: |
if [ "${{ inputs.skip_arm64 }}" == "true" ]; then
echo 'platforms=["linux/amd64"]' >> $GITHUB_OUTPUT
else
echo 'platforms=["linux/amd64", "linux/arm64"]' >> $GITHUB_OUTPUT
fi
build:
needs: setup
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: ${{ fromJSON(needs.setup.outputs.platforms) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare platform tag
id: platform
run: |
TAG=$(echo "${{ matrix.platform }}" | sed 's/\//-/g')
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: ./${{ inputs.context_dir }}
file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }}
push: false
platforms: ${{ matrix.platform }}
- name: Verify build
run: |
echo "Successfully built ${{ inputs.image_name }} for ${{ matrix.platform }}"
@@ -0,0 +1,251 @@
name: Reusable Docker Publish Workflow
on:
workflow_dispatch: # Prevents phantom runs on push, allows manual testing
workflow_call:
inputs:
image_name:
description: "Name of the Docker image (e.g. cua-ubuntu, cua-xfce)"
required: true
type: string
context_dir:
description: "Directory containing the Dockerfile relative to workspace root (e.g. libs/kasm, libs/xfce)"
required: true
type: string
dockerfile_path:
description: "Path to Dockerfile relative to context_dir (e.g. Dockerfile)"
required: false
type: string
default: "Dockerfile"
tag_prefix:
description: "Prefix for semantic version tags (e.g. docker-kasm-v, docker-xfce-v)"
required: true
type: string
docker_hub_org:
description: "Docker Hub organization name"
required: false
type: string
default: "trycua"
skip_arm64:
description: "Skip arm64 build (for images that don't support arm64)"
required: false
type: boolean
default: false
secrets:
DOCKER_HUB_TOKEN:
required: true
jobs:
build-and-push:
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
exclude:
- platform: ${{ inputs.skip_arm64 && 'linux/arm64' || 'none' }}
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
sudo docker system prune -af
df -h /
- name: Checkout
uses: actions/checkout@v4
- name: Prepare platform tag
id: platform
run: |
TAG=$(echo "${{ matrix.platform }}" | sed 's/\//-/g')
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_hub_org }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build only (PR) - validate Dockerfile without pushing
if: github.event_name == 'pull_request'
id: build-pr
uses: docker/build-push-action@v5
with:
context: ./${{ inputs.context_dir }}
file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }}
push: false
platforms: ${{ matrix.platform }}
cache-from: |
type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }}
- name: Extract metadata (main)
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
id: meta-main
uses: docker/metadata-action@v5
with:
images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }}
tags: |
type=raw,value=latest
- name: Build & push digest (main)
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
id: build-main
continue-on-error: true
uses: docker/build-push-action@v5
with:
context: ./${{ inputs.context_dir }}
file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }}
push: true
platforms: ${{ matrix.platform }}
outputs: type=registry,name=${{ inputs.docker_hub_org }}/${{ inputs.image_name }},push-by-digest=true
labels: ${{ steps.meta-main.outputs.labels }}
cache-from: |
type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }}
cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max
- name: Extract metadata (semver)
if: startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix))
id: meta-semver
uses: docker/metadata-action@v5
with:
images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }}
tags: |
type=match,pattern=${{ inputs.tag_prefix }}(\d+\.\d+\.\d+),group=1
type=match,pattern=${{ inputs.tag_prefix }}(\d+\.\d+),group=1
type=match,pattern=${{ inputs.tag_prefix }}(\d+),group=1
type=raw,value=latest
- name: Build & push digest (semver)
if: startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix))
id: build-semver
continue-on-error: true
uses: docker/build-push-action@v5
with:
context: ./${{ inputs.context_dir }}
file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }}
push: true
platforms: ${{ matrix.platform }}
outputs: type=registry,name=${{ inputs.docker_hub_org }}/${{ inputs.image_name }},push-by-digest=true
labels: ${{ steps.meta-semver.outputs.labels }}
cache-from: |
type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }}
cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max
- name: Export digest
id: export-digest
if: |
github.event_name != 'pull_request' &&
((steps.build-main.outcome == 'success') ||
(steps.build-semver.outcome == 'success'))
run: |
mkdir -p /tmp/digests
digest="${{ steps.build-main.outputs.digest || steps.build-semver.outputs.digest }}"
if [ -n "$digest" ]; then
echo "$digest" > "/tmp/digests/${{ steps.platform.outputs.tag }}.txt"
echo "Digest exported for platform ${{ matrix.platform }}: $digest"
else
echo "No digest to export for platform ${{ matrix.platform }}"
exit 1
fi
- name: Upload digest artifact (unique per platform)
if: steps.export-digest.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: digests-${{ steps.platform.outputs.tag }}
path: /tmp/digests/*.txt
retention-days: 1
publish-manifest-list:
runs-on: ubuntu-latest
needs:
- build-and-push
if: github.event_name != 'pull_request' && (success() || failure()) # Skip on PRs, run on push/tag even if some builds failed
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_hub_org }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Extract final metadata (main)
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
uses: docker/metadata-action@v5
with:
images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }}
tags: |
type=raw,value=latest
- name: Extract final metadata (semver)
if: startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix))
uses: docker/metadata-action@v5
with:
images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }}
tags: |
type=match,pattern=${{ inputs.tag_prefix }}(\d+\.\d+\.\d+),group=1
type=match,pattern=${{ inputs.tag_prefix }}(\d+\.\d+),group=1
type=match,pattern=${{ inputs.tag_prefix }}(\d+),group=1
type=raw,value=latest
- name: Download all digest artifacts
uses: actions/download-artifact@v4
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Check available digests
id: check-digests
run: |
DIGEST_COUNT=$(find /tmp/digests -type f -name "*.txt" | wc -l)
echo "digest_count=$DIGEST_COUNT" >> $GITHUB_OUTPUT
if [ "$DIGEST_COUNT" -eq 0 ]; then
echo "Error: No platform builds succeeded!"
exit 1
fi
echo "Found $DIGEST_COUNT platform digest(s)"
for f in $(find /tmp/digests -type f -name "*.txt"); do
platform=$(basename "$f" .txt)
echo " - $platform"
done
- name: Create & push multi-arch manifest
run: |
IMAGE="${{ inputs.docker_hub_org }}/${{ inputs.image_name }}"
DIGEST_ARGS=""
for f in $(find /tmp/digests -type f -name "*.txt"); do
d=$(cat "$f")
DIGEST_ARGS="$DIGEST_ARGS ${IMAGE}@${d}"
done
echo "Using digests:"
echo "$DIGEST_ARGS"
# Create manifest for each tag produced by metadata-action
echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read FULL_TAG; do
echo "Creating manifest: $FULL_TAG"
docker buildx imagetools create --tag "$FULL_TAG" $DIGEST_ARGS
done
- name: Inspect pushed manifests
run: |
IMAGE="${{ inputs.docker_hub_org }}/${{ inputs.image_name }}"
echo "Inspecting manifests:"
echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read FULL_TAG; do
echo ""
echo "Inspecting: $FULL_TAG"
docker buildx imagetools inspect "$FULL_TAG"
done
@@ -0,0 +1,229 @@
name: "CD: Docs MCP Server"
on:
push:
branches:
- main
paths:
- "docs/scripts/docs-mcp-server/**"
pull_request:
branches:
- main
paths:
- "docs/scripts/docs-mcp-server/**"
workflow_dispatch:
inputs:
force_push:
description: "Push image even on non-main branch"
required: false
default: false
type: boolean
env:
AWS_REGION: us-west-2
ECR_REGISTRY: 296062593712.dkr.ecr.us-west-2.amazonaws.com
ECR_REPOSITORY: docs-mcp-server
SERVICE_DIR: docs/scripts/docs-mcp-server
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
outputs:
image_tag: ${{ steps.meta.outputs.version }}
timestamp: ${{ steps.timestamp.outputs.ts }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare platform tag
id: platform
run: |
TAG=$(echo "${{ matrix.platform }}" | sed 's/\//-/g')
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::296062593712:role/github-actions-ecr-push-cua
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate timestamp
id: timestamp
run: echo "ts=$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}
tags: |
type=sha,prefix=,format=short
type=ref,event=branch
type=ref,event=pr
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=main-${{ steps.timestamp.outputs.ts }},enable=${{ github.ref == 'refs/heads/main' }}
- name: Determine if push is enabled
id: push-check
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "push=false" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "push=true" >> $GITHUB_OUTPUT
elif [[ "${{ inputs.force_push }}" == "true" ]]; then
echo "push=true" >> $GITHUB_OUTPUT
else
echo "push=false" >> $GITHUB_OUTPUT
fi
- name: Build only (PR)
if: steps.push-check.outputs.push == 'false'
uses: docker/build-push-action@v5
with:
context: ./${{ env.SERVICE_DIR }}
file: ./${{ env.SERVICE_DIR }}/Dockerfile
push: false
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=${{ env.ECR_REPOSITORY }}-${{ steps.platform.outputs.tag }}
cache-to: type=gha,mode=max,scope=${{ env.ECR_REPOSITORY }}-${{ steps.platform.outputs.tag }}
- name: Build and push by digest
if: steps.push-check.outputs.push == 'true'
id: build
uses: docker/build-push-action@v5
with:
context: ./${{ env.SERVICE_DIR }}
file: ./${{ env.SERVICE_DIR }}/Dockerfile
push: true
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }},push-by-digest=true,name-canonical=true
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ env.ECR_REPOSITORY }}-${{ steps.platform.outputs.tag }}
cache-to: type=gha,mode=max,scope=${{ env.ECR_REPOSITORY }}-${{ steps.platform.outputs.tag }}
- name: Export digest
if: steps.push-check.outputs.push == 'true'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
if [ -n "$digest" ]; then
echo "$digest" > "/tmp/digests/${{ steps.platform.outputs.tag }}.txt"
echo "Digest exported for platform ${{ matrix.platform }}: $digest"
else
echo "No digest to export for platform ${{ matrix.platform }}"
exit 1
fi
- name: Upload digest artifact
if: steps.push-check.outputs.push == 'true'
uses: actions/upload-artifact@v4
with:
name: digests-${{ steps.platform.outputs.tag }}
path: /tmp/digests/*.txt
retention-days: 1
merge:
runs-on: ubuntu-latest
needs: build
if: |
github.event_name != 'pull_request' &&
(github.ref == 'refs/heads/main' || inputs.force_push == true)
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::296062593712:role/github-actions-ecr-push-cua
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}
tags: |
type=sha,prefix=,format=short
type=ref,event=branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=main-${{ needs.build.outputs.timestamp }},enable=${{ github.ref == 'refs/heads/main' }}
- name: Download all digest artifacts
uses: actions/download-artifact@v4
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Check available digests
id: check-digests
run: |
DIGEST_COUNT=$(find /tmp/digests -type f -name "*.txt" | wc -l)
echo "digest_count=$DIGEST_COUNT" >> $GITHUB_OUTPUT
if [ "$DIGEST_COUNT" -eq 0 ]; then
echo "Error: No platform builds succeeded!"
exit 1
fi
echo "Found $DIGEST_COUNT platform digest(s)"
for f in $(find /tmp/digests -type f -name "*.txt"); do
platform=$(basename "$f" .txt)
echo " - $platform: $(cat $f)"
done
- name: Create and push multi-arch manifest
run: |
IMAGE="${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}"
# Collect all digests
DIGEST_ARGS=""
for f in $(find /tmp/digests -type f -name "*.txt"); do
d=$(cat "$f")
DIGEST_ARGS="$DIGEST_ARGS ${IMAGE}@${d}"
done
echo "Using digests:"
echo "$DIGEST_ARGS"
# Create manifest for each tag produced by metadata-action
echo "${{ steps.meta.outputs.tags }}" | while read FULL_TAG; do
if [ -n "$FULL_TAG" ]; then
echo "Creating manifest: $FULL_TAG"
docker buildx imagetools create --tag "$FULL_TAG" $DIGEST_ARGS
fi
done
- name: Inspect pushed manifests
run: |
echo "Inspecting manifests:"
echo "${{ steps.meta.outputs.tags }}" | while read FULL_TAG; do
if [ -n "$FULL_TAG" ]; then
echo ""
echo "Inspecting: $FULL_TAG"
docker buildx imagetools inspect "$FULL_TAG"
fi
done
@@ -0,0 +1,199 @@
name: "E2E: Rust Linux Wayland"
on:
workflow_dispatch:
inputs:
ref:
description: "Optional full 40-character commit SHA; defaults to dispatch SHA"
required: false
default: ""
environment:
description: "Wayland environment to validate"
required: false
type: choice
default: sway
options:
- sway
- cua-compositor
lane:
description: "Diagnostic lane; canonical dispatches use all"
required: false
type: choice
default: all
options:
- all
- shared
- native
- capture
cell_filter:
description: "Optional shared-cell substring for targeted diagnostics"
required: false
default: ""
permissions:
contents: read
actions: read
jobs:
source:
name: "Resolve exact source"
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.resolve.outputs.sha }}
lanes: ${{ steps.resolve.outputs.lanes }}
steps:
- id: resolve
name: Validate source SHA
shell: bash
env:
REQUESTED_SHA: ${{ inputs.ref }}
DISPATCH_SHA: ${{ github.sha }}
run: |
sha="${REQUESTED_SHA:-$DISPATCH_SHA}"
if [[ ! "$sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "ref must be a full 40-character commit SHA" >&2
exit 2
fi
echo "sha=${sha,,}" >> "$GITHUB_OUTPUT"
case "${{ inputs.lane }}" in
all) echo 'lanes=["shared","native","capture"]' >> "$GITHUB_OUTPUT" ;;
shared|native|capture) printf 'lanes=["%s"]\n' "${{ inputs.lane }}" >> "$GITHUB_OUTPUT" ;;
*) echo "unsupported lane: ${{ inputs.lane }}" >&2; exit 2 ;;
esac
wayland:
name: "Linux / ${{ inputs.environment }} / ${{ matrix.lane }}"
needs: source
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
lane: ${{ fromJSON(needs.source.outputs.lanes) }}
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
CUA_E2E_INTERNAL_LANE: ${{ matrix.lane }}
CUA_E2E_CELL_FILTER: ${{ inputs.cell_filter }}
CUA_E2E_WAYLAND_SESSION: ${{ inputs.environment }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- uses: dtolnay/rust-toolchain@stable
- name: Install Sway and Rust build dependencies
if: inputs.environment == 'sway'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
sway wf-recorder grim wtype dbus dbus-x11 at-spi2-core libglib2.0-bin \
python3-gi gir1.2-gtk-3.0 libgtk-3-dev clang pkg-config \
libdbus-1-dev libpipewire-0.3-dev libspa-0.2-dev libei-dev \
libwayland-dev libxkbcommon-dev libx11-dev libxi-dev libxtst-dev \
libxext-dev libdrm-dev libgbm-dev libwebkit2gtk-4.1-dev \
libssl-dev libxdo-dev libayatana-appindicator3-dev librsvg2-dev \
ffmpeg jq
- name: Install Nix for the custom compositor environment
if: inputs.environment == 'cua-compositor'
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Run complete native Wayland matrix
shell: bash
env:
CUA_AT_SPI_BUS_LAUNCHER: /usr/libexec/at-spi-bus-launcher
run: |
if [[ "${{ inputs.environment }}" == "cua-compositor" ]]; then
nix develop .#cua-driver-inject-e2e \
-c scripts/ci/linux/run-rust-e2e-inject.sh
else
scripts/ci/linux/run-rust-e2e-wayland.sh
fi
- name: Upload Wayland results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-linux-wayland-${{ inputs.environment }}-${{ matrix.lane }}
path: artifacts/cua-driver/linux
if-no-files-found: ignore
compression-level: 0
retention-days: 14
summary:
if: always()
needs: [source, wayland]
name: "Linux / pure Wayland summary"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- name: Download Wayland results
continue-on-error: true
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
pattern: rust-linux-wayland-${{ inputs.environment }}-*
path: artifacts
- name: Publish typed matrix summary
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
output=matrix-summary.md
artifacts_json=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100")
{
echo "# CUA Rust Linux Wayland E2E: ${{ inputs.environment }}"
echo
echo "Shared, native, and capture lanes run independently on clean runners."
} > "$output"
found=0
while IFS= read -r summary; do
found=1
artifact=$(basename "$(dirname "$summary")")
artifact_id=$(jq -r --arg name "$artifact" \
'.artifacts[] | select(.name == $name) | .id' <<< "$artifacts_json" | head -n 1)
echo >> "$output"
echo "## $artifact" >> "$output"
if [[ -n "$artifact_id" && "$artifact_id" != "null" ]]; then
artifact_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts/$artifact_id"
scripts/ci/link-e2e-evidence.sh "$summary" "$artifact_url" >> "$output"
else
cat "$summary" >> "$output"
fi
done < <(find artifacts -type f -name summary.md -print | sort)
if [[ "$found" == 0 ]]; then
echo >> "$output"
echo "No typed lane summary was produced; inspect the lane logs." >> "$output"
fi
{
echo
echo "## Trajectory videos"
echo
echo "| Lane | Videos | Artifact |"
echo "| --- | ---: | --- |"
} >> "$output"
for lane in shared native capture; do
artifact="rust-linux-wayland-${{ inputs.environment }}-$lane"
recording_dir="artifacts/$artifact/recordings"
video_count=0
[[ -d "$recording_dir" ]] && video_count=$(find "$recording_dir" -type f -name recording.mp4 | wc -l | tr -d ' ')
artifact_id=$(jq -r --arg name "$artifact" \
'.artifacts[] | select(.name == $name) | .id' <<< "$artifacts_json" | head -n 1)
if [[ -n "$artifact_id" && "$artifact_id" != "null" ]]; then
artifact_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts/$artifact_id"
link="[Open artifact]($artifact_url)"
else
link="Not produced"
fi
echo "| $lane | $video_count | $link |" >> "$output"
done
cat "$output" >> "$GITHUB_STEP_SUMMARY"
- name: Upload combined summary
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-linux-wayland-matrix-summary
path: matrix-summary.md
if-no-files-found: error
+255
View File
@@ -0,0 +1,255 @@
name: "E2E: Rust Linux interactive"
on:
workflow_dispatch:
inputs:
ref:
description: "Optional full 40-character commit SHA; defaults to dispatch SHA"
required: false
default: ""
lane:
description: "Diagnostic lane; canonical dispatches use all"
required: false
type: choice
default: all
options:
- all
- shared
- native
- capture
cell_filter:
description: "Optional shared-cell substring for targeted diagnostics"
required: false
default: ""
permissions:
contents: read
actions: read
jobs:
source:
name: "Resolve exact source"
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- id: resolve
name: Validate source SHA
shell: bash
env:
REQUESTED_SHA: ${{ inputs.ref }}
DISPATCH_SHA: ${{ github.sha }}
run: |
sha="${REQUESTED_SHA:-$DISPATCH_SHA}"
if [[ ! "$sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "ref must be a full 40-character commit SHA" >&2
exit 2
fi
echo "sha=${sha,,}" >> "$GITHUB_OUTPUT"
shared:
if: inputs.lane == 'all' || inputs.lane == 'shared'
name: "Linux / shared Electron + Tauri"
needs: source
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
CUA_E2E_CELL_FILTER: ${{ inputs.cell_filter }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- uses: dtolnay/rust-toolchain@stable
- name: Install GUI and Rust build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb dbus-x11 at-spi2-core openbox python3-gi gir1.2-gtk-3.0 \
libgtk-3-dev clang pkg-config libdbus-1-dev libpipewire-0.3-dev \
libspa-0.2-dev libei-dev libx11-dev libxi-dev libxtst-dev libxext-dev \
libwebkit2gtk-4.1-dev libssl-dev libxdo-dev \
libayatana-appindicator3-dev librsvg2-dev ffmpeg
- name: Run shared Rust behavior matrix
env:
CUA_E2E_INTERNAL_LANE: shared
run: |
xvfb-run -a --server-args="-screen 0 1920x1080x24" \
dbus-run-session -- bash -lc \
"openbox >/tmp/cua-openbox.log 2>&1 & sleep 2; scripts/ci/linux/run-rust-e2e.sh"
- name: Upload shared results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-linux-shared
path: artifacts/cua-driver/linux
if-no-files-found: ignore
compression-level: 0
retention-days: 14
native:
if: inputs.lane == 'all' || inputs.lane == 'native'
name: "Linux / GTK3 native harness"
needs: source
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- uses: dtolnay/rust-toolchain@stable
- name: Install GUI and Rust build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb dbus-x11 at-spi2-core openbox python3-gi gir1.2-gtk-3.0 \
libgtk-3-dev clang pkg-config libdbus-1-dev libpipewire-0.3-dev \
libspa-0.2-dev libei-dev libx11-dev libxi-dev libxtst-dev libxext-dev \
libwebkit2gtk-4.1-dev libssl-dev libxdo-dev \
libayatana-appindicator3-dev librsvg2-dev ffmpeg
- name: Run GTK3 native Rust harness
env:
CUA_E2E_INTERNAL_LANE: native
run: |
xvfb-run -a --server-args="-screen 0 1920x1080x24" \
dbus-run-session -- bash -lc \
"openbox >/tmp/cua-openbox.log 2>&1 & sleep 2; scripts/ci/linux/run-rust-e2e.sh"
- name: Upload native results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-linux-native
path: artifacts/cua-driver/linux
if-no-files-found: ignore
compression-level: 0
retention-days: 14
capture:
if: inputs.lane == 'all' || inputs.lane == 'capture'
name: "Linux / capture and desktop scope"
needs: source
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- uses: dtolnay/rust-toolchain@stable
- name: Install GUI and Rust build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb dbus-x11 at-spi2-core openbox python3-gi gir1.2-gtk-3.0 \
libgtk-3-dev clang pkg-config libdbus-1-dev libpipewire-0.3-dev \
libspa-0.2-dev libei-dev libx11-dev libxi-dev libxtst-dev libxext-dev \
libwebkit2gtk-4.1-dev libssl-dev libxdo-dev \
libayatana-appindicator3-dev librsvg2-dev ffmpeg
- name: Run capture and desktop-scope contracts
env:
CUA_E2E_INTERNAL_LANE: capture
run: |
xvfb-run -a --server-args="-screen 0 1920x1080x24" \
dbus-run-session -- bash -lc \
"openbox >/tmp/cua-openbox.log 2>&1 & sleep 2; scripts/ci/linux/run-rust-e2e.sh"
- name: Upload capture results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-linux-capture
path: artifacts/cua-driver/linux
if-no-files-found: ignore
compression-level: 0
retention-days: 14
summary:
if: always()
needs: [source, shared, native, capture]
name: "Linux / matrix summary"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- name: Download lane results
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
path: artifacts
- name: Publish matrix summary
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
summary_path=matrix-summary.md
{
echo "# CUA Rust Linux E2E matrix"
echo
echo "The lane jobs above are independent; a failure in one lane does not hide the others."
echo
} > "$summary_path"
artifacts_json=$(gh api \
"repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100")
found=0
while IFS= read -r summary; do
found=1
artifact=$(basename "$(dirname "$summary")")
echo "## $artifact" >> "$summary_path"
artifact_id=$(jq -r --arg name "$artifact" \
'.artifacts[] | select(.name == $name) | .id' <<< "$artifacts_json" | head -n 1)
if [[ -n "$artifact_id" && "$artifact_id" != "null" ]]; then
artifact_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts/$artifact_id"
scripts/ci/link-e2e-evidence.sh "$summary" "$artifact_url" >> "$summary_path"
else
cat "$summary" >> "$summary_path"
fi
echo >> "$summary_path"
done < <(find artifacts -type f -name summary.md -print | sort)
if [[ "$found" == 0 ]]; then
echo "No lane summary artifact was produced." >> "$summary_path"
fi
{
echo
echo "## Trajectory videos"
echo
echo "Full-desktop MP4s are retained for 14 days inside each lane artifact."
echo
echo "| Lane | Videos | Artifact |"
echo "| --- | ---: | --- |"
} >> "$summary_path"
while IFS='|' read -r lane artifact; do
recording_dir="artifacts/$artifact/recordings"
if [[ -d "$recording_dir" ]]; then
video_count=$(find "$recording_dir" -type f -name recording.mp4 | wc -l | tr -d ' ')
else
video_count=0
fi
artifact_id=$(jq -r --arg name "$artifact" \
'.artifacts[] | select(.name == $name) | .id' <<< "$artifacts_json" | head -n 1)
if [[ -n "$artifact_id" && "$artifact_id" != "null" ]]; then
artifact_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts/$artifact_id"
artifact_link="[Download $artifact]($artifact_url)"
else
artifact_link="Not produced in this dispatch"
fi
echo "| $lane | $video_count | $artifact_link |" >> "$summary_path"
done <<'EOF'
Shared Electron + Tauri|rust-linux-shared
GTK3 native|rust-linux-native
Capture + desktop scope|rust-linux-capture
EOF
{
echo
echo "Each evidence link opens its owning lane artifact; the row text is the exact \`recordings/<cell-label>-pid<pid>-<sequence>/recording.mp4\` path, with an adjacent \`trajectory.json\`."
} >> "$summary_path"
cat "$summary_path" >> "$GITHUB_STEP_SUMMARY"
- name: Upload matrix summary
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-linux-matrix-summary
path: matrix-summary.md
if-no-files-found: error
+242
View File
@@ -0,0 +1,242 @@
name: "E2E: Rust Windows interactive"
on:
workflow_dispatch:
inputs:
ref:
description: "Optional full 40-character commit SHA; defaults to dispatch SHA"
required: false
default: ""
runner:
description: "Runner label; use the Azure RDP runner label for VM e2e"
required: true
default: "windows-latest"
permissions:
contents: read
actions: read
jobs:
source:
name: "Resolve exact source"
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- id: resolve
name: Validate source SHA
shell: bash
env:
REQUESTED_SHA: ${{ inputs.ref }}
DISPATCH_SHA: ${{ github.sha }}
run: |
sha="${REQUESTED_SHA:-$DISPATCH_SHA}"
if [[ ! "$sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "ref must be a full 40-character commit SHA" >&2
exit 2
fi
echo "sha=${sha,,}" >> "$GITHUB_OUTPUT"
shared:
name: "Windows / shared Electron + Tauri"
needs: source
runs-on: ${{ inputs.runner }}
timeout-minutes: 90
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- name: Ensure FFmpeg for trajectory video
shell: pwsh
run: |
if (-not (Get-Command ffmpeg.exe -ErrorAction SilentlyContinue)) {
choco install ffmpeg -y --no-progress
}
ffmpeg -version
ffprobe -version
- name: Run shared Rust behavior matrix
shell: pwsh
env:
CUA_E2E_INTERNAL_LANE: shared
run: .\scripts\ci\windows\run-rust-e2e.ps1 -RequireGui
- name: Collect logs
if: always()
shell: pwsh
run: .\scripts\ci\windows\collect-artifacts.ps1
- name: Upload shared results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-windows-shared
path: artifacts/cua-driver/windows
if-no-files-found: ignore
compression-level: 0
retention-days: 14
native:
name: "Windows / native WPF + WinUI3 + WebView2"
needs: source
runs-on: ${{ inputs.runner }}
timeout-minutes: 90
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- name: Ensure FFmpeg for trajectory video
shell: pwsh
run: |
if (-not (Get-Command ffmpeg.exe -ErrorAction SilentlyContinue)) {
choco install ffmpeg -y --no-progress
}
ffmpeg -version
ffprobe -version
- name: Run native Rust harnesses
shell: pwsh
env:
CUA_E2E_INTERNAL_LANE: native
run: .\scripts\ci\windows\run-rust-e2e.ps1 -RequireGui
- name: Collect logs
if: always()
shell: pwsh
run: .\scripts\ci\windows\collect-artifacts.ps1
- name: Upload native results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-windows-native
path: artifacts/cua-driver/windows
if-no-files-found: ignore
compression-level: 0
retention-days: 14
capture:
name: "Windows / capture and desktop scope"
needs: source
runs-on: ${{ inputs.runner }}
timeout-minutes: 90
env:
CUA_E2E_SOURCE_SHA: ${{ needs.source.outputs.sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- name: Ensure FFmpeg for trajectory video
shell: pwsh
run: |
if (-not (Get-Command ffmpeg.exe -ErrorAction SilentlyContinue)) {
choco install ffmpeg -y --no-progress
}
ffmpeg -version
ffprobe -version
- name: Run capture and desktop-scope contracts
shell: pwsh
env:
CUA_E2E_INTERNAL_LANE: capture
run: .\scripts\ci\windows\run-rust-e2e.ps1 -RequireGui
- name: Collect logs
if: always()
shell: pwsh
run: .\scripts\ci\windows\collect-artifacts.ps1
- name: Upload capture results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-windows-capture
path: artifacts/cua-driver/windows
if-no-files-found: ignore
compression-level: 0
retention-days: 14
summary:
if: always()
needs: [source, shared, native, capture]
name: "Windows / matrix summary"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.source.outputs.sha }}
- name: Download lane results
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
path: artifacts
- name: Publish matrix summary
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
summary_path=matrix-summary.md
{
echo "# CUA Rust Windows E2E matrix"
echo
echo "The lane jobs above are independent; a failure in one lane does not hide the others."
echo
} > "$summary_path"
artifacts_json=$(gh api \
"repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100")
found=0
while IFS= read -r summary; do
found=1
artifact=$(basename "$(dirname "$summary")")
echo "## $artifact" >> "$summary_path"
artifact_id=$(jq -r --arg name "$artifact" \
'.artifacts[] | select(.name == $name) | .id' <<< "$artifacts_json" | head -n 1)
if [[ -n "$artifact_id" && "$artifact_id" != "null" ]]; then
artifact_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts/$artifact_id"
scripts/ci/link-e2e-evidence.sh "$summary" "$artifact_url" >> "$summary_path"
else
cat "$summary" >> "$summary_path"
fi
echo >> "$summary_path"
done < <(find artifacts -type f -name summary.md -print | sort)
if [[ "$found" == 0 ]]; then
echo "No lane summary artifact was produced." >> "$summary_path"
fi
{
echo
echo "## Trajectory videos"
echo
echo "Full-desktop MP4s are retained for 14 days inside each lane artifact."
echo
echo "| Lane | Videos | Artifact |"
echo "| --- | ---: | --- |"
} >> "$summary_path"
while IFS='|' read -r lane artifact; do
recording_dir="artifacts/$artifact/recordings"
if [[ -d "$recording_dir" ]]; then
video_count=$(find "$recording_dir" -type f -name recording.mp4 | wc -l | tr -d ' ')
else
video_count=0
fi
artifact_id=$(jq -r --arg name "$artifact" \
'.artifacts[] | select(.name == $name) | .id' <<< "$artifacts_json" | head -n 1)
if [[ -n "$artifact_id" && "$artifact_id" != "null" ]]; then
artifact_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts/$artifact_id"
artifact_link="[Download $artifact]($artifact_url)"
else
artifact_link="Not produced in this dispatch"
fi
echo "| $lane | $video_count | $artifact_link |" >> "$summary_path"
done <<'EOF'
Electron + Tauri|rust-windows-shared
WPF + WinUI3 + WebView2|rust-windows-native
Capture + desktop scope|rust-windows-capture
EOF
{
echo
echo "Each evidence link opens its owning lane artifact; the row text is the exact \`recordings/<cell-label>-pid<pid>-<sequence>/recording.mp4\` path, with an adjacent \`trajectory.json\`."
} >> "$summary_path"
cat "$summary_path" >> "$GITHUB_STEP_SUMMARY"
- name: Upload matrix summary
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
with:
name: rust-windows-matrix-summary
path: matrix-summary.md
if-no-files-found: error
@@ -0,0 +1,100 @@
name: Monitor branded installer endpoints
on:
schedule:
# Run daily; the first scheduled run will be the next day after merge.
- cron: '15 14 * * *'
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: monitor-branded-installers
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check public installer endpoints
id: endpoints
shell: bash
run: |
set -uo pipefail
endpoints=(
"https://cua.ai/driver/install.sh|cua-driver"
"https://cua.ai/driver/install.ps1|cua-driver"
"https://cua.ai/driver/uninstall.sh|cua-driver"
"https://cua.ai/driver/uninstall.ps1|cua-driver"
"https://cua.ai/driver/_install-rust.sh|cua-driver"
"https://cua.ai/driver/_install-common.sh|cua-driver"
"https://cua.ai/driver/_install-common.psm1|cua-driver"
"https://cua.ai/driver/post-install-hints.txt|cua-driver"
"https://cua.ai/lume/install.sh|Lume"
"https://cua.ai/lume/uninstall.sh|Lume"
)
failures=()
mkdir -p /tmp/branded-installer-check
for entry in "${endpoints[@]}"; do
url="${entry%%|*}"
marker="${entry##*|}"
key=$(echo "$url" | sed 's#https://##; s#[^A-Za-z0-9]#_#g')
headers="/tmp/branded-installer-check/${key}.headers"
body="/tmp/branded-installer-check/${key}.body"
status=$(curl --silent --show-error --location --max-redirs 0 \
--dump-header "$headers" --output "$body" --write-out '%{http_code}' "$url" || true)
content_type=$(awk -F': ' 'tolower($1)=="content-type" {print $2}' "$headers" | tail -1 | tr -d '\r')
if [[ "$status" != "200" ]]; then
failures+=("$url returned HTTP $status")
elif [[ "$content_type" != text/* ]]; then
failures+=("$url returned unexpected Content-Type: ${content_type:-missing}")
elif ! grep -qi -- "$marker" "$body"; then
failures+=("$url did not contain expected marker $marker")
fi
done
if ((${#failures[@]})); then
{
echo 'failures<<EOF'
printf '%s\n' "${failures[@]}"
echo EOF
} >> "$GITHUB_OUTPUT"
exit 1
fi
echo 'failures=' >> "$GITHUB_OUTPUT"
- name: Open or update monitoring issue
if: failure() && steps.endpoints.outputs.failures != ''
env:
GH_TOKEN: ${{ github.token }}
FAILURES: ${{ steps.endpoints.outputs.failures }}
shell: bash
run: |
set -euo pipefail
title='Branded installer endpoint check is failing'
body=$(cat <<EOF
The daily branded installer monitor found one or more failures.
```
${FAILURES}
```
Workflow run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
Checked at: ${GITHUB_SHA}
EOF
)
issue_number=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "$title in:title" --json number --jq '.[0].number // empty')
if [[ -n "$issue_number" ]]; then
gh issue comment "$issue_number" --repo "$GITHUB_REPOSITORY" --body "$body"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$body"
fi
+53
View File
@@ -0,0 +1,53 @@
name: Reusable Package Build Workflow
on:
workflow_call:
inputs:
package_name:
description: "Name of the package (e.g. computer, agent)"
required: true
type: string
package_dir:
description: "Directory containing the package relative to workspace root (e.g. libs/python/computer)"
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Create root pdm.lock file
run: touch pdm.lock
- name: Install PDM
uses: pdm-project/setup-pdm@v3
with:
python-version: "3.11"
cache: true
- name: Initialize PDM in package directory
run: |
cd ${{ inputs.package_dir }}
if [ ! -f "pdm.lock" ]; then
echo "No pdm.lock found, initializing PDM project..."
pdm lock
fi
- name: Build package
run: |
cd ${{ inputs.package_dir }}
pdm build
echo "Successfully built ${{ inputs.package_name }}"
- name: Verify build artifacts
run: |
cd ${{ inputs.package_dir }}
ls -la dist/
echo "Build artifacts created successfully"
+108
View File
@@ -0,0 +1,108 @@
name: Reusable Package Publish Workflow
on:
workflow_call:
inputs:
package_name:
description: "Name of the package (e.g. computer, agent)"
required: true
type: string
package_dir:
description: "Directory containing the package relative to workspace root (e.g. libs/python/computer)"
required: true
type: string
version:
description: "Version to publish"
required: true
type: string
base_package_name:
description: "PyPI package name (e.g. cua-agent)"
required: true
type: string
secrets:
PYPI_TOKEN:
required: true
outputs:
version:
description: "The version that was published"
value: ${{ jobs.build-and-publish.outputs.version }}
jobs:
build-and-publish:
runs-on: macos-latest
permissions:
contents: read
outputs:
version: ${{ steps.set-version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0 # Full history for release creation
- name: Ensure latest main branch
run: |
git fetch origin main
git reset --hard origin/main
echo "Current HEAD commit:"
git log -1 --oneline
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Create root pdm.lock file
run: |
# Create an empty pdm.lock file in the root
touch pdm.lock
- name: Install PDM
uses: pdm-project/setup-pdm@v3
with:
python-version: "3.11"
cache: true
- name: Set version
id: set-version
run: |
echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
- name: Verify version consistency
run: |
# Install toml parser
pip install toml
# Verify version matches using script (exits with error if mismatch)
python ${GITHUB_WORKSPACE}/.github/scripts/get_pyproject_version.py \
${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pyproject.toml \
${{ inputs.version }}
- name: Initialize PDM in package directory
run: |
# Make sure we're working with a properly initialized PDM project
cd ${{ inputs.package_dir }}
# Create pdm.lock if it doesn't exist
if [ ! -f "pdm.lock" ]; then
echo "No pdm.lock found, initializing PDM project..."
pdm lock
fi
- name: Build and publish
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
cd ${{ inputs.package_dir }}
# Build with PDM
pdm build
echo "Publishing ${{ inputs.base_package_name }} ${VERSION}"
# Install and use twine directly instead of PDM publish
echo "Installing twine for direct publishing..."
pip install twine
echo "Publishing to PyPI using twine..."
TWINE_USERNAME="__token__" TWINE_PASSWORD="$PYPI_TOKEN" python -m twine upload dist/*
+600
View File
@@ -0,0 +1,600 @@
name: "CD: Bump Version"
on:
workflow_dispatch:
inputs:
service:
description: "Service/Package to bump"
required: true
type: choice
options:
- pypi/cua
- pypi/agent
- pypi/auto
- pypi/bench
- pypi/bench-ui
- pypi/cli
- pypi/computer
- pypi/computer-server
- pypi/cloud
- pypi/core
- pypi/mcp-server
- pypi/sandbox
- pypi/sandbox-apps
- pypi/som
- pypi/train
- npm/cli
- npm/computer
- npm/core
- npm/playground
- npm/cuabot
- lume
- cua-driver
- cua-driver-rs
- docker/cuabot
- docker/kasm
- docker/xfce
- docker/lumier
- docker/qemu-android
- docker/qemu-linux
- docker/qemu-windows
bump_type:
description: "Version bump type"
required: true
type: choice
options:
- patch
- minor
- major
target_branch:
description: "Branch to push the bump commit + tag to"
required: false
type: string
default: "main"
permissions:
contents: write
jobs:
bump-version:
runs-on: ubuntu-latest
steps:
- name: Set package directory and type
id: package
run: |
case "${{ inputs.service }}" in
"pypi/cua")
echo "directory=libs/python/cua" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/agent")
echo "directory=libs/python/agent" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/auto")
echo "directory=libs/python/cua-auto" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/bench")
echo "directory=libs/cua-bench" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/bench-ui")
echo "directory=libs/python/bench-ui" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/cli")
echo "directory=libs/python/cua-cli" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/computer")
echo "directory=libs/python/computer" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/cloud")
echo "directory=libs/python/cua-cloud" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/computer-server")
echo "directory=libs/python/computer-server" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/core")
echo "directory=libs/python/core" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/mcp-server")
echo "directory=libs/python/mcp-server" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/sandbox")
echo "directory=libs/python/cua-sandbox" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/sandbox-apps")
echo "directory=libs/python/cua-sandbox-apps" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/som")
echo "directory=libs/python/som" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"pypi/train")
echo "directory=libs/python/cua-train" >> $GITHUB_OUTPUT
echo "type=python" >> $GITHUB_OUTPUT
;;
"npm/cli")
echo "directory=libs/typescript/cua-cli" >> $GITHUB_OUTPUT
echo "type=npm" >> $GITHUB_OUTPUT
;;
"npm/computer")
echo "directory=libs/typescript/computer" >> $GITHUB_OUTPUT
echo "type=npm" >> $GITHUB_OUTPUT
;;
"npm/core")
echo "directory=libs/typescript/core" >> $GITHUB_OUTPUT
echo "type=npm" >> $GITHUB_OUTPUT
;;
"npm/playground")
echo "directory=libs/typescript/playground" >> $GITHUB_OUTPUT
echo "type=npm" >> $GITHUB_OUTPUT
;;
"npm/cuabot")
echo "directory=libs/cuabot" >> $GITHUB_OUTPUT
echo "type=npm" >> $GITHUB_OUTPUT
;;
"lume")
echo "directory=libs/lume" >> $GITHUB_OUTPUT
echo "type=lume" >> $GITHUB_OUTPUT
;;
"cua-driver")
echo "directory=libs/cua-driver" >> $GITHUB_OUTPUT
echo "type=cua-driver" >> $GITHUB_OUTPUT
;;
"cua-driver-rs")
echo "directory=libs/cua-driver/rust" >> $GITHUB_OUTPUT
echo "type=cua-driver-rs" >> $GITHUB_OUTPUT
;;
"docker/cuabot")
echo "directory=libs/cuabot" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
echo "bumpversion_config=.bumpversion.docker.cfg" >> $GITHUB_OUTPUT
;;
"docker/kasm")
echo "directory=libs/kasm" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
;;
"docker/xfce")
echo "directory=libs/xfce" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
;;
"docker/lumier")
echo "directory=libs/lumier" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
;;
"docker/qemu-android")
echo "directory=libs/qemu-docker/android" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
;;
"docker/qemu-linux")
echo "directory=libs/qemu-docker/linux" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
;;
"docker/qemu-windows")
echo "directory=libs/qemu-docker/windows" >> $GITHUB_OUTPUT
echo "type=docker" >> $GITHUB_OUTPUT
;;
*)
echo "Unknown service: ${{ inputs.service }}"
exit 1
;;
esac
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.target_branch || 'main' }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install bump2version
run: pip install bump2version
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Reconcile cua-driver-rs bump base with Cargo.toml (drift guard)
if: ${{ inputs.service == 'cua-driver-rs' }}
run: |
# Cargo.toml is the single source of truth for the cua-driver-rs
# version. bump2version keeps a duplicate current_version in
# .bumpversion.cfg, and that copy has silently drifted before and cut
# a release at the wrong version (binary reported a stale version,
# tag could downgrade). Reconcile the cfg to Cargo.toml here, before
# the dry-run tag-clean and the real bump both read it, so a bump can
# never start from a stale base again no matter what the committed
# cfg says.
cd "${{ steps.package.outputs.directory }}"
CARGO_VERSION=$(grep -m1 '^version[[:space:]]*=' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
CFG_VERSION=$(grep -m1 '^current_version[[:space:]]*=' .bumpversion.cfg | sed -E 's/.*=[[:space:]]*//')
if [ -z "$CARGO_VERSION" ]; then
echo "::error::could not read version from libs/cua-driver/rust/Cargo.toml"
exit 1
fi
if [ "$CARGO_VERSION" != "$CFG_VERSION" ]; then
echo "::warning::.bumpversion.cfg current_version ($CFG_VERSION) drifted from Cargo.toml ($CARGO_VERSION); reconciling to Cargo.toml before bump."
sed -i.bak -E "s/^current_version[[:space:]]*=.*/current_version = ${CARGO_VERSION}/" .bumpversion.cfg
rm -f .bumpversion.cfg.bak
fi
echo "cua-driver-rs bump base: $(grep -m1 '^current_version' .bumpversion.cfg)"
- name: Clean existing tags if present
run: |
# Function to clean a tag for a given package directory
clean_tag() {
local dir=$1
local config_file=${2:-.bumpversion.cfg}
cd "$dir"
NEW_VERSION=$(bump2version --config-file "$config_file" --dry-run --list ${{ inputs.bump_type }} 2>/dev/null | grep -m1 '^new_version=' | sed 's/new_version=//')
TAG_NAME=$(grep -m1 '^tag_name' "$config_file" | sed 's/tag_name *= *//' | sed "s|{new_version}|$NEW_VERSION|")
echo "Checking for existing tag: $TAG_NAME"
git tag -d "$TAG_NAME" 2>/dev/null || true
git push origin ":refs/tags/$TAG_NAME" 2>/dev/null || true
cd - > /dev/null
}
# Clean tag for the primary package
BUMPVERSION_CONFIG="${{ steps.package.outputs.bumpversion_config }}"
clean_tag "${{ steps.package.outputs.directory }}" "${BUMPVERSION_CONFIG:-.bumpversion.cfg}"
# No cascade tag cleaning — each package is bumped independently.
# Dependent packages use version ranges and resolve deps at publish time.
- name: Run bump2version
run: |
cd ${{ steps.package.outputs.directory }}
CONFIG_FLAG=""
if [ -n "${{ steps.package.outputs.bumpversion_config }}" ]; then
CONFIG_FLAG="--config-file ${{ steps.package.outputs.bumpversion_config }}"
fi
bump2version $CONFIG_FLAG ${{ inputs.bump_type }}
- name: Sync Cargo.lock into the cua-driver-rs bump commit
# bump2version edits only `[workspace.package] version` in Cargo.toml,
# leaving Cargo.lock's workspace-member versions stale. The next plain
# `cargo build` anyone runs then re-locks them and diverges from the
# committed lockfile — which used to break the nix build's cargoHash.
# Re-lock the members now and fold the change into the bump commit so the
# lockfile never ships out of sync with the manifest. (cargo is
# preinstalled on ubuntu runners; `--workspace` touches members only, not
# registry deps, so this never silently bumps dependencies.)
if: ${{ inputs.service == 'cua-driver-rs' }}
run: |
cd ${{ steps.package.outputs.directory }}
cargo update --workspace
if ! git diff --quiet Cargo.lock; then
TAG=$(git tag --points-at HEAD | head -1)
git add Cargo.lock
git commit --amend --no-edit
# bump2version tagged the pre-amend commit; move the tag onto the
# amended commit so the release pipeline builds the synced lockfile.
[ -n "$TAG" ] && git tag -f "$TAG" HEAD
echo "Re-locked Cargo.lock and moved tag ${TAG:-<none>} to the amended bump commit."
else
echo "Cargo.lock already in sync; nothing to fold in."
fi
- name: Collect created tag
id: collect_tags
run: |
# Only one tag is created per bump (no cascades)
ALL_TAGS=$(git tag --points-at HEAD | tr '\n' ' ' | xargs)
echo "Tags to push: $ALL_TAGS"
echo "tags=$ALL_TAGS" >> $GITHUB_OUTPUT
- name: Capture bumped cua version
if: ${{ inputs.service == 'pypi/cua' }}
id: cua_version
run: |
cd libs/python/cua
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "cua version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped agent version
if: ${{ inputs.service == 'pypi/agent' }}
id: agent_version
run: |
cd libs/python/agent
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Agent version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped auto version
if: ${{ inputs.service == 'pypi/auto' }}
id: auto_version
run: |
cd libs/python/cua-auto
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Auto version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped bench version
if: ${{ inputs.service == 'pypi/bench' }}
id: bench_version
run: |
cd libs/cua-bench
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Bench version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped bench-ui version
if: ${{ inputs.service == 'pypi/bench-ui' }}
id: bench_ui_version
run: |
cd libs/python/bench-ui
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Bench-ui version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped cli version
if: ${{ inputs.service == 'pypi/cli' }}
id: cli_version
run: |
cd libs/python/cua-cli
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "CLI version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped computer version
if: ${{ inputs.service == 'pypi/computer' }}
id: computer_version
run: |
cd libs/python/computer
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Computer version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped cloud version
if: ${{ inputs.service == 'pypi/cloud' }}
id: cloud_version
run: |
cd libs/python/cua-cloud
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Cloud version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped computer-server version
if: ${{ inputs.service == 'pypi/computer-server' }}
id: computer_server_version
run: |
cd libs/python/computer-server
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Computer-server version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped core version
if: ${{ inputs.service == 'pypi/core' }}
id: core_version
run: |
cd libs/python/core
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Core version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped mcp-server version
if: ${{ inputs.service == 'pypi/mcp-server' }}
id: mcp_server_version
run: |
cd libs/python/mcp-server
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "MCP-server version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped sandbox version
if: ${{ inputs.service == 'pypi/sandbox' }}
id: sandbox_version
run: |
cd libs/python/cua-sandbox
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Sandbox version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped sandbox-apps version
if: ${{ inputs.service == 'pypi/sandbox-apps' }}
id: sandbox_apps_version
run: |
cd libs/python/cua-sandbox-apps
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Sandbox-apps version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped som version
if: ${{ inputs.service == 'pypi/som' }}
id: som_version
run: |
cd libs/python/som
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "SOM version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped train version
if: ${{ inputs.service == 'pypi/train' }}
id: train_version
run: |
cd libs/python/cua-train
VERSION=$(python -c "import tomllib; from pathlib import Path; data = tomllib.loads(Path('pyproject.toml').read_text()); print(data['project']['version'])")
echo "Train version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped npm/cli version
if: ${{ inputs.service == 'npm/cli' }}
id: npm_cli_version
run: |
VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/cua-cli/package.json)
echo "npm/cli version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped npm/computer version
if: ${{ inputs.service == 'npm/computer' }}
id: npm_computer_version
run: |
VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/computer/package.json)
echo "npm/computer version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped npm/core version
if: ${{ inputs.service == 'npm/core' }}
id: npm_core_version
run: |
VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/core/package.json)
echo "npm/core version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped npm/playground version
if: ${{ inputs.service == 'npm/playground' }}
id: npm_playground_version
run: |
VERSION=$(grep -oP '"version": "\K[^"]+' libs/typescript/playground/package.json)
echo "npm/playground version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped lume version
if: ${{ inputs.service == 'lume' }}
id: lume_version
run: |
VERSION=$(grep -oP 'static let current: String = "\K[^"]+' libs/lume/src/Main.swift)
echo "lume version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped cua-driver version
if: ${{ inputs.service == 'cua-driver' }}
id: cua_driver_version
run: |
VERSION=$(grep -oP 'public static let version = "\K[^"]+' libs/cua-driver/Sources/CuaDriverCore/CuaDriverCore.swift)
echo "cua-driver version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped cua-driver-rs version
if: ${{ inputs.service == 'cua-driver-rs' }}
id: cua_driver_rs_version
run: |
VERSION=$(grep -m1 -oP '^version = "\K[^"]+' libs/cua-driver/rust/Cargo.toml)
echo "cua-driver-rs version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Capture bumped cuabot version
if: ${{ inputs.service == 'npm/cuabot' }}
id: cuabot_version
run: |
VERSION=$(grep -oP '"version": "\K[^"]+' libs/cuabot/package.json)
echo "cuabot version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Generate GitHub App token
id: app-token
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: Push release to target branch
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TARGET_BRANCH: ${{ inputs.target_branch || 'main' }}
run: |
# Configure git to use the app token
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
# Verify the token works
echo "Verifying GitHub App token..."
gh auth status
# Get all tags created by bump2version
ALL_TAGS="${{ steps.collect_tags.outputs.tags }}"
echo "Tags to push: $ALL_TAGS"
# Use the first tag for naming the temp branch
FIRST_TAG=$(echo "$ALL_TAGS" | awk '{print $1}')
TEMP_BRANCH="temp-release-${FIRST_TAG}"
echo "Temp branch: $TEMP_BRANCH"
# Retry loop for handling concurrent bumps
MAX_RETRIES=5
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo ""
echo "=== Attempt $RETRY_COUNT/$MAX_RETRIES ==="
# Fetch latest target branch to check if we need to rebase
git fetch origin "${TARGET_BRANCH}"
# Check if our HEAD is a descendant of origin/<branch> (fast-forward possible)
if ! git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD; then
echo "Branch ${TARGET_BRANCH} has moved forward, rebasing..."
# Rebase our commit(s) onto the latest branch
git rebase "origin/${TARGET_BRANCH}"
# Update the tag to point to the rebased commit
for tag in $ALL_TAGS; do
git tag -f "$tag" HEAD
echo "Updated tag $tag to rebased HEAD"
done
fi
# Get the current commit SHA (may have changed after rebase)
COMMIT_SHA=$(git rev-parse HEAD)
echo "Commit SHA: $COMMIT_SHA"
# Push to temp branch
echo "Pushing to temp branch..."
git push origin "HEAD:refs/heads/${TEMP_BRANCH}" --force
# Try to update target branch via API
echo "Updating ${TARGET_BRANCH} branch via API..."
if gh api -X PATCH "/repos/${{ github.repository }}/git/refs/heads/${TARGET_BRANCH}" \
-f sha="${COMMIT_SHA}" \
-F force=false 2>&1; then
echo "Successfully updated ${TARGET_BRANCH} branch!"
# Create tag via API (only after main is successfully updated)
echo "Creating tag via API..."
for tag in $ALL_TAGS; do
echo "Creating tag: $tag"
gh api -X DELETE /repos/${{ github.repository }}/git/refs/tags/${tag} 2>/dev/null || true
gh api /repos/${{ github.repository }}/git/refs \
-f ref="refs/tags/${tag}" \
-f sha="${COMMIT_SHA}"
echo "Tag $tag created successfully!"
done
# Clean up temp branch
echo "Cleaning up temp branch..."
git push origin --delete "${TEMP_BRANCH}" || true
exit 0
fi
echo "Failed to update ${TARGET_BRANCH} (not a fast-forward), will retry..."
sleep 2
done
echo "ERROR: Failed to update ${TARGET_BRANCH} branch after $MAX_RETRIES attempts"
exit 1
# NOTE: Publishing is handled by tag-triggered workflows (cd-py-*.yml, cd-ts-*.yml, cd-swift-lume.yml)
# When bump-version pushes a tag (e.g., agent-v0.7.10), the corresponding CD workflow
# is automatically triggered by the tag push event. No need to call them here.
@@ -0,0 +1,181 @@
name: Reusable GitHub Release Workflow
on:
workflow_call:
inputs:
tag_name:
description: "Git tag name (e.g., computer-v0.5.0)"
required: true
type: string
release_name:
description: "Release display name (e.g., cua-computer v0.5.0)"
required: true
type: string
module_path:
description: "Path to the module for filtering commits (e.g., libs/python/agent)"
required: false
type: string
default: ""
generate_notes:
description: "Auto-generate release notes with attribution"
required: false
type: boolean
default: true
body:
description: "Custom release notes content (appended to auto-generated notes)"
required: false
type: string
default: ""
attach_artifacts:
description: "Whether to download and attach workflow artifacts to the release"
required: false
type: boolean
default: false
draft:
description: "Create as draft release"
required: false
type: boolean
default: false
prerelease:
description: "Mark as pre-release"
required: false
type: boolean
default: false
permissions:
contents: write
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate path-filtered release notes
if: inputs.module_path != '' && inputs.generate_notes
id: release-notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Extract tag prefix to find previous release (e.g., "pypi-agent-v" from "pypi-agent-v0.5.0")
TAG_PREFIX=$(echo "${{ inputs.tag_name }}" | sed 's/[0-9]*\.[0-9]*\.[0-9]*$//')
# Find previous tag with same prefix
PREV_TAG=$(git tag -l "${TAG_PREFIX}*" --sort=-v:refname | grep -v "^${{ inputs.tag_name }}$" | head -n 1 || echo "")
echo "Current tag: ${{ inputs.tag_name }}"
echo "Tag prefix: $TAG_PREFIX"
echo "Previous tag: $PREV_TAG"
# Generate release notes with GitHub username attribution
NOTES=""
if [ -n "$PREV_TAG" ]; then
echo "Generating notes for commits between $PREV_TAG and HEAD in ${{ inputs.module_path }}"
COMMIT_RANGE="${PREV_TAG}..HEAD"
else
echo "No previous tag found, generating notes for all commits in ${{ inputs.module_path }}"
COMMIT_RANGE="HEAD"
fi
# Get commits and process each one to fetch GitHub username
while IFS='|' read -r sha subject; do
# Skip automated bump commits (e.g., "Bump cua-agent to v0.7.22")
if [[ "$subject" =~ ^Bump\ cua- ]]; then
continue
fi
# Try to get GitHub username via API
USERNAME=$(gh api repos/${{ github.repository }}/commits/${sha} --jq '.author.login' 2>/dev/null || echo "")
# If API call fails or no author found (empty or "null"), fall back to commit author email
if [ -z "$USERNAME" ] || [ "$USERNAME" = "null" ]; then
AUTHOR_EMAIL=$(git show -s --format='%ae' ${sha})
# Try to extract username from GitHub noreply email format
if [[ "$AUTHOR_EMAIL" =~ ^([0-9]+\+)?([^@]+)@users\.noreply\.github\.com$ ]]; then
USERNAME="${BASH_REMATCH[2]}"
else
# Last resort: use author name without @ symbol
USERNAME=$(git show -s --format='%an' ${sha})
fi
fi
# Link PR numbers: (#123) -> ([#123](https://github.com/REPO/pull/123))
LINKED_SUBJECT=$(echo "$subject" | sed 's~(#\([0-9]*\))~([#\1](https://github.com/${{ github.repository }}/pull/\1))~g')
SHORT_SHA=$(echo ${sha} | cut -c1-7)
NOTES="${NOTES}* ${LINKED_SUBJECT} (${SHORT_SHA}) by @${USERNAME}"$'\n'
done < <(git log ${COMMIT_RANGE} --pretty=format:"%H|%s" -- "${{ inputs.module_path }}" | head -50)
if [ -z "$NOTES" ]; then
NOTES="Maintenance release — dependency updates only."
fi
# Store notes in output (handle multiline)
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
echo "## What's Changed" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Download artifacts
if: inputs.attach_artifacts
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: List downloaded artifacts
if: inputs.attach_artifacts
run: |
echo "Downloaded artifacts:"
ls -lah release-assets/
- name: Prepare release body
id: prepare-body
env:
# Use env vars to avoid shell interpretation of backticks in commit messages
RELEASE_NOTES: ${{ steps.release-notes.outputs.RELEASE_NOTES }}
CUSTOM_BODY: ${{ inputs.body }}
run: |
# Combine path-filtered notes with custom body
BODY=""
# Add path-filtered notes if available
if [ -n "$RELEASE_NOTES" ]; then
BODY="$RELEASE_NOTES"
fi
# Add custom body if provided
if [ -n "$CUSTOM_BODY" ]; then
if [ -n "$BODY" ]; then
BODY="$BODY
$CUSTOM_BODY"
else
BODY="$CUSTOM_BODY"
fi
fi
# Store in output
echo "BODY<<EOF" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.tag_name }}
name: ${{ inputs.release_name }}
body: ${{ steps.prepare-body.outputs.BODY }}
draft: ${{ inputs.draft }}
prerelease: ${{ inputs.prerelease }}
# Only use GitHub auto-generated notes if module_path is not specified
generate_release_notes: ${{ inputs.module_path == '' && inputs.generate_notes }}
files: |
release-assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+135
View File
@@ -0,0 +1,135 @@
name: "CD: Auto Release on Merge"
on:
pull_request:
types: [closed]
permissions:
actions: write
contents: read
pull-requests: read
jobs:
auto-release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Detect packages, release labeled, notify unlabeled
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_TITLE="${{ github.event.pull_request.title }}"
PR_URL="${{ github.event.pull_request.html_url }}"
REPO="${{ github.repository }}"
# Get labels
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
# Skip everything if no-release
if echo "$LABELS" | grep -q '"no-release"'; then
echo "no-release label found, skipping."
exit 0
fi
# Determine bump type from labels (default: patch)
BUMP_TYPE="patch"
if echo "$LABELS" | grep -q '"bump:major"'; then
BUMP_TYPE="major"
elif echo "$LABELS" | grep -q '"bump:minor"'; then
BUMP_TYPE="minor"
fi
echo "Bump type: $BUMP_TYPE"
# Get changed files
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only 2>/dev/null || true)
if [ -z "$CHANGED_FILES" ]; then
echo "No changed files, skipping."
exit 0
fi
# Map paths to publishable services
declare -A PATH_TO_SERVICE=(
["libs/python/cua-cli/"]="pypi/cli"
["libs/python/cua-auto/"]="pypi/auto"
["libs/python/agent/"]="pypi/agent"
["libs/python/cua-sandbox/"]="pypi/sandbox"
["libs/python/computer/"]="pypi/computer"
["libs/python/cua-cloud/"]="pypi/cloud"
["libs/python/core/"]="pypi/core"
["libs/python/som/"]="pypi/som"
["libs/python/bench-ui/"]="pypi/bench-ui"
["libs/cua-bench/"]="pypi/bench"
["libs/python/computer-server/"]="pypi/computer-server"
["libs/python/mcp-server/"]="pypi/mcp-server"
["libs/typescript/cua-cli/"]="npm/cli"
["libs/typescript/computer/"]="npm/computer"
["libs/typescript/core/"]="npm/core"
["libs/typescript/playground/"]="npm/playground"
["libs/cuabot/"]="npm/cuabot"
["libs/xfce/"]="docker/xfce"
["libs/kasm/"]="docker/kasm"
["libs/lumier/"]="docker/lumier"
["libs/qemu-docker/android/"]="docker/qemu-android"
["libs/qemu-docker/linux/"]="docker/qemu-linux"
["libs/qemu-docker/windows/"]="docker/qemu-windows"
["libs/lume/"]="lume"
["libs/cua-driver/rust/"]="cua-driver-rs"
)
# Find all affected services from changed files
declare -A AFFECTED_MAP
for path_prefix in "${!PATH_TO_SERVICE[@]}"; do
if echo "$CHANGED_FILES" | grep -q "^${path_prefix}"; then
service="${PATH_TO_SERVICE[$path_prefix]}"
AFFECTED_MAP["$service"]=1
fi
done
if [ ${#AFFECTED_MAP[@]} -eq 0 ]; then
echo "No publishable packages affected, skipping."
exit 0
fi
echo "Affected packages:"
for svc in "${!AFFECTED_MAP[@]}"; do echo " - $svc"; done
# Collect labeled packages for auto-release
LABELED=()
for svc in "${!AFFECTED_MAP[@]}"; do
if echo "$LABELS" | grep -q "\"release:${svc}\""; then
LABELED+=("$svc")
fi
done
if [ ${#LABELED[@]} -eq 0 ]; then
echo "No release labels found. Daily digest will catch unreleased packages."
exit 0
fi
echo ""
echo "Packages with release labels (will auto-release):"
for svc in "${LABELED[@]}"; do echo " - $svc"; done
# Trigger each labeled package independently (no cascade — deps use version ranges)
for svc in "${LABELED[@]}"; do
echo ""
echo "Triggering release-bump-version: service=$svc bump_type=$BUMP_TYPE"
gh workflow run release-bump-version.yml \
--repo "$REPO" \
-f service="$svc" \
-f bump_type="$BUMP_TYPE"
echo "Triggered successfully."
sleep 15
done
echo ""
echo "Done!"
@@ -0,0 +1,106 @@
name: "CD: Unreleased Changes Digest"
on:
schedule:
# Daily at 8pm PT / 4am UTC (Mon-Fri PT = Tue-Sat UTC)
- cron: "0 4 * * 2-6"
workflow_dispatch: {}
jobs:
digest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Check for unreleased changes per package
run: |
# Service → tag prefix → directory
declare -A SERVICE_TAG_DIR
SERVICE_TAG_DIR["pypi/cli"]="cli-v|libs/python/cua-cli/"
SERVICE_TAG_DIR["pypi/auto"]="auto-v|libs/python/cua-auto/"
SERVICE_TAG_DIR["pypi/agent"]="agent-v|libs/python/agent/"
SERVICE_TAG_DIR["pypi/computer"]="computer-v|libs/python/computer/"
SERVICE_TAG_DIR["pypi/cloud"]="cloud-v|libs/python/cua-cloud/"
SERVICE_TAG_DIR["pypi/core"]="core-v|libs/python/core/"
SERVICE_TAG_DIR["pypi/som"]="som-v|libs/python/som/"
SERVICE_TAG_DIR["pypi/bench"]="bench-v|libs/cua-bench/"
SERVICE_TAG_DIR["pypi/bench-ui"]="bench-ui-v|libs/python/bench-ui/"
SERVICE_TAG_DIR["pypi/computer-server"]="computer-server-v|libs/python/computer-server/"
SERVICE_TAG_DIR["pypi/mcp-server"]="mcp-server-v|libs/python/mcp-server/"
SERVICE_TAG_DIR["npm/cli"]="npm-cli-v|libs/typescript/cua-cli/"
SERVICE_TAG_DIR["npm/computer"]="npm-computer-v|libs/typescript/computer/"
SERVICE_TAG_DIR["npm/core"]="npm-core-v|libs/typescript/core/"
SERVICE_TAG_DIR["npm/playground"]="npm-playground-v|libs/typescript/playground/"
SERVICE_TAG_DIR["npm/cuabot"]="cuabot-v|libs/cuabot/"
SERVICE_TAG_DIR["docker/xfce"]="docker-xfce-v|libs/xfce/"
SERVICE_TAG_DIR["docker/kasm"]="docker-kasm-v|libs/kasm/"
SERVICE_TAG_DIR["docker/lumier"]="docker-lumier-v|libs/lumier/"
SERVICE_TAG_DIR["docker/qemu-android"]="docker-cua-qemu-android-v|libs/qemu-docker/android/"
SERVICE_TAG_DIR["docker/qemu-linux"]="docker-cua-qemu-linux-v|libs/qemu-docker/linux/"
SERVICE_TAG_DIR["docker/qemu-windows"]="docker-cua-qemu-windows-v|libs/qemu-docker/windows/"
SERVICE_TAG_DIR["lume"]="lume-v|libs/lume/"
SERVICE_TAG_DIR["cua-driver-rs"]="cua-driver-rs-v|libs/cua-driver/rust/"
UNRELEASED=""
UNRELEASED_COUNT=0
for service in $(printf '%s\n' "${!SERVICE_TAG_DIR[@]}" | sort); do
IFS='|' read -r tag_prefix directory <<< "${SERVICE_TAG_DIR[$service]}"
# Find the latest tag for this service
LATEST_TAG=$(git tag -l "${tag_prefix}*" --sort=-v:refname | head -1)
if [ -z "$LATEST_TAG" ]; then
# No tags at all — check if directory has any commits
COMMIT_COUNT=$(git log --oneline -- "$directory" | wc -l | xargs)
if [ "$COMMIT_COUNT" -gt 0 ]; then
UNRELEASED="${UNRELEASED}${service} (never released, ${COMMIT_COUNT} commits)\n"
UNRELEASED_COUNT=$((UNRELEASED_COUNT + 1))
fi
continue
fi
# Count commits in this directory since the latest tag
COMMIT_COUNT=$(git log --oneline "${LATEST_TAG}..HEAD" -- "$directory" | wc -l | xargs)
if [ "$COMMIT_COUNT" -gt 0 ]; then
UNRELEASED="${UNRELEASED}${service} (${COMMIT_COUNT} commits since ${LATEST_TAG})\n"
UNRELEASED_COUNT=$((UNRELEASED_COUNT + 1))
fi
done
if [ "$UNRELEASED_COUNT" -eq 0 ]; then
echo "All packages are up to date!"
exit 0
fi
echo "Packages with unreleased changes:"
echo -e "$UNRELEASED"
# Build description for AlertManager
DESCRIPTION=$(echo -e "$UNRELEASED" | sed 's/$/; /' | tr -d '\n' | sed 's/; $//')
# Send alert to AlertManager → Slack
curl -s -X POST https://am.cua.ai/api/v2/alerts \
-H "Content-Type: application/json" \
-d "[
{
\"labels\": {
\"alertname\": \"UnreleasedChangesDigest\",
\"severity\": \"info\",
\"source\": \"github-actions\"
},
\"annotations\": {
\"summary\": \"${UNRELEASED_COUNT} packages have unreleased changes on main\",
\"description\": \"${DESCRIPTION}\"
},
\"generatorURL\": \"https://github.com/${{ github.repository }}/actions/workflows/release-bump-version.yml\"
}
]"
echo ""
echo "Slack digest sent."
+66
View File
@@ -0,0 +1,66 @@
name: Reusable NPM Package Build Workflow
on:
workflow_call:
inputs:
package_name:
description: "Name of the package (e.g., cli, computer, core)"
required: true
type: string
package_dir:
description: "Directory containing the package relative to workspace root (e.g., libs/typescript/cua-cli)"
required: true
type: string
package_manager:
description: "Package manager to use (pnpm or bun)"
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: "24.x"
- name: Setup pnpm
if: inputs.package_manager == 'pnpm'
uses: pnpm/action-setup@v4
with:
package_json_file: ${{ inputs.package_dir }}/package.json
- name: Setup Bun
if: inputs.package_manager == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
working-directory: ./${{ inputs.package_dir }}
run: |
if [ "${{ inputs.package_manager }}" = "bun" ]; then
bun install --frozen-lockfile
else
pnpm install --frozen-lockfile
fi
- name: Build package
working-directory: ./${{ inputs.package_dir }}
run: |
if [ "${{ inputs.package_manager }}" = "bun" ]; then
if grep -q '"build"' package.json; then
bun run build
fi
else
pnpm run build --if-present
fi
- name: Verify build
working-directory: ./${{ inputs.package_dir }}
run: |
echo "Successfully built @trycua/${{ inputs.package_name }}"
ls -la
+109
View File
@@ -0,0 +1,109 @@
name: Reusable NPM Package Publish Workflow
on:
workflow_call:
inputs:
package_name:
description: "Name of the package (e.g., cli, computer, core)"
required: true
type: string
package_dir:
description: "Directory containing the package relative to workspace root (e.g., libs/typescript/cua-cli)"
required: true
type: string
package_manager:
description: "Package manager to use (pnpm or bun)"
required: true
type: string
version:
description: "Version to publish (optional - will check version diff if not provided)"
required: false
type: string
default: ""
secrets:
NPM_TOKEN:
required: true
jobs:
publish:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
- name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Setup pnpm
if: inputs.package_manager == 'pnpm'
uses: pnpm/action-setup@v4
with:
package_json_file: ${{ inputs.package_dir }}/package.json
- name: Setup Bun
if: inputs.package_manager == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Verify version consistency
if: inputs.version != ''
run: |
EXPECTED_VERSION="${{ inputs.version }}"
ACTUAL_VERSION=$(node -p "require('./${{ inputs.package_dir }}/package.json').version")
if [ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]; then
echo "ERROR: Version mismatch!"
echo "Expected version: $EXPECTED_VERSION"
echo "Actual version in package.json: $ACTUAL_VERSION"
exit 1
fi
echo "Version validation passed: $ACTUAL_VERSION"
echo "should_publish=true" >> $GITHUB_OUTPUT
id: verify-version
- name: Check if version changed
if: steps.verify-version.outputs.should_publish != 'true'
id: check-version
uses: EndBug/version-check@v2
with:
file-name: ${{ inputs.package_dir }}/package.json
diff-search: true
- name: Install dependencies
if: steps.verify-version.outputs.should_publish == 'true' || steps.check-version.outputs.changed == 'true'
working-directory: ./${{ inputs.package_dir }}
run: |
if [ "${{ inputs.package_manager }}" = "bun" ]; then
bun install --frozen-lockfile
else
pnpm install --frozen-lockfile
fi
- name: Build package
if: steps.verify-version.outputs.should_publish == 'true' || steps.check-version.outputs.changed == 'true'
working-directory: ./${{ inputs.package_dir }}
run: |
if [ "${{ inputs.package_manager }}" = "bun" ]; then
# Bun doesn't support --if-present, check if build script exists
if grep -q '"build"' package.json; then
bun run build
fi
else
pnpm run --if-present build
fi
- name: Publish to npm
if: steps.verify-version.outputs.should_publish == 'true' || steps.check-version.outputs.changed == 'true'
working-directory: ./${{ inputs.package_dir }}
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+220
View File
@@ -0,0 +1,220 @@
**/image/setup.iso
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
node_modules/*
*/node_modules
**/node_modules
# Distribution / packaging
.Python
build/
!libs/lume/scripts/build/
!libs/cua-driver/scripts/build/
!libs/cua-driver/tests/fixtures/build/
!libs/cua-driver/tests/fixtures/build/**
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/*
!libs/lumier/src/lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
artifacts/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Git worktrees
.worktrees/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Scripts
server/scripts/
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Conda
.conda/
# Local environment
.env.local
# macOS DS_Store
.DS_Store
weights/
weights/icon_detect/
weights/icon_detect/model.pt
weights/icon_detect/model.pt.zip
weights/icon_detect/model.pt.zip.part*
libs/python/omniparser/weights/icon_detect/model.pt
/screenshots/
/experiments/
/logs/
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm/
.build/
/Package.resolved
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Ignore folder
ignore
# .release
.release/
# Provisioning profiles (generated from CI secrets)
*.provisionprofile
# Shared folder
shared
# Trajectories
trajectories/
# Installation ID Storage
.storage/
# Gradio settings
.gradio_settings.json
# Lumier Storage
storage/
# Trashes
.Trashes
.Trash-1000/
post-provision
# Local secrets for act
.secrets
# Link checker scripts (dev tools)
scripts/check-repo-md-links.py
docs/scripts/check-links.py
docs/scripts/check-all-links.py
docs/scripts/check-mdx-links.py
# Local Windows RE scratch (downloaded PDBs, disassembly scripts)
.re-windows/
+10
View File
@@ -0,0 +1,10 @@
https://cua.ai/
https://api.cua.ai/
# Google Cloud Console links have HTTP/2 protocol issues with automated checkers
https://console.cloud.google.com/*
# hud.ai rate-limits automated link checkers (429)
https://www.hud.ai/*
# openai.com returns 403 to automated checkers but resolves fine in-browser
https://openai.com/*
# Self-referential install.sh URL: resolves once this PR merges to main
https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh
+50
View File
@@ -0,0 +1,50 @@
repos:
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0
hooks:
- id: prettier
name: Prettier (TS/JS/JSON/Markdown/YAML)
entry: prettier --write
language: node
additional_dependencies: ["prettier@3.6.2"]
files: \.(ts|tsx|js|jsx|json|md|mdx|yaml|yml)$
- repo: local
hooks:
- id: typescript-typecheck
name: TypeScript type check
entry: pnpm -C libs/typescript typecheck
language: system
files: ^libs/typescript/.*\.(ts|tsx)$
pass_filenames: false
- repo: https://github.com/PyCQA/isort
rev: 7.0.0
hooks:
- id: isort
name: isort code formatter
args: ["--profile", "black"]
files: \.(py)$
- repo: https://github.com/psf/black
rev: 25.9.0
hooks:
- id: black
name: Black code formatter
files: \.(py)$
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.14.1
hooks:
- id: ruff
name: ruff linter
args: ["--fix"]
files: \.(py)$
# Temporarily disabled due to untyped codebase
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.5.1
# hooks:
# - id: mypy
# name: mypy type checker
# files: \.(py)$
+42
View File
@@ -0,0 +1,42 @@
# Node / JS
node_modules/
dist/
build/
out/
.next/
*.min.js
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.venv/
venv/
.env
.env.local
# Logs
*.log
*.tmp
# VSCode / editor files
.vscode/
.idea/
# Other generated files
*.lock
*.db
*.sqlite
pnpm-lock.yaml
uv.lock
# Auto-generated docs source
docs/.source/
docs/next-env.d.ts
# MDX files (prettier mangles {/* */} comment syntax into {/_ _/})
*.mdx
# Git worktrees (separate branches)
.worktrees/
+12
View File
@@ -0,0 +1,12 @@
semi: true
singleQuote: true
trailingComma: es5
tabWidth: 2
printWidth: 100
arrowParens: always
bracketSpacing: true
overrides:
- files: "*.{yml,yaml}"
options:
singleQuote: false
+16
View File
@@ -0,0 +1,16 @@
{
"folders": [
{
"path": "../docs"
}
],
"settings": {
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"charliermarsh.ruff",
"ms-python.black-formatter",
"ms-python.mypy-type-checker",
"ms-python.vscode-pylance",
"ms-python.isort"
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"configurations": [
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:cua-root}/libs/lume",
"name": "Debug lume (libs/lume)",
"program": "${workspaceFolder:cua-root}/libs/lume/.build/debug/lume",
"preLaunchTask": "swift: Build Debug lume (libs/lume)"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:cua-root}/libs/lume",
"name": "Release lume (libs/lume)",
"program": "${workspaceFolder:cua-root}/libs/lume/.build/release/lume",
"preLaunchTask": "swift: Build Release lume (libs/lume)"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"folders": [
{
"name": "libs-ts",
"path": "../libs/typescript"
}
],
"extensions": {
"recommendations": [
"esbenp.prettier-vscode"
]
}
}

Some files were not shown because too many files have changed in this diff Show More