diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index f7f4daf469..a4dd4170f6 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -27,15 +27,15 @@ jobs: include: - name: Linux os: ubuntu-latest - script: electron:dist:linux + electron-target: --linux artifact-name: code-ux-linux - name: Windows os: windows-latest - script: electron:dist:win + electron-target: --win artifact-name: code-ux-windows - name: macOS os: macos-latest - script: electron:dist:mac + electron-target: --mac artifact-name: code-ux-macos env: @@ -94,7 +94,7 @@ jobs: run: pnpm run electron:install-deps - name: Build desktop package - run: pnpm run ${{ matrix.script }} + run: pnpm run build && pnpm run electron:prepare-deps && pnpm exec electron-builder --config electron-builder.config.cjs ${{ matrix.electron-target }} --publish never - name: Install and start desktop package run: pnpm run electron:smoke-installed diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index 845930ac3d..ea2916a6ea 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -22,15 +22,15 @@ jobs: include: - name: Linux os: ubuntu-latest - electron-script: electron:dist:linux + electron-target: --linux artifact-name: release-checks-linux - name: macOS os: macos-latest - electron-script: electron:dist:mac + electron-target: --mac artifact-name: release-checks-macos - name: Windows os: windows-latest - electron-script: electron:dist:win + electron-target: --win artifact-name: release-checks-windows env: @@ -92,7 +92,7 @@ jobs: run: pnpm run electron:install-deps - name: Build desktop package - run: pnpm run ${{ matrix.electron-script }} -- --publish never + run: pnpm run electron:prepare-deps && pnpm exec electron-builder --config electron-builder.config.cjs ${{ matrix.electron-target }} --publish never - name: Install and start release candidate run: pnpm run electron:smoke-installed diff --git a/docs-web/content/docs/developer-building-from-source.mdx b/docs-web/content/docs/developer-building-from-source.mdx index d5ee545f6c..ab761f1d56 100644 --- a/docs-web/content/docs/developer-building-from-source.mdx +++ b/docs-web/content/docs/developer-building-from-source.mdx @@ -172,11 +172,13 @@ The installed-Electron smoke uses each platform's native package and waits until backend and renderer are ready and the readiness marker is durably written. Linux and Windows then require the isolated app probe to exit with code zero. On Windows the harness passes the NSIS silent-install arguments verbatim so the required final `/D=` destination remains unquoted even -when that destination contains spaces, and retries only the known transient `0xC0000005` installer -access violation once in a fresh directory. On macOS the harness accepts the DMG's embedded MIT -license through `hdiutil` stdin, validates readiness, then owns probe teardown with `SIGTERM` and a -bounded `SIGKILL` fallback because Electron/AppKit can defer both Electron and Node exit paths. -Production Electron shutdowns still drain the embedded runtime normally. +when that destination contains spaces. The exact transient `0xC0000005` termination sometimes seen +while a GitHub runner inspects a newly produced unsigned installer receives bounded retries after +1.5, 5, and 15 seconds with fresh destinations; other failures are not retried. On macOS the harness +accepts the DMG's embedded MIT license through `hdiutil` stdin, validates readiness, then owns probe +teardown with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both +Electron and Node exit paths. Production Electron shutdowns still drain the embedded runtime +normally. They must also include `assets/models-dev/catalog.json`. The automatic token-pricing path reads this snapshot beside the compiled runtime; without it, known models can appear unpriced only in the desktop build. Electron packaging tests pin both runtime assets and a representative GPT-5.5 catalogue rate. diff --git a/docs-web/content/docs/developer-testing.mdx b/docs-web/content/docs/developer-testing.mdx index 6f0c4dfdc8..5a30620d06 100644 --- a/docs-web/content/docs/developer-testing.mdx +++ b/docs-web/content/docs/developer-testing.mdx @@ -131,7 +131,7 @@ It is staged as: The main branch ruleset still includes historical context names from older CI numbering and matrix definitions. Compatibility aggregate jobs emit those names only after the corresponding current backend, dashboard, audit, package, orchestration, 18-shard E2E, or desktop release-candidate gate succeeds. They preserve branch-protection compatibility without replacing any current validation job and can be removed once a repository administrator cleans up the obsolete ruleset entries. -`Playwright Diagnostics`, `Release Candidate Diagnostics`, and `Mockup Sprint Diagnostics` are manual-only rerun workflows. A PR cannot be merged with red CI. +`Playwright Diagnostics`, `Release Candidate Diagnostics`, and `Mockup Sprint Diagnostics` are manual-only rerun workflows. Desktop diagnostics invoke Electron Builder directly with an explicit platform target and `--publish never`, preventing CI auto-detection from turning an artifact-only validation into a publish attempt. A PR cannot be merged with red CI. ## Smoke test diff --git a/docs-web/developer/building-from-source.md b/docs-web/developer/building-from-source.md index 6b264b7dfd..7734129a3f 100644 --- a/docs-web/developer/building-from-source.md +++ b/docs-web/developer/building-from-source.md @@ -172,11 +172,13 @@ The installed-Electron smoke uses each platform's native package and waits until backend and renderer are ready and the readiness marker is durably written. Linux and Windows then require the isolated app probe to exit with code zero. On Windows the harness passes the NSIS silent-install arguments verbatim so the required final `/D=` destination remains unquoted even -when that destination contains spaces, and retries only the known transient `0xC0000005` installer -access violation once in a fresh directory. On macOS the harness accepts the DMG's embedded MIT -license through `hdiutil` stdin, validates readiness, then owns probe teardown with `SIGTERM` and a -bounded `SIGKILL` fallback because Electron/AppKit can defer both Electron and Node exit paths. -Production Electron shutdowns still drain the embedded runtime normally. +when that destination contains spaces. The exact transient `0xC0000005` termination sometimes seen +while a GitHub runner inspects a newly produced unsigned installer receives bounded retries after +1.5, 5, and 15 seconds with fresh destinations; other failures are not retried. On macOS the harness +accepts the DMG's embedded MIT license through `hdiutil` stdin, validates readiness, then owns probe +teardown with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both +Electron and Node exit paths. Production Electron shutdowns still drain the embedded runtime +normally. They must also include `assets/models-dev/catalog.json`. The automatic token-pricing path reads this snapshot beside the compiled runtime; without it, known models can appear unpriced only in the desktop build. Electron packaging tests pin both runtime assets and a representative GPT-5.5 catalogue rate. diff --git a/docs-web/developer/testing.md b/docs-web/developer/testing.md index 6f0c4dfdc8..5a30620d06 100644 --- a/docs-web/developer/testing.md +++ b/docs-web/developer/testing.md @@ -131,7 +131,7 @@ It is staged as: The main branch ruleset still includes historical context names from older CI numbering and matrix definitions. Compatibility aggregate jobs emit those names only after the corresponding current backend, dashboard, audit, package, orchestration, 18-shard E2E, or desktop release-candidate gate succeeds. They preserve branch-protection compatibility without replacing any current validation job and can be removed once a repository administrator cleans up the obsolete ruleset entries. -`Playwright Diagnostics`, `Release Candidate Diagnostics`, and `Mockup Sprint Diagnostics` are manual-only rerun workflows. A PR cannot be merged with red CI. +`Playwright Diagnostics`, `Release Candidate Diagnostics`, and `Mockup Sprint Diagnostics` are manual-only rerun workflows. Desktop diagnostics invoke Electron Builder directly with an explicit platform target and `--publish never`, preventing CI auto-detection from turning an artifact-only validation into a publish attempt. A PR cannot be merged with red CI. ## Smoke test diff --git a/docs/deployment/electron-desktop.md b/docs/deployment/electron-desktop.md index 9d2267d526..a73f6c2815 100644 --- a/docs/deployment/electron-desktop.md +++ b/docs/deployment/electron-desktop.md @@ -67,7 +67,7 @@ macOS DMG builds include the MIT license resource through `build/license_en.txt` - `pnpm run electron:dist:win`: build Windows targets. - `pnpm run electron:benchmark:runtime`: launch Electron with an isolated temporary user profile, navigate dashboard routes, probe backend endpoints, and write route/API/renderer/runtime metrics under `.cache/electron-runtime-benchmark/`. - `pnpm run electron:benchmark:win`: build Windows installers with `normal` and `store` compression and write timing/size data to `release/electron-benchmark/summary.json`. -- `pnpm run electron:smoke-installed`: select the native package in `release/electron/` whose artifact name matches the current `package.json` version, install it, start that installed app with an isolated profile, and wait until its backend and renderer are ready. Linux and Windows require the isolated app probe to exit with code zero. macOS validates the durably written readiness marker, then the parent smoke harness terminates only that validated probe with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both `app.exit()` and `process.exit()` after readiness. Linux requires passwordless `sudo` and `xvfb-run`, as provided by the release runners. Windows launches the silent NSIS installer with verbatim arguments because NSIS requires its final `/D=` destination segment to remain unquoted even when the path contains spaces; only the known transient `0xC0000005` installer access violation is retried once in a fresh directory. macOS supplies affirmative input to `hdiutil` so the CI-owned smoke can mount a DMG containing the embedded MIT license without an interactive terminal. +- `pnpm run electron:smoke-installed`: select the native package in `release/electron/` whose artifact name matches the current `package.json` version, install it, start that installed app with an isolated profile, and wait until its backend and renderer are ready. Linux and Windows require the isolated app probe to exit with code zero. macOS validates the durably written readiness marker, then the parent smoke harness terminates only that validated probe with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both `app.exit()` and `process.exit()` after readiness. Linux requires passwordless `sudo` and `xvfb-run`, as provided by the release runners. Windows launches the silent NSIS installer with verbatim arguments because NSIS requires its final `/D=` destination segment to remain unquoted even when the path contains spaces. GitHub Windows runners can transiently terminate a newly produced unsigned NSIS installer with `0xC0000005` while host security inspection settles, so only that exact status receives bounded retries after 1.5, 5, and 15 seconds, each with a fresh destination; every other installer failure remains immediate. macOS supplies affirmative input to `hdiutil` so the CI-owned smoke can mount a DMG containing the embedded MIT license without an interactive terminal. - `pnpm run electron:install-deps`: rebuild native app dependencies for Electron. The release output is written to `release/electron/`. @@ -113,11 +113,11 @@ Published desktop artifacts are built by `.github/workflows/release.yml` when a The workflow builds on native runners: -- `ubuntu-latest` runs `pnpm run electron:dist:linux` -- `windows-latest` runs `pnpm run electron:dist:win` -- `macos-latest` runs `pnpm run electron:dist:mac` +- `ubuntu-latest` runs Electron Builder with `--linux` +- `windows-latest` runs Electron Builder with `--win` +- `macos-latest` runs Electron Builder with `--mac` -Each release job uploads its generated files as a workflow artifact and attaches the same files to the published GitHub Release. Diagnostic rebuilds only upload workflow artifacts. +Each release job uploads its generated files as a workflow artifact and attaches the same files to the published GitHub Release. Diagnostic rebuilds only upload workflow artifacts. Both manual diagnostic workflows invoke Electron Builder directly with `--publish never`; forwarding that flag through the compound `electron:dist:*` package scripts is unsafe because pnpm can attach trailing arguments to the wrong command and let Electron Builder infer publishing from the CI environment. Release builds set `CSC_IDENTITY_AUTO_DISCOVERY=false`, so the default workflow produces unsigned desktop artifacts unless signing secrets and Electron Builder signing configuration are added later. @@ -129,7 +129,7 @@ Use `.github/workflows/release.yml` for published desktop releases. It is the la The no-secret release-candidate package lane is part of `.github/workflows/ci.yml`, named `Code UX CI Pipeline`. It runs for `main` validation and manual dispatches after package smoke, keeping the full desktop package proof out of the routine `dev` lane. -The `10 Release Candidate / desktop package` matrix starts as soon as the package smoke job passes, so desktop packaging can run beside the E2E and orchestration matrices instead of waiting for them to finish. It downloads the shared `codeux-build-linux` artifact, installs the cached Electron binary, rebuilds Electron native dependencies, prepares runtime assets, and runs Electron Builder directly with `--linux`, `--mac`, or `--win` plus `--publish never`. After compilation, every native runner installs its candidate—the Linux `.deb`, Windows NSIS `.exe`, or macOS app copied from the `.dmg`—and starts that installed copy with an isolated home and dashboard port. The Windows smoke preserves NSIS's required unquoted, final `/D=` destination segment when spawning the installer and retries only a first-attempt `0xC0000005` access violation in a clean destination. The macOS smoke accepts the DMG's embedded MIT license through `hdiutil` stdin before mounting. Success requires the packaged backend to start, the dashboard renderer to finish loading, and an atomic readiness marker to be written. Linux and Windows then require a zero-code probe exit; on macOS the parent harness owns teardown after validation, using `SIGTERM` with a short `SIGKILL` fallback so upstream AppKit exit deferral cannot turn a healthy installed app into a false failure. Built native artifacts are uploaded whenever packaging succeeded, even if the installed-app smoke fails, preserving evidence for release debugging. Production shutdowns remain unchanged and still drain the embedded server. The package smoke job that precedes it runs `node scripts/verify-release-install.mjs` with `CODE_UX_SKIP_RELEASE_INSTALL_BUILD=1`, so the npm tarball install check uses the same compiled artifact instead of rebuilding. +The `10 Release Candidate / desktop package` matrix starts as soon as the package smoke job passes, so desktop packaging can run beside the E2E and orchestration matrices instead of waiting for them to finish. It downloads the shared `codeux-build-linux` artifact, installs the cached Electron binary, rebuilds Electron native dependencies, prepares runtime assets, and runs Electron Builder directly with `--linux`, `--mac`, or `--win` plus `--publish never`. After compilation, every native runner installs its candidate—the Linux `.deb`, Windows NSIS `.exe`, or macOS app copied from the `.dmg`—and starts that installed copy with an isolated home and dashboard port. The Windows smoke preserves NSIS's required unquoted, final `/D=` destination segment when spawning the installer. It treats only the known transient `0xC0000005` runner termination as retryable, using three progressively longer waits and a fresh destination each time so a short host-security scan cannot make an otherwise valid candidate flaky. The macOS smoke accepts the DMG's embedded MIT license through `hdiutil` stdin before mounting. Success requires the packaged backend to start, the dashboard renderer to finish loading, and an atomic readiness marker to be written. Linux and Windows then require a zero-code probe exit; on macOS the parent harness owns teardown after validation, using `SIGTERM` with a short `SIGKILL` fallback so upstream AppKit exit deferral cannot turn a healthy installed app into a false failure. Built native artifacts are uploaded whenever packaging succeeded, even if the installed-app smoke fails, preserving evidence for release debugging. Production shutdowns remain unchanged and still drain the embedded server. The package smoke job that precedes it runs `node scripts/verify-release-install.mjs` with `CODE_UX_SKIP_RELEASE_INSTALL_BUILD=1`, so the npm tarball install check uses the same compiled artifact instead of rebuilding. Release-candidate packaging sets `CSC_IDENTITY_AUTO_DISCOVERY=false` for unsigned Electron packaging and passes `--publish never` to Electron Builder. It does not require provider API keys, npm publishing credentials, Docker credentials, GitHub Release events, or real project state. When Electron output exists, the workflow uploads files from `release/electron/` as workflow artifacts only; it does not publish to npm or attach files to a GitHub Release. @@ -141,11 +141,13 @@ Developers can reproduce the main-PR desktop package portion locally with: pnpm run build node scripts/verify-release-install.mjs pnpm run electron:install-deps -pnpm run electron:dist -- --publish never +pnpm run build +pnpm run electron:prepare-deps +pnpm exec electron-builder --config electron-builder.config.cjs --linux --publish never pnpm run electron:smoke-installed ``` -Use `pnpm run electron:dist:linux -- --publish never`, `pnpm run electron:dist:mac -- --publish never`, or `pnpm run electron:dist:win -- --publish never` when matching a specific GitHub Actions matrix leg. +Replace `--linux` with `--mac` or `--win` when matching another GitHub Actions matrix leg. ## Cross-Platform Compatibility Findings diff --git a/docs/development/testing-and-quality.md b/docs/development/testing-and-quality.md index 6f034baef2..c0b2974e52 100644 --- a/docs/development/testing-and-quality.md +++ b/docs/development/testing-and-quality.md @@ -227,7 +227,7 @@ The lane is intentionally numbered and staged: The main ruleset still contains nine historical context names from earlier CI numbering and matrix configuration. The workflow emits explicit compatibility aggregate jobs for those names only after their current backend, dashboard, audit, package, orchestration, 18-shard E2E, or release-candidate dependency has passed. These jobs do not replace or bypass validation; they bridge branch-protection naming until a repository administrator removes the obsolete contexts from ruleset `Protect main`. -The former standalone `Playwright Tests`, `Release Checks`, and `Mockup Sprint Orchestration` workflows are now manual diagnostics only: `Playwright Diagnostics`, `Release Candidate Diagnostics`, and `Mockup Sprint Diagnostics`. They remain useful for focused reruns, but the automatic PR signal comes from the numbered `Code UX CI Pipeline`. +The former standalone `Playwright Tests`, `Release Checks`, and `Mockup Sprint Orchestration` workflows are now manual diagnostics only: `Playwright Diagnostics`, `Release Candidate Diagnostics`, and `Mockup Sprint Diagnostics`. They remain useful for focused reruns, but the automatic PR signal comes from the numbered `Code UX CI Pipeline`. Desktop diagnostic jobs call Electron Builder directly with an explicit target and `--publish never`; do not forward publishing flags through the compound `electron:dist:*` scripts because CI auto-detection can otherwise trigger an unintended publish attempt. ### Main Release Version Gate diff --git a/scripts/smoke-installed-electron.mjs b/scripts/smoke-installed-electron.mjs index a177dd4370..09e866d084 100644 --- a/scripts/smoke-installed-electron.mjs +++ b/scripts/smoke-installed-electron.mjs @@ -19,7 +19,8 @@ const artifactDirectory = path.resolve( const timeoutMs = Number.parseInt(process.env.CODE_UX_ELECTRON_SMOKE_TIMEOUT_MS || "120000", 10); const packageJson = JSON.parse(await readFile(path.join(projectRoot, "package.json"), "utf8")); const WINDOWS_ACCESS_VIOLATION = 0xC0000005; -const WINDOWS_INSTALL_ATTEMPTS = 2; +const WINDOWS_INSTALL_RETRY_DELAYS_MS = [1_500, 5_000, 15_000]; +const WINDOWS_INSTALL_ATTEMPTS = WINDOWS_INSTALL_RETRY_DELAYS_MS.length + 1; const MAC_TERMINATION_GRACE_MS = 5_000; function runResult(command, args, options = {}) { @@ -106,10 +107,12 @@ async function installWindowsCandidate(temporaryRoot) { `${installer} failed during silent install: ${JSON.stringify(failures)}.`, ); } + const retryDelayMs = WINDOWS_INSTALL_RETRY_DELAYS_MS[attempt - 1]; console.warn( - `Windows installer hit transient access violation on attempt ${attempt}; retrying once in a fresh directory.`, + `Windows installer hit transient access violation on attempt ${attempt}; ` + + `retrying in ${retryDelayMs}ms with a fresh directory.`, ); - await new Promise((resolve) => setTimeout(resolve, 1_500)); + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); } throw new Error(`${installer} did not produce an installed application.`); } diff --git a/tests/backend/ci/workflow-health.test.ts b/tests/backend/ci/workflow-health.test.ts index f888268975..0dfdadbab4 100644 --- a/tests/backend/ci/workflow-health.test.ts +++ b/tests/backend/ci/workflow-health.test.ts @@ -328,7 +328,9 @@ describe("GitHub workflow health", () => { expect(releaseChecks).toContain("name: Release Candidate Diagnostics"); expectManualOnly(releaseChecks, "Release candidate diagnostics"); expect(releaseChecks).toContain("node scripts/verify-release-install.mjs"); - expect(releaseChecks).toContain("pnpm run ${{ matrix.electron-script }} -- --publish never"); + expect(releaseChecks).toContain("pnpm run electron:prepare-deps && pnpm exec electron-builder"); + expect(releaseChecks).toContain("${{ matrix.electron-target }} --publish never"); + expect(releaseChecks).not.toContain("pnpm run ${{ matrix.electron-script }}"); expect(releaseChecks).toContain(ELECTRON_INSTALL_SMOKE); expect(mockup).toContain("name: Mockup Sprint Diagnostics"); @@ -372,6 +374,9 @@ describe("GitHub workflow health", () => { expect(desktopRelease).toContain("permissions:\n contents: read"); expect(desktopRelease).toContain('GH_TOKEN: ""'); expect(desktopRelease).not.toContain("softprops/action-gh-release"); + expect(desktopRelease).toContain("pnpm run build && pnpm run electron:prepare-deps && pnpm exec electron-builder"); + expect(desktopRelease).toContain("${{ matrix.electron-target }} --publish never"); + expect(desktopRelease).not.toContain("pnpm run ${{ matrix.script }}"); expect(desktopRelease).toContain(ELECTRON_INSTALL_SMOKE); }); diff --git a/tests/backend/electron-builder-config.test.ts b/tests/backend/electron-builder-config.test.ts index 8d39a0321a..2c160bf474 100644 --- a/tests/backend/electron-builder-config.test.ts +++ b/tests/backend/electron-builder-config.test.ts @@ -133,8 +133,11 @@ describe("electron-builder packaged defaults", () => { expect(installerSmoke).toContain('["/S", `/D=${installDirectory}`]'); expect(installerSmoke).toContain("windowsVerbatimArguments: true"); expect(installerSmoke).toContain("WINDOWS_ACCESS_VIOLATION"); + expect(installerSmoke).toContain("WINDOWS_INSTALL_RETRY_DELAYS_MS = [1_500, 5_000, 15_000]"); + expect(installerSmoke).toContain("WINDOWS_INSTALL_RETRY_DELAYS_MS.length + 1"); expect(installerSmoke).toContain("normalizedStatus === WINDOWS_ACCESS_VIOLATION"); - expect(installerSmoke).toContain("retrying once in a fresh directory"); + expect(installerSmoke).toContain("WINDOWS_INSTALL_RETRY_DELAYS_MS[attempt - 1]"); + expect(installerSmoke).toContain("retrying in ${retryDelayMs}ms with a fresh directory"); expect(installerSmoke).toContain('findArtifact(".dmg")'); expect(installerSmoke).toContain('run("hdiutil", ["attach"'); expect(installerSmoke).toContain('input: "Y\\n"');