diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b55de892..2d5ec108 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,6 +81,19 @@ jobs: build-desktop-macos: needs: create-release runs-on: macos-26 + strategy: + fail-fast: false + matrix: + target: [aarch64-apple-darwin, x86_64-apple-darwin] + include: + - target: aarch64-apple-darwin + arch: aarch64 + platform: darwin-aarch64 + - target: x86_64-apple-darwin + arch: x86_64 + platform: darwin-x86_64 + outputs: + version: ${{ needs.create-release.outputs.version }} steps: - uses: actions/checkout@v4 @@ -94,11 +107,14 @@ jobs: - name: Install Rust stable uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} - name: Rust cache uses: swatinem/rust-cache@v2 with: workspaces: packages/desktop/src-tauri + key: ${{ matrix.target }} - name: Install dependencies run: bun install --frozen-lockfile @@ -148,7 +164,9 @@ jobs: run: bun run --cwd packages/ui build - name: Build Desktop app - run: bun run desktop:build + # Note: We use inline commands instead of desktop:build to pass architecture-specific --target flag + # This enables cross-compilation for both arm64 and x86_64 from the same runner + run: bun run --cwd packages/desktop build && bun run --cwd packages/desktop tauri build --target ${{ matrix.target }} env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} @@ -161,18 +179,41 @@ jobs: run: | mkdir -p artifacts VERSION="${{ needs.create-release.outputs.version }}" - - # Copy DMG - cp packages/desktop/src-tauri/target/release/bundle/dmg/*.dmg artifacts/ 2>/dev/null || true - + + # Copy DMG (Tauri names it with the target triple in the path) + DMG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg" + if ls $DMG_PATH 1> /dev/null 2>&1; then + cp $DMG_PATH artifacts/ + else + echo "Error: DMG file not found at $DMG_PATH" + exit 1 + fi + # Copy tar.gz and signature for updater - cp packages/desktop/src-tauri/target/release/bundle/macos/*.tar.gz artifacts/ 2>/dev/null || true - cp packages/desktop/src-tauri/target/release/bundle/macos/*.tar.gz.sig artifacts/ 2>/dev/null || true + TAR_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz" + SIG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig" + + if ls $TAR_PATH 1> /dev/null 2>&1; then + cp $TAR_PATH artifacts/ + else + echo "Error: tar.gz file not found at $TAR_PATH" + exit 1 + fi + + if ls $SIG_PATH 1> /dev/null 2>&1; then + cp $SIG_PATH artifacts/ + else + echo "Error: signature file not found at $SIG_PATH" + exit 1 + fi + + echo "Successfully prepared artifacts:" + ls -lh artifacts/ - name: Generate update manifest run: | VERSION="${{ needs.create-release.outputs.version }}" - + # Find the signature file SIG_FILE=$(find artifacts -name "*.tar.gz.sig" | head -1) if [ -f "$SIG_FILE" ]; then @@ -180,27 +221,27 @@ jobs: else SIGNATURE="" fi - + # Find the tar.gz file name TAR_FILE=$(find artifacts -name "*.tar.gz" ! -name "*.sig" | head -1) TAR_NAME=$(basename "$TAR_FILE" 2>/dev/null || echo "OpenChamber.app.tar.gz") - - cat > artifacts/latest.json << EOF + + cat > artifacts/latest-${{ matrix.platform }}.json << EOF { "version": "${VERSION}", "notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/v${VERSION}", "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "platforms": { - "darwin-aarch64": { + "${{ matrix.platform }}": { "signature": "${SIGNATURE}", "url": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${TAR_NAME}" } } } EOF - - echo "Generated latest.json:" - cat artifacts/latest.json + + echo "Generated latest-${{ matrix.platform }}.json:" + cat artifacts/latest-${{ matrix.platform }}.json - name: Upload release assets uses: softprops/action-gh-release@v2 @@ -210,9 +251,16 @@ jobs: artifacts/*.dmg artifacts/*.tar.gz artifacts/*.tar.gz.sig - artifacts/latest.json + artifacts/latest-${{ matrix.platform }}.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload manifest as artifact + uses: actions/upload-artifact@v4 + with: + name: manifest-${{ matrix.platform }} + path: artifacts/latest-${{ matrix.platform }}.json + retention-days: 1 publish-npm: needs: create-release @@ -254,8 +302,91 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + combine-manifests: + needs: [create-release, build-desktop-macos] + runs-on: ubuntu-latest + steps: + - name: Download aarch64 manifest + uses: actions/download-artifact@v4 + with: + name: manifest-darwin-aarch64 + path: artifacts + + - name: Download x86_64 manifest + uses: actions/download-artifact@v4 + with: + name: manifest-darwin-x86_64 + path: artifacts + + - name: Combine manifests + run: | + VERSION="${{ needs.create-release.outputs.version }}" + PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + REPO="${{ github.repository }}" + + # Validate that both manifest files exist and are valid JSON + if [ ! -f artifacts/latest-darwin-aarch64.json ]; then + echo "Error: aarch64 manifest not found" + exit 1 + fi + + if [ ! -f artifacts/latest-darwin-x86_64.json ]; then + echo "Error: x86_64 manifest not found" + exit 1 + fi + + # Validate JSON structure + if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then + echo "Error: aarch64 manifest is not valid JSON" + exit 1 + fi + + if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then + echo "Error: x86_64 manifest is not valid JSON" + exit 1 + fi + + # Validate platform data exists in manifests + if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then + echo "Error: darwin-aarch64 platform data not found in manifest" + exit 1 + fi + + if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then + echo "Error: darwin-x86_64 platform data not found in manifest" + exit 1 + fi + + # Use jq to properly combine the manifests + jq -n \ + --arg version "$VERSION" \ + --arg notes "See release notes at https://github.com/${REPO}/releases/tag/v${VERSION}" \ + --arg pub_date "$PUB_DATE" \ + --slurpfile aarch64 artifacts/latest-darwin-aarch64.json \ + --slurpfile x86_64 artifacts/latest-darwin-x86_64.json \ + '{ + version: $version, + notes: $notes, + pub_date: $pub_date, + platforms: { + "darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"], + "darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"] + } + }' > artifacts/latest.json + + echo "Generated combined latest.json:" + cat artifacts/latest.json + + - name: Upload combined manifest + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: artifacts/latest.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + finalize-release: - needs: [create-release, build-desktop-macos, publish-npm] + needs: [create-release, build-desktop-macos, publish-npm, combine-manifests] runs-on: ubuntu-latest steps: - name: Publish release diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a149ff..857a40b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Added Intel Mac (x86_64) support for the desktop application. +- Build workflow now generates separate builds for Apple Silicon (arm64) and Intel (x86_64) Macs. + ## [1.3.7] - 2025-12-28 - Redesigned Settings as a full-screen view with tabbed navigation. diff --git a/package.json b/package.json index 55db8d43..a99ac693 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,10 @@ "vscode:package": "bun run --cwd packages/vscode package", "vscode:type-check": "bun run --cwd packages/vscode type-check", "version:bump": "node scripts/bump-version.mjs", - "release:prepare": "bun run build && bun run type-check && bun run lint" + "release:prepare": "bun run build && bun run type-check && bun run lint", + "release:test": "./scripts/test-release-build.sh", + "release:test:intel": "./scripts/test-release-build.sh x86_64", + "release:test:arm": "./scripts/test-release-build.sh aarch64" }, "dependencies": { "@fontsource/ibm-plex-mono": "^5.2.7", diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index b3447221..f817d308 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2847,7 +2847,7 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.3.6" +version = "1.3.7" dependencies = [ "anyhow", "axum", diff --git a/scripts/test-release-build.sh b/scripts/test-release-build.sh new file mode 100755 index 00000000..bb3f5f93 --- /dev/null +++ b/scripts/test-release-build.sh @@ -0,0 +1,328 @@ +#!/usr/bin/env bash +# +# Test Release Build Script +# +# This script tests the release workflow locally before creating a PR. +# It supports two modes: +# 1. `act` mode - Runs the GitHub workflow via act (limited macOS support) +# 2. `native` mode (default for macOS builds) - Runs build steps directly +# +# Prerequisites: +# brew install act (optional, for workflow testing) +# +# Usage: +# ./scripts/test-release-build.sh [target] [options] +# +# Targets: +# aarch64 - Build for Apple Silicon (arm64) +# x86_64 - Build for Intel Mac (x86_64) +# all - Build for both architectures (default) +# +# Options: +# --act Force using act (Linux containers - limited macOS support) +# --native Force native build (default, recommended for macOS) +# --dry-run Show what would be run without executing +# --verbose, -v Enable verbose output +# --no-bundle Skip bundle creation (faster, just verify compilation) +# +# Examples: +# ./scripts/test-release-build.sh x86_64 # Test Intel Mac build natively +# ./scripts/test-release-build.sh --act # Run workflow via act +# ./scripts/test-release-build.sh --no-bundle # Quick compilation check + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Change to repo root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +# Default values +TARGET="all" +MODE="native" # native or act +DRY_RUN=false +VERBOSE=false +NO_BUNDLE=false + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_step() { + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +usage() { + echo "Usage: $0 [target] [options]" + echo "" + echo "Targets:" + echo " aarch64, arm64, arm Build for Apple Silicon" + echo " x86_64, intel, x86 Build for Intel Mac" + echo " all, both Build for both architectures (default)" + echo "" + echo "Options:" + echo " --act Run via act (GitHub Actions in Docker - limited macOS)" + echo " --native Run build commands directly (default, recommended)" + echo " --no-bundle Skip bundle creation (faster compilation check)" + echo " --dry-run Show what would be run without executing" + echo " --verbose, -v Enable verbose output" + echo " --help, -h Show this help message" + exit 0 +} + +# Run workflow via act +run_with_act() { + log_step "Running via act (GitHub Actions)" + + if ! command -v act &> /dev/null; then + log_error "act is not installed. Install with: brew install act" + log_info "Or use --native flag to run build commands directly (recommended)" + exit 1 + fi + log_success "act found: $(command -v act)" + + log_warn "⚠️ Note: act uses Docker (Linux containers)" + log_warn " macOS-specific steps (Tauri, Xcode, signing) may not work." + log_warn " For full macOS testing, use --native mode or GitHub's runners." + echo "" + + # Build act command + local ACT_CMD="act workflow_dispatch" + ACT_CMD+=" -W .github/workflows/release.yml" + ACT_CMD+=" --input version=0.0.0-test" + ACT_CMD+=" --input dry_run=true" + ACT_CMD+=" --container-architecture linux/amd64" + ACT_CMD+=" --artifact-server-path $REPO_ROOT/.act-artifacts" + + # Filter to specific job for faster testing + # publish-npm works in Linux containers; build-desktop-macos needs macOS + ACT_CMD+=" --job publish-npm" + + if [[ "$VERBOSE" == true ]]; then + ACT_CMD+=" --verbose" + fi + + log_info "Command: $ACT_CMD" + + if [[ "$DRY_RUN" == true ]]; then + log_warn "Dry run - not executing" + return + fi + + mkdir -p "$REPO_ROOT/.act-artifacts" + eval "$ACT_CMD" +} + +# Run build directly (native mode) +run_native_build() { + log_step "Checking prerequisites" + + check_command() { + if ! command -v "$1" &> /dev/null; then + log_error "$1 is not installed" + return 1 + fi + log_success "$1 found: $(command -v "$1")" + } + + check_command bun + check_command rustc + check_command cargo + + # Check Rust targets + log_info "Checking Rust targets..." + for target in "${TARGETS[@]}"; do + if ! rustup target list --installed | grep -q "$target"; then + log_warn "Rust target $target not installed. Installing..." + if [[ "$DRY_RUN" == false ]]; then + rustup target add "$target" + fi + log_success "Installed $target" + else + log_success "Rust target $target is installed" + fi + done + + if [[ "$DRY_RUN" == true ]]; then + log_step "Commands that would be executed (dry run)" + echo " 1. bun install --frozen-lockfile" + echo " 2. bun run --cwd packages/ui build" + for target in "${TARGETS[@]}"; do + echo " 3. bun run --cwd packages/desktop build" + if [[ "$NO_BUNDLE" == true ]]; then + echo " 4. bun run --cwd packages/desktop tauri build --target $target --no-bundle" + else + echo " 4. bun run --cwd packages/desktop tauri build --target $target" + fi + done + return + fi + + # Step 1: Install dependencies + log_step "Installing dependencies" + bun install --frozen-lockfile + + # Step 2: Build UI package + log_step "Building UI package" + bun run --cwd packages/ui build + + # Step 3: Build Desktop for each target + for target in "${TARGETS[@]}"; do + log_step "Building Desktop for $target" + + # Build the frontend + log_info "Building desktop frontend..." + bun run --cwd packages/desktop build + + # Build Tauri for the target + log_info "Building Tauri for $target (this may take a while)..." + + local TAURI_ARGS="--target $target" + if [[ "$NO_BUNDLE" == true ]]; then + TAURI_ARGS+=" --no-bundle" + fi + + if [[ "$VERBOSE" == true ]]; then + TAURI_ARGS+=" --verbose" + fi + + bun run --cwd packages/desktop tauri build $TAURI_ARGS + + log_success "Successfully built for $target" + done + + # Step 4: Show results + log_step "Build Summary" + + for target in "${TARGETS[@]}"; do + if [[ "$NO_BUNDLE" == true ]]; then + local BINARY_PATH="packages/desktop/src-tauri/target/$target/release/openchamber-desktop" + else + local BINARY_PATH="packages/desktop/src-tauri/target/$target/release/bundle/dmg" + fi + + if [[ -e "$BINARY_PATH" ]]; then + if [[ -d "$BINARY_PATH" ]]; then + log_success "$target: Bundle created at $BINARY_PATH" + ls -la "$BINARY_PATH"/*.dmg 2>/dev/null || true + else + local SIZE + SIZE=$(du -h "$BINARY_PATH" | cut -f1) + log_success "$target: Binary built successfully ($SIZE)" + file "$BINARY_PATH" 2>/dev/null || true + fi + else + log_warn "$target: Output not found at expected path" + log_info "Checking target directory..." + ls -la "packages/desktop/src-tauri/target/$target/release/" 2>/dev/null | head -10 || true + fi + done +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + aarch64|arm64|arm) + TARGET="aarch64" + shift + ;; + x86_64|intel|x86) + TARGET="x86_64" + shift + ;; + all|both) + TARGET="all" + shift + ;; + --act) + MODE="act" + shift + ;; + --native) + MODE="native" + shift + ;; + --no-bundle) + NO_BUNDLE=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --verbose|-v) + VERBOSE=true + shift + ;; + --help|-h) + usage + ;; + *) + log_error "Unknown argument: $1" + usage + ;; + esac +done + +# Determine which targets to build +declare -a TARGETS +case "$TARGET" in + aarch64) + TARGETS=("aarch64-apple-darwin") + ;; + x86_64) + TARGETS=("x86_64-apple-darwin") + ;; + all) + TARGETS=("aarch64-apple-darwin" "x86_64-apple-darwin") + ;; +esac + +log_step "Release Build Test Configuration" + +log_info "Repository root: $REPO_ROOT" +log_info "Mode: $MODE" +log_info "Target(s): ${TARGETS[*]}" +log_info "No bundle: $NO_BUNDLE" +log_info "Dry run: $DRY_RUN" + +# Run the selected mode +if [[ "$MODE" == "act" ]]; then + run_with_act +else + run_native_build +fi + +echo "" +log_success "Release build test completed!" +echo "" +log_info "This script mirrors the build steps in .github/workflows/release.yml" +log_info "Note: Signing and notarization are skipped (require Apple secrets)" +echo "" +log_info "Next steps:" +echo " 1. If builds succeeded, your changes should work in CI" +echo " 2. For full workflow testing, push and use GitHub workflow_dispatch" +echo " 3. Create your PR with confidence"