mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(deps): root and mobile resolve one zod, and CI fails if they drift (#31260)
* fix(deps): root and mobile resolve one zod, and CI fails if they drift
Mobile type-checks src/shared zod schemas. With mobile on zod 4.6.5 and the
root on 4.6.7, TypeScript treated them as unrelated packages, so wrapped
schemas inferred {} (tests/e2e/helpers/phone-feed-list.ts:154) whenever a
job installed mobile deps.
* fix(deps): guard every package mobile's tsconfig remaps, not only zod
* test(ci): pin the shared-dependency version check to the static phase
This commit is contained in:
@@ -432,6 +432,10 @@ jobs:
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-node-runtime-pin.mjs
|
||||
|
||||
- name: Check root and mobile lock the same shared dependencies
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-mobile-shared-dependency-versions.mjs
|
||||
|
||||
# Why: extraction writes sorted evidence to an isolated temporary path,
|
||||
# so feature PRs need one normalized AST pass rather than a three-OS matrix.
|
||||
# Use preflight's pinned Bun even when the optional local pin differs.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
// Root and mobile are separate pnpm projects, so nothing else stops their lockfiles drifting apart.
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { lockedVersion, lockfileRootImporter } from './pnpm-lockfile-root-importer.mjs'
|
||||
|
||||
// The packages src/shared imports that mobile/tsconfig.json remaps to mobile's copy. TypeScript
|
||||
// merges two copies only at the same version; else zod-wrapped schemas infer `{}` (phone-feed-list.ts).
|
||||
export const SHARED_DEPENDENCIES = ['zod', 'tweetnacl', '@noble/hashes', '@types/ws']
|
||||
|
||||
function importerVersion(importer, name) {
|
||||
return lockedVersion(importer.dependencies?.[name] ?? importer.devDependencies?.[name])
|
||||
}
|
||||
|
||||
export function findSharedDependencyVersionProblems({
|
||||
rootImporter,
|
||||
mobileImporter,
|
||||
names = SHARED_DEPENDENCIES
|
||||
}) {
|
||||
const problems = []
|
||||
for (const name of names) {
|
||||
const root = importerVersion(rootImporter, name)
|
||||
const mobile = importerVersion(mobileImporter, name)
|
||||
if (!root || !mobile) {
|
||||
problems.push(`${name} is not a direct dependency of ${root ? 'mobile' : 'the root'}`)
|
||||
} else if (root !== mobile) {
|
||||
problems.push(`${name}: the root locks ${root}, mobile locks ${mobile}`)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
export function main(root = resolve(import.meta.dirname, '../..')) {
|
||||
const readImporter = (lockfile) =>
|
||||
lockfileRootImporter(readFileSync(join(root, lockfile), 'utf8'))
|
||||
const problems = findSharedDependencyVersionProblems({
|
||||
rootImporter: readImporter('pnpm-lock.yaml'),
|
||||
mobileImporter: readImporter(join('mobile', 'pnpm-lock.yaml'))
|
||||
})
|
||||
if (problems.length > 0) {
|
||||
console.error('Root and mobile must lock the same version of:')
|
||||
for (const problem of problems) {
|
||||
console.error(`- ${problem}`)
|
||||
}
|
||||
console.error(
|
||||
'Set the same range in package.json and mobile/package.json, then pnpm install in both.'
|
||||
)
|
||||
return 1
|
||||
}
|
||||
console.log(`Root and mobile lock the same ${SHARED_DEPENDENCIES.join(', ')}.`)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exit(main())
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
findSharedDependencyVersionProblems,
|
||||
main
|
||||
} from './check-mobile-shared-dependency-versions.mjs'
|
||||
|
||||
const projectDir = path.resolve(import.meta.dirname, '../..')
|
||||
const locked = (version) => ({ dependencies: { zod: { specifier: '~4.6.0', version } } })
|
||||
const checkZod = (rootImporter, mobileImporter) =>
|
||||
findSharedDependencyVersionProblems({ rootImporter, mobileImporter, names: ['zod'] })
|
||||
|
||||
describe('findSharedDependencyVersionProblems', () => {
|
||||
it('passes when both lockfiles resolve the same version', () => {
|
||||
expect(checkZod(locked('4.6.7'), locked('4.6.7'))).toEqual([])
|
||||
})
|
||||
|
||||
it('reports two resolved versions', () => {
|
||||
expect(checkZod(locked('4.6.7'), locked('4.6.5'))).toEqual([
|
||||
'zod: the root locks 4.6.7, mobile locks 4.6.5'
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores peer and patch suffixes and reads devDependencies', () => {
|
||||
expect(
|
||||
checkZod(
|
||||
{ devDependencies: { zod: { version: '4.6.7(typescript@6.0.3)' } } },
|
||||
locked('4.6.7(patch_hash=abc)')
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('reports a side that stopped declaring the dependency', () => {
|
||||
expect(checkZod(locked('4.6.7'), {})).toEqual(['zod is not a direct dependency of mobile'])
|
||||
expect(checkZod({}, locked('4.6.7'))).toEqual(['zod is not a direct dependency of the root'])
|
||||
})
|
||||
|
||||
it('checks every shared dependency by default', () => {
|
||||
const problems = findSharedDependencyVersionProblems({
|
||||
rootImporter: locked('4.6.7'),
|
||||
mobileImporter: locked('4.6.7')
|
||||
})
|
||||
expect(problems).toEqual([
|
||||
'tweetnacl is not a direct dependency of the root',
|
||||
'@noble/hashes is not a direct dependency of the root',
|
||||
'@types/ws is not a direct dependency of the root'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('committed lockfiles', () => {
|
||||
it('pass the repository check', () => {
|
||||
expect(main(projectDir)).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -5,8 +5,8 @@ import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseAllDocuments } from 'yaml'
|
||||
import { nodeDistArchiveName, windowsImportLibFile } from './node-dist-archive-name.mjs'
|
||||
import { lockedVersion, lockfileRootImporter } from './pnpm-lockfile-root-importer.mjs'
|
||||
|
||||
// Why require: an ESM import of a .ts file under a typeless package.json prints MODULE_TYPELESS_PACKAGE_JSON.
|
||||
const {
|
||||
@@ -25,24 +25,6 @@ function majorOf(range) {
|
||||
return match ? Number(match[1]) : null
|
||||
}
|
||||
|
||||
/** Strips pnpm's peer suffix: `43.7.5(supports-color@7.2.0)` -> `43.7.5`. */
|
||||
function lockedVersion(entry) {
|
||||
const version = typeof entry === 'string' ? entry : entry?.version
|
||||
return typeof version === 'string' ? version.replace(/\(.*$/, '') : null
|
||||
}
|
||||
|
||||
/** pnpm 12 splits the lockfile into a package-manager document and the project one; merge both. */
|
||||
export function lockfileRootImporter(contents) {
|
||||
const importer = {}
|
||||
for (const document of parseAllDocuments(contents)) {
|
||||
if (document.errors.length) {
|
||||
throw document.errors[0]
|
||||
}
|
||||
Object.assign(importer, document.toJS()?.importers?.['.'])
|
||||
}
|
||||
return importer
|
||||
}
|
||||
|
||||
function findAssetTableProblems({ pin, assets, targets, table, targetList }) {
|
||||
const problems = []
|
||||
const expected = new Set(targets)
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
NODE_RUNTIME_PIN,
|
||||
SERVER_TARGETS
|
||||
} from '../../src/shared/node-runtime-pin.ts'
|
||||
import {
|
||||
findNodeRuntimePinProblems,
|
||||
lockfileRootImporter,
|
||||
main
|
||||
} from './check-node-runtime-pin.mjs'
|
||||
import { findNodeRuntimePinProblems, main } from './check-node-runtime-pin.mjs'
|
||||
|
||||
const projectDir = path.resolve(import.meta.dirname, '../..')
|
||||
const HASH = 'a'.repeat(64)
|
||||
@@ -186,30 +182,6 @@ describe('findNodeRuntimePinProblems', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('lockfileRootImporter', () => {
|
||||
it('merges the root importer across pnpm 12 lockfile documents', () => {
|
||||
const contents = [
|
||||
'---',
|
||||
"lockfileVersion: '9.0'",
|
||||
'importers:',
|
||||
' .:',
|
||||
' packageManagerDependencies: {}',
|
||||
'---',
|
||||
"lockfileVersion: '9.0'",
|
||||
'importers:',
|
||||
' .:',
|
||||
' devDependencies:',
|
||||
' electron:',
|
||||
' specifier: 43.7.5',
|
||||
' version: 43.7.5(supports-color@7.2.0)',
|
||||
''
|
||||
].join('\n')
|
||||
expect(lockfileRootImporter(contents).devDependencies.electron.version).toBe(
|
||||
'43.7.5(supports-color@7.2.0)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('committed pin', () => {
|
||||
it('passes the repository check', () => {
|
||||
expect(main(projectDir)).toBe(0)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { parseAllDocuments } from 'yaml'
|
||||
|
||||
/** Strips pnpm's peer suffix: `43.7.5(supports-color@7.2.0)` -> `43.7.5`. */
|
||||
export function lockedVersion(entry) {
|
||||
const version = typeof entry === 'string' ? entry : entry?.version
|
||||
return typeof version === 'string' ? version.replace(/\(.*$/, '') : null
|
||||
}
|
||||
|
||||
/** pnpm 12 splits the lockfile into a package-manager document and the project one; merge both. */
|
||||
export function lockfileRootImporter(contents) {
|
||||
const importer = {}
|
||||
for (const document of parseAllDocuments(contents)) {
|
||||
if (document.errors.length) {
|
||||
throw document.errors[0]
|
||||
}
|
||||
Object.assign(importer, document.toJS()?.importers?.['.'])
|
||||
}
|
||||
return importer
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { lockedVersion, lockfileRootImporter } from './pnpm-lockfile-root-importer.mjs'
|
||||
|
||||
describe('lockfileRootImporter', () => {
|
||||
it('merges the root importer across pnpm 12 lockfile documents', () => {
|
||||
const contents = [
|
||||
'---',
|
||||
"lockfileVersion: '9.0'",
|
||||
'importers:',
|
||||
' .:',
|
||||
' packageManagerDependencies: {}',
|
||||
'---',
|
||||
"lockfileVersion: '9.0'",
|
||||
'importers:',
|
||||
' .:',
|
||||
' devDependencies:',
|
||||
' electron:',
|
||||
' specifier: 43.7.5',
|
||||
' version: 43.7.5(supports-color@7.2.0)',
|
||||
''
|
||||
].join('\n')
|
||||
expect(lockfileRootImporter(contents).devDependencies.electron.version).toBe(
|
||||
'43.7.5(supports-color@7.2.0)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('lockedVersion', () => {
|
||||
it('strips the peer suffix from an entry or a bare string', () => {
|
||||
expect(lockedVersion({ version: '4.6.7(typescript@6.0.3)' })).toBe('4.6.7')
|
||||
expect(lockedVersion('8.22.0(patch_hash=abc)')).toBe('8.22.0')
|
||||
expect(lockedVersion(undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -260,6 +260,7 @@ it('pins every foreground and background step to its selected phase', () => {
|
||||
['Enforce id-only store write ratchet', staticPhase],
|
||||
['Enforce repo host-read ratchet', staticPhase],
|
||||
['Check Node runtime pin', staticPhase],
|
||||
['Check root and mobile lock the same shared dependencies', staticPhase],
|
||||
['Decide whether the orcad terminal smoke runs', staticPhase],
|
||||
['Verify the generated RPC params catalog', staticPhase],
|
||||
['Verify the generated ACP protocol schema', staticPhase],
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@
|
||||
"react-native-worklets": "^0.8.3",
|
||||
"remend": "1.4.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"zod": "~4.6.5"
|
||||
"zod": "~4.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@orca/process-host": "link:../src/packages/process-host",
|
||||
|
||||
Generated
+1284
-1282
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -14,7 +14,7 @@
|
||||
"audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src",
|
||||
"test:perf:contracts": "vitest run --config config/vitest.performance.config.ts",
|
||||
"format": "oxfmt --write .",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:unproven-pty-retirement-ratchet && pnpm run check:id-only-store-write-ratchet && pnpm run check:repo-host-read-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:unproven-pty-retirement-ratchet && pnpm run check:id-only-store-write-ratchet && pnpm run check:repo-host-read-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run check:mobile-shared-dependency-versions && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports",
|
||||
"audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor",
|
||||
"audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings",
|
||||
"audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings",
|
||||
@@ -52,6 +52,7 @@
|
||||
"check:daemon-protocol-crossing": "node config/scripts/check-daemon-protocol-crossing.mjs",
|
||||
"check:runtime-launcher-protocol-ratchet": "node config/scripts/check-runtime-launcher-protocol-ratchet.mjs",
|
||||
"check:node-runtime-pin": "node config/scripts/check-node-runtime-pin.mjs",
|
||||
"check:mobile-shared-dependency-versions": "node config/scripts/check-mobile-shared-dependency-versions.mjs",
|
||||
"build:orcad": "pnpm run build:packages && node config/scripts/build-orcad-node.mjs",
|
||||
"build:orcad-template": "pnpm run build:packages && node config/scripts/build-orcad-template.mjs",
|
||||
"build:orcad-prebuilds": "pnpm run build:packages && node config/scripts/build-orcad-prebuilds.mjs",
|
||||
|
||||
Reference in New Issue
Block a user