feat(tray): macOS menu bar with live session status #24
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |