Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 9 additions & 36 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,16 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
commit: ${{ steps.source.outputs.commit }}
name: ${{ steps.channel.outputs.name }}
app_id: ${{ steps.channel.outputs.app_id }}
origin: ${{ steps.channel.outputs.origin }}
release_repository: ${{ steps.channel.outputs.release_repository }}
token_kind: ${{ steps.channel.outputs.token_kind }}
smoke_shards: ${{ steps.smoke.outputs.shards }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.sha }}

# Prerelease versions carry their environment in the tag: -dev.N is a
# dev build, -staging.N a staging build. Legacy -alpha/-beta tags remain
Expand Down Expand Up @@ -108,7 +105,6 @@ jobs:
echo "Building $NAME ($APP_ID) for $RELEASE_REPOSITORY; default origin: ${ORIGIN:-production}"

- name: Validate release source
id: source
env:
PUBLISH: ${{ inputs.publish }}
SIGN: ${{ inputs.sign }}
Expand All @@ -123,6 +119,10 @@ jobs:
echo "::error::Manual desktop releases must use a stable source-repository tag."
exit 1
fi
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] && [ "$GITHUB_REF" != "refs/tags/$VERSION" ]; then
echo "::error::Run this workflow from the $VERSION tag, so it executes only that tag's code and its caches stay scoped to that tag."
exit 1
fi
if [ "$PUBLISH" = true ] && [ "$SIGN" != true ]; then
echo "::error::Desktop releases must be signed before publication."
exit 1
Expand All @@ -137,17 +137,6 @@ jobs:
fi
fi

echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Select available smoke partitioning
id: smoke
run: |
if [ -f scripts/desktop-e2e.ts ]; then
echo 'shards=[1,2,3,4]' >> "$GITHUB_OUTPUT"
else
echo 'shards=[1]' >> "$GITHUB_OUTPUT"
fi

- name: Validate release authentication
if: ${{ inputs.publish }}
env:
Expand All @@ -168,14 +157,13 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(needs.source.outputs.smoke_shards) }}
shard: [1, 2, 3, 4]
steps:
- &checkout-source
name: Checkout verified source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
ref: ${{ needs.source.outputs.commit }}

- &setup-bun
name: Setup Bun
Expand Down Expand Up @@ -218,9 +206,7 @@ jobs:
exit 1
fi

- &native-cache
name: Restore native compilation
if: hashFiles('.github/actions/desktop-native-cache/action.yml') != ''
- name: Restore native compilation
uses: ./.github/actions/desktop-native-cache

- &bundle-desktop
Expand All @@ -245,12 +231,7 @@ jobs:
BACKGROUND_EXECUTOR_REPORT_PATH: test-results/background-executor-report.json
TERMINAL_CANCEL_REPORT_PATH: test-results/terminal-cancel-report.json
CONTINUE_SIGN_IN_REPORT_PATH: test-results/continue-sign-in-report.json
run: |
if [ -f ../../scripts/desktop-e2e.ts ]; then
bun run ../../scripts/desktop-e2e.ts ${{ matrix.shard }}/${{ strategy.job-total }}
else
bun run test:e2e --reporter=line,json
fi
run: bun run ../../scripts/desktop-e2e.ts ${{ matrix.shard }}/${{ strategy.job-total }}

- name: Upload smoke coverage and timings
if: always()
Expand All @@ -271,6 +252,8 @@ jobs:
if-no-files-found: ignore
retention-days: 7

# Restores no caches: every byte that gets signed comes from this checkout or
# a fresh download, never from a cache another run could have written.
build:
name: build
needs: source
Expand All @@ -285,20 +268,10 @@ jobs:

- *setup-node

- name: Cache Electron binaries
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: electron-cache-${{ runner.os }}-${{ hashFiles('apps/desktop/package.json') }}

- *install-dependencies

- *inject-version

- *native-cache

- name: Verify desktop source
run: |
bun run --cwd apps/desktop lint:check
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ The build also derives the app icon from `SIM_DESKTOP_DEFAULT_ORIGIN`. Every cha

CI (`.github/workflows/desktop-release.yml`, wired into `ci.yml`):
- Stable builds run only after `release` on a `vX.Y.Z:` commit to main — **never before**: `scripts/create-single-release.ts` skips creation if the tag exists, so a desktop job publishing first would eat the changelog. Stable assets remain on `simstudioai/sim`; dev/staging assets publish to the public `simstudioai/sim-desktop-releases` repository so source-repository followers are not notified for internal shell builds. The job builds `--publish never`; reruns verify the size and SHA-256 digest of existing release assets instead of overwriting them.
- **Secrets gate**: `desktop-signing` in `ci.yml` probes the six Apple secrets and skips the desktop job with a warning until they exist — releases never fail on a missing Apple account, and the first release after the secrets land ships desktop artifacts automatically. Manual/one-off builds: Actions → "desktop-release" → Run workflow with a `vX.Y.Z` version (`publish: false` uploads artifacts to the run instead of the release).
- **Secrets gate**: `desktop-signing` in `ci.yml` probes the six Apple secrets and skips the desktop job with a warning until they exist — releases never fail on a missing Apple account, and the first release after the secrets land ships desktop artifacts automatically. Manual/one-off builds: Actions → "desktop-release" → Run workflow, select the `vX.Y.Z` tag under "Use workflow from", and enter the same version (`publish: false` uploads artifacts to the run instead of the release). Any other ref is refused, so a run only executes the tag's own code and workflow, and its caches stay scoped to that tag.
- **Cache trust**: the signed `build` job restores no caches, so nothing it signs can come from a cache another run wrote. Only the unsigned smoke jobs use caches.
- The product semver is **injected** from the release tag into `apps/desktop/package.json` at build time (repo package versions are placeholders). A mismatch guard fails the build.
- Fuses are flipped at package time (`electronFuses` in `electron-builder.yml`): runAsNode off, NODE_OPTIONS off, inspect args off, ASAR-only + integrity validation, and cookie encryption on. The packaged smoke test asserts every fuse byte so Electron upgrades fail until new fuses receive an explicit policy.
- **Cookie-encryption go/no-go**: on every Electron bump, verify a packaged build keeps its session across relaunch (there are historical cookie-persistence bugs with the `EnableCookieEncryption` fuse). If it reproduces, set `enableCookieEncryption: false` and record it here.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/docs/electron-upgrade-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ The rendering-parity guarantee (identical to Chrome of the pinned version) is on
- Workflow canvas (WebGL/ReactFlow), Monaco editing, a table export download.
- Offline page appears with networking off; Retry recovers.
6. **E2E:** `bun run test:e2e` green locally on the new pin; `desktop-e2e.yml` green in CI (the `latest` canary leg should already have hinted at surprises).
7. **Signing/notarization smoke:** run `desktop-release.yml` via `workflow_dispatch` with `publish: false` against a test tag; `spctl`/`stapler` steps must pass.
7. **Signing/notarization smoke:** run `desktop-release.yml` via `workflow_dispatch` with `publish: false`, selecting a test tag under "Use workflow from" and entering the same version; `spctl`/`stapler` steps must pass.
8. **Ship** behind a staged rollout (10% `stagingPercentage`) and watch `update_error` / `renderer_gone` rates in the event logs before raising.
Loading