408 lines
14 KiB
YAML
408 lines
14 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: 'Version to release (e.g., 0.1.0)'
|
|
required: true
|
|
type: string
|
|
dry_run:
|
|
description: 'Dry run (skip publishing)'
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
env:
|
|
CARGO_INCREMENTAL: 0
|
|
RUST_BACKTRACE: short
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
create-release:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_id: ${{ steps.create_release.outputs.id }}
|
|
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
version: ${{ steps.get_version.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Get version
|
|
id: get_version
|
|
run: |
|
|
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
|
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
|
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
|
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "version=0.0.0-dev" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Extract changelog for release
|
|
env:
|
|
VERSION: ${{ steps.get_version.outputs.version }}
|
|
run: |
|
|
node - <<'NODE'
|
|
const fs = require('fs');
|
|
const version = process.env.VERSION;
|
|
const changelogPath = 'CHANGELOG.md';
|
|
if (!fs.existsSync(changelogPath)) {
|
|
throw new Error('CHANGELOG.md not found; add it before releasing.');
|
|
}
|
|
const changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
const sections = changelog.split(/^## /m);
|
|
const section = sections.find(s => s.startsWith('[' + version + ']'));
|
|
if (!section) {
|
|
throw new Error('Changelog section [' + version + '] not found. Add a section like "## [' + version + '] - YYYY-MM-DD".');
|
|
}
|
|
const content = ('## ' + section).trim();
|
|
fs.mkdirSync('artifacts', { recursive: true });
|
|
fs.writeFileSync('artifacts/release-notes.md', content + '\n');
|
|
NODE
|
|
|
|
- name: Create GitHub Release
|
|
id: create_release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: v${{ steps.get_version.outputs.version }}
|
|
draft: true
|
|
generate_release_notes: false
|
|
body_path: artifacts/release-notes.md
|
|
name: OpenChamber v${{ steps.get_version.outputs.version }}
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
build-desktop-macos:
|
|
needs: create-release
|
|
runs-on: macos-26
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
|
include:
|
|
- target: aarch64-apple-darwin
|
|
arch: aarch64
|
|
platform: darwin-aarch64
|
|
- target: x86_64-apple-darwin
|
|
arch: x86_64
|
|
platform: darwin-x86_64
|
|
outputs:
|
|
version: ${{ needs.create-release.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@v2
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Install Rust stable
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- name: Rust cache
|
|
uses: swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: packages/desktop/src-tauri
|
|
key: ${{ matrix.target }}
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Install Apple Certificate
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
run: |
|
|
# Create temporary keychain
|
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
|
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
# Import certificate
|
|
echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
|
|
security import $RUNNER_TEMP/certificate.p12 \
|
|
-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:,codesign: \
|
|
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
- name: Set up notarization credentials
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
run: |
|
|
# Validate secrets are set
|
|
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
|
|
echo "Error: Missing Apple notarization credentials"
|
|
exit 1
|
|
fi
|
|
|
|
xcrun notarytool store-credentials "openchamber-notarize" \
|
|
--apple-id "$APPLE_ID" \
|
|
--team-id "$APPLE_TEAM_ID" \
|
|
--password "$APPLE_PASSWORD"
|
|
|
|
- name: Build UI package
|
|
run: bun run --cwd packages/ui build
|
|
|
|
- name: Build Desktop app
|
|
# Note: We use inline commands instead of desktop:build to pass architecture-specific --target flag
|
|
# This enables cross-compilation for both arm64 and x86_64 from the same runner
|
|
run: bun run --cwd packages/desktop build && bun run --cwd packages/desktop tauri build --target ${{ matrix.target }}
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_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 }}
|
|
|
|
- name: Prepare release artifacts
|
|
run: |
|
|
mkdir -p artifacts
|
|
VERSION="${{ needs.create-release.outputs.version }}"
|
|
|
|
# Copy DMG (Tauri names it with the target triple in the path)
|
|
DMG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg"
|
|
if ls $DMG_PATH 1> /dev/null 2>&1; then
|
|
DMG_FILE=$(ls $DMG_PATH | head -n 1)
|
|
DMG_BASE=$(basename "$DMG_FILE")
|
|
DMG_NAME="${DMG_BASE%.dmg}-${{ matrix.platform }}.dmg"
|
|
cp "$DMG_FILE" "artifacts/$DMG_NAME"
|
|
else
|
|
echo "Error: DMG file not found at $DMG_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
# Copy tar.gz and signature for updater
|
|
TAR_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz"
|
|
SIG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig"
|
|
|
|
if ls $TAR_PATH 1> /dev/null 2>&1; then
|
|
TAR_FILE=$(ls $TAR_PATH | head -n 1)
|
|
TAR_BASE=$(basename "$TAR_FILE")
|
|
TAR_NAME="${TAR_BASE%.tar.gz}-${{ matrix.platform }}.tar.gz"
|
|
cp "$TAR_FILE" "artifacts/$TAR_NAME"
|
|
else
|
|
echo "Error: tar.gz file not found at $TAR_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
if ls $SIG_PATH 1> /dev/null 2>&1; then
|
|
SIG_FILE=$(ls $SIG_PATH | head -n 1)
|
|
SIG_BASE=$(basename "$SIG_FILE")
|
|
SIG_NAME="${SIG_BASE%.tar.gz.sig}-${{ matrix.platform }}.tar.gz.sig"
|
|
cp "$SIG_FILE" "artifacts/$SIG_NAME"
|
|
else
|
|
echo "Error: signature file not found at $SIG_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Successfully prepared artifacts:"
|
|
ls -lh artifacts/
|
|
|
|
- name: Generate update manifest
|
|
run: |
|
|
VERSION="${{ needs.create-release.outputs.version }}"
|
|
|
|
# Find the signature file for this platform
|
|
SIG_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz.sig" | head -1)
|
|
if [ -f "$SIG_FILE" ]; then
|
|
SIGNATURE=$(cat "$SIG_FILE")
|
|
else
|
|
SIGNATURE=""
|
|
fi
|
|
|
|
# Find the tar.gz file name for this platform
|
|
TAR_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz" ! -name "*.sig" | head -1)
|
|
TAR_NAME=$(basename "$TAR_FILE" 2>/dev/null || echo "OpenChamber-${{ matrix.platform }}.app.tar.gz")
|
|
|
|
cat > artifacts/latest-${{ matrix.platform }}.json << EOF
|
|
{
|
|
"version": "${VERSION}",
|
|
"notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/v${VERSION}",
|
|
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"platforms": {
|
|
"${{ matrix.platform }}": {
|
|
"signature": "${SIGNATURE}",
|
|
"url": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${TAR_NAME}"
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
echo "Generated latest-${{ matrix.platform }}.json:"
|
|
cat artifacts/latest-${{ matrix.platform }}.json
|
|
|
|
- name: Upload release assets
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: |
|
|
artifacts/*.dmg
|
|
artifacts/*.tar.gz
|
|
artifacts/*.tar.gz.sig
|
|
artifacts/latest-${{ matrix.platform }}.json
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Upload manifest as artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: manifest-${{ matrix.platform }}
|
|
path: artifacts/latest-${{ matrix.platform }}.json
|
|
retention-days: 1
|
|
|
|
publish-npm:
|
|
needs: create-release
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup bun
|
|
uses: oven-sh/setup-bun@v2
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
|
|
- name: Install dependencies
|
|
run: bun install --frozen-lockfile
|
|
|
|
- name: Build packages
|
|
run: bun run build
|
|
|
|
- name: Create npm tarball
|
|
working-directory: packages/web
|
|
run: npm pack
|
|
|
|
- name: Upload npm tarball to release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: packages/web/*.tgz
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Publish to npm
|
|
if: ${{ github.event.inputs.dry_run != 'true' }}
|
|
working-directory: packages/web
|
|
run: npm publish --access public
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
combine-manifests:
|
|
needs: [create-release, build-desktop-macos]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Download aarch64 manifest
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: manifest-darwin-aarch64
|
|
path: artifacts
|
|
|
|
- name: Download x86_64 manifest
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: manifest-darwin-x86_64
|
|
path: artifacts
|
|
|
|
- name: Combine manifests
|
|
run: |
|
|
VERSION="${{ needs.create-release.outputs.version }}"
|
|
PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
REPO="${{ github.repository }}"
|
|
|
|
# Validate that both manifest files exist and are valid JSON
|
|
if [ ! -f artifacts/latest-darwin-aarch64.json ]; then
|
|
echo "Error: aarch64 manifest not found"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f artifacts/latest-darwin-x86_64.json ]; then
|
|
echo "Error: x86_64 manifest not found"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate JSON structure
|
|
if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then
|
|
echo "Error: aarch64 manifest is not valid JSON"
|
|
exit 1
|
|
fi
|
|
|
|
if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then
|
|
echo "Error: x86_64 manifest is not valid JSON"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate platform data exists in manifests
|
|
if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then
|
|
echo "Error: darwin-aarch64 platform data not found in manifest"
|
|
exit 1
|
|
fi
|
|
|
|
if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then
|
|
echo "Error: darwin-x86_64 platform data not found in manifest"
|
|
exit 1
|
|
fi
|
|
|
|
# Use jq to properly combine the manifests
|
|
jq -n \
|
|
--arg version "$VERSION" \
|
|
--arg notes "See release notes at https://github.com/${REPO}/releases/tag/v${VERSION}" \
|
|
--arg pub_date "$PUB_DATE" \
|
|
--slurpfile aarch64 artifacts/latest-darwin-aarch64.json \
|
|
--slurpfile x86_64 artifacts/latest-darwin-x86_64.json \
|
|
'{
|
|
version: $version,
|
|
notes: $notes,
|
|
pub_date: $pub_date,
|
|
platforms: {
|
|
"darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"],
|
|
"darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"]
|
|
}
|
|
}' > artifacts/latest.json
|
|
|
|
echo "Generated combined latest.json:"
|
|
cat artifacts/latest.json
|
|
|
|
- name: Upload combined manifest
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
files: artifacts/latest.json
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
finalize-release:
|
|
needs: [create-release, build-desktop-macos, publish-npm, combine-manifests]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Publish release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: v${{ needs.create-release.outputs.version }}
|
|
draft: false
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|