@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.
@@ -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
- function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = true, remote = 'origin', dryRun = false },
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
- // Single-quote the interpolated values. Nothing here executes this string — it is
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
- const pushed = run('git', ['push', remote, `HEAD:${branch || 'main'}`], { cwd: instanceDir, encoding: 'utf8' });
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
- // Committed but not pushed is still doomed: deploy.sh resets to the REMOTE.
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} HEAD:${branch || 'main'}`);
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: opts.gitRemote || 'origin',
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, resolveInstanceDb, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth, deriveVersionUrl, pollServedVersion,
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.735'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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
 
@@ -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 2089 / ADR 0134 — the synthesized package publishes PRIVATE by default.
389
- assert.deepEqual(pkg.publishConfig, { access: 'restricted' });
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: private (restricted) publish, dry-run unless live (task 2089 / ADR 0134)', () => {
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, restricted (never public, never a real upload).
395
- assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', 'restricted', '--dry-run']);
396
- // Live: still restricted (private) — the permanent publish is never public here.
397
- assert.deepEqual(publishArgs(tgz, true), ['publish', tgz, '--access', 'restricted']);
398
- // Belt-and-suspenders: `--access public` must never appear on the pre-launch path.
399
- assert.ok(!publishArgs(tgz, true).includes('public'));
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', () => {