Skip to content

fix(release): use update.json for in-app updates (v0.3.4) (#76) #17

fix(release): use update.json for in-app updates (v0.3.4) (#76)

fix(release): use update.json for in-app updates (v0.3.4) (#76) #17

Workflow file for this run

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"