@bongos/core 1.19.735 → 1.19.737
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 -21
- package/docs/adr/0134-private-first-npm-distribution.md +1 -1
- package/docs/adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md +53 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +2 -2
- package/scripts/gds/package-core.js +30 -16
- package/scripts/gds/provision-repo-auth.js +106 -0
- package/scripts/gds/provision-repo.js +8 -37
- package/scripts/gds/upgrade.js +187 -12
- package/src/module-api.js +1 -1
- package/tests/executed_closure.mjs +8 -0
- package/tests/package_core.mjs +27 -10
- package/tests/upgrade.mjs +284 -1
package/scripts/gds/upgrade.js
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
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
35
|
const { writeNpmrc, hasNpmToken, resolveNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc; resolveNpmToken task 1002754
|
|
@@ -170,18 +172,133 @@ function dirtyPinFiles(instanceDir, run = spawnSync) {
|
|
|
170
172
|
return (r.stdout || '').split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
// ---- which credential the pin push rides (task 1003081, ADR 0176 follow-up) ----
|
|
176
|
+
//
|
|
177
|
+
// THE BUG THIS EXISTS FOR. A PRIVATE standalone instance's `origin` authenticates with
|
|
178
|
+
// a per-repo deploy key that GitHub registers READ-ONLY (ADR 0176): it can pull, never
|
|
179
|
+
// push. The push leg below went `git push origin HEAD:main` regardless, so on such an
|
|
180
|
+
// instance GitHub refused the pin push, the bump was not durable, and the next
|
|
181
|
+
// pull-deploy `git reset --hard origin/main` + `npm ci` reinstalled the OLD core
|
|
182
|
+
// against an already-migrated DB — the exact silent rollback task 1002712 closed for
|
|
183
|
+
// every other path, back through a warning line nobody read. The scaffold push and the
|
|
184
|
+
// re-provision pin refresh already prefer the owner's stored `repo`-scoped OAuth token
|
|
185
|
+
// over the SSH origin (provision.js resolveOwnerGithubToken + buildCorePinRefreshCommit);
|
|
186
|
+
// the pin push now has the same preference — the token when one is stored, the git
|
|
187
|
+
// remote otherwise — and a refused push is LOUD, naming the read-only key and the fix.
|
|
188
|
+
|
|
189
|
+
// The GitHub owner/name behind `remote`, or null when the remote is not a GitHub
|
|
190
|
+
// repository (there is nothing to push a GitHub token at) or cannot be read. Accepts
|
|
191
|
+
// the plain github.com forms and the shared SSH alias instanceRepoRemote wires
|
|
192
|
+
// (`git@github-deploy:owner/repo.git`) — never another host, so a stored token is
|
|
193
|
+
// never pointed at github.com/<owner>/<repo> for a repo that lives elsewhere.
|
|
194
|
+
function remoteGithubRepo(instanceDir, remote = 'origin', run = spawnSync) {
|
|
195
|
+
const r = run('git', ['remote', 'get-url', remote], { cwd: instanceDir, encoding: 'utf8' });
|
|
196
|
+
if (!r || r.error || r.status !== 0) return null;
|
|
197
|
+
const url = String(r.stdout || '').trim();
|
|
198
|
+
let auth;
|
|
199
|
+
try { auth = require('./provision-repo-auth'); } catch { return null; } // a package without the provisioner has no token path
|
|
200
|
+
const repo = auth.parseTargetRef(url);
|
|
201
|
+
if (!repo) return null;
|
|
202
|
+
const host = (url.match(/^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?([^/:]+)[/:]/i) || [])[1] || '';
|
|
203
|
+
if (host.toLowerCase() !== 'github.com' && url !== auth.instanceRepoRemote(repo)) return null;
|
|
204
|
+
return repo;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The deploy key `remote` authenticates with when the checkout is wired the ADR 0176
|
|
208
|
+
// way (`core.sshCommand … -i <key>` in the repo's own config); null when it is not.
|
|
209
|
+
// What a refused origin push is diagnosed against — GitHub's SSH refusal is a terse
|
|
210
|
+
// one-liner that names no key.
|
|
211
|
+
function pinnedDeployKey(instanceDir, run = spawnSync) {
|
|
212
|
+
const r = run('git', ['config', '--get', 'core.sshCommand'], { cwd: instanceDir, encoding: 'utf8' });
|
|
213
|
+
if (!r || r.error || r.status !== 0) return null;
|
|
214
|
+
const m = String(r.stdout || '').match(/(?:^|\s)-i\s+(\S+)/);
|
|
215
|
+
return m ? m[1] : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Resolve { token, repo } for the pin push, or null → push via the git remote.
|
|
219
|
+
// repo — the remote's GitHub owner/name (remoteGithubRepo);
|
|
220
|
+
// row — provision-repo-auth lookupInstanceRow, used ONLY when its target_ref IS that
|
|
221
|
+
// repo, so another instance's credential is never spent on this checkout;
|
|
222
|
+
// token — provision-repo-auth resolveOwnerGithubToken (deps.getGithubToken / deps.db
|
|
223
|
+
// are its test seams), the same resolver the scaffold push uses.
|
|
224
|
+
// Every miss is the origin path, not an error, and the token is never logged. The row
|
|
225
|
+
// lookup and the token read BOTH live in the leaf rather than here because their default
|
|
226
|
+
// requires are computed, and this file is inside the merge resolver's executed closure
|
|
227
|
+
// with a pinned computed-require count of its own (task 1003081).
|
|
228
|
+
async function resolvePinPushAuth({ instanceDir, remote = 'origin' }, deps = {}, run = spawnSync) {
|
|
229
|
+
const log = deps.log || console.log;
|
|
230
|
+
const repo = remoteGithubRepo(instanceDir, remote, run);
|
|
231
|
+
if (!repo) return null;
|
|
232
|
+
const auth = require('./provision-repo-auth');
|
|
233
|
+
const inst = await auth.lookupInstanceRow(instanceDir, deps);
|
|
234
|
+
if (!inst) return null;
|
|
235
|
+
const rowRepo = auth.parseTargetRef(inst.target_ref);
|
|
236
|
+
const same = (a, b) => a && b && a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
|
|
237
|
+
if (!same(rowRepo, repo)) {
|
|
238
|
+
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}`);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const token = await auth.resolveOwnerGithubToken(inst, { ...deps, log }, { tag: '[pin-push]' });
|
|
242
|
+
if (!token) {
|
|
243
|
+
log(` • pin push: no owner GitHub token stored for ${repo.owner}/${repo.name} — pushing via ${remote}`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { token, repo };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The pin push over the owner's token: a chmod-600 credential-store file in the temp
|
|
250
|
+
// dir carries the secret, git reads it through `credential.helper=store`, and the file
|
|
251
|
+
// is removed in `finally` whatever the push did — the token is never in argv (ps,
|
|
252
|
+
// journald) and never in a log line. provision.js buildCorePinRefreshCommit's shape,
|
|
253
|
+
// as argv instead of a shell string.
|
|
254
|
+
function pushPinViaToken({ instanceDir, token, repo, target }, run = spawnSync) {
|
|
255
|
+
const credFile = path.join(os.tmpdir(), `bongos-gitcred-pin-${process.pid}-${crypto.randomBytes(6).toString('hex')}`);
|
|
256
|
+
try {
|
|
257
|
+
fs.writeFileSync(credFile, `https://x-access-token:${token}@github.com\n`, { mode: 0o600 });
|
|
258
|
+
return run('git', ['-c', `credential.helper=store --file=${credFile}`, 'push', `https://x-access-token@github.com/${repo.owner}/${repo.name}.git`, target], {
|
|
259
|
+
cwd: instanceDir, encoding: 'utf8',
|
|
260
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // a rejected credential must fail, not sit at a password prompt
|
|
261
|
+
});
|
|
262
|
+
} finally {
|
|
263
|
+
fs.rmSync(credFile, { force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// What git said on stderr, for a refusal line — GitHub's own reason ("ERROR: The key you
|
|
268
|
+
// are authenticating with has been marked as read only", "remote: Write access to
|
|
269
|
+
// repository not granted") is the fact an operator wants beside the diagnosis, and it
|
|
270
|
+
// is never the LAST line: git closes with its generic `fatal:`. Prefer the server's
|
|
271
|
+
// line, fall back to the last one.
|
|
272
|
+
function gitSaid(r) {
|
|
273
|
+
const lines = String((r && r.stderr) || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
274
|
+
const server = lines.filter((l) => /^(ERROR:|remote:)/i.test(l));
|
|
275
|
+
const said = server.length ? server[server.length - 1] : lines[lines.length - 1];
|
|
276
|
+
return said ? ` (git: ${said})` : '';
|
|
277
|
+
}
|
|
278
|
+
|
|
173
279
|
// `push: false` is CO-TENANT MODE (task 1003521). The reasoning above assumes the repo
|
|
174
280
|
// of record is OURS; on a provisioned instance it is the customer's own GitHub repo, so
|
|
175
281
|
// pushing there is both wrong and uncredentialed. Such an instance also has no
|
|
176
282
|
// pull-deploy timer, so nothing will `reset --hard` the pin away — the commit alone
|
|
177
283
|
// makes it durable, and leaves the tree clean for the customer's own pulls.
|
|
178
|
-
|
|
284
|
+
//
|
|
285
|
+
// `pushAuth` ({ token, repo }, from resolvePinPushAuth) makes the push ride the owner's
|
|
286
|
+
// GitHub token over HTTPS (task 1003081); null pushes via `remote`. The result carries
|
|
287
|
+
// `via: 'owner-token'` on the token path and `refused` ('read-only-deploy-key' |
|
|
288
|
+
// 'owner-token') with a plain-sentence `error` when GitHub turned the push away.
|
|
289
|
+
function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = true, remote = 'origin', dryRun = false, pushAuth = null },
|
|
179
290
|
run = spawnSync, log = console.log, err = console.error) {
|
|
180
291
|
const dirty = dirtyPinFiles(instanceDir, run);
|
|
181
292
|
if (dirty === null) return { applicable: false, reason: 'not a git repo' };
|
|
182
293
|
if (!dirty.length) return { applicable: false, reason: 'pin files already match the repo' };
|
|
183
294
|
|
|
184
295
|
const branch = gitCurrentBranch(instanceDir, run);
|
|
296
|
+
const target = `HEAD:${branch || 'main'}`;
|
|
297
|
+
// Single-quote the interpolated values. Nothing here executes these strings — they
|
|
298
|
+
// are printed for a human to paste — but an unquoted branch or remote carrying shell
|
|
299
|
+
// metacharacters would misbehave once pasted, so emit them paste-safe.
|
|
300
|
+
const q = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
301
|
+
const at = `git -C ${q(instanceDir)}`;
|
|
185
302
|
|
|
186
303
|
if (!commitPin) {
|
|
187
304
|
if (!push) {
|
|
@@ -198,12 +315,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
198
315
|
err(' On a pull-deploy instance the next push to the repo of record makes deploy.sh');
|
|
199
316
|
err(` 'git reset --hard ${remote}/${branch || 'main'}', which REVERTS this pin and lets npm ci`);
|
|
200
317
|
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'}`)}`);
|
|
318
|
+
err(` ${at} add ${PIN_FILES.map(q).join(' ')} && ${at} commit -m ${q(`pin @bongos/core ${toVersion}`)} && ${at} push ${q(remote)} ${q(target)}`);
|
|
207
319
|
return { applicable: true, committed: false, pushed: false, dirty, warned: true };
|
|
208
320
|
}
|
|
209
321
|
|
|
@@ -232,11 +344,62 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
232
344
|
return { applicable: true, committed: true, pushed: false, dirty, pushSkipped: true };
|
|
233
345
|
}
|
|
234
346
|
|
|
235
|
-
|
|
347
|
+
// Committed but not pushed is still doomed: deploy.sh resets to the REMOTE. So every
|
|
348
|
+
// refusal below is LOUD (task 1003081) — the pull-deploy consequence, the diagnosis,
|
|
349
|
+
// and a by-hand push that can actually write.
|
|
350
|
+
const revert = () => err(` The next pull-deploy 'git reset --hard ${remote}/${branch || 'main'}' + npm ci then reinstalls ${fromVersion || 'the previous core'}.`);
|
|
351
|
+
|
|
352
|
+
if (pushAuth && pushAuth.token && pushAuth.repo) {
|
|
353
|
+
const { repo } = pushAuth;
|
|
354
|
+
let pushed = null;
|
|
355
|
+
try {
|
|
356
|
+
pushed = pushPinViaToken({ instanceDir, token: pushAuth.token, repo, target }, run);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
// The credential file could not even be written (an unwritable temp dir): say so
|
|
359
|
+
// and fall through to the remote — a public origin may still take it.
|
|
360
|
+
err(` ! could not stage the owner's GitHub credential (${e && e.message}) — pushing via ${remote} instead`);
|
|
361
|
+
}
|
|
362
|
+
if (pushed && !pushed.error && pushed.status === 0) {
|
|
363
|
+
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`);
|
|
364
|
+
return { applicable: true, committed: true, pushed: true, dirty, via: 'owner-token' };
|
|
365
|
+
}
|
|
366
|
+
if (pushed) {
|
|
367
|
+
err(` ! PIN NOT PUSHED — GitHub refused the push over the owner's GitHub token, so this bump is NOT durable.`);
|
|
368
|
+
err(` The stored token could not write ${repo.owner}/${repo.name}: it may have expired (they live an hour), or`);
|
|
369
|
+
err(` lack the \`repo\` scope (\`public_repo\` cannot write a private repository — ADR 0155).${gitSaid(pushed)}`);
|
|
370
|
+
revert();
|
|
371
|
+
err(' Fix: have the instance owner reconnect GitHub with repository access, then re-run `bongos upgrade --commit-pin`;');
|
|
372
|
+
err(' or push the commit yourself with a credential that can write:');
|
|
373
|
+
err(` ${at} push ${q(`https://github.com/${repo.owner}/${repo.name}.git`)} ${q(target)}`);
|
|
374
|
+
return {
|
|
375
|
+
applicable: true, committed: true, pushed: false, dirty, refused: 'owner-token',
|
|
376
|
+
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}`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const pushed = run('git', ['push', remote, target], { cwd: instanceDir, encoding: 'utf8' });
|
|
236
382
|
if (!pushed || pushed.error || pushed.status !== 0) {
|
|
237
|
-
|
|
383
|
+
const key = pinnedDeployKey(instanceDir, run);
|
|
384
|
+
if (key) {
|
|
385
|
+
// A wired private instance (ADR 0176): the key can pull, never push, and no owner
|
|
386
|
+
// token was stored to push with instead — the bug this leg exists for.
|
|
387
|
+
const repo = remoteGithubRepo(instanceDir, remote, run);
|
|
388
|
+
err(` ! PIN NOT PUSHED — GitHub refused the push to ${remote}, so this bump is NOT durable.`);
|
|
389
|
+
err(` ${remote} authenticates with the deploy key at ${key}, which is registered READ-ONLY on the`);
|
|
390
|
+
err(` repository (ADR 0176): it can pull, never push. No owner GitHub token is stored to push with instead.${gitSaid(pushed)}`);
|
|
391
|
+
revert();
|
|
392
|
+
err(' Fix: have the instance owner reconnect GitHub with repository access (that stores a short-lived repo token),');
|
|
393
|
+
err(` then re-run \`bongos upgrade --commit-pin\` where that token is readable — the control plane, with --instance ${q(instanceDir)};`);
|
|
394
|
+
err(' or push the commit yourself with a credential that can write:');
|
|
395
|
+
err(` ${at} push ${q(`https://github.com/${repo ? `${repo.owner}/${repo.name}` : '<owner>/<repo>'}.git`)} ${q(target)}`);
|
|
396
|
+
return {
|
|
397
|
+
applicable: true, committed: true, pushed: false, dirty, refused: 'read-only-deploy-key',
|
|
398
|
+
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`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
238
401
|
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}
|
|
402
|
+
err(` Push it: git -C ${instanceDir} push ${remote} ${target}`);
|
|
240
403
|
return { applicable: true, committed: true, pushed: false, dirty, error: 'git push failed' };
|
|
241
404
|
}
|
|
242
405
|
log(` ✓ pushed the pin to ${remote}/${branch || 'main'} — a pull-deploy reset can no longer revert it`);
|
|
@@ -1144,14 +1307,23 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1144
1307
|
|
|
1145
1308
|
// Make the pin durable (task 1002712). Runs LAST — only a verified-healthy
|
|
1146
1309
|
// upgrade is worth persisting, and a rolled-back one returned long before here.
|
|
1310
|
+
//
|
|
1311
|
+
// Which credential the push rides is resolved HERE (async — it may read the token
|
|
1312
|
+
// store) and handed to persistPin, which stays synchronous — and only when a push is
|
|
1313
|
+
// actually coming: --commit-pin without --pin-no-push, not a dry run, and the pin
|
|
1314
|
+
// files really are dirty. Nothing reads a credential it will not spend (task 1003081).
|
|
1315
|
+
const remote = opts.gitRemote || 'origin';
|
|
1316
|
+
const pushComing = !!opts.commitPin && !opts.pinNoPush && !dryRun && (dirtyPinFiles(instanceDir, run) || []).length > 0;
|
|
1317
|
+
const pushAuth = pushComing ? await (deps.resolvePinPushAuth || resolvePinPushAuth)({ instanceDir, remote }, deps, run) : null;
|
|
1147
1318
|
const pin = persistPin({
|
|
1148
1319
|
instanceDir,
|
|
1149
1320
|
fromVersion,
|
|
1150
1321
|
toVersion: targetVersion,
|
|
1151
1322
|
commitPin: opts.commitPin,
|
|
1152
1323
|
push: !opts.pinNoPush,
|
|
1153
|
-
remote
|
|
1324
|
+
remote,
|
|
1154
1325
|
dryRun: opts.dryRun,
|
|
1326
|
+
pushAuth,
|
|
1155
1327
|
}, run, log, err);
|
|
1156
1328
|
|
|
1157
1329
|
const ok = versionOk && healthOk !== false && servedOk !== false;
|
|
@@ -1211,6 +1383,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1211
1383
|
' --skip-pin-verify do not verify the integrity pin (tree_sha256 + tarball.sha256) after install',
|
|
1212
1384
|
' --require-pin treat a MISSING reference manifest as fatal (default: warn — a MISMATCH is always fatal)',
|
|
1213
1385
|
' --no-rollback-on-failure do NOT auto-revert on a failed install/pin-verify/migrate/health check (default: auto-rollback ON)',
|
|
1386
|
+
' --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
1387
|
' --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
1388
|
' --dry-run show the plan; change nothing',
|
|
1216
1389
|
' --force skip the clean-tree + same-version guards',
|
|
@@ -1233,7 +1406,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1233
1406
|
module.exports = {
|
|
1234
1407
|
runUpgrade, parseArgs, main, rollback,
|
|
1235
1408
|
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
|
|
1236
|
-
vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity,
|
|
1409
|
+
vendorTarball, gitTreeClean, persistPin, dirtyPinFiles, gitCurrentBranch, PIN_FILES, preflightDbIdentity,
|
|
1410
|
+
resolveInstanceDb, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth, deriveVersionUrl, pollServedVersion,
|
|
1411
|
+
resolvePinPushAuth, remoteGithubRepo, pinnedDeployKey, // task 1003081 — the pin push prefers the owner's GitHub token (ADR 0176 follow-up)
|
|
1237
1412
|
readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
|
|
1238
1413
|
CORE_PKG, ARTIFACT, vendorRelFor,
|
|
1239
1414
|
};
|
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.737'; // 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
|
|
package/tests/package_core.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { strict as assert } from 'node:assert';
|
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import zlib from 'node:zlib';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { tmpdir } from 'node:os';
|
|
21
21
|
import { join, dirname } from 'node:path';
|
|
22
22
|
|
|
@@ -385,18 +385,35 @@ t('buildPackageJson: prunes to express+pg+pino, keeps the source ranges + the un
|
|
|
385
385
|
assert.equal(pkg.exports['./module-loader/loader'], './src/module-loader/loader.js');
|
|
386
386
|
assert.equal(pkg.exports['./modules'], './src/modules.js');
|
|
387
387
|
assert.equal(pkg.exports['./module-seams'], './src/module-seams.js');
|
|
388
|
-
// task
|
|
389
|
-
|
|
388
|
+
// task 1003872 / ADR 0283 — the synthesized package publishes PUBLIC
|
|
389
|
+
// (superseding ADR 0134's pre-launch `restricted`).
|
|
390
|
+
assert.deepEqual(pkg.publishConfig, { access: 'public' });
|
|
390
391
|
});
|
|
391
392
|
|
|
392
|
-
t('publishArgs:
|
|
393
|
+
t('publishArgs: public publish, dry-run unless live (task 1003872 / ADR 0283)', () => {
|
|
393
394
|
const tgz = 'dist/bongos-core-1.14.0.tgz';
|
|
394
|
-
// Default: dry-run
|
|
395
|
-
assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', '
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
assert.ok(!publishArgs(tgz, true).includes('
|
|
395
|
+
// Default: dry-run — building must never be a real upload, whatever the access.
|
|
396
|
+
assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', 'public', '--dry-run']);
|
|
397
|
+
assert.deepEqual(publishArgs(tgz, true), ['publish', tgz, '--access', 'public']);
|
|
398
|
+
// The posture is stated at the call site AND in publishConfig, so neither one
|
|
399
|
+
// drifting alone can silently take the package back to private.
|
|
400
|
+
assert.ok(!publishArgs(tgz, true).includes('restricted'));
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// THE PUBLISH LANE IS THE PATH THAT ACTUALLY RUNS (task 1003872). package-core's
|
|
404
|
+
// own `--publish` is the manual owner path; CI publishes the tarball itself, with
|
|
405
|
+
// its own `--access` flag. Flipping only package-core.js would have left the real
|
|
406
|
+
// releases restricted — the same shape as task 1003870, where the workflow did not
|
|
407
|
+
// arm the gate its own header comment claimed.
|
|
408
|
+
t('the publish workflow publishes PUBLIC, matching publishArgs', () => {
|
|
409
|
+
const yml = readFileSync(new URL('../.github/workflows/publish.yml', import.meta.url), 'utf8');
|
|
410
|
+
const m = yml.match(/npm publish "\$TGZ" --access (\w+)/);
|
|
411
|
+
assert.ok(m, 'publish.yml must publish the packaged tarball with an explicit --access');
|
|
412
|
+
assert.equal(m[1], 'public', 'the lane that actually releases must publish public, not restricted');
|
|
413
|
+
assert.ok(
|
|
414
|
+
!/--access restricted/.test(yml),
|
|
415
|
+
'no restricted publish may survive anywhere in the lane',
|
|
416
|
+
);
|
|
400
417
|
});
|
|
401
418
|
|
|
402
419
|
t('buildPackageJson: omits `overrides` when the source has none', () => {
|