380 lines
15 KiB
YAML
380 lines
15 KiB
YAML
name: Release (Alpha)
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
env:
|
|
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
|
|
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
|
|
|
|
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]
|
|
|
|
existing_tags = [
|
|
tag for tag in lines(["git", "tag", "--points-at", "HEAD"])
|
|
if tag.startswith("alpha-v")
|
|
]
|
|
|
|
if existing_tags:
|
|
tag = existing_tags[0]
|
|
version = tag.removeprefix("alpha-v")
|
|
else:
|
|
today = datetime.now(timezone.utc).date()
|
|
stable_date = None
|
|
stable_pattern = re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$")
|
|
|
|
for stable_tag in lines(["git", "tag", "--list", "stable-v*", "--sort=-version:refname"]):
|
|
match = stable_pattern.fullmatch(stable_tag)
|
|
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 = f"{calendar_version}-alpha.{sequence}"
|
|
tag = f"alpha-v{version}"
|
|
|
|
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
|
|
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 Bun (required for bundle-qmd.sh)
|
|
uses: oven-sh/setup-bun@v2
|
|
with:
|
|
bun-version: latest
|
|
|
|
- name: Setup Rust
|
|
uses: dtolnay/rust-toolchain@stable
|
|
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 sys
|
|
from urllib.parse import urlparse
|
|
|
|
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 is_http_url(value: str) -> bool:
|
|
parsed = urlparse(normalize_http_like(value))
|
|
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
|
|
|
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 not value:
|
|
errors.append(f"{name} must be set for release builds")
|
|
elif not is_http_url(value):
|
|
errors.append(f"{name} must be a valid http(s) URL")
|
|
|
|
if not values["VITE_POSTHOG_KEY"]:
|
|
errors.append("VITE_POSTHOG_KEY must be set for release builds")
|
|
|
|
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 }}
|
|
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
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Phase 3: Publish GitHub Release
|
|
# No lipo/re-signing — use the per-arch artifacts directly
|
|
# ─────────────────────────────────────────────────────────────
|
|
release:
|
|
name: GitHub Release (alpha)
|
|
needs: [version, build]
|
|
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: Generate release notes
|
|
run: |
|
|
PREV_TAG=$(git tag --list 'alpha-v*' --sort=-version:refname | head -n 1 || echo "")
|
|
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 "**Requires Apple Silicon (M1/M2/M3)**"
|
|
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}/"
|
|
|
|
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
|
|
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
|
|
|
|
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_TARBALL}"
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
echo "alpha-latest.json:"; cat alpha-latest.json
|
|
|
|
- name: Publish GitHub Release
|
|
uses: softprops/action-gh-release@v2
|
|
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
|
|
alpha-latest.json
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Phase 4: Update GitHub Pages with release history
|
|
# ─────────────────────────────────────────────────────────────
|
|
pages:
|
|
name: Update release history page
|
|
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 Bun
|
|
uses: oven-sh/setup-bun@v2
|
|
with:
|
|
bun-version: latest
|
|
|
|
- name: Build release history page
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
mkdir -p _site/alpha _site/stable
|
|
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/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@v4
|
|
with:
|
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
publish_dir: ./_site
|
|
commit_message: "Update release history for ${{ needs.version.outputs.tag }}"
|