diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index c5a2a8ecad1..fa3083f4220 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -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 @@ -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 }} @@ -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 @@ -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: @@ -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 @@ -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 @@ -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() @@ -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 @@ -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 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index ce6f3212a71..3b33249a679 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -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. diff --git a/apps/desktop/docs/electron-upgrade-checklist.md b/apps/desktop/docs/electron-upgrade-checklist.md index dbdd667bddf..157c8221e8a 100644 --- a/apps/desktop/docs/electron-upgrade-checklist.md +++ b/apps/desktop/docs/electron-upgrade-checklist.md @@ -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.