Skip to content

feat(tray): macOS menu bar with live session status #24

feat(tray): macOS menu bar with live session status

feat(tray): macOS menu bar with live session status #24

Workflow file for this run

name: Release
# Produces a downloadable DMG for a given ref and attaches it to a GitHub
# Release page. Two entry points:
#
# 1. Push a git tag matching v*.*.* (e.g. `git tag v1.2.3 && git push origin v1.2.3`).
# This is the primary way to cut a release. The release name equals the tag.
#
# 2. Manual `workflow_dispatch` for dry-runs / tester builds. The optional
# `tag` input defaults to a timestamp-based pre-release tag
# (`v0.0.0-dev-<timestamp>`) so the manual path always yields a
# prerelease that is easy to identify and discard.
#
# Signing has three modes: unsigned, signed (Developer ID only), and
# notarized (signed + Apple notary). Tag pushes default to `signed` while
# Apple's notary queue is congested; switch back to `notarized` in the
# resolve step once turnaround normalizes. See SIGNING.md.
#
# The job also uploads the DMG/zip as a workflow artifact (14-day retention)
# so internal users can grab builds without waiting for the Release page to
# finish populating — belt-and-suspenders backup.
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
signing-mode:
description: 'unsigned (fastest), signed (Developer ID, no notarization), notarized (signed + Apple notarization)'
type: choice
options:
- unsigned
- signed
- notarized
default: signed
tag:
description: 'Tag/release name for this manual run (defaults to v0.0.0-dev-<timestamp>)'
type: string
required: false
default: ''
# Scope concurrency to the tag/input so two simultaneous runs for the same
# release cannot race each other. `github.ref` resolves to `refs/tags/<tag>`
# on tag pushes, and the `tag` input (or the fallback) on manual runs.
concurrency:
group: release-${{ github.event.inputs.tag || github.ref }}
cancel-in-progress: false
env:
NODE_VERSION: '22'
jobs:
make:
name: Package + make DMG (macOS)
runs-on: macos-latest
timeout-minutes: 40
# Least-privilege: only this job needs contents:write (to publish the
# Release + upload assets via softprops/action-gh-release).
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
# Full history so release-notes generation can walk back to the
# previous tag.
fetch-depth: 0
- name: Resolve release tag
id: tag
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
elif [[ -n "${INPUT_TAG}" ]]; then
TAG="${INPUT_TAG}"
else
TAG="v0.0.0-dev-$(date -u +%Y%m%d%H%M%S)"
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
# Prerelease if the tag contains a recognized pre-release suffix.
if [[ "${TAG}" == *-dev* || "${TAG}" == *-rc* || "${TAG}" == *-beta* || "${TAG}" == *-alpha* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "Resolved tag: ${TAG}"
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
# yarn.lock contains a git+ssh URL for a baileys dep
# (whiskeysockets/libsignal-node). CI has no SSH key, so rewrite to HTTPS.
- name: Rewrite git SSH to HTTPS
run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
- name: Install JS dependencies
working-directory: my-app
# Repo's source of truth is yarn.lock (Taskfile runs `yarn install`).
# --frozen-lockfile keeps CI deps identical to dev.
run: yarn install --frozen-lockfile
# Decide signing mode. Three values:
# unsigned — SKIP_SIGNING=1, no Apple credentials needed
# signed — Developer ID code-signed, NOT notarized (fast; users
# must right-click → Open the first time)
# notarized — fully signed + Apple-notarized (slow; depends on
# Apple's notary queue, can take 5-60+ min)
#
# Resolution rules:
# - workflow_dispatch: honor the `signing-mode` input (default `signed`).
# - tag push: default to `signed` for now while Apple's notary queue
# is congested. Switch back to `notarized` here once queues normalize.
# Falls back to `unsigned` if signing secrets are missing.
- name: Resolve signing mode
id: signing
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
MODE_INPUT: ${{ github.event.inputs.signing-mode }}
HAS_SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY != '' }}
HAS_CERT: ${{ secrets.APPLE_CERT_P12_BASE64 != '' }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
mode="${MODE_INPUT:-signed}"
else
# Tag push default — change to "notarized" when ready.
mode="signed"
fi
# Fall back to unsigned if cert secrets are missing (so manual
# workflow_dispatch runs don't fail on a fresh fork).
if [[ "$mode" != "unsigned" ]]; then
if [[ "$HAS_SIGNING_IDENTITY" != "true" || "$HAS_CERT" != "true" ]]; then
echo "::warning::Signing requested but APPLE_CERT_P12_BASE64 / SIGNING_IDENTITY not set — falling back to unsigned"
mode="unsigned"
fi
fi
echo "mode=${mode}" >> "$GITHUB_OUTPUT"
echo "Signing mode: ${mode}"
# Import the Developer ID Application certificate into a temporary
# keychain so codesign can find it. macos-latest runners ship with an
# empty user keychain — without this, signing silently no-ops.
#
# Required secrets:
# APPLE_CERT_P12_BASE64 — base64-encoded .p12 export of the
# Developer ID Application cert + private key
# APPLE_CERT_PASSWORD — password used when exporting the .p12
- name: Import signing certificate
if: ${{ steps.signing.outputs.mode != 'unsigned' }}
env:
APPLE_CERT_P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 32)"
CERT_PATH="$RUNNER_TEMP/cert.p12"
echo "$APPLE_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Prepend our keychain so codesign finds it before the empty default.
security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychain -d user | tr -d '"')
rm -f "$CERT_PATH"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
- name: electron-forge make (unsigned)
if: ${{ steps.signing.outputs.mode == 'unsigned' }}
working-directory: my-app
env:
SKIP_SIGNING: '1'
run: npm run make
- name: electron-forge make (signed, no notarization)
if: ${{ steps.signing.outputs.mode == 'signed' }}
working-directory: my-app
env:
# Forge config notarizes only when APPLE_ID is set, so we
# deliberately omit it here.
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
run: npm run make
- name: electron-forge make (signed + notarized)
if: ${{ steps.signing.outputs.mode == 'notarized' }}
working-directory: my-app
env:
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npm run make
- name: Collect release assets + compute checksums
id: assets
shell: bash
run: |
set -euo pipefail
mkdir -p release-assets
# macOS runner's /bin/bash is 3.2 — no `globstar`. Use `find` instead.
dmg_count=0
while IFS= read -r f; do
[[ -z "$f" ]] && continue
cp "$f" release-assets/
dmg_count=$((dmg_count + 1))
# Version-less copy so the
# `/releases/latest/download/<file>` URL stays stable across
# releases. Arch is derived from the original filename (e.g.
# `...-arm64.dmg`, `...-x64.dmg`, `...-universal.dmg`).
base="$(basename "$f" .dmg)"
case "$base" in
*arm64) stable="Browser-Use-arm64.dmg" ;;
*x64) stable="Browser-Use-x64.dmg" ;;
*universal) stable="Browser-Use-universal.dmg" ;;
*) stable="Browser-Use.dmg" ;;
esac
cp "$f" "release-assets/${stable}"
done < <(find my-app/out/make -type f -name '*.dmg')
zip_count=0
while IFS= read -r f; do
[[ -z "$f" ]] && continue
cp "$f" release-assets/
zip_count=$((zip_count + 1))
done < <(find my-app/out/make -type f -name '*.zip')
if [[ "$dmg_count" -eq 0 ]]; then
echo "::error::No DMG produced by electron-forge make — refusing to publish empty release."
ls -R my-app/out/make || true
exit 1
fi
# SHA256SUMS.txt covers every asset we're about to publish.
# `nullglob` makes non-matching globs vanish instead of expanding
# literally — important because `*.zip` may not exist (Forge won't
# emit one for unsigned arm64 builds in some configurations).
(
cd release-assets
shopt -s nullglob
shasum -a 256 *.dmg *.zip \
| sed 's|\./||g' \
> SHA256SUMS.txt
)
echo "dmg_count=${dmg_count}" >> "$GITHUB_OUTPUT"
echo "zip_count=${zip_count}" >> "$GITHUB_OUTPUT"
echo "Collected ${dmg_count} DMG(s) and ${zip_count} zip(s):"
ls -la release-assets/
echo "---- SHA256SUMS.txt ----"
cat release-assets/SHA256SUMS.txt
# Backup path: the raw DMG/zip/checksums stay available as a workflow
# artifact for 14 days regardless of what happens to the Release page.
- name: Upload DMG artifact (backup)
uses: actions/upload-artifact@v7
with:
name: agentic-browser-${{ github.sha }}
path: |
release-assets/*.dmg
release-assets/*.zip
release-assets/SHA256SUMS.txt
if-no-files-found: error
retention-days: 14
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
prerelease: ${{ steps.tag.outputs.prerelease == 'true' }}
# Auto-generate notes from commits since the previous tag.
generate_release_notes: true
# For workflow_dispatch runs the tag doesn't exist yet — let the
# action create it against the workflow's sha.
target_commitish: ${{ github.sha }}
# We already hard-fail on missing DMG in the asset-collection
# step, so false here tolerates the legitimate case where Forge
# didn't emit a .zip.
fail_on_unmatched_files: false
files: |
release-assets/*.dmg
release-assets/*.zip
release-assets/SHA256SUMS.txt