From 2f5a6aaddf45bb91a2dd7e6a9b3e6045b1b2ea91 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:53:52 -0600 Subject: [PATCH] ci(updater): sign artifacts and consolidate updater manifest Adds TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) env to release-linux, release-linux-aarch64, and release-macos build steps, collects the resulting .sig files into the release artifacts, and adds a new release-updater-manifest workflow that waits for all platform sig files to land on the release and assembles latest.json for the updater endpoint. Cherry-picked from #305 by @afonsojramos. cc --- .github/workflows/release-linux-aarch64.yml | 4 + .github/workflows/release-linux.yml | 4 + .github/workflows/release-macos.yml | 11 +- .../workflows/release-updater-manifest.yml | 170 ++++++++++++++++++ 4 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release-updater-manifest.yml diff --git a/.github/workflows/release-linux-aarch64.yml b/.github/workflows/release-linux-aarch64.yml index 79940eda9..fd4168091 100644 --- a/.github/workflows/release-linux-aarch64.yml +++ b/.github/workflows/release-linux-aarch64.yml @@ -71,6 +71,9 @@ jobs: run: npm run build - name: Build Tauri (deb + rpm + appimage) + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: npm run tauri build -- --bundles deb,rpm,appimage - name: Collect artifacts @@ -81,6 +84,7 @@ jobs: cp -v src-tauri/target/release/bundle/deb/*.deb dist/ || true cp -v src-tauri/target/release/bundle/rpm/*.rpm dist/ || true cp -v src-tauri/target/release/bundle/appimage/*.AppImage dist/ || true + cp -v src-tauri/target/release/bundle/appimage/*.AppImage.sig dist/ || true # Create AUR tarball ARCH=aarch64 diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index a641cebfc..f5bc88160 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -99,6 +99,9 @@ jobs: run: npm ci - name: Build DEB/RPM/AppImage + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: npm run tauri build -- --bundles deb,rpm,appimage - name: Collect artifacts @@ -110,6 +113,7 @@ jobs: cp -v src-tauri/target/release/bundle/deb/*.deb dist/ || true cp -v src-tauri/target/release/bundle/rpm/*.rpm dist/ || true cp -v src-tauri/target/release/bundle/appimage/*.AppImage dist/ || true + cp -v src-tauri/target/release/bundle/appimage/*.AppImage.sig dist/ || true # Create tarball for AUR with proper structure TARBALL_DIR="qbz_${VERSION}_amd64" diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index ac62a9761..6aaf93648 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -95,13 +95,20 @@ jobs: - name: Install JS dependencies run: npm ci - - name: Build DMG - run: npm run tauri build -- --target ${{ matrix.target }} --bundles dmg + - name: Build macOS bundles + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: npm run tauri build -- --target ${{ matrix.target }} - name: Collect artifacts run: | mkdir -p dist + # DMG for manual distribution cp -v src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg dist/ || true + # .app.tar.gz + .sig for auto-updater + cp -v src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz dist/ || true + cp -v src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig dist/ || true ls -la dist - name: Upload artifacts diff --git a/.github/workflows/release-updater-manifest.yml b/.github/workflows/release-updater-manifest.yml new file mode 100644 index 000000000..c9aece83a --- /dev/null +++ b/.github/workflows/release-updater-manifest.yml @@ -0,0 +1,170 @@ +name: Generate Updater Manifest + +on: + workflow_run: + workflows: + - release-macos + - release-linux + - "Linux ARM64 (aarch64) Build" + types: + - completed + +concurrency: + group: updater-manifest-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + +jobs: + generate-manifest: + # Only run on successful tag-triggered builds (not manual dispatches) + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Resolve version from tag + id: version + run: | + TAG="${{ github.event.workflow_run.head_branch }}" + VERSION="${TAG#v}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Wait for all platform builds to finish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + run: | + # Wait up to 20 minutes for .sig files from all platforms + REQUIRED_SIGS=("aarch64.app.tar.gz.sig" "x64.app.tar.gz.sig" "AppImage.sig") + MAX_ATTEMPTS=40 + SLEEP_SECONDS=30 + + for attempt in $(seq 1 $MAX_ATTEMPTS); do + ASSETS=$(gh release view "$TAG" --repo "${{ github.repository }}" --json assets -q '.assets[].name' 2>/dev/null || echo "") + + FOUND_ALL=true + for sig_suffix in "${REQUIRED_SIGS[@]}"; do + if ! echo "$ASSETS" | grep -q "$sig_suffix"; then + FOUND_ALL=false + break + fi + done + + if [ "$FOUND_ALL" = true ]; then + echo "All signature files found on attempt $attempt" + break + fi + + if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then + echo "Warning: Not all .sig files found after $MAX_ATTEMPTS attempts. Generating manifest with available assets." + break + fi + + echo "Attempt $attempt/$MAX_ATTEMPTS: waiting for all .sig files..." + sleep $SLEEP_SECONDS + done + + - name: Generate latest.json + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + VERSION: ${{ steps.version.outputs.version }} + run: | + REPO="${{ github.repository }}" + RELEASE_URL="https://github.com/${REPO}/releases/download/${TAG}" + + # Get release info + RELEASE_JSON=$(gh release view "$TAG" --repo "$REPO" --json body,publishedAt) + NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // ""') + PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.publishedAt // ""') + + # Download all .sig files + mkdir -p sigs + ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name') + + for asset in $ASSETS; do + if [[ "$asset" == *.sig ]]; then + gh release download "$TAG" --repo "$REPO" --pattern "$asset" --dir sigs + fi + done + + ls -la sigs/ + + # Build platform entries + # Helper: read sig content for a glob pattern, return empty string if not found + read_sig() { + local sig_file + for pattern in "$@"; do + sig_file=$(find sigs -name "$pattern" -print -quit 2>/dev/null) + if [ -n "$sig_file" ] && [ -f "$sig_file" ]; then + cat "$sig_file" + return + fi + done + echo "" + } + + # Find asset names matching patterns (extended regex) + find_asset() { + local pattern="$1" + echo "$ASSETS" | grep -E "$pattern" | head -1 + } + + # Build platforms JSON + PLATFORMS="{}" + + # macOS aarch64 (.app.tar.gz for updater) + MACOS_ARM_TAR=$(find_asset "aarch64.app.tar.gz$") + MACOS_ARM_SIG=$(read_sig "*aarch64.app.tar.gz.sig") + if [ -n "$MACOS_ARM_TAR" ] && [ -n "$MACOS_ARM_SIG" ]; then + PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${MACOS_ARM_TAR}" --arg sig "$MACOS_ARM_SIG" \ + '. + {"darwin-aarch64": {"signature": $sig, "url": $url}}') + fi + + # macOS x86_64 (.app.tar.gz for updater) + MACOS_X64_TAR=$(find_asset "(x64|x86_64).app.tar.gz$") + MACOS_X64_SIG=$(read_sig "*x64.app.tar.gz.sig" "*x86_64.app.tar.gz.sig") + if [ -n "$MACOS_X64_TAR" ] && [ -n "$MACOS_X64_SIG" ]; then + PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${MACOS_X64_TAR}" --arg sig "$MACOS_X64_SIG" \ + '. + {"darwin-x86_64": {"signature": $sig, "url": $url}}') + fi + + # Linux x86_64 AppImage + LINUX_X64_APPIMAGE=$(find_asset "(amd64|x86_64).AppImage$") + LINUX_X64_SIG=$(read_sig "*amd64.AppImage.sig" "*x86_64.AppImage.sig") + if [ -n "$LINUX_X64_APPIMAGE" ] && [ -n "$LINUX_X64_SIG" ]; then + PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${LINUX_X64_APPIMAGE}" --arg sig "$LINUX_X64_SIG" \ + '. + {"linux-x86_64": {"signature": $sig, "url": $url}}') + fi + + # Linux aarch64 AppImage + LINUX_ARM_APPIMAGE=$(find_asset "aarch64.AppImage$") + LINUX_ARM_SIG=$(read_sig "*aarch64.AppImage.sig") + if [ -n "$LINUX_ARM_APPIMAGE" ] && [ -n "$LINUX_ARM_SIG" ]; then + PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${LINUX_ARM_APPIMAGE}" --arg sig "$LINUX_ARM_SIG" \ + '. + {"linux-aarch64": {"signature": $sig, "url": $url}}') + fi + + # Assemble final latest.json + jq -n \ + --arg version "$VERSION" \ + --arg notes "$NOTES" \ + --arg pub_date "$PUB_DATE" \ + --argjson platforms "$PLATFORMS" \ + '{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms}' \ + > latest.json + + echo "Generated latest.json:" + cat latest.json + + - name: Upload latest.json to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + run: | + # Delete existing latest.json if present (from a previous run) + gh release delete-asset "$TAG" latest.json --repo "${{ github.repository }}" --yes 2>/dev/null || true + # Upload the new one + gh release upload "$TAG" latest.json --repo "${{ github.repository }}"