OverviewFilesCommitsIssuesDocsReleasesPeers

pgh commits / e3b5c9a

deploy: pgh deploy gc — the release GC that existed but could not be called
ReleaseManager.gc() and the writer-gated gc-releases RPC have been in the daemon since the release manager was written. Nothing exposed them, so nothing ever collected anything: every repo keeps every version it has ever built, and release versions are plain files with no content-addressing or dedup. hyper-tps alone has ~30 versions listed. pgh deploy gc <repo> --dry-run list what would go pgh deploy gc <repo> --keep 1 keep only the newest, plus pinned The policy is the daemon's and is already the right one: a version any CHANNEL POINTS AT is never a candidate, so pinning to a channel is how you say "keep this one" — and everything else is a build artifact. One bug, found by testing the argument forms rather than the happy path: the positional-repo filter excluded index keepIdx + 1 to skip --keep's value, and with --keep ABSENT keepIdx is -1, so it excluded index 0 — the repo. `gc hyper-tps --dry-run` therefore parsed as repo undefined, and an omitted repo means EVERY repo. A deliberately scoped collection would have silently become a fleet-wide one. Guarded on keepIdx === -1, and all six argument forms are checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e3b5c9ae56ec797b9a8b18e462ee5a5355295a6b
by plankatron <fleeky@gmail.com> · 2026-08-25T21:30:38.000Z

Changed files (2) +40 -4

lib/commands/deploy.js +38 -2
……@@ -1,4 +1,4 @@
1/** pgh deploy <repo> [<ref>] | status | log <repo> | trigger <repo> [<ref>] */
1/** pgh deploy <repo> [<ref>] | status | log <repo> | trigger <repo> [<ref>] | gc [<repo>] */
22
33export const usage = `pgh deploy — CI/CD config + release log
44
……@@ -7,7 +7,19 @@ export const usage = `pgh deploy — CI/CD config + release log
77 pgh deploy trigger <repo> [<ref>] Build a release
88 pgh deploy <repo> [<ref>] Shorthand for trigger
99
10Default ref: refs/heads/master`
10 pgh deploy gc [<repo>] [--keep N] DELETE stale release versions
11 pgh deploy gc [<repo>] --dry-run List what gc WOULD delete
12
13Default ref: refs/heads/master
14Default --keep: 3 newest unpinned versions per repo.
15
16gc never touches a version a CHANNEL POINTS AT — pinning to a channel is how you
17say "keep this one". Everything else is a build artifact: release versions are
18plain files with no dedup, so an app publishing a large bundle accumulates it
19once per version, forever, unless something collects it.
20
21 pgh deploy gc hyper-tps --dry-run # always look first
22 pgh deploy gc hyper-tps --keep 1 # keep only the newest + pinned`
1123
1224const DEFAULT_REF = 'refs/heads/master'
1325
……@@ -30,6 +42,30 @@ export async function run (client, args) {
3042 return client.call('build-release', { repo, ref })
3143 }
3244
45 if (sub === 'gc') {
46 // The daemon has had gc-releases since the release manager was written; it
47 // had no way to be invoked, so nothing ever collected anything.
48 const rest = args.slice(1)
49 const dryRun = rest.includes('--dry-run')
50 const keepIdx = rest.indexOf('--keep')
51 let keepN
52 if (keepIdx !== -1) {
53 keepN = Number(rest[keepIdx + 1])
54 if (!Number.isInteger(keepN) || keepN < 0) {
55 throw new Error('--keep takes a non-negative integer')
56 }
57 }
58 // A bare positional, not a flag, and not the value consumed by --keep.
59 //
60 // The keepIdx === -1 guard is load-bearing: without it, ABSENT --keep gives
61 // keepIdx + 1 === 0, which excludes the first positional — so
62 // `gc hyper-tps --dry-run` dropped the repo, and an omitted repo means EVERY
63 // repo. A scoped collection would have silently become a fleet-wide one.
64 const repo = rest.find((a, i) =>
65 !a.startsWith('--') && (keepIdx === -1 || i !== keepIdx + 1))
66 return client.call('gc-releases', { repo, keepN, dryRun })
67 }
68
3369 // Default: treat first arg as repo name → trigger build
3470 const ref = args[1] || DEFAULT_REF
3571 return client.call('build-release', { repo: sub, ref })
package.json +2 -2
……@@ -1,7 +1,7 @@
11{
22 "name": "pgh",
3 "version": "0.1.147",
4 "description": "pgh \u2014 P2P content-addressed package manager and app launcher",
3 "version": "0.1.148",
4 "description": "pgh — P2P content-addressed package manager and app launcher",
55 "type": "module",
66 "main": "index.js",
77 "bin": {