diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index f4db35bc314..ac85f7051c8 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -1316,6 +1316,38 @@ jobs: # Kill only its child and require both PTY and watch recovery before packaging. node config/scripts/relay-watcher-fault-harness.mjs + # Why: main ships minified with sourcemap:'hidden' and packaging drops + # out/**/*.map from app.asar, so a crash trace from a released build is + # otherwise undecodable. The main bundle is platform-independent, so one + # leg publishes the maps for the whole release. + - name: Bundle main-process source maps + if: matrix.platform == 'linux-x64' + shell: bash + env: + TAG: ${{ needs.cut.outputs.tag }} + run: | + set -euo pipefail + if [ -z "$(find out/main -name '*.js.map' -print -quit)" ]; then + echo "::error::No main-process source maps in out/main. Did build.sourcemap regress in electron.vite.config.ts?" + exit 1 + fi + # Why: every entry in electron-builder's `files` is a negation, so + # app-builder prepends `**/*` and packs anything left in the workspace + # root into app.asar. Stage the bundle outside the checkout instead. + find out/main -name '*.js.map' -print | sort | zip -q -X "$RUNNER_TEMP/orca-sourcemaps-$TAG.zip" -@ + ls -l "$RUNNER_TEMP/orca-sourcemaps-$TAG.zip" + + - name: Publish main-process source maps + if: matrix.platform == 'linux-x64' + uses: nick-fields/retry@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: gh release upload "${{ needs.cut.outputs.tag }}" "${{ runner.temp }}/orca-sourcemaps-${{ needs.cut.outputs.tag }}.zip" --clobber --repo "${{ github.repository }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish release artifacts (Linux) if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64' uses: nick-fields/retry@v4 diff --git a/config/scripts/release-cut-sourcemap-publish.test.mjs b/config/scripts/release-cut-sourcemap-publish.test.mjs new file mode 100644 index 00000000000..d8da0c0b712 --- /dev/null +++ b/config/scripts/release-cut-sourcemap-publish.test.mjs @@ -0,0 +1,58 @@ +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +const projectDir = resolve(import.meta.dirname, '../..') +const buildSteps = parse( + readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8') +).jobs.build.steps + +function stepIndex(name) { + const index = buildSteps.findIndex((step) => step.name === name) + expect(index, `missing build step: ${name}`).toBeGreaterThanOrEqual(0) + return index +} + +describe('release-cut source map publication', () => { + it('bundles and uploads main source maps from exactly one platform leg', () => { + const bundle = buildSteps[stepIndex('Bundle main-process source maps')] + const publish = buildSteps[stepIndex('Publish main-process source maps')] + + // Why: the main bundle is platform-independent, so duplicating the ~8MB + // artifact across legs would only race the uploads against each other. + for (const step of [bundle, publish]) { + expect(step.if).toContain('linux-x64') + } + + expect(bundle.run).toContain("find out/main -name '*.js.map'") + expect(publish.with.command).toContain('gh release upload') + expect(publish.with.command).toContain('orca-sourcemaps-') + }) + + it('stages the bundle outside the checkout so packaging cannot absorb it', () => { + // Why: electron-builder's `files` is all negations, so app-builder prepends + // `**/*` and packs any stray workspace-root file into app.asar. + const bundle = buildSteps[stepIndex('Bundle main-process source maps')] + const publish = buildSteps[stepIndex('Publish main-process source maps')] + + expect(bundle.run).toContain('"$RUNNER_TEMP/orca-sourcemaps-$TAG.zip"') + expect(bundle.run).not.toMatch(/zip[^\n]*\s"orca-sourcemaps-/) + expect(publish.with.command).toContain('runner.temp') + }) + + it('fails the release when no source maps were emitted', () => { + // Why: a silent regression of build.sourcemap would ship an undecodable + // release rather than an obviously broken one. + const bundle = buildSteps[stepIndex('Bundle main-process source maps')] + expect(bundle.run).toContain('::error::') + expect(bundle.run).toContain('exit 1') + }) + + it('bundles maps after the build and before packaging strips them', () => { + const bundle = stepIndex('Bundle main-process source maps') + expect(bundle).toBeGreaterThan(stepIndex('Build app')) + expect(stepIndex('Publish main-process source maps')).toBeGreaterThan(bundle) + expect(bundle).toBeLessThan(stepIndex('Publish release artifacts (Linux)')) + }) +})