import test from 'brittle'
import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
const SHA = 'a'.repeat(40)
const DIGEST = 'sha256:' + 'b'.repeat(64)
const FLEET_CHANNEL_CONFIG_SHA = '3'.repeat(64)
const EXPECTED_IMAGE_NAME = 'ghcr.io/bigdestiny2/p2p-hiverelay'
const EXPECTED_REPOSITORY = 'bigdestiny2/P2P-Hiverelay'
const STARTOS_REGISTRY_URL = 'https://registry.start9.com/startos'
const STARTOS_REGISTRY_PACKAGE_URL = `${STARTOS_REGISTRY_URL}/blindspark`
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
const WRITE_RELEASE_EVIDENCE_SCRIPT = path.join(process.cwd(), 'scripts/write-release-evidence.mjs')
const S9PK_BYTES = 'startos package\n'
const IMAGE_MANIFEST_BYTES = '{"kind":"release-image-manifest"}\n'
const IMAGE_SMOKE_BYTES = '{"kind":"release-image-smoke"}\n'
const UMBREL_SMOKE_BYTES = '{"kind":"umbrel-package-smoke"}\n'
const ROLLOUT_BYTES = '{"kind":"fleet-rollout"}\n'
const STARTOS_REGISTRY_EVIDENCE_BYTES = '{"kind":"startos-registry-publication"}\n'
const S9PK_SHA = sha256(S9PK_BYTES)
const ROLLOUT_SHA = sha256(ROLLOUT_BYTES)
const IMAGE_MANIFEST_SHA = sha256(IMAGE_MANIFEST_BYTES)
const IMAGE_SMOKE_SHA = sha256(IMAGE_SMOKE_BYTES)
const UMBREL_SMOKE_SHA = sha256(UMBREL_SMOKE_BYTES)
const STARTOS_REGISTRY_EVIDENCE_SHA = sha256(STARTOS_REGISTRY_EVIDENCE_BYTES)
function completeFullReleaseEnv (overrides = {}) {
const env = {
HIVERELAY_RELEASE_VERSION: 'v9.9.9',
HIVERELAY_RELEASE_SEMVER: '9.9.9',
HIVERELAY_RELEASE_CHANNEL: 'both',
HIVERELAY_RELEASE_PRERELEASE: 'false',
HIVERELAY_RELEASE_SHA: SHA,
HIVERELAY_RELEASE_SURFACES_SHA: SHA,
HIVERELAY_IMAGE_NAME: EXPECTED_IMAGE_NAME,
HIVERELAY_IMAGE_DIGEST: DIGEST,
HIVERELAY_WORKFLOW_STATUS: 'success',
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_REPOSITORY: EXPECTED_REPOSITORY,
GITHUB_RUN_ID: '12345',
GITHUB_RUN_ATTEMPT: '2',
HIVERELAY_RELEASE_GATE_STATUS: 'passed',
HIVERELAY_RELEASE_DISTRIBUTION_PREFLIGHT_STATUS: 'passed',
HIVERELAY_RELEASE_IMAGE_MANIFEST_STATUS: 'passed',
HIVERELAY_RELEASE_IMAGE_MANIFEST_EVIDENCE: 'release-image-manifest-evidence.json',
HIVERELAY_RELEASE_IMAGE_MANIFEST_EVIDENCE_SHA256: IMAGE_MANIFEST_SHA,
HIVERELAY_RELEASE_IMAGE_SMOKE_STATUS: 'passed',
HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE: 'release-image-smoke-evidence.json',
HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE_SHA256: IMAGE_SMOKE_SHA,
HIVERELAY_UMBREL_SMOKE_STATUS: 'passed',
HIVERELAY_UMBREL_SMOKE_EVIDENCE: 'umbrel-package-smoke-evidence.json',
HIVERELAY_UMBREL_SMOKE_EVIDENCE_SHA256: UMBREL_SMOKE_SHA,
HIVERELAY_STARTOS_VERIFY_STATUS: 'passed',
HIVERELAY_STARTOS_PACKAGE_SHA256: S9PK_SHA,
HIVERELAY_RELEASE_SURFACES_STATUS: 'committed',
HIVERELAY_NPM_PUBLISH_STATUS: 'published',
HIVERELAY_STARTOS_RELEASE_ASSET_STATUS: 'uploaded',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'verified',
HIVERELAY_FLEET_ROLLOUT_CHANNEL: 'both',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE: 'fleet-rollout-evidence.json',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE_SHA256: ROLLOUT_SHA,
HIVERELAY_FLEET_CHANNEL_CONFIG: 'fleet/channels.json',
HIVERELAY_FLEET_CHANNEL_CONFIG_SHA256: FLEET_CHANNEL_CONFIG_SHA,
HIVERELAY_STARTOS_REGISTRY_STATUS: 'published',
HIVERELAY_STARTOS_REGISTRY_URL: STARTOS_REGISTRY_URL,
HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: STARTOS_REGISTRY_PACKAGE_URL,
HIVERELAY_STARTOS_REGISTRY_EVIDENCE: 'startos-registry-evidence.json',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE_SHA256: STARTOS_REGISTRY_EVIDENCE_SHA,
HIVERELAY_STARTOS_PACKAGE_ID: 'blindspark',
HIVERELAY_UMBREL_OFFICIAL_PR_STATUS: 'draft-pr-ready',
HIVERELAY_UMBREL_OFFICIAL_PR_URL: 'https://github.com/getumbrel/umbrel-apps/pull/123',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: 'bigdestiny2:blindspark-v9.9.9',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_SHA: SHA,
HIVERELAY_UMBREL_OFFICIAL_PR_STATE: 'OPEN',
HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: 'true',
HIVERELAY_UMBREL_OFFICIAL_PR_BASE: 'master',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: 'bigdestiny2',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: 'blindspark-v9.9.9',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: SHA,
HIVERELAY_UMBREL_COMMUNITY_STORE_VALIDATE_STATUS: 'passed',
HIVERELAY_UMBREL_COMMUNITY_STORE_STATUS: 'pushed',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT: SHA,
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT_URL: `https://github.com/bigdestiny2/blindspark-umbrel-store/commit/${SHA}`,
...overrides
}
if (!Object.prototype.hasOwnProperty.call(overrides, 'HIVERELAY_FLEET_ROLLOUT_CHANNEL')) {
env.HIVERELAY_FLEET_ROLLOUT_CHANNEL = env.HIVERELAY_RELEASE_PRERELEASE === 'true'
? ''
: env.HIVERELAY_RELEASE_CHANNEL
}
if (env.HIVERELAY_RELEASE_PRERELEASE === 'true') {
if (!Object.prototype.hasOwnProperty.call(overrides, 'HIVERELAY_NPM_PUBLISH_STATUS')) {
env.HIVERELAY_NPM_PUBLISH_STATUS = 'skipped'
}
if (!Object.prototype.hasOwnProperty.call(overrides, 'HIVERELAY_FLEET_CHANNEL_CONFIG')) {
env.HIVERELAY_FLEET_CHANNEL_CONFIG = ''
}
if (!Object.prototype.hasOwnProperty.call(overrides, 'HIVERELAY_FLEET_CHANNEL_CONFIG_SHA256')) {
env.HIVERELAY_FLEET_CHANNEL_CONFIG_SHA256 = ''
}
}
return env
}
function partialFailedReleaseEnv (overrides = {}) {
return {
HIVERELAY_RELEASE_VERSION: 'v9.9.9',
HIVERELAY_RELEASE_SEMVER: '9.9.9',
HIVERELAY_RELEASE_CHANNEL: 'canary',
HIVERELAY_RELEASE_PRERELEASE: 'false',
HIVERELAY_RELEASE_SHA: SHA,
HIVERELAY_IMAGE_NAME: EXPECTED_IMAGE_NAME,
HIVERELAY_WORKFLOW_STATUS: 'failure',
HIVERELAY_RELEASE_GATE_STATUS: 'passed',
HIVERELAY_RELEASE_DISTRIBUTION_PREFLIGHT_STATUS: 'failed',
HIVERELAY_RELEASE_IMAGE_SMOKE_STATUS: 'pending',
HIVERELAY_UMBREL_SMOKE_STATUS: 'pending',
HIVERELAY_STARTOS_VERIFY_STATUS: 'pending',
HIVERELAY_RELEASE_SURFACES_STATUS: 'pending',
HIVERELAY_NPM_PUBLISH_STATUS: 'pending',
HIVERELAY_STARTOS_RELEASE_ASSET_STATUS: 'pending',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'skipped',
HIVERELAY_STARTOS_REGISTRY_STATUS: 'skipped',
HIVERELAY_UMBREL_OFFICIAL_PR_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_VALIDATE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_STATUS: 'skipped',
...overrides
}
}
async function runEvidence (outFile, env, opts = {}) {
const cwd = opts.cwd || path.dirname(path.resolve(outFile))
if (opts.linkedArtifacts !== false) await writeReleaseArtifacts(cwd, opts.linkedArtifacts || {})
return new Promise((resolve, reject) => {
execFile(process.execPath, [WRITE_RELEASE_EVIDENCE_SCRIPT, '--out', outFile], {
cwd,
env: { ...process.env, ...env },
timeout: 10000
}, (err, stdout, stderr) => {
if (err) {
err.stdout = stdout
err.stderr = stderr
reject(err)
} else {
resolve({ stdout, stderr })
}
})
})
}
async function writeReleaseArtifacts (dir, overrides = {}) {
const files = {
'release-image-manifest-evidence.json': IMAGE_MANIFEST_BYTES,
'release-image-smoke-evidence.json': IMAGE_SMOKE_BYTES,
'umbrel-package-smoke-evidence.json': UMBREL_SMOKE_BYTES,
'fleet-rollout-evidence.json': ROLLOUT_BYTES,
'startos-registry-evidence.json': STARTOS_REGISTRY_EVIDENCE_BYTES,
'startos/blindspark.s9pk': S9PK_BYTES
}
for (const [file, defaultBytes] of Object.entries(files)) {
if (overrides[file] === false) continue
await writeIfMissing(path.join(dir, file), overrides[file] || defaultBytes)
}
}
async function writeIfMissing (file, bytes) {
try {
await lstat(file)
return
} catch (e) {
if (!e || e.code !== 'ENOENT') throw e
}
await mkdir(path.dirname(file), { recursive: true })
await writeFile(file, bytes)
}
function sha256 (value) {
return createHash('sha256').update(value).digest('hex')
}
test('release evidence writer records digest, gates, and external surfaces', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_CHANNEL: 'canary'
}))
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.schemaVersion, 1)
t.ok(ISO_TIMESTAMP_PATTERN.test(body.generatedAt), 'release evidence generatedAt is an ISO timestamp')
t.is(body.release.version, 'v9.9.9')
t.is(body.release.channel, 'canary')
t.is(body.release.candidate, false)
t.is(body.release.workflow.runUrl, `https://github.com/${EXPECTED_REPOSITORY}/actions/runs/12345`)
t.is(body.image.ref, `${EXPECTED_IMAGE_NAME}:9.9.9@${DIGEST}`)
t.is(body.artifacts.startosPackage.sha256, S9PK_SHA)
t.is(body.gates.auditAndUnit, 'passed')
t.is(body.gates.distributionPreflight, 'passed')
t.is(body.gates.imageManifest, 'passed')
t.is(body.gates.imageManifestEvidence.path, 'release-image-manifest-evidence.json')
t.is(body.gates.imageManifestEvidence.sha256, IMAGE_MANIFEST_SHA)
t.is(body.gates.pushedImageSmokeEvidence.path, 'release-image-smoke-evidence.json')
t.is(body.gates.pushedImageSmokeEvidence.sha256, IMAGE_SMOKE_SHA)
t.is(body.gates.umbrelPackageSmokeEvidence.path, 'umbrel-package-smoke-evidence.json')
t.is(body.gates.umbrelPackageSmokeEvidence.sha256, UMBREL_SMOKE_SHA)
t.is(body.surfaces.npmPackages, 'published')
t.is(body.surfaces.fleetRollout, 'verified')
t.is(body.surfaces.fleetRolloutChannel, 'canary')
t.is(body.surfaces.fleetRolloutEvidence.path, 'fleet-rollout-evidence.json')
t.is(body.surfaces.fleetRolloutEvidence.sha256, ROLLOUT_SHA)
t.is(body.surfaces.fleetChannelConfig.path, 'fleet/channels.json')
t.is(body.surfaces.fleetChannelConfig.sha256, FLEET_CHANNEL_CONFIG_SHA)
t.is(body.surfaces.startosRegistry, 'published')
t.is(body.surfaces.startosRegistryUrl, STARTOS_REGISTRY_URL)
t.is(body.surfaces.startosRegistryPackageUrl, STARTOS_REGISTRY_PACKAGE_URL)
t.is(body.surfaces.startosRegistryEvidence.path, 'startos-registry-evidence.json')
t.is(body.surfaces.startosRegistryEvidence.sha256, STARTOS_REGISTRY_EVIDENCE_SHA)
t.is(body.surfaces.startosPackageId, 'blindspark')
t.is(body.surfaces.umbrelOfficial.prUrl, 'https://github.com/getumbrel/umbrel-apps/pull/123')
t.is(body.surfaces.umbrelOfficial.head, 'bigdestiny2:blindspark-v9.9.9')
t.is(body.surfaces.umbrelOfficial.headSha, SHA)
t.is(body.surfaces.umbrelOfficial.state, 'OPEN')
t.is(body.surfaces.umbrelOfficial.isDraft, true)
t.is(body.surfaces.umbrelOfficial.base, 'master')
t.is(body.surfaces.umbrelOfficial.headOwner, 'bigdestiny2')
t.is(body.surfaces.umbrelOfficial.headRef, 'blindspark-v9.9.9')
t.is(body.surfaces.umbrelOfficial.headOid, SHA)
t.is(body.surfaces.umbrelCommunityStore.publish, 'pushed')
t.is(body.surfaces.umbrelCommunityStore.commit, SHA)
t.is(body.surfaces.umbrelCommunityStore.commitUrl, `https://github.com/bigdestiny2/blindspark-umbrel-store/commit/${SHA}`)
})
test('release evidence writer records public gateway fleet rollout as canary-gated deferred work', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_CHANNEL: 'none',
HIVERELAY_PUBLIC_GATEWAY_RELEASE_ENABLED: 'true',
HIVERELAY_PUBLIC_GATEWAY_MANIFEST_STATUS: 'enabled',
HIVERELAY_PUBLIC_GATEWAY_MANIFEST_PATH: 'fleet/public-hive-gateway-release.json',
HIVERELAY_PUBLIC_GATEWAY_MANIFEST_SHA256: '4'.repeat(64),
HIVERELAY_PUBLIC_GATEWAY_RELEASE_TARGET: 'v9.9.9',
HIVERELAY_PUBLIC_GATEWAY_COMMIT_SHA: SHA,
HIVERELAY_PUBLIC_GATEWAY_ADMISSION_PROFILE: 'blind-substrate-public-v1',
HIVERELAY_PUBLIC_GATEWAY_COHORT_SIZE: '3',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'deferred-gateway-canary-gated',
HIVERELAY_FLEET_ROLLOUT_CHANNEL: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE_SHA256: '',
HIVERELAY_FLEET_CHANNEL_CONFIG: '',
HIVERELAY_FLEET_CHANNEL_CONFIG_SHA256: ''
}), {
linkedArtifacts: { 'fleet-rollout-evidence.json': false }
})
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.release.channel, 'none')
t.alike(body.release.publicGateway, {
enabled: true,
manifestStatus: 'enabled',
manifestPath: 'fleet/public-hive-gateway-release.json',
manifestSha256: '4'.repeat(64),
releaseTarget: 'v9.9.9',
commitSha: SHA,
admissionProfile: 'blind-substrate-public-v1',
cohortSize: 3
})
t.is(body.surfaces.fleetRollout, 'deferred-gateway-canary-gated')
t.is(body.surfaces.fleetRolloutEvidence.path, '')
t.is(body.surfaces.fleetChannelConfig.path, '')
})
test('release evidence writer rejects symlinked present sidecars before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
await writeFile(path.join(dir, 'release-image-smoke-target.json'), '{}\n')
await symlink('release-image-smoke-target.json', path.join(dir, 'release-image-smoke-evidence.json'))
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv(), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release image smoke evidence file must not be a symlink'))
})
test('release evidence writer rejects oversized present sidecars before hashing', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
await writeFile(path.join(dir, 'release-image-smoke-evidence.json'), Buffer.alloc(2 * 1024 * 1024 + 1, 'x'))
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv(), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release image smoke evidence file must be 2097152 bytes or smaller'))
})
test('release evidence writer rejects present sidecar hash drift', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const smokeEvidence = '{"kind":"release-image-smoke"}\n'
await writeFile(path.join(dir, 'release-image-smoke-evidence.json'), smokeEvidence)
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE_SHA256: '0'.repeat(64)
}), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release image smoke evidence SHA-256 does not match release-image-smoke-evidence.json'))
t.ok(err.stderr.includes(sha256(smokeEvidence)), 'reports the actual present sidecar hash')
})
test('release evidence writer rejects unsafe or malformed present sidecars before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const unsafeSmoke = '{"kind":"release-image-smoke","token":"Bearer abcdefghijklmnopqrstuvwxyz"}\n'
const unsafeFile = path.join(dir, 'unsafe-sidecar.json')
let unsafeErr = null
try {
await runEvidence(unsafeFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE_SHA256: sha256(unsafeSmoke)
}), {
cwd: dir,
linkedArtifacts: {
'release-image-smoke-evidence.json': unsafeSmoke
}
})
} catch (e) {
unsafeErr = e
}
t.ok(unsafeErr)
t.ok(unsafeErr.stderr.includes('release image smoke evidence must not expose bearer token'))
const malformedDir = path.join(dir, 'malformed')
const malformedFile = path.join(malformedDir, 'malformed-sidecar.json')
let malformedErr = null
try {
await runEvidence(malformedFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE_SHA256: sha256('not-json\n')
}), {
cwd: malformedDir,
linkedArtifacts: {
'release-image-smoke-evidence.json': 'not-json\n'
}
})
} catch (e) {
malformedErr = e
}
t.ok(malformedErr)
t.ok(malformedErr.stderr.includes('release image smoke evidence file must contain valid JSON'))
})
test('release evidence writer hashes public sidecars as streams', async (t) => {
const script = await readFile(WRITE_RELEASE_EVIDENCE_SCRIPT, 'utf8')
const evidenceVerifier = script.match(/async function verifyPresentEvidenceFile \(label, relativePath, expectedSha256\)[\s\S]*?\n}\n\nfunction readPublicEvidenceJson/)
t.ok(evidenceVerifier, 'sidecar verifier is present')
t.ok(script.includes('await verifyPresentEvidenceFiles(body, true)'), 'full release validation waits for sidecar preflight')
t.ok(script.includes('assertPublicSafeValues(readPublicEvidenceJson(label, file), label)'), 'sidecar verifier scans public JSON before writing')
t.ok((evidenceVerifier?.[0] || '').includes('const actualSha256 = await sha256File(file)'), 'sidecar verifier uses the streaming hash helper')
t.absent((evidenceVerifier?.[0] || '').includes('readFileSync(file))'), 'sidecar verifier does not hash by buffering the sidecar')
})
test('release evidence writer requires successful release sidecars and package before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const missingSidecarFile = path.join(dir, 'missing-sidecar.json')
let sidecarErr = null
try {
await runEvidence(missingSidecarFile, completeFullReleaseEnv(), {
cwd: dir,
linkedArtifacts: { 'release-image-manifest-evidence.json': false }
})
} catch (e) {
sidecarErr = e
}
t.ok(sidecarErr)
t.ok(sidecarErr.stderr.includes('release image manifest evidence file is required before writing successful release evidence: release-image-manifest-evidence.json'))
const missingPackageDir = path.join(dir, 'missing-package')
const missingPackageFile = path.join(missingPackageDir, 'release-evidence.json')
let packageErr = null
try {
await runEvidence(missingPackageFile, completeFullReleaseEnv(), {
cwd: missingPackageDir,
linkedArtifacts: { 'startos/blindspark.s9pk': false }
})
} catch (e) {
packageErr = e
}
t.ok(packageErr)
t.ok(packageErr.stderr.includes('StartOS package file is required before writing successful release evidence: startos/blindspark.s9pk'))
})
test('release evidence writer verifies present StartOS package artifacts before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const packageBytes = 'startos package\n'
await mkdir(path.join(dir, 'startos'), { recursive: true })
await writeFile(path.join(dir, 'startos/blindspark.s9pk'), packageBytes)
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_STARTOS_PACKAGE_SHA256: sha256(packageBytes)
}), { cwd: dir })
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.artifacts.startosPackage.path, 'startos/blindspark.s9pk')
t.is(body.artifacts.startosPackage.sha256, sha256(packageBytes))
})
test('release evidence writer hashes present StartOS package artifacts as a stream', async (t) => {
const script = await readFile(WRITE_RELEASE_EVIDENCE_SCRIPT, 'utf8')
const artifactVerifier = script.match(/async function verifyPresentArtifactFile[\s\S]*?\n}\n\nfunction sha256File/)
t.ok(artifactVerifier, 'artifact verifier is present')
t.ok(script.includes('await verifyPresentStartosPackage(body)'), 'successful release validation waits for package preflight')
t.ok(script.includes('const actualSha256 = await sha256File(file)'), 'artifact verifier uses the streaming helper')
t.ok(script.includes('fs.createReadStream(file)'), 'package hash check streams the .s9pk')
t.absent((artifactVerifier?.[0] || '').includes('readFileSync'), 'artifact verifier does not load the whole .s9pk into memory')
})
test('release evidence writer rejects symlinked present StartOS package artifacts', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
await mkdir(path.join(dir, 'startos'), { recursive: true })
await writeFile(path.join(dir, 'startos/package-target.s9pk'), 'startos package\n')
await symlink('package-target.s9pk', path.join(dir, 'startos/blindspark.s9pk'))
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv(), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('StartOS package file must not be a symlink'))
})
test('release evidence writer rejects non-regular present StartOS package artifacts', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
await mkdir(path.join(dir, 'startos/blindspark.s9pk'), { recursive: true })
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv(), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('StartOS package file must be a regular file'))
})
test('release evidence writer rejects present StartOS package artifact hash drift', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const packageBytes = 'stale startos package\n'
await mkdir(path.join(dir, 'startos'), { recursive: true })
await writeFile(path.join(dir, 'startos/blindspark.s9pk'), packageBytes)
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_STARTOS_PACKAGE_SHA256: '0'.repeat(64)
}), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('StartOS package SHA-256 does not match startos/blindspark.s9pk'))
t.ok(err.stderr.includes(sha256(packageBytes)), 'reports the actual present package hash')
})
test('release evidence writer hashes canonical fleet channel config fallbacks from the release workspace', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const channels = '{"canary":{"target":"v9.9.9"}}\n'
await mkdir(path.join(dir, 'fleet'), { recursive: true })
await writeFile(path.join(dir, 'fleet/channels.json'), channels)
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, partialFailedReleaseEnv(), { cwd: dir })
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.surfaces.fleetChannelConfig.path, 'fleet/channels.json')
t.is(body.surfaces.fleetChannelConfig.sha256, sha256(channels))
})
test('release evidence writer rejects non-canonical fleet channel config hash fallback paths', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, partialFailedReleaseEnv({
HIVERELAY_FLEET_CHANNEL_CONFIG: '../channels.json'
}), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('fleet channel config path must be fleet/channels.json'))
})
test('release evidence writer rejects symlinked fleet channel config hash fallbacks', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
await mkdir(path.join(dir, 'fleet'), { recursive: true })
await writeFile(path.join(dir, 'fleet/channels-target.json'), '{}\n')
await symlink('channels-target.json', path.join(dir, 'fleet/channels.json'))
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, partialFailedReleaseEnv(), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('fleet channel config file must not be a symlink'))
})
test('release evidence writer rejects oversized fleet channel config hash fallbacks', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
await mkdir(path.join(dir, 'fleet'), { recursive: true })
await writeFile(path.join(dir, 'fleet/channels.json'), Buffer.alloc(2 * 1024 * 1024 + 1, 'x'))
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, partialFailedReleaseEnv(), { cwd: dir })
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('fleet channel config file must be 2097152 bytes or smaller'))
})
test('release evidence writer rejects successful full releases with incomplete distribution proof', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_STATUS: 'skipped'
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('successful full release StartOS registry publish'))
})
test('release evidence writer rejects successful full releases with stale official Umbrel PR state', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const closedFile = path.join(dir, 'closed-official-pr.json')
let closedErr = null
try {
await runEvidence(closedFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_STATE: 'CLOSED'
}))
} catch (e) {
closedErr = e
}
t.ok(closedErr)
t.ok(closedErr.stderr.includes('successful full release official Umbrel PR state'))
const zeroPrFile = path.join(dir, 'zero-official-pr.json')
let zeroPrErr = null
try {
await runEvidence(zeroPrFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_URL: 'https://github.com/getumbrel/umbrel-apps/pull/0'
}))
} catch (e) {
zeroPrErr = e
}
t.ok(zeroPrErr)
t.ok(zeroPrErr.stderr.includes('successful full release official Umbrel PR URL'))
const readyFile = path.join(dir, 'ready-official-pr.json')
let draftErr = null
try {
await runEvidence(readyFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: 'false'
}))
} catch (e) {
draftErr = e
}
t.ok(draftErr)
t.ok(draftErr.stderr.includes('successful full release official Umbrel PR draft'))
const baseFile = path.join(dir, 'wrong-official-pr-base.json')
let baseErr = null
try {
await runEvidence(baseFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_BASE: 'main'
}))
} catch (e) {
baseErr = e
}
t.ok(baseErr)
t.ok(baseErr.stderr.includes('successful full release official Umbrel PR base'))
const headRefFile = path.join(dir, 'wrong-official-pr-head-ref.json')
let headRefErr = null
try {
await runEvidence(headRefFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: 'other-branch'
}))
} catch (e) {
headRefErr = e
}
t.ok(headRefErr)
t.ok(headRefErr.stderr.includes('successful full release official Umbrel PR head ref matches head branch'))
const headOwnerFile = path.join(dir, 'wrong-official-pr-head-owner.json')
let headOwnerErr = null
try {
await runEvidence(headOwnerFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: 'attacker'
}))
} catch (e) {
headOwnerErr = e
}
t.ok(headOwnerErr)
t.ok(headOwnerErr.stderr.includes('successful full release official Umbrel PR head owner matches head owner'))
const headOidFile = path.join(dir, 'wrong-official-pr-head-oid.json')
let headOidErr = null
try {
await runEvidence(headOidFile, completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: 'b'.repeat(40)
}))
} catch (e) {
headOidErr = e
}
t.ok(headOidErr)
t.ok(headOidErr.stderr.includes('successful full release official Umbrel PR head OID matches head SHA'))
})
test('release evidence writer rejects credentialed StartOS registry URLs', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_URL: 'https://user:pass@registry.start9.com/startos'
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release evidence must not expose URL credentials at $.surfaces.startosRegistryUrl'))
})
test('release evidence writer rejects malformed official Umbrel PR GitHub owner names', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
for (const owner of ['bad_owner', 'bad.owner', '-bad', 'getumbrel', 'GetUmbrel']) {
let err = null
try {
await runEvidence(path.join(dir, `${owner}-release-evidence.json`), completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: `${owner}:blindspark-v9.9.9`,
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: owner
}))
} catch (e) {
err = e
}
t.ok(err, owner)
t.ok(err.stderr.includes('successful full release official Umbrel PR head owner must be a normal GitHub owner name'), owner)
}
})
test('release evidence writer rejects malformed official Umbrel PR GitHub head refs', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
for (const ref of ['bad..ref', '.bad-ref', 'bad/ref.lock']) {
let err = null
try {
await runEvidence(path.join(dir, `${ref.replace(/\//g, '-')}-release-evidence.json`), completeFullReleaseEnv({
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: `bigdestiny2:${ref}`,
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: ref
}))
} catch (e) {
err = e
}
t.ok(err, ref)
t.ok(err.stderr.includes('successful full release official Umbrel PR head ref must be a normal GitHub branch name'), ref)
}
})
test('release evidence writer rejects placeholder StartOS registry hosts', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_URL: 'https://registry.example/startos'
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('reserved/local hostnames'))
})
test('release evidence writer rejects unsafe public values before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_IMAGE_NAME: `${EXPECTED_IMAGE_NAME} Authorization: Bearer abcdefghijklmnopqrstuvwxyz`
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release evidence must not expose authorization header'))
let wroteEvidence = true
try {
await readFile(outFile, 'utf8')
} catch (e) {
if (e && e.code === 'ENOENT') wroteEvidence = false
else throw e
}
t.is(wroteEvidence, false)
})
test('release evidence writer rejects env-style secret public values before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_IMAGE_NAME: `${EXPECTED_IMAGE_NAME} APP_SEED=super-secret-seed`
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release evidence must not expose APP_SEED'))
let wroteEvidence = true
try {
await readFile(outFile, 'utf8')
} catch (e) {
if (e && e.code === 'ENOENT') wroteEvidence = false
else throw e
}
t.is(wroteEvidence, false)
})
test('release evidence writer rejects control-character public values before writing evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_IMAGE_NAME: `${EXPECTED_IMAGE_NAME}\nrelease`
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('release evidence must not contain control characters'))
let wroteEvidence = true
try {
await readFile(outFile, 'utf8')
} catch (e) {
if (e && e.code === 'ENOENT') wroteEvidence = false
else throw e
}
t.is(wroteEvidence, false)
})
test('release evidence writer rejects successful releases with malformed workflow identity', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
GITHUB_RUN_ID: 'not-a-number'
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('successful release workflow run id'))
const zeroRunIdFile = path.join(dir, 'zero-run-id.json')
let zeroRunIdErr = null
try {
await runEvidence(zeroRunIdFile, completeFullReleaseEnv({
GITHUB_RUN_ID: '0'
}))
} catch (e) {
zeroRunIdErr = e
}
t.ok(zeroRunIdErr)
t.ok(zeroRunIdErr.stderr.includes('successful release workflow run id'))
const zeroAttemptFile = path.join(dir, 'zero-run-attempt.json')
let zeroAttemptErr = null
try {
await runEvidence(zeroAttemptFile, completeFullReleaseEnv({
GITHUB_RUN_ATTEMPT: '0'
}))
} catch (e) {
zeroAttemptErr = e
}
t.ok(zeroAttemptErr)
t.ok(zeroAttemptErr.stderr.includes('successful release workflow run attempt'))
const wrongRepoFile = path.join(dir, 'wrong-repository.json')
let wrongRepoErr = null
try {
await runEvidence(wrongRepoFile, completeFullReleaseEnv({
GITHUB_REPOSITORY: 'attacker/P2P-Hiverelay'
}))
} catch (e) {
wrongRepoErr = e
}
t.ok(wrongRepoErr)
t.ok(wrongRepoErr.stderr.includes(`successful release workflow repository must be "${EXPECTED_REPOSITORY}"`))
})
test('release evidence writer rejects whitespace-normalized metadata before write', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const cases = [
['padded-version', { HIVERELAY_RELEASE_VERSION: 'v9.9.9 ' }, 'HIVERELAY_RELEASE_VERSION must be a v-prefixed semver tag'],
['padded-tag-sha', { HIVERELAY_RELEASE_SHA: `${SHA} ` }, 'HIVERELAY_RELEASE_SHA must be a 40-character commit SHA'],
['padded-run-id', { GITHUB_RUN_ID: '12345 ' }, 'successful release workflow run id'],
['padded-run-attempt', { GITHUB_RUN_ATTEMPT: '2 ' }, 'successful release workflow run attempt'],
['padded-workflow-server', { GITHUB_SERVER_URL: 'https://github.com ' }, 'successful release workflow URL'],
['padded-image-digest', { HIVERELAY_IMAGE_DIGEST: `${DIGEST} ` }, 'HIVERELAY_IMAGE_DIGEST must be sha256:<64 hex chars>'],
['padded-sidecar-path', { HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE: 'release-image-smoke-evidence.json ' }, 'successful release image smoke evidence path'],
['padded-registry-url', { HIVERELAY_STARTOS_REGISTRY_URL: `${STARTOS_REGISTRY_URL} ` }, 'HIVERELAY_STARTOS_REGISTRY_URL must be a public https URL'],
['padded-umbrel-pr-url', { HIVERELAY_UMBREL_OFFICIAL_PR_URL: 'https://github.com/getumbrel/umbrel-apps/pull/123 ' }, 'successful full release official Umbrel PR URL must point to getumbrel/umbrel-apps']
]
for (const [name, overrides, expectedError] of cases) {
const outFile = path.join(dir, `${name}.json`)
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv(overrides))
} catch (e) {
err = e
}
t.ok(err, `${name} is rejected`)
t.ok(err.stderr.includes(expectedError), `${name} reports the malformed raw field`)
let wroteEvidence = true
try {
await readFile(outFile, 'utf8')
} catch (e) {
if (e && e.code === 'ENOENT') wroteEvidence = false
else throw e
}
t.is(wroteEvidence, false, `${name} does not write release evidence`)
}
})
test('release evidence writer rejects internally inconsistent release identity', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const semverFile = path.join(dir, 'bad-semver.json')
let semverErr = null
try {
await runEvidence(semverFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_SEMVER: '9.9.8'
}))
} catch (e) {
semverErr = e
}
t.ok(semverErr)
t.ok(semverErr.stderr.includes('release semver matches version'))
const malformedImageFile = path.join(dir, 'malformed-image.json')
let malformedImageErr = null
try {
await runEvidence(malformedImageFile, completeFullReleaseEnv({
HIVERELAY_IMAGE_NAME: 'example/hiverelay'
}))
} catch (e) {
malformedImageErr = e
}
t.ok(malformedImageErr)
t.ok(malformedImageErr.stderr.includes('successful release image name'))
const wrongImageFile = path.join(dir, 'wrong-image.json')
let wrongImageErr = null
try {
await runEvidence(wrongImageFile, completeFullReleaseEnv({
HIVERELAY_IMAGE_NAME: 'ghcr.io/attacker/p2p-hiverelay'
}))
} catch (e) {
wrongImageErr = e
}
t.ok(wrongImageErr)
t.ok(wrongImageErr.stderr.includes(`successful release image name must be "${EXPECTED_IMAGE_NAME}"`))
const metadataFile = path.join(dir, 'bad-metadata.json')
let metadataErr = null
try {
await runEvidence(metadataFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_SURFACES_SHA: 'not-a-sha'
}))
} catch (e) {
metadataErr = e
}
t.ok(metadataErr)
t.ok(metadataErr.stderr.includes('HIVERELAY_RELEASE_SURFACES_SHA'))
})
test('release evidence writer rejects successful full releases without artifact or rollout-channel proof', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const missingArtifact = path.join(dir, 'missing-artifact.json')
let artifactErr = null
try {
await runEvidence(missingArtifact, completeFullReleaseEnv({
HIVERELAY_STARTOS_PACKAGE_SHA256: ''
}))
} catch (e) {
artifactErr = e
}
t.ok(artifactErr)
t.ok(artifactErr.stderr.includes('successful release StartOS package SHA-256'))
const mismatchedChannel = path.join(dir, 'mismatched-channel.json')
let channelErr = null
try {
await runEvidence(mismatchedChannel, completeFullReleaseEnv({
HIVERELAY_RELEASE_CHANNEL: 'stable',
HIVERELAY_FLEET_ROLLOUT_CHANNEL: 'canary'
}))
} catch (e) {
channelErr = e
}
t.ok(channelErr)
t.ok(channelErr.stderr.includes('successful full release fleet rollout channel'))
const missingRolloutEvidence = path.join(dir, 'missing-rollout-evidence.json')
let rolloutEvidenceErr = null
try {
await runEvidence(missingRolloutEvidence, completeFullReleaseEnv({
HIVERELAY_FLEET_ROLLOUT_EVIDENCE_SHA256: ''
}))
} catch (e) {
rolloutEvidenceErr = e
}
t.ok(rolloutEvidenceErr)
t.ok(rolloutEvidenceErr.stderr.includes('successful full release fleet rollout evidence SHA-256'))
const malformedFleetChannels = path.join(dir, 'malformed-fleet-channels.json')
let fleetChannelsErr = null
try {
await runEvidence(malformedFleetChannels, completeFullReleaseEnv({
HIVERELAY_FLEET_CHANNEL_CONFIG_SHA256: 'not-a-sha'
}))
} catch (e) {
fleetChannelsErr = e
}
t.ok(fleetChannelsErr)
t.ok(fleetChannelsErr.stderr.includes('HIVERELAY_FLEET_CHANNEL_CONFIG_SHA256'))
const missingImageSmokeEvidence = path.join(dir, 'missing-image-smoke-evidence.json')
let imageSmokeEvidenceErr = null
try {
await runEvidence(missingImageSmokeEvidence, completeFullReleaseEnv({
HIVERELAY_RELEASE_IMAGE_SMOKE_EVIDENCE_SHA256: ''
}))
} catch (e) {
imageSmokeEvidenceErr = e
}
t.ok(imageSmokeEvidenceErr)
t.ok(imageSmokeEvidenceErr.stderr.includes('successful release image smoke evidence SHA-256'))
const missingUmbrelSmokeEvidence = path.join(dir, 'missing-umbrel-smoke-evidence.json')
let umbrelSmokeEvidenceErr = null
try {
await runEvidence(missingUmbrelSmokeEvidence, completeFullReleaseEnv({
HIVERELAY_UMBREL_SMOKE_EVIDENCE_SHA256: ''
}))
} catch (e) {
umbrelSmokeEvidenceErr = e
}
t.ok(umbrelSmokeEvidenceErr)
t.ok(umbrelSmokeEvidenceErr.stderr.includes('successful release Umbrel package smoke evidence SHA-256'))
const missingStartosRegistryUrl = path.join(dir, 'missing-startos-registry-url.json')
let startosRegistryUrlErr = null
try {
await runEvidence(missingStartosRegistryUrl, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_URL: '',
HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: ''
}))
} catch (e) {
startosRegistryUrlErr = e
}
t.ok(startosRegistryUrlErr)
t.ok(startosRegistryUrlErr.stderr.includes('successful full release StartOS registry URL'))
const missingStartosRegistryPackageUrl = path.join(dir, 'missing-startos-registry-package-url.json')
let startosRegistryPackageUrlErr = null
try {
await runEvidence(missingStartosRegistryPackageUrl, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: ''
}))
} catch (e) {
startosRegistryPackageUrlErr = e
}
t.ok(startosRegistryPackageUrlErr)
t.ok(startosRegistryPackageUrlErr.stderr.includes('successful full release StartOS registry package URL'))
const missingStartosRegistryEvidencePath = path.join(dir, 'missing-startos-registry-evidence-path.json')
let startosRegistryEvidencePathErr = null
try {
await runEvidence(missingStartosRegistryEvidencePath, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_EVIDENCE: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE_SHA256: ''
}))
} catch (e) {
startosRegistryEvidencePathErr = e
}
t.ok(startosRegistryEvidencePathErr)
t.ok(startosRegistryEvidencePathErr.stderr.includes('successful full release StartOS registry evidence path'))
const missingStartosRegistryEvidence = path.join(dir, 'missing-startos-registry-evidence-hash.json')
let startosRegistryEvidenceErr = null
try {
await runEvidence(missingStartosRegistryEvidence, completeFullReleaseEnv({
HIVERELAY_STARTOS_REGISTRY_EVIDENCE_SHA256: ''
}))
} catch (e) {
startosRegistryEvidenceErr = e
}
t.ok(startosRegistryEvidenceErr)
t.ok(startosRegistryEvidenceErr.stderr.includes('successful full release StartOS registry evidence SHA-256'))
const missingUmbrelCommit = path.join(dir, 'missing-umbrel-commit.json')
let umbrelCommitErr = null
try {
await runEvidence(missingUmbrelCommit, completeFullReleaseEnv({
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT: ''
}))
} catch (e) {
umbrelCommitErr = e
}
t.ok(umbrelCommitErr)
t.ok(umbrelCommitErr.stderr.includes('successful full release Umbrel community-store commit'))
})
test('release evidence writer allows successful prereleases to skip distribution surfaces', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_VERSION: 'v9.9.9-beta.1',
HIVERELAY_RELEASE_SEMVER: '9.9.9-beta.1',
HIVERELAY_RELEASE_CHANNEL: 'none',
HIVERELAY_RELEASE_PRERELEASE: 'true',
HIVERELAY_RELEASE_DISTRIBUTION_PREFLIGHT_STATUS: 'skipped',
HIVERELAY_RELEASE_SURFACES_STATUS: 'skipped',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'skipped',
HIVERELAY_FLEET_ROLLOUT_CHANNEL: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE_SHA256: '',
HIVERELAY_STARTOS_REGISTRY_STATUS: 'skipped',
HIVERELAY_STARTOS_REGISTRY_URL: '',
HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE_SHA256: '',
HIVERELAY_STARTOS_PACKAGE_ID: '',
STARTOS_REGISTRY_URL,
STARTOS_REGISTRY_PACKAGE_URL,
HIVERELAY_UMBREL_OFFICIAL_PR_STATUS: 'skipped',
HIVERELAY_UMBREL_OFFICIAL_PR_URL: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_SHA: '',
HIVERELAY_UMBREL_OFFICIAL_PR_STATE: '',
HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: '',
HIVERELAY_UMBREL_OFFICIAL_PR_BASE: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: '',
HIVERELAY_UMBREL_COMMUNITY_STORE_VALIDATE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT: '',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT_URL: ''
}))
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.release.prerelease, true)
t.is(body.gates.distributionPreflight, 'skipped')
t.is(body.surfaces.npmPackages, 'skipped')
t.is(body.surfaces.startosReleaseAsset, 'uploaded')
t.is(body.surfaces.fleetRollout, 'skipped')
})
test('release evidence writer allows branch candidates to skip release asset publication', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_VERSION: 'v9.9.9-ship.20260624.abcdef123456',
HIVERELAY_RELEASE_SEMVER: '9.9.9-ship.20260624.abcdef123456',
HIVERELAY_RELEASE_CHANNEL: 'none',
HIVERELAY_RELEASE_PRERELEASE: 'true',
HIVERELAY_RELEASE_CANDIDATE: 'true',
HIVERELAY_RELEASE_DISTRIBUTION_PREFLIGHT_STATUS: 'skipped',
HIVERELAY_RELEASE_SURFACES_STATUS: 'skipped',
HIVERELAY_STARTOS_RELEASE_ASSET_STATUS: 'skipped',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'skipped',
HIVERELAY_FLEET_ROLLOUT_CHANNEL: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE_SHA256: '',
HIVERELAY_STARTOS_REGISTRY_STATUS: 'skipped',
HIVERELAY_STARTOS_REGISTRY_URL: '',
HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE_SHA256: '',
HIVERELAY_STARTOS_PACKAGE_ID: '',
HIVERELAY_UMBREL_OFFICIAL_PR_STATUS: 'skipped',
HIVERELAY_UMBREL_OFFICIAL_PR_URL: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_SHA: '',
HIVERELAY_UMBREL_OFFICIAL_PR_STATE: '',
HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: '',
HIVERELAY_UMBREL_OFFICIAL_PR_BASE: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: '',
HIVERELAY_UMBREL_COMMUNITY_STORE_VALIDATE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT: '',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT_URL: ''
}))
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.release.prerelease, true)
t.is(body.release.candidate, true)
t.is(body.surfaces.npmPackages, 'skipped')
t.is(body.surfaces.startosReleaseAsset, 'skipped')
t.is(body.surfaces.startosRegistryUrl, '')
t.is(body.surfaces.startosRegistryPackageUrl, '')
t.is(body.surfaces.fleetRollout, 'skipped')
})
test('release evidence writer rejects successful prereleases with promotion channels', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
let err = null
try {
await runEvidence(outFile, completeFullReleaseEnv({
HIVERELAY_RELEASE_VERSION: 'v9.9.9-beta.1',
HIVERELAY_RELEASE_SEMVER: '9.9.9-beta.1',
HIVERELAY_RELEASE_CHANNEL: 'canary',
HIVERELAY_RELEASE_PRERELEASE: 'true',
HIVERELAY_RELEASE_DISTRIBUTION_PREFLIGHT_STATUS: 'skipped',
HIVERELAY_RELEASE_SURFACES_STATUS: 'skipped',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'skipped',
HIVERELAY_FLEET_ROLLOUT_CHANNEL: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE: '',
HIVERELAY_FLEET_ROLLOUT_EVIDENCE_SHA256: '',
HIVERELAY_STARTOS_REGISTRY_STATUS: 'skipped',
HIVERELAY_STARTOS_REGISTRY_URL: '',
HIVERELAY_STARTOS_REGISTRY_PACKAGE_URL: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE: '',
HIVERELAY_STARTOS_REGISTRY_EVIDENCE_SHA256: '',
HIVERELAY_STARTOS_PACKAGE_ID: '',
HIVERELAY_UMBREL_OFFICIAL_PR_STATUS: 'skipped',
HIVERELAY_UMBREL_OFFICIAL_PR_URL: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_SHA: '',
HIVERELAY_UMBREL_OFFICIAL_PR_STATE: '',
HIVERELAY_UMBREL_OFFICIAL_PR_DRAFT: '',
HIVERELAY_UMBREL_OFFICIAL_PR_BASE: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OWNER: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_REF: '',
HIVERELAY_UMBREL_OFFICIAL_PR_HEAD_OID: '',
HIVERELAY_UMBREL_COMMUNITY_STORE_VALIDATE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT: '',
HIVERELAY_UMBREL_COMMUNITY_STORE_COMMIT_URL: ''
}))
} catch (e) {
err = e
}
t.ok(err)
t.ok(err.stderr.includes('successful prerelease channel'))
})
test('release evidence writer supports partial failed-run evidence', async (t) => {
const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-evidence-'))
t.teardown(async () => {
await rm(dir, { recursive: true, force: true })
})
const outFile = path.join(dir, 'release-evidence.json')
await runEvidence(outFile, {
HIVERELAY_RELEASE_VERSION: 'v9.9.9',
HIVERELAY_RELEASE_SEMVER: '9.9.9',
HIVERELAY_RELEASE_CHANNEL: 'canary',
HIVERELAY_RELEASE_PRERELEASE: 'false',
HIVERELAY_RELEASE_SHA: SHA,
HIVERELAY_IMAGE_NAME: EXPECTED_IMAGE_NAME,
HIVERELAY_WORKFLOW_STATUS: 'failure',
HIVERELAY_RELEASE_GATE_STATUS: 'passed',
HIVERELAY_RELEASE_DISTRIBUTION_PREFLIGHT_STATUS: 'failed',
HIVERELAY_RELEASE_IMAGE_SMOKE_STATUS: 'pending',
HIVERELAY_UMBREL_SMOKE_STATUS: 'pending',
HIVERELAY_STARTOS_VERIFY_STATUS: 'pending',
HIVERELAY_RELEASE_SURFACES_STATUS: 'pending',
HIVERELAY_STARTOS_RELEASE_ASSET_STATUS: 'pending',
HIVERELAY_FLEET_ROLLOUT_STATUS: 'skipped',
HIVERELAY_STARTOS_REGISTRY_STATUS: 'skipped',
HIVERELAY_UMBREL_OFFICIAL_PR_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_VALIDATE_STATUS: 'skipped',
HIVERELAY_UMBREL_COMMUNITY_STORE_STATUS: 'skipped'
})
const body = JSON.parse(await readFile(outFile, 'utf8'))
t.is(body.release.workflow.status, 'failure')
t.is(body.image.digest, '')
t.is(body.image.ref, '')
t.is(body.gates.auditAndUnit, 'passed')
t.is(body.gates.distributionPreflight, 'failed')
t.is(body.gates.pushedImageSmoke, 'pending')
t.is(body.surfaces.metadataCommit, 'pending')
t.is(body.surfaces.fleetRollout, 'skipped')
})