@bongos/core 1.19.736 → 1.19.738
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 +31 -36
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/upgrading-the-core.md +3 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/go-live.js +18 -44
- package/scripts/gds/init.js +21 -29
- package/scripts/gds/provision-config.js +0 -8
- package/scripts/gds/provision-repo-auth.js +106 -0
- package/scripts/gds/provision-repo.js +34 -67
- package/scripts/gds/provision.js +3 -5
- package/scripts/gds/start.js +1 -1
- package/scripts/gds/update-channel.js +3 -3
- package/scripts/gds/upgrade.js +204 -41
- package/src/module-api.js +1 -1
- package/tests/executed_closure.mjs +8 -0
- package/tests/go_live.mjs +20 -19
- package/tests/init.mjs +11 -6
- package/tests/provision.mjs +33 -10
- package/tests/start_env_guard.mjs +8 -3
- package/tests/upgrade.mjs +290 -4
- package/scripts/gds/npmrc.js +0 -124
- package/tests/npmrc.mjs +0 -154
|
@@ -14,7 +14,11 @@ const crypto = require('node:crypto');
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
|
-
const { CONFIG, REPO_ROOT, coreVersionSafe,
|
|
17
|
+
const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail } = require('./provision-config.js');
|
|
18
|
+
// The repo-identity + owner-credential leaf (task 1003081). Carved OUT of this file so
|
|
19
|
+
// upgrade.js can reach these three without pulling provision*.js into the merge
|
|
20
|
+
// resolver's executed closure; re-exported below so every existing caller is unchanged.
|
|
21
|
+
const { instanceRepoRemote, parseTargetRef, resolveOwnerGithubToken } = require('./provision-repo-auth.js');
|
|
18
22
|
|
|
19
23
|
function dbName(inst) { return inst.db_name || inst.slug; }
|
|
20
24
|
|
|
@@ -288,25 +292,10 @@ function dbRoleGrantsCmd(inst, { privileged = false } = {}) {
|
|
|
288
292
|
// THROW if the instance repo wasn't already cloned. These generate + run the scaffold
|
|
289
293
|
// that clones an EMPTY target_ref and turns it into an installable, bootable instance.
|
|
290
294
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.replace(/^git@[^:]+:/, '')
|
|
296
|
-
.replace(/^https?:\/\/[^/]+\//, '')
|
|
297
|
-
.match(/^([A-Za-z0-9][A-Za-z0-9_.-]*)\/([A-Za-z0-9][A-Za-z0-9_.-]*?)(?:\.git)?$/);
|
|
298
|
-
return m ? { owner: m[1], name: m[2] } : null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// The git remote for an instance's OWN repo. Reuses the SAME SSH deploy alias the
|
|
302
|
-
// control-plane checkout uses (git@<alias>:<owner>/<repo>.git) — NO secret in the
|
|
303
|
-
// command (SSH-key auth, nothing to leak into journald), consistent with how the
|
|
304
|
-
// platform repo is wired. Standing up a new instance repo therefore needs only that
|
|
305
|
-
// the control plane's deploy key has WRITE access to that repo (ADR 0125). Alias is
|
|
306
|
-
// overridable via PROVISION_GIT_SSH_ALIAS.
|
|
307
|
-
function instanceRepoRemote(repo, { sshAlias = process.env.PROVISION_GIT_SSH_ALIAS || 'github-deploy' } = {}) {
|
|
308
|
-
return `git@${sshAlias}:${repo.owner}/${repo.name}.git`;
|
|
309
|
-
}
|
|
295
|
+
// parseTargetRef + instanceRepoRemote (and resolveOwnerGithubToken, below the scaffold)
|
|
296
|
+
// now live in the ./provision-repo-auth.js leaf and are re-exported unchanged from here —
|
|
297
|
+
// upgrade.js needs them and is inside the merge resolver's executed closure, which a require of
|
|
298
|
+
// this file would have widened by five (task 1003081; see that module's header).
|
|
310
299
|
|
|
311
300
|
// A minimal, VALID `bongos init` spec (init.validateSpec greenfield) from the
|
|
312
301
|
// provisioning row + the owner's github login. The scaffold produces a bootable,
|
|
@@ -613,18 +602,20 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
613
602
|
exec(`git -C ${root} checkout -B main`);
|
|
614
603
|
}
|
|
615
604
|
|
|
616
|
-
// Core-install path
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
605
|
+
// Core-install path: vendor a tarball built from THIS control-plane checkout
|
|
606
|
+
// (offline, proven — task 2053).
|
|
607
|
+
//
|
|
608
|
+
// This used to be a fork: an opt-in env var selected the registry, and a missing npm
|
|
609
|
+
// credential silently failed it soft back to vendored. Both halves were auth-era and both
|
|
610
|
+
// are gone (task 1003878) — @bongos/core is public, so nothing needs a credential, and an
|
|
611
|
+
// env var that silently changed which install path ran is the divergence behind task
|
|
612
|
+
// 1003520. tests/provision.mjs pins both retired names and proves they are inert.
|
|
613
|
+
//
|
|
614
|
+
// Vendored stays the default here for a reason that has nothing to do with auth and
|
|
615
|
+
// survives the package going public: THIS CHECKOUT IS ROUTINELY AHEAD OF THE REGISTRY.
|
|
616
|
+
// A control plane on an unreleased CORE_VERSION has no published version to install, so
|
|
617
|
+
// the registry path would 404 on exactly the commits an operator most wants to stand up.
|
|
618
|
+
// Whether the vendored path still earns its keep at all is gate 6's call (task 1003879).
|
|
628
619
|
|
|
629
620
|
// materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
|
|
630
621
|
// --no-seed: the instance's own DB is seeded by migrate, not here (and on the adopt path
|
|
@@ -638,27 +629,21 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
638
629
|
// BLOCKING pre-flight finding still hard-stops — it cannot be waved through). init refuses
|
|
639
630
|
// --adopt together with --force, so the two flag sets are mutually exclusive by design.
|
|
640
631
|
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
// vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
|
|
632
|
+
// Core resolution is vendored (--vendor-core): a tarball built from this checkout,
|
|
633
|
+
// file:-pinned. init still accepts --registry-core for an operator who explicitly wants
|
|
634
|
+
// a published version, and that flag no longer needs any credential.
|
|
645
635
|
const modeFlags = mode === 'adopt' ? '--adopt --accept' : '--force';
|
|
646
636
|
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
647
637
|
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
648
638
|
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
});
|
|
658
|
-
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
659
|
-
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
660
|
-
if (packDest) exec(`rm -rf ${packDest}`);
|
|
661
|
-
}
|
|
639
|
+
// Vendored tarball — resolveVendorableCoreTarball's three-rung ladder (shared
|
|
640
|
+
// with the re-provision pin refresh, task 1003050).
|
|
641
|
+
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
642
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
|
|
643
|
+
});
|
|
644
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
645
|
+
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
646
|
+
if (packDest) exec(`rm -rf ${packDest}`);
|
|
662
647
|
|
|
663
648
|
// 4. commit + push to the instance's own main (keep node_modules out).
|
|
664
649
|
//
|
|
@@ -711,24 +696,6 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
711
696
|
|
|
712
697
|
// ── private-repo deploy key (ADR 0176, task 1002735; walk finding N4) ────────────
|
|
713
698
|
|
|
714
|
-
// The instance owner's stored `repo`-scoped OAuth token (ADR 0155), or null (none
|
|
715
|
-
// stored, org-owned repo, secret-box off). Shared by the scaffold push, the
|
|
716
|
-
// private-repo deploy-key wiring, and the teardown revoke. `deps.getGithubToken` is
|
|
717
|
-
// injectable for tests; production resolves it lazily against the core db slice
|
|
718
|
-
// with the runner's pool.
|
|
719
|
-
async function resolveOwnerGithubToken(inst, deps, { tag = '[repo-auth]' } = {}) {
|
|
720
|
-
if (!inst.owner_builder_id) return null;
|
|
721
|
-
try {
|
|
722
|
-
const fetchTok = deps.getGithubToken
|
|
723
|
-
|| ((bid) => require(path.join(REPO_ROOT, 'src/bongos/db')).getGithubToken({ builderId: bid, p: deps.db }));
|
|
724
|
-
const r = await fetchTok(inst.owner_builder_id);
|
|
725
|
-
return r && r.token ? r.token : null;
|
|
726
|
-
} catch (e) {
|
|
727
|
-
(deps.log || (() => {}))(` ${tag} owner OAuth token lookup failed (${e && e.message})`);
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
699
|
function deployKeyTitle(inst) { return `cloudbongos-deploy-${inst.slug}`; }
|
|
733
700
|
// Beside the checkout's own metadata: untracked by definition, survives the deploy's
|
|
734
701
|
// `git reset --hard`, and dies with the checkout rather than accreting in ~/.ssh.
|
package/scripts/gds/provision.js
CHANGED
|
@@ -360,11 +360,9 @@ async function provisionInstance(inst, deps) {
|
|
|
360
360
|
// repo + install the pinned @bongos/core, so the migrate + service below run
|
|
361
361
|
// from the INSTANCE root, not this control-plane checkout. cwd = standaloneRoot.
|
|
362
362
|
// boxExec = the control plane (where scaffoldStandaloneRepo just created the dir).
|
|
363
|
-
//
|
|
364
|
-
// .
|
|
365
|
-
//
|
|
366
|
-
// scaffold already gated on the token, so it is present; the VENDORED default needs
|
|
367
|
-
// no token (the tarball is committed). A tokenless registry `npm ci` 404s cleanly.
|
|
363
|
+
// CORE RESOLUTION: the vendored default needs no network at all (the tarball is
|
|
364
|
+
// committed). A registry pin resolves from the public registry — @bongos/core is public
|
|
365
|
+
// (task 1003872), so `npm ci` needs no credential either way (task 1003878).
|
|
368
366
|
const root = standaloneRoot(inst);
|
|
369
367
|
log(' [code] standalone — pull instance repo + npm ci (pinned @bongos/core)');
|
|
370
368
|
boxExec(standalonePullCmd(), { cwd: root });
|
package/scripts/gds/start.js
CHANGED
|
@@ -587,7 +587,7 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
|
|
|
587
587
|
out.push(`${surface.strong(`Don't run ${surface.code('bongos dev')} for this.`)} It stands up a separate local instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
|
|
588
588
|
if (depsMissing) {
|
|
589
589
|
out.push('');
|
|
590
|
-
out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is
|
|
590
|
+
out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is ${surface.strong('public')} on npm, so ${surface.code('npm install @bongos/core')} needs no token or login; a failure here is a real error, not the expected tokenless 404 it used to be.)`);
|
|
591
591
|
}
|
|
592
592
|
return out.join('\n');
|
|
593
593
|
}
|
|
@@ -122,9 +122,9 @@ function resolveChannelTarget({ installed, channel, available } = {}) {
|
|
|
122
122
|
|
|
123
123
|
// ---- registry: what versions of the core are published? ---------------------
|
|
124
124
|
|
|
125
|
-
// List published versions of @bongos/core from the registry via `npm view … versions --json
|
|
126
|
-
//
|
|
127
|
-
//
|
|
125
|
+
// List published versions of @bongos/core from the registry via `npm view … versions --json`.
|
|
126
|
+
// The package is public (task 1003872), so this needs no credential and no instance-local
|
|
127
|
+
// `.npmrc` — any cwd resolves it. Fail-SOFT: any error returns
|
|
128
128
|
// { ok:false } so the routine skips that instance rather than crashing the whole sweep. `run` +
|
|
129
129
|
// `pkg` are injectable for tests.
|
|
130
130
|
function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env } = {}) {
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
// against `--instance <consumer>` — see docs/recipes for the bootstrap note.
|
|
28
28
|
|
|
29
29
|
const fs = require('node:fs');
|
|
30
|
+
const os = require('node:os');
|
|
30
31
|
const path = require('node:path');
|
|
32
|
+
const crypto = require('node:crypto');
|
|
31
33
|
const { spawnSync } = require('node:child_process');
|
|
32
34
|
const { arg, hasFlag } = require('./cli-lib');
|
|
33
|
-
const { writeNpmrc, hasNpmToken, resolveNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc; resolveNpmToken task 1002754
|
|
34
35
|
|
|
35
36
|
const CORE_PKG = '@bongos/core';
|
|
36
37
|
const ARTIFACT = 'bongos-core';
|
|
@@ -96,9 +97,9 @@ function writePin(instanceDir, vendorRel, fsImpl = fs) {
|
|
|
96
97
|
return prev;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
// Rewrite the consumer package.json to pin an EXACT registry version
|
|
100
|
-
//
|
|
101
|
-
//
|
|
100
|
+
// Rewrite the consumer package.json to pin an EXACT registry version: the registry sibling
|
|
101
|
+
// of writePin. `npm install` then resolves @bongos/core@version from the public registry —
|
|
102
|
+
// no credential, since the package is public (task 1003872). Returns the previous dep string.
|
|
102
103
|
function writeRegistryPin(instanceDir, version, fsImpl = fs) {
|
|
103
104
|
const pkgPath = path.join(instanceDir, 'package.json');
|
|
104
105
|
const pkg = readJson(pkgPath, fsImpl);
|
|
@@ -170,18 +171,133 @@ function dirtyPinFiles(instanceDir, run = spawnSync) {
|
|
|
170
171
|
return (r.stdout || '').split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
// ---- which credential the pin push rides (task 1003081, ADR 0176 follow-up) ----
|
|
175
|
+
//
|
|
176
|
+
// THE BUG THIS EXISTS FOR. A PRIVATE standalone instance's `origin` authenticates with
|
|
177
|
+
// a per-repo deploy key that GitHub registers READ-ONLY (ADR 0176): it can pull, never
|
|
178
|
+
// push. The push leg below went `git push origin HEAD:main` regardless, so on such an
|
|
179
|
+
// instance GitHub refused the pin push, the bump was not durable, and the next
|
|
180
|
+
// pull-deploy `git reset --hard origin/main` + `npm ci` reinstalled the OLD core
|
|
181
|
+
// against an already-migrated DB — the exact silent rollback task 1002712 closed for
|
|
182
|
+
// every other path, back through a warning line nobody read. The scaffold push and the
|
|
183
|
+
// re-provision pin refresh already prefer the owner's stored `repo`-scoped OAuth token
|
|
184
|
+
// over the SSH origin (provision.js resolveOwnerGithubToken + buildCorePinRefreshCommit);
|
|
185
|
+
// the pin push now has the same preference — the token when one is stored, the git
|
|
186
|
+
// remote otherwise — and a refused push is LOUD, naming the read-only key and the fix.
|
|
187
|
+
|
|
188
|
+
// The GitHub owner/name behind `remote`, or null when the remote is not a GitHub
|
|
189
|
+
// repository (there is nothing to push a GitHub token at) or cannot be read. Accepts
|
|
190
|
+
// the plain github.com forms and the shared SSH alias instanceRepoRemote wires
|
|
191
|
+
// (`git@github-deploy:owner/repo.git`) — never another host, so a stored token is
|
|
192
|
+
// never pointed at github.com/<owner>/<repo> for a repo that lives elsewhere.
|
|
193
|
+
function remoteGithubRepo(instanceDir, remote = 'origin', run = spawnSync) {
|
|
194
|
+
const r = run('git', ['remote', 'get-url', remote], { cwd: instanceDir, encoding: 'utf8' });
|
|
195
|
+
if (!r || r.error || r.status !== 0) return null;
|
|
196
|
+
const url = String(r.stdout || '').trim();
|
|
197
|
+
let auth;
|
|
198
|
+
try { auth = require('./provision-repo-auth'); } catch { return null; } // a package without the provisioner has no token path
|
|
199
|
+
const repo = auth.parseTargetRef(url);
|
|
200
|
+
if (!repo) return null;
|
|
201
|
+
const host = (url.match(/^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?([^/:]+)[/:]/i) || [])[1] || '';
|
|
202
|
+
if (host.toLowerCase() !== 'github.com' && url !== auth.instanceRepoRemote(repo)) return null;
|
|
203
|
+
return repo;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The deploy key `remote` authenticates with when the checkout is wired the ADR 0176
|
|
207
|
+
// way (`core.sshCommand … -i <key>` in the repo's own config); null when it is not.
|
|
208
|
+
// What a refused origin push is diagnosed against — GitHub's SSH refusal is a terse
|
|
209
|
+
// one-liner that names no key.
|
|
210
|
+
function pinnedDeployKey(instanceDir, run = spawnSync) {
|
|
211
|
+
const r = run('git', ['config', '--get', 'core.sshCommand'], { cwd: instanceDir, encoding: 'utf8' });
|
|
212
|
+
if (!r || r.error || r.status !== 0) return null;
|
|
213
|
+
const m = String(r.stdout || '').match(/(?:^|\s)-i\s+(\S+)/);
|
|
214
|
+
return m ? m[1] : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Resolve { token, repo } for the pin push, or null → push via the git remote.
|
|
218
|
+
// repo — the remote's GitHub owner/name (remoteGithubRepo);
|
|
219
|
+
// row — provision-repo-auth lookupInstanceRow, used ONLY when its target_ref IS that
|
|
220
|
+
// repo, so another instance's credential is never spent on this checkout;
|
|
221
|
+
// token — provision-repo-auth resolveOwnerGithubToken (deps.getGithubToken / deps.db
|
|
222
|
+
// are its test seams), the same resolver the scaffold push uses.
|
|
223
|
+
// Every miss is the origin path, not an error, and the token is never logged. The row
|
|
224
|
+
// lookup and the token read BOTH live in the leaf rather than here because their default
|
|
225
|
+
// requires are computed, and this file is inside the merge resolver's executed closure
|
|
226
|
+
// with a pinned computed-require count of its own (task 1003081).
|
|
227
|
+
async function resolvePinPushAuth({ instanceDir, remote = 'origin' }, deps = {}, run = spawnSync) {
|
|
228
|
+
const log = deps.log || console.log;
|
|
229
|
+
const repo = remoteGithubRepo(instanceDir, remote, run);
|
|
230
|
+
if (!repo) return null;
|
|
231
|
+
const auth = require('./provision-repo-auth');
|
|
232
|
+
const inst = await auth.lookupInstanceRow(instanceDir, deps);
|
|
233
|
+
if (!inst) return null;
|
|
234
|
+
const rowRepo = auth.parseTargetRef(inst.target_ref);
|
|
235
|
+
const same = (a, b) => a && b && a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
|
|
236
|
+
if (!same(rowRepo, repo)) {
|
|
237
|
+
log(` • pin push: the provisioning row for ${path.basename(instanceDir)} names ${inst.target_ref}, not ${remote}'s ${repo.owner}/${repo.name} — its owner's token is not for this checkout; pushing via ${remote}`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const token = await auth.resolveOwnerGithubToken(inst, { ...deps, log }, { tag: '[pin-push]' });
|
|
241
|
+
if (!token) {
|
|
242
|
+
log(` • pin push: no owner GitHub token stored for ${repo.owner}/${repo.name} — pushing via ${remote}`);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return { token, repo };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The pin push over the owner's token: a chmod-600 credential-store file in the temp
|
|
249
|
+
// dir carries the secret, git reads it through `credential.helper=store`, and the file
|
|
250
|
+
// is removed in `finally` whatever the push did — the token is never in argv (ps,
|
|
251
|
+
// journald) and never in a log line. provision.js buildCorePinRefreshCommit's shape,
|
|
252
|
+
// as argv instead of a shell string.
|
|
253
|
+
function pushPinViaToken({ instanceDir, token, repo, target }, run = spawnSync) {
|
|
254
|
+
const credFile = path.join(os.tmpdir(), `bongos-gitcred-pin-${process.pid}-${crypto.randomBytes(6).toString('hex')}`);
|
|
255
|
+
try {
|
|
256
|
+
fs.writeFileSync(credFile, `https://x-access-token:${token}@github.com\n`, { mode: 0o600 });
|
|
257
|
+
return run('git', ['-c', `credential.helper=store --file=${credFile}`, 'push', `https://x-access-token@github.com/${repo.owner}/${repo.name}.git`, target], {
|
|
258
|
+
cwd: instanceDir, encoding: 'utf8',
|
|
259
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // a rejected credential must fail, not sit at a password prompt
|
|
260
|
+
});
|
|
261
|
+
} finally {
|
|
262
|
+
fs.rmSync(credFile, { force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// What git said on stderr, for a refusal line — GitHub's own reason ("ERROR: The key you
|
|
267
|
+
// are authenticating with has been marked as read only", "remote: Write access to
|
|
268
|
+
// repository not granted") is the fact an operator wants beside the diagnosis, and it
|
|
269
|
+
// is never the LAST line: git closes with its generic `fatal:`. Prefer the server's
|
|
270
|
+
// line, fall back to the last one.
|
|
271
|
+
function gitSaid(r) {
|
|
272
|
+
const lines = String((r && r.stderr) || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
273
|
+
const server = lines.filter((l) => /^(ERROR:|remote:)/i.test(l));
|
|
274
|
+
const said = server.length ? server[server.length - 1] : lines[lines.length - 1];
|
|
275
|
+
return said ? ` (git: ${said})` : '';
|
|
276
|
+
}
|
|
277
|
+
|
|
173
278
|
// `push: false` is CO-TENANT MODE (task 1003521). The reasoning above assumes the repo
|
|
174
279
|
// of record is OURS; on a provisioned instance it is the customer's own GitHub repo, so
|
|
175
280
|
// pushing there is both wrong and uncredentialed. Such an instance also has no
|
|
176
281
|
// pull-deploy timer, so nothing will `reset --hard` the pin away — the commit alone
|
|
177
282
|
// makes it durable, and leaves the tree clean for the customer's own pulls.
|
|
178
|
-
|
|
283
|
+
//
|
|
284
|
+
// `pushAuth` ({ token, repo }, from resolvePinPushAuth) makes the push ride the owner's
|
|
285
|
+
// GitHub token over HTTPS (task 1003081); null pushes via `remote`. The result carries
|
|
286
|
+
// `via: 'owner-token'` on the token path and `refused` ('read-only-deploy-key' |
|
|
287
|
+
// 'owner-token') with a plain-sentence `error` when GitHub turned the push away.
|
|
288
|
+
function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = true, remote = 'origin', dryRun = false, pushAuth = null },
|
|
179
289
|
run = spawnSync, log = console.log, err = console.error) {
|
|
180
290
|
const dirty = dirtyPinFiles(instanceDir, run);
|
|
181
291
|
if (dirty === null) return { applicable: false, reason: 'not a git repo' };
|
|
182
292
|
if (!dirty.length) return { applicable: false, reason: 'pin files already match the repo' };
|
|
183
293
|
|
|
184
294
|
const branch = gitCurrentBranch(instanceDir, run);
|
|
295
|
+
const target = `HEAD:${branch || 'main'}`;
|
|
296
|
+
// Single-quote the interpolated values. Nothing here executes these strings — they
|
|
297
|
+
// are printed for a human to paste — but an unquoted branch or remote carrying shell
|
|
298
|
+
// metacharacters would misbehave once pasted, so emit them paste-safe.
|
|
299
|
+
const q = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
300
|
+
const at = `git -C ${q(instanceDir)}`;
|
|
185
301
|
|
|
186
302
|
if (!commitPin) {
|
|
187
303
|
if (!push) {
|
|
@@ -198,12 +314,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
198
314
|
err(' On a pull-deploy instance the next push to the repo of record makes deploy.sh');
|
|
199
315
|
err(` 'git reset --hard ${remote}/${branch || 'main'}', which REVERTS this pin and lets npm ci`);
|
|
200
316
|
err(` reinstall ${fromVersion || 'the previous core'}. Re-run with --commit-pin, or commit + push:`);
|
|
201
|
-
|
|
202
|
-
// printed for a human to paste — but an unquoted branch or remote carrying shell
|
|
203
|
-
// metacharacters would misbehave once pasted, so emit it paste-safe.
|
|
204
|
-
const q = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
205
|
-
const at = `git -C ${q(instanceDir)}`;
|
|
206
|
-
err(` ${at} add ${PIN_FILES.map(q).join(' ')} && ${at} commit -m ${q(`pin @bongos/core ${toVersion}`)} && ${at} push ${q(remote)} ${q(`HEAD:${branch || 'main'}`)}`);
|
|
317
|
+
err(` ${at} add ${PIN_FILES.map(q).join(' ')} && ${at} commit -m ${q(`pin @bongos/core ${toVersion}`)} && ${at} push ${q(remote)} ${q(target)}`);
|
|
207
318
|
return { applicable: true, committed: false, pushed: false, dirty, warned: true };
|
|
208
319
|
}
|
|
209
320
|
|
|
@@ -232,11 +343,62 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
232
343
|
return { applicable: true, committed: true, pushed: false, dirty, pushSkipped: true };
|
|
233
344
|
}
|
|
234
345
|
|
|
235
|
-
|
|
346
|
+
// Committed but not pushed is still doomed: deploy.sh resets to the REMOTE. So every
|
|
347
|
+
// refusal below is LOUD (task 1003081) — the pull-deploy consequence, the diagnosis,
|
|
348
|
+
// and a by-hand push that can actually write.
|
|
349
|
+
const revert = () => err(` The next pull-deploy 'git reset --hard ${remote}/${branch || 'main'}' + npm ci then reinstalls ${fromVersion || 'the previous core'}.`);
|
|
350
|
+
|
|
351
|
+
if (pushAuth && pushAuth.token && pushAuth.repo) {
|
|
352
|
+
const { repo } = pushAuth;
|
|
353
|
+
let pushed = null;
|
|
354
|
+
try {
|
|
355
|
+
pushed = pushPinViaToken({ instanceDir, token: pushAuth.token, repo, target }, run);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
// The credential file could not even be written (an unwritable temp dir): say so
|
|
358
|
+
// and fall through to the remote — a public origin may still take it.
|
|
359
|
+
err(` ! could not stage the owner's GitHub credential (${e && e.message}) — pushing via ${remote} instead`);
|
|
360
|
+
}
|
|
361
|
+
if (pushed && !pushed.error && pushed.status === 0) {
|
|
362
|
+
log(` ✓ pushed the pin to ${repo.owner}/${repo.name}@${branch || 'main'} via the owner's GitHub token — a pull-deploy reset can no longer revert it`);
|
|
363
|
+
return { applicable: true, committed: true, pushed: true, dirty, via: 'owner-token' };
|
|
364
|
+
}
|
|
365
|
+
if (pushed) {
|
|
366
|
+
err(` ! PIN NOT PUSHED — GitHub refused the push over the owner's GitHub token, so this bump is NOT durable.`);
|
|
367
|
+
err(` The stored token could not write ${repo.owner}/${repo.name}: it may have expired (they live an hour), or`);
|
|
368
|
+
err(` lack the \`repo\` scope (\`public_repo\` cannot write a private repository — ADR 0155).${gitSaid(pushed)}`);
|
|
369
|
+
revert();
|
|
370
|
+
err(' Fix: have the instance owner reconnect GitHub with repository access, then re-run `bongos upgrade --commit-pin`;');
|
|
371
|
+
err(' or push the commit yourself with a credential that can write:');
|
|
372
|
+
err(` ${at} push ${q(`https://github.com/${repo.owner}/${repo.name}.git`)} ${q(target)}`);
|
|
373
|
+
return {
|
|
374
|
+
applicable: true, committed: true, pushed: false, dirty, refused: 'owner-token',
|
|
375
|
+
error: `GitHub refused the pin push over the owner's GitHub token (expired, or missing the repo scope) — the pin is committed locally but not on ${repo.owner}/${repo.name}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const pushed = run('git', ['push', remote, target], { cwd: instanceDir, encoding: 'utf8' });
|
|
236
381
|
if (!pushed || pushed.error || pushed.status !== 0) {
|
|
237
|
-
|
|
382
|
+
const key = pinnedDeployKey(instanceDir, run);
|
|
383
|
+
if (key) {
|
|
384
|
+
// A wired private instance (ADR 0176): the key can pull, never push, and no owner
|
|
385
|
+
// token was stored to push with instead — the bug this leg exists for.
|
|
386
|
+
const repo = remoteGithubRepo(instanceDir, remote, run);
|
|
387
|
+
err(` ! PIN NOT PUSHED — GitHub refused the push to ${remote}, so this bump is NOT durable.`);
|
|
388
|
+
err(` ${remote} authenticates with the deploy key at ${key}, which is registered READ-ONLY on the`);
|
|
389
|
+
err(` repository (ADR 0176): it can pull, never push. No owner GitHub token is stored to push with instead.${gitSaid(pushed)}`);
|
|
390
|
+
revert();
|
|
391
|
+
err(' Fix: have the instance owner reconnect GitHub with repository access (that stores a short-lived repo token),');
|
|
392
|
+
err(` then re-run \`bongos upgrade --commit-pin\` where that token is readable — the control plane, with --instance ${q(instanceDir)};`);
|
|
393
|
+
err(' or push the commit yourself with a credential that can write:');
|
|
394
|
+
err(` ${at} push ${q(`https://github.com/${repo ? `${repo.owner}/${repo.name}` : '<owner>/<repo>'}.git`)} ${q(target)}`);
|
|
395
|
+
return {
|
|
396
|
+
applicable: true, committed: true, pushed: false, dirty, refused: 'read-only-deploy-key',
|
|
397
|
+
error: `GitHub refused the pin push: ${remote} authenticates with the read-only deploy key at ${key}, and no owner GitHub token is stored to push with — the pin is committed locally but not on the remote`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
238
400
|
err(` ! commit landed locally but the push to ${remote} FAILED — this bump is still not durable.`);
|
|
239
|
-
err(` Push it: git -C ${instanceDir} push ${remote}
|
|
401
|
+
err(` Push it: git -C ${instanceDir} push ${remote} ${target}`);
|
|
240
402
|
return { applicable: true, committed: true, pushed: false, dirty, error: 'git push failed' };
|
|
241
403
|
}
|
|
242
404
|
log(` ✓ pushed the pin to ${remote}/${branch || 'main'} — a pull-deploy reset can no longer revert it`);
|
|
@@ -315,13 +477,13 @@ function reportModulePreflight(mods, targetVersion, { log = console.log, err = c
|
|
|
315
477
|
return lines;
|
|
316
478
|
}
|
|
317
479
|
|
|
318
|
-
function npmInstall(instanceDir, run = spawnSync
|
|
480
|
+
function npmInstall(instanceDir, run = spawnSync) {
|
|
319
481
|
// `npm install` (NOT `npm ci`): the pin just changed, so the lockfile must be regenerated from the
|
|
320
482
|
// new tarball; `npm ci` would refuse on the package.json↔lock divergence.
|
|
321
|
-
//
|
|
322
|
-
//
|
|
483
|
+
// This used to take an extraEnv carrying a resolved npm credential, because the instance
|
|
484
|
+
// `.npmrc`'s env expansion could shadow a valid user-level one (task 1002754). The core is
|
|
485
|
+
// public and writes no `.npmrc`, so there is no credential to resolve (task 1003878).
|
|
323
486
|
const opts = { cwd: instanceDir, stdio: 'inherit' };
|
|
324
|
-
if (extraEnv) opts.env = { ...process.env, ...extraEnv };
|
|
325
487
|
const r = run('npm', ['install', '--no-audit', '--no-fund'], opts);
|
|
326
488
|
return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
|
|
327
489
|
}
|
|
@@ -820,11 +982,8 @@ async function rollback({ instanceDir, snapshot, fromVersion, toVersion, opts },
|
|
|
820
982
|
try { restore(instanceDir, snapshot.prevPin, fsImpl); result.restoreOk = true; log(` ✓ restored pin → ${snapshot.prevPin}`); }
|
|
821
983
|
catch (e) { result.restoreOk = false; err(` ! could not restore pin (${e.message}) — package.json may need manual repair`); }
|
|
822
984
|
|
|
823
|
-
// 2. reinstall the previous core.
|
|
824
|
-
|
|
825
|
-
// must never fail on the env-shadowing trap the forward install just avoided.
|
|
826
|
-
const rbCred = resolveNpmToken();
|
|
827
|
-
const inst = npmInstall(instanceDir, run, rbCred.source === 'user-npmrc' ? { NPM_TOKEN: rbCred.token } : null);
|
|
985
|
+
// 2. reinstall the previous core.
|
|
986
|
+
const inst = npmInstall(instanceDir, run);
|
|
828
987
|
result.reinstallOk = !!inst.ok;
|
|
829
988
|
if (inst.ok) log(' ✓ reinstalled previous core');
|
|
830
989
|
else err(` ! reinstall of previous core failed (${inst.reason || 'exit ' + inst.code}) — the instance may need manual repair`);
|
|
@@ -931,24 +1090,16 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
931
1090
|
return { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service, but no --health-url was given.\n Pass --health-url <url> to confirm the new core serves after restart, or --no-health-check to explicitly skip the confirmation.' };
|
|
932
1091
|
}
|
|
933
1092
|
|
|
934
|
-
// 2. bump the pin. Two mutually exclusive core sources
|
|
935
|
-
// REGISTRY (--registry): pin @bongos/core@<version>
|
|
936
|
-
// the
|
|
1093
|
+
// 2. bump the pin. Two mutually exclusive core sources:
|
|
1094
|
+
// REGISTRY (--registry): pin @bongos/core@<version> and let `npm install` pull it from
|
|
1095
|
+
// the public registry. No credential — the package is public (task 1003872), which
|
|
1096
|
+
// also retired the whole resolve-a-token-and-inject-it dance (task 1002754/1003878).
|
|
937
1097
|
// VENDORED (default, --from <tgz>): copy the .tgz into vendor/ + rewrite the file: dep.
|
|
938
|
-
let installEnv = null;
|
|
939
1098
|
if (opts.registry) {
|
|
940
1099
|
if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
|
|
941
|
-
// task 1002754 — resolve the credential HERE instead of demanding a hand-exported
|
|
942
|
-
// env var: env NPM_TOKEN wins, else the user-level ~/.npmrc's literal token is
|
|
943
|
-
// injected into the install's env so the instance `.npmrc`'s `${NPM_TOKEN}`
|
|
944
|
-
// expansion (which OUTRANKS the user-level key and expands EMPTY when unset)
|
|
945
|
-
// can never shadow a working credential again.
|
|
946
|
-
const cred = resolveNpmToken();
|
|
947
|
-
if (!cred.token) log(' ! no NPM_TOKEN in env and no literal token in ~/.npmrc — `npm install` will 404 on the PRIVATE @bongos/core until a read-only automation token is provided (ADR 0134).');
|
|
948
|
-
else if (cred.source === 'user-npmrc') { installEnv = { NPM_TOKEN: cred.token }; log(' ✓ registry credential: inherited from the user-level ~/.npmrc (no NPM_TOKEN export needed — task 1002754)'); }
|
|
949
1100
|
const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
|
|
950
|
-
if (!dryRun) {
|
|
951
|
-
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${
|
|
1101
|
+
if (!dryRun) { snapshot = { prevPin, prevVersion: fromVersion }; }
|
|
1102
|
+
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${prevPin ? ` (was ${prevPin})` : ''}`);
|
|
952
1103
|
} else {
|
|
953
1104
|
let vendorRel = opts.pinPath || (opts.from ? null : vendorRelFor(targetVersion));
|
|
954
1105
|
if (opts.from) {
|
|
@@ -967,7 +1118,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
967
1118
|
if (dryRun) { log(' [dry-run] would: npm install → npm run migrate → materialize .claude → regen API docs+client → restart → verify'); return { ok: true, dryRun: true, fromVersion, toVersion: targetVersion }; }
|
|
968
1119
|
|
|
969
1120
|
// 3. npm install.
|
|
970
|
-
const inst = npmInstall(instanceDir, run
|
|
1121
|
+
const inst = npmInstall(instanceDir, run);
|
|
971
1122
|
if (!inst.ok) {
|
|
972
1123
|
const error = `npm install failed (${inst.reason || 'exit ' + inst.code})`;
|
|
973
1124
|
if (rollbackOnFailure && snapshot) {
|
|
@@ -1144,14 +1295,23 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1144
1295
|
|
|
1145
1296
|
// Make the pin durable (task 1002712). Runs LAST — only a verified-healthy
|
|
1146
1297
|
// upgrade is worth persisting, and a rolled-back one returned long before here.
|
|
1298
|
+
//
|
|
1299
|
+
// Which credential the push rides is resolved HERE (async — it may read the token
|
|
1300
|
+
// store) and handed to persistPin, which stays synchronous — and only when a push is
|
|
1301
|
+
// actually coming: --commit-pin without --pin-no-push, not a dry run, and the pin
|
|
1302
|
+
// files really are dirty. Nothing reads a credential it will not spend (task 1003081).
|
|
1303
|
+
const remote = opts.gitRemote || 'origin';
|
|
1304
|
+
const pushComing = !!opts.commitPin && !opts.pinNoPush && !dryRun && (dirtyPinFiles(instanceDir, run) || []).length > 0;
|
|
1305
|
+
const pushAuth = pushComing ? await (deps.resolvePinPushAuth || resolvePinPushAuth)({ instanceDir, remote }, deps, run) : null;
|
|
1147
1306
|
const pin = persistPin({
|
|
1148
1307
|
instanceDir,
|
|
1149
1308
|
fromVersion,
|
|
1150
1309
|
toVersion: targetVersion,
|
|
1151
1310
|
commitPin: opts.commitPin,
|
|
1152
1311
|
push: !opts.pinNoPush,
|
|
1153
|
-
remote
|
|
1312
|
+
remote,
|
|
1154
1313
|
dryRun: opts.dryRun,
|
|
1314
|
+
pushAuth,
|
|
1155
1315
|
}, run, log, err);
|
|
1156
1316
|
|
|
1157
1317
|
const ok = versionOk && healthOk !== false && servedOk !== false;
|
|
@@ -1198,7 +1358,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1198
1358
|
'',
|
|
1199
1359
|
' --to <version> target core version — presence switches from pre-check to a real bump',
|
|
1200
1360
|
' --from <tgz> source tarball to vendor into vendor/ (else it must already be there)',
|
|
1201
|
-
' --registry pull @bongos/core@<--to> from the npm registry (
|
|
1361
|
+
' --registry pull @bongos/core@<--to> from the public npm registry (no token needed) instead of vendoring a tarball; mutually exclusive with --from',
|
|
1202
1362
|
' --instance <dir> consumer instance root (default: cwd)',
|
|
1203
1363
|
' --service <unit> systemd unit to restart after applying',
|
|
1204
1364
|
' --restart-cmd <cmd> explicit restart command (overrides --service)',
|
|
@@ -1211,6 +1371,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1211
1371
|
' --skip-pin-verify do not verify the integrity pin (tree_sha256 + tarball.sha256) after install',
|
|
1212
1372
|
' --require-pin treat a MISSING reference manifest as fatal (default: warn — a MISMATCH is always fatal)',
|
|
1213
1373
|
' --no-rollback-on-failure do NOT auto-revert on a failed install/pin-verify/migrate/health check (default: auto-rollback ON)',
|
|
1374
|
+
' --commit-pin commit + push package.json/-lock so a pull-deploy reset cannot revert the bump. The push rides the instance owner\'s stored GitHub token when one is stored (a PRIVATE instance\'s origin is a read-only deploy key — ADR 0176), else the git remote (--git-remote <name>, default origin); a refused push is reported loudly with the fix',
|
|
1214
1375
|
' --pin-no-push with --commit-pin: commit the pin but do NOT push it — for an instance whose repo of record is not ours (a provisioned co-tenant), where the push is wrong and no pull-deploy can revert the commit',
|
|
1215
1376
|
' --dry-run show the plan; change nothing',
|
|
1216
1377
|
' --force skip the clean-tree + same-version guards',
|
|
@@ -1233,7 +1394,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1233
1394
|
module.exports = {
|
|
1234
1395
|
runUpgrade, parseArgs, main, rollback,
|
|
1235
1396
|
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
|
|
1236
|
-
vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity,
|
|
1397
|
+
vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity,
|
|
1398
|
+
resolveInstanceDb, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth, deriveVersionUrl, pollServedVersion,
|
|
1399
|
+
resolvePinPushAuth, remoteGithubRepo, pinnedDeployKey, // task 1003081 — the pin push prefers the owner's GitHub token (ADR 0176 follow-up)
|
|
1237
1400
|
readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
|
|
1238
1401
|
CORE_PKG, ARTIFACT, vendorRelFor,
|
|
1239
1402
|
};
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.738'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -97,6 +97,14 @@ test('every computed require inside the closure is one we have accounted for', (
|
|
|
97
97
|
// floor) is lazy and inside a function.
|
|
98
98
|
['scripts/gds/package-core.js', 6], // publish-manifest/mirror-redact/secret-scrub/leak-scan-allowlist/…
|
|
99
99
|
['scripts/gds/upgrade.js', 1], // a `node -e` child, not a require in this process
|
|
100
|
+
// Added by task 1003081: upgrade.js's pin push needs the owner-credential helpers,
|
|
101
|
+
// and requiring provision.js for them would have pulled provision-config/-repo/-units
|
|
102
|
+
// (7/1/1 unaccounted computed requires) into the closure. This LEAF is the whole of
|
|
103
|
+
// that growth: 3 computed requires — src/bongos/db in resolveOwnerGithubToken, and
|
|
104
|
+
// modules/provisioning + src/bongos/pool in lookupInstanceRow — each inside a function
|
|
105
|
+
// body reached only from the pin push, which no generator calls. Its top level requires
|
|
106
|
+
// node builtins only, so it drags nothing else in behind it.
|
|
107
|
+
['scripts/gds/provision-repo-auth.js', 3],
|
|
100
108
|
['scripts/gds/gen-api-docs.js', 1], // require(path.join(CORE_ROOT,'package.json')) — JSON
|
|
101
109
|
]);
|
|
102
110
|
|