name: Release on: push: branches: - main concurrency: group: release-${{ github.ref }} cancel-in-progress: true env: APP_NAME: Laputa BUNDLE_ID: com.tauri.dev jobs: # ───────────────────────────────────────────────────────────── # Phase 1: Compute the version string once # ───────────────────────────────────────────────────────────── version: name: Compute version runs-on: ubuntu-latest outputs: version: ${{ steps.ver.outputs.version }} tag: ${{ steps.ver.outputs.tag }} steps: - id: ver run: | VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" echo "### Version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" # ───────────────────────────────────────────────────────────── # Phase 2: Build each architecture in parallel # ───────────────────────────────────────────────────────────── build: name: Build (${{ matrix.arch }}) needs: version runs-on: macos-latest strategy: fail-fast: true matrix: include: - arch: aarch64 target: aarch64-apple-darwin - arch: x86_64 target: x86_64-apple-darwin steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'pnpm' - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Rust cache uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - name: Install frontend dependencies run: pnpm install --frozen-lockfile # Stamp the computed version into tauri.conf.json and Cargo.toml - name: Set version run: | VERSION="${{ needs.version.outputs.version }}" # tauri.conf.json jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json # Cargo.toml sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml - name: Build frontend run: pnpm build - name: Build Tauri app env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | pnpm tauri build --target ${{ matrix.target }} # Upload the .app bundle so the merge job can lipo them together. # Also upload the per-arch .tar.gz updater artifact and its .sig. - name: Upload arch artifacts uses: actions/upload-artifact@v4 with: name: app-${{ matrix.arch }} path: | src-tauri/target/${{ matrix.target }}/release/bundle/macos/${{ env.APP_NAME }}.app retention-days: 1 - name: Upload updater artifacts uses: actions/upload-artifact@v4 with: name: updater-${{ matrix.arch }} path: | src-tauri/target/${{ matrix.target }}/release/bundle/macos/${{ env.APP_NAME }}.app.tar.gz src-tauri/target/${{ matrix.target }}/release/bundle/macos/${{ env.APP_NAME }}.app.tar.gz.sig retention-days: 1 - name: Upload dmg artifact uses: actions/upload-artifact@v4 with: name: dmg-${{ matrix.arch }} path: | src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg retention-days: 1 # ───────────────────────────────────────────────────────────── # Phase 3: Merge into universal binary, re-package, and release # ───────────────────────────────────────────────────────────── release: name: Universal binary & GitHub Release needs: [version, build] runs-on: macos-latest permissions: contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # for release notes generation - name: Download all artifacts uses: actions/download-artifact@v4 - name: Create universal binary run: | set -euo pipefail mkdir -p universal ARM_APP="app-aarch64/${{ env.APP_NAME }}.app" X86_APP="app-x86_64/${{ env.APP_NAME }}.app" # Copy the aarch64 .app as base (preserves bundle structure) cp -R "$ARM_APP" "universal/${{ env.APP_NAME }}.app" # Find the main executable inside the .app bundle EXEC_NAME="${{ env.APP_NAME }}" # Tauri uses lowercase binary name EXEC_NAME_LOWER=$(echo "$EXEC_NAME" | tr '[:upper:]' '[:lower:]') ARM_BIN="$ARM_APP/Contents/MacOS/$EXEC_NAME_LOWER" X86_BIN="$X86_APP/Contents/MacOS/$EXEC_NAME_LOWER" UNI_BIN="universal/${{ env.APP_NAME }}.app/Contents/MacOS/$EXEC_NAME_LOWER" # If the binary is the product name (case-sensitive), try that too if [ ! -f "$ARM_BIN" ]; then ARM_BIN="$ARM_APP/Contents/MacOS/$EXEC_NAME" X86_BIN="$X86_APP/Contents/MacOS/$EXEC_NAME" UNI_BIN="universal/${{ env.APP_NAME }}.app/Contents/MacOS/$EXEC_NAME" fi echo "Merging: $ARM_BIN + $X86_BIN → $UNI_BIN" lipo -create "$ARM_BIN" "$X86_BIN" -output "$UNI_BIN" lipo -info "$UNI_BIN" - name: Create universal .dmg run: | set -euo pipefail VERSION="${{ needs.version.outputs.version }}" DMG_NAME="${{ env.APP_NAME }}_${VERSION}_universal.dmg" hdiutil create -volname "${{ env.APP_NAME }}" \ -srcfolder "universal/${{ env.APP_NAME }}.app" \ -ov -format UDZO \ "universal/$DMG_NAME" echo "DMG_PATH=universal/$DMG_NAME" >> "$GITHUB_ENV" echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV" - name: Create universal updater tarball and sign env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | set -euo pipefail # Create tar.gz of the universal .app TARBALL="universal/${{ env.APP_NAME }}.app.tar.gz" tar -czf "$TARBALL" -C universal "${{ env.APP_NAME }}.app" # Install tauri-cli for signing npm install -g @tauri-apps/cli # Sign the tarball using Tauri's signer tauri signer sign "$TARBALL" --private-key "$TAURI_SIGNING_PRIVATE_KEY" --password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" echo "TARBALL_PATH=$TARBALL" >> "$GITHUB_ENV" echo "SIG_PATH=${TARBALL}.sig" >> "$GITHUB_ENV" - name: Generate release notes id: notes run: | set -euo pipefail # Find the previous release tag PREV_TAG=$(git tag --sort=-version:refname | head -n 1 || echo "") if [ -z "$PREV_TAG" ]; then # No previous tag — use all commits on main NOTES=$(git log --oneline --no-merges -20) else NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD") fi # Write to file for the release body { echo "## What's Changed" echo "" echo "$NOTES" | while IFS= read -r line; do echo "- $line" done echo "" echo "---" echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*" } > release_notes.md cat release_notes.md - name: Build latest.json run: | set -euo pipefail VERSION="${{ needs.version.outputs.version }}" TAG="${{ needs.version.outputs.tag }}" REPO="refactoringhq/laputa-app" # Read signatures UNI_SIG=$(cat "${SIG_PATH}") ARM_SIG=$(cat updater-aarch64/${{ env.APP_NAME }}.app.tar.gz.sig) X86_SIG=$(cat updater-x86_64/${{ env.APP_NAME }}.app.tar.gz.sig) # The universal tarball URL UNI_URL="https://github.com/${REPO}/releases/download/${TAG}/${{ env.APP_NAME }}.app.tar.gz" # Per-arch URLs (fallback) ARM_URL="https://github.com/${REPO}/releases/download/${TAG}/${{ env.APP_NAME }}_aarch64.app.tar.gz" X86_URL="https://github.com/${REPO}/releases/download/${TAG}/${{ env.APP_NAME }}_x86_64.app.tar.gz" cat > latest.json << MANIFEST { "version": "${VERSION}", "notes": "See https://refactoringhq.github.io/laputa-app/ for full release notes.", "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "platforms": { "darwin-aarch64": { "signature": "${ARM_SIG}", "url": "${ARM_URL}" }, "darwin-x86_64": { "signature": "${X86_SIG}", "url": "${X86_URL}" }, "darwin-universal": { "signature": "${UNI_SIG}", "url": "${UNI_URL}" } } } MANIFEST echo "latest.json:" cat latest.json - name: Rename per-arch updater artifacts run: | cp "updater-aarch64/${{ env.APP_NAME }}.app.tar.gz" "universal/${{ env.APP_NAME }}_aarch64.app.tar.gz" cp "updater-aarch64/${{ env.APP_NAME }}.app.tar.gz.sig" "universal/${{ env.APP_NAME }}_aarch64.app.tar.gz.sig" cp "updater-x86_64/${{ env.APP_NAME }}.app.tar.gz" "universal/${{ env.APP_NAME }}_x86_64.app.tar.gz" cp "updater-x86_64/${{ env.APP_NAME }}.app.tar.gz.sig" "universal/${{ env.APP_NAME }}_x86_64.app.tar.gz.sig" - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.version.outputs.tag }} name: Laputa ${{ needs.version.outputs.version }} body_path: release_notes.md draft: false prerelease: false files: | ${{ env.DMG_PATH }} ${{ env.TARBALL_PATH }} ${{ env.SIG_PATH }} universal/${{ env.APP_NAME }}_aarch64.app.tar.gz universal/${{ env.APP_NAME }}_aarch64.app.tar.gz.sig universal/${{ env.APP_NAME }}_x86_64.app.tar.gz universal/${{ env.APP_NAME }}_x86_64.app.tar.gz.sig latest.json dmg-aarch64/*.dmg dmg-x86_64/*.dmg # ───────────────────────────────────────────────────────────── # Phase 4: Update GitHub Pages with release history # ───────────────────────────────────────────────────────────── pages: name: Update release history page needs: [version, release] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: Build release history page env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail mkdir -p _site # Fetch all releases as JSON gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json # Build the HTML page cat > _site/index.html << 'HTMLEOF' Laputa — Release History

Laputa Release History

Auto-updated on every release

HTMLEOF - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./_site commit_message: "Update release history for ${{ needs.version.outputs.tag }}"