Repository navigation
publish desktop v1.6.10 #148
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: Publish Desktop App | |
| run-name: "${{ format('publish desktop {0}', github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name) }}" | |
| # Triggered manually or by publish-executor-package.yml after a CLI release | |
| # lands. Builds Electron distributables for mac/win/linux with the compiled | |
| # executor CLI bundled in `resources/executor/`, then attaches them to the GitHub | |
| # release matching the tag so electron-updater can pick them up. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Git tag to publish (e.g. v1.4.1) | |
| required: true | |
| type: string | |
| dry_run: | |
| description: Build all distributables but do not touch the GitHub release | |
| required: false | |
| default: false | |
| type: boolean | |
| executor_run_id: | |
| description: >- | |
| Publish Executor run id for this tag. Set by the automatic trigger; | |
| the promote gate waits for exactly this run. Leave empty for manual | |
| dispatches. | |
| required: false | |
| default: "" | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: publish-desktop-${{ github.ref }} | |
| cancel-in-progress: false | |
| # CI is not a user. Without this, executor's own product telemetry reports to the | |
| # production PostHog project from every e2e shard: the analytics layer | |
| # (integration_added, execution_completed, artifact_*) and the | |
| # integrations-registry fetch, which lands as a `hit` event. Each shard is a | |
| # fresh container, so it mints a fresh anonymous id and arrives on a fresh | |
| # runner IP — inflating machine and user counts by roughly an order of | |
| # magnitude. Both opt-outs are the packages' documented CI/test hooks. | |
| env: | |
| DO_NOT_TRACK: "1" | |
| EXECUTOR_DISABLE_ANALYTICS: "1" | |
| EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1" | |
| jobs: | |
| build: | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # smoke: run the compiled-sidecar smoke test on legs whose target | |
| # matches the runner (the mac x64 leg cross-compiles on an arm64 | |
| # runner, so its binary can't be executed natively there). | |
| - os: blacksmith-6vcpu-macos-latest | |
| arch: arm64 | |
| platform: mac | |
| bun-target: bun-darwin-arm64 | |
| smoke: true | |
| - os: blacksmith-6vcpu-macos-latest | |
| arch: x64 | |
| platform: mac | |
| bun-target: bun-darwin-x64 | |
| smoke: false | |
| - os: blacksmith-4vcpu-ubuntu-2404 | |
| arch: x64 | |
| platform: linux | |
| bun-target: bun-linux-x64 | |
| smoke: true | |
| - os: blacksmith-8vcpu-windows-2025 | |
| arch: x64 | |
| platform: win | |
| bun-target: bun-windows-x64 | |
| smoke: true | |
| runs-on: ${{ matrix.os }} | |
| # A healthy leg finishes in ~10 minutes. Without a deadline, a hung step | |
| # wedges the whole publish queue: the concurrency group below queues later | |
| # runs behind this one without cancelling it (v1.5.7's mac leg sat 4 hours | |
| # on a bun install that errored without exiting, holding v1.5.8 pending). | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Validate release tag | |
| env: | |
| RAW_RELEASE_TAG: ${{ inputs.tag }} | |
| run: bun run scripts/validate-release-ref.ts --tag-env RAW_RELEASE_TAG --write-env RELEASE_TAG | |
| - name: Checkout release tag | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_PAT || github.token }} | |
| shell: bash | |
| run: | | |
| auth_remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
| git fetch --force --tags "$auth_remote" "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG" | |
| git checkout --detach "$RELEASE_TAG" | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| # No package/electron caches here on purpose: on Blacksmith's NVMe | |
| # runners a cold `bun install` (8-55s) is as fast as a cache | |
| # restore + warm install, and bun.lock changes on every release | |
| # (Version Packages), so each publish would pay the cache-save tail | |
| # (~45s on the mac critical path) for nothing. | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| # apps/desktop/build/ holds committed static inputs (icon, entitlements) | |
| # that no module imports, so a sweep can delete them without breaking a | |
| # build or a test. v1.6.1 was tagged without them: both mac legs ran ~8 | |
| # minutes, then died inside codesign with "build/entitlements.mac.plist: | |
| # cannot read entitlement data" against an arbitrary locale.pak, while | |
| # the missing icon only ever produced a "default Electron icon is used" | |
| # notice. Check the tag's own tree here so the failure names the cause. | |
| - name: Check desktop build resources | |
| run: bun ./scripts/check-build-resources.ts | |
| working-directory: apps/desktop | |
| - name: Build web app | |
| run: bun run --filter @executor-js/local build | |
| - name: Build bundled executor | |
| env: | |
| BUN_TARGET: ${{ matrix.bun-target }} | |
| run: bun ./scripts/build-sidecar.ts | |
| working-directory: apps/desktop | |
| # Gate the release on the compiled binary actually booting. v1.5.0/.1 | |
| # shipped local-server binaries that died on launch (missing libsql native | |
| # binding) — a regression dev mode can't catch because `bun run` resolves | |
| # node_modules that `bun build --compile` does not bundle. | |
| - name: Smoke test bundled executor | |
| if: matrix.smoke | |
| run: bun run test:smoke | |
| working-directory: apps/desktop | |
| - name: Build Electron main/preload/renderer | |
| env: | |
| # Crash-report DSN baked into the main bundle (see the define in | |
| # electron.vite.config.ts). Unset (forks, local) → crash reporting | |
| # is compiled out and dumps stay local. | |
| DESKTOP_SENTRY_DSN: ${{ vars.DESKTOP_SENTRY_DSN }} | |
| run: bunx --bun electron-vite build | |
| working-directory: apps/desktop | |
| - name: Stage Apple API key (mac signing + notarization) | |
| if: matrix.platform == 'mac' && env.APPLE_API_KEY != '' | |
| env: | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| run: | | |
| mkdir -p "${RUNNER_TEMP}/private_keys" | |
| printf '%s' "$APPLE_API_KEY" > "${RUNNER_TEMP}/private_keys/AuthKey.p8" | |
| echo "APPLE_API_KEY_PATH=${RUNNER_TEMP}/private_keys/AuthKey.p8" >> "$GITHUB_ENV" | |
| - name: Build desktop distributables | |
| env: | |
| # electron-builder reads GH_TOKEN for the publish step. We use | |
| # --publish never here and attach assets explicitly in the release | |
| # job so we don't fight electron-updater's metadata expectations. | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Apple signing + notarization. electron-builder picks these up: | |
| # CSC_LINK / CSC_KEY_PASSWORD → import the Developer ID Application | |
| # cert (.p12, base64) into a temp keychain for codesign | |
| # APPLE_API_KEY (file path) / APPLE_API_KEY_ID / APPLE_API_ISSUER | |
| # → notarytool authentication for the notarization upload | |
| # When the secrets aren't set (forks, local), electron-builder | |
| # silently produces an unsigned build. | |
| # | |
| # CSC_LINK / CSC_KEY_PASSWORD MUST stay scoped to the mac legs. | |
| # Despite the name used here, CSC_LINK is electron-builder's | |
| # cross-platform certificate variable: WinPackager falls back to it | |
| # whenever WIN_CSC_LINK is unset, via | |
| # platformPackager.getCscLink("WIN_CSC_LINK"). Handing the Apple | |
| # Developer ID .p12 to the windows leg made electron-builder read its | |
| # subject and write `publisherName: Developer ID Application: ...` | |
| # into app-update.yml, so every Windows client rejected the update it | |
| # had just downloaded with "New version is not signed by the | |
| # application owner". Empty string is the documented "no certificate" | |
| # value — WindowsSignToolManager treats `cscLink === ""` exactly like | |
| # unset. The APPLE_* vars below need no such guard: they are only | |
| # read by notarytool on darwin and are inert elsewhere. | |
| CSC_LINK: ${{ matrix.platform == 'mac' && secrets.CSC_LINK || '' }} | |
| CSC_KEY_PASSWORD: ${{ matrix.platform == 'mac' && secrets.CSC_KEY_PASSWORD || '' }} | |
| APPLE_API_KEY: ${{ env.APPLE_API_KEY_PATH }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| run: bunx --bun electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish never --config electron-builder.config.ts | |
| working-directory: apps/desktop | |
| # electron-updater installs from the zip, not the DMG, and Squirrel.Mac | |
| # rejects it unless the extracted app passes codesign. 1.6.9 shipped a | |
| # zip whose framework symlinks (Versions/Current -> A) had been expanded | |
| # into copies by a 7-Zip upgrade inside electron-builder; the DMG was | |
| # fine, every auto-update silently failed. Extract the zip the way | |
| # Squirrel does and verify it before anything is uploaded. | |
| - name: Verify mac update zip | |
| if: matrix.platform == 'mac' | |
| shell: bash | |
| env: | |
| CSC_LINK: ${{ secrets.CSC_LINK }} | |
| run: | | |
| set -euo pipefail | |
| zip="apps/desktop/dist/executor-desktop-mac-${{ matrix.arch }}.zip" | |
| links=$(unzip -Z "$zip" | grep -c '^l' || true) | |
| echo "symlink entries in $zip: $links" | |
| if [ "$links" -eq 0 ]; then | |
| echo "::error::$zip has no symlink entries; framework bundles were flattened and Squirrel.Mac will reject the update" | |
| exit 1 | |
| fi | |
| # Unsigned builds (forks, no CSC_LINK) cannot pass codesign; the | |
| # symlink check above still catches the flattening on its own. | |
| if [ -z "${CSC_LINK:-}" ]; then | |
| echo "no signing certificate configured; skipping codesign verification" | |
| exit 0 | |
| fi | |
| tmp=$(mktemp -d) | |
| ditto -x -k "$zip" "$tmp" | |
| codesign --verify --deep --strict --verbose=1 "$tmp/Executor.app" | |
| rm -rf "$tmp" | |
| # The two mac legs each emit a latest-mac.yml listing only their own | |
| # arch. Rename per-arch here; the release job merges them back into the | |
| # single latest-mac.yml electron-updater clients fetch. Without this, | |
| # merge-multiple in the release job lets one arch clobber the other and | |
| # every Mac gets pointed at whichever leg uploaded last. | |
| - name: Rename mac update manifest per arch | |
| if: matrix.platform == 'mac' | |
| shell: bash | |
| run: mv apps/desktop/dist/latest-mac.yml "apps/desktop/dist/latest-mac-${{ matrix.arch }}.yml" | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: | | |
| apps/desktop/dist/*.dmg | |
| apps/desktop/dist/*.zip | |
| apps/desktop/dist/*.exe | |
| apps/desktop/dist/*.AppImage | |
| apps/desktop/dist/*.deb | |
| apps/desktop/dist/*.rpm | |
| apps/desktop/dist/*.blockmap | |
| apps/desktop/dist/latest*.yml | |
| if-no-files-found: warn | |
| release: | |
| needs: build | |
| # dry_run builds and uploads workflow artifacts but never touches the | |
| # GitHub release — used to rehearse workflow changes against a real tag. | |
| if: ${{ !inputs.dry_run }} | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| permissions: | |
| contents: write | |
| # Read Publish Executor run state for the promote gate below. | |
| actions: read | |
| steps: | |
| - name: Checkout validation script | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Validate release tag | |
| env: | |
| RAW_RELEASE_TAG: ${{ inputs.tag }} | |
| run: bun run scripts/validate-release-ref.ts --tag-env RAW_RELEASE_TAG --write-env RELEASE_TAG | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Merge mac update manifests | |
| run: | | |
| set -euo pipefail | |
| bun scripts/merge-latest-mac-yml.ts \ | |
| artifacts/latest-mac-x64.yml \ | |
| artifacts/latest-mac-arm64.yml \ | |
| artifacts/latest-mac.yml | |
| rm artifacts/latest-mac-x64.yml artifacts/latest-mac-arm64.yml | |
| - name: Upload to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Parallel uploads; xargs exits non-zero if any single upload fails, | |
| # which keeps the promote step below from running on a partial set. | |
| find artifacts -type f \ | |
| \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" \ | |
| -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \ | |
| -o -name "*.blockmap" \ | |
| -o -name "latest*.yml" \) -print0 \ | |
| | xargs -0 -n1 -P8 -I{} sh -c \ | |
| 'echo "Uploading: $1"; exec gh release upload "$RELEASE_TAG" "$1" --repo "$GITHUB_REPOSITORY" --clobber' _ {} | |
| # A published release must imply the npm packages for this tag exist. | |
| # Publish Executor triggers this workflow before its npm publishes (to | |
| # overlap them with the desktop build) and hands over its own run id, | |
| # so wait for exactly that run to conclude successfully before going | |
| # public. A run-name search would fail open whenever the run fell | |
| # outside the listing window, so only an explicitly empty run id (a | |
| # manual dispatch) skips the gate — the operator owns the invariant | |
| # then. | |
| - name: Wait for executor package publish | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| EXECUTOR_RUN_ID: ${{ inputs.executor_run_id }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$EXECUTOR_RUN_ID" ]; then | |
| echo "No executor_run_id given (manual dispatch); skipping the publish gate." | |
| exit 0 | |
| fi | |
| deadline=$((SECONDS + 1200)) | |
| while :; do | |
| run=$(gh run view "$EXECUTOR_RUN_ID" --repo "$GITHUB_REPOSITORY" \ | |
| --json status,conclusion,url,displayTitle) | |
| title=$(echo "$run" | jq -r .displayTitle) | |
| if [ "$title" != "publish executor $RELEASE_TAG" ]; then | |
| echo "Run $EXECUTOR_RUN_ID is '$title', not 'publish executor $RELEASE_TAG' — refusing to promote." | |
| exit 1 | |
| fi | |
| status=$(echo "$run" | jq -r .status) | |
| if [ "$status" = "completed" ]; then | |
| conclusion=$(echo "$run" | jq -r .conclusion) | |
| if [ "$conclusion" = "success" ]; then | |
| echo "Publish Executor succeeded." | |
| break | |
| fi | |
| echo "Publish Executor concluded '$conclusion' — leaving the release in draft." | |
| echo "$run" | jq -r .url | |
| exit 1 | |
| fi | |
| if [ "$SECONDS" -ge "$deadline" ]; then | |
| echo "Timed out waiting for Publish Executor — leaving the release in draft." | |
| exit 1 | |
| fi | |
| echo "Publish Executor status: $status; waiting..." | |
| sleep 15 | |
| done | |
| # Flip draft → published only after every desktop asset is uploaded — | |
| # this is the atomic point where the new tag becomes "latest". | |
| - name: Promote release (draft → published) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh release edit "$RELEASE_TAG" --draft=false --repo "$GITHUB_REPOSITORY" |