Skip to content

chore: bump version to 0.39.0 #599

chore: bump version to 0.39.0

chore: bump version to 0.39.0 #599

Workflow file for this run

name: Installers
on:
push:
# Every branch, not just main: this repo squash-merges locally and opens no
# pull requests of its own, so a feature branch has no other way to reach
# this lane before it lands. `branches-ignore` rather than deleting the key —
# a push trigger that names `tags` and omits `branches` fires ONLY on tags,
# which would disable branch CI outright. Dependabot is excluded because it
# pushes its branch AND opens a PR, and the two land in different
# concurrency groups, so the push run is a pure duplicate of the PR run.
branches-ignore: ['dependabot/**']
# The old 'feature/**' glob could not match the branch names actually used
# here (feat/..., fix/...), so non-main pushes were effectively unbuilt.
tags: ['v*']
paths:
- 'Package.swift'
- 'Sources/**'
- 'Tests/**'
- 'Resources/**'
- 'mirroir.sh'
- 'npm/**'
- 'scripts/package-fake-app.sh'
- '.github/actions/setup-embacle/**'
- '.github/workflows/installers.yml'
pull_request:
branches: [main]
paths:
- 'Package.swift'
- 'Sources/**'
- 'Tests/**'
- 'Resources/**'
- 'mirroir.sh'
- 'npm/**'
- 'scripts/package-fake-app.sh'
- '.github/actions/setup-embacle/**'
- '.github/workflows/installers.yml'
workflow_dispatch:
# Cancel superseded runs on the same branch / PR — three macos-15 jobs run here,
# so a superseded run is expensive to leave in flight.
concurrency:
group: installers-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
source-install:
name: Source install (mirroir.sh)
runs-on: macos-15
permissions:
contents: read
env:
MIRROIR_BUNDLE_ID: com.jfarcand.FakeMirroring
MIRROIR_PROCESS_NAME: FakeMirroring
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-embacle
- name: Clone skills repo
run: |
git clone https://github.com/jfarcand/mirroir-skills.git ../mirroir-skills
mkdir -p .mirroir-mcp
ln -s "$(cd ../mirroir-skills && pwd)" .mirroir-mcp/skills
- name: Run mirroir.sh
run: ./mirroir.sh
- name: Verify binaries built
run: |
test -x .build/release/mirroir-mcp
# Verify mirroir CLI symlink was created (Homebrew bin dir)
test -L /opt/homebrew/bin/mirroir || test -L /usr/local/bin/mirroir
mirroir record --help 2>&1 | grep -q "Record user interactions"
- name: Verify embacle FFI linked
run: nm .build/release/mirroir-mcp | grep -q embacle_init
- name: Build and package FakeMirroring
run: |
swift build -c release --product FakeMirroring
./scripts/package-fake-app.sh
- name: Quit Setup Assistant (macos-15 runner intercepts taps)
run: |
# Some macos-15 runners boot with Setup Assistant in the foreground
# — it intercepts cghidEventTap mouse events and the target app
# receives no mouseUp. Quit it before launching FakeMirroring.
osascript -e 'tell application "Setup Assistant" to quit' 2>/dev/null || true
pkill -f "Setup Assistant" 2>/dev/null || true
sleep 1
- name: Ensure display fits the FakeMirroring window
# GH macOS runners boot with a display too short for the 410x930
# window: the bottom ~60pt (the simulated tab bar) is clipped
# off-screen and CGEvent taps there never land. Grow the virtual
# display before launching FakeMirroring.
run: |
brew install --quiet jakehilborn/jakehilborn/displayplacer || brew install --quiet displayplacer || true
echo "--- displays before ---"
displayplacer list 2>/dev/null | grep -E "Persistent|Resolution" || true
SCREEN_ID=$(displayplacer list 2>/dev/null | awk -F': ' '/Persistent screen id/{print $2; exit}')
if [ -n "$SCREEN_ID" ]; then
for RES in 1920x1080 1680x1050 1600x1200; do
if displayplacer "id:$SCREEN_ID res:$RES" 2>/dev/null; then
echo "display set to $RES"
break
fi
done
fi
echo "--- displays after ---"
displayplacer list 2>/dev/null | grep -E "Resolution" || true
- name: Launch FakeMirroring
run: |
open .build/release/FakeMirroring.app
sleep 3
- name: Integration tests (FakeMirroring)
run: swift test --filter IntegrationTests
- name: MCP initialize (installed binary)
run: |
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
| .build/release/mirroir-mcp 2>/dev/null \
| python3 -c "
import sys, json
obj = json.loads(sys.stdin.readline())
assert obj['result']['serverInfo']['name'] == 'mirroir-mcp'
print('MCP initialize: OK')
"
- name: MCP tools/call screenshot (installed binary + FakeMirroring)
run: |
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screenshot","arguments":{}}}\n' \
| .build/release/mirroir-mcp --dangerously-skip-permissions 2>/dev/null \
| python3 -c "
import sys, json
lines = sys.stdin.readlines()
resp = json.loads(lines[1])
content = resp['result']['content']
images = [c for c in content if c.get('type') == 'image']
assert len(images) > 0, f'Expected image in screenshot response, got: {content}'
print(f'screenshot tool: OK ({len(images)} image block(s))')
"
homebrew-install:
name: Homebrew install (local formula)
runs-on: macos-15
permissions:
contents: read
env:
MIRROIR_BUNDLE_ID: com.jfarcand.FakeMirroring
MIRROIR_PROCESS_NAME: FakeMirroring
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-embacle
- name: Clone skills repo
run: |
git clone https://github.com/jfarcand/mirroir-skills.git ../mirroir-skills
mkdir -p .mirroir-mcp
ln -s "$(cd ../mirroir-skills && pwd)" .mirroir-mcp/skills
- name: Create source tarball
run: |
TAR_PATH="/tmp/mirroir-mcp-local.tar.gz"
git archive --format=tar.gz --prefix=mirroir-mcp-local/ HEAD > "$TAR_PATH"
SHA=$(shasum -a 256 "$TAR_PATH" | cut -d' ' -f1)
echo "TARBALL_PATH=$TAR_PATH" >> $GITHUB_ENV
echo "TARBALL_SHA=$SHA" >> $GITHUB_ENV
- name: Create local tap with formula
run: |
brew tap-new local/test
TAP_DIR="$(brew --repository)/Library/Taps/local/homebrew-test/Formula"
mkdir -p "$TAP_DIR"
cat > "$TAP_DIR/mirroir-mcp.rb" << RUBY
class MirroirMcp < Formula
desc "MCP server for controlling iPhone through macOS iPhone Mirroring"
homepage "https://github.com/jfarcand/mirroir-mcp"
url "file://${TARBALL_PATH}"
version "0.0.0-ci"
sha256 "${TARBALL_SHA}"
license "Apache-2.0"
depends_on :macos
depends_on xcode: ["15.0", :build]
depends_on "dravr-ai/tap/embacle-ffi"
def install
embacle_lib = Formula["embacle-ffi"].opt_lib
system "swift", "build", "-c", "release", "--disable-sandbox",
"-Xlinker", "-L#{embacle_lib}"
bin.install ".build/release/mirroir-mcp"
bin.install_symlink "mirroir-mcp" => "mirroir"
end
test do
assert_match "mirroir-mcp", shell_output("#{bin}/mirroir-mcp --help 2>&1", 1)
end
end
RUBY
- name: Install from local tap
run: brew install local/test/mirroir-mcp
- name: Verify Homebrew-installed binaries
run: |
which mirroir-mcp
which mirroir
# Verify mirroir CLI works (symlink to mirroir-mcp)
mirroir test --help 2>&1 | grep -q "mirroir-mcp test"
- name: Verify embacle FFI linked
run: nm "$(brew --prefix)/bin/mirroir-mcp" | grep -q embacle_init
- name: Build and package FakeMirroring
run: |
swift build -c release --product FakeMirroring
./scripts/package-fake-app.sh
- name: Quit Setup Assistant (macos-15 runner intercepts taps)
run: |
# Some macos-15 runners boot with Setup Assistant in the foreground
# — it intercepts cghidEventTap mouse events and the target app
# receives no mouseUp. Quit it before launching FakeMirroring.
osascript -e 'tell application "Setup Assistant" to quit' 2>/dev/null || true
pkill -f "Setup Assistant" 2>/dev/null || true
sleep 1
- name: Ensure display fits the FakeMirroring window
# GH macOS runners boot with a display too short for the 410x930
# window: the bottom ~60pt (the simulated tab bar) is clipped
# off-screen and CGEvent taps there never land. Grow the virtual
# display before launching FakeMirroring.
run: |
brew install --quiet jakehilborn/jakehilborn/displayplacer || brew install --quiet displayplacer || true
echo "--- displays before ---"
displayplacer list 2>/dev/null | grep -E "Persistent|Resolution" || true
SCREEN_ID=$(displayplacer list 2>/dev/null | awk -F': ' '/Persistent screen id/{print $2; exit}')
if [ -n "$SCREEN_ID" ]; then
for RES in 1920x1080 1680x1050 1600x1200; do
if displayplacer "id:$SCREEN_ID res:$RES" 2>/dev/null; then
echo "display set to $RES"
break
fi
done
fi
echo "--- displays after ---"
displayplacer list 2>/dev/null | grep -E "Resolution" || true
- name: Launch FakeMirroring
run: |
open .build/release/FakeMirroring.app
sleep 3
- name: Integration tests (FakeMirroring)
run: swift test --filter IntegrationTests
- name: MCP initialize (brew-installed binary)
run: |
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
| mirroir-mcp 2>/dev/null \
| python3 -c "
import sys, json
obj = json.loads(sys.stdin.readline())
assert obj['result']['serverInfo']['name'] == 'mirroir-mcp'
print('MCP initialize (brew path): OK')
"
- name: MCP tools/call screenshot (brew-installed binary + FakeMirroring)
run: |
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screenshot","arguments":{}}}\n' \
| mirroir-mcp --dangerously-skip-permissions 2>/dev/null \
| python3 -c "
import sys, json
lines = sys.stdin.readlines()
resp = json.loads(lines[1])
content = resp['result']['content']
images = [c for c in content if c.get('type') == 'image']
assert len(images) > 0, f'Expected image in screenshot response, got: {content}'
print(f'screenshot tool: OK ({len(images)} image block(s))')
"
- name: Cleanup
if: always()
run: |
sudo brew uninstall mirroir-mcp || true
npx-install:
name: NPX install (npm package)
runs-on: macos-15
permissions:
contents: read
env:
MIRROIR_BUNDLE_ID: com.jfarcand.FakeMirroring
MIRROIR_PROCESS_NAME: FakeMirroring
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-embacle
- name: Clone skills repo
run: |
git clone https://github.com/jfarcand/mirroir-skills.git ../mirroir-skills
mkdir -p .mirroir-mcp
ln -s "$(cd ../mirroir-skills && pwd)" .mirroir-mcp/skills
- name: Build release binaries
run: swift build -c release
- name: Verify embacle FFI linked
run: nm .build/release/mirroir-mcp | grep -q embacle_init
- name: Stage binaries into npm/bin/ (simulates install.js postinstall)
run: |
mkdir -p npm/bin
cp .build/release/mirroir-mcp npm/bin/mirroir-mcp-native
chmod +x npm/bin/mirroir-mcp-native
- name: Build and package FakeMirroring
run: |
swift build -c release --product FakeMirroring
./scripts/package-fake-app.sh
- name: Quit Setup Assistant (macos-15 runner intercepts taps)
run: |
# Some macos-15 runners boot with Setup Assistant in the foreground
# — it intercepts cghidEventTap mouse events and the target app
# receives no mouseUp. Quit it before launching FakeMirroring.
osascript -e 'tell application "Setup Assistant" to quit' 2>/dev/null || true
pkill -f "Setup Assistant" 2>/dev/null || true
sleep 1
- name: Ensure display fits the FakeMirroring window
# GH macOS runners boot with a display too short for the 410x930
# window: the bottom ~60pt (the simulated tab bar) is clipped
# off-screen and CGEvent taps there never land. Grow the virtual
# display before launching FakeMirroring.
run: |
brew install --quiet jakehilborn/jakehilborn/displayplacer || brew install --quiet displayplacer || true
echo "--- displays before ---"
displayplacer list 2>/dev/null | grep -E "Persistent|Resolution" || true
SCREEN_ID=$(displayplacer list 2>/dev/null | awk -F': ' '/Persistent screen id/{print $2; exit}')
if [ -n "$SCREEN_ID" ]; then
for RES in 1920x1080 1680x1050 1600x1200; do
if displayplacer "id:$SCREEN_ID res:$RES" 2>/dev/null; then
echo "display set to $RES"
break
fi
done
fi
echo "--- displays after ---"
displayplacer list 2>/dev/null | grep -E "Resolution" || true
- name: Launch FakeMirroring
run: |
open .build/release/FakeMirroring.app
sleep 3
- name: Integration tests (FakeMirroring)
run: swift test --filter IntegrationTests
- name: MCP initialize (npx wrapper)
run: |
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
| node npm/bin/mirroir-mcp 2>/dev/null \
| python3 -c "
import sys, json
obj = json.loads(sys.stdin.readline())
assert obj['result']['serverInfo']['name'] == 'mirroir-mcp'
print('MCP initialize (npx wrapper): OK')
"
- name: MCP tools/call screenshot (npx wrapper + FakeMirroring)
run: |
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screenshot","arguments":{}}}\n' \
| node npm/bin/mirroir-mcp --dangerously-skip-permissions 2>/dev/null \
| python3 -c "
import sys, json
lines = sys.stdin.readlines()
resp = json.loads(lines[1])
content = resp['result']['content']
images = [c for c in content if c.get('type') == 'image']
assert len(images) > 0, f'Expected image in screenshot response, got: {content}'
print(f'screenshot tool: OK ({len(images)} image block(s))')
"