fix: remove Windows ARM64 from build matrix (x86_64 runs fine via emu… #52
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Build (Enhanced with Updater) | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag/version to release (e.g. v2026.1.0 or v2026.1.0-beta.1)" | |
| required: true | |
| type: string | |
| build_arm: | |
| description: "Build ARM64 targets (Linux aarch64, Windows aarch64)?" | |
| required: false | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: write # Required for creating releases | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false # Don't cancel release builds | |
| jobs: | |
| create-release: | |
| name: Create Release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.get_version.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # Full history needed for changelog generation from commits | |
| - name: Get version from tag | |
| id: get_version | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| VERSION="${{ github.event.inputs.tag }}" | |
| else | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Building version: $VERSION" | |
| - name: Create Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.get_version.outputs.version }} | |
| run: | | |
| # Delete existing release with same tag if exists | |
| if gh release view "$VERSION" &> /dev/null; then | |
| echo "🗑️ Deleting existing release $VERSION..." | |
| gh release delete "$VERSION" --yes | |
| fi | |
| VERSION_NUMBER="${VERSION#v}" | |
| RELEASE_DATE=$(date -u +"%Y-%m-%d") | |
| # Extract the full changelog section for this version from CHANGELOG.md. | |
| # Include the ## [version] - date header so the format matches the local | |
| # release script (release-local.sh generate_changelog output). | |
| CHANGELOG_SECTION=$(awk "/^## \[$VERSION_NUMBER\]/{found=1} found && /^## \[/ && NR>1{exit} found" CHANGELOG.md 2>/dev/null || true) | |
| if [ -n "$CHANGELOG_SECTION" ]; then | |
| echo "$CHANGELOG_SECTION" > /tmp/release-notes.md | |
| else | |
| echo "⚠️ No changelog entry found for $VERSION_NUMBER in CHANGELOG.md" | |
| echo "📝 Generating release notes from git commits..." | |
| # Find the previous tag | |
| PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${VERSION}$" | head -n 1 || true) | |
| if [ -n "$PREV_TAG" ]; then | |
| COMMIT_RANGE="${PREV_TAG}..${VERSION}" | |
| else | |
| COMMIT_RANGE="${VERSION}" | |
| fi | |
| # Generate categorized changelog from conventional commits | |
| FEATURES=$(git log "$COMMIT_RANGE" --pretty=format:"- %s" --no-merges --grep="^feat" 2>/dev/null || true) | |
| FIXES=$(git log "$COMMIT_RANGE" --pretty=format:"- %s" --no-merges --grep="^fix" 2>/dev/null || true) | |
| OTHER=$(git log "$COMMIT_RANGE" --pretty=format:"- %s" --no-merges --grep="^chore\|^refactor\|^perf\|^docs\|^style\|^ci" 2>/dev/null || true) | |
| # Build notes matching local release format: version header + sections | |
| echo "## [$VERSION_NUMBER] - $RELEASE_DATE" > /tmp/release-notes.md | |
| echo "" >> /tmp/release-notes.md | |
| if [ -n "$FEATURES" ]; then | |
| printf "### New Features\n%s\n\n" "$FEATURES" >> /tmp/release-notes.md | |
| fi | |
| if [ -n "$FIXES" ]; then | |
| printf "### Bug Fixes\n%s\n\n" "$FIXES" >> /tmp/release-notes.md | |
| fi | |
| if [ -n "$OTHER" ]; then | |
| printf "### Other Changes\n%s\n" "$OTHER" >> /tmp/release-notes.md | |
| fi | |
| # If conventional commits didn't match, list all commits under a generic section | |
| if [ -z "$FEATURES" ] && [ -z "$FIXES" ] && [ -z "$OTHER" ]; then | |
| ALL=$(git log "$COMMIT_RANGE" --pretty=format:"- %s" --no-merges 2>/dev/null | head -30 || true) | |
| if [ -n "$ALL" ]; then | |
| printf "### Changes\n%s\n" "$ALL" >> /tmp/release-notes.md | |
| else | |
| echo "No notable changes in this release." >> /tmp/release-notes.md | |
| fi | |
| fi | |
| fi | |
| gh release create "$VERSION" \ | |
| --draft \ | |
| --title "Query Pilot $VERSION" \ | |
| --notes-file /tmp/release-notes.md | |
| setup-matrix: | |
| name: Setup Build Matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.set-matrix.outputs.matrix }} | |
| steps: | |
| - name: Set build matrix | |
| id: set-matrix | |
| run: | | |
| # Base matrix (always built) | |
| BASE='[ | |
| {"os":"macos-26","platform":"macos","target":"aarch64-apple-darwin","arch":"aarch64"}, | |
| {"os":"macos-26","platform":"macos","target":"x86_64-apple-darwin","arch":"x86_64"}, | |
| {"os":"ubuntu-latest","platform":"linux","target":"x86_64-unknown-linux-gnu","arch":"x86_64"}, | |
| {"os":"windows-latest","platform":"windows","target":"x86_64-pc-windows-msvc","arch":"x86_64"} | |
| ]' | |
| BUILD_ARM="${{ github.event.inputs.build_arm || 'true' }}" | |
| if [ "$BUILD_ARM" = "true" ]; then | |
| MATRIX=$(echo "$BASE" | jq '. + [ | |
| {"os":"ubuntu-24.04-arm","platform":"linux","target":"aarch64-unknown-linux-gnu","arch":"aarch64"} | |
| ]') | |
| else | |
| MATRIX="$BASE" | |
| fi | |
| echo "matrix=$(echo "$MATRIX" | jq -c .)" >> $GITHUB_OUTPUT | |
| echo "Building matrix:" | |
| echo "$MATRIX" | jq . | |
| build-release: | |
| name: Build (${{ matrix.platform }} ${{ matrix.arch }}) | |
| needs: [create-release, setup-matrix] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.setup-matrix.outputs.matrix) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| - name: Install Linux dependencies | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| pkg-config \ | |
| libwebkit2gtk-4.1-dev \ | |
| build-essential \ | |
| curl \ | |
| wget \ | |
| file \ | |
| libxdo-dev \ | |
| libssl-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| libglib2.0-dev \ | |
| libgtk-3-dev \ | |
| libfuse2 \ | |
| xdg-utils | |
| - name: Import GPG key for Linux signing | |
| if: matrix.platform == 'linux' | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| run: | | |
| if [ -z "$GPG_PRIVATE_KEY" ]; then | |
| echo "No GPG key configured, skipping signing" | |
| exit 0 | |
| fi | |
| echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| echo "GPG key imported for AppImage/RPM signing" | |
| - name: Import Windows certificate | |
| if: matrix.platform == 'windows' | |
| env: | |
| WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} | |
| WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} | |
| run: | | |
| if (-not $env:WINDOWS_CERTIFICATE) { | |
| Write-Host "No Windows certificate configured, skipping" | |
| exit 0 | |
| } | |
| New-Item -ItemType directory -Path certificate | |
| Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE | |
| certutil -decode certificate/tempCert.txt certificate/certificate.pfx | |
| Remove-Item -path certificate -include tempCert.txt | |
| Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText) | |
| Write-Host "Windows certificate imported" | |
| shell: powershell | |
| - name: Cleanup Windows certificate | |
| if: always() && matrix.platform == 'windows' | |
| shell: powershell | |
| run: Remove-Item -Recurse -Force -ErrorAction SilentlyContinue certificate | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-targets: true | |
| cache-on-failure: true | |
| cache-all-crates: true | |
| shared-key: "release-${{ matrix.target }}" | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: "pnpm" | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| # ============================================ | |
| # macOS Code Signing & Notarization Setup | |
| # ============================================ | |
| - name: Import Apple certificate | |
| if: matrix.platform == 'macos' | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: temporary_build_keychain_password | |
| run: | | |
| if [ -z "$APPLE_CERTIFICATE" ]; then | |
| echo "No Apple certificate configured, skipping signing setup" | |
| exit 0 | |
| fi | |
| # Create temporary keychain with password | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain | |
| security set-keychain-settings -t 3600 -u build.keychain | |
| # Decode and import certificate | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 | |
| security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| # Allow codesigning to access keychain | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain | |
| # Verify certificate was imported | |
| security find-identity -v -p codesigning build.keychain | |
| # Clean up | |
| rm certificate.p12 | |
| # ============================================ | |
| # Build querypilot CLI (required by externalBin) | |
| # ============================================ | |
| - name: Build querypilot CLI | |
| shell: bash | |
| run: | | |
| TARGET="${{ matrix.target }}" | |
| mkdir -p target/release | |
| cargo build --release --package querypilot --target "$TARGET" | |
| # Determine binary name (Windows adds .exe) | |
| if [[ "$TARGET" == *windows* ]]; then | |
| BIN_NAME="querypilot.exe" | |
| else | |
| BIN_NAME="querypilot" | |
| fi | |
| # Tauri sidecar on Windows requires the .exe suffix on the renamed binary | |
| if [[ "$TARGET" == *windows* ]]; then | |
| cp "target/$TARGET/release/$BIN_NAME" "target/release/querypilot-$TARGET.exe" | |
| EXPECTED_PATH="target/release/querypilot-$TARGET.exe" | |
| else | |
| cp "target/$TARGET/release/$BIN_NAME" "target/release/querypilot-$TARGET" | |
| chmod 755 "target/release/querypilot-$TARGET" | |
| EXPECTED_PATH="target/release/querypilot-$TARGET" | |
| fi | |
| if [ ! -f "$EXPECTED_PATH" ]; then | |
| echo "❌ ERROR: Missing querypilot CLI artifact: $EXPECTED_PATH" | |
| exit 1 | |
| fi | |
| # Smoke test (skip on Windows — cross-shell piping issues) | |
| if [[ "$TARGET" != *windows* ]]; then | |
| if [ ! -x "$EXPECTED_PATH" ]; then | |
| echo "❌ ERROR: querypilot CLI is not executable: $EXPECTED_PATH" | |
| exit 1 | |
| fi | |
| # Smoke test: verify binary parses input and returns structured JSON. | |
| # ok=false is expected (no app running in CI); we just check the envelope fields. | |
| # Use || true to suppress the sidecar's exit code 1 (connection failure), | |
| # since pipefail would otherwise fail the step before python3 validates. | |
| SMOKE_REQUEST='{"version":"1","requestId":"ci-smoke","params":{}}' | |
| SMOKE_OUTPUT=$(printf '%s' "$SMOKE_REQUEST" | "$EXPECTED_PATH" agent workspace.listTabs 2>/dev/null || true) | |
| echo "$SMOKE_OUTPUT" | python3 -c 'import json,sys; o=json.load(sys.stdin); assert o.get("requestId")=="ci-smoke", f"bad requestId: {o}"; assert o.get("capability")=="workspace.listTabs", f"bad capability: {o}"' | |
| fi | |
| ls -la target/release/querypilot* | |
| echo "✅ querypilot CLI built for $TARGET" | |
| # ============================================ | |
| # Build Tauri App | |
| # ============================================ | |
| - name: Build Tauri app (macOS with signing) | |
| if: matrix.platform == 'macos' | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| VITE_DISABLE_SOURCEMAPS: "true" | |
| APPLE_ID: ${{ secrets.APPLE_DEVELOPER_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} | |
| # Sentry Configuration (optional - only used if secrets are set) | |
| # Single DSN for all components (Frontend, Backend, Sidecar) | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| # SENTRY_AUTH_TOKEN disabled - source maps upload causes OOM on M1 runners | |
| # SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG || 'query-pilot' }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT || 'query-pilot' }} | |
| shell: bash | |
| run: | | |
| # Build frontend with Sentry source maps (if SENTRY_AUTH_TOKEN is set) | |
| if [ -n "$SENTRY_AUTH_TOKEN" ]; then | |
| echo "🔍 Sentry configured - source maps will be uploaded" | |
| else | |
| echo "⚠️ Sentry not configured - telemetry disabled" | |
| fi | |
| # Build Tauri with telemetry feature if Sentry DSN is configured | |
| if [ -n "$SENTRY_DSN" ]; then | |
| echo "🚀 Building with telemetry feature enabled" | |
| pnpm tauri build --target ${{ matrix.target }} -- --features telemetry | |
| else | |
| echo "🚀 Building without telemetry" | |
| pnpm tauri build --target ${{ matrix.target }} | |
| fi | |
| - name: Cleanup macOS keychain | |
| if: always() && matrix.platform == 'macos' | |
| run: | | |
| security delete-keychain build.keychain || true | |
| rm -f certificate.p12 || true | |
| - name: Build Tauri app (Linux) | |
| if: matrix.platform == 'linux' | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| VITE_DISABLE_SOURCEMAPS: "true" | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} | |
| # AppImage GPG signing | |
| SIGN: ${{ secrets.GPG_PRIVATE_KEY != '' && '1' || '' }} | |
| SIGN_KEY: ${{ secrets.GPG_KEY_ID }} | |
| APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| APPIMAGETOOL_FORCE_SIGN: ${{ secrets.GPG_PRIVATE_KEY != '' && '1' || '' }} | |
| # RPM GPG signing | |
| TAURI_SIGNING_RPM_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| TAURI_SIGNING_RPM_KEY_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| # Sentry | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| shell: bash | |
| run: | | |
| if [ -n "$SENTRY_DSN" ]; then | |
| echo "🚀 Building with telemetry feature enabled" | |
| pnpm tauri build --target ${{ matrix.target }} -- --features telemetry | |
| else | |
| echo "🚀 Building without telemetry" | |
| pnpm tauri build --target ${{ matrix.target }} | |
| fi | |
| - name: Build Tauri app (Windows) | |
| if: matrix.platform == 'windows' | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| VITE_DISABLE_SOURCEMAPS: "true" | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| shell: bash | |
| run: | | |
| if [ -n "$SENTRY_DSN" ]; then | |
| echo "🚀 Building with telemetry feature enabled" | |
| pnpm tauri build --target ${{ matrix.target }} -- --features telemetry | |
| else | |
| echo "🚀 Building without telemetry" | |
| pnpm tauri build --target ${{ matrix.target }} | |
| fi | |
| # ============================================ | |
| # Upload Artifacts to Release | |
| # ============================================ | |
| - name: Find and upload macOS artifacts | |
| if: matrix.platform == 'macos' | |
| run: | | |
| VERSION="${{ needs.create-release.outputs.version }}" | |
| ARCH="${{ matrix.arch }}" | |
| DMG_PATH=$(find target/${{ matrix.target }}/release/bundle/dmg -name "*.dmg" | head -n 1) | |
| UPDATER_ARCHIVE=$(find target/${{ matrix.target }}/release/bundle -name "*.app.tar.gz" | head -n 1) | |
| UPDATER_SIG=$(find target/${{ matrix.target }}/release/bundle -name "*.app.tar.gz.sig" | head -n 1) | |
| if [ -z "$DMG_PATH" ]; then | |
| echo "❌ ERROR: No DMG file found!" | |
| exit 1 | |
| fi | |
| if [ -z "$UPDATER_ARCHIVE" ] || [ -z "$UPDATER_SIG" ]; then | |
| echo "❌ ERROR: Missing updater archive/signature (.app.tar.gz + .sig)" | |
| exit 1 | |
| fi | |
| # Name artifacts with arch suffix | |
| DMG_NAME="QueryPilot_${VERSION}_${ARCH}.dmg" | |
| ARCHIVE_NAME="QueryPilot_${VERSION}_${ARCH}.app.tar.gz" | |
| SIG_NAME="QueryPilot_${VERSION}_${ARCH}.app.tar.gz.sig" | |
| mv "$DMG_PATH" "$DMG_NAME" | |
| cp "$UPDATER_ARCHIVE" "$ARCHIVE_NAME" | |
| cp "$UPDATER_SIG" "$SIG_NAME" | |
| echo "🚀 Uploading: $DMG_NAME, $ARCHIVE_NAME, $SIG_NAME" | |
| gh release upload "$VERSION" "$DMG_NAME" "$ARCHIVE_NAME" "$SIG_NAME" --clobber | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Find and upload Linux artifacts | |
| if: matrix.platform == 'linux' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.create-release.outputs.version }}" | |
| ARCH="${{ matrix.arch }}" | |
| # Find AppImage | |
| APPIMAGE_PATH=$(find target/${{ matrix.target }}/release/bundle/appimage -name "*.AppImage" | head -n 1) | |
| # Find .deb | |
| DEB_PATH=$(find target/${{ matrix.target }}/release/bundle/deb -name "*.deb" | head -n 1) | |
| # Find .rpm | |
| RPM_PATH=$(find target/${{ matrix.target }}/release/bundle/rpm -name "*.rpm" 2>/dev/null | head -n 1) | |
| # Find updater archives | |
| UPDATER_ARCHIVE=$(find target/${{ matrix.target }}/release/bundle -name "*.AppImage.tar.gz" | head -n 1) | |
| UPDATER_SIG=$(find target/${{ matrix.target }}/release/bundle -name "*.AppImage.tar.gz.sig" | head -n 1) | |
| if [ -z "$APPIMAGE_PATH" ]; then | |
| echo "❌ ERROR: No AppImage file found!" | |
| exit 1 | |
| fi | |
| # Name artifacts | |
| APPIMAGE_NAME="QueryPilot_${VERSION}_${ARCH}.AppImage" | |
| DEB_NAME="QueryPilot_${VERSION}_${ARCH}.deb" | |
| RPM_NAME="QueryPilot_${VERSION}_${ARCH}.rpm" | |
| mv "$APPIMAGE_PATH" "$APPIMAGE_NAME" | |
| UPLOAD_FILES="$APPIMAGE_NAME" | |
| if [ -n "$DEB_PATH" ]; then | |
| mv "$DEB_PATH" "$DEB_NAME" | |
| UPLOAD_FILES="$UPLOAD_FILES $DEB_NAME" | |
| fi | |
| if [ -n "$RPM_PATH" ]; then | |
| mv "$RPM_PATH" "$RPM_NAME" | |
| UPLOAD_FILES="$UPLOAD_FILES $RPM_NAME" | |
| fi | |
| if [ -n "$UPDATER_ARCHIVE" ] && [ -n "$UPDATER_SIG" ]; then | |
| ARCHIVE_NAME="QueryPilot_${VERSION}_${ARCH}.AppImage.tar.gz" | |
| SIG_NAME="QueryPilot_${VERSION}_${ARCH}.AppImage.tar.gz.sig" | |
| cp "$UPDATER_ARCHIVE" "$ARCHIVE_NAME" | |
| cp "$UPDATER_SIG" "$SIG_NAME" | |
| UPLOAD_FILES="$UPLOAD_FILES $ARCHIVE_NAME $SIG_NAME" | |
| fi | |
| echo "🚀 Uploading: $UPLOAD_FILES" | |
| gh release upload "$VERSION" $UPLOAD_FILES --clobber | |
| - name: Find and upload Windows artifacts | |
| if: matrix.platform == 'windows' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| VERSION="${{ needs.create-release.outputs.version }}" | |
| ARCH="${{ matrix.arch }}" | |
| # Find NSIS installer | |
| NSIS_PATH=$(find target/${{ matrix.target }}/release/bundle/nsis -name "*.exe" | head -n 1) | |
| # Find updater archives | |
| UPDATER_ARCHIVE=$(find target/${{ matrix.target }}/release/bundle -name "*.nsis.zip" | head -n 1) | |
| UPDATER_SIG=$(find target/${{ matrix.target }}/release/bundle -name "*.nsis.zip.sig" | head -n 1) | |
| if [ -z "$NSIS_PATH" ]; then | |
| echo "❌ ERROR: No NSIS installer found!" | |
| exit 1 | |
| fi | |
| # Name artifacts | |
| NSIS_NAME="QueryPilot_${VERSION}_${ARCH}-setup.exe" | |
| mv "$NSIS_PATH" "$NSIS_NAME" | |
| UPLOAD_FILES="$NSIS_NAME" | |
| if [ -n "$UPDATER_ARCHIVE" ] && [ -n "$UPDATER_SIG" ]; then | |
| ARCHIVE_NAME="QueryPilot_${VERSION}_${ARCH}.nsis.zip" | |
| SIG_NAME="QueryPilot_${VERSION}_${ARCH}.nsis.zip.sig" | |
| cp "$UPDATER_ARCHIVE" "$ARCHIVE_NAME" | |
| cp "$UPDATER_SIG" "$SIG_NAME" | |
| UPLOAD_FILES="$UPLOAD_FILES $ARCHIVE_NAME $SIG_NAME" | |
| fi | |
| echo "🚀 Uploading: $UPLOAD_FILES" | |
| gh release upload "$VERSION" $UPLOAD_FILES --clobber | |
| - name: Upload build artifacts (backup) | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: app-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: | | |
| target/${{ matrix.target }}/release/bundle/ | |
| retention-days: 3 | |
| generate-updater-manifest: | |
| name: Generate Updater Manifest | |
| needs: [create-release, build-release] | |
| runs-on: ubuntu-latest | |
| if: success() | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Download release assets | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.create-release.outputs.version }} | |
| run: | | |
| echo "📥 Downloading release assets..." | |
| # macOS updater archives (required — fail fast if missing) | |
| gh release download "$VERSION" --pattern "*.app.tar.gz" | |
| gh release download "$VERSION" --pattern "*.app.tar.gz.sig" | |
| # Linux and Windows updater archives (optional — present only if those builds ran) | |
| gh release download "$VERSION" --pattern "*.AppImage.tar.gz" || true | |
| gh release download "$VERSION" --pattern "*.AppImage.tar.gz.sig" || true | |
| gh release download "$VERSION" --pattern "*.nsis.zip" || true | |
| gh release download "$VERSION" --pattern "*.nsis.zip.sig" || true | |
| echo "📦 Downloaded assets:" | |
| ls -lh *.tar.gz *.tar.gz.sig *.nsis.zip *.nsis.zip.sig 2>/dev/null || true | |
| - name: Generate update manifest | |
| env: | |
| VERSION: ${{ needs.create-release.outputs.version }} | |
| run: | | |
| echo "📄 Generating update manifest..." | |
| VERSION_NUMBER="${VERSION#v}" | |
| PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | |
| # Extract release notes body from CHANGELOG.md (body only, no version header — | |
| # matches release-local.sh generate_manifest which uses the same awk pattern) | |
| RAW_NOTES=$(awk "/^## \[$VERSION_NUMBER\]/{found=1;next} found && /^## \[/{exit} found" CHANGELOG.md 2>/dev/null | sed '/^[[:space:]]*$/d' || true) | |
| # Fallback: generate from git commits (matches release-local.sh fallback) | |
| if [ -z "$RAW_NOTES" ]; then | |
| PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${VERSION}$" | head -n 1 || true) | |
| if [ -n "$PREV_TAG" ]; then | |
| FEAT=$(git log "${PREV_TAG}..${VERSION}" --pretty=format:"- %s" --no-merges --grep="^feat" 2>/dev/null || true) | |
| BUGF=$(git log "${PREV_TAG}..${VERSION}" --pretty=format:"- %s" --no-merges --grep="^fix" 2>/dev/null || true) | |
| [ -n "$FEAT" ] && RAW_NOTES="$(printf '### New Features\n%s' "$FEAT")" | |
| [ -n "$BUGF" ] && RAW_NOTES="$(printf '%s\n### Bug Fixes\n%s' "$RAW_NOTES" "$BUGF")" | |
| [ -z "$RAW_NOTES" ] && RAW_NOTES=$(git log "${PREV_TAG}..${VERSION}" --pretty=format:"- %s" --no-merges 2>/dev/null | head -20 || true) | |
| fi | |
| fi | |
| if [ -z "$RAW_NOTES" ]; then | |
| RAW_NOTES="See CHANGELOG.md for details" | |
| fi | |
| BASE_URL="https://github.com/${{ github.repository }}/releases/download/$VERSION" | |
| # macOS artifacts (required) | |
| AARCH64_ARCHIVE="QueryPilot_${VERSION}_aarch64.app.tar.gz" | |
| AARCH64_SIG="QueryPilot_${VERSION}_aarch64.app.tar.gz.sig" | |
| X86_64_ARCHIVE="QueryPilot_${VERSION}_x86_64.app.tar.gz" | |
| X86_64_SIG="QueryPilot_${VERSION}_x86_64.app.tar.gz.sig" | |
| for f in "$AARCH64_ARCHIVE" "$AARCH64_SIG" "$X86_64_ARCHIVE" "$X86_64_SIG"; do | |
| if [ ! -f "$f" ]; then | |
| echo "❌ Missing macOS updater asset: $f" | |
| exit 1 | |
| fi | |
| done | |
| AARCH64_SIGNATURE=$(tr -d '\r\n' < "$AARCH64_SIG") | |
| X86_64_SIGNATURE=$(tr -d '\r\n' < "$X86_64_SIG") | |
| # Linux artifacts (optional) | |
| LINUX_X86_64_ARCHIVE="QueryPilot_${VERSION}_x86_64.AppImage.tar.gz" | |
| LINUX_X86_64_SIG="QueryPilot_${VERSION}_x86_64.AppImage.tar.gz.sig" | |
| INCLUDE_LINUX="false" | |
| if [ -f "$LINUX_X86_64_ARCHIVE" ] && [ -f "$LINUX_X86_64_SIG" ]; then | |
| INCLUDE_LINUX="true" | |
| LINUX_X86_64_SIGNATURE=$(tr -d '\r\n' < "$LINUX_X86_64_SIG") | |
| fi | |
| # Windows artifacts (optional) | |
| WINDOWS_X86_64_ARCHIVE="QueryPilot_${VERSION}_x86_64.nsis.zip" | |
| WINDOWS_X86_64_SIG="QueryPilot_${VERSION}_x86_64.nsis.zip.sig" | |
| INCLUDE_WINDOWS="false" | |
| if [ -f "$WINDOWS_X86_64_ARCHIVE" ] && [ -f "$WINDOWS_X86_64_SIG" ]; then | |
| INCLUDE_WINDOWS="true" | |
| WINDOWS_X86_64_SIGNATURE=$(tr -d '\r\n' < "$WINDOWS_X86_64_SIG") | |
| fi | |
| # Build manifest with jq, adding platforms incrementally | |
| MANIFEST=$(jq -n \ | |
| --arg version "$VERSION_NUMBER" \ | |
| --arg notes "$RAW_NOTES" \ | |
| --arg pub_date "$PUB_DATE" \ | |
| --arg aarch64_sig "$AARCH64_SIGNATURE" \ | |
| --arg aarch64_url "$BASE_URL/$AARCH64_ARCHIVE" \ | |
| --arg x86_64_sig "$X86_64_SIGNATURE" \ | |
| --arg x86_64_url "$BASE_URL/$X86_64_ARCHIVE" \ | |
| '{ | |
| version: $version, | |
| notes: $notes, | |
| pub_date: $pub_date, | |
| platforms: { | |
| "darwin-aarch64": { signature: $aarch64_sig, url: $aarch64_url }, | |
| "darwin-x86_64": { signature: $x86_64_sig, url: $x86_64_url } | |
| } | |
| }') | |
| if [ "$INCLUDE_LINUX" = "true" ]; then | |
| MANIFEST=$(echo "$MANIFEST" | jq \ | |
| --arg sig "$LINUX_X86_64_SIGNATURE" \ | |
| --arg url "$BASE_URL/$LINUX_X86_64_ARCHIVE" \ | |
| '.platforms["linux-x86_64"] = { signature: $sig, url: $url }') | |
| fi | |
| if [ "$INCLUDE_WINDOWS" = "true" ]; then | |
| MANIFEST=$(echo "$MANIFEST" | jq \ | |
| --arg sig "$WINDOWS_X86_64_SIGNATURE" \ | |
| --arg url "$BASE_URL/$WINDOWS_X86_64_ARCHIVE" \ | |
| '.platforms["windows-x86_64"] = { signature: $sig, url: $url }') | |
| fi | |
| # Linux aarch64 artifacts (optional) | |
| LINUX_AARCH64_ARCHIVE="QueryPilot_${VERSION}_aarch64.AppImage.tar.gz" | |
| LINUX_AARCH64_SIG="QueryPilot_${VERSION}_aarch64.AppImage.tar.gz.sig" | |
| INCLUDE_LINUX_AARCH64="false" | |
| if [ -f "$LINUX_AARCH64_ARCHIVE" ] && [ -f "$LINUX_AARCH64_SIG" ]; then | |
| INCLUDE_LINUX_AARCH64="true" | |
| LINUX_AARCH64_SIGNATURE=$(tr -d '\r\n' < "$LINUX_AARCH64_SIG") | |
| fi | |
| if [ "$INCLUDE_LINUX_AARCH64" = "true" ]; then | |
| MANIFEST=$(echo "$MANIFEST" | jq \ | |
| --arg sig "$LINUX_AARCH64_SIGNATURE" \ | |
| --arg url "$BASE_URL/$LINUX_AARCH64_ARCHIVE" \ | |
| '.platforms["linux-aarch64"] = { signature: $sig, url: $url }') | |
| fi | |
| echo "$MANIFEST" > latest.json | |
| echo "✓ Generated updater manifest (linux_x86_64=$INCLUDE_LINUX, linux_aarch64=$INCLUDE_LINUX_AARCH64, windows_x86_64=$INCLUDE_WINDOWS)" | |
| cat latest.json | |
| - name: Upload manifest to source release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.create-release.outputs.version }} | |
| run: | | |
| echo 'Uploading update manifest to source release...' | |
| gh release upload "$VERSION" latest.json --clobber | |
| echo 'Uploaded to release' | |
| - name: Upload manifest as artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: update-manifest | |
| path: latest.json | |
| retention-days: 90 | |
| finalize-release: | |
| name: Finalize Release | |
| needs: [create-release, build-release, generate-updater-manifest] | |
| runs-on: ubuntu-latest | |
| if: | | |
| always() && | |
| needs.build-release.result == 'success' && | |
| needs.generate-updater-manifest.result == 'success' | |
| steps: | |
| - name: Publish draft release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.create-release.outputs.version }} | |
| run: | | |
| echo "Publishing draft release $VERSION..." | |
| gh release edit "$VERSION" --draft=false --repo "${{ github.repository }}" | |
| echo "Release $VERSION published!" | |
| - name: Release summary | |
| run: | | |
| echo "🎉 All builds completed successfully!" | |
| echo "" | |
| echo "Release: ${{ needs.create-release.outputs.version }}" | |
| echo "📦 https://github.com/${{ github.repository }}/releases" |