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
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:
+229
@@ -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/
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
@@ -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']
|
||||
Executable
+78
@@ -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()
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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/"]
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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."
|
||||
@@ -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 }}
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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/*
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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: ""
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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 .
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 -
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "╚══════════════════════════════════════════════════════════════════╝"
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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/*
|
||||
@@ -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 }}
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
@@ -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
|
||||
@@ -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)$
|
||||
@@ -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/
|
||||
@@ -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
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "../docs"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -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"
|
||||
]
|
||||
}
|
||||
Vendored
+22
@@ -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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+13
@@ -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
Reference in New Issue
Block a user