mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
Merge origin/main into structured-session-eviction
Three mobile conflicts, all from this stack's own extractions meeting main: - rpc-client.ts: this branch had already moved RpcClient into rpc-client-contract and re-exports it; main still declares it inline. Keeping both produced a duplicate declaration. Dropped the inline copy and ported the one member main had that the contract lacked (getLastInboundAt). - worktree-create-retry(.test).ts: the cutover-error split moved isLogicalClientCutoverError out of stable-logical-rpc-client, and main added two new imports alongside the old path. Both kept, pointed at the new module. - StreamRequest keeps paramsForReconnect from this branch. Root typecheck, mobile tsc, full lint gate, and the mobile suite (3755 tests) green; pnpm-lock.yaml untouched.
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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 readWorkflow = (relativePath) => parse(readFileSync(join(projectDir, relativePath), 'utf8'))
|
||||
|
||||
const MAC_WORKFLOWS = [
|
||||
['hourly', '.github/workflows/hourly-mac-build.yml', 'build-hourly-mac'],
|
||||
['daily', '.github/workflows/daily-mac-build.yml', 'build-daily-mac'],
|
||||
['adhoc', '.github/workflows/adhoc-mac-build.yml', 'build-adhoc-mac']
|
||||
]
|
||||
|
||||
const winWorkflow = () => readWorkflow('.github/workflows/dev-channel-win-build.yml')
|
||||
const winSteps = () => winWorkflow().jobs['build-win'].steps
|
||||
const stepNamed = (steps, name) => steps.find((step) => step.name === name)
|
||||
|
||||
describe('dev-channel Windows build wiring', () => {
|
||||
it.each(MAC_WORKFLOWS)(
|
||||
'calls the shared Windows workflow from %s with its own channel',
|
||||
(channel, path, jobName) => {
|
||||
const jobs = readWorkflow(path).jobs
|
||||
const winJob = jobs[`build-${channel}-win`]
|
||||
|
||||
expect(winJob).toBeDefined()
|
||||
expect(winJob.uses).toBe('./.github/workflows/dev-channel-win-build.yml')
|
||||
expect(winJob.with.channel).toBe(channel)
|
||||
// Why `./` and not a pinned @main: `uses:` resolves against the ref this
|
||||
// workflow file came from, which for an ordinary dispatch is main. A branch
|
||||
// deliberately selected in the Actions picker already supplies its own copy
|
||||
// of the whole file, so this follows that rule rather than adding a second.
|
||||
expect(winJob.uses.startsWith('./')).toBe(true)
|
||||
expect(winJob.needs).toBe(jobName)
|
||||
expect(winJob.secrets).toBe('inherit')
|
||||
}
|
||||
)
|
||||
|
||||
// Why gated on published: the Windows leg uploads into a release the mac leg
|
||||
// created, and refuses to create one itself. Without this it would run for a
|
||||
// tag that was never published — or, for hourly, for a run that skipped.
|
||||
it.each(MAC_WORKFLOWS)(
|
||||
'only runs the %s Windows leg once the release is live',
|
||||
(channel, path, jobName) => {
|
||||
expect(readWorkflow(path).jobs[`build-${channel}-win`].if).toBe(
|
||||
`needs.${jobName}.outputs.published == 'true'`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// The Windows job names the release by consuming these; a renamed step id
|
||||
// would silently resolve them to empty strings and upload nowhere.
|
||||
it.each(MAC_WORKFLOWS)(
|
||||
'exposes the %s tag, version and commit as job outputs',
|
||||
(_c, path, jobName) => {
|
||||
const job = readWorkflow(path).jobs[jobName]
|
||||
const stepIds = job.steps.map((step) => step.id).filter(Boolean)
|
||||
|
||||
for (const key of ['tag', 'version', 'head_sha', 'published']) {
|
||||
expect(job.outputs[key]).toBeTruthy()
|
||||
}
|
||||
// Every referenced step id must actually exist in the job.
|
||||
for (const expression of Object.values(job.outputs)) {
|
||||
const referenced = [...expression.matchAll(/steps\.([A-Za-z0-9_-]+)\./g)].map((m) => m[1])
|
||||
for (const id of referenced) {
|
||||
expect(stepIds).toContain(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Why this is asserted: the previous design dispatched a sibling run, which
|
||||
// needed actions:write on a job holding macOS signing credentials. Calling the
|
||||
// workflow directly removes that, and it must not creep back.
|
||||
it.each(MAC_WORKFLOWS)(
|
||||
'does not widen the %s signing job to actions:write',
|
||||
(_c, path, jobName) => {
|
||||
expect(readWorkflow(path).jobs[jobName].permissions?.actions).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('dev-channel Windows build workflow', () => {
|
||||
it('is both callable by the mac workflows and dispatchable on its own', () => {
|
||||
const triggers = winWorkflow().on ?? winWorkflow()[true]
|
||||
|
||||
expect(Object.keys(triggers)).toEqual(
|
||||
expect.arrayContaining(['workflow_call', 'workflow_dispatch'])
|
||||
)
|
||||
// workflow_call cannot express `type: choice`, so the channel set has to be
|
||||
// enforced in the job itself.
|
||||
expect(stepNamed(winSteps(), 'Vet the requested inputs').run).toContain('hourly|daily|adhoc')
|
||||
})
|
||||
|
||||
// Why windows-2022: windows-latest moved to the Windows 2025 / VS 2026 image
|
||||
// before node-gyp could detect VS 18, breaking native dependency install.
|
||||
it('pins the same Windows image release-cut builds on', () => {
|
||||
expect(winWorkflow().jobs['build-win']['runs-on']).toBe('windows-2022')
|
||||
})
|
||||
|
||||
// The guard against a branch whose electron-builder config predates Windows
|
||||
// dev builds: without it, publish.repo resolves to the main repo.
|
||||
it('verifies the dev-channel packaging identity before building', () => {
|
||||
const steps = winSteps()
|
||||
const names = steps.map((step) => step.name)
|
||||
const verify = names.indexOf('Verify dev-channel packaging identity')
|
||||
const build = names.indexOf('Build app')
|
||||
|
||||
expect(verify).toBeGreaterThanOrEqual(0)
|
||||
expect(build).toBeGreaterThan(verify)
|
||||
expect(stepNamed(steps, 'Verify dev-channel packaging identity').run).toContain(
|
||||
'verify-dev-channel-packaging.mjs'
|
||||
)
|
||||
})
|
||||
|
||||
// Why: electron-publish creates a release when it cannot find the tag under
|
||||
// `--publish always`. If the mac leg discarded its draft while this was
|
||||
// building, that would mint an untitled Windows-only release.
|
||||
it('confirms the target release exists before publishing into it', () => {
|
||||
const names = winSteps().map((step) => step.name)
|
||||
const confirm = names.indexOf('Confirm the target release still exists')
|
||||
const publish = names.indexOf('Publish Windows artifacts')
|
||||
|
||||
expect(confirm).toBeGreaterThanOrEqual(0)
|
||||
expect(publish).toBeGreaterThan(confirm)
|
||||
})
|
||||
|
||||
// electron-publish refuses to upload into a release published more than two
|
||||
// hours ago. The mac leg publishes as soon as it finishes, so a slow notary
|
||||
// queue plus a slow Windows build crosses that line and drops every asset.
|
||||
it('opts out of the publisher two-hour upload window', () => {
|
||||
expect(stepNamed(winSteps(), 'Publish Windows artifacts').env.EP_GH_IGNORE_TIME).toBe('true')
|
||||
})
|
||||
|
||||
it('packages Windows unsigned through the shared electron-builder config', () => {
|
||||
const publish = stepNamed(winSteps(), 'Publish Windows artifacts')
|
||||
|
||||
expect(publish.with.command).toContain(
|
||||
'electron-builder --config config/electron-builder.config.cjs --win --publish always'
|
||||
)
|
||||
// No signing env: SignPath's approval waits cannot fit a dev cadence, which
|
||||
// is the entire reason these builds are unsigned.
|
||||
expect(Object.keys(publish.env)).not.toContain('SIGNPATH_API_TOKEN')
|
||||
})
|
||||
|
||||
// Why both: a release carrying the installer but not the manifest is a row the
|
||||
// picker offers and the in-app update 404s on.
|
||||
it('requires the manifest and the installer before calling the build good', () => {
|
||||
const verify = stepNamed(winSteps(), 'Verify Windows update manifest published')
|
||||
|
||||
expect(verify.run).toContain('latest.yml')
|
||||
expect(verify.run).toContain('orca-windows-setup.exe')
|
||||
})
|
||||
|
||||
// Why: this is callable and separately dispatchable, and workflow_call takes
|
||||
// its inputs as free text, so it re-derives the ref guarantee rather than
|
||||
// trusting whoever called it.
|
||||
it('vets the requested commit before checking it out', () => {
|
||||
const names = winSteps().map((step) => step.name)
|
||||
const vet = names.indexOf('Vet the requested inputs')
|
||||
const checkout = names.indexOf('Checkout the built commit')
|
||||
|
||||
expect(vet).toBe(0)
|
||||
expect(checkout).toBeGreaterThan(vet)
|
||||
expect(stepNamed(winSteps(), 'Vet the requested inputs').run).toContain('--contains')
|
||||
})
|
||||
|
||||
// Telemetry's transport gate accepts only 'stable' or 'rc'; leaving the build
|
||||
// identity unset is what keeps unvetted artifacts silent.
|
||||
it('never stamps an official telemetry build identity', () => {
|
||||
const build = stepNamed(winSteps(), 'Build app')
|
||||
|
||||
expect(Object.keys(build.env ?? {})).not.toContain('ORCA_BUILD_IDENTITY')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
// Why this exists: the dev-channel workflows run from main, but they build (and
|
||||
// therefore read `config/electron-builder.config.cjs` from) whatever ref was
|
||||
// asked for. A branch cut before Windows dev builds landed has a config that
|
||||
// ignores ORCA_WIN_*, which would resolve `publish.repo` to the *main* repo and
|
||||
// leave the release identity signed-looking. Publishing would then fail deep
|
||||
// inside electron-builder with a 404 from a token scoped to the dev repo — or,
|
||||
// worse, succeed against a repo it was never meant to touch.
|
||||
//
|
||||
// So: load the config exactly as electron-builder will, and assert the identity
|
||||
// it produced matches the channel and platform the workflow believes it is
|
||||
// building. Runs before packaging, fails with a sentence someone can act on.
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const CHANNEL_REPOS = {
|
||||
hourly: 'orca-hourly',
|
||||
daily: 'orca-daily',
|
||||
adhoc: 'orca-adhoc'
|
||||
}
|
||||
|
||||
const CHANNEL_VERSION_ENV = {
|
||||
hourly: 'ORCA_HOURLY_BUILD_VERSION',
|
||||
daily: 'ORCA_DAILY_BUILD_VERSION',
|
||||
adhoc: 'ORCA_ADHOC_BUILD_VERSION'
|
||||
}
|
||||
|
||||
export function collectDevChannelPackagingProblems({ channel, platform, config, env }) {
|
||||
const problems = []
|
||||
const expectedRepo = CHANNEL_REPOS[channel]
|
||||
if (!expectedRepo) {
|
||||
return [
|
||||
`Unknown dev channel "${channel}"; expected one of ${Object.keys(CHANNEL_REPOS).join(', ')}.`
|
||||
]
|
||||
}
|
||||
|
||||
if (config.publish?.repo !== expectedRepo) {
|
||||
problems.push(
|
||||
`publish.repo is "${config.publish?.repo}" but this ${channel} build must publish to "${expectedRepo}". ` +
|
||||
`The checked-out ref's electron-builder config does not understand this channel on ${platform} — rebase it onto a main that does.`
|
||||
)
|
||||
}
|
||||
|
||||
if (config.publish?.releaseType !== 'prerelease') {
|
||||
problems.push(
|
||||
`publish.releaseType is "${config.publish?.releaseType}" but dev-channel builds must publish as "prerelease".`
|
||||
)
|
||||
}
|
||||
|
||||
// Why version too: `extraMetadata.version` is what stamps the tag the workflow
|
||||
// already created. A config that dropped it would package package.json's
|
||||
// version and upload into the wrong release entirely.
|
||||
const expectedVersion = env[CHANNEL_VERSION_ENV[channel]]
|
||||
if (expectedVersion && config.extraMetadata?.version !== expectedVersion) {
|
||||
problems.push(
|
||||
`extraMetadata.version is "${config.extraMetadata?.version}" but the workflow computed "${expectedVersion}".`
|
||||
)
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
// The one that silently breaks updates rather than failing the build: a dev
|
||||
// build that advertises a publisherName can never install its own channel's
|
||||
// next build, because electron-updater verifies against the name baked into
|
||||
// the installed app.
|
||||
if (config.win?.verifyUpdateCodeSignature !== false) {
|
||||
problems.push(
|
||||
'win.verifyUpdateCodeSignature must be false for unsigned dev builds, or electron-updater will Authenticode-verify every installer this build downloads and reject all of them.'
|
||||
)
|
||||
}
|
||||
if (config.win?.signtoolOptions?.publisherName != null) {
|
||||
problems.push(
|
||||
`win.signtoolOptions.publisherName is set to "${config.win.signtoolOptions.publisherName}" on an unsigned dev build; it must be absent.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {}
|
||||
for (const entry of argv) {
|
||||
const match = /^--([^=]+)=(.*)$/.exec(entry)
|
||||
if (match) {
|
||||
args[match[1]] = match[2]
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { channel, platform = process.platform } = parseArgs(process.argv.slice(2))
|
||||
if (!channel) {
|
||||
console.error(
|
||||
'Usage: verify-dev-channel-packaging.mjs --channel=<hourly|daily|adhoc> [--platform=win32]'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
const require = createRequire(import.meta.url)
|
||||
const config = require(resolve(import.meta.dirname, '../electron-builder.config.cjs'))
|
||||
const problems = collectDevChannelPackagingProblems({
|
||||
channel,
|
||||
platform,
|
||||
config,
|
||||
env: process.env
|
||||
})
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) {
|
||||
console.error(`::error::${problem}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(
|
||||
`Dev-channel packaging verified: ${channel} on ${platform} → stablyai/${CHANNEL_REPOS[channel]} @ ${config.extraMetadata?.version}`
|
||||
)
|
||||
}
|
||||
|
||||
// Why the guard: the test imports the pure collector without running the CLI.
|
||||
// pathToFileURL, not a `file://` template: this also runs on Windows, where a
|
||||
// drive-letter path does not concatenate into a valid URL.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main()
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { collectDevChannelPackagingProblems } from './verify-dev-channel-packaging.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const CONFIG_PATH = resolve(import.meta.dirname, '../electron-builder.config.cjs')
|
||||
|
||||
/** The config reads process.env at require time, so each identity needs a fresh load. */
|
||||
function loadConfigWithEnv(env) {
|
||||
const saved = { ...process.env }
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (key.startsWith('ORCA_')) {
|
||||
delete process.env[key]
|
||||
}
|
||||
}
|
||||
Object.assign(process.env, env)
|
||||
try {
|
||||
delete require.cache[require.resolve(CONFIG_PATH)]
|
||||
return require(CONFIG_PATH)
|
||||
} finally {
|
||||
process.env = saved
|
||||
delete require.cache[require.resolve(CONFIG_PATH)]
|
||||
}
|
||||
}
|
||||
|
||||
const WIN_ADHOC_ENV = {
|
||||
ORCA_WIN_ADHOC: '1',
|
||||
ORCA_ADHOC_BUILD_VERSION: '1.4.178-adhoc.20260819010203'
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete require.cache[require.resolve(CONFIG_PATH)]
|
||||
})
|
||||
|
||||
describe('electron-builder dev-channel identity', () => {
|
||||
it('keeps the SignPath publisherName on stable Windows builds', () => {
|
||||
const config = loadConfigWithEnv({})
|
||||
|
||||
expect(config.win.signtoolOptions.publisherName).toBe('SignPath Foundation')
|
||||
expect(config.win.verifyUpdateCodeSignature).toBeUndefined()
|
||||
expect(config.publish.repo).toBe('orca')
|
||||
expect(config.publish.releaseType).toBe('release')
|
||||
})
|
||||
|
||||
// The whole point of the change: an unsigned build that advertised a
|
||||
// publisherName would Authenticode-verify — and reject — every installer it
|
||||
// ever downloaded, including its own way back to stable.
|
||||
it('drops the publisherName and disables update signature checks on Windows dev builds', () => {
|
||||
const config = loadConfigWithEnv(WIN_ADHOC_ENV)
|
||||
|
||||
expect(config.win.signtoolOptions?.publisherName).toBeUndefined()
|
||||
expect(config.win.verifyUpdateCodeSignature).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['hourly', { ORCA_WIN_HOURLY: '1' }, 'orca-hourly'],
|
||||
['daily', { ORCA_WIN_DAILY: '1' }, 'orca-daily'],
|
||||
['adhoc', { ORCA_WIN_ADHOC: '1' }, 'orca-adhoc']
|
||||
])('publishes %s Windows builds to its own repo as a prerelease', (_channel, env, repo) => {
|
||||
const config = loadConfigWithEnv(env)
|
||||
|
||||
expect(config.publish.repo).toBe(repo)
|
||||
expect(config.publish.releaseType).toBe('prerelease')
|
||||
})
|
||||
|
||||
// Why: ORCA_MAC_* gates hardened runtime, notarization, and root-level
|
||||
// forceCodeSigning. If the Windows variables leaked into that, the Windows job
|
||||
// would fail packaging for want of a cert it deliberately does not use.
|
||||
it('leaves mac release signing off for Windows dev builds', () => {
|
||||
const config = loadConfigWithEnv(WIN_ADHOC_ENV)
|
||||
|
||||
expect(config.forceCodeSigning).toBe(false)
|
||||
expect(config.mac.notarize).toBe(false)
|
||||
expect(config.mac.hardenedRuntime).toBe(false)
|
||||
})
|
||||
|
||||
it('still notarizes mac dev builds', () => {
|
||||
const config = loadConfigWithEnv({
|
||||
ORCA_MAC_ADHOC: '1',
|
||||
ORCA_ADHOC_BUILD_VERSION: '1.4.178-adhoc.20260819010203'
|
||||
})
|
||||
|
||||
expect(config.mac.notarize).toBe(true)
|
||||
expect(config.publish.repo).toBe('orca-adhoc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('collectDevChannelPackagingProblems', () => {
|
||||
const goodWinConfig = {
|
||||
publish: { repo: 'orca-adhoc', releaseType: 'prerelease' },
|
||||
extraMetadata: { version: '1.4.178-adhoc.20260819010203' },
|
||||
win: { verifyUpdateCodeSignature: false }
|
||||
}
|
||||
const env = { ORCA_ADHOC_BUILD_VERSION: '1.4.178-adhoc.20260819010203' }
|
||||
|
||||
it('accepts a correctly configured Windows dev build', () => {
|
||||
expect(
|
||||
collectDevChannelPackagingProblems({
|
||||
channel: 'adhoc',
|
||||
platform: 'win32',
|
||||
config: goodWinConfig,
|
||||
env
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
// The failure this script exists for: a branch predating Windows dev builds
|
||||
// resolves publish.repo to the main repo.
|
||||
it('rejects a config that resolved the main repo', () => {
|
||||
const problems = collectDevChannelPackagingProblems({
|
||||
channel: 'adhoc',
|
||||
platform: 'win32',
|
||||
config: { ...goodWinConfig, publish: { repo: 'orca', releaseType: 'release' } },
|
||||
env
|
||||
})
|
||||
|
||||
expect(problems.join('\n')).toContain('must publish to "orca-adhoc"')
|
||||
expect(problems.join('\n')).toContain('rebase it onto a main that does')
|
||||
})
|
||||
|
||||
it('rejects a Windows dev build that still advertises a publisherName', () => {
|
||||
const problems = collectDevChannelPackagingProblems({
|
||||
channel: 'adhoc',
|
||||
platform: 'win32',
|
||||
config: {
|
||||
...goodWinConfig,
|
||||
win: { signtoolOptions: { publisherName: 'SignPath Foundation' } }
|
||||
},
|
||||
env
|
||||
})
|
||||
|
||||
expect(problems.join('\n')).toContain('verifyUpdateCodeSignature must be false')
|
||||
expect(problems.join('\n')).toContain('publisherName is set to "SignPath Foundation"')
|
||||
})
|
||||
|
||||
it('rejects a build packaging a version other than the tag the workflow created', () => {
|
||||
const problems = collectDevChannelPackagingProblems({
|
||||
channel: 'adhoc',
|
||||
platform: 'win32',
|
||||
config: { ...goodWinConfig, extraMetadata: { version: '1.4.178' } },
|
||||
env
|
||||
})
|
||||
|
||||
expect(problems.join('\n')).toContain('but the workflow computed')
|
||||
})
|
||||
|
||||
// macOS builds are signed, so the Windows-only assertions must not fire there.
|
||||
it('does not apply Windows signature rules to a mac dev build', () => {
|
||||
expect(
|
||||
collectDevChannelPackagingProblems({
|
||||
channel: 'adhoc',
|
||||
platform: 'darwin',
|
||||
config: {
|
||||
publish: { repo: 'orca-adhoc', releaseType: 'prerelease' },
|
||||
extraMetadata: { version: '1.4.178-adhoc.20260819010203' },
|
||||
win: { signtoolOptions: { publisherName: 'SignPath Foundation' } }
|
||||
},
|
||||
env
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an unknown channel', () => {
|
||||
expect(
|
||||
collectDevChannelPackagingProblems({
|
||||
channel: 'nightly',
|
||||
platform: 'win32',
|
||||
config: goodWinConfig,
|
||||
env
|
||||
})
|
||||
).toEqual(['Unknown dev channel "nightly"; expected one of hourly, daily, adhoc.'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user