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
141 changes: 141 additions & 0 deletions thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
date: 2026-05-06
owner: jp@jeevanpillay.com
branch: main
plan: thoughts/shared/plans/2026-05-06-desktop-rc1-ad-hoc-dry-run.md
status: complete
final_tag: "@lightfast/desktop@0.1.0-rc.4"
final_commit: ac986f9a9
final_workflow: https://github.com/lightfastai/lightfast/actions/runs/25423025160
prs:
- 638 # codesign correctness pre-fixes
- 639 # tagPrefix + slash-safe Sentry release id
- 640 # vite sourcemaps
- 641 # observability hardening (debug-id injection + IPC bridge)
- 642 # restore `releases new` before finalize
- 643 # bridge renderer errors to main Sentry SDK
---

# Desktop `0.1.0-rc.1` β†’ `0.1.0-rc.4` Ad-Hoc Dry-Run β€” Final Report

## TL;DR

Cutting four release candidates uncovered seven distinct release-pipeline bugs that would have shipped silently on the first developer-id build. All seven are fixed and merged. The ad-hoc workflow now runs green end-to-end, produces a launchable signed-equivalent `.app`, and registers a Sentry release with paired-debug-id sourcemaps. The renderer-error β†’ Sentry bridge is the only piece that's not directly observable today (Sentry org is over its free-tier quota), but every link in the chain up to and including `Sentry.captureException` execution is verified.

## What this dry-run was for

Per the plan: cut `@lightfast/desktop@0.1.0-rc.1` in ad-hoc mode to exercise ~90% of the release pipeline before Apple Developer enrollment unblocks. The bet was that latent bugs in the pipeline would surface on real tags, not on local `pnpm package` smoke-runs. That bet paid out β€” the local run was green for every fix that later shipped, but seven bugs only surfaced after pushing real tags.

## Final pipeline state (rc.4 evidence)

| Component | Evidence |
|---|---|
| Workflow | run `25423025160`, all jobs βœ… |
| Tag β†’ release | `@lightfast/desktop@0.1.0-rc.4` undrafted, prerelease, 6 assets (2 dmg + 2 zip + 2 update feeds) |
| Codesign (ad-hoc) | `optionsForFile` camelCase keys honored by `@electron/osx-sign@1.3.3`; ad-hoc fallback (`identity: "-"`, `hardenedRuntime: false`) produces a launchable arm64 `.app` post-quarantine clear |
| Sourcemaps | Vite emits `.map` for main/preload/renderer; `packageAfterCopy` Forge hook injects matching `//# debugId=<uuid>` into staging asar bundles before pack |
| Sentry release | `lightfast-desktop@0.1.0-rc.4+6` created β†’ uploaded β†’ finalized; 5 sources + 5 maps with paired debug-ids |
| Runtime release id matches | `apps/desktop/src/main/sentry.ts:23-26` computes `lightfast-desktop@${version}+${buildNumber}`, identical to the upload script's transform |
| Build provenance | GitHub attestation issued for both arm64 and x64 dmg/zip |
| Renderer error pipeline | renderer throw β†’ `installErrorBoundary` window error listener β†’ `lightfastBridge.reportError` IPC β†’ main `Sentry.captureException` (verified in rc.4 smoke test via CDP throw + `[renderer]` log line in main process stdout) |

## Bugs found and fixed

Seven distinct bugs. Each one would have shipped silently into a developer-id build.

### Bug A β€” Sentry release id contained `/`

**PR #639.** `pkg.name` is `@lightfast/desktop`, so the upload script built release id `@lightfast/desktop@0.1.0-rc.1+1`. sentry-cli rejected with "Slashes and certain whitespace characters are not permitted." Fix: strip leading `@` and replace `/` with `-` in both `apps/desktop/scripts/upload-sourcemaps.mjs` and `apps/desktop/src/main/sentry.ts` so runtime release id matches uploaded sourcemaps.

### Bug B β€” Forge `tagPrefix` defaulted to `v`

**PR #639.** `PublisherGithub` defaulted to creating release tags as `v0.1.0-rc.1` while the workflow draft was created at `@lightfast/desktop@0.1.0-rc.1`. Result: a parallel `v0.1.0-rc.1` release with all 4 build assets, while the workflow's `@lightfast/desktop@0.1.0-rc.1` draft stayed empty and got force-undrafted with zero assets. Fix: `tagPrefix: "@lightfast/desktop@"` on the publisher.

### Bug C β€” Vite emitted no `.map` files

**PR #640.** Default Vite library mode does not emit sourcemaps β€” `electron-forge plugin-vite` doesn't override. Without `.map` files there's nothing for sentry-cli to upload, no debug-ids, no symbolication. Fix: `build: { sourcemap: true, ... }` in all three `vite.{main,preload,renderer}.config.ts`.

### Bug D β€” `LIGHTFAST_REMOTE_DEBUG_PORT` env var not honored in packaged builds

**Deferred (intentional security hardening).** `bootstrap.ts` gates the env-var bind path with `if (!app.isPackaged)`. Workaround: pass `--remote-debugging-port=9222` as a CLI flag directly. Documented in plan; not a release-pipeline bug, leaving as-is for prod hardening.

### Bug E β€” No debug-id comments in shipped asar bundles

**PR #641.** Even with `.map` files emitted, `sentry-cli sourcemaps inject` was running against the build artifacts after asar pack β€” too late. The asar still carried bundles without `//# debugId=<uuid>` comments, so symbolication couldn't pair runtime stacks to uploaded sourcemaps. Forge's `prePackage` user hook runs *before* plugin-vite (opposite of expected), so injection there happens too early. Fix: switch to `packageAfterCopy` (runs after vite + after copy to staging dir, before asar pack), inject debug-ids into the staging dir, then mirror the modified files back to source `.vite/` so the post-package sourcemap upload reads the same ids that got packed.

### Bug F β€” Renderer SDK silently fails to register a client (v10 carrier shape)

**PR #641 + PR #643.** First half (#641): renderer was using `@sentry/browser` directly, which fetches the Sentry ingest URL β€” blocked by renderer CSP. Switched to `@sentry/electron/renderer` which routes through main via the `sentry-ipc:` CSP-bypass scheme, with `@sentry/electron/preload` installing the bridge.

Second half (#643): the `sentry-ipc:` bridge worked (`__SENTRY_IPC__` was exposed, no more CSP fetch errors), but `__SENTRY__["10.47.0"]` carrier showed `clientPresent: false`, `defaultClient: null`, no `acs` key. `Sentry.init()` was a silent no-op in the renderer. Investigation ruled out the `Z9()` browser-extension guard (returned `false` correctly because `chrome.runtime.id` was undefined). Root cause never pinned exactly, but the symptom was reproducible across rc.1, rc.2, rc.3 (all three: zero events ingested by Sentry).

Fix: bridge renderer errors through main's `@sentry/electron/main` SDK (which has a working client). `installErrorBoundary` already IPCs renderer errors to main via `lightfastBridge.reportError`; the new code in `apps/desktop/src/main/index.ts:74-92` forwards those payloads to `Sentry.captureException` with `bundle: "renderer"` tag and the renderer-side stack preserved (so debug-id sourcemaps still symbolicate). Renderer-side `Sentry.init` removed entirely; `@sentry/electron/preload` import removed; v10 deps (`@sentry/browser`, `@sentry-internal/*`, `@sentry/node`, `@sentry/core`) deleted from `package.json` since nothing imports them. Renderer bundle: 525K β†’ 421K. Preload: 2.5K (was much larger with the bridge).

### Bug G β€” `releases finalize` failed without `releases new`

**PR #642.** PR #641 switched from `releases files upload-sourcemaps --url-prefix` to modern `sourcemaps upload --release` β€” but assumed `sourcemaps upload --release X` would auto-create release `X`. It does not. The subsequent `releases finalize X` step then failed with "release does not exist." Fix: restore explicit `sentry-cli releases new <release>` as the first step before upload.

## Other latent bugs surfaced

These were not bugs in the release pipeline per se, but became visible during the dry-run and would have caused regressions:

### Crash regression β€” `factory.ts` `import.meta` polyfill

**PR #641.** While modifying `apps/desktop/src/main/windows/factory.ts` for observability hardening, used `dirname(fileURLToPath(import.meta.url))`. Vite emits the main bundle as CJS and Rollup strips `import.meta` to literal `{}` in CJS output (no polyfill). So `fileURLToPath({}.url) === fileURLToPath(undefined)` β†’ crash on launch. Same applied to `import.meta.dirname`. Fix: use `__dirname` (CJS-native) with biome-ignore comment because lint forbids globals.

### `gh attestation list --repo` flag invalid

**Deferred.** `gh attestation list` doesn't support `--repo`; verification of build provenance attestation across the repo can't be scripted via `gh attestation list`. Not blocking β€” attestations are issued and verifiable individually.

### Sentry org auth token scope (`org:ci`)

**Documented, not fixed.** The org-level token has scope `org:ci` which covers releases/sourcemaps but not issue read (`/api/0/projects/.../issues/`). Personal token has more. Either scope is acceptable for the upload pipeline; only investigation tooling needed personal-token level access.

## What changed in the codebase

```
apps/desktop/forge.config.ts # G-2 camelCase, tagPrefix, packageAfterCopy debug-id hook
apps/desktop/build/entitlements.mac.inherit.plist # G-3 dropped disable-library-validation
apps/desktop/scripts/upload-sourcemaps.mjs # slash-safe release id, releases new + finalize, modern sourcemaps upload
apps/desktop/src/main/sentry.ts # release id transform mirrors upload script
apps/desktop/src/main/index.ts # forwardRendererErrorToSentry IPC β†’ captureException bridge
apps/desktop/src/main/windows/factory.ts # __dirname instead of import.meta (CJS strip)
apps/desktop/src/preload/preload.ts # dropped @sentry/electron/preload + sentryInit field
apps/desktop/src/renderer/src/main.ts # dropped @sentry/electron/renderer (no-op v10 init)
apps/desktop/src/shared/ipc.ts # dropped getSentryInitOptionsSync + SentryInitSnapshot
apps/desktop/vite.{main,preload,renderer}.config.ts # sourcemap: true
apps/desktop/package.json # removed dead v10 deps
```

## Pipeline timing reference

End-to-end wall time per cut (push tag β†’ undrafted release):

| rc | wall time | result |
|---|---|---|
| rc.1 | ~7 min (failed first attempt β€” slash in release id; re-cut succeeded) | green after fix |
| rc.2 | ~7 min | green |
| rc.3 | ~9 min (failed first attempt β€” `releases finalize` without `releases new`; re-cut succeeded) | green after fix |
| rc.4 | ~7 min | green |

Build job: ~5 min per arch, parallel. Finalize: ~30s. Bottleneck is per-arch build, not Sentry/release steps.

## What's left for the first developer-id cut

Once Apple Developer enrollment lands:

1. Provision Apple secrets in repo: `APPLE_SIGNING_IDENTITY`, `APPLE_TEAM_ID`, `APPLE_NOTARIZE_API_KEY_*` (set, contents, key id, issuer id). Workflow auto-flips `signingMode` to `developer-id` based on `APPLE_SIGNING_IDENTITY` presence (`desktop-release.yml:120-124`).
2. Confirm `forge.config.ts` developer-id branch β€” camelCase keys are pre-fixed (PR #638), but verify locally with the real cert before tagging.
3. Tag `@lightfast/desktop@0.1.0` (drop the `-rc.N` suffix for non-prerelease).
4. After undraft, smoke-test the signed `.dmg` end-to-end: launch from Applications without quarantine clear; trigger renderer error; confirm Sentry receives an event with symbolicated stack (requires Sentry quota restored).
5. Verify Sparkle update feed serves the new build to existing rc.* installs (updater is gated off in ad-hoc; switching to developer-id flips `updater.ts:87-89` on).

## Open items (not blockers)

- **Sentry quota.** Org is on free tier and over quota. New events get accepted into stats but not into searchable issues until quota restores. Doesn't affect the release pipeline; affects observability of any error post-release.
- **Bug D (`LIGHTFAST_REMOTE_DEBUG_PORT` in packaged builds).** Intentional `if (!app.isPackaged)` gate. CDP via CLI flag works as a manual workaround. Can revisit if a documented dev-friendly debug path becomes important.
- **Bug F root cause.** Pinned the symptom (no client registered in v10 carrier) but not the underlying reason `Sentry.init` no-ops. Unblocked by the main-side bridge but worth filing upstream if it recurs in a future SDK upgrade.

## Plan vs. reality

Plan called for three phases (status update, codesign pre-fixes, rc.1 cut). Reality required four `rc.N` cuts to surface and fix everything. The plan's premise β€” that real tags surface bugs that local `pnpm package` doesn't β€” held: every one of the seven bugs above was invisible until a real tag pushed. The ad-hoc dry-run was the right call.
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,40 @@ G-7 from the verification doc (custom URL scheme audit) is **closed** β€” verifi
3. Cut `@lightfast/desktop@0.1.0-rc.1` ad-hoc dry-run (G-5 partial). Validates everything except codesign + notarize.
4. When Apple unblocks: provision the remaining 8 secrets, cut `@lightfast/desktop@0.1.0-rc.2` (signed), verify codesign + notarize + stapled ticket. Promote to `@lightfast/desktop@0.1.0`.
5. Branch protection (G-4) β€” repo Settings UI. Do this after rc.1 to confirm `Desktop CI` is the right check name to require.

## Status Update 2026-05-06 (post-dry-run)

The ad-hoc dry-run plan ([`thoughts/shared/plans/2026-05-06-desktop-rc1-ad-hoc-dry-run.md`](../plans/2026-05-06-desktop-rc1-ad-hoc-dry-run.md)) executed end-to-end. Final report: [`thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md`](../2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md). Four release candidates (`rc.1` β†’ `rc.4`) cut against `main`; final tag `@lightfast/desktop@0.1.0-rc.4` at `ac986f9a9` produced a green workflow, six assets, and a Sentry release with paired-debug-id sourcemaps.

### Gate closures

| Gate | Prior state | Outcome |
|---|---|---|
| **G-1** Apple secrets + Sentry secrets | external blocker | **Sentry side closed** (`SENTRY_DSN`, `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT` provisioned via `sentry-cli` + `gh secret set`). Apple side still pending β€” workflow continues to flip to ad-hoc when `APPLE_SIGNING_IDENTITY` absent. |
| **G-2** osxSign kebab-case | open | **Closed** by PR #638 β€” camelCase rename + per-file ones moved into `optionsForFile`. |
| **G-3** inherit plist `disable-library-validation` | open | **Closed** by PR #638 β€” dropped from `entitlements.mac.inherit.plist`. Ad-hoc cut launches; signed-cut helper-validation behavior TBD on first developer-id tag. |
| **G-4** Desktop CI branch protection | open (UI-only) | **Unchanged**. Worth doing after the first developer-id cut to confirm the check name remains `Desktop CI / Typecheck + package (unsigned)`. |
| **G-5** First end-to-end dry run | open | **Closed** β€” rc.1 β†’ rc.4 cut and verified. See report for the seven distinct pipeline bugs surfaced and fixed. |
| **G-6** Auto-updater disabled for ad-hoc | by design | **Unchanged**. |
| **G-7** Deep-link removal post-N-3 | unverified | **Closed** in dry-run plan Phase 1 β€” `grep` confirmed zero `setAsDefaultProtocolClient` / `onDeepLink` / `open-url` references in `apps/desktop/src/`. |

### New gates surfaced and closed during the dry-run

These were invisible to local `pnpm package`; only real tag pushes surfaced them. All seven are now closed.

| Gate | Issue | Fix |
|---|---|---|
| **G-8** | `PublisherGithub` defaulted `tagPrefix: "v"`, creating a parallel `v0.1.0-rc.1` release alongside the workflow's `@lightfast/desktop@0.1.0-rc.1` draft (which stayed empty) | PR #639 β€” `tagPrefix: "@lightfast/desktop@"` |
| **G-9** | sentry-cli rejects `/` in release id; `@lightfast/desktop@…` parsed as path | PR #639 β€” strip `@`, replace `/` with `-` in both `apps/desktop/scripts/upload-sourcemaps.mjs` and `apps/desktop/src/main/sentry.ts` |
| **G-10** | Vite library mode emits no `.map` files by default | PR #640 β€” `build.sourcemap: true` in all three `vite.{main,preload,renderer}.config.ts` |
| **G-11** | `sentry-cli sourcemaps inject` ran post-asar-pack; user-defined Forge `prePackage` hook fires *before* plugin-vite (opposite of expected) | PR #641 β€” switched to `packageAfterCopy` hook (runs after vite + after copy to staging, before asar pack); inject into staging dir, mirror back to source `.vite/` |
| **G-12** | `@sentry/electron/renderer` `Sentry.init` is a silent no-op in v10 carrier β€” `__SENTRY__["10.47.0"]` never registers a client. Root cause not pinned | PR #643 β€” bridge renderer errors through main's `@sentry/electron/main` SDK via `IpcChannels.rendererError` β†’ `Sentry.captureException`. Renderer-side init dropped; preload `@sentry/electron/preload` import dropped; v10 deps removed |
| **G-13** | `sentry-cli sourcemaps upload --release X` does not auto-create release `X`; subsequent `releases finalize X` fails | PR #642 β€” restore explicit `sentry-cli releases new <release>` as first step before upload |
| **G-14** | Vite CJS Rollup strips `import.meta` to literal `{}` (no polyfill); `import.meta.url` and `import.meta.dirname` both crash on access | PR #641 β€” use `__dirname` in `apps/desktop/src/main/windows/factory.ts` (CJS-native), with biome-ignore comment |

### Remaining gates for first-class signed v0.1.0

- **G-1 (Apple half)** β€” 8 Apple secrets still pending: `APPLE_SIGNING_IDENTITY`, `APPLE_TEAM_ID`, `APPLE_CERT_BASE64`, `APPLE_CERT_PASSWORD`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER`, `APPLE_API_KEY_CONTENT`, `KEYCHAIN_PASSWORD`. Workflow auto-flips `signingMode` to `developer-id` when `APPLE_SIGNING_IDENTITY` lands.
- **G-4** β€” Branch protection still UI-only; do after first signed cut.
- **G-6** β€” Auto-updater stays disabled until first signed cut (intentional).
- **First signed cut** β€” once Apple secrets land, drop the `-rc.N` suffix and tag `@lightfast/desktop@0.1.0`. Smoke test: launch from Applications without quarantine clear; confirm Sparkle update feed serves the new build to existing rc.* installs (updater flips on automatically).
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ git_commit: ab634170e1eafdbb4e89fbd2255819cbe53178e3
branch: main
topic: "apps/desktop production-readiness: verification of Codex-gap Status Tracker"
tags: [research, desktop, electron, production, codex-gap-verification, sparkle, signing, sentry, ci]
status: complete
status: superseded-by-dry-run-2026-05-06
superseded_by: thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md
last_updated: 2026-05-06
based_on:
- thoughts/shared/research/2026-04-23-codex-vs-lightfast-desktop-production-gap.md
Expand Down Expand Up @@ -37,6 +38,8 @@ What remains to ship the first signed v0.1.0:

Everything else in the Status Tracker that was marked DEFERRED or RELEASE remains intentionally out of scope for v0.1.0.

> **Update 2026-05-06 (post-dry-run).** This document is **superseded** by the rc.1 β†’ rc.4 dry-run, captured at [`thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md`](../2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md). Of the seven gaps listed in Β§"Gaps / Risks Still Blocking Prod" below: **G-2** (osxSign kebab-case) closed by PR #638; **G-3** (inherit plist `disable-library-validation`) closed by PR #638; **G-5** (first-release dry run) closed by rc.4 cut at `ac986f9a9` (workflow run [`25423025160`](https://github.com/lightfastai/lightfast/actions/runs/25423025160)); **G-7** (deep-link audit) closed in dry-run plan Phase 1. **G-1** is now Apple-half only β€” Sentry secrets fully provisioned. **G-4** and **G-6** unchanged. Seven additional gates surfaced and closed during the dry-run (PRs #639–#643); the gap research at [`2026-04-23-codex-vs-lightfast-desktop-production-gap.md`](2026-04-23-codex-vs-lightfast-desktop-production-gap.md) Β§"Status Update 2026-05-06 (post-dry-run)" enumerates them as G-8…G-14. The Β§"Open Questions" at the bottom of this doc are also resolved by the dry-run except for the Sparkle public-key follow-up.

## Verification Method

For each row in the Status Tracker:
Expand Down
Loading