Skip to content

Release

Release #95

Workflow file for this run

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"