Skip to content

fix: remove Windows ARM64 from build matrix (x86_64 runs fine via emu… #52

fix: remove Windows ARM64 from build matrix (x86_64 runs fine via emu…

fix: remove Windows ARM64 from build matrix (x86_64 runs fine via emu… #52

Workflow file for this run

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"