/**
* vdrive in ci.yaml — config parsing and the publish hook.
*
* The hook writes a real drive over a release version dir and returns a
* pear:// link, without the drive storing any of the bytes.
*/
import test from 'brittle'
import fs from '#node:fs'
import fsp from '#node:fs/promises'
import path from '#node:path'
import os from '#node:os'
import { loadCIConfig } from '../lib/ci-runner.js'
import { publishVersionToDrive, vdriveStoreDir } from '../lib/vdrive-publish.js'
function tmpdir (name) {
return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-vdrive-' + name + '-'))
}
function writeConfig (dir, yaml) {
fs.mkdirSync(path.join(dir, '.pear-git'), { recursive: true })
fs.writeFileSync(path.join(dir, '.pear-git', 'ci.yaml'), yaml)
return dir
}
const STEPS = 'steps:\n - name: build\n run: "true"\n'
test('ci.yaml vdrive: absent → null', async (t) => {
const dir = await tmpdir('cfg1')
writeConfig(dir, STEPS)
t.is(loadCIConfig(dir).vdrive, null)
})
test('ci.yaml vdrive: true → enabled, all channels', async (t) => {
const dir = await tmpdir('cfg2')
writeConfig(dir, STEPS + 'vdrive: true\n')
const cfg = loadCIConfig(dir)
t.is(cfg.vdrive.enabled, true)
t.is(cfg.vdrive.channels, null)
})
test('ci.yaml vdrive: false → disabled', async (t) => {
const dir = await tmpdir('cfg3')
writeConfig(dir, STEPS + 'vdrive: false\n')
t.is(loadCIConfig(dir).vdrive.enabled, false)
})
test('ci.yaml vdrive: mapping with channels', async (t) => {
const dir = await tmpdir('cfg4')
writeConfig(dir, STEPS + 'vdrive:\n channels: [production]\n')
const cfg = loadCIConfig(dir)
t.is(cfg.vdrive.enabled, true)
t.alike(cfg.vdrive.channels, ['production'])
})
test('ci.yaml vdrive: garbage is rejected', async (t) => {
const dir = await tmpdir('cfg5')
writeConfig(dir, STEPS + 'vdrive: maybe\n')
t.exception(() => loadCIConfig(dir), /vdrive/)
const dir2 = await tmpdir('cfg6')
writeConfig(dir2, STEPS + 'vdrive:\n channels: notalist\n')
t.exception(() => loadCIConfig(dir2), /channels/)
})
test('hook is inert when not enabled', async (t) => {
const res = await publishVersionToDrive({ vdrive: null })
t.is(res.ok, false)
t.is(res.skipped, true)
})
test('hook skips a channel that is not selected', async (t) => {
const res = await publishVersionToDrive({
releaseDir: '/tmp',
repo: 'x',
version: '1',
channel: 'experimental',
vdrive: { enabled: true, channels: ['production'] }
})
t.is(res.skipped, true)
t.ok(res.reason.includes('channel not selected'))
})
test('hook publishes a version dir into a drive holding no blob bytes', async (t) => {
const releaseDir = await tmpdir('rel')
const repo = 'demo'
const version = '1.0.0'
const versionDir = path.join(releaseDir, repo, 'versions', version)
await fsp.mkdir(path.join(versionDir, 'by-arch', 'linux-x64', 'app'), { recursive: true })
const payload = Buffer.alloc(2 * 1024 * 1024, 7)
await fsp.writeFile(path.join(versionDir, 'by-arch', 'linux-x64', 'app', 'demo'), payload)
await fsp.writeFile(path.join(versionDir, 'package.json'), JSON.stringify({ name: repo }))
await fsp.writeFile(path.join(versionDir, 'empty.stamp'), '')
const { chunk } = await import('hyper-overlay/chunker')
const res = await publishVersionToDrive({
releaseDir,
repo,
version,
channel: 'production',
vdrive: { enabled: true, channels: null },
chunk
})
t.is(res.ok, true, 'published')
t.ok(res.link.startsWith('pear://'), 'has a pear link: ' + res.link)
t.is(res.files, 3, 'all three files, including the 0-byte one')
t.ok(res.pointerBlocks > 0, 'blocks were redirected, not stored')
// the drive must be far smaller than the bytes it publishes
const storeDir = vdriveStoreDir(releaseDir, repo)
const size = await dirSize(storeDir)
t.ok(size < payload.length / 4, `drive store ${size}B << payload ${payload.length}B`)
await fsp.rm(releaseDir, { recursive: true, force: true })
})
async function dirSize (dir) {
let total = 0
for (const e of await fsp.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name)
if (e.isDirectory()) total += await dirSize(full)
else total += (await fsp.stat(full)).size
}
return total
}
test('the drive is recorded in the release manifest', async (t) => {
// Without this the link is unrecoverable: it was never in the manifest, never
// in `pgh deploy log`, and once the seeder holds the store the only trace is a
// daemon log line.
const releaseDir = await tmpdir('manifest')
const repo = 'demo'
const version = '1.0.0'
const versionDir = path.join(releaseDir, repo, 'versions', version)
await fsp.mkdir(versionDir, { recursive: true })
await fsp.writeFile(path.join(versionDir, 'app.bin'), Buffer.alloc(256 * 1024, 4))
// a manifest as ReleaseManager would have written it
await fsp.writeFile(
path.join(versionDir, 'manifest.json'),
JSON.stringify({ version, builtAt: Date.now(), artifacts: [] }, null, 2)
)
const { chunk } = await import('hyper-overlay/chunker')
const res = await publishVersionToDrive({
releaseDir,
repo,
version,
channel: 'production',
vdrive: { enabled: true, channels: null },
chunk
})
t.is(res.ok, true)
t.is(res.annotated, true, 'manifest was annotated')
const manifest = JSON.parse(await fsp.readFile(path.join(versionDir, 'manifest.json'), 'utf8'))
t.ok(manifest.vdrive, 'manifest carries a vdrive block')
t.is(manifest.vdrive.link, res.link, 'and the link matches what was published')
t.ok(manifest.vdrive.key && manifest.vdrive.blocks > 0, 'with key and block count')
t.ok(manifest.version === version, 'existing manifest fields survive')
await fsp.rm(releaseDir, { recursive: true, force: true })
})