834 lines
35 KiB
YAML
834 lines
35 KiB
YAML
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 \
|
|
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:
|
|
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
|
|
installers=(
|
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
|
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 [ ${#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
|
|
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: write
|
|
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: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Build docs and release pages
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
VITEPRESS_BASE="/${GITHUB_REPOSITORY#*/}/" 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
|
|
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"
|
|
|
|
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
|
|
curl -fsSL "${PAGES_URL}/stable/latest.json" -o _site/stable/latest.json || echo '{}' > _site/stable/latest.json
|
|
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: Deploy to GitHub Pages
|
|
uses: peaceiris/actions-gh-pages@e9c66a37f080288a11235e32cbe2dc5fb3a679cc
|
|
with:
|
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
publish_dir: ./_site
|
|
commit_message: "Update release history for ${{ needs.version.outputs.tag }}"
|