mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* fix(release): prune optional natives before the linux arch floor check
The linux-arm64 release build failed packaging, and retried three times:
[verify-linux-glibc-floor] 1 bundled native binary is built for the
wrong architecture (target arm64):
resources/node_modules/@parcel/watcher-linux-x64-glibc/watcher.node
is x64, expected arm64 (from its own path)
`afterPack` ran `verifyLinuxGlibcFloor` at its top, before
`prunePackagedRuntimeNodeModules`. A cross-build intentionally installs
every optional native variant, so at that point the arm64 slice still
carries the x64 `@parcel/watcher` package that the prune exists to drop.
The check was reading a file that was never going to ship.
Move the floor check below the prune so it inspects the binaries actually
packed. Same ordering as 35f1b0ebb9 on the v1.4.197 release branch, which
shipped green and was never merged back to main.
(cherry picked from commit 6400598212)
(cherry picked from commit a883e17116)
* fix(release): restore the minified telemetry constant fallback
The macOS, Windows, and both Linux release builds all failed on "Verify
telemetry constants present in app.asar", blocking publish-release:
::error::BUILD_IDENTITY constant missing or unexpected value in
dist/mac-arm64/Orca.app/Contents/Resources/app.asar
The verifier printed no context sample, because the string
`BUILD_IDENTITY` does not occur anywhere in the shipped bundle at all.
#17527 added `minify: 'oxc'` to the main bundle, so oxc renames the
module-local `const BUILD_IDENTITY` to a short identifier.
`BUILD_IDENTITY_RE` keys off the literal name and therefore cannot match a
production bundle. `MINIFIED_TELEMETRY_RE` matches the adjacent injected
identity/key pair instead, which survives renaming.
#11019 created these patterns without that fallback, and its own comment
predicted this exact failure if minification were ever enabled on the main
bundle. The fallback was written on the v1.4.197 release branch in
35f1b0ebb9 and never merged back to main, so main has never been able to
verify a minified bundle.
Verified against a real local bundle built with the release env vars
(ORCA_BUILD_IDENTITY=stable, ORCA_POSTHOG_WRITE_KEY=phc_...): the bundle
does not contain "BUILD_IDENTITY"; both the narrowed and the widened
BUILD_IDENTITY_RE fail; MINIFIED_TELEMETRY_RE matches and recovers
identity=stable plus the write key.
(cherry picked from commit 39c3e44cd1)
(cherry picked from commit d0bbe4475d)
* fix(skills): stop asserting readdir order in the skill root walk test
The Windows skill-sharing release gate — which blocks publish-release —
failed on this test, wedging the 1.4.198 cut. The implementation is
correct; the assertion was not.
`findSkillFiles` pushes results in `readdir` order and dedupes visited
directories by realpath, so of the 32 junctions pointing at one target
exactly one survives. The assertion hardcoded both the array order and
which link won. `readdir` order is filesystem-dependent: APFS and ext4
return SKILL.md first, while NTFS enumerates its filename index
alphabetically on the uppercased name, where "LINK00" sorts before
"SKILL.MD" (L=0x4C < S=0x53). Windows therefore returned the same two
paths in the opposite order.
Assert the contract instead: the real file is present, exactly one
link-routed path survives dedup, and nothing else does.
(cherry picked from commit 9a832e9bda)
(cherry picked from commit 505b85cd0d)
* test(release): pin ported release gate fixes
* style(skills): apply pinned test formatting
---------
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Merge Sim <sim@local>
234 lines
10 KiB
JavaScript
234 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Asserts that the telemetry compile-time constants — `ORCA_BUILD_IDENTITY`
|
|
// and `ORCA_POSTHOG_WRITE_KEY` — were substituted into the shipped binary.
|
|
// Closes the gap PR #1385 flagged: a release built without those env vars
|
|
// produces an `IS_OFFICIAL_BUILD === false` binary that ships silently and
|
|
// transmits nothing. This script runs after `electron-builder` has packed
|
|
// the app and BEFORE the GitHub release is flipped from draft → published.
|
|
// A failure here fails the matrix job; the draft release stays in place
|
|
// until a human resolves it.
|
|
//
|
|
// We grep the packed `app.asar`'s `out/main/*.js` files (the same files
|
|
// that the runtime loads) rather than the unpacked `out/`. asar is a
|
|
// tar-like archive that doesn't transform contents, so the two are
|
|
// byte-equivalent today — but verifying the asar protects against any
|
|
// future config change that excludes the main bundle from the package.
|
|
//
|
|
// Forward-compat: while `TELEMETRY_ENABLED` is `false` in the source, the
|
|
// bundler dead-code-eliminates the entire transport block, so the
|
|
// `BUILD_IDENTITY = "..."` and `WRITE_KEY = "phc_..."` constants do not
|
|
// appear in the binary even when env vars are correctly injected. This
|
|
// script reads the source flag at build time and skips the assertion in
|
|
// that case — the moment the flag flips to `true` (PR #1385), every
|
|
// subsequent release is enforced.
|
|
//
|
|
// Cross-platform note: written in Node so it runs identically on the Mac,
|
|
// Linux, and Windows release runners. Locating `app.asar` via `fs.readdir`
|
|
// (instead of POSIX `find`) avoids depending on Git Bash on Windows.
|
|
// Reading the asar via the programmatic `@electron/asar` API (instead of
|
|
// shelling out to `npx asar`) avoids both a network fetch on every run
|
|
// and the Windows `.cmd`-shim/`shell: true` workaround.
|
|
|
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
// Why @electron/asar: canonical replacement for the deprecated `asar` package.
|
|
// It's transitively available via electron-builder (and pnpm's
|
|
// `shamefullyHoist: true` in `pnpm-workspace.yaml` flattens it into the root
|
|
// `node_modules`). If electron-builder ever drops it, promote this to a
|
|
// direct devDependency in package.json.
|
|
import { extractFile, listPackage } from '@electron/asar'
|
|
import {
|
|
BUILD_IDENTITY_RE,
|
|
MINIFIED_TELEMETRY_RE,
|
|
WRITE_KEY_RE
|
|
} from './telemetry-bundle-constant-patterns.mjs'
|
|
|
|
// Why resolve from import.meta.url instead of cwd: a release runner (or a
|
|
// developer debugging locally) may invoke this script from a non-root cwd.
|
|
// Resolving relative to the script's own location turns a misleading
|
|
// "could not parse TELEMETRY_ENABLED flag" parse error into a clear
|
|
// file-not-found error, and decouples the script from the caller's cwd.
|
|
const repoRoot = resolve(import.meta.dirname, '..', '..')
|
|
|
|
function findAsar(rootDir) {
|
|
// Why: electron-builder produces one `app.asar` per platform-arch combo.
|
|
// Linux/Windows targets ship one (`dist/linux-unpacked/resources/app.asar`,
|
|
// `dist/win-unpacked/resources/app.asar`); macOS dual-arch ships two
|
|
// (`dist/mac/Orca.app/Contents/Resources/app.asar` for x64,
|
|
// `dist/mac-arm64/Orca.app/Contents/Resources/app.asar` for arm64) because
|
|
// `electron-builder.config.cjs` declares `arch: ['x64', 'arm64']`. Both
|
|
// arches share the same JS bundle through electron-vite's single `main`
|
|
// build, so the constants are identical across them — but verifying every
|
|
// match catches the regression where one arch's pack drifts (e.g. a future
|
|
// arch-specific bundle split that forgets to thread the `define` block).
|
|
const matches = []
|
|
const stack = [rootDir]
|
|
while (stack.length > 0) {
|
|
const dir = stack.pop()
|
|
let entries
|
|
try {
|
|
entries = readdirSync(dir, { withFileTypes: true })
|
|
} catch {
|
|
continue
|
|
}
|
|
for (const entry of entries) {
|
|
const fullPath = join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath)
|
|
} else if (entry.isFile() && entry.name === 'app.asar') {
|
|
matches.push(fullPath)
|
|
}
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
const distDir = process.argv[2] ?? 'dist'
|
|
if (!existsSync(distDir) || !statSync(distDir).isDirectory()) {
|
|
console.error(`::error::dist directory not found at ${distDir}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Why: with `TELEMETRY_ENABLED = false` in source, Rollup eliminates the
|
|
// transport block and the substituted constants vanish from the binary.
|
|
// Verifying in that state would always fail. Read the flag from source
|
|
// instead of inferring it from the build, so the gate cannot drift.
|
|
const clientSrcPath = join(repoRoot, 'src/main/telemetry/client.ts')
|
|
const clientSrc = readFileSync(clientSrcPath, 'utf8')
|
|
const enabledMatch = /^const\s+TELEMETRY_ENABLED\s*=\s*(true|false)/m.exec(clientSrc)
|
|
if (!enabledMatch) {
|
|
console.error(`::error::could not parse TELEMETRY_ENABLED flag from ${clientSrcPath}`)
|
|
process.exit(1)
|
|
}
|
|
if (enabledMatch[1] === 'false') {
|
|
console.log(
|
|
'TELEMETRY_ENABLED is false in source — transport is dead-code-eliminated, ' +
|
|
'so the BUILD_IDENTITY/WRITE_KEY constants are not expected in the binary. ' +
|
|
'Skipping asar grep. (Once the flag flips to true, this verify becomes enforcing.)'
|
|
)
|
|
process.exit(0)
|
|
}
|
|
|
|
const asarMatches = findAsar(distDir)
|
|
if (asarMatches.length === 0) {
|
|
console.error(`::error::could not locate app.asar under ${distDir}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`Found ${asarMatches.length} app.asar payload(s) under ${distDir}:`)
|
|
for (const m of asarMatches) {
|
|
console.log(` - ${m}`)
|
|
}
|
|
|
|
// Why these regexes: electron-vite's `define` block substitutes the bare
|
|
// identifiers `ORCA_BUILD_IDENTITY` and `ORCA_POSTHOG_WRITE_KEY` with their
|
|
// JSON-stringified values at build time. `src/main/telemetry/client.ts`
|
|
// then assigns those into module-local declarations named `BUILD_IDENTITY`
|
|
// and `WRITE_KEY`. Rollup may preserve `const` or lower it to `var`.
|
|
|
|
function verifyAsar(asarPath) {
|
|
console.log(`Verifying ${asarPath}`)
|
|
|
|
// Why list-then-extract (not a hardcoded path): future electron-vite
|
|
// chunking (e.g. `manualChunks`) could move the constants out of
|
|
// `out/main/index.js` into `out/main/chunks/telemetry-XYZ.js`. Enumerate
|
|
// every `.js` under `out/main/` and concatenate before grepping so the
|
|
// verify is resilient to chunking. listPackage builds entries with the
|
|
// host `path` module, so on Windows runners separators are backslashes
|
|
// (`\out\main\index.js`); normalize to forward slashes for the filter
|
|
// and strip the leading separator (of either kind) before passing the
|
|
// entry to extractFile.
|
|
const allEntries = listPackage(asarPath)
|
|
const mainJsEntries = allEntries.filter((p) => {
|
|
const normalized = p.replace(/\\/g, '/').replace(/^\/+/, '')
|
|
return normalized.startsWith('out/main/') && normalized.endsWith('.js')
|
|
})
|
|
|
|
if (mainJsEntries.length === 0) {
|
|
console.error(`::error::no .js files found under out/main/ in ${asarPath}`)
|
|
return null
|
|
}
|
|
|
|
const indexJs = mainJsEntries
|
|
.map((entry) => {
|
|
// extractFile uses the host path module internally; pass the
|
|
// host-separator path with the leading separator stripped.
|
|
const internal = entry.replace(/^[\\/]+/, '')
|
|
return extractFile(asarPath, internal).toString('utf8')
|
|
})
|
|
.join('\n')
|
|
|
|
const buildIdentityMatch = BUILD_IDENTITY_RE.exec(indexJs)
|
|
const writeKeyMatch = WRITE_KEY_RE.exec(indexJs)
|
|
const minifiedTelemetryMatch = MINIFIED_TELEMETRY_RE.exec(indexJs)
|
|
|
|
// Rolldown renames module-local constants in production output. In that
|
|
// form, verify the adjacent injected identity/key declaration instead.
|
|
const verifiedIdentity = buildIdentityMatch?.[1] ?? minifiedTelemetryMatch?.[1]
|
|
const verifiedWriteKey = writeKeyMatch?.[1] ?? minifiedTelemetryMatch?.[2]
|
|
|
|
if (!verifiedIdentity) {
|
|
console.error(`::error::BUILD_IDENTITY constant missing or unexpected value in ${asarPath}`)
|
|
const sample = indexJs.match(/.{0,80}BUILD_IDENTITY.{0,80}/g)?.slice(0, 5) ?? []
|
|
for (const line of sample) {
|
|
console.error(` ${line.slice(0, 200)}`)
|
|
}
|
|
return null
|
|
}
|
|
if (!verifiedWriteKey) {
|
|
console.error(`::error::PostHog WRITE_KEY missing from ${asarPath}`)
|
|
const sample = indexJs.match(/.{0,80}WRITE_KEY.{0,80}/g)?.slice(0, 5) ?? []
|
|
for (const line of sample) {
|
|
console.error(` ${line.slice(0, 200)}`)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return { asarPath, buildIdentity: verifiedIdentity, writeKey: verifiedWriteKey }
|
|
}
|
|
|
|
// Why verify every match (not just the first): macOS dual-arch produces one
|
|
// asar per arch from the same `out/main` bundle, so identical constants are
|
|
// expected — but if a future change introduces an arch-specific bundle split
|
|
// (or a per-arch `electron-vite build` invocation) that forgets to thread
|
|
// the `define` values, only one of the asars would carry the constants and
|
|
// the broken arch would ship transmitting nothing. Looping is cheap insurance
|
|
// against that class of regression. We additionally require every asar to
|
|
// agree on the BUILD_IDENTITY and WRITE_KEY values — a mismatch means the
|
|
// matrix shipped inconsistent build-identity claims, or (worse) per-arch
|
|
// PostHog keys that would split events across projects, both of which are
|
|
// release bugs.
|
|
const results = []
|
|
for (const asarPath of asarMatches) {
|
|
const result = verifyAsar(asarPath)
|
|
if (!result) {
|
|
process.exit(1)
|
|
}
|
|
results.push(result)
|
|
}
|
|
|
|
const distinctIdentities = new Set(results.map((r) => r.buildIdentity))
|
|
if (distinctIdentities.size > 1) {
|
|
console.error(`::error::asars disagree on BUILD_IDENTITY: ${[...distinctIdentities].join(', ')}`)
|
|
for (const r of results) {
|
|
console.error(` - ${r.asarPath}: ${r.buildIdentity}`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
const distinctWriteKeys = new Set(results.map((r) => r.writeKey))
|
|
if (distinctWriteKeys.size > 1) {
|
|
console.error(`::error::asars disagree on WRITE_KEY across arches`)
|
|
for (const r of results) {
|
|
console.error(` - ${r.asarPath}: ${r.writeKey.slice(0, 8)}... (length=${r.writeKey.length})`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
const [first] = results
|
|
console.log(
|
|
`Telemetry constants verified across ${results.length} asar(s): ` +
|
|
`BUILD_IDENTITY="${first.buildIdentity}", ` +
|
|
`WRITE_KEY="${first.writeKey.slice(0, 8)}..." (length=${first.writeKey.length})`
|
|
)
|