2ff9ccabfe
* fix(ci): enable automatic release workflow triggering - Use PAT_TOKEN instead of GITHUB_TOKEN in prepare-release.yml When GITHUB_TOKEN pushes a tag, GitHub prevents it from triggering other workflows (security feature to prevent infinite loops). PAT_TOKEN allows the tag push to trigger release.yml automatically. - Change dry_run default to false in release.yml Manual workflow triggers should create real releases by default, not dry runs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): add PAT_TOKEN validation with clear error message Addresses Sentry review feedback - fail fast with actionable error if PAT_TOKEN secret is not configured, instead of cryptic auth failure. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
754 lines
28 KiB
YAML
754 lines
28 KiB
YAML
name: Release
|
|
# Triggers on version tags (v*) to build and publish releases
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: 'Test build without creating release'
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
jobs:
|
|
# Intel build on Intel runner for native compilation
|
|
# Note: macos-15-intel is the last Intel runner, supported until Fall 2027
|
|
build-macos-intel:
|
|
runs-on: macos-15-intel
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '24'
|
|
|
|
- name: Get npm cache directory
|
|
id: npm-cache
|
|
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: ${{ steps.npm-cache.outputs.dir }}
|
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: ${{ runner.os }}-npm-
|
|
|
|
- name: Install dependencies
|
|
run: cd apps/frontend && npm ci
|
|
|
|
- name: Install Rust toolchain (for building native Python packages)
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Cache pip wheel cache (for compiled packages like real_ladybug)
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/Library/Caches/pip
|
|
key: pip-wheel-${{ runner.os }}-x64-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
pip-wheel-${{ runner.os }}-x64-
|
|
|
|
- name: Cache bundled Python
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: apps/frontend/python-runtime
|
|
key: python-bundle-${{ runner.os }}-x64-3.12.8-rust-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
python-bundle-${{ runner.os }}-x64-3.12.8-rust-
|
|
|
|
- name: Build application
|
|
run: cd apps/frontend && npm run build
|
|
env:
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Package macOS (Intel)
|
|
run: cd apps/frontend && npm run package:mac -- --x64
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Notarize macOS Intel app
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -z "$APPLE_ID" ]; then
|
|
echo "Skipping notarization: APPLE_ID not configured"
|
|
exit 0
|
|
fi
|
|
cd apps/frontend
|
|
for dmg in dist/*.dmg; do
|
|
echo "Notarizing $dmg..."
|
|
xcrun notarytool submit "$dmg" \
|
|
--apple-id "$APPLE_ID" \
|
|
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
|
|
--team-id "$APPLE_TEAM_ID" \
|
|
--wait
|
|
xcrun stapler staple "$dmg"
|
|
echo "Successfully notarized and stapled $dmg"
|
|
done
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: macos-intel-builds
|
|
path: |
|
|
apps/frontend/dist/*.dmg
|
|
apps/frontend/dist/*.zip
|
|
apps/frontend/dist/*.yml
|
|
apps/frontend/dist/*.blockmap
|
|
|
|
# Apple Silicon build on ARM64 runner for native compilation
|
|
build-macos-arm64:
|
|
runs-on: macos-15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '24'
|
|
|
|
- name: Get npm cache directory
|
|
id: npm-cache
|
|
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: ${{ steps.npm-cache.outputs.dir }}
|
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: ${{ runner.os }}-npm-
|
|
|
|
- name: Install dependencies
|
|
run: cd apps/frontend && npm ci
|
|
|
|
- name: Cache pip wheel cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/Library/Caches/pip
|
|
key: pip-wheel-${{ runner.os }}-arm64-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
pip-wheel-${{ runner.os }}-arm64-
|
|
|
|
- name: Cache bundled Python
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: apps/frontend/python-runtime
|
|
key: python-bundle-${{ runner.os }}-arm64-3.12.8-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
python-bundle-${{ runner.os }}-arm64-3.12.8-
|
|
|
|
- name: Build application
|
|
run: cd apps/frontend && npm run build
|
|
env:
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Package macOS (Apple Silicon)
|
|
run: cd apps/frontend && npm run package:mac -- --arm64
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Notarize macOS ARM64 app
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -z "$APPLE_ID" ]; then
|
|
echo "Skipping notarization: APPLE_ID not configured"
|
|
exit 0
|
|
fi
|
|
cd apps/frontend
|
|
for dmg in dist/*.dmg; do
|
|
echo "Notarizing $dmg..."
|
|
xcrun notarytool submit "$dmg" \
|
|
--apple-id "$APPLE_ID" \
|
|
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
|
|
--team-id "$APPLE_TEAM_ID" \
|
|
--wait
|
|
xcrun stapler staple "$dmg"
|
|
echo "Successfully notarized and stapled $dmg"
|
|
done
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: macos-arm64-builds
|
|
path: |
|
|
apps/frontend/dist/*.dmg
|
|
apps/frontend/dist/*.zip
|
|
apps/frontend/dist/*.yml
|
|
apps/frontend/dist/*.blockmap
|
|
|
|
build-windows:
|
|
runs-on: windows-latest
|
|
permissions:
|
|
id-token: write # Required for OIDC authentication with Azure
|
|
contents: read
|
|
env:
|
|
# Job-level env so AZURE_CLIENT_ID is available for step-level if conditions
|
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '24'
|
|
|
|
- name: Get npm cache directory
|
|
id: npm-cache
|
|
shell: bash
|
|
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: ${{ steps.npm-cache.outputs.dir }}
|
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: ${{ runner.os }}-npm-
|
|
|
|
- name: Install dependencies
|
|
run: cd apps/frontend && npm ci
|
|
|
|
- name: Cache pip wheel cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~\AppData\Local\pip\Cache
|
|
key: pip-wheel-${{ runner.os }}-x64-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
pip-wheel-${{ runner.os }}-x64-
|
|
|
|
- name: Cache bundled Python
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: apps/frontend/python-runtime
|
|
key: python-bundle-${{ runner.os }}-x64-3.12.8-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
python-bundle-${{ runner.os }}-x64-3.12.8-
|
|
|
|
- name: Build application
|
|
run: cd apps/frontend && npm run build
|
|
env:
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Package Windows
|
|
run: cd apps/frontend && npm run package:win
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
# Disable electron-builder's built-in signing (we use Azure Trusted Signing instead)
|
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Azure Login (OIDC)
|
|
if: env.AZURE_CLIENT_ID != ''
|
|
uses: azure/login@v2
|
|
with:
|
|
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
|
|
- name: Sign Windows executable with Azure Trusted Signing
|
|
if: env.AZURE_CLIENT_ID != ''
|
|
uses: azure/trusted-signing-action@v0.5.11
|
|
with:
|
|
endpoint: https://neu.codesigning.azure.net/
|
|
trusted-signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
|
|
certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE }}
|
|
files-folder: apps/frontend/dist
|
|
files-folder-filter: exe
|
|
file-digest: SHA256
|
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
timestamp-digest: SHA256
|
|
|
|
- name: Verify Windows executable is signed
|
|
if: env.AZURE_CLIENT_ID != ''
|
|
shell: pwsh
|
|
run: |
|
|
cd apps/frontend/dist
|
|
$exeFile = Get-ChildItem -Filter "*.exe" | Select-Object -First 1
|
|
if ($exeFile) {
|
|
Write-Host "Verifying signature on $($exeFile.Name)..."
|
|
$sig = Get-AuthenticodeSignature -FilePath $exeFile.FullName
|
|
if ($sig.Status -ne 'Valid') {
|
|
Write-Host "::error::Signature verification failed: $($sig.Status)"
|
|
Write-Host "::error::Status Message: $($sig.StatusMessage)"
|
|
exit 1
|
|
}
|
|
Write-Host "✅ Signature verified successfully"
|
|
Write-Host " Subject: $($sig.SignerCertificate.Subject)"
|
|
Write-Host " Issuer: $($sig.SignerCertificate.Issuer)"
|
|
Write-Host " Thumbprint: $($sig.SignerCertificate.Thumbprint)"
|
|
} else {
|
|
Write-Host "::error::No .exe file found to verify"
|
|
exit 1
|
|
}
|
|
|
|
- name: Regenerate checksums after signing
|
|
if: env.AZURE_CLIENT_ID != ''
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = "Stop"
|
|
cd apps/frontend/dist
|
|
|
|
# Find the installer exe (electron-builder names it with "Setup" or just the app name)
|
|
# electron-builder produces one installer exe per build
|
|
$exeFiles = Get-ChildItem -Filter "*.exe"
|
|
if ($exeFiles.Count -eq 0) {
|
|
Write-Host "::error::No .exe files found in dist folder"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "Found $($exeFiles.Count) exe file(s): $($exeFiles.Name -join ', ')"
|
|
|
|
$ymlFile = "latest.yml"
|
|
if (-not (Test-Path $ymlFile)) {
|
|
Write-Host "::error::$ymlFile not found - cannot update checksums"
|
|
exit 1
|
|
}
|
|
|
|
$content = Get-Content $ymlFile -Raw
|
|
$originalContent = $content
|
|
|
|
# Process each exe file and update its hash in latest.yml
|
|
foreach ($exeFile in $exeFiles) {
|
|
Write-Host "Processing $($exeFile.Name)..."
|
|
|
|
# Compute SHA512 hash and convert to base64 (electron-builder format)
|
|
$bytes = [System.IO.File]::ReadAllBytes($exeFile.FullName)
|
|
$sha512 = [System.Security.Cryptography.SHA512]::Create()
|
|
$hashBytes = $sha512.ComputeHash($bytes)
|
|
$hash = [System.Convert]::ToBase64String($hashBytes)
|
|
$size = $exeFile.Length
|
|
|
|
Write-Host " Hash: $hash"
|
|
Write-Host " Size: $size"
|
|
}
|
|
|
|
# For electron-builder, latest.yml has a single file entry for the installer
|
|
# Update the sha512 and size for the primary exe (first one, typically the installer)
|
|
$primaryExe = $exeFiles | Select-Object -First 1
|
|
$bytes = [System.IO.File]::ReadAllBytes($primaryExe.FullName)
|
|
$sha512 = [System.Security.Cryptography.SHA512]::Create()
|
|
$hashBytes = $sha512.ComputeHash($bytes)
|
|
$hash = [System.Convert]::ToBase64String($hashBytes)
|
|
$size = $primaryExe.Length
|
|
|
|
# Update sha512 hash (base64 pattern: alphanumeric, +, /, =)
|
|
$content = $content -replace 'sha512: [A-Za-z0-9+/=]+', "sha512: $hash"
|
|
# Update size
|
|
$content = $content -replace 'size: \d+', "size: $size"
|
|
|
|
if ($content -eq $originalContent) {
|
|
Write-Host "::error::Checksum replacement failed - content unchanged. Check if latest.yml format has changed."
|
|
exit 1
|
|
}
|
|
|
|
Set-Content -Path $ymlFile -Value $content -NoNewline
|
|
Write-Host "✅ Updated $ymlFile with new base64 hash and size for $($primaryExe.Name)"
|
|
|
|
- name: Skip signing notice
|
|
if: env.AZURE_CLIENT_ID == ''
|
|
run: echo "::warning::Windows signing skipped - AZURE_CLIENT_ID not configured. The .exe will be unsigned."
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: windows-builds
|
|
path: |
|
|
apps/frontend/dist/*.exe
|
|
apps/frontend/dist/*.yml
|
|
apps/frontend/dist/*.blockmap
|
|
|
|
build-linux:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '24'
|
|
|
|
- name: Get npm cache directory
|
|
id: npm-cache
|
|
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: ${{ steps.npm-cache.outputs.dir }}
|
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: ${{ runner.os }}-npm-
|
|
|
|
- name: Install dependencies
|
|
run: cd apps/frontend && npm ci
|
|
|
|
- name: Setup Flatpak
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y flatpak flatpak-builder
|
|
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
flatpak install -y --user flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08
|
|
flatpak install -y --user flathub org.electronjs.Electron2.BaseApp//25.08
|
|
|
|
- name: Cache pip wheel cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: pip-wheel-${{ runner.os }}-x64-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
pip-wheel-${{ runner.os }}-x64-
|
|
|
|
- name: Cache bundled Python
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: apps/frontend/python-runtime
|
|
key: python-bundle-${{ runner.os }}-x64-3.12.8-${{ hashFiles('apps/backend/requirements.txt') }}
|
|
restore-keys: |
|
|
python-bundle-${{ runner.os }}-x64-3.12.8-
|
|
|
|
- name: Build application
|
|
run: cd apps/frontend && npm run build
|
|
env:
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Package Linux
|
|
run: cd apps/frontend && npm run package:linux
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
|
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-builds
|
|
path: |
|
|
apps/frontend/dist/*.AppImage
|
|
apps/frontend/dist/*.deb
|
|
apps/frontend/dist/*.flatpak
|
|
apps/frontend/dist/*.yml
|
|
apps/frontend/dist/*.blockmap
|
|
|
|
create-release:
|
|
needs: [build-macos-intel, build-macos-arm64, build-windows, build-linux]
|
|
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
|
|
with:
|
|
path: dist
|
|
|
|
- name: Flatten and validate artifacts
|
|
run: |
|
|
mkdir -p release-assets
|
|
find dist -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.flatpak" -o -name "*.yml" -o -name "*.blockmap" \) -exec cp {} release-assets/ \;
|
|
|
|
# Validate that installer files exist (not just manifests)
|
|
installer_count=$(find release-assets -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.flatpak" \) | wc -l)
|
|
if [ "$installer_count" -eq 0 ]; then
|
|
echo "::error::No installer artifacts found! Expected .dmg, .zip, .exe, .AppImage, .deb, or .flatpak files."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Found $installer_count installer(s):"
|
|
find release-assets -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.flatpak" \) -exec basename {} \;
|
|
|
|
# Validate that electron-updater manifest files are present (required for auto-updates)
|
|
yml_count=$(find release-assets -type f -name "*.yml" | wc -l)
|
|
if [ "$yml_count" -eq 0 ]; then
|
|
echo "::error::No update manifest (.yml) files found! Auto-update architecture detection will not work."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Found $yml_count manifest file(s):"
|
|
find release-assets -type f -name "*.yml" -exec basename {} \;
|
|
|
|
echo ""
|
|
echo "All release assets:"
|
|
ls -la release-assets/
|
|
|
|
- name: Generate checksums
|
|
run: |
|
|
cd release-assets
|
|
sha256sum ./* > checksums.sha256
|
|
cat checksums.sha256
|
|
|
|
- name: Dry run summary
|
|
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
|
|
run: |
|
|
echo "## Dry Run Complete" >> $GITHUB_STEP_SUMMARY
|
|
echo "Build artifacts created successfully:" >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
|
ls -la release-assets/ >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
|
echo "### Checksums" >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
|
cat release-assets/checksums.sha256 >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Extract changelog from CHANGELOG.md
|
|
if: ${{ github.event_name == 'push' }}
|
|
id: changelog
|
|
run: |
|
|
# Extract version from tag (v2.7.2 -> 2.7.2)
|
|
VERSION=${GITHUB_REF_NAME#v}
|
|
CHANGELOG_FILE="CHANGELOG.md"
|
|
|
|
echo "📋 Extracting release notes for version $VERSION from CHANGELOG.md..."
|
|
|
|
if [ ! -f "$CHANGELOG_FILE" ]; then
|
|
echo "::warning::CHANGELOG.md not found, using minimal release notes"
|
|
echo "body=Release v$VERSION" >> $GITHUB_OUTPUT
|
|
exit 0
|
|
fi
|
|
|
|
# Extract changelog section for this version
|
|
# Looks for "## X.Y.Z" header and captures until next "## " or "---"
|
|
CHANGELOG_CONTENT=$(awk -v ver="$VERSION" '
|
|
BEGIN { found=0; content="" }
|
|
/^## / {
|
|
if (found) exit
|
|
# Match version at start of header (e.g., "## 2.7.3 -" or "## 2.7.3")
|
|
if ($2 == ver || $2 ~ "^"ver"[[:space:]]*-") {
|
|
found=1
|
|
next
|
|
}
|
|
}
|
|
/^---$/ { if (found) exit }
|
|
found { content = content $0 "\n" }
|
|
END {
|
|
if (!found) {
|
|
print "NOT_FOUND"
|
|
exit 0
|
|
}
|
|
# Trim leading/trailing whitespace
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", content)
|
|
print content
|
|
}
|
|
' "$CHANGELOG_FILE")
|
|
|
|
if [ "$CHANGELOG_CONTENT" = "NOT_FOUND" ] || [ -z "$CHANGELOG_CONTENT" ]; then
|
|
echo "::warning::Version $VERSION not found in CHANGELOG.md, using minimal release notes"
|
|
REPO="${{ github.repository }}"
|
|
CHANGELOG_CONTENT="Release v$VERSION"$'\n\n'"See [CHANGELOG.md](https://github.com/${REPO}/blob/main/CHANGELOG.md) for details."
|
|
fi
|
|
|
|
echo "✅ Extracted changelog content"
|
|
|
|
# Save to file first (more reliable for multiline)
|
|
echo "$CHANGELOG_CONTENT" > changelog-body.md
|
|
|
|
# Use file-based output for multiline content
|
|
{
|
|
echo "body<<CHANGELOG_EOF"
|
|
cat changelog-body.md
|
|
echo "CHANGELOG_EOF"
|
|
} >> $GITHUB_OUTPUT
|
|
|
|
- name: Create Release
|
|
if: ${{ github.event_name == 'push' }}
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
body: |
|
|
${{ steps.changelog.outputs.body }}
|
|
|
|
---
|
|
|
|
**Full Changelog**: https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md
|
|
|
|
_VirusTotal scan results will be added automatically after release._
|
|
files: release-assets/*
|
|
draft: false
|
|
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Update README with new version after successful release
|
|
update-readme:
|
|
needs: [create-release]
|
|
runs-on: ubuntu-latest
|
|
# Only update README on actual releases (tag push), not dry runs
|
|
if: ${{ github.event_name == 'push' }}
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: main
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract version and detect release type
|
|
id: version
|
|
run: |
|
|
# Extract version from tag (v2.7.2 -> 2.7.2)
|
|
VERSION=${GITHUB_REF_NAME#v}
|
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
|
|
# Detect if this is a prerelease (contains - after version, e.g., 2.7.2-beta.10)
|
|
if [[ "$VERSION" == *-* ]]; then
|
|
echo "is_prerelease=true" >> $GITHUB_OUTPUT
|
|
echo "Detected PRERELEASE: $VERSION"
|
|
else
|
|
echo "is_prerelease=false" >> $GITHUB_OUTPUT
|
|
echo "Detected STABLE release: $VERSION"
|
|
fi
|
|
|
|
- name: Update README.md
|
|
run: |
|
|
python3 << 'EOF'
|
|
import re
|
|
import sys
|
|
|
|
version = "${{ steps.version.outputs.version }}"
|
|
is_prerelease = "${{ steps.version.outputs.is_prerelease }}" == "true"
|
|
|
|
# Shields.io escapes hyphens as --
|
|
version_badge = version.replace("-", "--")
|
|
|
|
# Read README
|
|
with open("README.md", "r") as f:
|
|
content = f.read()
|
|
|
|
# Semver pattern: matches X.Y.Z or X.Y.Z-prerelease (e.g., 2.7.2, 2.7.2-beta.10)
|
|
# Prerelease MUST contain a dot (beta.10, alpha.1, rc.1) to avoid matching platform suffixes (win32, darwin)
|
|
semver = r'\d+\.\d+\.\d+(?:-[a-zA-Z]+\.[a-zA-Z0-9.]+)?'
|
|
# Shields.io escaped pattern (hyphens as --)
|
|
semver_badge = r'\d+\.\d+\.\d+(?:--[a-zA-Z]+\.[a-zA-Z0-9.]+)?'
|
|
|
|
def update_section(text, start_marker, end_marker, replacements):
|
|
"""Update content between markers with given replacements."""
|
|
pattern = f'({re.escape(start_marker)})(.*?)({re.escape(end_marker)})'
|
|
def replace_section(match):
|
|
section = match.group(2)
|
|
for old_pattern, new_value in replacements:
|
|
section = re.sub(old_pattern, new_value, section)
|
|
return match.group(1) + section + match.group(3)
|
|
return re.sub(pattern, replace_section, text, flags=re.DOTALL)
|
|
|
|
if is_prerelease:
|
|
print(f"Updating BETA section to {version} (badge: {version_badge})")
|
|
|
|
# Update beta badge
|
|
content = re.sub(
|
|
rf'beta-{semver_badge}-orange',
|
|
f'beta-{version_badge}-orange',
|
|
content
|
|
)
|
|
|
|
# Update beta version badge link
|
|
content = update_section(content,
|
|
'<!-- BETA_VERSION_BADGE -->', '<!-- BETA_VERSION_BADGE_END -->',
|
|
[(rf'tag/v{semver}\)', f'tag/v{version})')])
|
|
|
|
# Update beta downloads
|
|
content = update_section(content,
|
|
'<!-- BETA_DOWNLOADS -->', '<!-- BETA_DOWNLOADS_END -->',
|
|
[
|
|
(rf'Auto-Claude-{semver}', f'Auto-Claude-{version}'),
|
|
(rf'download/v{semver}/', f'download/v{version}/'),
|
|
])
|
|
else:
|
|
print(f"Updating STABLE section to {version} (badge: {version_badge})")
|
|
|
|
# Update top version badge
|
|
content = update_section(content,
|
|
'<!-- TOP_VERSION_BADGE -->', '<!-- TOP_VERSION_BADGE_END -->',
|
|
[
|
|
(rf'version-{semver_badge}-blue', f'version-{version_badge}-blue'),
|
|
(rf'tag/v{semver}\)', f'tag/v{version})'),
|
|
])
|
|
|
|
# Update stable badge
|
|
content = re.sub(
|
|
rf'stable-{semver_badge}-blue',
|
|
f'stable-{version_badge}-blue',
|
|
content
|
|
)
|
|
|
|
# Update stable version badge link
|
|
content = update_section(content,
|
|
'<!-- STABLE_VERSION_BADGE -->', '<!-- STABLE_VERSION_BADGE_END -->',
|
|
[(rf'tag/v{semver}\)', f'tag/v{version})')])
|
|
|
|
# Update stable downloads
|
|
content = update_section(content,
|
|
'<!-- STABLE_DOWNLOADS -->', '<!-- STABLE_DOWNLOADS_END -->',
|
|
[
|
|
(rf'Auto-Claude-{semver}', f'Auto-Claude-{version}'),
|
|
(rf'download/v{semver}/', f'download/v{version}/'),
|
|
])
|
|
|
|
# Write updated README
|
|
with open("README.md", "w") as f:
|
|
f.write(content)
|
|
|
|
print(f"README.md updated for {version} (prerelease={is_prerelease})")
|
|
EOF
|
|
|
|
echo "--- Verifying update ---"
|
|
grep -E "(stable-|beta-|version-)[0-9]" README.md | head -5
|
|
|
|
- name: Commit and push README update
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
# Check if there are changes to commit
|
|
if git diff --quiet README.md; then
|
|
echo "No changes to README.md, skipping commit"
|
|
exit 0
|
|
fi
|
|
|
|
git add README.md
|
|
git commit -m "docs: update README to v${{ steps.version.outputs.version }} [skip ci]"
|
|
git push origin main
|