diff --git a/.github/workflows/adhoc-mac-build.yml b/.github/workflows/adhoc-mac-build.yml index 3772f4e4d06..7417a099afb 100644 --- a/.github/workflows/adhoc-mac-build.yml +++ b/.github/workflows/adhoc-mac-build.yml @@ -75,6 +75,7 @@ concurrency: cancel-in-progress: false env: + ORCA_BACKGROUND_LAUNCH: '1' ADHOC_REPO: stablyai/orca-adhoc # Why age and not a count like hourly: this channel is low-volume and bursty, so # a count would either hold one week's experiments forever or evict a build @@ -89,7 +90,7 @@ jobs: # The mac job still vets this commit's reachability before anything is signed. resolve-ref: if: github.repository == 'stablyai/orca' - runs-on: ubuntu-latest + runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 5 permissions: contents: read @@ -131,7 +132,7 @@ jobs: # Resolve shared build identity while detecting whether this ref ships a template. orcad-template-support: needs: resolve-ref - runs-on: ubuntu-latest + runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 5 permissions: contents: read @@ -224,13 +225,23 @@ jobs: with: ref: ${{ needs.resolve-ref.outputs.sha }} build_template: true + fast_runners: true + + release-javascript: + needs: [resolve-ref, orcad-template-support] + uses: ./.github/workflows/release-javascript.yml + with: + ref: ${{ needs.resolve-ref.outputs.sha }} + dev_build: true + runner: blacksmith-8vcpu-ubuntu-2404 build-adhoc-mac: - needs: [resolve-ref, orcad-template-support, orcad-template] + needs: [resolve-ref, orcad-template-support, orcad-template, release-javascript] # Why not the implicit success(): a ref without the orcad template skips that job on purpose. if: >- !cancelled() && github.repository == 'stablyai/orca' && needs.orcad-template-support.result == 'success' && + needs.release-javascript.result == 'success' && (needs.orcad-template.result == 'success' || (needs.orcad-template.result == 'skipped' && needs.orcad-template-support.outputs.ships == 'false')) @@ -376,6 +387,7 @@ jobs: # Why here: electron-builder's beforePack requires out/mobile-web, and the bundle # build resolves React Native and Expo from mobile/node_modules. - uses: ./.github/actions/install-mobile-dependencies + if: needs.release-javascript.outputs.supported != 'true' # Why: signing is what makes an adhoc build installable over an existing # Orca, so a missing cert must fail here rather than after a 20-minute build. @@ -388,9 +400,24 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + - name: Download shared JavaScript + if: needs.release-javascript.outputs.supported == 'true' + uses: actions/download-artifact@v8 + with: + name: release-javascript + path: .build/release-javascript + - name: Build app - run: pnpm build:release + run: | + if [[ "$ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA" != '' ]]; then + node config/scripts/release-javascript-artifact.mjs restore + pnpm run build:release:host + else + pnpm build:release + fi env: + ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ needs.release-javascript.outputs.supported == 'true' && needs.release-javascript.outputs.source_sha || '' }} + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: dev NODE_OPTIONS: --max-old-space-size=4096 # Why: adhoc builds are not an official channel — telemetry's transport # gate accepts only 'stable' or 'rc', so leaving this unset keeps them @@ -608,10 +635,11 @@ jobs: # Build unsigned Windows artifacts alongside Mac; publication waits below. build-adhoc-win: - needs: [resolve-ref, orcad-template-support, orcad-template] + needs: [resolve-ref, orcad-template-support, orcad-template, release-javascript] if: >- !cancelled() && github.repository == 'stablyai/orca' && needs.orcad-template-support.result == 'success' && + needs.release-javascript.result == 'success' && (needs.orcad-template.result == 'success' || (needs.orcad-template.result == 'skipped' && needs.orcad-template-support.outputs.ships == 'false')) @@ -624,6 +652,8 @@ jobs: version: ${{ needs.orcad-template-support.outputs.version }} orcad_template: ${{ needs.orcad-template-support.outputs.ships == 'true' }} stage_only: true + fast_release: true + javascript_source_sha: ${{ needs.release-javascript.outputs.supported == 'true' && needs.release-javascript.outputs.source_sha || '' }} # Upload only after Mac succeeds; an unsigned build may overlap its notarization. publish-adhoc-win: diff --git a/.github/workflows/dev-channel-win-build.yml b/.github/workflows/dev-channel-win-build.yml index adebe5da5e7..07868d08e4f 100644 --- a/.github/workflows/dev-channel-win-build.yml +++ b/.github/workflows/dev-channel-win-build.yml @@ -59,6 +59,14 @@ on: required: false type: boolean default: false + fast_release: + description: Use the faster unsigned-build runner outside signing provenance + type: boolean + default: false + javascript_source_sha: + description: Restore this run's shared dev JavaScript instead of rebuilding it + type: string + default: '' orcad_template: description: Ship the orcad-template artifact the calling run built required: false @@ -104,12 +112,11 @@ jobs: # that can write to the dev-channel repos, and it should be reachable from # exactly the same place those secrets already live. environment: adhoc-mac-build - # Why windows-2022 and not windows-latest: windows-latest moved to the - # Windows 2025 / VS 2026 image before node-gyp could detect VS 18, breaking - # native dependency install. release-cut pins the same image. - runs-on: windows-2022 + # The fast image has VS Build Tools 2022; signed release provenance stays separate. + runs-on: ${{ inputs.fast_release && 'blacksmith-8vcpu-windows-2025' || 'windows-2022' }} timeout-minutes: 90 env: + ORCA_BACKGROUND_LAUNCH: '1' NODE_OPTIONS: --max-old-space-size=4096 CHANNEL: ${{ inputs.channel }} TAG: ${{ inputs.tag }} @@ -172,6 +179,11 @@ jobs: fi echo "Vetted $CHANNEL $TAG at $REQUESTED_SHA" + - name: Enable parallel checkout + if: inputs.fast_release + shell: bash + run: git config --global checkout.workers 0 + - name: Checkout the built commit uses: actions/checkout@v6 with: @@ -250,6 +262,7 @@ jobs: # Why here: electron-builder's beforePack requires out/mobile-web, and the bundle # build resolves React Native and Expo from mobile/node_modules. - uses: ./.github/actions/install-mobile-dependencies + if: inputs.javascript_source_sha == '' # Why the packaging check runs before the 20-minute build: it only needs # node_modules, and a stale config should cost seconds rather than a build. @@ -257,10 +270,25 @@ jobs: shell: bash run: node config/scripts/verify-dev-channel-packaging.mjs --channel="$CHANNEL" --platform=win32 + - name: Download shared JavaScript + if: inputs.javascript_source_sha != '' + uses: actions/download-artifact@v8 + with: + name: release-javascript + path: .build/release-javascript + - name: Build app shell: bash - run: pnpm build:release + run: | + if [[ "$ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA" != '' ]]; then + node config/scripts/release-javascript-artifact.mjs restore + pnpm run build:release:host + else + pnpm build:release + fi env: + ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ inputs.javascript_source_sha }} + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: dev # Why unset ORCA_BUILD_IDENTITY: telemetry's transport gate accepts only # 'stable' or 'rc', so leaving it unset keeps dev builds silent — which # is correct for unvetted artifacts. Same as the mac dev channels. diff --git a/.github/workflows/dev-channel-win-publish.yml b/.github/workflows/dev-channel-win-publish.yml index 6864d1277d0..6d7c0f2c7a5 100644 --- a/.github/workflows/dev-channel-win-publish.yml +++ b/.github/workflows/dev-channel-win-publish.yml @@ -24,7 +24,7 @@ jobs: publish-win: if: github.repository == 'stablyai/orca' environment: adhoc-mac-build - runs-on: ubuntu-latest + runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 15 env: CHANNEL: ${{ inputs.channel }} diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index 15be17eb40e..c0a810bced9 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -27,6 +27,10 @@ on: description: Also merge every lane's slot into the desktop orcad template artifact type: boolean default: false + fast_runners: + description: Use faster runners where the platform and architecture are available + type: boolean + default: false # Release and dev builds qualify every platform for the template they ship. workflow_call: inputs: @@ -38,6 +42,10 @@ on: description: Upload each lane's release slot and merge them into the orcad-template artifact type: boolean default: false + fast_runners: + description: Use faster runners where the platform and architecture are available + type: boolean + default: false # Requalify fresh builds daily; publish missing immutable Windows cache keys. schedule: - cron: '30 11 * * *' @@ -55,7 +63,7 @@ jobs: if: >- github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft != true) - runs-on: ubuntu-latest + runs-on: ${{ inputs.fast_runners && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 5 outputs: should_run: ${{ steps.scope.outputs.should_run || steps.graph.outputs.should_run }} @@ -142,7 +150,9 @@ jobs: fail-fast: false matrix: os: ${{ fromJSON(needs.changes.outputs.runners || '["ubuntu-22.04","ubuntu-24.04-arm","macos-15","macos-15-intel","windows-2022","windows-11-arm"]') }} - runs-on: ${{ matrix.os }} + # Keep platform keys stable for slot names and Windows cross-compilation. + runs-on: >- + ${{ inputs.fast_runners && fromJSON('{"ubuntu-22.04":"blacksmith-8vcpu-ubuntu-2204","ubuntu-24.04-arm":"blacksmith-8vcpu-ubuntu-2404-arm","macos-15":"blacksmith-6vcpu-macos-15","windows-2022":"blacksmith-8vcpu-windows-2025"}')[matrix.os] || matrix.os }} timeout-minutes: 20 env: ORCA_BACKGROUND_LAUNCH: '1' @@ -314,7 +324,7 @@ jobs: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && needs.changes.outputs.qualification != 'false' && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.fast_runners && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 15 steps: - uses: actions/checkout@v6 @@ -354,7 +364,7 @@ jobs: image: quay.io/pypa/manylinux_2_28_x86_64@sha256:407f771c51a2c3e83ebe5a7970b4289ead3a6db21d9b9c089168775cad11d328 - os: ubuntu-24.04-arm image: quay.io/pypa/manylinux_2_28_aarch64@sha256:c22ffd129ac99a8a42d1f2c2f4e88a9089288dd9ee987a7092da1c7dc48f27a9 - runs-on: ${{ matrix.os }} + runs-on: ${{ inputs.fast_runners && (matrix.os == 'ubuntu-22.04' && 'blacksmith-8vcpu-ubuntu-2204' || 'blacksmith-8vcpu-ubuntu-2404-arm') || matrix.os }} container: ${{ matrix.image }} timeout-minutes: 25 env: @@ -416,7 +426,7 @@ jobs: (github.event_name != 'pull_request' || github.event.pull_request.draft != true) && (github.event_name != 'pull_request' || inputs.build_template || inputs.ref != '' || needs.changes.outputs.native_compatibility != 'false') }} - runs-on: ubuntu-22.04 + runs-on: ${{ inputs.fast_runners && 'blacksmith-8vcpu-ubuntu-2204' || 'ubuntu-22.04' }} timeout-minutes: 20 env: ORCA_BACKGROUND_LAUNCH: '1' @@ -475,7 +485,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, ubuntu-24.04-arm] - runs-on: ${{ matrix.os }} + runs-on: ${{ inputs.fast_runners && (matrix.os == 'ubuntu-22.04' && 'blacksmith-8vcpu-ubuntu-2204' || 'blacksmith-8vcpu-ubuntu-2404-arm') || matrix.os }} timeout-minutes: 20 env: ORCA_BACKGROUND_LAUNCH: '1' @@ -532,7 +542,7 @@ jobs: needs.persistence.result == 'success' && needs.mobile_web_bundle.result == 'success' && needs.linux_glibc_floor.result == 'success' && needs.linux_glibc217_compat.result == 'success' && needs.linux_musl.result == 'success' }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.fast_runners && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 30 steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/release-javascript.yml b/.github/workflows/release-javascript.yml index 2fc33c8d30c..7ff8be03267 100644 --- a/.github/workflows/release-javascript.yml +++ b/.github/workflows/release-javascript.yml @@ -6,6 +6,13 @@ on: ref: required: true type: string + dev_build: + description: Build without official-channel telemetry constants + type: boolean + default: false + runner: + type: string + default: ubuntu-latest outputs: supported: value: ${{ jobs.bundle.outputs.supported }} @@ -13,7 +20,7 @@ on: value: ${{ jobs.bundle.outputs.source_sha }} secrets: ORCA_POSTHOG_WRITE_KEY: - required: true + required: false permissions: contents: read @@ -23,7 +30,7 @@ env: jobs: bundle: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 20 outputs: supported: ${{ steps.source.outputs.supported }} @@ -40,7 +47,19 @@ jobs: shell: bash run: | echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - node -e 'const p = require("./package.json"); console.log("supported=" + Boolean(p.scripts["build:release:javascript"] && p.scripts["build:release:host"]))' >> "$GITHUB_OUTPUT" + node <<'NODE' >> "$GITHUB_OUTPUT" + const fs = require('node:fs') + const { scripts, devDependencies } = require('./package.json') + const supported = Boolean(scripts['build:release:javascript'] && scripts['build:release:host']) + const devSupported = supported && Boolean( + scripts['build:release:javascript:parallel'] && devDependencies?.tsrs && + fs.readFileSync('config/scripts/release-javascript-artifact.mjs', 'utf8') + .includes('ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY') + ) + console.log('supported=' + (process.env.DEV_BUILD === 'true' ? devSupported : supported)) + NODE + env: + DEV_BUILD: ${{ inputs.dev_build }} - uses: ./.github/actions/install-node-dependencies if: steps.source.outputs.supported == 'true' @@ -52,7 +71,7 @@ jobs: - name: Resolve release build identity id: identity - if: steps.source.outputs.supported == 'true' + if: steps.source.outputs.supported == 'true' && !inputs.dev_build run: | RELEASE_TAG="v$(node -p 'require("./package.json").version')" export RELEASE_TAG @@ -62,11 +81,14 @@ jobs: if: steps.source.outputs.supported == 'true' env: NODE_OPTIONS: --max-old-space-size=4096 + JAVASCRIPT_BUILD_SCRIPT: ${{ inputs.dev_build && 'build:release:javascript:parallel' || 'build:release:javascript' }} + ORCA_CLI_BUILD_COMPILER: ${{ inputs.dev_build && 'tsrs' || 'bun' }} ORCA_BUILD_IDENTITY: ${{ steps.identity.outputs.value }} + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: ${{ inputs.dev_build && 'dev' || '' }} ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token - ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }} + ORCA_POSTHOG_WRITE_KEY: ${{ !inputs.dev_build && secrets.ORCA_POSTHOG_WRITE_KEY || '' }} run: | - pnpm run build:release:javascript + pnpm run "$JAVASCRIPT_BUILD_SCRIPT" node config/scripts/release-javascript-artifact.mjs pack - uses: actions/upload-artifact@v7 diff --git a/config/scripts/adhoc-windows-parallelism.test.mjs b/config/scripts/adhoc-windows-parallelism.test.mjs index 9f3dd54feee..71e5d96e85f 100644 --- a/config/scripts/adhoc-windows-parallelism.test.mjs +++ b/config/scripts/adhoc-windows-parallelism.test.mjs @@ -1,4 +1,5 @@ import { readFileSync } from 'node:fs' +import { runInNewContext } from 'node:vm' import { describe, expect, it } from 'vitest' import { parse } from 'yaml' import { compileAggregateExpression } from './pr-aggregate-gate-context.mjs' @@ -7,6 +8,7 @@ const { jobs } = parse(readFileSync('.github/workflows/adhoc-mac-build.yml', 'ut const mac = jobs['build-adhoc-mac'] const windows = jobs['build-adhoc-win'] const publish = jobs['publish-adhoc-win'] +const bundleWorkflow = parse(readFileSync('.github/workflows/release-javascript.yml', 'utf8')) function qualification(template = 'success', ships = 'true') { return { @@ -14,6 +16,7 @@ function qualification(template = 'success', ships = 'true') { needs: { 'resolve-ref': { result: 'success', outputs: { sha: 'a'.repeat(40) } }, 'orcad-template-support': { result: 'success', outputs: { ships } }, + 'release-javascript': { result: 'success' }, 'orcad-template': { result: template } } } @@ -24,6 +27,7 @@ describe('adhoc Windows compilation overlaps Mac without publishing early', () = expect(windows.needs).toEqual(mac.needs) expect(windows.needs).not.toContain('build-adhoc-mac') expect(windows.with.stage_only).toBe(true) + expect(windows.with.fast_release).toBe(true) expect(windows.with.ref).toBe('${{ needs.resolve-ref.outputs.sha }}') expect(windows.with.tag).toBe('v${{ needs.orcad-template-support.outputs.version }}') expect(windows.with.version).toBe(mac.outputs.version) @@ -39,6 +43,60 @@ describe('adhoc Windows compilation overlaps Mac without publishing early', () = ) }) + it('builds portable JavaScript alongside qualification and restores it on both platforms', () => { + const bundle = jobs['release-javascript'] + expect(bundle.needs).not.toContain('orcad-template') + expect(bundle.with.ref).toBe('${{ needs.resolve-ref.outputs.sha }}') + expect(bundle.with.dev_build).toBe(true) + expect(bundle.secrets).toBeUndefined() + expect(jobs['orcad-template'].with.fast_runners).toBe(true) + for (const job of [mac, windows]) { + expect(job.needs).toContain('release-javascript') + const evaluate = compileAggregateExpression(job.if, ['needs', 'github']) + for (const result of ['failure', 'cancelled', 'skipped']) { + const ctx = qualification() + ctx.needs['release-javascript'].result = result + expect(evaluate(ctx).coerceString()).toBe('false') + } + } + const download = mac.steps.find((step) => step.name === 'Download shared JavaScript') + expect(download.with).toEqual({ name: 'release-javascript', path: '.build/release-javascript' }) + expect(download.if).toBe("needs.release-javascript.outputs.supported == 'true'") + expect(mac.steps.find((step) => step.name === 'Build app').run).toContain('build:release:host') + expect(windows.with.javascript_source_sha).toContain( + 'needs.release-javascript.outputs.source_sha' + ) + }) + + it('falls back for older refs and retains official bundle support', () => { + const source = bundleWorkflow.jobs.bundle.steps.find((step) => step.id === 'source').run + const script = source.split('node <<\'NODE\' >> "$GITHUB_OUTPUT"\n')[1].split('\nNODE')[0] + const current = JSON.parse(readFileSync('package.json', 'utf8')) + for (const [manifest, dev, expected] of [ + [current, true, true], + [current, false, true], + [{ scripts: {} }, true, false], + [ + { scripts: { 'build:release:javascript': 'build', 'build:release:host': 'host' } }, + true, + false + ], + [ + { scripts: { 'build:release:javascript': 'build', 'build:release:host': 'host' } }, + false, + true + ] + ]) { + const lines = [] + runInNewContext(script, { + require: (name) => (name === 'node:fs' ? { readFileSync } : manifest), + process: { env: { DEV_BUILD: String(dev) } }, + console: { log: (line) => lines.push(line) } + }) + expect(lines).toEqual([`supported=${expected}`]) + } + }) + it.each([ ['success', 'true', true], ['failure', 'true', false], diff --git a/config/scripts/build-cli.mjs b/config/scripts/build-cli.mjs index 0495c0b9262..6a66dfdb7a3 100644 --- a/config/scripts/build-cli.mjs +++ b/config/scripts/build-cli.mjs @@ -1,4 +1,6 @@ import { availableParallelism } from 'node:os' +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { runProcess, runProcessSync } from '@orca/process-host' import { resolvePinnedBunExecutable } from './pinned-bun-executable.mjs' @@ -18,8 +20,36 @@ export async function buildCli({ outDir = 'out', check = runTypecheckProjects, execute = runProcess, + compiler = process.env.ORCA_CLI_BUILD_COMPILER ?? 'bun', resolveBunExecutable = (directory) => resolvePinnedBunExecutable(directory, runProcessSync) } = {}) { + if (compiler !== 'bun' && compiler !== 'tsrs') { + throw new Error(`Invalid ORCA_CLI_BUILD_COMPILER: ${compiler}`) + } + const emitArguments = [ + '-p', + CONFIG, + '--outDir', + outDir, + '--composite', + 'false', + '--incremental', + 'false' + ] + if (compiler === 'tsrs') { + const manifest = createRequire(join(root, 'package.json')).resolve('tsrs/package.json') + const result = await execute({ + program: process.execPath, + args: [join(dirname(manifest), 'bin', 'tsrs'), ...emitArguments, '--noEmitOnError'], + cwd: root, + stdio: 'inherit', + timeoutMs: null + }) + if (result.code !== 0) { + throw new Error(`CLI compilation failed: ${describeProcessFailure(result)}`) + } + return + } const selection = selectTypecheckCompiler('bun', { root, resolveBunExecutable }) const failures = await check({ ...selection, @@ -34,18 +64,7 @@ export async function buildCli({ // Bun has checked this exact project; retain TypeScript's emitter without checking it twice. const result = await execute({ program: process.execPath, - args: [ - resolveProjectCompiler(CONFIG, root), - '-p', - CONFIG, - '--outDir', - outDir, - '--composite', - 'false', - '--incremental', - 'false', - '--noCheck' - ], + args: [resolveProjectCompiler(CONFIG, root), ...emitArguments, '--noCheck'], cwd: root, stdio: 'inherit', timeoutMs: null diff --git a/config/scripts/build-cli.test.mjs b/config/scripts/build-cli.test.mjs index 159dce8198b..0f14f55ce45 100644 --- a/config/scripts/build-cli.test.mjs +++ b/config/scripts/build-cli.test.mjs @@ -1,5 +1,6 @@ import { mkdirSync, + existsSync, mkdtempSync, readFileSync, readdirSync, @@ -124,6 +125,14 @@ it('propagates emitter failures', async () => { ).rejects.toThrow('signal=SIGKILL') }) +it('rejects unknown compiler selections before emission', async () => { + const execute = vi.fn() + await expect(buildCli({ compiler: 'unknown', execute })).rejects.toThrow( + 'Invalid ORCA_CLI_BUILD_COMPILER' + ) + expect(execute).not.toHaveBeenCalled() +}) + it('produces byte-identical JavaScript, declarations and maps with the real compilers', async () => { const root = fixture() const baseline = await runProcess({ @@ -181,3 +190,31 @@ it('blocks output for real type errors and rejects compiler version drift', asyn ) expect(execute).not.toHaveBeenCalled() }) + +const supportsRustCompiler = + (process.platform === 'darwin' && process.arch === 'arm64') || + (process.platform === 'linux' && ['arm64', 'x64'].includes(process.arch)) + +it.skipIf(!supportsRustCompiler)( + 'the optional compiler preserves emitted bytes and blocks output on type errors', + async () => { + const root = fixture() + await buildCli({ root, outDir: 'baseline', compiler: 'bun', resolveBunExecutable }) + await buildCli({ root, outDir: 'optimized', compiler: 'tsrs' }) + const files = readdirSync(join(root, 'baseline')).sort() + expect(readdirSync(join(root, 'optimized')).sort()).toEqual(files) + for (const file of files) { + expect( + readFileSync(join(root, 'optimized', file)).equals( + readFileSync(join(root, 'baseline', file)) + ), + file + ).toBe(true) + } + writeFileSync(join(root, 'src', 'entry.ts'), 'export const value: number = "wrong"\n') + await expect(buildCli({ root, outDir: 'failed', compiler: 'tsrs' })).rejects.toThrow( + 'CLI compilation failed' + ) + expect(existsSync(join(root, 'failed'))).toBe(false) + } +) diff --git a/config/scripts/node-server-fast-runners.test.mjs b/config/scripts/node-server-fast-runners.test.mjs new file mode 100644 index 00000000000..3dc863b6577 --- /dev/null +++ b/config/scripts/node-server-fast-runners.test.mjs @@ -0,0 +1,78 @@ +import { readFileSync } from 'node:fs' +import { runInNewContext } from 'node:vm' +import { expect, it } from 'vitest' +import { parse } from 'yaml' + +const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8')) + +function runnerFor(job, os, fast) { + return runInNewContext(job['runs-on'].slice(3, -2), { + inputs: { fast_runners: fast }, + matrix: { os }, + fromJSON: JSON.parse + }) +} + +it('keeps faster runners opt-in for both reusable and manual qualification', () => { + for (const trigger of ['workflow_call', 'workflow_dispatch']) { + expect(workflow.on[trigger].inputs.fast_runners).toMatchObject({ + type: 'boolean', + default: false + }) + } +}) + +it.each([ + ['ubuntu-22.04', 'blacksmith-8vcpu-ubuntu-2204'], + ['ubuntu-24.04-arm', 'blacksmith-8vcpu-ubuntu-2404-arm'], + ['macos-15', 'blacksmith-6vcpu-macos-15'], + ['macos-15-intel', 'macos-15-intel'], + ['windows-2022', 'blacksmith-8vcpu-windows-2025'], + ['windows-11-arm', 'windows-11-arm'] +])('preserves the qualification architecture for %s', (os, fastRunner) => { + expect(runnerFor(workflow.jobs.persistence, os, false)).toBe(os) + expect(runnerFor(workflow.jobs.persistence, os, true)).toBe(fastRunner) + expect(runnerFor(workflow.jobs.persistence, os, undefined)).toBe(os) +}) + +it('retains both Linux architectures and the pinned libc qualification containers', () => { + for (const name of ['linux_glibc_floor', 'linux_musl']) { + const job = workflow.jobs[name] + expect(job.strategy.matrix.os).toEqual(['ubuntu-22.04', 'ubuntu-24.04-arm']) + for (const os of job.strategy.matrix.os) { + expect(runnerFor(job, os, false)).toBe(os) + expect(runnerFor(job, os, true)).toBe( + `blacksmith-8vcpu-ubuntu-${os === 'ubuntu-22.04' ? '2204' : '2404-arm'}` + ) + } + } + const floor = workflow.jobs.linux_glibc_floor + expect(floor.container).toBe('${{ matrix.image }}') + for (const { image } of floor.strategy.matrix.include) { + expect(image).toMatch(/^quay\.io\/pypa\/manylinux_2_28_.*@sha256:/) + } + expect(workflow.jobs.linux_glibc217_compat.steps.map((step) => step.run).join('\n')).toContain( + 'quay.io/pypa/manylinux2014_x86_64@sha256:' + ) +}) + +it.each([ + ['changes', 'ubuntu-latest', 'blacksmith-2vcpu-ubuntu-2404'], + ['mobile_web_bundle', 'ubuntu-latest', 'blacksmith-8vcpu-ubuntu-2404'], + ['desktop_template', 'ubuntu-latest', 'blacksmith-4vcpu-ubuntu-2404'], + ['linux_glibc217_compat', 'ubuntu-22.04', 'blacksmith-8vcpu-ubuntu-2204'] +])('sizes %s for its work without changing default callers', (name, hostedRunner, fastRunner) => { + expect(runnerFor(workflow.jobs[name], undefined, false)).toBe(hostedRunner) + expect(runnerFor(workflow.jobs[name], undefined, true)).toBe(fastRunner) +}) + +it('still builds and uploads cross-compiled Windows template addons on the x64 lane', () => { + const steps = workflow.jobs.persistence.steps + for (const fragment of ['process-table addons', 'CLI launchers', 'security addons']) { + const upload = steps.find((step) => step.name?.includes(`Keep the Windows ${fragment}`)) + expect(upload.if).toContain("matrix.os == 'windows-2022'") + } + const launcher = steps.find((step) => step.uses?.includes('prepare-windows-cli-launcher')) + expect(launcher.with.arches).toContain("matrix.os == 'windows-2022'") + expect(launcher.with.arches).toContain("'x64 arm64'") +}) diff --git a/config/scripts/node-server-native-compat-scope.test.mjs b/config/scripts/node-server-native-compat-scope.test.mjs index 5792ee90898..1fe0acb514d 100644 --- a/config/scripts/node-server-native-compat-scope.test.mjs +++ b/config/scripts/node-server-native-compat-scope.test.mjs @@ -1,4 +1,5 @@ import { readFileSync } from 'node:fs' +import { runInNewContext } from 'node:vm' import { expect, it, vi } from 'vitest' import { parse } from 'yaml' import { compileAggregateExpression } from './pr-aggregate-gate-context.mjs' @@ -336,7 +337,9 @@ it('builds server glibc slots on glibc 2.28 and the compat slot on glibc 2.17 (d expect(compat.if).toContain( "github.event_name != 'pull_request' || inputs.build_template || inputs.ref != ''" ) - expect(compat['runs-on']).toBe('ubuntu-22.04') + expect(runInNewContext(compat['runs-on'].slice(3, -2), { inputs: { fast_runners: false } })).toBe( + 'ubuntu-22.04' + ) const run = compat.steps.map((step) => step.run ?? '').join('\n') expect(run).toMatch(/quay\.io\/pypa\/manylinux2014_x86_64@sha256:[0-9a-f]{64} /) expect(run).toContain('--slot=linux-x64-glibc217 --print-runtime') diff --git a/config/scripts/release-javascript-artifact.mjs b/config/scripts/release-javascript-artifact.mjs index a04cf2445ef..20578fcd588 100644 --- a/config/scripts/release-javascript-artifact.mjs +++ b/config/scripts/release-javascript-artifact.mjs @@ -63,17 +63,26 @@ export function releaseJavascriptConfiguration(root, env = process.env) { ) } const { version, packageManager } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) - const identity = releaseBuildIdentity(`v${version}`) - assert.equal(env.ORCA_BUILD_IDENTITY, identity, 'Release build identity differs from version') + const requestedIdentity = env.ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY + const identity = requestedIdentity === 'dev' ? 'dev' : releaseBuildIdentity(`v${version}`) + if (requestedIdentity) { + assert.equal(requestedIdentity, identity, 'Invalid JavaScript build identity') + } + if (identity === 'dev') { + assert(!env.ORCA_BUILD_IDENTITY, 'Development bundle must not set an official build identity') + assert(!env.ORCA_POSTHOG_WRITE_KEY, 'Development bundle must not set a release telemetry key') + } else { + assert.equal(env.ORCA_BUILD_IDENTITY, identity, 'Release build identity differs from version') + assert(env.ORCA_POSTHOG_WRITE_KEY, 'Missing release telemetry key') + } assert(env.ORCA_DIAGNOSTICS_TOKEN_URL, 'Missing release diagnostics URL') - assert(env.ORCA_POSTHOG_WRITE_KEY, 'Missing release telemetry key') return { sourceSha, version, packageManager, identity, diagnosticsTokenUrl: env.ORCA_DIAGNOSTICS_TOKEN_URL, - telemetryKeySha256: sha256(env.ORCA_POSTHOG_WRITE_KEY) + telemetryKeySha256: identity === 'dev' ? null : sha256(env.ORCA_POSTHOG_WRITE_KEY) } } diff --git a/config/scripts/release-javascript-artifact.test.mjs b/config/scripts/release-javascript-artifact.test.mjs index c98efdb5456..ad81b23dca6 100644 --- a/config/scripts/release-javascript-artifact.test.mjs +++ b/config/scripts/release-javascript-artifact.test.mjs @@ -1,11 +1,13 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' +import { runProcessSync } from '@orca/process-host' import { afterEach, describe, expect, it } from 'vitest' import { javascriptInventory, packReleaseJavascript, releaseBuildIdentity, + releaseJavascriptConfiguration, restoreReleaseJavascript } from './release-javascript-artifact.mjs' @@ -49,7 +51,136 @@ function fixture() { return { root, artifactDir, configuration } } +function configurationFixture(version = '1.2.3') { + const root = mkdtempSync(join(tmpdir(), 'orca-javascript-configuration-test-')) + directories.push(root) + const hooks = join(root, 'empty-hooks') + mkdirSync(hooks) + writeFileSync( + join(root, 'package.json'), + JSON.stringify({ version, packageManager: 'pnpm@12.14.0' }) + ) + for (const args of [ + ['init', '--quiet'], + [ + '-c', + 'user.name=Build test', + '-c', + 'user.email=build@example.test', + '-c', + 'commit.gpgsign=false', + '-c', + `core.hooksPath=${hooks}`, + 'commit', + '--allow-empty', + '--quiet', + '-m', + 'Build fixture' + ] + ]) { + const result = runProcessSync({ program: 'git', args, cwd: root, timeoutMs: 10_000 }) + expect(result.code, result.stderr).toBe(0) + } + const env = { + ORCA_BUILD_IDENTITY: 'stable', + ORCA_DIAGNOSTICS_TOKEN_URL: 'https://example.test/diagnostics', + ORCA_POSTHOG_WRITE_KEY: 'release-key' + } + return { root, env } +} + +describe('release JavaScript configuration', () => { + it.each([ + ['1.2.3', 'stable'], + ['1.2.3-rc.4.perf', 'rc'] + ])('preserves official build checks for %s', (version, identity) => { + const { root, env } = configurationFixture(version) + const result = releaseJavascriptConfiguration(root, { ...env, ORCA_BUILD_IDENTITY: identity }) + expect(result.identity).toBe(identity) + expect(result.telemetryKeySha256).toMatch(/^[a-f0-9]{64}$/) + expect(result.sourceSha).toMatch(/^[a-f0-9]{40}$/) + }) + + it('requires explicit development mode and unset official constants', () => { + const { root, env } = configurationFixture('0.0.0-adhoc.test') + const devEnv = { + ...env, + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: 'dev', + ORCA_BUILD_IDENTITY: '', + ORCA_POSTHOG_WRITE_KEY: '' + } + expect(releaseJavascriptConfiguration(root, devEnv)).toMatchObject({ + identity: 'dev', + version: '0.0.0-adhoc.test', + telemetryKeySha256: null, + diagnosticsTokenUrl: env.ORCA_DIAGNOSTICS_TOKEN_URL + }) + expect(() => + releaseJavascriptConfiguration(root, { ...devEnv, ORCA_BUILD_IDENTITY: 'stable' }) + ).toThrow('must not set an official build identity') + expect(() => + releaseJavascriptConfiguration(root, { ...devEnv, ORCA_POSTHOG_WRITE_KEY: 'release-key' }) + ).toThrow('must not set a release telemetry key') + expect(() => + releaseJavascriptConfiguration(root, { + ...devEnv, + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: '' + }) + ).toThrow('Invalid desktop release tag') + }) + + it('does not downgrade an official build when its identity or telemetry key is missing', () => { + const { root, env } = configurationFixture() + expect(() => releaseJavascriptConfiguration(root, { ...env, ORCA_BUILD_IDENTITY: '' })).toThrow( + 'Release build identity differs from version' + ) + expect(() => + releaseJavascriptConfiguration(root, { ...env, ORCA_POSTHOG_WRITE_KEY: '' }) + ).toThrow('Missing release telemetry key') + expect(() => + releaseJavascriptConfiguration(root, { ...env, ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: 'rc' }) + ).toThrow('Invalid JavaScript build identity') + expect(() => + releaseJavascriptConfiguration(root, { + ...env, + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: 'typo' + }) + ).toThrow('Invalid JavaScript build identity') + }) + + it('checks the source checkout and diagnostics endpoint in development mode', () => { + const { root, env } = configurationFixture() + const devEnv = { + ...env, + ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: 'dev', + ORCA_BUILD_IDENTITY: '', + ORCA_POSTHOG_WRITE_KEY: '' + } + expect(() => + releaseJavascriptConfiguration(root, { + ...devEnv, + ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: 'b'.repeat(40) + }) + ).toThrow('Release checkout differs from bundle source') + expect(() => + releaseJavascriptConfiguration(root, { ...devEnv, ORCA_DIAGNOSTICS_TOKEN_URL: '' }) + ).toThrow('Missing release diagnostics URL') + }) +}) + describe('release JavaScript transfer', () => { + it('restores development bundles and rejects them for an official build', () => { + const options = fixture() + const devConfiguration = { ...configuration, identity: 'dev', telemetryKeySha256: null } + packReleaseJavascript({ ...options, configuration: devConfiguration }) + expect(() => restoreReleaseJavascript(options)).toThrow( + 'JavaScript build configuration differs' + ) + expect( + restoreReleaseJavascript({ ...options, configuration: devConfiguration }).configuration + ).toEqual(devConfiguration) + }) + it('preserves hidden manifests, source maps and portable WASM while replacing stale outputs', () => { const options = fixture() const before = javascriptInventory(join(options.root, 'out')) diff --git a/package.json b/package.json index ea389329ee8..725a195665d 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "build": "pnpm run build:desktop && pnpm run build:native", "build:release": "pnpm run build:wsl-guest && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "build:release:javascript": "pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", + "build:release:javascript:parallel": "pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "build:release:host": "pnpm run build:wsl-guest && pnpm run build:native && pnpm run verify:computer-native && pnpm run verify:built-skills-cli", "build:release:parallel": "pnpm run build:wsl-guest && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "postinstall": "pnpm run build:packages && node config/scripts/rebuild-native-deps.mjs", @@ -334,6 +335,7 @@ "tailwind-merge": "^3.7.0", "tailwindcss": "^4.3.3", "tar": "7.5.22", + "tsrs": "0.16.0-ts7.1.0-dev.20260929", "tw-animate-css": "^1.4.0", "typescript": "^7.0.2", "typescript-api": "npm:typescript@6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf8be11648d..dec670dc93f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -617,6 +617,9 @@ importers: tar: specifier: 7.5.22 version: 7.5.22 + tsrs: + specifier: 0.16.0-ts7.1.0-dev.20260929 + version: 0.16.0-ts7.1.0-dev.20260929 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -4010,6 +4013,40 @@ packages: '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@tsrs/darwin-arm64@0.16.0-ts7.1.0-dev.20260929': + resolution: {integrity: sha512-9e7ZeELv0Fq06PL7xr0w7jeU45cd2UcDAejH3uloTPD37Vm6h099u/hAj90CPbY88ky15eXCiKE5hJzqvSvCBA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@tsrs/linux-arm64-gnu@0.16.0-ts7.1.0-dev.20260929': + resolution: {integrity: sha512-+pWQ4gnuIVeDJsUYKxwRuSF9smST209Srto2B0LrCwnomgbMHENB7hzN+kAOe+HGWvmss9sjoQiwNHME3C9nNA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tsrs/linux-arm64-musl@0.16.0-ts7.1.0-dev.20260929': + resolution: {integrity: sha512-T2whHu5XbgEAnre71GVHBavdypwZu4tD/vEZDWTctfKrQ2LDbtt5QTbWhxN1SOkNS9i8EqWYaRUKQEDPjFntZg==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tsrs/linux-x64-gnu@0.16.0-ts7.1.0-dev.20260929': + resolution: {integrity: sha512-hPsRzR/oG4+wQwiCFSLM7Vct70yO1C4mHjDebEUAqyzyVlhRzZsqJVDpy+xYVyB1eebDD9VJ+c5UEzA2PuKDiw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tsrs/linux-x64-musl@0.16.0-ts7.1.0-dev.20260929': + resolution: {integrity: sha512-oW49ztnYwI0uOc/Yf8PCcXqAaVlkbvYJByLcz3u1cT2I7R8D649jDdegHWrqDfPPCa9yM7Q7WdNKstZIaQCjeg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + libc: [musl] + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -8013,6 +8050,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsrs@0.16.0-ts7.1.0-dev.20260929: + resolution: {integrity: sha512-/SfYc9iIH353Fj1evYYtiRWbz+/+ieYhlHuu2A3iDvA2DLjzlV1vKMzyh2eXuACPNsmFI2hc3w9EPPx6B+E8OA==} + engines: {node: '>=16.20.0'} + hasBin: true + tsx@4.23.15: resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} @@ -11269,6 +11311,21 @@ snapshots: minimatch: 10.2.5 path-browserify: 1.0.1 + '@tsrs/darwin-arm64@0.16.0-ts7.1.0-dev.20260929': + optional: true + + '@tsrs/linux-arm64-gnu@0.16.0-ts7.1.0-dev.20260929': + optional: true + + '@tsrs/linux-arm64-musl@0.16.0-ts7.1.0-dev.20260929': + optional: true + + '@tsrs/linux-x64-gnu@0.16.0-ts7.1.0-dev.20260929': + optional: true + + '@tsrs/linux-x64-musl@0.16.0-ts7.1.0-dev.20260929': + optional: true + '@types/aria-query@5.0.4': {} '@types/cacheable-request@6.0.3': @@ -15829,6 +15886,14 @@ snapshots: tslib@2.8.1: {} + tsrs@0.16.0-ts7.1.0-dev.20260929: + optionalDependencies: + '@tsrs/darwin-arm64': 0.16.0-ts7.1.0-dev.20260929 + '@tsrs/linux-arm64-gnu': 0.16.0-ts7.1.0-dev.20260929 + '@tsrs/linux-arm64-musl': 0.16.0-ts7.1.0-dev.20260929 + '@tsrs/linux-x64-gnu': 0.16.0-ts7.1.0-dev.20260929 + '@tsrs/linux-x64-musl': 0.16.0-ts7.1.0-dev.20260929 + tsx@4.23.15: dependencies: esbuild: 0.28.2