Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 98 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,108 @@ jobs:
node-version: "22"
cache: npm
cache-dependency-path: apps/microbridge-ui/package-lock.json
- name: Build Microbridge.app

- 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"
Comment on lines +108 to +119

- 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 }}
run: |
npm ci
npm run tauri build -- --bundles app --target ${{ matrix.target }}
- name: Package .app
if [[ "${SIGNED:-0}" == "1" ]]; then
npm run tauri build -- --bundles app,dmg --target ${{ matrix.target }}
else
npm run tauri build -- --bundles app --target ${{ matrix.target }}
fi

- name: Package .app and DMG
run: |
APP="$(find apps/microbridge-ui/src-tauri/target/${{ matrix.target }}/release/bundle/macos apps/microbridge-ui/src-tauri/target/release/bundle/macos -name 'Microbridge.app' -type d 2>/dev/null | head -n1)"
set -euo pipefail
APP="$(find apps/microbridge-ui/src-tauri/target/${{ matrix.target }}/release/bundle/macos \
apps/microbridge-ui/src-tauri/target/release/bundle/macos \
-name 'Microbridge.app' -type d 2>/dev/null | head -n1)"
test -n "$APP"
STAGE="microbridge-ui-${GITHUB_REF_NAME}-${{ matrix.target }}"
mkdir -p "staging/${STAGE}"
mkdir -p "staging/${STAGE}" "ui-out"
cp -R "$APP" "staging/${STAGE}/"
cp README.md LICENSE-MIT LICENSE-APACHE INSTALL.md "staging/${STAGE}/"
tar -C staging -czf "${STAGE}.tar.gz" "${STAGE}"
echo "ASSET=${STAGE}.tar.gz" >> "$GITHUB_ENV"
tar -C staging -czf "ui-out/${STAGE}.tar.gz" "${STAGE}"

DMG="$(find apps/microbridge-ui/src-tauri/target/${{ matrix.target }}/release/bundle/dmg \
apps/microbridge-ui/src-tauri/target/release/bundle/dmg \
-name '*.dmg' -type f 2>/dev/null | head -n1 || true)"
if [[ -n "${DMG:-}" ]]; then
DMG_OUT="ui-out/microbridge-ui-${GITHUB_REF_NAME}-${{ matrix.target }}.dmg"
cp "$DMG" "$DMG_OUT"
echo "Packed signed DMG: $DMG_OUT"
spctl --assess --type open --context context:primary-signature "$DMG_OUT" || true
codesign -dv --verbose=2 "$APP" || true
Comment on lines +159 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '130,190p' .github/workflows/release.yml

Repository: DevVig/microbridge

Length of output: 2591


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/release.yml')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if 145 <= i <= 170:
        print(f"{i:4}: {line}")
PY

Repository: DevVig/microbridge

Length of output: 1483


🏁 Script executed:

rg -n "spctl|stapler|codesign" .github/workflows/release.yml .github/workflows -g '*.yml' -g '*.yaml'

Repository: DevVig/microbridge

Length of output: 1534


🏁 Script executed:

sed -n '1,220p' .github/workflows/release.yml

Repository: DevVig/microbridge

Length of output: 8789


🌐 Web query:

Apple codesign -dv does not verify integrity spctl assess notarization stapler validate documentation

💡 Result:

The tools codesign, spctl, and stapler serve distinct roles in verifying the integrity and notarization status of macOS software [1][2][3]. It is inaccurate to state that codesign does not verify integrity; rather, it verifies code signature integrity, while other tools verify notarization status [4][3]. 1. codesign: This tool verifies the integrity of the code signature itself [4][3]. It confirms that the binary is signed, the signature is cryptographically sound, and that all sealed components (the executable code and resources) remain unaltered [4]. Running codesign -vvv --deep --strict /path/to/app verifies that the app's signature is structurally and cryptographically valid [1]. 2. spctl: This tool assesses whether the software is authorized by system policies, including Gatekeeper [2][5]. To check if an app is notarized, you use spctl --assess --verbose /path/to/app [1][2]. A successful result indicates that the app is notarized (e.g., source=Notarized Developer ID) [6]. It essentially asks the system policy daemon (syspolicyd) to evaluate the app against current security requirements, which includes checking for a notarization ticket [2]. 3. stapler: This tool is used to attach (staple) a notarization ticket to an app bundle, allowing the app to be verified offline [7][8]. The command stapler validate /path/to/app confirms whether a notarization ticket is successfully stapled to the bundle [9][10]. If an app is notarized but the ticket is not stapled, stapler validate will report that no ticket is found, even if the app is notarized online [3][10]. In summary, codesign confirms the app's internal integrity (that it hasn't been modified since it was signed), while spctl and stapler are used to verify the app's notarization status with Apple [1][2][3]. If you need to confirm both, you must use both categories of tools [2][10].

Citations:


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
text = Path('.github/workflows/release.yml').read_text().splitlines()
for i, line in enumerate(text, 1):
    if 'spctl' in line or 'stapler' in line or 'codesign' in line:
        start = max(1, i-3)
        end = min(len(text), i+3)
        print(f'--- around line {i} ---')
        for j in range(start, end+1):
            print(f'{j:4}: {text[j-1]}')
PY

Repository: DevVig/microbridge

Length of output: 3119


🏁 Script executed:

python3 - <<'PY'
import subprocess, textwrap, json, os, sys
# Probe local tool availability only; do not modify files.
for cmd in [
    ["bash","-lc","command -v gh || true"],
    ["bash","-lc","command -v jq || true"],
]:
    r = subprocess.run(cmd, capture_output=True, text=True)
    print("CMD:", " ".join(cmd))
    print(r.stdout.strip() or "<no stdout>")
PY

Repository: DevVig/microbridge

Length of output: 252


Make release validation fail closed. .github/workflows/release.yml:159-160
spctl --assess and codesign -dv here are only diagnostics, and || true lets the release continue even when the DMG is untrusted or unstapled. Replace this with a real verification step that fails the job, e.g. codesign --verify --deep --strict and stapler validate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 159 - 160, Update the release
validation commands in the workflow to fail the job when signing or notarization
verification fails: replace the diagnostic-only spctl and codesign invocations
and remove their `|| true` suppression, using strict deep code-signature
verification and stapler validation for the generated DMG or app artifacts.

Comment on lines +158 to +160
else
echo "No DMG produced (unsigned build or bundle skipped)"
fi
ls -la ui-out

- uses: actions/upload-artifact@v7
with:
name: ui-${{ matrix.target }}
path: ${{ env.ASSET }}
path: ui-out/*

publish:
name: publish release
Expand All @@ -97,7 +180,7 @@ jobs:
- name: Collect assets
run: |
mkdir -p release-assets
find artifacts -name '*.tar.gz' -exec cp {} release-assets/ \;
find artifacts -type f \( -name '*.tar.gz' -o -name '*.dmg' \) -exec cp {} release-assets/ \;
ls -la release-assets
- uses: softprops/action-gh-release@v3
with:
Expand All @@ -117,6 +200,12 @@ jobs:

Upgrade later: `brew update && brew upgrade microbridge`

### Direct download (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. Pair with the daemon archive or Homebrew formula.

Binary + app archive: `./scripts/install-from-release.sh ${{ github.ref_name }}`

Full guide: [INSTALL.md](INSTALL.md).
Expand Down
11 changes: 8 additions & 3 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,19 @@ Sample unit: [`scripts/microbridge.service`](scripts/microbridge.service).
## Install from a GitHub Release (binaries)

When a `v*` tag is published, CI attaches platform archives (daemon +
arch-specific `Microbridge.app` for `aarch64-apple-darwin` /
`x86_64-apple-darwin`):
arch-specific menu bar app). On macOS, releases also include a
**Developer ID–signed and notarized** DMG
(`microbridge-ui-<tag>-<arch>.dmg`).
Comment on lines +86 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use <target> in the documented DMG filename.

The published suffix is the complete target triple, such as aarch64-apple-darwin, rather than only the architecture.

-(`microbridge-ui-<tag>-<arch>.dmg`).
+(`microbridge-ui-<tag>-<target>.dmg`, for example
+`microbridge-ui-<tag>-aarch64-apple-darwin.dmg`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
arch-specific menu bar app). On macOS, releases also include a
**Developer ID–signed and notarized** DMG
(`microbridge-ui-<tag>-<arch>.dmg`).
arch-specific menu bar app). On macOS, releases also include a
**Developer ID–signed and notarized** DMG
(`microbridge-ui-<tag>-<target>.dmg`, for example
`microbridge-ui-<tag>-aarch64-apple-darwin.dmg`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@INSTALL.md` around lines 86 - 88, Update the documented macOS DMG filename in
INSTALL.md to use the complete <target> suffix, such as aarch64-apple-darwin,
instead of only <arch>; preserve the existing tag placeholder and filename
structure.


```sh
./scripts/install-from-release.sh # latest
./scripts/install-from-release.sh # latest (prefers DMG on macOS)
./scripts/install-from-release.sh v0.0.1
```

Or open the DMG from the GitHub Release page and drag Microbridge into
Applications, then install/start the daemon via Homebrew or the daemon
archive.

## Layout after install

| Path | Purpose |
Expand Down
14 changes: 14 additions & 0 deletions apps/microbridge-ui/src-tauri/Entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
Comment on lines +5 to +9
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
4 changes: 3 additions & 1 deletion apps/microbridge-ui/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
"icons/icon.png"
],
"macOS": {
"minimumSystemVersion": "13.0"
"minimumSystemVersion": "13.0",
"hardenedRuntime": true,
"entitlements": "Entitlements.plist"
}
}
}
26 changes: 26 additions & 0 deletions docs/macos-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# macOS signing & notarization

Direct-download DMGs are signed with **Developer ID Application** and
notarized via the App Store Connect API. This is **not** Mac App Store
distribution.

## GitHub Actions secrets

| Secret | Purpose |
|---|---|
| `APPLE_CERTIFICATE` | Base64-encoded `.p12` (Developer ID Application + private key) |
| `APPLE_CERTIFICATE_PASSWORD` | Password for that `.p12` |
| `APPLE_SIGNING_IDENTITY` | e.g. `Developer ID Application: Vig Solutions LLC (3NQG568C4Q)` |
| `KEYCHAIN_PASSWORD` | Ephemeral CI keychain password |
| `APPLE_API_KEY` | App Store Connect API Key ID |
| `APPLE_API_ISSUER` | App Store Connect Issuer ID |
| `APPLE_API_KEY_P8` | Contents of the `.p8` private key file |
| `APPLE_TEAM_ID` | Team ID (`3NQG568C4Q`) |

Release tags (`v*`) run `.github/workflows/release.yml`, which builds
`app` + `dmg` bundles with Tauri when those secrets are present.

## Local assets (do not commit)

Developer ID material lives outside the repo, typically under
`~/.asc/signing/developer-id/`.
86 changes: 56 additions & 30 deletions scripts/install-from-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,22 @@ EOF
launchctl enable "gui/$(id -u)/${LABEL}"
launchctl kickstart -k "gui/$(id -u)/${LABEL}"

UI_ASSET="microbridge-ui-${TAG}-${TARGET}.tar.gz"
UI_URL="https://github.com/${REPO}/releases/download/${TAG}/${UI_ASSET}"
# Backward-compatible fallback for older releases that shipped a single asset.
UI_FALLBACK="microbridge-ui-${TAG}-macos.tar.gz"
echo "==> Downloading menu bar app $UI_URL"
if curl -fsSL -o "$TMP/$UI_ASSET" "$UI_URL" \
|| curl -fsSL -o "$TMP/$UI_ASSET" "https://github.com/${REPO}/releases/download/${TAG}/${UI_FALLBACK}"; then
tar -xzf "$TMP/$UI_ASSET" -C "$TMP"
APP_SRC="$(find "$TMP" -name 'Microbridge.app' -type d | head -n1 || true)"
if [[ -n "$APP_SRC" ]]; then
DEST="$HOME/Applications/Microbridge.app"
MARKER="$DEST/.microbridge-release"
if [[ -d "$DEST" && ! -f "$MARKER" && "${MICROBRIDGE_FORCE_APP:-}" != "1" ]]; then
echo " warning: $DEST exists and is not release-managed — leave it"
echo " set MICROBRIDGE_FORCE_APP=1 to replace"
else
rm -rf "$DEST"
mkdir -p "$HOME/Applications"
cp -R "$APP_SRC" "$DEST"
echo "owned-by-release" >"$MARKER"
UI_PLIST="$HOME/Library/LaunchAgents/${UI_LABEL}.plist"
cat >"$UI_PLIST" <<EOF
DEST="$HOME/Applications/Microbridge.app"
MARKER="$DEST/.microbridge-release"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant installer script section with line numbers.
sed -n '80,140p' scripts/install-from-release.sh | cat -n

# Show the full file length so I can judge whether a wider slice is needed.
wc -l scripts/install-from-release.sh

Repository: DevVig/microbridge

Length of output: 2587


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find other macOS installer/signing-related logic.
rg -n --hidden --glob '!**/.git/**' 'codesign|spctl|quarantine|\.microbridge-release|release-managed|MARKER=' .

# Inspect the full installer script around the marker and install flow.
sed -n '1,220p' scripts/install-from-release.sh | cat -n

Repository: DevVig/microbridge

Length of output: 8623


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the release workflow sections that sign the app and build the DMG.
sed -n '120,180p' .github/workflows/release.yml | cat -n

Repository: DevVig/microbridge

Length of output: 3138


Move the release marker out of Microbridge.app. The release workflow signs the app bundle, and writing .microbridge-release after copying mutates that signed bundle and can break Gatekeeper/code-signature validation. Store the marker in a separate state directory instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install-from-release.sh` at line 106, Update the release marker
assignment in the install script so MARKER points to a separate state directory
outside Microbridge.app, rather than placing .microbridge-release under DEST.
Ensure the directory exists before the marker is written, while preserving the
existing marker read/write behavior.

install_app_bundle() {
local APP_SRC="$1"
if [[ -d "$DEST" && ! -f "$MARKER" && "${MICROBRIDGE_FORCE_APP:-}" != "1" ]]; then
echo " warning: $DEST exists and is not release-managed — leave it"
echo " set MICROBRIDGE_FORCE_APP=1 to replace"
return 0
fi
rm -rf "$DEST"
mkdir -p "$HOME/Applications"
cp -R "$APP_SRC" "$DEST"
xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the installer script around the referenced lines.
git ls-files scripts/install-from-release.sh
wc -l scripts/install-from-release.sh
cat -n scripts/install-from-release.sh | sed -n '90,180p'

Repository: DevVig/microbridge

Length of output: 4260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect surrounding logic and any other quarantine / assessment handling in the repo.
rg -n "xattr -dr com.apple.quarantine|spctl --assess|hdiutil attach|curl -fsSL -o" scripts/install-from-release.sh

Repository: DevVig/microbridge

Length of output: 591


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether release packaging signs/notarizes the macOS artifacts.
rg -n "codesign|notarytool|altool|spctl|hdiutil create|create-dmg|productbuild|pkgbuild|xcrun" .github scripts src .

Repository: DevVig/microbridge

Length of output: 889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any release or packaging workflow that may explain whether the DMG is signed/notarized.
fd -a -H -t f .github scripts

Repository: DevVig/microbridge

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the release workflow around DMG creation/assessment.
cat -n .github/workflows/release.yml | sed -n '70,170p'

Repository: DevVig/microbridge

Length of output: 5746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the installer path that strips quarantine and whether any later step re-adds it.
cat -n scripts/install-from-release.sh | sed -n '145,185p'

Repository: DevVig/microbridge

Length of output: 2075


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if the installer itself documents expected Gatekeeper behavior.
rg -n "quarantine|Gatekeeper|notar|signed DMG|spctl|security" scripts/install-from-release.sh README* .github/workflows/release.yml

Repository: DevVig/microbridge

Length of output: 1877


Keep quarantine on the installed app bundle

install_app_bundle() clears com.apple.quarantine for both the DMG and tarball paths, which suppresses Gatekeeper for the downloaded app. Leave the xattr intact and let macOS enforce trust on first launch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install-from-release.sh` at line 117, Remove the xattr command that
clears com.apple.quarantine from install_app_bundle(), including the DMG and
tarball installation paths, so the installed app bundle retains quarantine and
macOS can enforce Gatekeeper on first launch.

echo "owned-by-release" >"$MARKER"
Comment on lines +105 to +118
UI_PLIST="$HOME/Library/LaunchAgents/${UI_LABEL}.plist"
cat >"$UI_PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Expand All @@ -141,17 +135,49 @@ EOF
</dict>
</plist>
EOF
launchctl bootout "gui/$(id -u)/${UI_LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$UI_PLIST"
launchctl enable "gui/$(id -u)/${UI_LABEL}"
launchctl kickstart -k "gui/$(id -u)/${UI_LABEL}" 2>/dev/null || open "$HOME/Applications/Microbridge.app"
echo " installed ~/Applications/Microbridge.app"
launchctl bootout "gui/$(id -u)/${UI_LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$UI_PLIST"
launchctl enable "gui/$(id -u)/${UI_LABEL}"
launchctl kickstart -k "gui/$(id -u)/${UI_LABEL}" 2>/dev/null || open "$HOME/Applications/Microbridge.app"
echo " installed ~/Applications/Microbridge.app"
}

DMG_ASSET="microbridge-ui-${TAG}-${TARGET}.dmg"
DMG_URL="https://github.com/${REPO}/releases/download/${TAG}/${DMG_ASSET}"
UI_ASSET="microbridge-ui-${TAG}-${TARGET}.tar.gz"
UI_URL="https://github.com/${REPO}/releases/download/${TAG}/${UI_ASSET}"
# Backward-compatible fallback for older releases that shipped a single asset.
UI_FALLBACK="microbridge-ui-${TAG}-macos.tar.gz"

INSTALLED_UI=0
echo "==> Trying signed DMG $DMG_URL"
if curl -fsSL -o "$TMP/$DMG_ASSET" "$DMG_URL"; then
MOUNT="$(mktemp -d "$TMP/dmg.XXXXXX")"
if hdiutil attach "$TMP/$DMG_ASSET" -mountpoint "$MOUNT" -nobrowse -quiet; then
APP_SRC="$(find "$MOUNT" -name 'Microbridge.app' -type d | head -n1 || true)"
if [[ -n "$APP_SRC" ]]; then
install_app_bundle "$APP_SRC"
INSTALLED_UI=1
fi
hdiutil detach "$MOUNT" -quiet || true
fi
fi

if [[ "$INSTALLED_UI" -eq 0 ]]; then
echo "==> Downloading menu bar app archive $UI_URL"
if curl -fsSL -o "$TMP/$UI_ASSET" "$UI_URL" \
|| curl -fsSL -o "$TMP/$UI_ASSET" "https://github.com/${REPO}/releases/download/${TAG}/${UI_FALLBACK}"; then
tar -xzf "$TMP/$UI_ASSET" -C "$TMP"
APP_SRC="$(find "$TMP" -name 'Microbridge.app' -type d | head -n1 || true)"
if [[ -n "$APP_SRC" ]]; then
install_app_bundle "$APP_SRC"
INSTALLED_UI=1
else
echo " warning: archive had no Microbridge.app"
fi
else
echo " warning: archive had no Microbridge.app"
echo " warning: no UI asset for ${TAG} — install UI with ./scripts/install.sh or brew"
fi
else
echo " warning: no UI asset for ${TAG} — install UI with ./scripts/install.sh or brew"
fi
fi

Expand Down
Loading