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 <e2e@test.local>
This commit is contained in:
Jinwoo Hong
2026-10-20 07:22:34 -04:00
committed by GitHub
co-authored by E2E Test
parent 085cc38e75
commit 444064105c
7 changed files with 85 additions and 13 deletions
+18 -5
View File
@@ -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
+3 -2
View File
@@ -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' },
+4 -2
View File
@@ -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()
+17 -3
View File
@@ -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`)
}
+19 -1
View File
@@ -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()
})
})
+6
View File
@@ -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`
@@ -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')