From 444064105c6a7397a2f48ec7132ce92579f7d6b7 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Tue, 20 Oct 2026 07:22:34 -0400 Subject: [PATCH] build(orcad): a dark test template opt-in, marked so packaging refuses it (#31097) build-orcad-template.mjs takes --dark-test-template to let ORCA_LAYOUT_OWNER=main through for test servers; the template and out/orcad carry the switch marker, and packaging refuses a marked template. The e2e template steps build a switch-on server when the workflow runs with layout_owner=main. Co-authored-by: E2E Test --- .github/workflows/e2e.yml | 23 +++++++++++++++---- config/dark-build-switches.cjs | 5 ++-- config/electron-builder.config.cjs | 6 +++-- config/scripts/build-orcad-template.mjs | 20 +++++++++++++--- config/scripts/build-orcad-template.test.mjs | 20 +++++++++++++++- config/scripts/build-orcad.mjs | 6 +++++ .../scripts/electron-builder-config.test.mjs | 18 +++++++++++++++ 7 files changed, 85 insertions(+), 13 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cafcbfb75d6..ad0f571ce44 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -38,7 +38,7 @@ on: required: false type: string default: '' - # Why only the app builds read it: the orcad template build refuses a dark switch. + # Why: the app, orcad and its test template (--dark-test-template) build with it; releases refuse it. layout_owner: description: Set to main to build with ORCA_LAYOUT_OWNER=main and record save-divergence reports required: false @@ -471,13 +471,18 @@ jobs: slot="$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" pnpm build:orcad-prebuilds pnpm build:orcad-prebuilds --require-slots "$slot" - # The headless oracle's orcad host must hold the same layout owner as the Electron host. + # The headless oracle's orcad host must hold the same layout owner as the Electron host, + # and so must the template `orca serve` materializes its server from. if [ -n "$LAYOUT_OWNER" ]; then - ORCA_LAYOUT_OWNER="$LAYOUT_OWNER" pnpm build:orcad + export ORCA_LAYOUT_OWNER="$LAYOUT_OWNER" + pnpm build:orcad + node config/scripts/build-orcad-template.mjs --targets "$slot" --dark-test-template + test -f out/orcad/ORCA_LAYOUT_OWNER_MAIN + test -f out/orcad-template/ORCA_LAYOUT_OWNER_MAIN else pnpm build:orcad + node config/scripts/build-orcad-template.mjs --targets "$slot" fi - node config/scripts/build-orcad-template.mjs --targets "$slot" - name: Switch serve hosts on one profile if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') env: @@ -557,11 +562,19 @@ jobs: name: e2e-build-out${{ inputs.artifact_suffix }} path: out/ # Built to a path the app does not look at by default, so the relay-era phase has no template. + # The switch-on leg deploys a switch-on server, so both sides of the host run one layout owner. - name: Build the linux-x64-glibc orcad template + env: + LAYOUT_OWNER: ${{ inputs.layout_owner }} run: | pnpm build:orcad-prebuilds pnpm build:orcad-prebuilds --require-slots linux-x64-glibc - node config/scripts/build-orcad-template.mjs --targets linux-x64-glibc + if [ -n "$LAYOUT_OWNER" ]; then + ORCA_LAYOUT_OWNER="$LAYOUT_OWNER" node config/scripts/build-orcad-template.mjs --targets linux-x64-glibc --dark-test-template + test -f out/orcad-template/ORCA_LAYOUT_OWNER_MAIN + else + node config/scripts/build-orcad-template.mjs --targets linux-x64-glibc + fi mv out/orcad-template "$RUNNER_TEMP/orcad-convert-template" # The Windows CLI matrix collects here too and skips: it needs a Windows host descriptor. - name: Run managed SSH specs on a Docker host diff --git a/config/dark-build-switches.cjs b/config/dark-build-switches.cjs index 5ec4448d06c..b46f40cb686 100644 --- a/config/dark-build-switches.cjs +++ b/config/dark-build-switches.cjs @@ -2,8 +2,9 @@ const { existsSync } = require('node:fs') const { join } = require('node:path') // Dark build switches: a build compiles a switch's path in only when its variable is 'main'. Such a -// main build leaves the switch's marker in out/main and in the phone page bundle (out/mobile-web), -// and packaging refuses either; packaging and the orcad template also refuse the variable itself. +// main build leaves the switch's marker in out/main, the phone page bundle (out/mobile-web), out/orcad +// and a --dark-test-template orcad template; packaging refuses any of them, and packaging and a +// release orcad template also refuse the variable itself. const DARK_BUILD_SWITCHES = [ // src/shared/layout-owner-switch.ts { variable: 'ORCA_LAYOUT_OWNER', off: 'window' }, diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index ff5c20876f1..dab5258053e 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -406,9 +406,11 @@ module.exports = { beforePack: ( context, mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR, - ensureWaylandClipboard = ensureBundledWaylandClipboard + ensureWaylandClipboard = ensureBundledWaylandClipboard, + orcadTemplateDir = join(__dirname, '..', orcadTemplateExtraResource.from) ) => { - assertNotDarkBuild([join(__dirname, '..', 'out', 'main'), mobileWebBundleDir]) + // The template is the server every SSH host gets, so a dark test template must never ship. + assertNotDarkBuild([join(__dirname, '..', 'out', 'main'), mobileWebBundleDir, orcadTemplateDir]) assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch) assertBundledRipgrepInstalled() assertProcessHostOutputBuilt() diff --git a/config/scripts/build-orcad-template.mjs b/config/scripts/build-orcad-template.mjs index f5b2a955275..f477c2b012f 100644 --- a/config/scripts/build-orcad-template.mjs +++ b/config/scripts/build-orcad-template.mjs @@ -38,7 +38,7 @@ import { import { runProcessSync } from '@orca/process-host' import { verifyPackagedOrcadTemplate } from './verify-packaged-orcad-template.cjs' import { readOrcadMobileWebAssetFilenames } from './orcad-mobile-web-bundle.mjs' -import { assertNotDarkBuild } from '../dark-build-switches.cjs' +import { assertNotDarkBuild, darkBuildMarkers } from '../dark-build-switches.cjs' const root = resolve(import.meta.dirname, '../..') const outputDir = join(root, 'out', 'orcad-template') @@ -175,13 +175,23 @@ function stageCompatTarget(compat, basePackageDir) { return { files } } +/** + * `--dark-test-template` lets a dark switch through for test servers only; the template then + * carries the switch's marker, which packaging refuses. + */ +export function assertTemplateBuildAllowed(argv = process.argv, env = process.env) { + if (!argv.includes('--dark-test-template')) { + // The template is what releases ship as the server; a dark build must never become one. + assertNotDarkBuild([], env) + } +} + function sameBytes(left, right) { return sha256(left) === sha256(right) } async function main() { - // The template is what releases ship as the server; a dark build must never become one. - assertNotDarkBuild([]) + assertTemplateBuildAllowed() const templateTargets = requestedTemplateTargets() rmSync(buildDir, { recursive: true, force: true }) const packages = Object.fromEntries( @@ -224,6 +234,10 @@ async function main() { `${JSON.stringify({ schemaVersion: 3, commonSha256, targets }, null, 2)}\n` ) verifyPackagedOrcadTemplate(join(root, 'out'), templateTargets) + // Outside the manifest, so deploys never copy it; it marks the whole template as test-only. + for (const marker of darkBuildMarkers()) { + writeFileSync(join(outputDir, marker.fileName), marker.text) + } rmSync(buildDir, { recursive: true, force: true }) process.stdout.write(`[build-orcad-template] ok — ${Object.keys(targets).length} targets\n`) } diff --git a/config/scripts/build-orcad-template.test.mjs b/config/scripts/build-orcad-template.test.mjs index c501b25a6a9..f39cb62879f 100644 --- a/config/scripts/build-orcad-template.test.mjs +++ b/config/scripts/build-orcad-template.test.mjs @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { ORCAD_TEMPLATE_TARGETS } from '../../src/shared/node-runtime-pin.ts' -import { requestedTemplateTargets } from './build-orcad-template.mjs' +import { assertTemplateBuildAllowed, requestedTemplateTargets } from './build-orcad-template.mjs' describe('requestedTemplateTargets', () => { it('builds every target unless a subset is named', () => { @@ -29,3 +29,21 @@ describe('requestedTemplateTargets', () => { } ) }) + +describe('assertTemplateBuildAllowed', () => { + const argv = ['node', 'build-orcad-template.mjs'] + const dark = { ORCA_LAYOUT_OWNER: 'main' } + + it('refuses a dark switch for a release template', () => { + expect(() => assertTemplateBuildAllowed(argv, dark)).toThrow('ORCA_LAYOUT_OWNER=main') + }) + + it('lets a dark switch through only for an explicit test template', () => { + expect(() => assertTemplateBuildAllowed([...argv, '--dark-test-template'], dark)).not.toThrow() + }) + + it('allows a plain template either way', () => { + expect(() => assertTemplateBuildAllowed(argv, {})).not.toThrow() + expect(() => assertTemplateBuildAllowed([...argv, '--dark-test-template'], {})).not.toThrow() + }) +}) diff --git a/config/scripts/build-orcad.mjs b/config/scripts/build-orcad.mjs index 17d25f1c100..057636d8a11 100644 --- a/config/scripts/build-orcad.mjs +++ b/config/scripts/build-orcad.mjs @@ -60,6 +60,8 @@ import { runProcessSync } from '@orca/process-host' import { describeProcessFailure } from './process-failure-message.mjs' import { resolveBuildSourceCommit } from './build-source-commit.mjs' +const { darkBuildMarkers } = createRequire(import.meta.url)('../dark-build-switches.cjs') + const ROOT = join(import.meta.dirname, '..', '..') const OUT_DIR = process.env.ORCAD_OUT_DIR ? resolve(process.env.ORCAD_OUT_DIR) @@ -159,6 +161,10 @@ function stageNodePty(target) { rmSync(OUT_DIR, { recursive: true, force: true }) mkdirSync(OUT_DIR, { recursive: true }) writeFileSync(join(OUT_DIR, ORCAD_SERVER_TARGET_FILENAME), `${BUILD_TARGET}\n`) +// Outside the artifact list, so the version hash and every deploy ignore it. +for (const marker of darkBuildMarkers()) { + writeFileSync(join(OUT_DIR, marker.fileName), marker.text) +} writeFileSync( join(OUT_DIR, ORCAD_NODE_RUNTIME_MARKER_FILENAME), `${runtimeAsset.executableSha256}\n` diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index 4c76725621a..4d43b9523bb 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -616,6 +616,24 @@ describe('arch-aware packaging guard', () => { } }) + it('refuses to package an orcad template marked by a dark test build', async () => { + const templateDir = join(scratch, 'orcad-template') + await mkdir(templateDir, { recursive: true }) + await writeFile(join(templateDir, 'ORCA_LAYOUT_OWNER_MAIN'), 'dark\n') + try { + expect(() => + electronBuilderConfig.beforePack( + { electronPlatformName: process.platform, arch: HOST_ARCH }, + bundleDir, + () => undefined, + templateDir + ) + ).toThrow(`${templateDir} holds an ORCA_LAYOUT_OWNER=main build`) + } finally { + await rm(templateDir, { recursive: true, force: true }) + } + }) + it('requires the other architecture natives to be installed', () => { const otherSherpaInstalled = existsSync( join(REPO_ROOT, 'node_modules', otherSherpa, 'package.json')