diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b3be16f9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +# App CI — the coworker Python suite + the GUI unit tests. +# +# The Playwright e2e suite (surfaces/gui, `npm run e2e`) is NOT wired in yet: it has +# known pre-existing failures (automations/standing-approvals specs) inherited from the +# source repo. Add it here once those are fixed so CI starts green and stays green. + +name: CI + +on: [push, pull_request] + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ".[messaging,dev]" + - name: Test + run: pytest tests -q + + gui-unit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: surfaces/gui/package-lock.json + - name: npm ci + working-directory: surfaces/gui + run: npm ci + - name: Unit tests + working-directory: surfaces/gui + run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..213de4a6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,193 @@ +# Desktop release builds — macOS (.dmg, arm64 + Intel) and Windows (.msi + NSIS .exe). +# +# CI calls the SAME scripts developers run locally (packaging/build_dmg.sh and +# build_windows.ps1); this file only provisions the toolchain (Node, Rust, a Python venv at +# .venv with PyInstaller) and publishes the results. +# +# Triggers: +# - tag push `v*` → builds all targets and attaches them to a DRAFT GitHub Release +# (review, then publish by hand). +# - manual run → builds and uploads workflow artifacts only (no release). +# +# Each installer is uploaded twice: once with its versioned name (archive) and once with a +# stable name (OpenWorker-macos-arm64.dmg, …) so the website can link to +# github.com//releases/latest/download/ +# and never need updating. +# +# macOS signing + notarization: Tauri's bundler handles both during `tauri build` when the +# APPLE_* env vars are present (import cert → sign app + sidecar with hardened runtime → +# notarize via notarytool → staple). Driven by repo secrets: +# APPLE_CERTIFICATE base64 .p12 (Developer ID Application cert + key) +# APPLE_CERTIFICATE_PASSWORD the .p12 export password +# APPLE_SIGNING_IDENTITY e.g. "Developer ID Application: Name (TEAMID)" +# APPLE_API_KEY_CONTENT base64 App Store Connect API .p8 (notarytool) +# APPLE_API_KEY the API key id +# APPLE_API_ISSUER the API issuer id +# When the secrets are absent (forks, scratch runs) the build degrades to unsigned — +# installable via `xattr -cr`. Windows remains unsigned (Authenticode is a later step). + +name: Release + +on: + push: + tags: ["v*", "app-v*"] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + # No Intel macOS target: macos-13 (the last Intel runner image) is deprecated and + # its queue waits run to hours, which blocks the release job. Intel Macs are + # 2020-and-earlier hardware — revisit only if beta users actually ask. + - os: macos-latest # Apple Silicon + slug: macos-arm64 + - os: windows-latest + slug: windows + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: surfaces/gui/package-lock.json + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: surfaces/gui/src-tauri + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up the sidecar venv (.venv) + # The build scripts expect a venv at .venv with the package + PyInstaller. + # typer/tzdata are build-time-only (PyInstaller walks mcp.cli, which needs typer; + # tzdata ships zoneinfo for Windows). aisuite installs like any other dependency + # (git-pinned in pyproject.toml). + run: | + python -m venv .venv + if [ "$RUNNER_OS" = "Windows" ]; then VPY=.venv/Scripts/python; else VPY=.venv/bin/python; fi + "$VPY" -m pip install --upgrade pip + "$VPY" -m pip install -e . pyinstaller typer tzdata + "$VPY" -c "import aisuite, coworker" # fail fast if either import breaks + + - name: npm ci + working-directory: surfaces/gui + run: npm ci + + - name: Build .dmg (macOS) + if: runner.os == 'macOS' + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + # Auto-update artifact signing (minisign, separate from Apple signing). Absent → + # the build script skips updater artifacts with a warning (fork/scratch runs). + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + # Unset empty APPLE_* vars so runs without secrets stay cleanly unsigned + # (Tauri treats a present-but-empty var as a config error). + for v in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_API_KEY APPLE_API_ISSUER; do + [ -n "$(eval echo "\${$v:-}")" ] || unset "$v" + done + if [ -n "${APPLE_API_KEY_CONTENT:-}" ]; then + echo "$APPLE_API_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/AuthKey.p8" + export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8" + fi + unset APPLE_API_KEY_CONTENT + bash packaging/build_dmg.sh + + - name: Build .msi + NSIS .exe (Windows) + if: runner.os == 'Windows' + shell: pwsh + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: ./packaging/build_windows.ps1 + + - name: Stage artifacts (versioned + stable names) + run: | + mkdir -p out + BUNDLE=surfaces/gui/src-tauri/target/release/bundle + if [ "$RUNNER_OS" = "Windows" ]; then + cp "$BUNDLE"/nsis/*.exe out/ + cp "$BUNDLE"/nsis/*.exe out/OpenWorker-windows-setup.exe + cp "$BUNDLE"/msi/*.msi out/ + cp "$BUNDLE"/msi/*.msi out/OpenWorker-windows.msi + # Updater signature for the NSIS installer (present only when the updater key + # secret is configured). The .sig signs CONTENT, so the stable rename is safe. + SIG=$(ls "$BUNDLE"/nsis/*.exe.sig 2>/dev/null | head -1 || true) + [ -n "$SIG" ] && cp "$SIG" out/OpenWorker-windows-setup.exe.sig + else + cp "$BUNDLE"/dmg/*.dmg out/ + cp "$BUNDLE"/dmg/*.dmg out/OpenWorker-${{ matrix.slug }}.dmg + # macOS updater artifact: the signed .app tarball the installed app swaps in. + if [ -f "$BUNDLE"/macos/OpenWorker.app.tar.gz ]; then + cp "$BUNDLE"/macos/OpenWorker.app.tar.gz out/OpenWorker-${{ matrix.slug }}.app.tar.gz + cp "$BUNDLE"/macos/OpenWorker.app.tar.gz.sig out/OpenWorker-${{ matrix.slug }}.app.tar.gz.sig + fi + fi + ls -la out + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.slug }} + path: out/* + if-no-files-found: error + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Compose the auto-update manifest (latest.json) + # Shipped apps poll releases/latest/download/latest.json (via the branded + # download.openworker.com redirect) — publishing this release IS pushing the + # update. The tag must match tauri.conf.json's version or installed apps would + # see a permanent phantom update; fail loudly on drift. Runs only when signed + # updater artifacts exist (i.e. the TAURI_SIGNING_PRIVATE_KEY secret is set). + run: | + TAG="${GITHUB_REF_NAME}" + CONF_VERSION=$(python3 -c "import json; print(json.load(open('surfaces/gui/src-tauri/tauri.conf.json'))['version'])") + if [ "${TAG#v}" != "$CONF_VERSION" ]; then + echo "::error::tag $TAG != tauri.conf.json version $CONF_VERSION — bump the config before tagging" + exit 1 + fi + if ls dist/*.sig >/dev/null 2>&1; then + python3 packaging/make_update_manifest.py \ + --version "${TAG#v}" --tag "$TAG" --repo "$GITHUB_REPOSITORY" \ + --dist dist --out dist/latest.json \ + --notes "OpenWorker ${TAG#v}" + else + echo "::warning::no updater signatures in dist/ — release ships WITHOUT auto-update manifest" + fi + + - uses: softprops/action-gh-release@v2 + with: + draft: true + files: dist/* + generate_release_notes: true