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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Install hidapi build deps (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libudev-dev libhidapi-dev
Expand All @@ -37,13 +39,29 @@ jobs:
working-directory: apps/microbridge-ui
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
cache-dependency-path: apps/microbridge-ui/package-lock.json
- run: npm ci
- run: npm test
- run: npm run build

adapters:
name: adapters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "22"
- run: node --test adapters/cursor/hooks/microbridge-event.test.mjs
- run: node -e 'for (const file of ["adapters/cursor/.cursor-plugin/plugin.json", "adapters/cursor/hooks/hooks.json"]) JSON.parse(require("fs").readFileSync(file, "utf8"))'

# Keep required-check names stable for the main branch ruleset.
# Job names above are what GitHub shows as status contexts.
136 changes: 136 additions & 0 deletions .github/workflows/finalize-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Finalize release after Homebrew

on:
pull_request:
types: [closed]
paths:
- Formula/microbridge.rb

permissions:
contents: read

jobs:
resolve:
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
startsWith(github.event.pull_request.head.ref, 'chore/brew-v')
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
steps:
- id: release
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
TAG="${HEAD_REF#chore/brew-}"
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"

public-install:
name: clean public install (${{ matrix.arch }})
needs: resolve
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
arch: arm64
- os: macos-15-intel
arch: x86_64
steps:
- name: Install from public tap and exercise app/service lifecycle
env:
VERSION: ${{ needs.resolve.outputs.version }}
EXPECTED_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
test "$(uname -m)" = "$EXPECTED_ARCH"
brew tap DevVig/microbridge https://github.com/DevVig/microbridge
brew update
HOMEBREW_NO_INSTALL_CLEANUP=1 brew install DevVig/microbridge/microbridge
test "$(brew info --json=v2 DevVig/microbridge/microbridge | jq -r '.formulae[0].installed[0].version')" = "$VERSION"
microbridgectl help | grep -q Usage
APP="$HOME/Applications/Microbridge.app"
test -x "$(brew --prefix DevVig/microbridge/microbridge)/bin/microbridged"
brew services start DevVig/microbridge/microbridge
for _ in {1..10}; do
[[ -f "$APP/.microbridge-brew" ]] && break
sleep 1
done
test -f "$APP/.microbridge-brew"
test "$(defaults read "$APP/Contents/Info" CFBundleShortVersionString)" = "$VERSION"
brew services list | grep -E '^microbridge[[:space:]]+(started|scheduled)'
open "$APP"
APP_PID=""
for _ in {1..10}; do
APP_PID="$(pgrep -f "$APP/Contents/MacOS/" | head -1 || true)"
[[ -n "$APP_PID" ]] && break
sleep 1
done
test -n "$APP_PID"
kill "$APP_PID"
brew services stop DevVig/microbridge/microbridge
HOMEBREW_NO_INSTALL_CLEANUP=1 brew uninstall DevVig/microbridge/microbridge
if brew services list | grep -q '^microbridge[[:space:]]'; then
echo "microbridge service is still registered after uninstall" >&2
exit 1
fi
test -f "$APP/.microbridge-brew"
rm -rf "$APP"
test ! -e "$APP"

promote:
name: promote verified release
needs: [resolve, public-install]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Mark release final and record public availability
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.resolve.outputs.tag }}
run: |
set -euo pipefail
BODY_FILE="$RUNNER_TEMP/release-body.md"
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body --jq .body > "$BODY_FILE"
python3 - "$BODY_FILE" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
body = path.read_text()
body = body.replace(
"Signed artifacts are published as a prerelease. This version is not\n"
"promoted to a final release until the Homebrew formula PR merges and\n"
"clean public-tap installations pass on Apple Silicon and Intel.",
"Signed artifacts and the Homebrew formula are publicly available. "
"Clean installs passed on Apple Silicon and Intel.",
)
body = body.replace(
"Homebrew publication is pending; this release remains a prerelease.",
"Homebrew installation is publicly available and verified on Apple Silicon and Intel.",
)
body = body.replace(
"## Direct download (prerelease assets available now)",
"## Direct download",
)
body = body.replace(
"## Homebrew (available after the formula PR merges)",
"## Homebrew (publicly available)",
)
path.write_text(body)
PY
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --prerelease=false --notes-file "$BODY_FILE"
{
echo "## Release finalized"
echo "GitHub assets: published"
echo "Homebrew formula: merged"
echo "Clean public installs: Apple Silicon and Intel passed"
echo "Release: https://github.com/$GITHUB_REPOSITORY/releases/tag/$TAG"
} >> "$GITHUB_STEP_SUMMARY"
145 changes: 139 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ jobs:
name: ui-${{ matrix.target }}
path: ui-out/*

publish:
name: publish release
assemble-assets:
name: assemble release assets
needs: [build, build-ui-macos]
runs-on: ubuntu-latest
steps:
Expand All @@ -225,9 +225,27 @@ jobs:
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 (latest.json)
run: |
set -euo pipefail
Expand All @@ -253,12 +271,65 @@ jobs:
> release-assets/latest.json
echo "Wrote latest.json:"
cat release-assets/latest.json
test "$(jq -r .version release-assets/latest.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: |
## Install (macOS)
## 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):

Expand All @@ -271,7 +342,7 @@ jobs:

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

### Direct download (signed + notarized DMG)
### 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
Expand All @@ -281,8 +352,8 @@ jobs:

Full guide: [INSTALL.md](INSTALL.md).

bump-formula:
name: bump Homebrew formula
prepare-formula:
name: prepare Homebrew formula
needs: publish
runs-on: ubuntu-latest
steps:
Expand All @@ -295,7 +366,49 @@ jobs:
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: candidate
- name: Install, launch, and remove candidate formula
run: ./scripts/smoke-formula.sh 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: candidate
- name: Use smoke-tested formula
run: cp candidate/microbridge.rb Formula/microbridge.rb
- name: Open PR
id: formula-pr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -310,3 +423,23 @@ jobs:
```
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"
Loading
Loading