Release #95
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| permissions: | |
| contents: write | |
| on: | |
| push: | |
| tags: | |
| - '**' | |
| branches: | |
| - ci/** | |
| workflow_dispatch: | |
| jobs: | |
| build: | |
| timeout-minutes: 30 | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| target: universal-apple-darwin | |
| args: --target universal-apple-darwin --bundles app,updater | |
| - os: ubuntu-22.04 | |
| target: x86_64-unknown-linux-gnu | |
| args: --bundles appimage | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| args: --bundles nsis | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.os == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || matrix.target }} | |
| - name: Cache Rust build | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: src-tauri | |
| key: ${{ matrix.target }} | |
| - name: Install Linux dependencies | |
| if: matrix.os == 'ubuntu-22.04' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| libappindicator3-dev \ | |
| librsvg2-dev \ | |
| patchelf \ | |
| libsecret-1-dev \ | |
| libssl-dev \ | |
| pkg-config | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| # Import the Apple Developer ID certificate into the macOS keychain for signing. | |
| - name: Import Apple Developer certificate | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain | |
| # Increase keychain timeout to avoid relocking | |
| security set-keychain-settings -t 3600 -u build.keychain | |
| security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| # Grant codesign partition access to avoid UI prompts | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain | |
| rm certificate.p12 | |
| # On tag pushes: build, sign, notarize, and publish a draft release. | |
| # latest.json is generated by the separate `publish` job once all platforms are done. | |
| # includeUpdaterJson is disabled here because tauri-action has a bug where it | |
| # fails to find .sig files for universal macOS builds. The `publish` job handles it. | |
| - name: Build and publish Tauri app (tag) | |
| if: startsWith(github.ref, 'refs/tags/') | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| RUST_LOG: debug | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| TAURI_SIGNING_PUBLIC_KEY: ${{ secrets.TAURI_SIGNING_PUBLIC_KEY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| # Fallbacks for tauri-bundler / xcrun notarytool natively | |
| AC_USERNAME: ${{ secrets.APPLE_ID }} | |
| AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| # Brandfetch CDN client ID — inlined into the JS bundle at build time by Vite | |
| VITE_BRANDFETCH_CLIENT_ID: ${{ secrets.VITE_BRANDFETCH_CLIENT_ID }} | |
| # Baked into the Rust binary at compile time via option_env!() | |
| AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} | |
| with: | |
| tagName: ${{ github.ref_name }} | |
| releaseName: Automatic ${{ github.ref_name }} | |
| releaseDraft: true | |
| prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }} | |
| # Disable tauri-action's built-in latest.json — it fails to find .sig files for | |
| # universal macOS builds. The `publish` job handles it. | |
| includeUpdaterJson: false | |
| args: ${{ matrix.args }} | |
| # Upload the macOS updater bundle explicitly. Depending on tauri-action to | |
| # publish the universal updater archive has been unreliable. | |
| - name: List macOS bundle output (diagnostic) | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'macos-latest' | |
| run: ls -la src-tauri/target/universal-apple-darwin/release/bundle/macos/ || true | |
| - name: Upload macOS updater bundle and signature | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'macos-latest' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| BUNDLE_DIR=src-tauri/target/universal-apple-darwin/release/bundle/macos | |
| APP_TAR_GZ=$(ls "$BUNDLE_DIR"/*.app.tar.gz 2>/dev/null | grep -v '\.sig' | head -1) | |
| SIG_FILE="${APP_TAR_GZ}.sig" | |
| if [ -z "$APP_TAR_GZ" ] || [ ! -f "$SIG_FILE" ]; then | |
| echo "ERROR: Could not find macOS updater archive or its .sig in $BUNDLE_DIR" >&2 | |
| ls "$BUNDLE_DIR"/ || true | |
| exit 1 | |
| fi | |
| gh release upload "$TAG" "$APP_TAR_GZ" "$SIG_FILE" --clobber | |
| - name: Stash macOS signature as workflow artifact | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'macos-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sig-macos | |
| path: src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz.sig | |
| if-no-files-found: error | |
| - name: List Linux bundle output (diagnostic) | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-22.04' | |
| run: ls -la src-tauri/target/release/bundle/appimage/ || true | |
| - name: Upload Linux updater bundle and signature | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-22.04' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| BUNDLE_DIR=src-tauri/target/release/bundle/appimage | |
| APPIMAGE=$(ls "$BUNDLE_DIR"/*.AppImage 2>/dev/null | grep -v '\.sig' | head -1) | |
| SIG_FILE="${APPIMAGE}.sig" | |
| if [ -z "$APPIMAGE" ] || [ ! -f "$SIG_FILE" ]; then | |
| echo "ERROR: Could not find AppImage or its .sig in $BUNDLE_DIR" >&2 | |
| ls "$BUNDLE_DIR"/ || true | |
| exit 1 | |
| fi | |
| gh release upload "$TAG" "$APPIMAGE" "$SIG_FILE" --clobber | |
| - name: Stash Linux signature as workflow artifact | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-22.04' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sig-linux | |
| path: src-tauri/target/release/bundle/appimage/*.AppImage.sig | |
| if-no-files-found: error | |
| - name: List Windows bundle output (diagnostic) | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows-latest' | |
| shell: bash | |
| run: ls -la src-tauri/target/release/bundle/nsis/ || true | |
| - name: Upload Windows updater bundle and signature | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows-latest' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| shell: bash | |
| run: | | |
| BUNDLE_DIR=src-tauri/target/release/bundle/nsis | |
| EXE=$(ls "$BUNDLE_DIR"/*-setup.exe 2>/dev/null | grep -v '\.sig' | head -1) | |
| SIG_FILE="${EXE}.sig" | |
| if [ -z "$EXE" ] || [ ! -f "$SIG_FILE" ]; then | |
| echo "ERROR: Could not find NSIS setup .exe or its .sig in $BUNDLE_DIR" >&2 | |
| ls "$BUNDLE_DIR"/ || true | |
| exit 1 | |
| fi | |
| gh release upload "$TAG" "$EXE" "$SIG_FILE" --clobber | |
| - name: Stash Windows signature as workflow artifact | |
| if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sig-windows | |
| path: src-tauri/target/release/bundle/nsis/*-setup.exe.sig | |
| if-no-files-found: error | |
| - name: Build Tauri app (branch / dispatch) | |
| if: "!startsWith(github.ref, 'refs/tags/')" | |
| uses: tauri-apps/tauri-action@v0 | |
| env: | |
| RUST_LOG: debug | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| TAURI_SIGNING_PUBLIC_KEY: ${{ secrets.TAURI_SIGNING_PUBLIC_KEY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| # Fallbacks for tauri-bundler / xcrun notarytool natively | |
| AC_USERNAME: ${{ secrets.APPLE_ID }} | |
| AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| # Brandfetch CDN client ID — inlined into the JS bundle at build time by Vite | |
| VITE_BRANDFETCH_CLIENT_ID: ${{ secrets.VITE_BRANDFETCH_CLIENT_ID }} | |
| # Baked into the Rust binary at compile time via option_env!() | |
| AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} | |
| with: | |
| args: ${{ matrix.args }} | |
| # Runs after all platform builds succeed. Downloads the .sig workflow artifacts uploaded | |
| # by each build job, generates a valid latest.json, and uploads it to the draft release. | |
| publish: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| if: startsWith(github.ref, 'refs/tags/') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download macOS signature | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sig-macos | |
| path: sigs/macos | |
| - name: Download Linux signature | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sig-linux | |
| path: sigs/linux | |
| - name: Download Windows signature | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sig-windows | |
| path: sigs/windows | |
| - name: Read version from tauri.conf.json | |
| id: version | |
| run: | | |
| VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Generate and upload latest.json | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| VERSION: ${{ steps.version.outputs.version }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| BASE_URL="https://github.com/${REPO}/releases/download/${TAG}" | |
| # The macOS release asset is uploaded with a stable public name. | |
| # The downloaded workflow artifact keeps the local build filename, | |
| # so do not derive the Darwin asset name from the .sig basename. | |
| DARWIN_FILE="Automatic_universal.app.tar.gz" | |
| LINUX_FILE=$(ls sigs/linux/*.AppImage.sig | xargs -I{} basename {} .sig) | |
| WINDOWS_FILE=$(ls sigs/windows/*-setup.exe.sig | xargs -I{} basename {} .sig) | |
| echo "Resolved artifact filenames:" | |
| echo " Darwin: $DARWIN_FILE" | |
| echo " Linux: $LINUX_FILE" | |
| echo " Windows: $WINDOWS_FILE" | |
| export DARWIN_URL="${BASE_URL}/${DARWIN_FILE}" | |
| export LINUX_URL="${BASE_URL}/${LINUX_FILE}" | |
| export WINDOWS_URL="${BASE_URL}/${WINDOWS_FILE}" | |
| export PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | |
| python3 - <<PYEOF | |
| import glob | |
| import json | |
| import os | |
| import re | |
| from pathlib import Path | |
| def read_release_notes(version: str, tag: str) -> str: | |
| changelog_lines = Path('CHANGELOG.md').read_text(encoding='utf-8').splitlines() | |
| header_pattern = re.compile(r'^## \[(?P<version>[^\]]+)\]') | |
| target_versions = {version, tag, version.removeprefix('v'), tag.removeprefix('v')} | |
| current_version = None | |
| collected = [] | |
| for line in changelog_lines: | |
| header_match = header_pattern.match(line) | |
| if header_match: | |
| if current_version in target_versions: | |
| break | |
| current_version = header_match.group('version') | |
| continue | |
| if current_version not in target_versions: | |
| continue | |
| if line.strip() == '---': | |
| break | |
| collected.append(line) | |
| notes = '\n'.join(collected).strip() | |
| if not notes: | |
| targets = ', '.join(sorted(target_versions)) | |
| print(f'WARNING: Could not find changelog notes for release version/tag: {targets}') | |
| return '' | |
| return notes | |
| darwin_sig_f = glob.glob('sigs/macos/*.app.tar.gz.sig')[0] | |
| darwin_sig = open(darwin_sig_f).read().strip() | |
| linux_sig_f = glob.glob('sigs/linux/*.AppImage.sig')[0] | |
| linux_sig = open(linux_sig_f).read().strip() | |
| win_sig_f = glob.glob('sigs/windows/*-setup.exe.sig')[0] | |
| win_sig = open(win_sig_f).read().strip() | |
| release_notes = read_release_notes(os.environ['VERSION'], os.environ['TAG']) | |
| payload = { | |
| "version": os.environ['VERSION'], | |
| "notes": release_notes, | |
| "pub_date": os.environ['PUB_DATE'], | |
| "platforms": { | |
| "darwin-aarch64": {"signature": darwin_sig, "url": os.environ['DARWIN_URL']}, | |
| "darwin-x86_64": {"signature": darwin_sig, "url": os.environ['DARWIN_URL']}, | |
| "linux-x86_64": {"signature": linux_sig, "url": os.environ['LINUX_URL']}, | |
| "windows-x86_64": {"signature": win_sig, "url": os.environ['WINDOWS_URL']}, | |
| } | |
| } | |
| with open('latest.json', 'w') as f: | |
| json.dump(payload, f, indent=2) | |
| print("Generated latest.json:") | |
| print(json.dumps(payload, indent=2)) | |
| PYEOF | |
| # Verify it is valid JSON | |
| python3 -c "import json; json.load(open('latest.json')); print('JSON is valid')" | |
| # Upload to the draft release (overwrites any existing latest.json) | |
| gh release upload "$TAG" latest.json#latest.json --clobber | |
| if ! gh release view "$TAG" --json assets -q '.assets[].name' | grep -Fx 'latest.json' >/dev/null; then | |
| echo "ERROR: latest.json was not found on release $TAG after upload" >&2 | |
| gh release view "$TAG" --json assets -q '.assets[].name' | |
| exit 1 | |
| fi | |
| echo "latest.json successfully uploaded to release $TAG" |