Files
sim/.github/workflows/desktop-release.yml
T
Waleed 856fe0ffb6 fix(docker): upgrade bun to 1.3.14 (#6236)
* fix(docker): upgrade bun to 1.3.14 to unbreak the Next 16.3.0 server

Bun 1.3.13 cannot load Next 16.3.0's compiled server runtime. The app container
runs the Next server under Bun (`oven/bun:1.3.13-slim`, `bun apps/sim/bootstrap.js`),
so every app-page render threw and `/api/health` returned 500:

  ⨯ Error: Failed to load external module
    next/dist/compiled/next-server/app-page-turbo.runtime.prod.js:
    TypeError: Expected CommonJS module to have a function wrapper.
    If you weren't messing around with Bun's internals, this is a bug in Bun

Isolated to Bun, not Next, by loading that exact module in the real images:

  Next 16.2.12 + Bun 1.3.13 -> loads (why staging was fine before)
  Next 16.3.0  + Bun 1.3.13 -> CJS wrapper error
  Next 16.3.0  + Bun 1.3.14 -> loads

Bun 1.3.14 is the current stable and already fixes it, so this bumps every pin
rather than reverting the framework upgrade, which would only defer the same
latent Bun bug to the next attempt.

Why no gate caught it: local dev machines and this bump's own verification run
Bun 1.3.14, while the container and CI pinned 1.3.13 — and CI only *builds* the
image, it never boots one and probes `/api/health`. A container smoke test in CI
would have caught this before merge; that is worth adding separately.

* fix(docker): align the remaining bun pins with 1.3.14

Two pins were missed in the first pass because the search was scoped to
docker/, package.json and .github/workflows/:

- .devcontainer/Dockerfile still built on oven/bun:1.3.13-alpine
- PI_BUN_VERSION in apps/sim/scripts/pi-sandbox-packages.ts was still 1.3.13,
  despite being documented as mirroring the root packageManager field, so Pi
  sandbox images would have kept installing the Bun release that cannot load
  the Next 16.3.0 server runtime.

Fixed surgically rather than with a repo-wide replace: "1.3.13" also appears
inside SVG path data in apps/sim/components/icons.tsx and
apps/docs/components/icons.tsx, which a blind sed would have corrupted.
2026-08-03 19:23:52 -07:00

213 lines
7.9 KiB
YAML

name: Desktop Release (macOS)
# Builds, signs, notarizes, and uploads the desktop app to an existing GitHub
# release. Ordering is load-bearing: scripts/create-single-release.ts skips
# creation when the tag already exists, so this workflow must never create the
# release itself — it only uploads assets after create-release ran (wired via
# workflow_call from ci.yml with needs: [create-release]).
on:
workflow_call:
inputs:
version:
description: Release tag (vX.Y.Z) to attach desktop artifacts to
required: true
type: string
publish:
description: Upload artifacts to the GitHub release
required: false
type: boolean
default: true
sign:
description: Sign and notarize with the Apple Developer identity. When
false (prerelease testing before the signing secrets exist) the build
is packaged unsigned; installed shells detect this and offer manual
downloads instead of Squirrel installs.
required: false
type: boolean
default: true
workflow_dispatch:
inputs:
version:
description: Release tag (vX.Y.Z) to attach desktop artifacts to
required: true
type: string
publish:
description: Upload artifacts to the GitHub release
required: false
type: boolean
default: false
sign:
description: Sign and notarize with the Apple Developer identity
required: false
type: boolean
default: true
permissions:
contents: write
jobs:
build-sign-notarize:
name: Build, Sign, Notarize
runs-on: macos-14
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: electron-cache-${{ runner.os }}-${{ hashFiles('apps/desktop/package.json') }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Inject release version
env:
VERSION: ${{ inputs.version }}
run: |
SEMVER="${VERSION#v}"
if ! [[ "$SEMVER" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.].+)?$ ]]; then
echo "Refusing to build: '$VERSION' is not a vX.Y.Z release tag" >&2
exit 1
fi
npm pkg set version="$SEMVER" --prefix apps/desktop
INJECTED="$(node -p "require('./apps/desktop/package.json').version")"
if [ "$INJECTED" != "$SEMVER" ]; then
echo "Version injection mismatch: wanted $SEMVER got $INJECTED" >&2
exit 1
fi
# Prerelease versions carry their environment in the tag: -alpha.N is a
# dev build, -beta.N a staging build. The channel decides the app's
# identity (name/bundle id — a separate app per environment, installable
# side by side) and the default origin baked into the bundle, which in
# turn selects the update feed the installed app polls.
- name: Resolve channel identity
id: channel
env:
VERSION: ${{ inputs.version }}
run: |
case "$VERSION" in
*-alpha.*)
NAME='Sim Dev'; APP_ID=ai.sim.desktop.dev; ORIGIN=https://www.dev.sim.ai ;;
*-beta.*)
NAME='Sim Staging'; APP_ID=ai.sim.desktop.staging; ORIGIN=https://www.staging.sim.ai ;;
*)
NAME='Sim'; APP_ID=ai.sim.desktop; ORIGIN='' ;;
esac
{
echo "name=$NAME"
echo "app_id=$APP_ID"
echo "origin=$ORIGIN"
} >> "$GITHUB_OUTPUT"
echo "Building $NAME ($APP_ID) default origin: ${ORIGIN:-production}"
- name: Bundle main and preload
working-directory: apps/desktop
env:
SIM_DESKTOP_DEFAULT_ORIGIN: ${{ steps.channel.outputs.origin }}
run: bun run build
- name: Write App Store Connect API key
if: ${{ inputs.sign }}
env:
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
run: |
mkdir -p "$RUNNER_TEMP/appstoreconnect"
printf '%s' "$APPLE_API_KEY_P8" > "$RUNNER_TEMP/appstoreconnect/AuthKey.p8"
chmod 600 "$RUNNER_TEMP/appstoreconnect/AuthKey.p8"
- name: Package, sign, and notarize
if: ${{ inputs.sign }}
working-directory: apps/desktop
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
# Absolute path — @electron/notarize reads this via Node fs, which
# does not expand a leading '~'.
APPLE_API_KEY: ${{ runner.temp }}/appstoreconnect/AuthKey.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
PRODUCT_NAME: ${{ steps.channel.outputs.name }}
APP_ID: ${{ steps.channel.outputs.app_id }}
run: >
bunx electron-builder --mac --publish never
-c.productName="$PRODUCT_NAME" -c.appId="$APP_ID"
# Unsigned prerelease path: no Developer ID, no notarization. The
# binaries end up ad-hoc/linker-signed, which runs locally but gets
# quarantined when downloaded — fine for testing the update pipeline.
- name: Package unsigned
if: ${{ !inputs.sign }}
working-directory: apps/desktop
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
PRODUCT_NAME: ${{ steps.channel.outputs.name }}
APP_ID: ${{ steps.channel.outputs.app_id }}
run: >
bunx electron-builder --mac --publish never -c.mac.notarize=false
-c.productName="$PRODUCT_NAME" -c.appId="$APP_ID"
- name: Validate signature and notarization
if: ${{ inputs.sign }}
run: |
DMG="$(ls apps/desktop/release/*.dmg | head -1)"
xcrun stapler validate "$DMG"
hdiutil attach "$DMG" -mountpoint /tmp/sim-dmg -nobrowse -quiet
spctl --assess --type execute --verbose /tmp/sim-dmg/*.app
codesign --verify --deep --strict /tmp/sim-dmg/*.app
hdiutil detach /tmp/sim-dmg -quiet
- name: Upload artifacts to the release
if: ${{ inputs.publish }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
# electron-builder's GitHub provider always names the manifest
# latest-mac.yml (channels are a generic-provider concept), and the
# update feed expects exactly that asset name on every release —
# normalize defensively in case a config change ever produces a
# channel-named manifest.
YML="$(find apps/desktop/release -maxdepth 1 -name '*-mac.yml' | head -1)"
if [ -z "$YML" ]; then
echo "::error::No *-mac.yml updater manifest found in apps/desktop/release"
exit 1
fi
if [ "$(basename "$YML")" != "latest-mac.yml" ]; then
mv "$YML" apps/desktop/release/latest-mac.yml
fi
gh release upload "$VERSION" \
apps/desktop/release/*.dmg \
apps/desktop/release/*.zip \
apps/desktop/release/*.blockmap \
apps/desktop/release/latest-mac.yml \
--clobber
- name: Upload artifacts to the workflow run
if: ${{ !inputs.publish }}
uses: actions/upload-artifact@v4
with:
name: sim-desktop-${{ inputs.version }}
path: |
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
apps/desktop/release/*.blockmap
apps/desktop/release/*-mac.yml
retention-days: 7