@bongos/core 1.19.332 → 1.19.334
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.
- package/.bongos-core.json +173 -43
- package/docs/adr/0187-oauth-handshake-cookie-lifetime.md +84 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +2 -2
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +202 -202
- package/docs/copy-registry.json +384 -384
- package/modules/grading/grader-config.js +98 -0
- package/modules/grading/grader-model.js +150 -0
- package/modules/grading/grader-prompt.js +139 -0
- package/modules/grading/grader-score.js +706 -0
- package/modules/grading/grader-subagent.js +328 -0
- package/modules/grading/grader.js +5 -1287
- package/modules/hall-ui/public/builders.js +19 -1182
- package/modules/hall-ui/public/goals-page.js +11 -997
- package/modules/hall-ui/public/goals-render.js +1032 -0
- package/modules/hall-ui/public/goals.html +4 -0
- package/modules/hall-ui/public/hall-render.js +1211 -0
- package/modules/hall-ui/public/index.html +6 -0
- package/modules/lifecycle/routes/goal-route-authz.js +400 -0
- package/modules/lifecycle/routes/goals.js +1 -367
- package/modules/lifecycle/routes/task-route-helpers.js +219 -0
- package/modules/lifecycle/routes/task-write-routes.js +542 -0
- package/modules/lifecycle/routes/tasks.js +3 -696
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-infra.js +280 -0
- package/scripts/gds/box.js +1 -245
- package/scripts/gds/fitness-checks-identity.js +320 -0
- package/scripts/gds/fitness-checks-vocabulary.js +230 -0
- package/scripts/gds/fitness-checks-write-validation.js +144 -0
- package/scripts/gds/fitness-lib.js +82 -0
- package/scripts/gds/fitness.js +55 -668
- package/scripts/gds/provision-config.js +204 -0
- package/scripts/gds/provision-net.js +221 -0
- package/scripts/gds/provision-repo.js +849 -0
- package/scripts/gds/provision-units.js +262 -0
- package/scripts/gds/provision.js +4 -1395
- package/src/bongos/auth-admission.js +264 -0
- package/src/bongos/auth-config.js +151 -0
- package/src/bongos/auth-discord.js +198 -0
- package/src/bongos/auth-github.js +336 -0
- package/src/bongos/auth-idp.js +330 -0
- package/src/bongos/auth.js +5 -1100
- package/src/bongos/routes/auth.js +54 -10
- package/src/module-api.js +5 -1
- package/tests/auth_admission_gate.mjs +3 -1
- package/tests/auth_web_recovery.mjs +63 -0
- package/tests/est_advisory.mjs +3 -1
- package/tests/goal_advisory.mjs +3 -2
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
- package/tests/goal_subtitle.mjs +5 -2
- package/tests/goals_card_view.mjs +10 -3
- package/tests/grader_generated_provenance.mjs +3 -1
- package/tests/hall_palette.mjs +3 -1
- package/tests/helpers.mjs +58 -25
- package/tests/lib_sh_resolution.mjs +3 -1
- package/tests/membership_kind_lockstep.mjs +3 -1
- package/tests/migration_allocation.mjs +5 -2
- package/tests/patch_gate_ordering.mjs +3 -1
- package/tests/rank_tier_single_source.mjs +16 -3
- package/tests/reward_gate.mjs +3 -2
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
// scripts/gds/provision-repo.js — everything that makes an instance's own repo
|
|
2
|
+
// and database real: the db create/migrate/seed commands, the standalone repo
|
|
3
|
+
// scaffold, adopt-vs-greenfield detection, the private-repo deploy key, and the
|
|
4
|
+
// re-provision core-pin refresh.
|
|
5
|
+
//
|
|
6
|
+
// Carved out of scripts/gds/provision.js by task 1003206 (goal 1000079, the R03
|
|
7
|
+
// de-monolith plan) to bring the control-plane CLI under the 1,500-line ratchet.
|
|
8
|
+
// A STRUCTURAL move: bodies unchanged, provision.js requires every name back, so
|
|
9
|
+
// the intent runner and its unit templates behave exactly as before.
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { CONFIG, REPO_ROOT, coreVersionSafe, hasNpmToken, provisionerBotEmail, registryCoreRequested } = require('./provision-config.js');
|
|
17
|
+
|
|
18
|
+
function dbName(inst) { return inst.db_name || inst.slug; }
|
|
19
|
+
|
|
20
|
+
// Escalate a DB command to run as the postgres OS user (→ PG superuser via peer auth,
|
|
21
|
+
// the Ubuntu default) when privileged — the DB-auth fix (task 2066). The runner's
|
|
22
|
+
// `lars` is not a usable PG role, so bare peer-auth-as-lars fails; `sudo -u postgres`
|
|
23
|
+
// sidesteps it without granting lars a role. PURE given the flag.
|
|
24
|
+
function asPostgres(cmd, privileged) { return privileged ? `sudo -u postgres ${cmd}` : cmd; }
|
|
25
|
+
|
|
26
|
+
function dbCreateCmd(inst, { privileged = false } = {}) {
|
|
27
|
+
// Idempotent: the caller runs it allowFail — "database already exists" is success.
|
|
28
|
+
// Non-privileged = bare `createdb` (peer auth as the running user, same as the
|
|
29
|
+
// standup runbook / OTB); privileged = `sudo -u postgres createdb` (task 2066).
|
|
30
|
+
return asPostgres(`createdb ${dbName(inst)}`, privileged);
|
|
31
|
+
}
|
|
32
|
+
function migrateCmd(inst, { privileged = false } = {}) {
|
|
33
|
+
// GDS-only schema — skips migrations/game/*.sql (no game tables on a vanilla
|
|
34
|
+
// instance), per the standup runbook (ADR 0105 instance seeds off). Privileged
|
|
35
|
+
// runs migrate as postgres (task 2066): `sudo` resets env, so the PGDATABASE/
|
|
36
|
+
// MEDUSA_GDS_ONLY vars ride `env …` and the script runs via `bash …` (a bare
|
|
37
|
+
// `VAR=val ./script` would be parsed BY sudo as a command). Precondition: the
|
|
38
|
+
// control-plane checkout must be readable by the postgres user (ADR 0128).
|
|
39
|
+
return privileged
|
|
40
|
+
? `sudo -u postgres env PGDATABASE=${dbName(inst)} MEDUSA_GDS_ONLY=1 bash ./scripts/migrate.sh`
|
|
41
|
+
: `PGDATABASE=${dbName(inst)} MEDUSA_GDS_ONLY=1 ./scripts/migrate.sh`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Seed the instance's FIRST version so it can take tasks from minute one (task 2119,
|
|
45
|
+
// goal 35). A freshly-migrated instance DB comes up with an EMPTY versions table —
|
|
46
|
+
// migrate runs instance-seeds-off (ADR 0105) and `bongos init` ran --no-seed — yet
|
|
47
|
+
// every task requires a version (tasks.version_id NOT NULL) and POST /versions is
|
|
48
|
+
// Archon-only (no Archon exists until first sign-in). Without a seed here, a brand-new
|
|
49
|
+
// project has NOWHERE to file a task. This runs on the box's DB path (no rank gate),
|
|
50
|
+
// ONLY IF the versions table is empty: idempotent, safe to re-run, and it never
|
|
51
|
+
// disturbs a project that already has versions. Mirrors seedViaApi's shape — V1
|
|
52
|
+
// (status 'building') + one done-when criterion so /status has a finish line from day
|
|
53
|
+
// one. `track` is set EXPLICITLY: versions.track is NOT NULL with no default (mig 112
|
|
54
|
+
// dropped the CHECK), and 'product' matches the canonical V1 seed in 003_pms.sql.
|
|
55
|
+
//
|
|
56
|
+
// The id/name/done_when come from instanceInitSpec so the starter version has ONE
|
|
57
|
+
// source of truth (the same spec the greenfield `bongos init` seed uses). SQL string
|
|
58
|
+
// literals are DOLLAR-QUOTED ($v$…$v$) so the whole -c argument is single-quoted —
|
|
59
|
+
// the one form that is safe UNCHANGED across all three executors: inline (/bin/sh),
|
|
60
|
+
// over SSH (bash -s), AND embedded in a dedicated droplet's cloud-init (bash -lc "…"),
|
|
61
|
+
// with no nested-quote or $-expansion hazard.
|
|
62
|
+
function seedFirstVersionCmd(inst, { privileged = false } = {}) {
|
|
63
|
+
const v = instanceInitSpec(inst).version;
|
|
64
|
+
const q = (s) => `$v$${s}$v$`; // dollar-quoted literal (no value contains a single quote or $)
|
|
65
|
+
const sql =
|
|
66
|
+
`INSERT INTO versions (id, name, track, status, done_when) ` +
|
|
67
|
+
`SELECT ${q(v.id)}, ${q(v.name)}, $v$product$v$, $v$building$v$, ${q(v.done_when)} ` +
|
|
68
|
+
`WHERE NOT EXISTS (SELECT 1 FROM versions); ` +
|
|
69
|
+
`INSERT INTO done_when_criteria (version_id, criterion_id, criterion_md, sort_order) ` +
|
|
70
|
+
`SELECT ${q(v.id)}, $v$first-goal$v$, ${q(v.done_when)}, 10 ` +
|
|
71
|
+
`WHERE EXISTS (SELECT 1 FROM versions WHERE id = ${q(v.id)}) ` +
|
|
72
|
+
`AND NOT EXISTS (SELECT 1 FROM done_when_criteria WHERE version_id = ${q(v.id)} AND criterion_id = $v$first-goal$v$);`;
|
|
73
|
+
return asPostgres(`psql -v ON_ERROR_STOP=1 -d ${dbName(inst)} -c '${sql}'`, privileged);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── STANDALONE shape (hosting_shape='standalone', ADR 0108, task 1978) ────────
|
|
77
|
+
// A standalone instance runs from its OWN repo checkout (host content: config/,
|
|
78
|
+
// migrations/instance/, the core PIN in package.json) with @bongos/core installed
|
|
79
|
+
// as an npm DEPENDENCY — NOT the shared control-plane checkout the co-tenant shape uses.
|
|
80
|
+
// It is the ADR 0108 DESTINATION model; these generators make provision.js drive the
|
|
81
|
+
// scripts/deploy/deploy-instance.sh runbook API-driven instead of by hand.
|
|
82
|
+
const CORE_PKG_DIR = 'node_modules/@bongos/core';
|
|
83
|
+
// The unix account the instance's Node process + its backup timer run as. Mirrors
|
|
84
|
+
// serviceUnit's hardcoded `User=lars`/`Group=lars` (the droplet's app user); kept as
|
|
85
|
+
// one constant so the backup dir owner + the backup unit's User= can't drift from it.
|
|
86
|
+
const APP_USER = 'lars';
|
|
87
|
+
function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return path.posix.join(base, inst.slug); }
|
|
88
|
+
// Sync the instance's own repo (host content + the core pin). Mirrors deploy-instance.sh
|
|
89
|
+
// step 1 — assumes the repo is already cloned at standaloneRoot (initial clone is a
|
|
90
|
+
// precondition, like the runbook's `git pull`). Run with cwd = standaloneRoot.
|
|
91
|
+
function standalonePullCmd() { return `git pull --ff-only origin main`; }
|
|
92
|
+
// Install the pinned core + instance deps — replaces co-tenant's shared checkout
|
|
93
|
+
// (deploy-instance.sh step 2). Run with cwd = standaloneRoot.
|
|
94
|
+
function standaloneInstallCmd() { return `npm ci --omit=dev`; }
|
|
95
|
+
// Migrate core + instance from the INSTANCE root's copy of the three-root migrate.sh
|
|
96
|
+
// (the installed core's, falling back to an in-tree copy — deploy-instance.sh step 3).
|
|
97
|
+
// Run with cwd = standaloneRoot.
|
|
98
|
+
function standaloneMigrateCmd(inst, { privileged = false } = {}) {
|
|
99
|
+
// Privileged: run as postgres (task 2066), passing env via `env …` so it survives
|
|
100
|
+
// sudo's env reset (the instance root must be postgres-readable, ADR 0128).
|
|
101
|
+
return privileged
|
|
102
|
+
? `sudo -u postgres env PGDATABASE=${dbName(inst)} MEDUSA_GDS_ONLY=1 bash ${CORE_PKG_DIR}/scripts/migrate.sh`
|
|
103
|
+
: `PGDATABASE=${dbName(inst)} MEDUSA_GDS_ONLY=1 bash ${CORE_PKG_DIR}/scripts/migrate.sh`;
|
|
104
|
+
}
|
|
105
|
+
// Regenerate the instance's whole-file navigation docs (session-log index) from the pinned core,
|
|
106
|
+
// AFTER the pull+install+migrate — the standalone analogue of the monorepo's deploy-prod.sh regen
|
|
107
|
+
// block (ADR 0110 §2, task 1002314). Best-effort + LOUD (the script always exits 0 + prints
|
|
108
|
+
// ::ERROR:: on a generator failure) so a stale/absent nav doc never fails a deploy. Run with
|
|
109
|
+
// cwd = standaloneRoot so resolveInstanceRoot() writes into the instance repo. Without this a
|
|
110
|
+
// standalone instance's docs never refresh on deploy — the gap the owner hit ("docs didn't
|
|
111
|
+
// transfer from primea"): its bespoke pull-deploy regenerated nothing.
|
|
112
|
+
function standaloneRegenDocsCmd() { return `node ${CORE_PKG_DIR}/scripts/gds/regen-instance-docs.js`; }
|
|
113
|
+
|
|
114
|
+
// ── STANDALONE scaffold (ADR 0108 / 0125, task 2055) ──────────────────────────
|
|
115
|
+
// The MISSING first leg of the greenfield "one guided flow": provision.js used to
|
|
116
|
+
// THROW if the instance repo wasn't already cloned. These generate + run the scaffold
|
|
117
|
+
// that clones an EMPTY target_ref and turns it into an installable, bootable instance.
|
|
118
|
+
|
|
119
|
+
// Parse an 'owner/repo' (or a github SSH/HTTPS URL) target_ref → { owner, name }.
|
|
120
|
+
// Null when unparseable, so the scaffold can fail with actionable guidance.
|
|
121
|
+
function parseTargetRef(ref) {
|
|
122
|
+
const m = String(ref || '').trim()
|
|
123
|
+
.replace(/^git@[^:]+:/, '')
|
|
124
|
+
.replace(/^https?:\/\/[^/]+\//, '')
|
|
125
|
+
.match(/^([A-Za-z0-9][A-Za-z0-9_.-]*)\/([A-Za-z0-9][A-Za-z0-9_.-]*?)(?:\.git)?$/);
|
|
126
|
+
return m ? { owner: m[1], name: m[2] } : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The git remote for an instance's OWN repo. Reuses the SAME SSH deploy alias the
|
|
130
|
+
// control-plane checkout uses (git@<alias>:<owner>/<repo>.git) — NO secret in the
|
|
131
|
+
// command (SSH-key auth, nothing to leak into journald), consistent with how the
|
|
132
|
+
// platform repo is wired. Standing up a new instance repo therefore needs only that
|
|
133
|
+
// the control plane's deploy key has WRITE access to that repo (ADR 0125). Alias is
|
|
134
|
+
// overridable via PROVISION_GIT_SSH_ALIAS.
|
|
135
|
+
function instanceRepoRemote(repo, { sshAlias = process.env.PROVISION_GIT_SSH_ALIAS || 'github-deploy' } = {}) {
|
|
136
|
+
return `git@${sshAlias}:${repo.owner}/${repo.name}.git`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A minimal, VALID `bongos init` spec (init.validateSpec greenfield) from the
|
|
140
|
+
// provisioning row + the owner's github login. The scaffold produces a bootable,
|
|
141
|
+
// NEUTRAL instance; the owner brands + scopes it later via init's kickoff tasks.
|
|
142
|
+
function instanceInitSpec(inst, { ownerLogin } = {}) {
|
|
143
|
+
const repo = parseTargetRef(inst.target_ref);
|
|
144
|
+
return {
|
|
145
|
+
identity: { productName: inst.slug },
|
|
146
|
+
repo: repo || { owner: ownerLogin || 'owner', name: inst.slug },
|
|
147
|
+
firstAdmin: ownerLogin || (repo && repo.owner) || 'owner',
|
|
148
|
+
currency: { label: 'credits' },
|
|
149
|
+
domains: { publicOrigin: inst.domain ? `https://${inst.domain}` : `http://localhost:${inst.port || 3000}` },
|
|
150
|
+
modules: [],
|
|
151
|
+
version: { id: 'V1', name: 'V1', done_when: 'A task goes claim → grade → ship.' },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The .gitignore a scaffolded instance repo carries: keep node_modules + the local
|
|
156
|
+
// dist/ + secrets OUT of the commit, while the vendored core tarball (vendor/*.tgz)
|
|
157
|
+
// and package-lock.json stay IN — `npm ci` on deploy resolves the pinned core from
|
|
158
|
+
// the committed tarball (ADR 0108), so they MUST be tracked.
|
|
159
|
+
//
|
|
160
|
+
// docs/session-log-index.md is a WHOLE-FILE generated navigation doc (ADR 0110): every byte is
|
|
161
|
+
// machine-owned, regenerated at deploy by regen-instance-docs.js (task 1002314) into the served
|
|
162
|
+
// tree. Untracked so `git reset --hard origin/main` never wipes it and no branch can diff it —
|
|
163
|
+
// the same reasoning the core's own .gitignore applies to it. (docs/repo-map.md is listed for
|
|
164
|
+
// parity/forward-compat; it is core-scoped today so a standalone instance doesn't generate its
|
|
165
|
+
// own — see regenerateNavDocs() in upgrade.js — but ignoring it costs nothing and prevents a
|
|
166
|
+
// stray future write from entering the tree.)
|
|
167
|
+
const INSTANCE_GITIGNORE = [
|
|
168
|
+
'node_modules/', 'dist/', '.env', '.env.local', '*.log',
|
|
169
|
+
'# Whole-file generated navigation docs — regenerated at deploy (ADR 0110, task 1002314).',
|
|
170
|
+
'docs/session-log-index.md', 'docs/repo-map.md', '',
|
|
171
|
+
].join('\n');
|
|
172
|
+
|
|
173
|
+
// A PRE-BUILT @bongos/core tarball this control plane can vendor into a NEW instance
|
|
174
|
+
// WITHOUT rebuilding from source — the consumer-control-plane fix (task 1002275).
|
|
175
|
+
//
|
|
176
|
+
// The scaffold's default core-install path rebuilds the tarball with package-core.js,
|
|
177
|
+
// which enumerates GIT-TRACKED files (`git -C REPO_ROOT ls-tree`). That works from a core
|
|
178
|
+
// SOURCE checkout, but NOT when this runner is ITSELF a consumer instance whose core is an
|
|
179
|
+
// installed npm dependency: provision.js then runs out of node_modules/@bongos/core, so
|
|
180
|
+
// REPO_ROOT points INSIDE gitignored node_modules, git sees ZERO tracked files there, and
|
|
181
|
+
// package-core aborts ("the core set is EMPTY") — the scaffold never pushes and the freshly
|
|
182
|
+
// created repo is left EMPTY (seen live: trial/test on cloudbongos.com).
|
|
183
|
+
//
|
|
184
|
+
// But the exact pinned tarball this instance installed FROM is on disk, named by its own
|
|
185
|
+
// package.json ("@bongos/core": "file:vendor/bongos-core-<v>.tgz"). Reuse it: byte-identical,
|
|
186
|
+
// already proven, and the child pins the SAME core the hub runs. Returns an absolute .tgz
|
|
187
|
+
// path, or null on a source checkout (no `file:` core pin) — where package-core.js is the
|
|
188
|
+
// right, unchanged path. `instanceRoot` defaults to $BONGOS_INSTANCE_ROOT (what
|
|
189
|
+
// infra/core-node.sh resolves against) else process.cwd() (the runner's WorkingDirectory).
|
|
190
|
+
// fs injectable for tests. PURE-ish (fs reads only, no mutation → dry-run safe).
|
|
191
|
+
function installedCoreTarball({ instanceRoot, fsImpl = fs } = {}) {
|
|
192
|
+
const root = instanceRoot || process.env.BONGOS_INSTANCE_ROOT || process.cwd();
|
|
193
|
+
let pkg;
|
|
194
|
+
try { pkg = JSON.parse(fsImpl.readFileSync(path.join(root, 'package.json'), 'utf8')); }
|
|
195
|
+
catch { return null; } // no/unreadable package.json → treat as a source checkout
|
|
196
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
197
|
+
// Both scope aliases the loader accepts (infra/core-node.sh checks the same two).
|
|
198
|
+
for (const name of ['@bongos/core', '@cloudbongos/core']) {
|
|
199
|
+
const m = /^file:(.+\.tgz)$/.exec(String(deps[name] || '').trim());
|
|
200
|
+
if (m) {
|
|
201
|
+
const abs = path.resolve(root, m[1]);
|
|
202
|
+
if (fsImpl.existsSync(abs)) return abs; // a dangling pin (tarball pruned) → fall through to null
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The REGISTRY-pinned sibling of installedCoreTarball (task 1002706). After a control
|
|
209
|
+
// plane moves its own core pin off the vendored tarball onto the npm registry (the
|
|
210
|
+
// cloudbongos.com cutover, task 1002621), its package.json holds a semver pin — no
|
|
211
|
+
// `file:` — so installedCoreTarball returns null and the scaffold would fall into the
|
|
212
|
+
// package-core.js source rebuild, which ABORTS on a consumer instance (the empty-set
|
|
213
|
+
// abort of task 1002275, resurrected by the cutover). The installed package itself is
|
|
214
|
+
// still the exact core this hub runs: return its node_modules dir + version + the
|
|
215
|
+
// `npm pack` tarball name so the scaffold can pack it into a vendorable .tgz. The child
|
|
216
|
+
// stays file:-pinned — its owner needs no npm token (the registry package is PRIVATE
|
|
217
|
+
// until the ADR 0134 go-public step). Returns { dir, version, tarballName } or null
|
|
218
|
+
// (a file: pin, an uninstalled dep, or a source checkout). fs reads only → dry-run safe.
|
|
219
|
+
function installedCorePackDir({ instanceRoot, fsImpl = fs } = {}) {
|
|
220
|
+
const root = instanceRoot || process.env.BONGOS_INSTANCE_ROOT || process.cwd();
|
|
221
|
+
let pkg;
|
|
222
|
+
try { pkg = JSON.parse(fsImpl.readFileSync(path.join(root, 'package.json'), 'utf8')); }
|
|
223
|
+
catch { return null; } // no/unreadable package.json → a source checkout
|
|
224
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
225
|
+
for (const name of ['@bongos/core', '@cloudbongos/core']) {
|
|
226
|
+
const spec = String(deps[name] || '').trim();
|
|
227
|
+
if (!spec || spec.startsWith('file:')) continue; // file: pins belong to installedCoreTarball
|
|
228
|
+
const dir = path.join(root, 'node_modules', ...name.split('/'));
|
|
229
|
+
try {
|
|
230
|
+
const installed = JSON.parse(fsImpl.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
231
|
+
const version = installed && installed.version ? String(installed.version) : null;
|
|
232
|
+
if (!version) continue;
|
|
233
|
+
// npm pack names a scoped tarball <scope>-<name>-<version>.tgz (no @, / → -).
|
|
234
|
+
const base = String(installed.name || name).replace(/^@/, '').replace(/\//g, '-');
|
|
235
|
+
return { dir, version, tarballName: `${base}-${version}.tgz` };
|
|
236
|
+
} catch { /* pinned but not installed → fall through */ }
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Resolve a vendorable @bongos/core tarball — the three-rung ladder, freshest-correct
|
|
242
|
+
// first, shared by the fresh scaffold and the re-provision pin refresh (task 1003050):
|
|
243
|
+
// 1. a `file:`-pinned control plane reuses its own pinned tarball (task 1002275 —
|
|
244
|
+
// rebuilding via package-core.js aborts with an EMPTY core set when this runner
|
|
245
|
+
// IS a consumer instance, leaving new repos empty);
|
|
246
|
+
// 2. a REGISTRY-pinned consumer (the post-1002621 mothership shape) packs the core
|
|
247
|
+
// it has INSTALLED — the child pins the SAME, CURRENT core the hub runs, ending
|
|
248
|
+
// the frozen-old-tarball births (task 1002706, gap 13) without requiring the
|
|
249
|
+
// child's owner to hold a token for the still-private registry package;
|
|
250
|
+
// 3. a genuine core source checkout rebuilds fresh via package-core.js.
|
|
251
|
+
// Returns { tgz, packDest } — packDest (set only on rung 2) is a temp dir the CALLER
|
|
252
|
+
// must `rm -rf` once the tarball has been vendored.
|
|
253
|
+
function resolveVendorableCoreTarball({ instanceRoot, exec, log, coreV, tag = '[core]' }) {
|
|
254
|
+
let tgz = installedCoreTarball({ instanceRoot });
|
|
255
|
+
let packDest = null;
|
|
256
|
+
if (tgz) {
|
|
257
|
+
log(` ${tag} reusing the pinned core tarball this control plane runs (${path.basename(tgz)}) — no source rebuild (task 1002275)`);
|
|
258
|
+
} else {
|
|
259
|
+
const packSrc = installedCorePackDir({ instanceRoot });
|
|
260
|
+
if (packSrc) {
|
|
261
|
+
// Keyed by VERSION, not slug: no request-derived value enters this command, and
|
|
262
|
+
// two concurrent scaffolds of the same core pack identical bytes — collision-safe.
|
|
263
|
+
packDest = path.join(os.tmpdir(), `bongos-core-pack-${packSrc.version}`);
|
|
264
|
+
log(` ${tag} packing the registry-installed core this control plane runs (v${packSrc.version}) — no source rebuild (task 1002706)`);
|
|
265
|
+
exec(`mkdir -p ${packDest}`);
|
|
266
|
+
exec(`npm pack ${packSrc.dir} --pack-destination ${packDest}`);
|
|
267
|
+
tgz = path.join(packDest, packSrc.tarballName);
|
|
268
|
+
} else {
|
|
269
|
+
log(` ${tag} building a fresh vendored tarball from source (package-core.js)`);
|
|
270
|
+
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
271
|
+
tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { tgz, packDest };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The onboard mode a provisioning row carries (migration provisioning_004, task 1002562).
|
|
278
|
+
// 'adopt' means target_ref is a repo the owner ALREADY has, so the scaffold must LAYER
|
|
279
|
+
// onto its history. Anything else — including a row written before the column existed, or
|
|
280
|
+
// an unrecognized value — resolves to 'greenfield', deliberately the SAFE direction:
|
|
281
|
+
// greenfield pushes a fresh tree and (carrying no --force) is REJECTED by a repo with
|
|
282
|
+
// history, whereas wrongly choosing 'adopt' would start rewriting a repo nobody declared.
|
|
283
|
+
function onboardMode(inst) {
|
|
284
|
+
return String((inst && inst.onboard_mode) || '') === 'adopt' ? 'adopt' : 'greenfield';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Has the scaffold already completed in `root`? The marker is MODE-DEPENDENT, and reusing
|
|
288
|
+
// the greenfield one for adopt is a real bug: greenfield's marker is "package.json exists"
|
|
289
|
+
// (a fresh empty repo has none until we scaffold it), but an adopted repo is an existing
|
|
290
|
+
// project that almost certainly HAS a package.json already — so the greenfield check would
|
|
291
|
+
// report "already scaffolded" on the very first run and the instance would never become
|
|
292
|
+
// bootable. For adopt the honest marker is the thing the layering itself contributes: a
|
|
293
|
+
// @bongos/core dependency (both scope aliases the loader accepts, matching
|
|
294
|
+
// installedCoreTarball). fs injectable for tests; read-only, so dry-run safe.
|
|
295
|
+
function alreadyScaffolded(root, mode, { fsImpl = fs } = {}) {
|
|
296
|
+
const pkgPath = path.join(root, 'package.json');
|
|
297
|
+
if (!fsImpl.existsSync(pkgPath)) return false;
|
|
298
|
+
if (mode !== 'adopt') return true;
|
|
299
|
+
let pkg;
|
|
300
|
+
try { pkg = JSON.parse(fsImpl.readFileSync(pkgPath, 'utf8')); }
|
|
301
|
+
catch { return false; } // unparseable → let the adopt layer report it, don't skip
|
|
302
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
303
|
+
return !!(deps['@bongos/core'] || deps['@cloudbongos/core']);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Materialize + push the instance's own repo. TWO paths, chosen by onboard_mode:
|
|
307
|
+
//
|
|
308
|
+
// greenfield — the target_ref repo is EMPTY (github-repos.createUserRepo makes it with
|
|
309
|
+
// auto_init:false), so there is nothing to clone: `git init` locally,
|
|
310
|
+
// materialize, and push the first commit at main.
|
|
311
|
+
// adopt — the target_ref repo is the owner's OWN existing project. CLONE it (so its
|
|
312
|
+
// history and files come along), LAYER the instance config additively via
|
|
313
|
+
// `bongos init --adopt --accept` (which never overwrites a pre-existing
|
|
314
|
+
// file), and push the layer commit as an ordinary fast-forward on top.
|
|
315
|
+
//
|
|
316
|
+
// Both end at a repo carrying config/ + .claude/ + a file:-pinned @bongos/core + lockfile,
|
|
317
|
+
// which is what makes it `npm ci --omit=dev`-installable by the rest of provisioning.
|
|
318
|
+
//
|
|
319
|
+
// NEITHER PATH EVER USES --force. That is a safety property, not an oversight: since ADR
|
|
320
|
+
// 0155 the credential here can write to every repo the owner owns, so a force-push would
|
|
321
|
+
// be a route to destroying a user's work. A rejected push is the correct, recoverable
|
|
322
|
+
// outcome. Asserted by tests.
|
|
323
|
+
//
|
|
324
|
+
// Idempotent (see alreadyScaffolded) and dry-run safe (every mutation goes through the
|
|
325
|
+
// injected exec/writeFile). ADR 0108 / 0121 / 0125.
|
|
326
|
+
async function scaffoldStandaloneRepo(inst, deps) {
|
|
327
|
+
// The scaffold BUILDS + pushes the instance's repo (git clone, package-core,
|
|
328
|
+
// `bongos init`, git push) — control-plane work that needs the packaging tools +
|
|
329
|
+
// the GitHub deploy key, so it runs LOCALLY even when box-mutating steps go
|
|
330
|
+
// remote (task 2065). Falls back to exec/writeFile when a caller passes no
|
|
331
|
+
// control executor (then all are the same local executor — unchanged behavior).
|
|
332
|
+
const { db, log } = deps;
|
|
333
|
+
const exec = deps.controlExec || deps.exec;
|
|
334
|
+
const writeFile = deps.controlWriteFile || deps.writeFile;
|
|
335
|
+
const root = standaloneRoot(inst);
|
|
336
|
+
const repo = parseTargetRef(inst.target_ref);
|
|
337
|
+
const mode = onboardMode(inst);
|
|
338
|
+
if (!repo) {
|
|
339
|
+
throw new Error(`standalone instance ${inst.slug}: target_ref "${inst.target_ref || '(unset)'}" is not an owner/repo — set it (e.g. <owner>/${inst.slug}) so the scaffold knows which repo to clone + push.`);
|
|
340
|
+
}
|
|
341
|
+
if (alreadyScaffolded(root, mode)) {
|
|
342
|
+
log(` [scaffold] ${root} already scaffolded (mode=${mode}) — skipping`);
|
|
343
|
+
return { already: true };
|
|
344
|
+
}
|
|
345
|
+
const coreV = coreVersionSafe();
|
|
346
|
+
if (!coreV) {
|
|
347
|
+
throw new Error('cannot resolve the core version (src/module-api CORE_VERSION) — run the scaffold from the control-plane checkout.');
|
|
348
|
+
}
|
|
349
|
+
const remote = instanceRepoRemote(repo);
|
|
350
|
+
log(mode === 'adopt'
|
|
351
|
+
? ` [scaffold] ${repo.owner}/${repo.name} → ${root} (ADOPT: clone → layer additively → pin core ${coreV} → commit → fast-forward push)`
|
|
352
|
+
: ` [scaffold] ${repo.owner}/${repo.name} → ${root} (clone → materialize → pin core ${coreV} → commit → push)`);
|
|
353
|
+
|
|
354
|
+
// Owner login = the founding firstAdmin. Best-effort; the spec falls back to the repo owner.
|
|
355
|
+
let ownerLogin = null;
|
|
356
|
+
if (inst.owner_builder_id && db) {
|
|
357
|
+
try {
|
|
358
|
+
const r = await db.query('SELECT github_login FROM builders WHERE id = $1', [inst.owner_builder_id]);
|
|
359
|
+
ownerLogin = r && r.rows && r.rows[0] ? r.rows[0].github_login : null;
|
|
360
|
+
} catch { /* fall back to the repo owner */ }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Resolve the instance owner's OAuth token BEFORE any git network op.
|
|
364
|
+
//
|
|
365
|
+
// Greenfield only needed it at PUSH time, so the lookup used to live down beside the
|
|
366
|
+
// push. Adopt needs it EARLIER: its first act is to clone the owner's own repo, and a
|
|
367
|
+
// PRIVATE one is unreadable by the control plane's shared deploy key (a deploy key is
|
|
368
|
+
// per-repo). Since ADR 0155 this token carries the `repo` scope, so it can both read and
|
|
369
|
+
// write the owner's private repos — which is what makes adopt possible at all.
|
|
370
|
+
//
|
|
371
|
+
// A GitHub DEPLOY KEY is per-repo, so the box's shared key also can't WRITE to a new
|
|
372
|
+
// project repo — the second empty-repo cause (task 1002317) — hence preferring this
|
|
373
|
+
// token on both paths. It rides a chmod-600 git credential-store file, never the logged
|
|
374
|
+
// command or makeExec's env (makeExec surfaces both), and is removed in a `finally` so a
|
|
375
|
+
// failed git op can't leave it on disk. No token (org-owned repo, or secret-box off) →
|
|
376
|
+
// fall back to the SSH `origin` remote.
|
|
377
|
+
const ownerToken = await resolveOwnerGithubToken(inst, deps, { tag: '[scaffold]' });
|
|
378
|
+
// The credential-store file + the credential-LESS URL git resolves against it. The URL
|
|
379
|
+
// carries only the `x-access-token` username (no secret), so it is safe in a logged argv.
|
|
380
|
+
const credFile = ownerToken ? path.join(os.tmpdir(), `bongos-gitcred-${inst.slug}-${process.pid}`) : null;
|
|
381
|
+
const httpsUrl = `https://x-access-token@github.com/${repo.owner}/${repo.name}.git`;
|
|
382
|
+
const credOpt = credFile ? `-c credential.helper='store --file=${credFile}' ` : '';
|
|
383
|
+
if (credFile) writeFile(credFile, `https://x-access-token:${ownerToken}@github.com\n`, { mode: 0o600 });
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// 1. Materialize the working tree.
|
|
387
|
+
//
|
|
388
|
+
// ADOPT — CLONE the owner's existing repo, so its history + files come along and the
|
|
389
|
+
// layer commit lands as an ordinary descendant of its current tip. This is the whole
|
|
390
|
+
// point: `git init`ing a fresh tree here produces an UNRELATED history, which the
|
|
391
|
+
// remote rejects as a non-fast-forward (and, were it ever forced, would erase the
|
|
392
|
+
// owner's work). Cloning over the token's HTTPS URL also covers the private case the
|
|
393
|
+
// shared deploy key cannot read.
|
|
394
|
+
//
|
|
395
|
+
// GREENFIELD — a freshly-created instance repo is EMPTY (github-repos.createUserRepo,
|
|
396
|
+
// auto_init:false), so there is NOTHING to clone; and the shared PROVISION_GIT_SSH_ALIAS
|
|
397
|
+
// deploy key can't even READ a consumer's brand-new repo under their own account
|
|
398
|
+
// (`git clone` fails "Repository not found" — the clone-side twin of the task-1002317
|
|
399
|
+
// push bug). So `git init` locally and set `origin` to the SSH remote for the no-token
|
|
400
|
+
// fallback push. Unchanged behavior.
|
|
401
|
+
//
|
|
402
|
+
// Either way a re-run after a partial attempt reuses the existing dir.
|
|
403
|
+
const hasGit = fs.existsSync(path.join(root, '.git'));
|
|
404
|
+
if (mode === 'adopt') {
|
|
405
|
+
if (!hasGit) {
|
|
406
|
+
exec(`mkdir -p ${root}`);
|
|
407
|
+
exec(`git ${credOpt}clone ${credFile ? httpsUrl : remote} ${root}`);
|
|
408
|
+
// Normalize `origin` to the SSH remote, matching what the greenfield path leaves
|
|
409
|
+
// behind. Two reasons: the clone URL would otherwise persist as origin, and the
|
|
410
|
+
// ongoing deploy (standalonePullCmd → `git pull --ff-only origin main`, and the box
|
|
411
|
+
// timer's `git reset --hard origin/main`) reads origin — it must be a form that can
|
|
412
|
+
// authenticate durably, which the one-shot token URL cannot (its credential file is
|
|
413
|
+
// deleted below). Nothing durable ever holds the token either way; the pushes here
|
|
414
|
+
// always pass an explicit URL.
|
|
415
|
+
if (credFile) exec(`git -C ${root} remote set-url origin ${remote}`);
|
|
416
|
+
} else {
|
|
417
|
+
exec(`git -C ${root} ${credOpt}fetch ${credFile ? httpsUrl : 'origin'} main`);
|
|
418
|
+
}
|
|
419
|
+
// The rest of provisioning is main-only (standalonePullCmd is `git pull --ff-only
|
|
420
|
+
// origin main`, and the box's deploy timer does `git reset --hard origin/main`), so a
|
|
421
|
+
// repo whose default branch is not `main` would be layered and pushed somewhere the
|
|
422
|
+
// deploy never reads. Fail loudly with the fix rather than silently half-working.
|
|
423
|
+
const head = exec(`git -C ${root} rev-parse --abbrev-ref HEAD`, { allowFail: true });
|
|
424
|
+
const branch = String((head && head.stdout) || '').trim();
|
|
425
|
+
if (branch && branch !== 'main' && branch !== 'HEAD') {
|
|
426
|
+
throw new Error(`standalone instance ${inst.slug}: adopting ${repo.owner}/${repo.name} but its default branch is "${branch}", not "main". The instance deploy only ever reads origin/main, so the layer would never be served. Rename the default branch to main (or point target_ref at a repo whose default is main) and re-run.`);
|
|
427
|
+
}
|
|
428
|
+
exec(`git -C ${root} checkout main`, { allowFail: true });
|
|
429
|
+
} else {
|
|
430
|
+
if (!hasGit) {
|
|
431
|
+
exec(`mkdir -p ${root}`);
|
|
432
|
+
exec(`git -C ${root} init -q`);
|
|
433
|
+
exec(`git -C ${root} remote add origin ${remote}`);
|
|
434
|
+
}
|
|
435
|
+
exec(`git -C ${root} checkout -B main`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Core-install path (ADR 0134, task 2090). DEFAULT = vendor a tarball built from THIS
|
|
439
|
+
// control-plane checkout (offline, proven — task 2053). OPT-IN = pull the PRIVATE
|
|
440
|
+
// @bongos/core from the npm registry (PROVISION_CORE_FROM_REGISTRY=1). The registry
|
|
441
|
+
// path needs NPM_TOKEN at `npm ci` time; if the flag is set but no token is present we
|
|
442
|
+
// FAIL SOFT to the vendored path so a tokenless standup still works (never a broken
|
|
443
|
+
// instance). The env is injectable for tests; the runner falls back to process.env.
|
|
444
|
+
const env = deps.env || process.env;
|
|
445
|
+
const wantRegistry = registryCoreRequested(env);
|
|
446
|
+
const useRegistry = wantRegistry && hasNpmToken(env);
|
|
447
|
+
if (wantRegistry && !useRegistry) {
|
|
448
|
+
log(' [scaffold] PROVISION_CORE_FROM_REGISTRY set but no NPM_TOKEN in env — falling back to the vendored-tarball core (ADR 0134 fail-soft).');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
|
|
452
|
+
// --no-seed: the instance's own DB is seeded by migrate, not here (and on the adopt path
|
|
453
|
+
// the REAL-state seeding + backlog import happen when the owner runs `bongos init --adopt`
|
|
454
|
+
// themselves, against the live instance — the step the wizard walks them through).
|
|
455
|
+
//
|
|
456
|
+
// MODE FLAGS. Greenfield passes --force (overwrite is safe on an empty repo). Adopt passes
|
|
457
|
+
// --adopt --accept instead and NEVER --force: --adopt takes init's layering path, which
|
|
458
|
+
// writes each config only where absent and leaves pre-existing files untouched, and
|
|
459
|
+
// --accept clears the interactive pre-flight gate for this non-interactive runner (a
|
|
460
|
+
// BLOCKING pre-flight finding still hard-stops — it cannot be waved through). init refuses
|
|
461
|
+
// --adopt together with --force, so the two flag sets are mutually exclusive by design.
|
|
462
|
+
//
|
|
463
|
+
// Two mutually exclusive core-resolution shapes, orthogonal to the mode:
|
|
464
|
+
// registry (--registry-core): caret pin + env-fed .npmrc; `npm ci` pulls from the
|
|
465
|
+
// registry. init generates the lockfile (NPM_TOKEN is present — we gated on it).
|
|
466
|
+
// vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
|
|
467
|
+
const modeFlags = mode === 'adopt' ? '--adopt --accept' : '--force';
|
|
468
|
+
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
469
|
+
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
470
|
+
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
471
|
+
if (useRegistry) {
|
|
472
|
+
log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
|
|
473
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --registry-core`);
|
|
474
|
+
} else {
|
|
475
|
+
// Vendored tarball — resolveVendorableCoreTarball's three-rung ladder (shared
|
|
476
|
+
// with the re-provision pin refresh, task 1003050).
|
|
477
|
+
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
478
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
|
|
479
|
+
});
|
|
480
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
481
|
+
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
482
|
+
if (packDest) exec(`rm -rf ${packDest}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 4. commit + push to the instance's own main (keep node_modules out).
|
|
486
|
+
//
|
|
487
|
+
// .gitignore: greenfield OWNS the file. Adopt must not clobber the owner's existing
|
|
488
|
+
// ignores, so it only writes one where none exists — and where one does, it APPENDS the
|
|
489
|
+
// entries we actually require (node_modules/, the vendored-core exceptions, the generated
|
|
490
|
+
// nav docs) if they are missing, since `npm ci` + `git reset --hard` on the box depend on
|
|
491
|
+
// them. Duplicate lines in a .gitignore are harmless, so the append is idempotent-safe.
|
|
492
|
+
const giPath = path.join(root, '.gitignore');
|
|
493
|
+
if (mode === 'adopt' && fs.existsSync(giPath)) {
|
|
494
|
+
const existing = fs.readFileSync(giPath, 'utf8');
|
|
495
|
+
const missing = INSTANCE_GITIGNORE.split('\n')
|
|
496
|
+
.filter((l) => l.trim() && !l.startsWith('#'))
|
|
497
|
+
.filter((l) => !existing.split('\n').some((e) => e.trim() === l.trim()));
|
|
498
|
+
if (missing.length) {
|
|
499
|
+
writeFile(giPath, `${existing.replace(/\n*$/, '\n')}\n# Added by Cloud Bongos (adopt) — required by the instance deploy.\n${missing.join('\n')}\n`);
|
|
500
|
+
log(` [scaffold] appended ${missing.length} required .gitignore entr(ies) — existing rules left intact`);
|
|
501
|
+
} else {
|
|
502
|
+
log(' [scaffold] .gitignore already covers the required entries — left untouched');
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
writeFile(giPath, INSTANCE_GITIGNORE);
|
|
506
|
+
}
|
|
507
|
+
exec(`git -C ${root} config user.name "Cloud Bongos"`);
|
|
508
|
+
exec(`git -C ${root} config user.email "${provisionerBotEmail()}"`);
|
|
509
|
+
exec(`git -C ${root} add -A`);
|
|
510
|
+
const commitMsg = mode === 'adopt'
|
|
511
|
+
? `chore: adopt Cloud Bongos onto ${inst.slug} on a pinned @bongos/core ${coreV} (provision.js, ADR 0121)`
|
|
512
|
+
: `chore: scaffold ${inst.slug} on a pinned @bongos/core ${coreV} (provision.js, ADR 0108)`;
|
|
513
|
+
exec(`git -C ${root} commit -m "${commitMsg}"`, { allowFail: true });
|
|
514
|
+
|
|
515
|
+
// Push. NO --force on either path, ever (see the function header): the ADR 0155 `repo`
|
|
516
|
+
// credential can write to every repo the owner owns, so a force here would be a route to
|
|
517
|
+
// destroying their work. Adopt's commit is a descendant of the tip it cloned, so an
|
|
518
|
+
// ordinary push fast-forwards; if the remote moved underneath us the rejection is the
|
|
519
|
+
// correct outcome and a re-run picks up the new tip.
|
|
520
|
+
if (credFile) {
|
|
521
|
+
exec(`git -C ${root} ${credOpt}push ${httpsUrl} HEAD:main`);
|
|
522
|
+
log(` [scaffold] ${mode === 'adopt' ? 'pushed the adopt layer' : 'pushed'} to ${repo.owner}/${repo.name}@main via the owner's GitHub token (task 1002317)`);
|
|
523
|
+
} else {
|
|
524
|
+
exec(`git -C ${root} push -u origin main`);
|
|
525
|
+
log(` [scaffold] pushed ${mode === 'adopt' ? 'the adopt layer' : 'initial scaffold'} to ${repo.owner}/${repo.name}@main (SSH origin)`);
|
|
526
|
+
}
|
|
527
|
+
return { ok: true, scaffolded: true, mode };
|
|
528
|
+
} finally {
|
|
529
|
+
// Always remove the credential file — a failed clone/push must not leave it on disk.
|
|
530
|
+
if (credFile) exec(`rm -f ${credFile}`, { allowFail: true });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── private-repo deploy key (ADR 0176, task 1002735; walk finding N4) ────────────
|
|
535
|
+
|
|
536
|
+
// The instance owner's stored `repo`-scoped OAuth token (ADR 0155), or null (none
|
|
537
|
+
// stored, org-owned repo, secret-box off). Shared by the scaffold push, the
|
|
538
|
+
// private-repo deploy-key wiring, and the teardown revoke. `deps.getGithubToken` is
|
|
539
|
+
// injectable for tests; production resolves it lazily against the core db slice
|
|
540
|
+
// with the runner's pool.
|
|
541
|
+
async function resolveOwnerGithubToken(inst, deps, { tag = '[repo-auth]' } = {}) {
|
|
542
|
+
if (!inst.owner_builder_id) return null;
|
|
543
|
+
try {
|
|
544
|
+
const fetchTok = deps.getGithubToken
|
|
545
|
+
|| ((bid) => require(path.join(REPO_ROOT, 'src/bongos/db')).getGithubToken({ builderId: bid, p: deps.db }));
|
|
546
|
+
const r = await fetchTok(inst.owner_builder_id);
|
|
547
|
+
return r && r.token ? r.token : null;
|
|
548
|
+
} catch (e) {
|
|
549
|
+
(deps.log || (() => {}))(` ${tag} owner OAuth token lookup failed (${e && e.message})`);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function deployKeyTitle(inst) { return `cloudbongos-deploy-${inst.slug}`; }
|
|
555
|
+
// Beside the checkout's own metadata: untracked by definition, survives the deploy's
|
|
556
|
+
// `git reset --hard`, and dies with the checkout rather than accreting in ~/.ssh.
|
|
557
|
+
function deployKeyPath(inst) { return path.posix.join(standaloneRoot(inst), '.git', 'cloudbongos-deploy-key'); }
|
|
558
|
+
|
|
559
|
+
// Make a PRIVATE target_ref durably readable by the recurring deploy pull. The
|
|
560
|
+
// owner's token covers only the ONE-SHOT clone/push (its credential file is deleted
|
|
561
|
+
// after the scaffold), while `origin` rides the shared SSH alias — which GitHub
|
|
562
|
+
// answers "Repository not found" for a private repo it isn't registered on. That
|
|
563
|
+
// mismatch is walk finding N4: every private standup failed at `git pull --ff-only
|
|
564
|
+
// origin main`, token or not (seen live: mercury, 2026-08-17).
|
|
565
|
+
//
|
|
566
|
+
// The fix (ADR 0176): mint the instance its OWN ed25519 deploy key, register it
|
|
567
|
+
// READ-ONLY on the repo with the owner's token (idempotent by title; a stale
|
|
568
|
+
// same-title key is replaced), then wire the CHECKOUT — origin re-pointed at plain
|
|
569
|
+
// github.com and core.sshCommand pinned to the key with -F /dev/null so no
|
|
570
|
+
// ~/.ssh/config identity can shadow it. Public repos return untouched; no token →
|
|
571
|
+
// skip (the request-time refusal is task 1002909's preflight). Dry-run plans only.
|
|
572
|
+
async function ensurePrivateRepoAccess(inst, deps) {
|
|
573
|
+
const { log, apply } = deps;
|
|
574
|
+
const exec = deps.controlExec || deps.exec;
|
|
575
|
+
if (inst.hosting_shape !== 'standalone') return { skipped: 'shape' };
|
|
576
|
+
const repo = parseTargetRef(inst.target_ref);
|
|
577
|
+
if (!repo) return { skipped: 'no_repo' };
|
|
578
|
+
// Dry-run stays OFFLINE like every other plan path here (the checkDnsTokenScope
|
|
579
|
+
// preflight is apply-gated for the same reason): no token read, no GitHub call —
|
|
580
|
+
// a plan run must stay a safe inspection tool for a snagged instance.
|
|
581
|
+
if (!apply) {
|
|
582
|
+
log(` [repo-auth] would check ${repo.owner}/${repo.name} visibility with the owner's credential and, if PRIVATE, mint + register a read-only deploy key and wire origin + core.sshCommand to it (ADR 0176)`);
|
|
583
|
+
return { planned: true };
|
|
584
|
+
}
|
|
585
|
+
const ownerToken = await resolveOwnerGithubToken(inst, deps);
|
|
586
|
+
if (!ownerToken) {
|
|
587
|
+
log(' [repo-auth] no usable owner GitHub credential (none stored, or it could not be read) — a PRIVATE repo would fail at the deploy pull (public repos are unaffected)');
|
|
588
|
+
return { skipped: 'no_token' };
|
|
589
|
+
}
|
|
590
|
+
const ghRepos = deps.githubRepos || require(path.join(REPO_ROOT, 'src/bongos/github-repos'));
|
|
591
|
+
let info;
|
|
592
|
+
try {
|
|
593
|
+
info = await ghRepos.getUserRepo(ownerToken, { owner: repo.owner, name: repo.name, fetchImpl: deps.fetchImpl });
|
|
594
|
+
} catch (e) {
|
|
595
|
+
if (e && e.code === 'repo_not_found') {
|
|
596
|
+
// Plain sentence: this rides error_note onto the owner's project card.
|
|
597
|
+
throw new Error(`repository not found: GitHub answered 404 for ${repo.owner}/${repo.name} even with your granted credential — the repository may have been renamed or deleted, or the credential lost access. Check it on GitHub, then try again.`);
|
|
598
|
+
}
|
|
599
|
+
log(` [repo-auth] could not read ${repo.owner}/${repo.name} (${e && e.message}) — continuing; a PRIVATE repo would fail at the deploy pull`);
|
|
600
|
+
return { skipped: 'github_unreachable' };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const root = standaloneRoot(inst);
|
|
604
|
+
const keyPath = deployKeyPath(inst);
|
|
605
|
+
const title = deployKeyTitle(inst);
|
|
606
|
+
const fsImpl = deps.fsImpl || fs;
|
|
607
|
+
|
|
608
|
+
if (!info.private) {
|
|
609
|
+
// Flipped private → public since we wired it: un-wire, or the checkout stays
|
|
610
|
+
// pinned (IdentitiesOnly + -F /dev/null admit no other identity) to a key the
|
|
611
|
+
// owner is now free to delete from their repo settings. Keyed on our key file
|
|
612
|
+
// so the common public path stays zero-mutation.
|
|
613
|
+
if (fsImpl.existsSync(keyPath)) {
|
|
614
|
+
log(` [repo-auth] ${repo.owner}/${repo.name} is public again — un-wiring its deploy key`);
|
|
615
|
+
try {
|
|
616
|
+
const keys = await ghRepos.listDeployKeys(ownerToken, { owner: repo.owner, name: repo.name, fetchImpl: deps.fetchImpl });
|
|
617
|
+
const ours = keys.find((k) => k.title === title);
|
|
618
|
+
if (ours) await ghRepos.deleteDeployKey(ownerToken, { owner: repo.owner, name: repo.name, keyId: ours.id, fetchImpl: deps.fetchImpl });
|
|
619
|
+
} catch (e) { log(` [repo-auth] (remote key revoke skipped: ${e && e.message})`); }
|
|
620
|
+
exec(`git -C ${root} remote set-url origin ${instanceRepoRemote(repo)}`);
|
|
621
|
+
exec(`git -C ${root} config --unset core.sshCommand`, { allowFail: true });
|
|
622
|
+
exec(`rm -f ${keyPath} ${keyPath}.pub`, { allowFail: true });
|
|
623
|
+
return { private: false, unwired: true };
|
|
624
|
+
}
|
|
625
|
+
return { private: false }; // the shared-alias path reads public repos as-is
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Registering a deploy key needs repository ADMIN; getUserRepo already told us.
|
|
629
|
+
// Refuse up front with the fix, instead of a raw 403 mid-standup.
|
|
630
|
+
if (info.admin === false) {
|
|
631
|
+
throw new Error(`repository access is not admin: your granted credential can read ${repo.owner}/${repo.name}, but registering its deploy key needs repository ADMIN. Ask a repository admin to raise your access (or make the repository public), then try again.`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
log(` [repo-auth] ${repo.owner}/${repo.name} is PRIVATE — wiring its own read-only deploy key (ADR 0176)`);
|
|
635
|
+
if (!fsImpl.existsSync(keyPath) || !fsImpl.existsSync(`${keyPath}.pub`)) {
|
|
636
|
+
exec(`rm -f ${keyPath} ${keyPath}.pub`, { allowFail: true });
|
|
637
|
+
exec(`ssh-keygen -q -t ed25519 -N '' -C ${title} -f ${keyPath}`);
|
|
638
|
+
exec(`chmod 600 ${keyPath}`); // ssh-keygen's 0600 default, made an explicit invariant
|
|
639
|
+
}
|
|
640
|
+
const pubKey = String(fsImpl.readFileSync(`${keyPath}.pub`, 'utf8')).trim();
|
|
641
|
+
// Compare algorithm+key only — GitHub stores the key without the local comment.
|
|
642
|
+
const keyBody = (k) => String(k || '').trim().split(/\s+/).slice(0, 2).join(' ');
|
|
643
|
+
let minted = false;
|
|
644
|
+
try {
|
|
645
|
+
const keys = await ghRepos.listDeployKeys(ownerToken, { owner: repo.owner, name: repo.name, fetchImpl: deps.fetchImpl });
|
|
646
|
+
const ours = keys.find((k) => k.title === title);
|
|
647
|
+
const current = !!(ours && keyBody(ours.key) === keyBody(pubKey));
|
|
648
|
+
if (ours && !current) {
|
|
649
|
+
// GitHub 422s a duplicate title/key, and a stale registration is a dead credential —
|
|
650
|
+
// replace, never accumulate.
|
|
651
|
+
await ghRepos.deleteDeployKey(ownerToken, { owner: repo.owner, name: repo.name, keyId: ours.id, fetchImpl: deps.fetchImpl });
|
|
652
|
+
log(` [repo-auth] removed the stale "${title}" key on ${repo.owner}/${repo.name} (local key changed)`);
|
|
653
|
+
}
|
|
654
|
+
if (!current) {
|
|
655
|
+
try {
|
|
656
|
+
await ghRepos.addDeployKey(ownerToken, { owner: repo.owner, name: repo.name, title, key: pubKey, readOnly: true, fetchImpl: deps.fetchImpl });
|
|
657
|
+
} catch (e) {
|
|
658
|
+
if (!e || e.code !== 'key_in_use') throw e;
|
|
659
|
+
// The keypair is slug-keyed but the repo can change underneath it (renamed
|
|
660
|
+
// then recreated, target_ref re-pointed): the key is registered SOMEWHERE
|
|
661
|
+
// else, so retrying with this pair can never succeed. Mint fresh, try once.
|
|
662
|
+
log(' [repo-auth] GitHub holds this key elsewhere (422) — minting a fresh keypair and retrying once');
|
|
663
|
+
exec(`rm -f ${keyPath} ${keyPath}.pub`, { allowFail: true });
|
|
664
|
+
exec(`ssh-keygen -q -t ed25519 -N '' -C ${title} -f ${keyPath}`);
|
|
665
|
+
exec(`chmod 600 ${keyPath}`);
|
|
666
|
+
const freshPub = String(fsImpl.readFileSync(`${keyPath}.pub`, 'utf8')).trim();
|
|
667
|
+
await ghRepos.addDeployKey(ownerToken, { owner: repo.owner, name: repo.name, title, key: freshPub, readOnly: true, fetchImpl: deps.fetchImpl });
|
|
668
|
+
}
|
|
669
|
+
minted = true;
|
|
670
|
+
log(` [repo-auth] registered read-only deploy key "${title}" on ${repo.owner}/${repo.name}`);
|
|
671
|
+
} else {
|
|
672
|
+
log(` [repo-auth] deploy key "${title}" already registered — nothing to mint`);
|
|
673
|
+
}
|
|
674
|
+
} catch (e) {
|
|
675
|
+
if (e && e.code === 'not_repo_admin') {
|
|
676
|
+
throw new Error(`repository access is not admin: your granted credential can read ${repo.owner}/${repo.name}, but registering its deploy key needs repository ADMIN. Ask a repository admin to raise your access (or make the repository public), then try again.`);
|
|
677
|
+
}
|
|
678
|
+
// Plain sentence for the owner card — a transient GitHub hiccup must not read as jargon.
|
|
679
|
+
throw new Error(`deploy-key setup failed for ${repo.owner}/${repo.name}: ${e && e.message}. Nothing is wrong on your side — try again, and if it keeps failing make the repository public and retry.`);
|
|
680
|
+
}
|
|
681
|
+
exec(`git -C ${root} remote set-url origin git@github.com:${repo.owner}/${repo.name}.git`);
|
|
682
|
+
exec(`git -C ${root} config core.sshCommand "ssh -F /dev/null -i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"`);
|
|
683
|
+
return { private: true, minted };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ── re-provision core-pin refresh (gap-list W3, task 1003050) ────────────────────
|
|
687
|
+
//
|
|
688
|
+
// The hazard: a standalone instance's RUNNING core can move ahead of its repo's
|
|
689
|
+
// committed `file:` pin (an out-of-band `bongos upgrade`, a pre-fix re-provision that
|
|
690
|
+
// refreshed the box without committing) — seen live on Spike Harbor, running a modern
|
|
691
|
+
// core while the repo still pinned 1.19.2. The deploy path is repo-driven
|
|
692
|
+
// (`git reset --hard origin/main` + `npm ci`), so the NEXT deploy silently rolls the
|
|
693
|
+
// instance back against a DB the newer core has already migrated. ADR 0168 fixed
|
|
694
|
+
// fresh scaffolds only; this closes the re-provision leg: every provision of an
|
|
695
|
+
// already-scaffolded instance heals the committed pin to the core this control plane
|
|
696
|
+
// runs, as a commit pushed to the instance repo.
|
|
697
|
+
|
|
698
|
+
// The PURE decision half (tested in tests/provision.mjs). Registry pins are npm's to
|
|
699
|
+
// resolve; an absent dep is not ours to invent; only `file:` pins are healed.
|
|
700
|
+
function planCorePinRefresh({ pinnedDep, coreVersion }) {
|
|
701
|
+
const dep = String(pinnedDep || '').trim();
|
|
702
|
+
if (!dep) return { action: 'skip', reason: 'no @bongos/core dependency in the instance package.json' };
|
|
703
|
+
if (!/^file:/.test(dep)) return { action: 'skip', reason: `registry pin (${dep}) — npm ci resolves it; nothing to vendor` };
|
|
704
|
+
const upgrade = require('./upgrade');
|
|
705
|
+
const pinned = upgrade.versionFromTgzPath(dep);
|
|
706
|
+
if (pinned && pinned === coreVersion) return { action: 'current', pinned };
|
|
707
|
+
return { action: 'refresh', pinned: pinned || null };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Coerce a version label to a semver-safe charset BEFORE it is interpolated into any
|
|
711
|
+
// shell string. The "from" label ultimately derives from the INSTANCE's own repo
|
|
712
|
+
// package.json `file:` pin (lower trust than the shared control plane) via
|
|
713
|
+
// versionFromTgzPath, whose regex admits any char but `/` — including `"`, backtick,
|
|
714
|
+
// `$()`, `;`. A crafted pin (e.g. `file:vendor/bongos-core-1.0.0";touch x;echo".tgz`)
|
|
715
|
+
// would otherwise break out of a `git commit -m "…"` argument and run as the
|
|
716
|
+
// control-plane user. This chokepoint rejects anything outside semver's own alphabet
|
|
717
|
+
// (task 1003050 grade — command-injection block). PURE.
|
|
718
|
+
function safeVersionLabel(v) {
|
|
719
|
+
const s = String(v == null ? '' : v).trim();
|
|
720
|
+
return /^[0-9A-Za-z.+-]{1,64}$/.test(s) ? s : '(unversioned)';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// The commit + push command sequence for a pin refresh — PURE, so tests/provision.mjs
|
|
724
|
+
// covers "the runner's commit step" (task 1003050 AC) deterministically. Version
|
|
725
|
+
// labels pass through safeVersionLabel (the one interpolation-safety chokepoint); the
|
|
726
|
+
// push prefers the owner's OAuth token URL (the shared deploy key can't write a
|
|
727
|
+
// consumer repo — task 1002317) and falls back to the SSH origin.
|
|
728
|
+
function buildCorePinRefreshCommit({ root, fromVersion, toVersion, repo, ownerToken, credFile }) {
|
|
729
|
+
const from = safeVersionLabel(fromVersion);
|
|
730
|
+
const to = safeVersionLabel(toVersion);
|
|
731
|
+
const msg = `chore: re-provision refreshes the pinned @bongos/core ${from} -> ${to} (task 1003050)`;
|
|
732
|
+
const push = (ownerToken && repo && credFile)
|
|
733
|
+
? `git -C ${root} -c credential.helper='store --file=${credFile}' push https://x-access-token@github.com/${repo.owner}/${repo.name}.git HEAD:main`
|
|
734
|
+
: `git -C ${root} push origin main`;
|
|
735
|
+
return {
|
|
736
|
+
config: [
|
|
737
|
+
`git -C ${root} config user.name "Cloud Bongos"`,
|
|
738
|
+
`git -C ${root} config user.email "${provisionerBotEmail()}"`,
|
|
739
|
+
],
|
|
740
|
+
add: `git -C ${root} add -A`,
|
|
741
|
+
commit: `git -C ${root} commit -m "${msg}"`,
|
|
742
|
+
push,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function refreshStandaloneCorePin(inst, deps) {
|
|
747
|
+
const { log } = deps;
|
|
748
|
+
const exec = deps.controlExec || deps.exec;
|
|
749
|
+
const writeFile = deps.controlWriteFile || deps.writeFile;
|
|
750
|
+
const upgrade = require('./upgrade');
|
|
751
|
+
const root = standaloneRoot(inst);
|
|
752
|
+
const coreV = coreVersionSafe();
|
|
753
|
+
if (!coreV) { log(' [core-pin] cannot resolve the running core version — leaving the committed pin alone'); return { action: 'skip', reason: 'no core version' }; }
|
|
754
|
+
let pkg = null;
|
|
755
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { /* unreadable → skip below */ }
|
|
756
|
+
const pinnedDep = pkg && ((pkg.dependencies || {})['@bongos/core'] || (pkg.devDependencies || {})['@bongos/core']);
|
|
757
|
+
const plan = planCorePinRefresh({ pinnedDep, coreVersion: coreV });
|
|
758
|
+
if (plan.action === 'skip') { log(` [core-pin] ${plan.reason}`); return plan; }
|
|
759
|
+
if (plan.action === 'current') { log(` [core-pin] committed pin already matches the running core (${coreV})`); return plan; }
|
|
760
|
+
log(` [core-pin] committed pin ${safeVersionLabel(plan.pinned)} trails this control plane's core ${coreV} — refreshing so the next repo-driven deploy cannot roll the instance back (task 1003050)`);
|
|
761
|
+
|
|
762
|
+
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
763
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[core-pin]',
|
|
764
|
+
});
|
|
765
|
+
try {
|
|
766
|
+
const targetVersion = upgrade.versionFromTgzPath(path.basename(tgz)) || coreV;
|
|
767
|
+
const vendorRel = upgrade.vendorTarball({ instanceDir: root, fromTgz: tgz, targetVersion });
|
|
768
|
+
// Prune superseded vendored tarballs (+ manifests) so the repo carries exactly ONE
|
|
769
|
+
// core. fs.rmSync, NOT exec — a filename comes off the instance's own (lower-trust)
|
|
770
|
+
// vendor/ dir, so shelling `rm -f ${f}` would be the same injection vector as the
|
|
771
|
+
// commit label (a committed file named `bongos-core-x";touch pwn;".tgz`). A direct
|
|
772
|
+
// fs call takes the name as data, never as shell (task 1003050 grade).
|
|
773
|
+
const vendorDir = path.join(root, 'vendor');
|
|
774
|
+
const keep = new Set([`${upgrade.ARTIFACT}-${targetVersion}.tgz`, `${upgrade.ARTIFACT}-${targetVersion}.manifest.json`]);
|
|
775
|
+
// vendorTarball just mkdirSync'd this, so it exists on the happy path; guard anyway
|
|
776
|
+
// so an unexpectedly-absent dir prunes nothing instead of crashing the whole provision.
|
|
777
|
+
if (fs.existsSync(vendorDir)) {
|
|
778
|
+
for (const f of fs.readdirSync(vendorDir)) {
|
|
779
|
+
if (f.startsWith(`${upgrade.ARTIFACT}-`) && !keep.has(f)) fs.rmSync(path.join(vendorDir, f), { force: true });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
upgrade.writePin(root, vendorRel);
|
|
783
|
+
// `npm install` (NOT ci): the pin just changed, the lockfile must be regenerated.
|
|
784
|
+
const instRes = upgrade.npmInstall(root);
|
|
785
|
+
if (!instRes.ok) {
|
|
786
|
+
throw new Error(`npm install after the core-pin refresh failed (${instRes.reason || `exit ${instRes.code}`}) — lockfile not regenerated; the pin refresh cannot be committed half-done.`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Commit + push the refresh to the instance repo — the shared deploy key cannot
|
|
790
|
+
// write a consumer's repo (task 1002317), so prefer the owner's OAuth token,
|
|
791
|
+
// exactly like the scaffold's push.
|
|
792
|
+
const ownerToken = await resolveOwnerGithubToken(inst, deps, { tag: '[core-pin]' });
|
|
793
|
+
const repo = parseTargetRef(inst.target_ref);
|
|
794
|
+
const credFile = ownerToken && repo ? path.join(os.tmpdir(), `bongos-gitcred-pin-${inst.slug}-${process.pid}`) : null;
|
|
795
|
+
const cmd = buildCorePinRefreshCommit({ root, fromVersion: plan.pinned, toVersion: targetVersion, repo, ownerToken, credFile });
|
|
796
|
+
try {
|
|
797
|
+
cmd.config.forEach((c) => exec(c));
|
|
798
|
+
exec(cmd.add);
|
|
799
|
+
exec(cmd.commit, { allowFail: true });
|
|
800
|
+
if (credFile) writeFile(credFile, `https://x-access-token:${ownerToken}@github.com\n`, { mode: 0o600 });
|
|
801
|
+
exec(cmd.push);
|
|
802
|
+
} finally {
|
|
803
|
+
if (credFile) exec(`rm -f ${credFile}`, { allowFail: true });
|
|
804
|
+
}
|
|
805
|
+
log(` [core-pin] refreshed + pushed: @bongos/core ${safeVersionLabel(plan.pinned)} → ${targetVersion}`);
|
|
806
|
+
return { ...plan, refreshed: true, toVersion: targetVersion };
|
|
807
|
+
} finally {
|
|
808
|
+
if (packDest) exec(`rm -rf ${packDest}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// The per-instance systemd unit (the infra/cloudbongos.service shape): one Node process
|
|
813
|
+
// bound to the instance's port + DB + brand, secrets from its own env file. SHAPE-AWARE:
|
|
814
|
+
// a STANDALONE instance runs the CORE PACKAGE's platform-server from its OWN repo dir
|
|
815
|
+
// (WorkingDirectory=standaloneRoot → resolveInstanceRoot() resolves to the instance via
|
|
816
|
+
// cwd, deploy-instance.sh §4); co-tenant/dedicated run this control-plane checkout's src/.
|
|
817
|
+
|
|
818
|
+
module.exports = {
|
|
819
|
+
APP_USER,
|
|
820
|
+
CORE_PKG_DIR,
|
|
821
|
+
INSTANCE_GITIGNORE,
|
|
822
|
+
alreadyScaffolded,
|
|
823
|
+
asPostgres,
|
|
824
|
+
buildCorePinRefreshCommit,
|
|
825
|
+
dbCreateCmd,
|
|
826
|
+
dbName,
|
|
827
|
+
deployKeyPath,
|
|
828
|
+
deployKeyTitle,
|
|
829
|
+
ensurePrivateRepoAccess,
|
|
830
|
+
installedCorePackDir,
|
|
831
|
+
installedCoreTarball,
|
|
832
|
+
instanceInitSpec,
|
|
833
|
+
instanceRepoRemote,
|
|
834
|
+
migrateCmd,
|
|
835
|
+
onboardMode,
|
|
836
|
+
parseTargetRef,
|
|
837
|
+
planCorePinRefresh,
|
|
838
|
+
refreshStandaloneCorePin,
|
|
839
|
+
resolveOwnerGithubToken,
|
|
840
|
+
resolveVendorableCoreTarball,
|
|
841
|
+
safeVersionLabel,
|
|
842
|
+
scaffoldStandaloneRepo,
|
|
843
|
+
seedFirstVersionCmd,
|
|
844
|
+
standaloneInstallCmd,
|
|
845
|
+
standaloneMigrateCmd,
|
|
846
|
+
standalonePullCmd,
|
|
847
|
+
standaloneRegenDocsCmd,
|
|
848
|
+
standaloneRoot,
|
|
849
|
+
};
|