OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / commands / deploy.js

73 lines · javascript ref:
/** pgh deploy <repo> [<ref>] | status | log <repo> | trigger <repo> [<ref>] | gc [<repo>] */

export const usage = `pgh deploy — CI/CD config + release log

  pgh deploy status                        Overall release status
  pgh deploy log <repo>                    Releases for one repo
  pgh deploy trigger <repo> [<ref>]        Build a release
  pgh deploy <repo> [<ref>]                Shorthand for trigger

  pgh deploy gc [<repo>] [--keep N]        DELETE stale release versions
  pgh deploy gc [<repo>] --dry-run         List what gc WOULD delete

Default ref: refs/heads/master
Default --keep: 3 newest unpinned versions per repo.

gc never touches a version a CHANNEL POINTS AT — pinning to a channel is how you
say "keep this one". Everything else is a build artifact: release versions are
plain files with no dedup, so an app publishing a large bundle accumulates it
once per version, forever, unless something collects it.

  pgh deploy gc hyper-tps --dry-run        # always look first
  pgh deploy gc hyper-tps --keep 1         # keep only the newest + pinned`

const DEFAULT_REF = 'refs/heads/master'

export async function run (client, args) {
  const sub = args[0]
  if (!sub) throw new Error(usage)

  if (sub === 'status') return client.call('release-status', {})

  if (sub === 'log') {
    const repo = args[1]
    if (!repo) throw new Error('usage: pgh deploy log <repo>')
    return client.call('list-repo-releases', { repo })
  }

  if (sub === 'trigger') {
    const repo = args[1]
    const ref = args[2] || DEFAULT_REF
    if (!repo) throw new Error('usage: pgh deploy trigger <repo> [<ref>]')
    return client.call('build-release', { repo, ref })
  }

  if (sub === 'gc') {
    // The daemon has had gc-releases since the release manager was written; it
    // had no way to be invoked, so nothing ever collected anything.
    const rest = args.slice(1)
    const dryRun = rest.includes('--dry-run')
    const keepIdx = rest.indexOf('--keep')
    let keepN
    if (keepIdx !== -1) {
      keepN = Number(rest[keepIdx + 1])
      if (!Number.isInteger(keepN) || keepN < 0) {
        throw new Error('--keep takes a non-negative integer')
      }
    }
    // A bare positional, not a flag, and not the value consumed by --keep.
    //
    // The keepIdx === -1 guard is load-bearing: without it, ABSENT --keep gives
    // keepIdx + 1 === 0, which excludes the first positional — so
    // `gc hyper-tps --dry-run` dropped the repo, and an omitted repo means EVERY
    // repo. A scoped collection would have silently become a fleet-wide one.
    const repo = rest.find((a, i) =>
      !a.startsWith('--') && (keepIdx === -1 || i !== keepIdx + 1))
    return client.call('gc-releases', { repo, keepN, dryRun })
  }

  // Default: treat first arg as repo name → trigger build
  const ref = args[1] || DEFAULT_REF
  return client.call('build-release', { repo: sub, ref })
}