mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
fix: include orcad application payload in build identity
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { computeLocalOrcadBuildHash } from '../ssh/orcad-local-build-hash'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -172,3 +174,48 @@ describe('computeOrcadBuildHash', () => {
|
||||
expect(computeOrcadBuildHash('')).toBe('unknown')
|
||||
})
|
||||
})
|
||||
|
||||
describe('shared orcad build identity', () => {
|
||||
let directory: string
|
||||
beforeEach(() => {
|
||||
directory = mkdtempSync(join(tmpdir(), 'orcad-build-identity-'))
|
||||
})
|
||||
afterEach(() => rmSync(directory, { recursive: true, force: true }))
|
||||
|
||||
it('changes matching host and client hashes when only the application changes', () => {
|
||||
const entry = join(directory, 'orcad.js')
|
||||
writeFileSync(entry, 'import("./orcad-app")')
|
||||
writeFileSync(join(directory, 'orcad-app.js'), 'application-a')
|
||||
const first = computeLocalOrcadBuildHash(directory)
|
||||
expect(computeOrcadBuildHash(entry)).toBe(first)
|
||||
writeFileSync(join(directory, 'orcad-app.js'), 'application-b')
|
||||
const second = computeLocalOrcadBuildHash(directory)
|
||||
expect(second).not.toBe(first)
|
||||
expect(computeOrcadBuildHash(entry)).toBe(second)
|
||||
writeFileSync(entry, '// launcher changed\nimport("./orcad-app")')
|
||||
expect(computeLocalOrcadBuildHash(directory)).not.toBe(second)
|
||||
expect(computeOrcadBuildHash(entry)).toBe(computeLocalOrcadBuildHash(directory))
|
||||
})
|
||||
|
||||
it('preserves the original single-bundle hash for older deployments', () => {
|
||||
const entry = join(directory, 'orcad.js')
|
||||
const legacy = 'original single-bundle application'
|
||||
writeFileSync(entry, legacy)
|
||||
const expected = createHash('sha256').update(legacy).digest('hex').slice(0, 16)
|
||||
expect(computeLocalOrcadBuildHash(directory)).toBe(expected)
|
||||
expect(computeOrcadBuildHash(entry)).toBe(expected)
|
||||
})
|
||||
|
||||
it.each(['missing', 'unreadable'])(
|
||||
'refuses a %s application instead of hashing only the launcher',
|
||||
(kind) => {
|
||||
const entry = join(directory, 'orcad.js')
|
||||
writeFileSync(entry, 'import("./orcad-app")')
|
||||
if (kind === 'unreadable') {
|
||||
mkdirSync(join(directory, 'orcad-app.js'))
|
||||
}
|
||||
expect(() => computeLocalOrcadBuildHash(directory)).toThrow()
|
||||
expect(computeOrcadBuildHash(entry)).toBe('unknown')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
* to cross the process boundary: orcad drives it, the daemon performs it, and the verdict
|
||||
* travels back over the daemon's socket.
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { readOrcadBuildHash } from '../../shared/orcad-build-hash'
|
||||
import process from 'node:process'
|
||||
import { checkDaemonHealth, type DaemonHealth } from '../daemon/daemon-health'
|
||||
import {
|
||||
@@ -81,7 +80,7 @@ export function computeOrcadBuildHash(entryPath = process.argv[1]): string {
|
||||
return 'unknown'
|
||||
}
|
||||
try {
|
||||
return createHash('sha256').update(readFileSync(entryPath)).digest('hex').slice(0, 16)
|
||||
return readOrcadBuildHash(entryPath)
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
/**
|
||||
* The build identity the deploy expects the host to answer with.
|
||||
*
|
||||
* It must be computed the same way `computeOrcadBuildHash` computes it on the host —
|
||||
* sha256 of `orcad.js`, first 16 hex characters — or the activation gate would reject every
|
||||
* healthy candidate. Keeping the two in one comment is deliberate: they are one contract
|
||||
* split across a network, and the version string cannot stand in for it, because
|
||||
* `ORCA_VERSION` is whatever the launch command exported and two builds can carry one value.
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { readOrcadBuildHash } from '../../shared/orcad-build-hash'
|
||||
|
||||
export const ORCAD_BUILD_HASH_LENGTH = 16
|
||||
export { ORCAD_BUILD_HASH_LENGTH } from '../../shared/orcad-build-hash'
|
||||
|
||||
/** The activation gate compares this with the host's hash of the same installed bytes. */
|
||||
export function computeLocalOrcadBuildHash(localOrcadDir: string): string {
|
||||
const entry = join(localOrcadDir, 'orcad.js')
|
||||
return createHash('sha256')
|
||||
.update(readFileSync(entry))
|
||||
.digest('hex')
|
||||
.slice(0, ORCAD_BUILD_HASH_LENGTH)
|
||||
return readOrcadBuildHash(join(localOrcadDir, 'orcad.js'))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
export const ORCAD_BUILD_HASH_LENGTH = 16
|
||||
const APPLICATION_FILENAME = 'orcad-app.js'
|
||||
|
||||
/** Keep legacy identities stable; split builds must identify the application as well. */
|
||||
export function readOrcadBuildHash(entryPath: string): string {
|
||||
const entry = readFileSync(entryPath)
|
||||
let application: Buffer
|
||||
try {
|
||||
application = readFileSync(join(dirname(entryPath), APPLICATION_FILENAME))
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
'code' in error &&
|
||||
error.code === 'ENOENT' &&
|
||||
!entry.includes('orcad-app')
|
||||
) {
|
||||
return createHash('sha256').update(entry).digest('hex').slice(0, ORCAD_BUILD_HASH_LENGTH)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return createHash('sha256')
|
||||
.update(`orcad-split-build\0${entry.length}\0`)
|
||||
.update(entry)
|
||||
.update(`\0${application.length}\0`)
|
||||
.update(application)
|
||||
.digest('hex')
|
||||
.slice(0, ORCAD_BUILD_HASH_LENGTH)
|
||||
}
|
||||
Reference in New Issue
Block a user