name: Release (Alpha) on: push: branches: - main paths-ignore: - ".husky/**" - ".github/workflows/deploy-docs.yml" - ".github/workflows/release.yml" - "site/**" env: # Bump this when Tauri/Rust target artifacts capture stale absolute paths. RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria # The production Vite bundle can exceed Node's default ~2GB heap on # macOS arm64 runners while Tauri runs beforeBuildCommand. NODE_OPTIONS: --max-old-space-size=4096 concurrency: group: release-alpha-${{ github.ref }} cancel-in-progress: true jobs: # ───────────────────────────────────────────────────────────── # Phase 1: Compute the alpha version string once # Alpha builds use calendar semver and stay newer than the latest stable tag. # ───────────────────────────────────────────────────────────── version: name: Compute alpha version runs-on: ubuntu-latest outputs: version: ${{ steps.ver.outputs.version }} display_version: ${{ steps.ver.outputs.display_version }} tag: ${{ steps.ver.outputs.tag }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - id: ver shell: bash run: | python3 <<'PY' > version.env import re import subprocess from datetime import datetime, timedelta, timezone def lines(command: list[str]) -> list[str]: output = subprocess.check_output(command, text=True).strip() return [line for line in output.splitlines() if line] alpha_pattern = re.compile(r"^alpha-v(\d{4}\.\d{1,2}\.\d{1,2})-alpha\.(\d+)$") def parse_alpha_tag(tag: str) -> tuple[str, int] | None: match = alpha_pattern.fullmatch(tag) if not match: return None calendar_version, sequence = match.groups() return calendar_version, int(sequence) def alpha_version(calendar_version: str, sequence: int) -> str: return f"{calendar_version}-alpha.{sequence}" def alpha_tag(calendar_version: str, sequence: int) -> str: return f"alpha-v{calendar_version}-alpha.{sequence:04d}" existing_tags = [ tag for tag in lines(["git", "tag", "--points-at", "HEAD"]) if tag.startswith("alpha-v") ] if existing_tags: tag = existing_tags[0] parsed = parse_alpha_tag(tag) version = alpha_version(*parsed) if parsed is not None else tag.removeprefix("alpha-v") else: today = datetime.now(timezone.utc).date() stable_date = None stable_patterns = ( re.compile(r"^v(\d{4})-(\d{2})-(\d{2})$"), re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$"), ) stable_tags = lines([ "git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)", "refs/tags/v20*", "refs/tags/stable-v*", ]) for stable_tag in stable_tags: match = next((pattern.fullmatch(stable_tag) for pattern in stable_patterns if pattern.fullmatch(stable_tag)), None) if not match: continue year, month, day = map(int, match.groups()) try: stable_date = datetime(year, month, day, tzinfo=timezone.utc).date() except ValueError: continue break alpha_date = today if stable_date is None or today > stable_date else stable_date + timedelta(days=1) calendar_version = f"{alpha_date.year}.{alpha_date.month}.{alpha_date.day}" sequence = len(lines(["git", "tag", "--list", f"alpha-v{calendar_version}-alpha.*"])) + 1 version = alpha_version(calendar_version, sequence) tag = alpha_tag(calendar_version, sequence) display_match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)", version) display_version = ( f"Alpha {int(display_match.group(1))}.{int(display_match.group(2))}.{int(display_match.group(3))}.{int(display_match.group(4))}" if display_match else version ) print(f"version={version}") print(f"display_version={display_version}") print(f"tag={tag}") PY cat version.env >> "$GITHUB_OUTPUT" VERSION=$(grep '^version=' version.env | cut -d= -f2-) DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-) echo "### Alpha version: \`$DISPLAY_VERSION\` (\`$VERSION\`)" >> "$GITHUB_STEP_SUMMARY" # ───────────────────────────────────────────────────────────── # Phase 2: Build each architecture in parallel # tauri build handles signing automatically via env vars # ───────────────────────────────────────────────────────────── build: name: Build (${{ matrix.arch }}) needs: version runs-on: macos-15 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@f40ffcd9367d9f12939873eb1018b921a783ffaa with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'pnpm' - name: Setup Bun (required for bundle-qmd.sh) uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: latest - name: Setup Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with: targets: ${{ matrix.target }} - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git src-tauri/target key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} restore-keys: | ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}- - name: Install frontend dependencies run: pnpm install --frozen-lockfile - name: Clear cached bundle artifacts run: | rm -rf src-tauri/target/${{ matrix.target }}/release/bundle - name: Set version run: | VERSION="${{ needs.version.outputs.version }}" jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml - name: Import Apple Developer certificate into keychain env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | CERT_PATH="$RUNNER_TEMP/apple_cert.p12" KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db" KEYCHAIN_PASSWORD="$(uuidgen)" echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" security list-keychain -d user -s "$KEYCHAIN_PATH" security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" - name: Validate telemetry env env: VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} run: | python3 <<'PY' import os import re import sys from urllib.parse import urlparse DISALLOWED_PLACEHOLDERS = { "", "-", "_", "false", "true", "null", "undefined", "none", "disabled", } def normalize(name: str) -> str: value = os.getenv(name, "").strip() if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): value = value[1:-1].strip() return value def normalize_http_like(value: str) -> str: if "://" in value: return value return f"https://{value}" def normalize_hostname(hostname: str) -> str: normalized = hostname.strip().rstrip('.').lower() if normalized.startswith('[') and normalized.endswith(']'): normalized = normalized[1:-1] return normalized def is_ip_address(hostname: str) -> bool: if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname): return all(0 <= int(part) <= 255 for part in hostname.split('.')) return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None def is_allowed_hostname(hostname: str) -> bool: normalized = normalize_hostname(hostname) if not normalized or normalized in DISALLOWED_PLACEHOLDERS: return False if normalized == 'localhost': return True return '.' in normalized or is_ip_address(normalized) def is_http_url(value: str) -> bool: parsed = urlparse(normalize_http_like(value)) return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "") values = { name: normalize(name) for name in ( "VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_KEY", "VITE_POSTHOG_HOST", ) } errors = [] for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"): value = values[name] if value.lower() in DISALLOWED_PLACEHOLDERS: errors.append(f"{name} must be set to a real value, not a placeholder") elif not is_http_url(value): errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host") if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS: errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder") if errors: print("Telemetry env validation failed:", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) raise SystemExit(1) print("Telemetry env validation passed.") PY - name: Build Tauri app (with signing + notarization) env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 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 }} VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} run: | # Alpha releases only need the notarized app bundle and updater tarball. # Skipping DMG packaging avoids fragile bundle_dmg.sh failures on macOS runners. pnpm tauri build --target ${{ matrix.target }} --bundles app - name: Upload updater artifacts (.tar.gz + .sig) uses: actions/upload-artifact@v4 with: name: updater-${{ matrix.arch }} path: | src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig retention-days: 1 build-linux: name: Build (linux-x86_64) needs: version runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install Tauri Linux system dependencies run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libsoup-3.0-dev \ libxdo-dev \ libssl-dev \ libayatana-appindicator3-dev \ fcitx5-frontend-gtk3 \ libfuse2 \ librsvg2-dev \ curl \ wget \ patchelf \ build-essential \ file \ rpm - name: Setup pnpm uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa 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@29eef336d9b2848a0b548edc03f92a220660cdb8 with: targets: x86_64-unknown-linux-gnu - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git src-tauri/target key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} restore-keys: | ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}- - name: Install frontend dependencies run: pnpm install --frozen-lockfile - name: Clear cached bundle artifacts run: | rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle - name: Set version run: | VERSION="${{ needs.version.outputs.version }}" jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml - name: Build Tauri app (Linux bundles) env: APPIMAGE_EXTRACT_AND_RUN: 1 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} run: | pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage - name: Validate Linux bundles run: | shopt -s nullglob appimages=( src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage ) installers=( "${appimages[@]}" src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm ) signatures=( src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig ) if [ ${#appimages[@]} -eq 0 ]; then echo "::error::Linux build produced no AppImage bundle." exit 1 fi if [ ${#installers[@]} -eq 0 ]; then echo "::error::Linux build produced no AppImage, deb or rpm bundle." exit 1 fi if [ ${#signatures[@]} -eq 0 ]; then echo "::error::Linux build produced no updater signature (.sig) artifact." exit 1 fi - name: Upload Linux bundles uses: actions/upload-artifact@v4 with: name: linux-x86_64-bundles path: | src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig if-no-files-found: error retention-days: 1 build-windows: name: Build (windows-x86_64) needs: version runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa 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@29eef336d9b2848a0b548edc03f92a220660cdb8 with: targets: x86_64-pc-windows-msvc - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~\.cargo\registry ~\.cargo\git src-tauri\target key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} restore-keys: | ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}- - name: Cache Tauri Windows tools uses: actions/cache@v4 with: path: ~\AppData\Local\tauri key: ${{ runner.os }}-tauri-tools-nsis-3.11-nsis-tauri-utils-0.5.3 - name: Prefetch Tauri NSIS toolchain shell: pwsh run: ./.github/scripts/prefetch-tauri-nsis.ps1 - name: Install frontend dependencies run: pnpm install --frozen-lockfile - name: Clear cached Windows bundle artifacts shell: pwsh run: | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle" - name: Set version shell: pwsh run: | $version = "${{ needs.version.outputs.version }}" $tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json $tauri.version = $version $tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json" (Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml" - name: Validate Windows release env shell: bash env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} run: | for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do if [ -z "${!name}" ]; then echo "::error::$name is required to build signed Windows updater artifacts." exit 1 fi done - name: Build Tauri app (Windows bundles) env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} run: | pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis - name: Validate Windows bundles shell: bash run: | shopt -s nullglob installers=( src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi ) signatures=( src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig ) if [ ${#installers[@]} -eq 0 ]; then echo "::error::Windows build produced no installable NSIS or MSI bundle." exit 1 fi for installer in "${installers[@]}"; do if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then echo "::error::Windows build produced an installer for a different version: $(basename "$installer")" exit 1 fi done if [ ${#signatures[@]} -eq 0 ]; then echo "::error::Windows build produced no updater signature (.sig) artifact." exit 1 fi - name: Upload Windows bundles uses: actions/upload-artifact@v4 with: name: windows-x86_64-bundles path: | src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig if-no-files-found: error retention-days: 1 # ───────────────────────────────────────────────────────────── # Phase 3: Publish GitHub Release # No lipo/re-signing — use the per-arch artifacts directly # ───────────────────────────────────────────────────────────── release: name: GitHub Release (alpha) needs: [version, build, build-linux, build-windows] runs-on: ubuntu-latest permissions: contents: write pages: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download all artifacts uses: actions/download-artifact@v4 - name: Normalize macOS updater artifact names run: | normalize_updater() { local arch="$1" local normalized_updater="$2" local artifact_dir="updater-${arch}" local updater_file updater_file=$(find "$artifact_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit) if [ -z "$updater_file" ]; then echo "::error::Missing macOS updater artifact in ${artifact_dir}" >&2 return 1 fi local sig_file="${updater_file}.sig" if [ ! -f "$sig_file" ]; then echo "::error::Missing macOS updater signature for ${updater_file}" >&2 return 1 fi local normalized_sig="${normalized_updater}.sig" if [ "$updater_file" != "$normalized_updater" ]; then mv "$updater_file" "$normalized_updater" fi if [ "$sig_file" != "$normalized_sig" ]; then mv "$sig_file" "$normalized_sig" fi } normalize_updater aarch64 "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" normalize_updater x86_64 "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" - name: Generate release notes run: | PREV_TAG=$(python3 <<'PY' import re import subprocess current_tag = '${{ needs.version.outputs.tag }}' pattern = re.compile(r'^alpha-v(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)$') output = subprocess.check_output(['git', 'tag', '--list', 'alpha-v*'], text=True).strip() tags = [line for line in output.splitlines() if line and line != current_tag] parsed_tags = [] for tag in tags: match = pattern.fullmatch(tag) if not match: continue year, month, day, sequence = map(int, match.groups()) parsed_tags.append(((year, month, day, sequence), tag)) print(max(parsed_tags)[1] if parsed_tags else '') PY ) if [ -z "$PREV_TAG" ]; then NOTES=$(git log --oneline --no-merges -20) else NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD") fi { echo "## What's Changed (Alpha)" echo "" echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done echo "" echo "---" echo "**Alpha build — updated on every push to \`main\`**" echo "" echo "**Includes macOS (Apple Silicon and Intel), Linux x64, and Windows x64 bundles**" echo "" echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*" } > release_notes.md - name: Build alpha-latest.json run: | VERSION="${{ needs.version.outputs.version }}" TAG="${{ needs.version.outputs.tag }}" REPO="${GITHUB_REPOSITORY}" REPO_NAME="${REPO#*/}" PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/" find_required() { for pattern in "$@"; do set -- $pattern if [ -e "$1" ]; then printf '%s\n' "$1" return 0 fi done return 1 } ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig") ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}" ARM_SIG=$(cat "$ARM_SIG_FILE") ARM_UPDATER=$(basename "$ARM_UPDATER_FILE") INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig") INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}" INTEL_SIG=$(cat "$INTEL_SIG_FILE") INTEL_UPDATER=$(basename "$INTEL_UPDATER_FILE") LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig") LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}" LINUX_SIG=$(cat "$LINUX_SIG_FILE") LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE") LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")") WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig") WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}" WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE") WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE") WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")") cat > alpha-latest.json << EOF { "version": "${VERSION}", "notes": "Alpha build. See ${PAGES_URL} for full release notes.", "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "platforms": { "darwin-aarch64": { "signature": "${ARM_SIG}", "url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}", "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}" }, "darwin-x86_64": { "signature": "${INTEL_SIG}", "url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}", "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}" }, "linux-x86_64": { "signature": "${LINUX_SIG}", "url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}", "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}" }, "windows-x86_64": { "signature": "${WINDOWS_SIG}", "url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}", "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}" } } } EOF echo "alpha-latest.json:"; cat alpha-latest.json - name: Publish GitHub Release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 with: tag_name: ${{ needs.version.outputs.tag }} name: Tolaria ${{ needs.version.outputs.display_version }} body_path: release_notes.md draft: false prerelease: true files: | updater-aarch64/*.app.tar.gz updater-aarch64/*.app.tar.gz.sig updater-x86_64/*.app.tar.gz updater-x86_64/*.app.tar.gz.sig linux-x86_64-bundles/*.deb linux-x86_64-bundles/*.deb.sig linux-x86_64-bundles/*.rpm linux-x86_64-bundles/*.AppImage linux-x86_64-bundles/*.AppImage.sig linux-x86_64-bundles/*.AppImage.tar.gz linux-x86_64-bundles/*.AppImage.tar.gz.sig linux-x86_64-bundles/*/*.deb linux-x86_64-bundles/*/*.deb.sig linux-x86_64-bundles/*/*.rpm linux-x86_64-bundles/*/*.AppImage linux-x86_64-bundles/*/*.AppImage.sig linux-x86_64-bundles/*/*.AppImage.tar.gz linux-x86_64-bundles/*/*.AppImage.tar.gz.sig windows-x86_64-bundles/*.exe windows-x86_64-bundles/*.exe.sig windows-x86_64-bundles/*.msi windows-x86_64-bundles/*.msi.sig windows-x86_64-bundles/*.zip windows-x86_64-bundles/*.zip.sig windows-x86_64-bundles/*/*.exe windows-x86_64-bundles/*/*.exe.sig windows-x86_64-bundles/*/*.msi windows-x86_64-bundles/*/*.msi.sig windows-x86_64-bundles/*/*.zip windows-x86_64-bundles/*/*.zip.sig alpha-latest.json # ───────────────────────────────────────────────────────────── # Phase 4: Update GitHub Pages with docs, release history, and download assets # ───────────────────────────────────────────────────────────── pages: name: Update docs and release pages needs: [version, release] runs-on: ubuntu-latest permissions: contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} concurrency: group: github-pages cancel-in-progress: false steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'pnpm' - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: latest - name: Setup Pages uses: actions/configure-pages@v5 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build docs and release pages env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VITEPRESS_BASE="/" pnpm docs:build mkdir -p _site/alpha _site/stable _site/release-notes cp -R site/.vitepress/dist/. _site/ if [ -d release-notes ]; then cp release-notes/*.md _site/release-notes/ 2>/dev/null || true; fi gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json STABLE_TAG=$(gh release list --repo ${{ github.repository }} --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // ""') gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json if [ -n "$STABLE_TAG" ]; then gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json else echo '{}' > _site/stable/latest.json fi bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/releases/index.html mkdir -p _site/download cp _site/stable/download/index.html _site/download/index.html cp _site/alpha/latest.json _site/latest.json cp _site/alpha/latest.json _site/latest-canary.json - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4 with: path: ./_site - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4