| … | … | @@ -0,0 +1,76 @@ |
| 1 | // A CI build that publishes a release collects that repo's older ones: newest |
| 2 | // N (default 2) stay, channel-pinned versions stay. |
| 3 | // |
| 4 | // Why: `gc-releases` existed but nothing ran it. On tabertron 2026-10-11 the |
| 5 | // release dir held 453 stale versions across 16 repos (11 GB -> 2 GB once |
| 6 | // collected by hand); hyper-tps alone had 138. |
| 7 | |
| 8 | import test from 'brittle' |
| 9 | import { mkdtempSync, mkdirSync, writeFileSync } from '#node:fs' |
| 10 | import { join } from '#node:path' |
| 11 | import { tmpdir } from '#node:os' |
| 12 | import { execSync } from '../lib/_compat-exec.js' |
| 13 | |
| 14 | import { ReleaseManager } from '../lib/release-manager.js' |
| 15 | import { buildAndPublish } from '../lib/ci-orchestrator.js' |
| 16 | |
| 17 | function setup () { |
| 18 | const base = mkdtempSync(join(tmpdir(), 'pgv3-ci-keep-')) |
| 19 | const bare = join(base, 'app.git') |
| 20 | const work = join(base, 'work') |
| 21 | execSync(`git init -q --bare --initial-branch=main ${bare}`) |
| 22 | execSync(`git init -q --initial-branch=main ${work}`) |
| 23 | const g = (args) => execSync(`git -C ${work} ${args}`, { stdio: 'ignore' }) |
| 24 | g('config user.email ci@test') |
| 25 | g('config user.name CI') |
| 26 | mkdirSync(join(work, '.pear-git'), { recursive: true }) |
| 27 | writeFileSync(join(work, '.pear-git', 'ci.yaml'), ` |
| 28 | steps: |
| 29 | - name: build |
| 30 | run: git_sha=$GIT_COMMIT_SHORT; echo "$git_sha" > built.txt |
| 31 | artifacts: |
| 32 | - built.txt |
| 33 | channels: |
| 34 | experimental: |
| 35 | branches: ['main'] |
| 36 | `) |
| 37 | const commitAndPush = (n) => { |
| 38 | writeFileSync(join(work, 'n.txt'), String(n)) |
| 39 | g('add -A') |
| 40 | g(`commit -q -m c${n}`) |
| 41 | g(`push -q ${bare} main`) |
| 42 | } |
| 43 | const rm = new ReleaseManager({ |
| 44 | releaseDir: join(base, 'releases'), |
| 45 | deploy: { async rescan () {} }, |
| 46 | eventLog: { writable: true, async append () {} } |
| 47 | }) |
| 48 | const sync = { info: () => ({ path: bare }) } |
| 49 | return { rm, sync, commitAndPush } |
| 50 | } |
| 51 | |
| 52 | test('each CI publish keeps only the newest 2 releases of that repo', async (t) => { |
| 53 | const { rm, sync, commitAndPush } = setup() |
| 54 | const built = [] |
| 55 | for (let i = 1; i <= 4; i++) { |
| 56 | commitAndPush(i) |
| 57 | const r = await buildAndPublish({ sync, releaseManager: rm, repo: 'app', ref: 'main' }) |
| 58 | t.ok(r.ok, 'build ' + i + ' ok') |
| 59 | built.push(r.version) |
| 60 | } |
| 61 | const left = (await rm.list('app')).versions.map((v) => v.version).sort() |
| 62 | t.alike(left, built.slice(2).sort(), 'the two newest remain') |
| 63 | }) |
| 64 | |
| 65 | test('a channel-pinned old version survives', async (t) => { |
| 66 | const { rm, sync, commitAndPush } = setup() |
| 67 | commitAndPush(1) |
| 68 | const first = await buildAndPublish({ sync, releaseManager: rm, repo: 'app', ref: 'main', channel: 'production' }) |
| 69 | for (let i = 2; i <= 4; i++) { |
| 70 | commitAndPush(i) |
| 71 | await buildAndPublish({ sync, releaseManager: rm, repo: 'app', ref: 'main' }) |
| 72 | } |
| 73 | const left = (await rm.list('app')).versions.map((v) => v.version) |
| 74 | t.ok(left.includes(first.version), 'pinned production version kept') |
| 75 | t.is(left.length, 3, 'the production pin + the newest two builds') |
| 76 | }) |