Skip to content

fix(desktop): make tray quit reliable via menu event handler #7

fix(desktop): make tray quit reliable via menu event handler

fix(desktop): make tray quit reliable via menu event handler #7

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
build-desktop:
strategy:
fail-fast: false
matrix:
include:
# Windows x86_64
- os: windows-latest
target: x86_64-pc-windows-msvc
client: windows
archive: zip
ext: .exe
# macOS x86_64 (Intel)
- os: macos-latest
target: x86_64-apple-darwin
client: mac
archive: tar.gz
ext: ""
# macOS aarch64 (Apple Silicon)
- os: macos-latest
target: aarch64-apple-darwin
client: mac
archive: tar.gz
ext: ""
# Linux x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
client: linux
archive: tar.gz
ext: ""
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
libxdo-dev \
libx11-dev \
libxrandr-dev \
libxi-dev \
libxtst-dev \
libglib2.0-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libdbus-1-dev
- name: Build
working-directory: ${{ matrix.client }}
run: cargo build --release --target ${{ matrix.target }}
- name: Determine archive name
id: archive
shell: bash
run: |
version="${GITHUB_REF_NAME}"
arch="${{ matrix.target }}"
name="screenmcp-${{ matrix.client }}-${arch}-${version}"
echo "name=${name}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Sign Windows binary
if: matrix.client == 'windows' && env.CODE_SIGNING_PFX != ''
env:
CODE_SIGNING_PFX: ${{ secrets.CODE_SIGNING_PFX }}
CODE_SIGNING_PASSWORD: ${{ secrets.CODE_SIGNING_PASSWORD }}
shell: pwsh
run: |
$pfxBytes = [Convert]::FromBase64String($env:CODE_SIGNING_PFX)
[IO.File]::WriteAllBytes("$pwd\codesign.pfx", $pfxBytes)
$exe = "${{ matrix.client }}\target\${{ matrix.target }}\release\screenmcp-${{ matrix.client }}.exe"
$signtool = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits" -Recurse -Filter signtool.exe | Where-Object { $_.FullName -like "*x64*" } | Select-Object -First 1 -ExpandProperty FullName
& $signtool sign /f codesign.pfx /p $env:CODE_SIGNING_PASSWORD /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $exe
Remove-Item codesign.pfx
- name: Package macOS (.app + .dmg)
if: matrix.client == 'mac'
shell: bash
run: |
APP="ScreenMCP.app"
BINARY="mac/target/${{ matrix.target }}/release/screenmcp-mac"
# Create .app bundle
mkdir -p "${APP}/Contents/MacOS" "${APP}/Contents/Resources"
cp mac/Info.plist "${APP}/Contents/Info.plist"
cp "$BINARY" "${APP}/Contents/MacOS/screenmcp-mac"
chmod +x "${APP}/Contents/MacOS/screenmcp-mac"
echo "APPLscmc" > "${APP}/Contents/PkgInfo"
# Build .icns from pre-made PNGs
ICON_DIR="mac/assets/icon-pngs"
if [ -d "$ICON_DIR" ]; then
mkdir -p /tmp/screenmcp.iconset
cp "$ICON_DIR/16.png" /tmp/screenmcp.iconset/icon_16x16.png
cp "$ICON_DIR/32.png" /tmp/screenmcp.iconset/icon_16x16@2x.png
cp "$ICON_DIR/32.png" /tmp/screenmcp.iconset/icon_32x32.png
cp "$ICON_DIR/64.png" /tmp/screenmcp.iconset/icon_32x32@2x.png
cp "$ICON_DIR/128.png" /tmp/screenmcp.iconset/icon_128x128.png
cp "$ICON_DIR/256.png" /tmp/screenmcp.iconset/icon_128x128@2x.png
cp "$ICON_DIR/256.png" /tmp/screenmcp.iconset/icon_256x256.png
cp "$ICON_DIR/512.png" /tmp/screenmcp.iconset/icon_256x256@2x.png
cp "$ICON_DIR/512.png" /tmp/screenmcp.iconset/icon_512x512.png
cp "$ICON_DIR/1024.png" /tmp/screenmcp.iconset/icon_512x512@2x.png
iconutil -c icns /tmp/screenmcp.iconset -o "${APP}/Contents/Resources/AppIcon.icns"
rm -rf /tmp/screenmcp.iconset
fi
# Ad-hoc sign (no developer cert needed, satisfies basic integrity checks)
codesign --force --deep -s - "${APP}"
# Create .dmg using hdiutil (native on macOS runners)
staging="${{ steps.archive.outputs.name }}"
hdiutil create -volname "ScreenMCP" -srcfolder "${APP}" -ov -format UDZO "${staging}.dmg"
echo "ASSET=${staging}.dmg" >> "$GITHUB_ENV"
- name: Package (Unix, non-Mac)
if: matrix.archive == 'tar.gz' && matrix.client != 'mac'
shell: bash
run: |
staging="${{ steps.archive.outputs.name }}"
mkdir -p "${staging}"
cp "${{ matrix.client }}/target/${{ matrix.target }}/release/screenmcp-${{ matrix.client }}${{ matrix.ext }}" "${staging}/"
cp "${{ matrix.client }}/README.md" "${staging}/" 2>/dev/null || true
tar czf "${staging}.tar.gz" "${staging}"
echo "ASSET=${staging}.tar.gz" >> "$GITHUB_ENV"
- name: Package (Windows)
if: matrix.archive == 'zip'
shell: bash
run: |
staging="${{ steps.archive.outputs.name }}"
mkdir -p "${staging}"
cp "${{ matrix.client }}/target/${{ matrix.target }}/release/screenmcp-${{ matrix.client }}${{ matrix.ext }}" "${staging}/"
cp "${{ matrix.client }}/README.md" "${staging}/" 2>/dev/null || true
7z a "${staging}.zip" "${staging}"
echo "ASSET=${staging}.zip" >> "$GITHUB_ENV"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.archive.outputs.name }}
path: ${{ env.ASSET }}
build-android:
runs-on: ubuntu-latest
defaults:
run:
working-directory: android
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Verify google-services.json exists
run: test -f app/google-services.json
- name: Build release APK
run: ./gradlew assembleRelease
- name: Sign APK
if: env.KEYSTORE_BASE64 != ''
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
echo "$KEYSTORE_BASE64" | base64 -d > keystore.jks
# Find the unsigned APK
APK=$(find app/build/outputs/apk/release -name '*.apk' | head -1)
ALIGNED="app/build/outputs/apk/release/screenmcp-aligned.apk"
SIGNED="app/build/outputs/apk/release/screenmcp-signed.apk"
zipalign -v -p 4 "$APK" "$ALIGNED"
apksigner sign \
--ks keystore.jks \
--ks-pass "pass:$KEYSTORE_PASSWORD" \
--ks-key-alias "$KEY_ALIAS" \
--key-pass "pass:$KEY_PASSWORD" \
--out "$SIGNED" \
"$ALIGNED"
rm keystore.jks
- name: Determine APK name
id: apk
shell: bash
run: |
version="${GITHUB_REF_NAME}"
# Use signed APK if it exists, otherwise unsigned
if [ -f "app/build/outputs/apk/release/screenmcp-signed.apk" ]; then
APK_PATH="app/build/outputs/apk/release/screenmcp-signed.apk"
else
APK_PATH=$(find app/build/outputs/apk/release -name '*.apk' | head -1)
fi
DEST="screenmcp-android-${version}.apk"
cp "$APK_PATH" "$DEST"
echo "path=${DEST}" >> "$GITHUB_OUTPUT"
echo "name=${DEST}" >> "$GITHUB_OUTPUT"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: screenmcp-android
path: ${{ steps.apk.outputs.path }}
build-windows-installer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y nsis mingw-w64
- name: Install Rust toolchain (Windows GNU target)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-gnu
- name: Build Windows installer (.exe)
working-directory: windows
run: ./build-installer.sh "${GITHUB_REF_NAME#v}"
- name: Sign Windows installer
if: env.CODE_SIGNING_PFX != ''
env:
CODE_SIGNING_PFX: ${{ secrets.CODE_SIGNING_PFX }}
CODE_SIGNING_PASSWORD: ${{ secrets.CODE_SIGNING_PASSWORD }}
run: |
sudo apt-get install -y osslsigncode
echo "$CODE_SIGNING_PFX" | base64 -d > codesign.pfx
version="${GITHUB_REF_NAME#v}"
file="windows/target/installer/screenmcp-setup-${version}-x86_64.exe"
osslsigncode sign \
-pkcs12 codesign.pfx \
-pass "$CODE_SIGNING_PASSWORD" \
-ts http://timestamp.digicert.com \
-h sha256 \
-in "$file" \
-out "${file}.signed"
mv "${file}.signed" "$file"
rm codesign.pfx
- name: Determine installer name
id: installer
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
file="screenmcp-setup-${version}-x86_64.exe"
cp "windows/target/installer/${file}" "${file}"
echo "path=${file}" >> "$GITHUB_OUTPUT"
echo "name=${file}" >> "$GITHUB_OUTPUT"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: screenmcp-windows-installer
path: ${{ steps.installer.outputs.path }}
build-linux-deb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libxdo-dev \
libx11-dev \
libxrandr-dev \
libxi-dev \
libxtst-dev \
libglib2.0-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libdbus-1-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build
working-directory: linux
run: cargo build --release
- name: Package .deb
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
arch="amd64"
pkg="screenmcp_${version}_${arch}"
binary="linux/target/release/screenmcp-linux"
strip "$binary" 2>/dev/null || true
mkdir -p "${pkg}/DEBIAN"
mkdir -p "${pkg}/usr/bin"
mkdir -p "${pkg}/usr/share/applications"
mkdir -p "${pkg}/usr/share/icons/hicolor/512x512/apps"
cp "$binary" "${pkg}/usr/bin/screenmcp-linux"
chmod 755 "${pkg}/usr/bin/screenmcp-linux"
cp linux/screenmcp.desktop "${pkg}/usr/share/applications/"
cp linux/assets/icon-app.png "${pkg}/usr/share/icons/hicolor/512x512/apps/screenmcp.png"
installed_size=$(du -sk "${pkg}" | cut -f1)
cat > "${pkg}/DEBIAN/control" <<EOF
Package: screenmcp
Version: ${version}
Section: utils
Priority: optional
Architecture: ${arch}
Installed-Size: ${installed_size}
Depends: libgtk-3-0, libssl3 | libssl1.1, libxdo3, wmctrl
Maintainer: ScreenMCP <support@screenmcp.com>
Homepage: https://screenmcp.com
Description: AI desktop control via MCP
ScreenMCP gives AI assistants real-time vision and control
over desktop computers via the Model Context Protocol.
System tray app that connects to a ScreenMCP worker.
EOF
# Remove leading whitespace from control file (YAML indent)
sed -i 's/^ //' "${pkg}/DEBIAN/control"
cat > "${pkg}/DEBIAN/postinst" <<'SCRIPT'
#!/bin/sh
set -e
gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true
update-desktop-database /usr/share/applications 2>/dev/null || true
SCRIPT
sed -i 's/^ //' "${pkg}/DEBIAN/postinst"
chmod 755 "${pkg}/DEBIAN/postinst"
cat > "${pkg}/DEBIAN/postrm" <<'SCRIPT'
#!/bin/sh
set -e
gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true
update-desktop-database /usr/share/applications 2>/dev/null || true
SCRIPT
sed -i 's/^ //' "${pkg}/DEBIAN/postrm"
chmod 755 "${pkg}/DEBIAN/postrm"
dpkg-deb --build --root-owner-group "${pkg}" "screenmcp-linux-${version}-amd64.deb"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: screenmcp-linux-deb
path: screenmcp-linux-*.deb
release:
needs: [build-desktop, build-android, build-windows-installer, build-linux-deb]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Collect release assets
run: |
mkdir -p release
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.apk' -o -name '*.dmg' -o -name '*.deb' -o -name 'screenmcp-setup-*-x86_64.exe' \) -exec mv {} release/ \;
ls -lh release/
- name: Generate checksums
working-directory: release
run: |
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
release/*
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
body: |
## Downloads
| Platform | File | Notes |
|----------|------|-------|
| **macOS (Apple Silicon)** | `screenmcp-mac-aarch64-apple-darwin-*.dmg` | M1/M2/M3/M4 Macs |
| **macOS (Intel)** | `screenmcp-mac-x86_64-apple-darwin-*.dmg` | Older Intel Macs |
| **Windows** | `screenmcp-windows-*.zip` | x86_64 |
| **Windows Installer** | `screenmcp-setup-*-x86_64.exe` | NSIS setup wizard |
| **Linux (.deb)** | `screenmcp-linux-*-amd64.deb` | Debian/Ubuntu x86_64 |
| **Linux (.tar.gz)** | `screenmcp-linux-*.tar.gz` | x86_64 |
| **Android** | `screenmcp-android-*.apk` | ARM/x86 |
## macOS Installation
1. Download the `.dmg` for your architecture (Apple Silicon or Intel)
2. Open the `.dmg` and drag **ScreenMCP** to `/Applications`
3. **Important** — the app is not notarized, so macOS Gatekeeper will block it. Run:
```bash
xattr -cr /Applications/ScreenMCP.app
```
4. Launch ScreenMCP — it appears as a menu bar icon (no Dock icon)
5. Grant **Accessibility** and **Screen Recording** permissions when prompted
## Windows
Extract the archive and run `screenmcp-windows.exe`, or use the NSIS installer.
## Linux
Install the `.deb` package:
```bash
sudo dpkg -i screenmcp-linux-*-amd64.deb
```
Or extract the `.tar.gz` and run the `screenmcp-linux` binary directly.
## Android
Install the APK directly. You may need to enable "Install from unknown sources".