mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 16:02:46 +00:00
* perf(ci): share release JavaScript across packaging hosts * fix(ci): verify the projected web entry in release archives * fix(ci): use the Windows system archive tool for release bundles * test(ci): retain stylesheet evidence in release build comparisons * test(ci): verify release parity across native color rounding * test(ci): normalize manifest asset references without changing import order * test(ci): compare portable outputs across Windows text and color formatting * test(ci): preserve module identity across dependent asset hashes * fix(ci): keep SVG build inputs identical across release hosts * fix(ci): stabilize compiler inputs and projected web bindings * fix(ci): retain vendor minification in projected web output * test(ci): normalize platform-specific pnpm manifest source paths * fix(packaging): exclude shared build staging from application files * test: align thinking-state fixtures with the current source shape * test(mobile): reuse message fixtures within the line limit
177 lines
6.5 KiB
JavaScript
177 lines
6.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { createHash } from 'node:crypto'
|
|
import {
|
|
chmodSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
renameSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
import { runProcessSync, describeProcessFailure } from './script-child-process.mjs'
|
|
import { isDirectInvocation } from './script-entry-detection.mjs'
|
|
import { DESKTOP_RC_TAG, DESKTOP_STABLE_TAG } from './release-tag-patterns.mjs'
|
|
import { getTarProgram } from './zip-extractor-command.mjs'
|
|
|
|
const REQUIRED_FILES = [
|
|
'cli/index.js',
|
|
'main/index.js',
|
|
'preload/index.js',
|
|
'renderer/index.html',
|
|
'renderer/.vite/manifest.json',
|
|
'web/web-index.html',
|
|
'mobile-web/manifest.json',
|
|
'package.json'
|
|
]
|
|
const NATIVE_FILE = /\.(?:node|exe|dll|dylib|so(?:\.\d+)*|a)$/i
|
|
const ARCHIVE_NAME = 'release-javascript.tar.gz'
|
|
|
|
function sha256(bytes) {
|
|
return createHash('sha256').update(bytes).digest('hex')
|
|
}
|
|
|
|
function command(program, args, cwd) {
|
|
const result = runProcessSync({ program, args, cwd, timeoutMs: 120_000 })
|
|
assert.equal(result.code, 0, describeProcessFailure(result))
|
|
return result.stdout.trim()
|
|
}
|
|
|
|
export function releaseBuildIdentity(tag) {
|
|
if (DESKTOP_RC_TAG.test(tag)) {
|
|
return 'rc'
|
|
}
|
|
if (DESKTOP_STABLE_TAG.test(tag)) {
|
|
return 'stable'
|
|
}
|
|
throw new Error(`Invalid desktop release tag: ${tag}`)
|
|
}
|
|
|
|
export function releaseJavascriptConfiguration(root, env = process.env) {
|
|
const sourceSha = command('git', ['rev-parse', 'HEAD'], root)
|
|
assert.match(sourceSha, /^[a-f0-9]{40}$/)
|
|
if (env.ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA) {
|
|
assert.equal(
|
|
sourceSha,
|
|
env.ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA,
|
|
'Release checkout differs from bundle source'
|
|
)
|
|
}
|
|
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')
|
|
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)
|
|
}
|
|
}
|
|
|
|
export function javascriptInventory(directory, prefix = '', { excludeHostOutput = false } = {}) {
|
|
return readdirSync(directory, { withFileTypes: true })
|
|
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
|
|
.flatMap((entry) => {
|
|
const name = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
const hostOutput = ['relay', 'orcad-template', 'orcad-prebuilds'].includes(name.split('/')[0])
|
|
if (excludeHostOutput && hostOutput) {
|
|
return []
|
|
}
|
|
assert(!entry.isSymbolicLink(), `JavaScript artifact contains a symlink: ${name}`)
|
|
assert(!NATIVE_FILE.test(name), `JavaScript artifact contains a native binary: ${name}`)
|
|
assert(!hostOutput, `JavaScript artifact contains host output: ${name}`)
|
|
const file = join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
return javascriptInventory(file, name, { excludeHostOutput })
|
|
}
|
|
assert(entry.isFile(), `JavaScript artifact contains a special file: ${name}`)
|
|
const bytes = readFileSync(file)
|
|
return [{ path: name, bytes: bytes.length, sha256: sha256(bytes) }]
|
|
})
|
|
}
|
|
|
|
function verifyRequiredFiles(files) {
|
|
const present = new Set(files.map((file) => file.path))
|
|
for (const file of REQUIRED_FILES) {
|
|
assert(present.has(file), `Missing JavaScript output: ${file}`)
|
|
}
|
|
}
|
|
|
|
export function packReleaseJavascript({ root = process.cwd(), artifactDir, configuration }) {
|
|
configuration ??= releaseJavascriptConfiguration(root)
|
|
const out = join(root, 'out')
|
|
const files = javascriptInventory(out)
|
|
verifyRequiredFiles(files)
|
|
mkdirSync(artifactDir, { recursive: true })
|
|
const archive = join(artifactDir, ARCHIVE_NAME)
|
|
command(getTarProgram(), ['-czf', archive, '-C', root, 'out'], root)
|
|
const manifest = {
|
|
schema: 1,
|
|
configuration,
|
|
archiveSha256: sha256(readFileSync(archive)),
|
|
archiveBytes: statSync(archive).size,
|
|
files
|
|
}
|
|
writeFileSync(join(artifactDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
return manifest
|
|
}
|
|
|
|
export function restoreReleaseJavascript({ root = process.cwd(), artifactDir, configuration }) {
|
|
configuration ??= releaseJavascriptConfiguration(root)
|
|
const manifest = JSON.parse(readFileSync(join(artifactDir, 'manifest.json'), 'utf8'))
|
|
assert.equal(manifest.schema, 1, 'Unsupported JavaScript artifact schema')
|
|
assert.deepEqual(manifest.configuration, configuration, 'JavaScript build configuration differs')
|
|
const archive = join(artifactDir, ARCHIVE_NAME)
|
|
assert.equal(sha256(readFileSync(archive)), manifest.archiveSha256, 'JavaScript archive differs')
|
|
const entries = command(getTarProgram(), ['-tzf', archive], root).split(/\r?\n/)
|
|
for (const entry of entries) {
|
|
assert(entry === 'out/' || entry.startsWith('out/'), `Unexpected archive entry: ${entry}`)
|
|
assert(
|
|
!entry.includes('\\') && !entry.split('/').includes('..'),
|
|
`Unsafe archive entry: ${entry}`
|
|
)
|
|
}
|
|
mkdirSync(join(root, '.build'), { recursive: true })
|
|
const temporary = mkdtempSync(join(root, '.build', 'release-javascript-'))
|
|
try {
|
|
command(getTarProgram(), ['-xzf', archive, '-C', temporary], root)
|
|
const staged = join(temporary, 'out')
|
|
const files = javascriptInventory(staged)
|
|
verifyRequiredFiles(files)
|
|
assert.deepEqual(files, manifest.files, 'JavaScript file inventory differs')
|
|
rmSync(join(root, 'out'), { recursive: true, force: true })
|
|
renameSync(staged, join(root, 'out'))
|
|
if (process.platform !== 'win32') {
|
|
chmodSync(join(root, 'out', 'cli', 'index.js'), 0o755)
|
|
}
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true })
|
|
}
|
|
return manifest
|
|
}
|
|
|
|
if (isDirectInvocation(import.meta.url, process.argv[1])) {
|
|
const [mode, artifact] = process.argv.slice(2)
|
|
if (mode === 'identity') {
|
|
console.log(releaseBuildIdentity(process.env.RELEASE_TAG))
|
|
} else {
|
|
const artifactDir = resolve(artifact ?? '.build/release-javascript')
|
|
const result =
|
|
mode === 'pack'
|
|
? packReleaseJavascript({ artifactDir })
|
|
: mode === 'restore'
|
|
? restoreReleaseJavascript({ artifactDir })
|
|
: assert.fail('Expected pack, restore, or identity')
|
|
console.log(
|
|
`[release-javascript] ${mode}: ${result.files.length} files, ${result.archiveBytes} archived bytes`
|
|
)
|
|
}
|
|
}
|