diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..07e042d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,227 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + # sccache config + SCCACHE_GHA_ENABLED: true + RUSTC_WRAPPER: sccache + +jobs: + # ────────────────────────────────────────────── + # Rust lint: fmt + clippy (fast, catches issues early) + # ────────────────────────────────────────────── + rust-lint: + name: Rust Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev libssl-dev + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Setup sccache + uses: SeedyROM/sccache-action@v0.0.10 + + - name: Cargo registry cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-registry- + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + # ────────────────────────────────────────────── + # Rust tests: full test suite + # ────────────────────────────────────────────── + rust-test: + name: Rust Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev libssl-dev + + - uses: dtolnay/rust-toolchain@stable + + - name: Setup sccache + uses: SeedyROM/sccache-action@v0.0.10 + + - name: Cargo registry cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-registry- + + - name: Run tests + run: cargo test --all-features + + # ────────────────────────────────────────────── + # Frontend: vitest unit tests + typecheck + # ────────────────────────────────────────────── + frontend-test: + name: Frontend Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm store + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('app/pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + + - name: Install dependencies + run: pnpm install + working-directory: app + + - name: TypeScript check + run: pnpm exec tsc --noEmit + working-directory: app + + - name: Vitest + run: pnpm test + working-directory: app + + # ────────────────────────────────────────────── + # E2E: Playwright (needs Vite dev server + variance binary) + # ────────────────────────────────────────────── + e2e-test: + name: E2E Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev libssl-dev + + - uses: dtolnay/rust-toolchain@stable + + - name: Setup sccache + uses: SeedyROM/sccache-action@v0.0.10 + + - name: Cargo registry cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-registry- + + - name: Build variance CLI binary + run: cargo build --bin variance + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm store + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('app/pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + + - name: Install dependencies + run: pnpm install + working-directory: app + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + working-directory: app + + - name: Run E2E tests + run: pnpm exec playwright test + working-directory: app + env: + CI: true + + - name: Upload Playwright report + uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: | + app/test-results/ + app/playwright-report/ + if-no-files-found: ignore + retention-days: 14 + + # ────────────────────────────────────────────── + # Gate: all checks must pass + # ────────────────────────────────────────────── + ci-pass: + name: CI Pass + if: always() + needs: [rust-lint, rust-test, frontend-test, e2e-test] + runs-on: ubuntu-latest + steps: + - name: Check results + run: | + if [[ "${{ needs.rust-lint.result }}" != "success" || + "${{ needs.rust-test.result }}" != "success" || + "${{ needs.frontend-test.result }}" != "success" || + "${{ needs.e2e-test.result }}" != "success" ]]; then + echo "One or more CI jobs failed" + exit 1 + fi + echo "All CI checks passed" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..88cbc58 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,318 @@ +name: Release + +# Trigger on version tags or manual dispatch +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + inputs: + tag: + description: "Version tag (e.g. v0.1.0)" + required: true + type: string + dry_run: + description: "Dry run — build artifacts but skip publishing the GitHub Release" + required: false + type: boolean + default: true + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: true + RUSTC_WRAPPER: sccache + +permissions: + contents: write # needed for creating releases and uploading assets + +jobs: + # ────────────────────────────────────────────── + # Build matrix: macOS (universal), Windows, Linux + # All three run in parallel + # ────────────────────────────────────────────── + + build-macos: + name: Build macOS (Universal) + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin + + - name: Setup sccache + uses: SeedyROM/sccache-action@v0.0.10 + + - name: Cargo registry cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-registry-macos-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-registry-macos- + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm store + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('app/pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + + - name: Install frontend dependencies + run: pnpm install + working-directory: app + + # Tauri's built-in universal macOS build (fat binary: arm64 + x86_64) + - name: Build Tauri app (universal) + run: pnpm tauri build --target universal-apple-darwin + working-directory: app + env: + # TODO: Configure code signing for production releases + # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + # APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + # APPLE_ID: ${{ secrets.APPLE_ID }} + # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + CI: true + + - name: Upload macOS DMG + uses: actions/upload-artifact@v6 + with: + name: macos-dmg + path: app/src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg + if-no-files-found: error + + - name: Upload macOS .app (zipped) + uses: actions/upload-artifact@v6 + with: + name: macos-app + path: app/src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app + if-no-files-found: error + + build-windows: + name: Build Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + + - name: Setup sccache + uses: SeedyROM/sccache-action@v0.0.10 + + - name: Cargo registry cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-registry-windows-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-registry-windows- + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm store + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('app/pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + + - name: Install frontend dependencies + run: pnpm install + working-directory: app + + - name: Build Tauri app + run: pnpm tauri build + working-directory: app + env: + # TODO: Configure code signing for production releases + # TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + CI: true + + # NSIS installer (.exe) is the modern default; MSI also generated by "all" target + - name: Upload Windows NSIS installer + uses: actions/upload-artifact@v6 + with: + name: windows-nsis + path: app/src-tauri/target/release/bundle/nsis/*.exe + if-no-files-found: error + + - name: Upload Windows MSI + uses: actions/upload-artifact@v6 + with: + name: windows-msi + path: app/src-tauri/target/release/bundle/msi/*.msi + if-no-files-found: warn + + build-linux: + name: Build Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + protobuf-compiler \ + libwebkit2gtk-4.1-dev \ + libayatana-appindicator3-dev \ + libgtk-3-dev \ + libssl-dev \ + librsvg2-dev \ + patchelf + + - uses: dtolnay/rust-toolchain@stable + + - name: Setup sccache + uses: SeedyROM/sccache-action@v0.0.10 + + - name: Cargo registry cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: cargo-registry-linux-${{ hashFiles('**/Cargo.lock') }} + restore-keys: cargo-registry-linux- + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm store + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('app/pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + + - name: Install frontend dependencies + run: pnpm install + working-directory: app + + - name: Build Tauri app + run: pnpm tauri build + working-directory: app + env: + CI: true + + - name: Upload Linux .deb + uses: actions/upload-artifact@v6 + with: + name: linux-deb + path: app/src-tauri/target/release/bundle/deb/*.deb + if-no-files-found: error + + - name: Upload Linux AppImage + uses: actions/upload-artifact@v6 + with: + name: linux-appimage + path: app/src-tauri/target/release/bundle/appimage/*.AppImage + if-no-files-found: error + + # ────────────────────────────────────────────── + # Publish: create GitHub Release with all artifacts + # ────────────────────────────────────────────── + publish: + name: Publish Release + if: ${{ github.event_name == 'push' || inputs.dry_run != true }} + needs: [build-macos, build-windows, build-linux] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Determine version + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + fi + + - name: Download all artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts/ + + - name: List artifacts + run: find artifacts/ -type f | sort + + # Flatten all artifacts into a single directory for upload + - name: Prepare release assets + run: | + mkdir -p release-assets + # macOS DMG + cp artifacts/macos-dmg/*.dmg release-assets/ 2>/dev/null || true + # Windows + cp artifacts/windows-nsis/*.exe release-assets/ 2>/dev/null || true + cp artifacts/windows-msi/*.msi release-assets/ 2>/dev/null || true + # Linux + cp artifacts/linux-deb/*.deb release-assets/ 2>/dev/null || true + cp artifacts/linux-appimage/*.AppImage release-assets/ 2>/dev/null || true + # macOS .app -> zip it for release + if [ -d "artifacts/macos-app" ]; then + cd artifacts/macos-app + zip -r "../../release-assets/Variance-macos-universal.app.zip" *.app + cd ../.. + fi + echo "Release assets:" + ls -lh release-assets/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: Variance ${{ steps.version.outputs.tag }} + draft: true + prerelease: ${{ contains(steps.version.outputs.tag, '-') }} + generate_release_notes: true + files: release-assets/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 5605188..9f5a741 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod state; use commands::*; use state::NodeState; use tauri::Manager; +#[cfg(target_os = "macos")] use tauri_plugin_decorum::WebviewWindowExt; use tracing::info; @@ -24,10 +25,10 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_notification::init()) - .setup(|app| { + .setup(|_app| { #[cfg(target_os = "macos")] { - let win = app.get_webview_window("main").unwrap(); + let win = _app.get_webview_window("main").unwrap(); win.set_traffic_lights_inset(12.0, 16.0).unwrap(); } Ok(()) diff --git a/crates/variance-app/src/config.rs b/crates/variance-app/src/config.rs index bf757f2..b718a03 100644 --- a/crates/variance-app/src/config.rs +++ b/crates/variance-app/src/config.rs @@ -3,6 +3,9 @@ use std::fs; use std::path::{Path, PathBuf}; fn variance_data_dir() -> PathBuf { + if let Ok(dir) = std::env::var("VARIANCE_DATA_DIR") { + return PathBuf::from(dir); + } dirs::data_local_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("variance")