// A CI build that publishes a release collects that repo's older ones: newest
// N (default 2) stay, channel-pinned versions stay.
//
// Why: `gc-releases` existed but nothing ran it. On tabertron 2026-10-11 the
// release dir held 453 stale versions across 16 repos (11 GB -> 2 GB once
// collected by hand); hyper-tps alone had 138.
import test from 'brittle'
import { mkdtempSync, mkdirSync, writeFileSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { execSync } from '../lib/_compat-exec.js'
import { ReleaseManager } from '../lib/release-manager.js'
import { buildAndPublish } from '../lib/ci-orchestrator.js'
function setup () {
const base = mkdtempSync(join(tmpdir(), 'pgv3-ci-keep-'))
const bare = join(base, 'app.git')
const work = join(base, 'work')
execSync(`git init -q --bare --initial-branch=main ${bare}`)
execSync(`git init -q --initial-branch=main ${work}`)
const g = (args) => execSync(`git -C ${work} ${args}`, { stdio: 'ignore' })
g('config user.email ci@test')
g('config user.name CI')
mkdirSync(join(work, '.pear-git'), { recursive: true })
writeFileSync(join(work, '.pear-git', 'ci.yaml'), `
steps:
- name: build
run: git_sha=$GIT_COMMIT_SHORT; echo "$git_sha" > built.txt
artifacts:
- built.txt
channels:
experimental:
branches: ['main']
`)
const commitAndPush = (n) => {
writeFileSync(join(work, 'n.txt'), String(n))
g('add -A')
g(`commit -q -m c${n}`)
g(`push -q ${bare} main`)
}
const rm = new ReleaseManager({
releaseDir: join(base, 'releases'),
deploy: { async rescan () {} },
eventLog: { writable: true, async append () {} }
})
const sync = { info: () => ({ path: bare }) }
return { rm, sync, commitAndPush }
}
test('each CI publish keeps only the newest 2 releases of that repo', async (t) => {
const { rm, sync, commitAndPush } = setup()
const built = []
for (let i = 1; i <= 4; i++) {
commitAndPush(i)
const r = await buildAndPublish({ sync, releaseManager: rm, repo: 'app', ref: 'main' })
t.ok(r.ok, 'build ' + i + ' ok')
built.push(r.version)
}
const left = (await rm.list('app')).versions.map((v) => v.version).sort()
t.alike(left, built.slice(2).sort(), 'the two newest remain')
})
test('a channel-pinned old version survives', async (t) => {
const { rm, sync, commitAndPush } = setup()
commitAndPush(1)
const first = await buildAndPublish({ sync, releaseManager: rm, repo: 'app', ref: 'main', channel: 'production' })
for (let i = 2; i <= 4; i++) {
commitAndPush(i)
await buildAndPublish({ sync, releaseManager: rm, repo: 'app', ref: 'main' })
}
const left = (await rm.list('app')).versions.map((v) => v.version)
t.ok(left.includes(first.version), 'pinned production version kept')
t.is(left.length, 3, 'the production pin + the newest two builds')
})