@bongos/core 1.19.319 → 1.19.320

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.bongos-core.json +121 -36
  2. package/.claude/skills/merge-mode/SKILL.md +3 -1
  3. package/.devcontainer/init-firewall.sh +41 -5
  4. package/clients/bongos-client/examples/hello-world.mjs +11 -2
  5. package/docs/copy-inventory.md +20 -20
  6. package/docs/copy-registry.json +22 -22
  7. package/modules/hall-ui/public/builders.js +0 -1
  8. package/package-lock.json +2 -2
  9. package/package.json +1 -1
  10. package/scripts/gds/audit-replay.sh +20 -3
  11. package/scripts/gds/box.js +79 -86
  12. package/scripts/gds/db-backup-nightly.sh +17 -4
  13. package/scripts/gds/db-backup-offsite.sh +23 -6
  14. package/scripts/gds/discipline-modes.json +1 -1
  15. package/scripts/gds/doc-cli-guard.js +34 -2
  16. package/scripts/gds/fitness.js +8 -4
  17. package/scripts/gds/gds-db-backup.service +15 -4
  18. package/scripts/gds/gds-db-backup.timer +12 -1
  19. package/scripts/gds/gds-db-offsite.service +16 -5
  20. package/scripts/gds/gds-db-offsite.timer +12 -1
  21. package/scripts/gds/instance-apex.js +28 -0
  22. package/scripts/gds/newcomer-floor.templates.json +2 -2
  23. package/scripts/gds/provision.js +7 -3
  24. package/scripts/gds/ship-deploy-target.js +388 -0
  25. package/scripts/gds/ship-finish.js +377 -0
  26. package/scripts/gds/ship-flow.js +814 -0
  27. package/scripts/gds/ship-git.js +100 -0
  28. package/scripts/gds/ship-grade-steps.js +376 -0
  29. package/scripts/gds/ship-grade.js +319 -0
  30. package/scripts/gds/ship-io.js +100 -0
  31. package/scripts/gds/ship-land.js +539 -0
  32. package/scripts/gds/ship-memory-sync.js +192 -0
  33. package/scripts/gds/ship-merge.js +297 -0
  34. package/scripts/gds/ship-preflight-steps.js +323 -0
  35. package/scripts/gds/ship-regen.js +286 -0
  36. package/scripts/gds/ship-resume.js +151 -0
  37. package/scripts/gds/ship-session-upload.js +216 -0
  38. package/scripts/gds/ship-state.js +73 -0
  39. package/scripts/gds/ship-surprise.js +84 -0
  40. package/scripts/gds/ship.js +105 -4195
  41. package/scripts/status-probe.mjs +30 -2
  42. package/src/module-api.js +3 -1
  43. package/tests/helpers.mjs +13 -0
  44. package/tests/provision.mjs +7 -1
  45. package/tests/regrade_eligibility.mjs +43 -0
  46. package/tests/ship_assurance.mjs +4 -2
  47. package/tests/ship_publish_error.mjs +43 -0
@@ -0,0 +1,388 @@
1
+ // scripts/gds/ship-deploy-target.js — deploy-mode + deploy-target resolution, the gh helpers and push-path selection.
2
+ //
3
+ // Carved out of scripts/gds/ship.js by task 1003205 (goal 1000079, the R03
4
+ // de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
5
+ // original ship.js lines 639-989 unchanged. ship.js remains the facade that
6
+ // re-exports every name and owns the CLI entry point, so the ship contract and
7
+ // every flag are untouched, and the other scripts that import from it are too.
8
+
9
+ 'use strict';
10
+
11
+ const { spawnSync } = require('node:child_process');
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+ const { loadSessionSync } = require('./cli-lib');
15
+ const { argvOk, argvOut } = require('./ship-git.js');
16
+ const { REPO_ROOT } = require('./ship-state.js');
17
+
18
+ // ---------- CI deploy mode (#679, ADR 0042) ----------
19
+ //
20
+ // Two ways the confirmed→shipped land can run, chosen by a SINGLE committed
21
+ // switch (config/deploy.json `mode`, env OTB_DEPLOY_MODE overrides):
22
+ //
23
+ // 'laptop' (default) — the operator's machine merges to main locally and, IFF a
24
+ // deploy target is configured (config/deploy.json "sshTarget" or
25
+ // <PREFIX>_DEPLOY_SSH_TARGET, resolveDeployTarget()), SSHes `~/deploy.sh` on it.
26
+ // The core carries NO target of its own, so by default this is a merge-to-main
27
+ // with no SSH deploy — a change reaches a live instance via package-core +
28
+ // `bongos upgrade` (pin-bump), NOT an SSH deploy (ADR 0108). When a target IS
29
+ // set it requires that host's SSH key, so only the operator can land.
30
+ //
31
+ // 'ci' — NO builder box holds the prod key. ship pushes the branch, opens a
32
+ // PR, and enables GitHub auto-merge; once required checks pass, GitHub
33
+ // merges to main and .github/workflows/deploy-prod.yml deploys from
34
+ // Actions. ship then CONFIRMS the deploy landed (PR merged + the
35
+ // deploy-prod workflow run succeeded for the merge commit + public
36
+ // healthz 200) so the synchronous "deploy-confirmed" UX is preserved
37
+ // without the key. This is what lets a Windows/Chromebook builder with no
38
+ // main checkout and no droplet key land their own work.
39
+ //
40
+ // parseDeployMode is pure (env string + raw config file contents → mode) and
41
+ // exported so the test can assert the precedence + the safe default without
42
+ // touching fs/env. The default is 'laptop' precisely so this code is INERT
43
+ // until the deliberate, atomic cutover (config flip + GitHub arming together).
44
+ function parseDeployMode(envVal, configRaw) {
45
+ const fromEnv = String(envVal || '').trim().toLowerCase();
46
+ if (fromEnv === 'ci' || fromEnv === 'laptop') return fromEnv;
47
+ if (configRaw) {
48
+ try {
49
+ const m = String(JSON.parse(configRaw).mode || '').trim().toLowerCase();
50
+ if (m === 'ci' || m === 'laptop') return m;
51
+ } catch (_) {
52
+ /* malformed config → fall through to the safe default */
53
+ }
54
+ }
55
+ return 'laptop';
56
+ }
57
+
58
+ // effectiveDeployMode — credential- and protection-aware adjustment of the parsed
59
+ // mode (pure; exported for the regression test). The 'laptop' land path pushes
60
+ // main + SSHes the droplet FROM THIS MACHINE, so it requires a local GitHub push
61
+ // credential. A machine without one — a dev box, or a fresh/stale local clone that
62
+ // can't even `git pull` (token-less origin, ADR 0053) — physically cannot run it;
63
+ // it must ship via the 'ci' server-mediated publish path instead
64
+ // (pushVia()==='server', ADR 0055), which lets the GDS server push with its own
65
+ // credential. So when the parsed mode is 'laptop' but the machine can't push
66
+ // locally AND the mode was NOT pinned by an explicit OTB_DEPLOY_MODE, fall back to
67
+ // 'ci' rather than strand every ship at merge. This is the scootermack 2026-06-22
68
+ // case: a checkout left at the pre-cutover 'laptop' default that can't pull
69
+ // forward to 'ci', so it kept attempting a push it had no credential for. An
70
+ // explicit env override always wins (tests/staging may force 'laptop'). Mirrors
71
+ // pushVia()'s existing credential-awareness — the asymmetry (pushVia smart,
72
+ // resolveDeployMode blind) WAS the bug. (task 1449)
73
+ //
74
+ // Second input, same shape (task 1002629): a machine WITH push credentials but a
75
+ // branch-PROTECTED main also cannot run the laptop land — GitHub refuses the
76
+ // direct `git push origin main` no matter whose credential it is. When task
77
+ // 1002619 armed protection (required `unit` check), every ship resolved 'laptop'
78
+ // and pushed into the wall, stranding graded-PASS tasks at confirmed (tasks
79
+ // 1002608/1002609; learning 1000177). mainProtected must be STRICTLY true to
80
+ // trigger — null/undefined means the probe couldn't answer (no gh, offline, no
81
+ // permission) and keeps today's behavior rather than rerouting on a guess.
82
+ function effectiveDeployMode(mode, { canPushLocally, envForced, mainProtected } = {}) {
83
+ if (mode === 'laptop' && !envForced && !canPushLocally) return 'ci';
84
+ if (mode === 'laptop' && !envForced && mainProtected === true) return 'ci';
85
+ return mode;
86
+ }
87
+
88
+ // mainIsProtected — true/false when GitHub answers, null when it can't. The
89
+ // `gh api` resolves {owner}/{repo} from the checkout's origin; any failure
90
+ // (gh missing, offline, non-200) is null = unknown, which effectiveDeployMode
91
+ // treats as "no change". Same spawn discipline as ghAvailableAndAuthed().
92
+ function mainIsProtected() {
93
+ try {
94
+ const r = spawnSync('gh', ['api', 'repos/{owner}/{repo}/branches/main', '--jq', '.protected'], {
95
+ encoding: 'utf8',
96
+ shell: process.platform === 'win32',
97
+ windowsHide: true,
98
+ timeout: 15000,
99
+ });
100
+ if (!r || r.error || r.status !== 0) return null;
101
+ const out = String(r.stdout || '').trim();
102
+ if (out === 'true') return true;
103
+ if (out === 'false') return false;
104
+ return null;
105
+ } catch (_) {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function resolveDeployMode() {
111
+ const configRaw = readDeployConfigRaw();
112
+ const mode = parseDeployMode(process.env.OTB_DEPLOY_MODE, configRaw);
113
+ const envForced = ['ci', 'laptop'].includes(
114
+ String(process.env.OTB_DEPLOY_MODE || '').trim().toLowerCase()
115
+ );
116
+ // Probe `gh` ONLY when it can change the answer (resolved 'laptop', no env
117
+ // override) — the happy 'ci' path and an env-pinned mode spawn no extra gh,
118
+ // keeping the documented spawn discipline (the line-1019 note). The protection
119
+ // probe runs only when the credential check kept us on 'laptop' (a
120
+ // credential-less machine already falls back without it).
121
+ if (mode === 'laptop' && !envForced) {
122
+ const canPushLocally = ghAvailableAndAuthed();
123
+ const mainProtected = canPushLocally ? mainIsProtected() : null;
124
+ const effective = effectiveDeployMode(mode, { canPushLocally, envForced, mainProtected });
125
+ if (effective === 'ci' && mainProtected === true) {
126
+ console.log(' deploy mode: main is branch-protected (direct pushes refused) — landing via the ci PR path');
127
+ }
128
+ return effective;
129
+ }
130
+ return mode;
131
+ }
132
+
133
+ // readDeployConfigRaw — raw contents of config/deploy.json (or null if absent /
134
+ // unreadable). One reader shared by resolveDeployMode (the mode switch) and
135
+ // resolveDeployTarget (the SSH target) so they never diverge on where the file is.
136
+ function readDeployConfigRaw() {
137
+ try {
138
+ return fs.readFileSync(path.join(REPO_ROOT, 'config', 'deploy.json'), 'utf8');
139
+ } catch (_) {
140
+ return null; // no config file → callers apply their safe default
141
+ }
142
+ }
143
+
144
+ // safeBranding — the instance brand pack, or null if it can't be read. Guarded the
145
+ // same way ship.js guards the sound player (line ~91): a deploy must never crash
146
+ // because the brand file is missing on a fresh / re-seeded checkout.
147
+ function safeBranding() {
148
+ try {
149
+ return require('../../src/branding').branding();
150
+ } catch (_) {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ // readSessionApiBase — the signed-in session's persisted api_base (the instance the
156
+ // CLI is actually pointed at), or null when not signed in / unreadable. This is the
157
+ // session's OWN value only — deliberately NOT readSessionToken().apiBase, which
158
+ // already folds in the env override + branding default; resolveInstanceBase orders
159
+ // that precedence itself so it stays pure over its injected inputs.
160
+ function readSessionApiBase() {
161
+ try {
162
+ const s = loadSessionSync();
163
+ return (s && typeof s.api_base === 'string' && s.api_base.trim()) ? s.api_base.trim() : null;
164
+ } catch (_) {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ // resolveInstanceBase — the ORIGIN of the instance this ship is landing on, used by
170
+ // the ci-land deployed-version probe (/version), the /healthz re-probe, the ci
171
+ // PR-body deploy note, and the manual-confirm curl guidance. Precedence mirrors the
172
+ // CLI's own base resolution: <PREFIX>_API_BASE env → the signed-in session's
173
+ // api_base → branding publicOrigin → null (unknowable). There is NO hardcoded
174
+ // founder host — 5 sites here baked in `https://amazonprimea.com`, so a ci-land /
175
+ // manual-confirm on a migrated instance pointed the builder at the WRONG box; the
176
+ // ci-land/guidance sibling of resolveDeployTarget()'s laptop-mode fix (task 1002232
177
+ // ⊃ task 1002202). Pure over its injected inputs (env / sessionApiBase / branding)
178
+ // so the precedence is unit-tested without fs/env/network; the defaults read the
179
+ // real session file + brand pack.
180
+ function resolveInstanceBase({
181
+ env = process.env,
182
+ sessionApiBase = readSessionApiBase(),
183
+ branding = safeBranding(),
184
+ } = {}) {
185
+ const ic = require('../../src/instance-config');
186
+ const trim = (s) => (typeof s === 'string' && s.trim() ? s.trim().replace(/\/+$/, '') : null);
187
+ const envBase = trim(ic.resolveEnv('API_BASE', { env }));
188
+ if (envBase) return envBase;
189
+ const sess = trim(sessionApiBase);
190
+ if (sess) return sess;
191
+ const origin = branding && branding.domains && branding.domains.publicOrigin;
192
+ return trim(origin);
193
+ }
194
+
195
+ // deployedVersionUrl — the instance's /version endpoint, or null when no instance
196
+ // origin is resolvable (then fetchDeployedVersion is a no-op). Derived from
197
+ // resolveInstanceBase — no hardcoded host (task 1002232).
198
+ function deployedVersionUrl() {
199
+ const base = resolveInstanceBase();
200
+ return base ? `${base}/version` : null;
201
+ }
202
+
203
+ // resolveDeployTarget — WHERE a laptop-mode ship SSHes `~/deploy.sh`, and which
204
+ // /healthz it re-probes when deploy.sh's own health check races the restart window.
205
+ // Pure (env + raw config + branding → target) and exported so the precedence and the
206
+ // safe "no target" default are unit-tested without touching fs/env/network.
207
+ //
208
+ // There is deliberately NO hardcoded default. The founder instance's old game-box
209
+ // SSH target was baked in here, so a laptop-mode ship from a migrated
210
+ // checkout SSHed the WRONG server (task 1002202). The core carries no deploy target
211
+ // of its own — under the standalone model a core change reaches a live instance via
212
+ // package-core + `bongos upgrade` (pin-bump), not an SSH deploy (ADR 0108). An
213
+ // instance whose OWN checkout is its live server opts back into laptop-SSH deploy by
214
+ // setting the target:
215
+ // config/deploy.json → { "sshTarget": "user@host", "healthzUrl": "https://…/healthz" }
216
+ // env → <PREFIX>_DEPLOY_SSH_TARGET, <PREFIX>_DEPLOY_HEALTHZ_URL
217
+ // healthzUrl, when unset, falls back to the instance's own publicOrigin + /healthz
218
+ // (never a hardcoded host); null when even that is unknown (re-probe then skipped).
219
+ function resolveDeployTarget({ env = process.env, configRaw = null, branding = null } = {}) {
220
+ const ic = require('../../src/instance-config');
221
+ let cfg = {};
222
+ if (configRaw) {
223
+ try { cfg = JSON.parse(configRaw) || {}; } catch (_) { cfg = {}; }
224
+ }
225
+ const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
226
+
227
+ const sshTarget = ic.resolveEnv('DEPLOY_SSH_TARGET', { env }) || str(cfg.sshTarget) || null;
228
+
229
+ let healthzUrl = ic.resolveEnv('DEPLOY_HEALTHZ_URL', { env }) || str(cfg.healthzUrl) || null;
230
+ const publicOrigin = branding && branding.domains && branding.domains.publicOrigin;
231
+ if (!healthzUrl && str(publicOrigin)) {
232
+ try { healthzUrl = new URL('/healthz', publicOrigin).toString(); }
233
+ catch (_) { /* malformed origin → leave healthzUrl null */ }
234
+ }
235
+ return { sshTarget, healthzUrl };
236
+ }
237
+
238
+ // gh runners — mirror gitArgvOk/gitOk but for the GitHub CLI. No shell, ever:
239
+ // branch names + DB-sourced value_summary reach `gh` as literal argv elements
240
+ // (same injection discipline as #681). ghArgvOk = boolean side-effect; ghJson =
241
+ // parse `gh ... --json` stdout to an object/array (null on any failure).
242
+ function ghArgvOk(args, opts = {}) {
243
+ return argvOk('gh', args, opts);
244
+ }
245
+
246
+ function ghJson(args, opts = {}) {
247
+ const out = argvOut('gh', args, opts);
248
+ if (out == null) return null;
249
+ try { return JSON.parse(out); } catch (_) { return null; }
250
+ }
251
+
252
+ // Pure argv builders for the PR land path — exported + tested so the untrusted
253
+ // value_summary is proven to ride as ONE literal argv element (never a shell
254
+ // string), exactly like buildMergeArgv.
255
+ function buildPrCreateArgv(branch, title, body) {
256
+ return ['pr', 'create', '--head', branch, '--base', 'main', '--title', title, '--body', body];
257
+ }
258
+
259
+ // --merge keeps the project's --no-ff merge-commit convention (architecture.md
260
+ // branch-cleanup note); --auto arms GitHub to merge the instant required checks
261
+ // pass + branch protection is satisfied; --delete-branch mirrors the laptop
262
+ // path's origin-branch cleanup.
263
+ function buildPrMergeArgv(branch) {
264
+ return ['pr', 'merge', '--auto', '--merge', '--delete-branch', branch];
265
+ }
266
+
267
+ // Pure: did the prod-deploy workflow finish successfully for the merge commit?
268
+ // `runs` is the parsed `gh run list --json headSha,status,conclusion` array.
269
+ function deployRunSucceeded(runs, mergeSha) {
270
+ if (!mergeSha) return false;
271
+ return (runs || []).some(
272
+ (r) => r && r.headSha === mergeSha && r.status === 'completed' && r.conclusion === 'success'
273
+ );
274
+ }
275
+
276
+ // Pure: does the prod /version endpoint (#858) report it's running the commit we
277
+ // just merged? 'match' = commit-pinned confirmation; 'mismatch' = prod is on a
278
+ // different commit (e.g. a later deploy already superseded ours — still fine, our
279
+ // run succeeded); 'unknown' = /version absent or unparseable (older prod / blip)
280
+ // → caller falls back to the deploy-run + healthz signal. Compares on the shorter
281
+ // length so a short vs full sha still matches. Exported + tested.
282
+ function deployedCommitStatus(versionBody, mergeSha) {
283
+ if (!versionBody || !mergeSha) return 'unknown';
284
+ const c = String(versionBody.commit || '');
285
+ if (!/^[0-9a-f]{7,40}$/i.test(c)) return 'unknown';
286
+ const n = Math.min(c.length, String(mergeSha).length);
287
+ return c.slice(0, n).toLowerCase() === String(mergeSha).slice(0, n).toLowerCase()
288
+ ? 'match'
289
+ : 'mismatch';
290
+ }
291
+
292
+ // Fetch + parse the prod /version JSON (#858). null on any failure — never throws.
293
+ // Default target is the resolved instance origin (no hardcoded host — task 1002232);
294
+ // null when none is resolvable, in which case the probe is skipped.
295
+ function fetchDeployedVersion(url = deployedVersionUrl()) {
296
+ if (!url) return null; // no resolvable instance origin → nothing to probe
297
+ // stdio override DISCARDS stderr (a curl progress/error line must not pollute
298
+ // the JSON parse) — the documented per-caller difference from ghJson.
299
+ const out = argvOut('curl', ['-fsS', '--max-time', '10', url], { stdio: ['ignore', 'pipe', 'ignore'] });
300
+ if (out == null) return null;
301
+ try { return JSON.parse(out); } catch (_) { return null; }
302
+ }
303
+
304
+ // ciLand — the 'ci' deploy-mode analogue of the laptop merge+SSH block. Pushes
305
+ // the branch, ensures a PR exists, enables auto-merge, then confirms the deploy
306
+ // actually landed. Returns true ONLY when prod is confirmed live on the merge
307
+ // commit — so the caller flips the task to shipped exactly as the laptop path
308
+ // does. Any earlier exit leaves the task at confirmed with a clear next step.
309
+ // pushVia — which publish path ship uses in 'ci' mode (ADR 0055 / task 1025):
310
+ // 'local' — this machine pushes the branch + opens the PR with git + gh
311
+ // (laptop: gh present + authenticated). Today's behaviour.
312
+ // 'server' — this machine has NO GitHub push credential (a dev box: no gh, a
313
+ // credential-less origin). ship uploads a thin branch bundle to the
314
+ // GDS, which pushes + opens the PR + auto-merges with the SERVER's
315
+ // credential. The box holds nothing (#919 model preserved).
316
+ // Override with OTB_PUSH_VIA_SERVER=1 (force server) / =0 (force local) for tests.
317
+ function ghAvailableAndAuthed() {
318
+ try {
319
+ const a = spawnSync('gh', ['auth', 'status'], {
320
+ stdio: 'ignore',
321
+ shell: process.platform === 'win32',
322
+ windowsHide: true,
323
+ timeout: 15000,
324
+ });
325
+ return !!a && !a.error && a.status === 0;
326
+ } catch (_) {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ function pushVia() {
332
+ const o = String(process.env.OTB_PUSH_VIA_SERVER || '').trim();
333
+ if (o === '1') return 'server';
334
+ if (o === '0') return 'local';
335
+ // No explicit override: a machine that can run an authenticated gh (laptop)
336
+ // takes the local path; everything else (a dev box) goes server-mediated.
337
+ return ghAvailableAndAuthed() ? 'local' : 'server';
338
+ }
339
+
340
+ // broadcastHandledByServer — task 1045: on the server-mediated publish path the
341
+ // SERVER posts the Discord ship-broadcast after it confirms the deploy (it holds
342
+ // the webhook secrets; the box does NOT — ADR 0053). So ship.js must NOT also post
343
+ // locally there, or the box would either double-post (impossible — no secret) or,
344
+ // as before, fail silently. True only when the publish actually goes server-side:
345
+ // ci deploy mode AND this machine has no GitHub push credential (pushVia 'server').
346
+ // The laptop path (pushVia 'local') keeps posting locally, exactly as today. Pure
347
+ // w.r.t. its inputs (env + committed deploy.json) + exported so the gate is tested.
348
+ function broadcastHandledByServer() {
349
+ return resolveDeployMode() === 'ci' && pushVia() === 'server';
350
+ }
351
+
352
+ // shouldBailOnFetchFailure — autoMerge's decision when `git fetch origin main`
353
+ // fails. A credential-less dev box (private origin, scrubbed clone token —
354
+ // ADR 0053) CANNOT fetch origin, and that is FINE for the server-mediated
355
+ // publish path (ADR 0055): ciLandServer bundles against the box's LOCAL
356
+ // origin/main and the SERVER tops up main with its own credential. So a fetch
357
+ // failure is fatal ONLY when we are NOT taking that path — otherwise the box can
358
+ // never reach ciLand and strands every ship at confirmed (the bug task 1027
359
+ // surfaced: ADR 0055 shipped the server path but left this fetch guard in front
360
+ // of it). Pure + exported for the regression test (no git, no network).
361
+ // NOTE: callers must still guard the pushVia() gh-probe behind `!fetchedMain` so
362
+ // the happy path (fetch ok) spawns no extra gh and laptop behavior is unchanged.
363
+ function shouldBailOnFetchFailure(fetchedMain, deployMode, via) {
364
+ if (fetchedMain) return false;
365
+ const serverPublish = deployMode === 'ci' && via === 'server';
366
+ return !serverPublish;
367
+ }
368
+
369
+ module.exports = {
370
+ broadcastHandledByServer,
371
+ buildPrCreateArgv,
372
+ buildPrMergeArgv,
373
+ deployRunSucceeded,
374
+ deployedCommitStatus,
375
+ effectiveDeployMode,
376
+ fetchDeployedVersion,
377
+ ghArgvOk,
378
+ ghJson,
379
+ mainIsProtected,
380
+ parseDeployMode,
381
+ pushVia,
382
+ readDeployConfigRaw,
383
+ resolveDeployMode,
384
+ resolveDeployTarget,
385
+ resolveInstanceBase,
386
+ safeBranding,
387
+ shouldBailOnFetchFailure,
388
+ };