fix(release): use update.json for in-app updates (v0.3.4) (#76) #17
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 | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| build: | |
| name: build (${{ matrix.target }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install hidapi build deps (Linux) | |
| if: runner.os == 'Linux' | |
| run: sudo apt-get update && sudo apt-get install -y libudev-dev libhidapi-dev | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build | |
| run: cargo build --release -p microbridged -p microbridgectl --target ${{ matrix.target }} | |
| - name: Package | |
| run: | | |
| STAGE=microbridge-${{ github.ref_name }}-${{ matrix.target }} | |
| mkdir -p "staging/${STAGE}" | |
| cp "target/${{ matrix.target }}/release/microbridged" "staging/${STAGE}/" | |
| cp "target/${{ matrix.target }}/release/microbridgectl" "staging/${STAGE}/" | |
| cp README.md LICENSE INSTALL.md "staging/${STAGE}/" | |
| tar -C staging -czf "${STAGE}.tar.gz" "${STAGE}" | |
| echo "ASSET=${STAGE}.tar.gz" >> "$GITHUB_ENV" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: binaries-${{ matrix.target }} | |
| path: ${{ env.ASSET }} | |
| build-ui-macos: | |
| name: build menu bar app (${{ matrix.target }}) | |
| runs-on: macos-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [aarch64-apple-darwin, x86_64-apple-darwin] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: "apps/microbridge-ui/src-tauri -> target" | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: apps/microbridge-ui/package-lock.json | |
| - name: Import Developer ID certificate | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then | |
| echo "APPLE_CERTIFICATE secret missing — UI build will be unsigned" | |
| echo "SIGNED=0" >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| echo "SIGNED=1" >> "$GITHUB_ENV" | |
| CERT_PATH="$RUNNER_TEMP/certificate.p12" | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH" | |
| 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 | |
| security import "$CERT_PATH" -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain | |
| security list-keychains -d user -s build.keychain $(security list-keychains -d user | sed -e 's/"//g') | |
| IDENTITY="${{ secrets.APPLE_SIGNING_IDENTITY }}" | |
| if [[ -z "$IDENTITY" ]]; then | |
| IDENTITY="$(security find-identity -v -p codesigning build.keychain \ | |
| | awk -F'"' '/Developer ID Application/{print $2; exit}')" | |
| fi | |
| test -n "$IDENTITY" | |
| echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" | |
| security find-identity -v -p codesigning build.keychain | |
| - name: Prepare App Store Connect API key for notarization | |
| if: env.SIGNED == '1' | |
| env: | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| test -n "${APPLE_API_KEY:-}" | |
| test -n "${APPLE_API_ISSUER:-}" | |
| test -n "${APPLE_API_KEY_P8:-}" | |
| KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" | |
| printf '%s\n' "$APPLE_API_KEY_P8" > "$KEY_PATH" | |
| chmod 600 "$KEY_PATH" | |
| { | |
| echo "APPLE_API_KEY=$APPLE_API_KEY" | |
| echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" | |
| echo "APPLE_API_KEY_PATH=$KEY_PATH" | |
| echo "APPLE_TEAM_ID=${APPLE_TEAM_ID:-3NQG568C4Q}" | |
| } >> "$GITHUB_ENV" | |
| - name: Build signed Microbridge.app + DMG | |
| working-directory: apps/microbridge-ui | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} | |
| APPLE_API_KEY: ${{ env.APPLE_API_KEY }} | |
| APPLE_API_ISSUER: ${{ env.APPLE_API_ISSUER }} | |
| APPLE_API_KEY_PATH: ${{ env.APPLE_API_KEY_PATH }} | |
| APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| npm ci | |
| # Bundle the daemon inside direct-download apps so the device twin | |
| # always has a real local source of truth without a separate install. | |
| cargo build --manifest-path ../../Cargo.toml --release -p microbridged -p microbridgectl --target ${{ matrix.target }} | |
| mkdir -p src-tauri/binaries | |
| cp "../../target/${{ matrix.target }}/release/microbridged" \ | |
| "src-tauri/binaries/microbridged-${{ matrix.target }}" | |
| cp "../../target/${{ matrix.target }}/release/microbridgectl" \ | |
| "src-tauri/binaries/microbridgectl-${{ matrix.target }}" | |
| # createUpdaterArtifacts (tauri.conf.json) requires the updater signing | |
| # key. Forks without the secret still build — just without in-app | |
| # updater artifacts (they can use the DMG / Homebrew). | |
| UPDATER_OFF=() | |
| if [[ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]]; then | |
| echo "TAURI_SIGNING_PRIVATE_KEY missing — building without updater artifacts" | |
| UPDATER_OFF=(--config '{"bundle":{"createUpdaterArtifacts":false}}') | |
| fi | |
| if [[ "${SIGNED:-0}" == "1" ]]; then | |
| npm run tauri build -- --bundles app,dmg --target ${{ matrix.target }} "${UPDATER_OFF[@]}" | |
| else | |
| npm run tauri build -- --bundles app --target ${{ matrix.target }} "${UPDATER_OFF[@]}" | |
| fi | |
| - name: Package .app and DMG | |
| run: | | |
| set -euo pipefail | |
| # Locate bundles without failing under pipefail when a search root is absent. | |
| echo "==> bundle tree (debug)" | |
| find apps/microbridge-ui/src-tauri/target -path '*/bundle/*' \( -name '*.app' -o -name '*.dmg' \) 2>/dev/null | head -50 || true | |
| APP="" | |
| while IFS= read -r candidate; do | |
| APP="$candidate" | |
| break | |
| done < <(find apps/microbridge-ui/src-tauri/target -type d -name 'Microbridge.app' 2>/dev/null || true) | |
| if [[ -z "$APP" ]]; then | |
| echo "error: Microbridge.app not found under src-tauri/target" >&2 | |
| exit 1 | |
| fi | |
| echo "Using APP=$APP" | |
| STAGE="microbridge-ui-${GITHUB_REF_NAME}-${{ matrix.target }}" | |
| mkdir -p "staging/${STAGE}" "ui-out" | |
| cp -R "$APP" "staging/${STAGE}/" | |
| cp README.md LICENSE INSTALL.md "staging/${STAGE}/" | |
| tar -C staging -czf "ui-out/${STAGE}.tar.gz" "${STAGE}" | |
| DMG="" | |
| while IFS= read -r candidate; do | |
| DMG="$candidate" | |
| break | |
| done < <(find apps/microbridge-ui/src-tauri/target -type f -name '*.dmg' 2>/dev/null || true) | |
| if [[ -n "${DMG:-}" ]]; then | |
| DMG_OUT="ui-out/microbridge-ui-${GITHUB_REF_NAME}-${{ matrix.target }}.dmg" | |
| cp "$DMG" "$DMG_OUT" | |
| echo "Notarizing DMG: $DMG_OUT" | |
| # Tauri notarizes and staples the app before it creates the disk | |
| # image. The outer DMG needs its own notarization ticket; without | |
| # it Gatekeeper can reject the download even when the app inside | |
| # is valid. Keep every check fail-closed so a rejected artifact can | |
| # never reach the GitHub Release. | |
| codesign --verify --deep --strict --verbose=4 "$APP" | |
| spctl --assess --type execute --verbose=4 "$APP" | |
| xcrun stapler validate "$APP" | |
| xcrun notarytool submit "$DMG_OUT" \ | |
| --key "$APPLE_API_KEY_PATH" \ | |
| --key-id "$APPLE_API_KEY" \ | |
| --issuer "$APPLE_API_ISSUER" \ | |
| --wait | |
| xcrun stapler staple "$DMG_OUT" | |
| xcrun stapler validate "$DMG_OUT" | |
| spctl --assess --type open --context context:primary-signature \ | |
| --verbose=4 "$DMG_OUT" | |
| echo "Packed signed, notarized, and stapled DMG: $DMG_OUT" | |
| else | |
| echo "No DMG produced (unsigned build or bundle skipped)" | |
| fi | |
| # Updater artifacts (in-app self-updater). Renamed per-arch so the two | |
| # matrix legs don't collide as release assets; update.json points here. | |
| UPD="" | |
| while IFS= read -r candidate; do | |
| UPD="$candidate" | |
| break | |
| done < <(find apps/microbridge-ui/src-tauri/target -type f -name '*.app.tar.gz' 2>/dev/null || true) | |
| SIG="" | |
| while IFS= read -r candidate; do | |
| SIG="$candidate" | |
| break | |
| done < <(find apps/microbridge-ui/src-tauri/target -type f -name '*.app.tar.gz.sig' 2>/dev/null || true) | |
| if [[ -n "$UPD" && -n "$SIG" ]]; then | |
| cp "$UPD" "ui-out/Microbridge-${{ matrix.target }}.app.tar.gz" | |
| cp "$SIG" "ui-out/Microbridge-${{ matrix.target }}.app.tar.gz.sig" | |
| echo "Packed updater artifact for ${{ matrix.target }}" | |
| else | |
| echo "No updater artifact produced (updater disabled or signing key absent)" | |
| fi | |
| ls -la ui-out | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: ui-${{ matrix.target }} | |
| path: ui-out/* | |
| assemble-assets: | |
| name: assemble release assets | |
| needs: [build, build-ui-macos] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| - name: Collect assets | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release-assets | |
| find artifacts -type f \( -name '*.tar.gz' -o -name '*.dmg' \) -exec cp {} release-assets/ \; | |
| ls -la release-assets | |
| - name: Validate release versions and asset names | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF_NAME}" | |
| VERSION="${TAG#v}" | |
| WORKSPACE_VERSION="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)" | |
| UI_PACKAGE_VERSION="$(jq -r .version apps/microbridge-ui/package.json)" | |
| UI_BUNDLE_VERSION="$(jq -r .version apps/microbridge-ui/src-tauri/tauri.conf.json)" | |
| test "$VERSION" = "$WORKSPACE_VERSION" | |
| test "$VERSION" = "$UI_PACKAGE_VERSION" | |
| test "$VERSION" = "$UI_BUNDLE_VERSION" | |
| for target in aarch64-apple-darwin x86_64-apple-darwin; do | |
| test -f "release-assets/microbridge-${TAG}-${target}.tar.gz" | |
| test -f "release-assets/microbridge-ui-${TAG}-${target}.tar.gz" | |
| test -f "release-assets/microbridge-ui-${TAG}-${target}.dmg" | |
| done | |
| shasum -a 256 release-assets/* > release-assets/SHA256SUMS | |
| - name: Generate updater manifest (update.json) | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF_NAME}" | |
| VERSION="${TAG#v}" | |
| BASE="https://github.com/DevVig/microbridge/releases/download/${TAG}" | |
| SIG_AARCH="$(find artifacts -type f -name 'Microbridge-aarch64-apple-darwin.app.tar.gz.sig' | head -1)" | |
| SIG_X86="$(find artifacts -type f -name 'Microbridge-x86_64-apple-darwin.app.tar.gz.sig' | head -1)" | |
| if [[ -z "$SIG_AARCH" || -z "$SIG_X86" ]]; then | |
| echo "Updater signatures missing — skipping update.json (no in-app update for this release)" | |
| exit 0 | |
| fi | |
| PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| # Use update.json (not latest.json): GitHub's /releases/latest/download/latest.json | |
| # redirect can stick to an older release asset of the same name. | |
| jq -n \ | |
| --arg version "$VERSION" \ | |
| --arg notes "Microbridge ${TAG}. See the GitHub release notes for details." \ | |
| --arg pub_date "$PUB_DATE" \ | |
| --arg sig_a "$(cat "$SIG_AARCH")" \ | |
| --arg url_a "${BASE}/Microbridge-aarch64-apple-darwin.app.tar.gz" \ | |
| --arg sig_x "$(cat "$SIG_X86")" \ | |
| --arg url_x "${BASE}/Microbridge-x86_64-apple-darwin.app.tar.gz" \ | |
| '{version: $version, notes: $notes, pub_date: $pub_date, platforms: {"darwin-aarch64": {signature: $sig_a, url: $url_a}, "darwin-x86_64": {signature: $sig_x, url: $url_x}}}' \ | |
| > release-assets/update.json | |
| echo "Wrote update.json:" | |
| cat release-assets/update.json | |
| test "$(jq -r .version release-assets/update.json)" = "$VERSION" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-assets-${{ github.ref_name }} | |
| path: release-assets/* | |
| formula-smoke-prepublish: | |
| name: pre-publish formula smoke (${{ matrix.arch }}) | |
| needs: assemble-assets | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-15 | |
| arch: arm64 | |
| - os: macos-15-intel | |
| arch: x86_64 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: release-assets-${{ github.ref_name }} | |
| path: release-assets | |
| - name: Rewrite formula to exact local workflow assets | |
| env: | |
| MICROBRIDGE_ASSET_BASE: file://${{ github.workspace }}/release-assets | |
| run: | | |
| ./scripts/bump-formula.sh "${{ github.ref_name }}" | |
| test "$(sed -n 's/ version "\([^"]*\)"/\1/p' Formula/microbridge.rb)" = "${GITHUB_REF_NAME#v}" | |
| - name: Install, launch, and remove candidate formula | |
| run: ./scripts/smoke-formula.sh Formula/microbridge.rb "${GITHUB_REF_NAME#v}" "${{ matrix.arch }}" | |
| publish: | |
| name: publish prerelease assets | |
| needs: [assemble-assets, formula-smoke-prepublish] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: release-assets-${{ github.ref_name }} | |
| path: release-assets | |
| - uses: softprops/action-gh-release@v3 | |
| with: | |
| prerelease: true | |
| generate_release_notes: true | |
| files: release-assets/* | |
| body: | | |
| ## Release state | |
| Signed artifacts are published as a prerelease. This version is not | |
| promoted to a final release until the Homebrew formula PR merges and | |
| clean public-tap installations pass on Apple Silicon and Intel. | |
| ## Direct download (prerelease assets available now) | |
| Signed/notarized app assets are attached to this release. | |
| ## Homebrew (available after the formula PR merges) | |
| Menu bar app + daemon (not CLI-only): | |
| ```sh | |
| brew tap DevVig/microbridge https://github.com/DevVig/microbridge | |
| brew install microbridge | |
| brew services start microbridge | |
| open ~/Applications/Microbridge.app | |
| ``` | |
| Upgrade later: `brew update && brew upgrade microbridge` | |
| ### Signed + notarized DMG | |
| Grab `microbridge-ui-${{ github.ref_name }}-aarch64-apple-darwin.dmg` (Apple Silicon) | |
| or `…-x86_64-apple-darwin.dmg` (Intel) from the assets below, open it, and drag | |
| Microbridge into Applications. The app includes and starts its own local daemon; | |
| no separate daemon or Marketplace plugin download is required. | |
| Binary + app archive: `./scripts/install-from-release.sh ${{ github.ref_name }}` | |
| Full guide: [INSTALL.md](INSTALL.md). | |
| prepare-formula: | |
| name: prepare Homebrew formula | |
| needs: publish | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: main | |
| - name: Bump Formula url + sha256 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| chmod +x scripts/bump-formula.sh | |
| ./scripts/bump-formula.sh "${{ github.ref_name }}" | |
| test "$(sed -n 's/ version "\([^"]*\)"/\1/p' Formula/microbridge.rb)" = "${GITHUB_REF_NAME#v}" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: formula-${{ github.ref_name }} | |
| path: Formula/microbridge.rb | |
| formula-smoke-public-assets: | |
| name: public-asset formula smoke (${{ matrix.arch }}) | |
| needs: prepare-formula | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-15 | |
| arch: arm64 | |
| - os: macos-15-intel | |
| arch: x86_64 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: formula-${{ github.ref_name }} | |
| path: ${{ runner.temp }}/candidate | |
| - name: Install, launch, and remove candidate formula | |
| run: ./scripts/smoke-formula.sh "$RUNNER_TEMP/candidate/microbridge.rb" "${GITHUB_REF_NAME#v}" "${{ matrix.arch }}" | |
| bump-formula: | |
| name: open Homebrew formula PR | |
| needs: [publish, formula-smoke-public-assets] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: main | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: formula-${{ github.ref_name }} | |
| path: ${{ runner.temp }}/candidate | |
| - name: Use smoke-tested formula | |
| run: cp "$RUNNER_TEMP/candidate/microbridge.rb" Formula/microbridge.rb | |
| - name: Open PR | |
| id: formula-pr | |
| uses: peter-evans/create-pull-request@v7 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| commit-message: "chore(brew): bump formula to ${{ github.ref_name }}" | |
| title: "chore(brew): bump formula to ${{ github.ref_name }}" | |
| body: | | |
| Auto-bump `Formula/microbridge.rb` after release `${{ github.ref_name }}`. | |
| After merge, users get the new version via: | |
| ```sh | |
| brew update && brew upgrade microbridge | |
| ``` | |
| branch: chore/brew-${{ github.ref_name }} | |
| delete-branch: true | |
| - name: Publish formula status | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| { | |
| echo "## Homebrew publication" | |
| echo "Pre-publication and public-asset formula smoke tests passed on Apple Silicon and Intel." | |
| echo "Formula PR: ${{ steps.formula-pr.outputs.pull-request-url }}" | |
| echo "The release remains a prerelease until that PR merges and clean public installs pass." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| BODY_FILE="$RUNNER_TEMP/release-body.md" | |
| gh release view "$GITHUB_REF_NAME" --json body --jq .body > "$BODY_FILE" | |
| { | |
| echo | |
| echo "## Homebrew formula PR" | |
| echo "${{ steps.formula-pr.outputs.pull-request-url }}" | |
| echo | |
| echo "Homebrew publication is pending; this release remains a prerelease." | |
| } >> "$BODY_FILE" | |
| gh release edit "$GITHUB_REF_NAME" --notes-file "$BODY_FILE" |