diff --git a/thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md b/thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md new file mode 100644 index 000000000..7067f9641 --- /dev/null +++ b/thoughts/shared/2026-05-06-desktop-rc1-ad-hoc-dry-run-report.md @@ -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=` 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=` 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 ` 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. diff --git a/thoughts/shared/research/2026-04-23-codex-vs-lightfast-desktop-production-gap.md b/thoughts/shared/research/2026-04-23-codex-vs-lightfast-desktop-production-gap.md index 5f49a3e5c..ad036c110 100644 --- a/thoughts/shared/research/2026-04-23-codex-vs-lightfast-desktop-production-gap.md +++ b/thoughts/shared/research/2026-04-23-codex-vs-lightfast-desktop-production-gap.md @@ -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 ` 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). diff --git a/thoughts/shared/research/2026-05-06-desktop-prod-readiness-status-verification.md b/thoughts/shared/research/2026-05-06-desktop-prod-readiness-status-verification.md index eea88f295..4d3526109 100644 --- a/thoughts/shared/research/2026-05-06-desktop-prod-readiness-status-verification.md +++ b/thoughts/shared/research/2026-05-06-desktop-prod-readiness-status-verification.md @@ -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 @@ -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: