OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 commits / e451e80

0.1.254: each CI publish collects that repo's older releases (keep 2)
gc-releases existed but nothing ran it: tabertron's release dir held 453 stale versions across 16 repos (11 GB; hyper-tps alone 138) on 2026-10-11. buildAndPublish now ends with ReleaseManager.gc for the built repo. keepN counts UNPINNED versions and the new build has just pinned its channel, so it passes keepReleases - 1: the new build + the previous one remain, plus any other channel's pin. PGV3_KEEP_RELEASES (default 2; 0 disables). Non-fatal. test/ci-keep-releases.test.js (red without it: all 4 builds remained). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e451e8063a2d0a921aa05edd6c4632a3bbccd653
by plankatron <fleeky@gmail.com> · 2026-10-11T21:47:10.000Z

Changed files (3) +98 -1

lib/ci-orchestrator.js +21 -0
……@@ -26,6 +26,10 @@ import { spawn } from '#node:child_process'
2626import { loadCIConfig, pickChannel, deriveVersion, runSteps, collectArtifacts } from './ci-runner.js'
2727import { publishVersionToDrive, deriveVdrivePrimaryKey } from './vdrive-publish.js'
2828
29// Releases kept per repo after each CI publish (step 9). PGV3_KEEP_RELEASES=0 disables.
30const keepEnv = parseInt(process.env.PGV3_KEEP_RELEASES, 10)
31const DEFAULT_KEEP_RELEASES = Number.isFinite(keepEnv) && keepEnv >= 0 ? keepEnv : 2
32
2933export async function buildAndPublish ({
3034 sync,
3135 releaseManager,
……@@ -36,6 +40,7 @@ export async function buildAndPublish ({
3640 channel: forcedChannel,
3741 vdriveSeeder,
3842 vdriveRootKey = null,
43 keepReleases = DEFAULT_KEEP_RELEASES,
3944 onLog
4045} = {}) {
4146 if (!sync) throw new Error('buildAndPublish requires sync service')
……@@ -186,6 +191,22 @@ export async function buildAndPublish ({
186191 }
187192 }
188193
194 // 9. Collect this repo's older releases: newest `keepReleases` stay, and
195 // channel-pinned versions always stay (ReleaseManager.gc). gc-releases
196 // existed but nothing ran it — tabertron's release dir held 453 stale
197 // versions (11 GB) on 2026-10-11. Non-fatal; 0 disables.
198 // gc's keepN counts UNPINNED versions, and this build just pinned its
199 // channel — so with a channel, keepN = keepReleases - 1 leaves exactly
200 // keepReleases: the new build + the previous one(s).
201 if (keepReleases > 0) {
202 try {
203 const gc = await releaseManager.gc({ repo, keepN: channel ? keepReleases - 1 : keepReleases })
204 if (gc.deleted.length) log(onLog, `[ci] collected ${gc.deleted.length} older release(s) of ${repo} (keep ${keepReleases})`)
205 } catch (err) {
206 log(onLog, `[ci] release gc failed (release unaffected): ${err.message}`)
207 }
208 }
209
189210 return {
190211 ok: true,
191212 version,
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pear-git-v3",
3 "version": "0.1.253",
3 "version": "0.1.254",
44 "description": "P2P git forge \u2014 multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.",
55 "type": "module",
66 "main": "index.js",
test/ci-keep-releases.test.js +76 -0
……@@ -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
8import test from 'brittle'
9import { mkdtempSync, mkdirSync, writeFileSync } from '#node:fs'
10import { join } from '#node:path'
11import { tmpdir } from '#node:os'
12import { execSync } from '../lib/_compat-exec.js'
13
14import { ReleaseManager } from '../lib/release-manager.js'
15import { buildAndPublish } from '../lib/ci-orchestrator.js'
16
17function 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'), `
28steps:
29 - name: build
30 run: git_sha=$GIT_COMMIT_SHORT; echo "$git_sha" > built.txt
31artifacts:
32 - built.txt
33channels:
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
52test('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
65test('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})