Skip to content

publish desktop v1.6.10 #148

publish desktop v1.6.10

publish desktop v1.6.10 #148

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"