Files
hermes-agent/tests-js/bundle-inputs.test.mjs
ethernet b0ab0162b0 feat(release): gate stable promotion through the full release pipeline
Run the entire CI workflow before Docker build and tests. Require Nix,
native payload smoke tests, install/update E2E and signed-package upgrade
acceptance before publishing. Keep Desktop Playwright E2E deferred.

Archive tested Docker images and signed bundle candidates with provenance
and hashes. Publishers consume those exact artifacts without rebuilding.
Advance stable channels only after all required publications succeed.
Keep canaries on their separate path and reject direct stable-builder
publication that bypasses the gate.

Move shared release transport, manifests and gates to Python. Keep native
Electron adapters in JS and share feed/MIME facts as JSON. Replace the
R2/feed JS implementation and move its protocol tests to Python.

Verified targeted Python and JS tests, real loopback transport and CLI
execution, temporary Git admission, workflow graph lint, and typechecks.
No live stable release was run. Native signing, package upgrades and real
registry/Store promotion still need their release-run receipts. Separate
services cannot promote atomically. A promotion failure keeps the run red.
2026-09-07 14:40:10 -04:00

89 lines
5.3 KiB
JavaScript

import { createHash } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { createServer } from 'node:http'
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, expect, test } from 'vitest'
import { stageBundleInputs, validateBundleInputs } from '../tests/install/e2e-assets/bundle-inputs.mjs'
import { bundledMatrix, renderMarkdownResults } from '../scripts/sandbox/generate-e2e-matrix.mjs'
const cleanup = []
afterEach(async () => { while (cleanup.length) await cleanup.pop()() })
function fixture(platform = 'windows') {
const extension = platform === 'windows' ? 'msixbundle' : 'zip'
return { schema: 1, platform, arch: 'x64', old: {
tag: 'v1.2.0', version: platform === 'windows' ? '1.2.0.0' : '1.2.0',
commit: 'a'.repeat(40), identity: 'test.bundle', teamId: 'ABCDEFGHIJ', publisher: 'CN=Test', applicationId: 'Test',
artifact: { url: `https://example.com/old.${extension}`, sha256: 'a'.repeat(64) }
}, new: {
tag: 'v1.3.0', version: platform === 'windows' ? '1.3.0.0' : '1.3.0',
commit: 'b'.repeat(40), identity: 'test.bundle', teamId: 'ABCDEFGHIJ', publisher: 'CN=Test', applicationId: 'Test',
artifact: { url: `https://example.com/new.${extension}`, sha256: 'b'.repeat(64) }
} }
}
test('package transitions are ordered, identity-preserving, and report through the existing family', () => {
for (const platform of ['windows', 'macos']) {
const manifest = fixture(platform)
expect(validateBundleInputs(manifest, platform, 'x64')).toBe(manifest)
for (const change of [
value => { value.new.version = value.old.version },
value => { value.new.identity = 'other' },
value => { value.new.commit = 'not-a-commit' },
value => { value.new.artifact.sha256 = 'not-a-digest' },
value => { value.new.artifact.url = 'http://example.com/package.zip' },
value => { value.arch = 'arm64' }
]) {
const bad = structuredClone(manifest)
change(bad)
expect(() => validateBundleInputs(bad, platform, 'x64')).toThrow()
}
const { include } = bundledMatrix(platform, manifest.old.tag)
expect(include).toHaveLength(1)
expect(include[0].update_method).toBe('open-app-update')
expect(renderMarkdownResults([{ name: `${include[0].name} / e2e`, conclusion: 'failure' }])).toContain('1 failed')
}
})
test('staging streams actual bytes, verifies hashes and removes rejected partial downloads', async () => {
// These bytes test download integrity only, not native package acceptance.
const bytes = { old: Buffer.from('old transport payload'), new: Buffer.from('new transport payload') }
const manifest = fixture()
const server = createServer((req, res) => {
res.end(req.url === '/manifest.json' ? JSON.stringify(manifest) : bytes[req.url.slice(1).split('.')[0]])
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
cleanup.push(() => new Promise(resolve => { server.closeAllConnections(); server.close(resolve) }))
const directory = await mkdtemp(path.join(os.tmpdir(), 'bundle-input-test-'))
cleanup.push(() => rm(directory, { recursive: true, force: true }))
const base = `http://127.0.0.1:${server.address().port}`
for (const slot of ['old', 'new']) {
manifest[slot].artifact.url = `${base}/${slot}.msixbundle`
manifest[slot].artifact.sha256 = createHash('sha256').update(bytes[slot]).digest('hex')
}
const out = path.join(directory, 'good')
const manifestSha256 = createHash('sha256').update(JSON.stringify(manifest)).digest('hex')
await expect(stageBundleInputs({ manifestUrl: `${base}/manifest.json`, platform: 'windows', arch: 'x64', out, manifestSha256: '0'.repeat(64) })).rejects.toThrow('manifest SHA-256')
const filename = await stageBundleInputs({ manifestUrl: `${base}/manifest.json`, platform: 'windows', arch: 'x64', out, manifestSha256 })
const result = JSON.parse(await readFile(filename, 'utf8'))
for (const slot of ['old', 'new']) expect(await readFile(result[slot].artifact.path)).toEqual(bytes[slot])
manifest.old.artifact.sha256 = 'c'.repeat(64)
const bad = path.join(directory, 'bad')
await expect(stageBundleInputs({ manifestUrl: `${base}/manifest.json`, platform: 'windows', arch: 'x64', out: bad })).rejects.toThrow('SHA-256')
expect(await readdir(bad)).toEqual([])
})
test('explicit bundled routes refuse absent package inputs instead of reporting a skipped pass', async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), 'bundle-plan-test-'))
cleanup.push(() => rm(directory, { recursive: true, force: true }))
const script = fileURLToPath(new URL('../tests/install/e2e-assets/bundle-plan.mjs', import.meta.url))
const env = { ...process.env, BUNDLE_WINDOWS_MANIFEST: '', BUNDLE_MACOS_MANIFEST: '',
GITHUB_OUTPUT: path.join(directory, 'output'), GITHUB_STEP_SUMMARY: path.join(directory, 'summary') }
expect(() => execFileSync(process.execPath, [script], { env: { ...env, BUNDLE_ROUTE: 'windows-bundled' }, stdio: 'pipe' })).toThrow()
execFileSync(process.execPath, [script], { env: { ...env, BUNDLE_ROUTE: 'all' }, stdio: 'pipe' })
expect(await readFile(env.GITHUB_STEP_SUMMARY, 'utf8')).toContain('no signed package pair supplied')
expect(await readFile(env.GITHUB_OUTPUT, 'utf8')).toContain('windows={"include":[]}')
})