@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/upgrade-migrate.js — WHICH command `bongos upgrade` migrates an instance
|
|
4
|
+
// with (task 1004274, found preparing the task 1004047 catch-up pass). Kept out of
|
|
5
|
+
// upgrade.js, which sits at the 1500-line cap.
|
|
6
|
+
//
|
|
7
|
+
// THE GAP. upgrade.js migrated with `npm run migrate`, and that only works where the
|
|
8
|
+
// instance's package.json DECLARES a migrate script. A greenfield scaffold always does
|
|
9
|
+
// (init.js buildInstancePackageJson writes INSTANCE_MIGRATE_SCRIPT), but the ADOPT layer
|
|
10
|
+
// does not: init.js mergeCoreDependency adds the core dependency and nothing else, and
|
|
11
|
+
// for a repo that had no package.json of its own it mints `{ name, private,
|
|
12
|
+
// dependencies }`. So an adopted project that is not a JavaScript app has no scripts at
|
|
13
|
+
// all. There `npm run migrate` answers "Missing script" — AFTER the pin has moved and
|
|
14
|
+
// the new core is installed — and the upgrade rolls itself back, restarting someone's
|
|
15
|
+
// live app to change nothing. mercury (a Python/Flask project; its package.json is
|
|
16
|
+
// exactly name/private/dependencies) is that shape, and it is why the catch-up pass
|
|
17
|
+
// could not reach it. The dry run could not warn either, because it never looked.
|
|
18
|
+
//
|
|
19
|
+
// THE FALLBACK IS NOT A NEW MIGRATION PATH. It is the command a scaffold's own script
|
|
20
|
+
// IS, and the one the control plane ran to create that instance's database at stand-up
|
|
21
|
+
// (provision-repo.js standaloneMigrateCmd): the installed core's scripts/migrate.sh,
|
|
22
|
+
// run from the instance root. A declared script still wins, whatever it says — an
|
|
23
|
+
// owner who wrote their own migrate step meant it — and `--skip-migrate` stays the
|
|
24
|
+
// only way to run no migration at all, which on a real jump leaves the new code
|
|
25
|
+
// running against the old schema.
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
// Relative to the instance root, POSIX on purpose: it is an argument to bash, and it
|
|
31
|
+
// must stay byte-identical to the scaffold's script (tests/upgrade_migrate.mjs pins it
|
|
32
|
+
// against init.js INSTANCE_MIGRATE_SCRIPT).
|
|
33
|
+
const CORE_MIGRATE_SCRIPT = 'node_modules/@bongos/core/scripts/migrate.sh';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How this instance is migrated: its own declared script, or the core's migrate.sh.
|
|
37
|
+
*
|
|
38
|
+
* Only a package.json we could READ, and that names no migrate script, earns the
|
|
39
|
+
* fallback. A missing or unparseable one keeps the old command, which then fails the
|
|
40
|
+
* way it always did — guessing at a file this could not parse would be worse.
|
|
41
|
+
*
|
|
42
|
+
* `env` exists only on the fallback, and only to hand migrate.sh what `npm run` would:
|
|
43
|
+
* migrate.sh finds the instance from INIT_CWD before its own cwd (task 2180), and npm
|
|
44
|
+
* sets it to the directory the script runs in. Launched directly, a stray INIT_CWD
|
|
45
|
+
* inherited from whoever started this upgrade would aim it at the wrong project.
|
|
46
|
+
*
|
|
47
|
+
* `note` is a pre-flight line, so the dry run says which command the move will run.
|
|
48
|
+
*/
|
|
49
|
+
function migratePlan(instanceDir, { fsImpl = fs, env = process.env } = {}) {
|
|
50
|
+
let pkg = null;
|
|
51
|
+
try { pkg = JSON.parse(fsImpl.readFileSync(path.join(instanceDir, 'package.json'), 'utf8')); } catch { /* absent or unreadable */ }
|
|
52
|
+
const scripts = pkg && typeof pkg.scripts === 'object' && pkg.scripts ? pkg.scripts : {};
|
|
53
|
+
const declared = typeof scripts.migrate === 'string' && scripts.migrate.trim() !== '';
|
|
54
|
+
if (!pkg || declared) {
|
|
55
|
+
return { via: 'npm-script', cmd: 'npm', args: ['run', 'migrate'], label: 'npm run migrate', env: null, note: null };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
via: 'core-script',
|
|
59
|
+
cmd: 'bash',
|
|
60
|
+
args: [CORE_MIGRATE_SCRIPT],
|
|
61
|
+
label: `bash ${CORE_MIGRATE_SCRIPT}`,
|
|
62
|
+
env: { ...env, INIT_CWD: instanceDir },
|
|
63
|
+
note: 'migrate: this project declares no migrate script (an adopted repo), so the core\'s own scripts/migrate.sh will run — the command a scaffold\'s script runs',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { migratePlan, CORE_MIGRATE_SCRIPT };
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// pre-flight clean git tree + module-compat vs the TARGET core version + the artist gate (ADR 0241)
|
|
9
9
|
// → bump pin vendor the new core .tgz, rewrite the package.json `file:` dependency
|
|
10
10
|
// → npm install node_modules/@bongos/core becomes the new version
|
|
11
|
-
// → migrate `npm run migrate`
|
|
11
|
+
// → migrate `npm run migrate` (no such script: the core's migrate.sh itself, upgrade-migrate.js) applies additive `core_*` migrations
|
|
12
12
|
// → materialize refresh .claude/ skills+settings+hooks from the new core (claude-materialize / W3)
|
|
13
13
|
// → restart restart the instance's service so the new core is live
|
|
14
14
|
// → verify installed package version == target (+ optional /healthz + ledger row)
|
|
@@ -246,7 +246,7 @@ async function resolvePinPushAuth({ instanceDir, remote = 'origin' }, deps = {},
|
|
|
246
246
|
if (!repo) return null;
|
|
247
247
|
const auth = require('./provision-repo-auth');
|
|
248
248
|
const inst = await auth.lookupInstanceRow(instanceDir, deps);
|
|
249
|
-
if (!inst) return null;
|
|
249
|
+
if (!inst) { const key = pinnedDeployKey(instanceDir, run); if (key) log(auth.missingRowNote(key, remote)); return null; } // task 1004065
|
|
250
250
|
const rowRepo = auth.parseTargetRef(inst.target_ref);
|
|
251
251
|
const same = (a, b) => a && b && a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
|
|
252
252
|
if (!same(rowRepo, repo)) {
|
|
@@ -407,7 +407,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
407
407
|
const repo = remoteGithubRepo(instanceDir, remote, run);
|
|
408
408
|
err(` ! PIN NOT PUSHED — GitHub refused the push to ${remote}, so this bump is NOT durable.`);
|
|
409
409
|
err(` ${remote} authenticates with the deploy key at ${key}, which is registered READ-ONLY on the`);
|
|
410
|
-
err(` repository (ADR 0176): it can pull, never push. No owner GitHub token
|
|
410
|
+
err(` repository (ADR 0176): it can pull, never push. No owner GitHub token was available to push with instead (none is stored, or — on a hosted project's own box — the hub's record of it is not readable from here).${gitSaid(pushed)}`);
|
|
411
411
|
revert();
|
|
412
412
|
err(' Fix: have the instance owner reconnect GitHub with repository access (that stores a short-lived repo token),');
|
|
413
413
|
err(` then re-run \`bongos upgrade --commit-pin\` where that token is readable — the control plane, with --instance ${q(instanceDir)};`);
|
|
@@ -415,7 +415,7 @@ function persistPin({ instanceDir, fromVersion, toVersion, commitPin, push = tru
|
|
|
415
415
|
err(` ${at} push ${q(`https://github.com/${repo ? `${repo.owner}/${repo.name}` : '<owner>/<repo>'}.git`)} ${q(target)}`);
|
|
416
416
|
return {
|
|
417
417
|
applicable: true, committed: true, pushed: false, dirty, refused: 'read-only-deploy-key',
|
|
418
|
-
error: `GitHub refused the pin push: ${remote} authenticates with the read-only deploy key at ${key}, and no owner GitHub token
|
|
418
|
+
error: `GitHub refused the pin push: ${remote} authenticates with the read-only deploy key at ${key}, and no owner GitHub token was available to push with — the pin is committed locally but not on the remote`,
|
|
419
419
|
};
|
|
420
420
|
}
|
|
421
421
|
err(` ! commit landed locally but the push to ${remote} FAILED — this bump is still not durable.`);
|
|
@@ -538,7 +538,8 @@ function preflightDbIdentity({ instanceDir, db }, run = spawnSync) {
|
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
function runMigrate(instanceDir, run = spawnSync) {
|
|
541
|
-
const
|
|
541
|
+
const { cmd, args, env } = require('./upgrade-migrate').migratePlan(instanceDir); // its own migrate script, else the core's migrate.sh (task 1004274)
|
|
542
|
+
const r = run(cmd, args, { cwd: instanceDir, stdio: 'inherit', ...(env ? { env } : {}) });
|
|
542
543
|
return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
|
|
543
544
|
}
|
|
544
545
|
|
|
@@ -1142,6 +1143,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1142
1143
|
if (!dbId.ok) return { ok: false, error: dbId.reason };
|
|
1143
1144
|
if (dbId.skipped) log(` • db identity pre-check${db ? ` (${db})` : ''}: ${dbId.skipped}`);
|
|
1144
1145
|
else log(` ✓ pre-flight: database${db ? ` "${db}"` : ''} reachable as the current user`);
|
|
1146
|
+
const { note: migrateNote } = require('./upgrade-migrate').migratePlan(instanceDir); if (migrateNote) log(` ✓ pre-flight: ${migrateNote}`);
|
|
1145
1147
|
}
|
|
1146
1148
|
|
|
1147
1149
|
// 1a. artist gate (ADR 0241, task 1004205): an open review holds the pin, --force too; a downgrade never waits.
|
|
@@ -1181,7 +1183,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1181
1183
|
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → file:${vendorRel}${prevPin ? ` (was ${prevPin})` : ''}`);
|
|
1182
1184
|
}
|
|
1183
1185
|
|
|
1184
|
-
if (dryRun) { log(
|
|
1186
|
+
if (dryRun) { log(` [dry-run] would: npm install → ${opts.skipMigrate ? '(no migrate)' : require('./upgrade-migrate').migratePlan(instanceDir).label} → materialize .claude → regen API docs+client → restart → verify`); return { ok: true, dryRun: true, fromVersion, toVersion: targetVersion }; }
|
|
1185
1187
|
|
|
1186
1188
|
// 3. npm install.
|
|
1187
1189
|
const inst = npmInstall(instanceDir, run);
|
|
@@ -1455,7 +1457,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1455
1457
|
' --health-url <url> poll after restart (~30s) until 2xx; a still-failing check fails the bump (auto-rollback). REQUIRED for a bump that restarts the service, unless --no-health-check.',
|
|
1456
1458
|
' --version-url <url> read the RUNNING process\'s coreVersion back after restart and require it to equal --to (default: /version on the --health-url origin). A health check alone cannot tell a new core from an old one that never went down.',
|
|
1457
1459
|
' --no-health-check explicitly skip the post-restart health confirmation (default: the check is ON — a bump that restarts the service must confirm it serves); also skips the served-version read-back',
|
|
1458
|
-
' --skip-migrate do not
|
|
1460
|
+
' --skip-migrate do not migrate (the default runs the instance\'s `migrate` script, else the core\'s scripts/migrate.sh)',
|
|
1459
1461
|
' --skip-materialize do not refresh .claude/',
|
|
1460
1462
|
' --skip-regen-docs do not regenerate the OpenAPI spec + typed client from the new core',
|
|
1461
1463
|
' --skip-pin-verify do not verify the integrity pin (tree_sha256 + tarball.sha256) after install',
|
|
@@ -41,17 +41,18 @@ fetched live — read-only GETs — and layered over the fixtures in memory. The
|
|
|
41
41
|
banner names whose hall you are looking at:
|
|
42
42
|
|
|
43
43
|
```
|
|
44
|
-
account: @
|
|
44
|
+
account: @your-login (archon) — your own, live
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Nothing is written to `fixtures/` (that's `refresh-fixtures.js`'s job), so the
|
|
48
48
|
checked-in snapshot stays the fallback. It serves whenever the live read can't
|
|
49
49
|
happen — signed out, expired session, offline, or an endpoint your rank can't
|
|
50
50
|
call (that one keeps its own fixture and the rest of your identity still lands).
|
|
51
|
-
Every fallback says why, and says the hall is about to show you someone else
|
|
51
|
+
Every fallback says why, and says the hall is about to show you someone else — a
|
|
52
|
+
stand-in, since the snapshot's people are scrubbed (see [Fixtures](#fixtures)):
|
|
52
53
|
|
|
53
54
|
```
|
|
54
|
-
account: @
|
|
55
|
+
account: @builder-4 (metic) — NOT YOURS, the checked-in fixture: this machine has no Bongos CLI session — run /builder-setup
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
**`--fixture-me` pins the snapshot.** Use it for the screenshot matrix below —
|
|
@@ -74,6 +75,22 @@ node scripts/hall-preview/refresh-fixtures.js
|
|
|
74
75
|
|
|
75
76
|
Box/terminal endpoints are deliberately never snapshotted (connection details).
|
|
76
77
|
|
|
78
|
+
**No real person is ever checked in (task 1003647).** `fixtures/` is committed, so
|
|
79
|
+
`refresh-fixtures.js` runs every snapshot through [`scrub-identity.js`](scrub-identity.js)
|
|
80
|
+
*before* writing it: logins, display names, avatars, GitHub ids and emails become
|
|
81
|
+
stand-ins (`builder-3` / `Builder 3`, GitHub's ghost avatar), one per person across the
|
|
82
|
+
run, and the snapshotting account's own credit balance, karma and spend (`me`, `me__*`)
|
|
83
|
+
become fixed values. Instance-local ids and free text (task titles, descriptions) are
|
|
84
|
+
left alone. Hand-edited a fixture? Run the same pass over the files on disk — no
|
|
85
|
+
network, and it rewrites only the values that change:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node scripts/hall-preview/refresh-fixtures.js --scrub-only
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`tests/hall_preview_scrub.mjs` fails while any committed fixture still carries a real
|
|
92
|
+
identity.
|
|
93
|
+
|
|
77
94
|
## The evidence matrix (per page-redesign ship)
|
|
78
95
|
|
|
79
96
|
Screenshot every changed page at **{light, dark} × {1280w, 390w}**, plus one
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The harness answers every read from checked-in fixtures, and fixtures/me.json
|
|
6
6
|
// is one particular builder's snapshot. So every preview greeted whoever ran it
|
|
7
|
-
// as somebody else ("Welcome back,
|
|
7
|
+
// as somebody else ("Welcome back, <whoever was snapshotted>"), with that builder's credits,
|
|
8
8
|
// achievements, goals and RANK — and because that snapshot is a metic, the
|
|
9
9
|
// Archon-gated pages rendered their sealed notice, so a UI change to them could
|
|
10
10
|
// not be reviewed at all. Reviewing your own UI change against someone else's
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
// Read-only: GETs only. Endpoints that 403/404 for the caller's rank are
|
|
8
8
|
// skipped (the existing fixture, if any, is kept). Box/terminal endpoints are
|
|
9
9
|
// deliberately NOT snapshotted (they carry host/connection details).
|
|
10
|
+
//
|
|
11
|
+
// PEOPLE ARE SCRUBBED ON WRITE (task 1003647). fixtures/ is committed, so every
|
|
12
|
+
// snapshot goes through ./scrub-identity.js before it touches the disk: logins,
|
|
13
|
+
// names, avatars, GitHub ids and emails become stand-ins (`builder-3` / `Builder
|
|
14
|
+
// 3`), and the snapshotting account's own balance and spend become fixed values.
|
|
15
|
+
// Hand-edited a fixture? Re-run the same pass over the files on disk, no network:
|
|
16
|
+
// node scripts/hall-preview/refresh-fixtures.js --scrub-only
|
|
10
17
|
'use strict';
|
|
11
18
|
|
|
12
19
|
const fs = require('fs');
|
|
13
20
|
const path = require('path');
|
|
14
|
-
const {
|
|
21
|
+
const { createScrubber, isOwnAccountFixture, highestStandIn, rewriteScalars } = require('./scrub-identity');
|
|
15
22
|
|
|
16
23
|
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
17
24
|
|
|
@@ -77,20 +84,73 @@ const SNAPSHOTS = [
|
|
|
77
84
|
['public/versions', 'public__versions'],
|
|
78
85
|
];
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
// Every existing fixture's text — read so a run can number new stand-ins above the
|
|
88
|
+
// ones already in use (a hand-written fixture may carry `builder-7`).
|
|
89
|
+
function readFixtureTexts(dir) {
|
|
90
|
+
try {
|
|
91
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e.code === 'ENOENT') return [];
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// refreshFixtures — GET each snapshot, scrub the people out of it, write it. The api,
|
|
99
|
+
// directory and logger are injected so tests/hall_preview_scrub.mjs drives this exact
|
|
100
|
+
// writer with no network and a temp directory.
|
|
101
|
+
async function refreshFixtures({ api, dir = FIXTURES, snapshots = SNAPSHOTS, log = console.log } = {}) {
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
const scrubber = createScrubber({ firstNumber: highestStandIn(readFixtureTexts(dir)) + 1 });
|
|
83
104
|
let wrote = 0;
|
|
84
|
-
for (const [apiPath, name, trim] of
|
|
105
|
+
for (const [apiPath, name, trim] of snapshots) {
|
|
85
106
|
const res = await api.request('GET', `/${apiPath}`, {});
|
|
86
107
|
if (!res.ok) {
|
|
87
|
-
|
|
108
|
+
log(` skip ${apiPath} → ${res.status} (kept existing fixture if present)`);
|
|
88
109
|
continue;
|
|
89
110
|
}
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
// Scrubbed BEFORE the write, never after: fixtures/ is committed, so a copy of
|
|
112
|
+
// the live response that touched the disk would already be one `git add` away.
|
|
113
|
+
const data = scrubber.scrub(trim ? trim(res.data) : res.data, { ownAccount: isOwnAccountFixture(name) });
|
|
114
|
+
fs.writeFileSync(path.join(dir, `${name}.json`), `${JSON.stringify(data, null, 2)}\n`);
|
|
92
115
|
wrote += 1;
|
|
93
|
-
|
|
116
|
+
log(` wrote ${name}.json (${apiPath})`);
|
|
94
117
|
}
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
log(`refresh-fixtures: ${wrote}/${snapshots.length} snapshots written to ${dir} (people replaced by stand-ins)`);
|
|
119
|
+
return wrote;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// scrubFixturesOnDisk — the same pass over the files already in fixtures/: the
|
|
123
|
+
// hand-written ones (several copied real people in by hand) and anything written
|
|
124
|
+
// before the scrub existed. Only values that change are rewritten, so a fixture
|
|
125
|
+
// someone formatted by hand keeps its formatting.
|
|
126
|
+
function scrubFixturesOnDisk({ dir = FIXTURES, log = console.log } = {}) {
|
|
127
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
|
|
128
|
+
const texts = files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
129
|
+
const scrubber = createScrubber({ firstNumber: highestStandIn(texts) + 1 });
|
|
130
|
+
let changed = 0;
|
|
131
|
+
files.forEach((file, idx) => {
|
|
132
|
+
const clean = scrubber.scrub(JSON.parse(texts[idx]), { ownAccount: isOwnAccountFixture(file.slice(0, -'.json'.length)) });
|
|
133
|
+
const next = rewriteScalars(texts[idx], clean);
|
|
134
|
+
if (next === texts[idx]) return;
|
|
135
|
+
fs.writeFileSync(path.join(dir, file), next);
|
|
136
|
+
changed += 1;
|
|
137
|
+
log(` scrubbed ${file}`);
|
|
138
|
+
});
|
|
139
|
+
log(`refresh-fixtures --scrub-only: ${changed}/${files.length} fixture(s) rewritten in ${dir}`);
|
|
140
|
+
return changed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { refreshFixtures, scrubFixturesOnDisk, SNAPSHOTS };
|
|
144
|
+
|
|
145
|
+
if (require.main === module) {
|
|
146
|
+
(async () => {
|
|
147
|
+
if (process.argv.includes('--scrub-only')) {
|
|
148
|
+
scrubFixturesOnDisk();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Required here, not at the top: a test that loads this file for the writer must
|
|
152
|
+
// not also load the CLI session machinery.
|
|
153
|
+
const { cliClient } = require('../gds/cli-lib');
|
|
154
|
+
await refreshFixtures({ api: await cliClient() });
|
|
155
|
+
})().catch((e) => { console.error(e.message || e); process.exit(1); });
|
|
156
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/hall-preview/scrub-identity.js — replace the real people in a hall-preview
|
|
3
|
+
// fixture with stand-ins before the file is written (task 1003647).
|
|
4
|
+
//
|
|
5
|
+
// WHY. refresh-fixtures.js snapshots LIVE API responses into fixtures/, and fixtures/
|
|
6
|
+
// is committed. So every refresh published the GitHub login, display name, avatar and
|
|
7
|
+
// GitHub id of everyone the snapshot could see — and, in me.json, the snapshotting
|
|
8
|
+
// account's own credit balance and month-to-date spend — into the repo and every clone
|
|
9
|
+
// of it. A leak like that is fixed in the WRITER: cleaning the output once re-leaks on
|
|
10
|
+
// the next refresh. So refresh-fixtures.js passes every snapshot through here, and its
|
|
11
|
+
// --scrub-only mode runs the same pass over files already on disk (hand-written ones
|
|
12
|
+
// included, which copied real people in by hand).
|
|
13
|
+
//
|
|
14
|
+
// THE SCHEME.
|
|
15
|
+
// • A person becomes `builder-<n>` (login-shaped fields) / `Builder <n>` (names), one
|
|
16
|
+
// n per person for the whole run, so a builder reads the same on every page. The
|
|
17
|
+
// person is keyed by login; a name travels with the login beside it.
|
|
18
|
+
// • An avatar becomes GitHub's own "ghost" avatar — the account GitHub shows for
|
|
19
|
+
// deleted users. It is a real image, so pages still render, and it is nobody.
|
|
20
|
+
// • A GitHub id becomes the stand-in's n; an email, `builder-<n>@example.com`.
|
|
21
|
+
// • Private account figures (month-to-date spend, budgets) become fixed values
|
|
22
|
+
// wherever they appear. In the snapshotting account's OWN files (me, me__*), so do
|
|
23
|
+
// its credit balance and karma — the "caller's credits" this task was filed about.
|
|
24
|
+
// Other builders' balances are kept: with their names gone they describe "Builder
|
|
25
|
+
// 3", not a person, and flattening them would flatten the leaderboard and roster
|
|
26
|
+
// previews the harness exists for.
|
|
27
|
+
// • null stays null and "" stays "". Pages branch on absence (initials when there is
|
|
28
|
+
// no avatar, the login when there is no display name) and tests pin those states.
|
|
29
|
+
//
|
|
30
|
+
// NOT TOUCHED. Instance-local numeric ids (builder, task, goal) — the harness routes on
|
|
31
|
+
// them (analytics/builder/:id, /profile?id=…) and they name nobody outside that one
|
|
32
|
+
// instance. Free text (task titles, descriptions) — project content, not a person's
|
|
33
|
+
// record. The repo's own coordinates (instance.repo.owner is where the code lives).
|
|
34
|
+
//
|
|
35
|
+
// FIXED POINT. Every stand-in is recognised on the way back in (`builder-3` is person
|
|
36
|
+
// 3), so scrubbing an already-scrubbed fixture changes nothing. tests/hall_preview_scrub.mjs
|
|
37
|
+
// leans on exactly that: each committed fixture must already be a fixed point, which is
|
|
38
|
+
// the same as saying no real identity is committed.
|
|
39
|
+
|
|
40
|
+
// GitHub's "ghost" user (id 10137): the placeholder GitHub itself substitutes for a
|
|
41
|
+
// deleted account. An https URL on the host the real avatars use, because the hall
|
|
42
|
+
// renders avatars only through an http(s) check (roster.js / collab-lib.js httpUrl).
|
|
43
|
+
const GHOST_AVATAR_URL = 'https://avatars.githubusercontent.com/u/10137?v=4';
|
|
44
|
+
|
|
45
|
+
// The fixed values that replace private figures. Round, obviously fake, stable.
|
|
46
|
+
const OWN_CREDITS = 1000;
|
|
47
|
+
const OWN_KARMA = 10;
|
|
48
|
+
const PRIVATE_FIGURES = { spend_mtd_usd: 42, budget_mtd_usd: 100, monthly_budget_usd: 100 };
|
|
49
|
+
const PLACEHOLDER_TEXT = {
|
|
50
|
+
device_name: 'Preview device',
|
|
51
|
+
anthropic_user_id: 'anthropic-user-preview',
|
|
52
|
+
bio: 'A builder bio, replaced for the preview.',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const STAND_IN_LOGIN = /^builder-(\d+)$/i;
|
|
56
|
+
const STAND_IN_NAME = /^Builder (\d+)$/;
|
|
57
|
+
const STAND_IN_EMAIL = /^builder-(\d+)@example\.com$/i;
|
|
58
|
+
|
|
59
|
+
const isLoginKey = (k) => k === 'github_login' || k === 'login' || k === 'handle' || /_login$/.test(k);
|
|
60
|
+
const isAvatarKey = (k) => k === 'avatar_url' || /_avatar_url$/.test(k);
|
|
61
|
+
const isGithubIdKey = (k) => k === 'github_id' || /_github_id$/.test(k);
|
|
62
|
+
const isEmailKey = (k) => k === 'email' || /_email$/.test(k);
|
|
63
|
+
// `by` / `*_by` holds a builder ID almost everywhere (created_by, shipped_by); only a
|
|
64
|
+
// non-numeric value is a login (sandbox_review.by, who approved a staged change).
|
|
65
|
+
const isByKey = (k) => k === 'by' || /_by$/.test(k);
|
|
66
|
+
const looksLikeLogin = (v) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(v) && !/^\d+$/.test(v);
|
|
67
|
+
|
|
68
|
+
// The login a name-shaped key belongs to, or undefined when the key is not a person's
|
|
69
|
+
// name. `display_name` pairs with the object's own login; `<who>_name` and
|
|
70
|
+
// `<who>_display_name` pair with `<who>_login` beside them — which is what keeps
|
|
71
|
+
// `worktree_name` (no `worktree_login`) out.
|
|
72
|
+
function nameOwnerKey(key, obj) {
|
|
73
|
+
if (key === 'display_name') return ['github_login', 'login', 'handle'].find((k) => k in obj) || null;
|
|
74
|
+
const m = /^(.+?)(?:_display)?_name$/.exec(key);
|
|
75
|
+
if (!m) return undefined;
|
|
76
|
+
const loginKey = `${m[1]}_login`;
|
|
77
|
+
return loginKey in obj ? loginKey : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// createScrubber — one registry of people for one run, so a builder gets the same n in
|
|
81
|
+
// every file the run touches. `firstNumber` lets a run start above stand-ins already on
|
|
82
|
+
// disk, so a new person never collides with one a hand-written fixture already uses.
|
|
83
|
+
function createScrubber({ firstNumber = 1 } = {}) {
|
|
84
|
+
const people = new Map(); // 'login:<lower>' | 'name:<text>' | 'id:<github id>' | 'email:<lower>' → n
|
|
85
|
+
let next = Math.max(1, Number(firstNumber) || 1);
|
|
86
|
+
|
|
87
|
+
function numberFor(keys) {
|
|
88
|
+
for (const k of keys) if (k && people.has(k)) return remember(keys, people.get(k));
|
|
89
|
+
return remember(keys, next++);
|
|
90
|
+
}
|
|
91
|
+
function remember(keys, n) {
|
|
92
|
+
for (const k of keys) if (k) people.set(k, n);
|
|
93
|
+
if (n >= next) next = n + 1;
|
|
94
|
+
return n;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The person an object describes, from whichever identity fields it carries. A
|
|
98
|
+
// stand-in keeps its own number; a real value gets the registry's.
|
|
99
|
+
function personOf(obj, loginKey, nameKey) {
|
|
100
|
+
const login = loginKey && typeof obj[loginKey] === 'string' ? obj[loginKey] : '';
|
|
101
|
+
const name = nameKey && typeof obj[nameKey] === 'string' ? obj[nameKey] : '';
|
|
102
|
+
const standIn = STAND_IN_LOGIN.exec(login) || STAND_IN_NAME.exec(name);
|
|
103
|
+
if (standIn) return remember([], Number(standIn[1]));
|
|
104
|
+
return numberFor([login && `login:${login.toLowerCase()}`, name && `name:${name}`]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scrubObject(obj, ctx) {
|
|
108
|
+
const out = {};
|
|
109
|
+
// The object's own person, resolved once so its login, name, avatar, GitHub id and
|
|
110
|
+
// email all land on the same n.
|
|
111
|
+
const ownLoginKey = ['github_login', 'login', 'handle'].find((k) => typeof obj[k] === 'string' && obj[k]);
|
|
112
|
+
const ownNameKey = typeof obj.display_name === 'string' && obj.display_name ? 'display_name' : null;
|
|
113
|
+
let selfN = null;
|
|
114
|
+
const self = () => {
|
|
115
|
+
if (selfN == null) selfN = (ownLoginKey || ownNameKey) ? personOf(obj, ownLoginKey, ownNameKey) : null;
|
|
116
|
+
return selfN;
|
|
117
|
+
};
|
|
118
|
+
const isPersonRecord = Boolean(ownLoginKey || ownNameKey);
|
|
119
|
+
|
|
120
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
121
|
+
out[key] = scrubValue(key, value, obj, ctx, self, isPersonRecord);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scrubArray(arr, ctx) {
|
|
127
|
+
return arr.map((v) => (Array.isArray(v) ? scrubArray(v, ctx) : v && typeof v === 'object' ? scrubObject(v, ctx) : v));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function scrubValue(key, value, obj, ctx, self, isPersonRecord) {
|
|
131
|
+
if (value && typeof value === 'object') {
|
|
132
|
+
if (key === 'links' && isPersonRecord) return scrubLinks(value);
|
|
133
|
+
const inner = Array.isArray(value) ? scrubArray(value, ctx) : scrubObject(value, ctx);
|
|
134
|
+
// An OWN-account file's roll-up totals are the account's own credits too.
|
|
135
|
+
if (ctx.ownAccount && key === 'totals' && !Array.isArray(inner)) {
|
|
136
|
+
if (typeof inner.credits === 'number') inner.credits = OWN_CREDITS;
|
|
137
|
+
if (typeof inner.karma === 'number') inner.karma = OWN_KARMA;
|
|
138
|
+
}
|
|
139
|
+
return inner;
|
|
140
|
+
}
|
|
141
|
+
if (value == null || value === '') return value; // absence is a state the pages render
|
|
142
|
+
if (key in PRIVATE_FIGURES) return typeof value === 'string' ? String(PRIVATE_FIGURES[key]) : PRIVATE_FIGURES[key];
|
|
143
|
+
|
|
144
|
+
if (typeof value === 'string') {
|
|
145
|
+
if (isLoginKey(key)) {
|
|
146
|
+
if (STAND_IN_LOGIN.test(value)) return value;
|
|
147
|
+
const n = ['github_login', 'login', 'handle'].includes(key) ? self() : personOf(obj, key, siblingNameKey(key, obj));
|
|
148
|
+
return `builder-${n}`;
|
|
149
|
+
}
|
|
150
|
+
const owner = nameOwnerKey(key, obj);
|
|
151
|
+
if (owner !== undefined) {
|
|
152
|
+
if (STAND_IN_NAME.test(value)) return value;
|
|
153
|
+
const n = key === 'display_name' ? self() : personOf(obj, owner, key);
|
|
154
|
+
return `Builder ${n}`;
|
|
155
|
+
}
|
|
156
|
+
if (isAvatarKey(key)) return GHOST_AVATAR_URL;
|
|
157
|
+
if (isEmailKey(key)) {
|
|
158
|
+
if (STAND_IN_EMAIL.test(value)) return value;
|
|
159
|
+
const n = isPersonRecord ? self() : numberFor([`email:${value.toLowerCase()}`]);
|
|
160
|
+
return `builder-${n}@example.com`;
|
|
161
|
+
}
|
|
162
|
+
if (isByKey(key) && looksLikeLogin(value)) {
|
|
163
|
+
if (STAND_IN_LOGIN.test(value)) return value;
|
|
164
|
+
return `builder-${numberFor([`login:${value.toLowerCase()}`])}`;
|
|
165
|
+
}
|
|
166
|
+
// A PR / commit author is a GitHub login; elsewhere `author` credits an
|
|
167
|
+
// organisation (a module's author), which is nobody's personal record.
|
|
168
|
+
if (key === 'author' && 'head_sha' in obj && looksLikeLogin(value)) {
|
|
169
|
+
if (STAND_IN_LOGIN.test(value)) return value;
|
|
170
|
+
return `builder-${numberFor([`login:${value.toLowerCase()}`])}`;
|
|
171
|
+
}
|
|
172
|
+
if (key in PLACEHOLDER_TEXT) return PLACEHOLDER_TEXT[key];
|
|
173
|
+
if (isGithubIdKey(key)) return String(githubIdFor(obj, value, self, isPersonRecord));
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof value === 'number') {
|
|
178
|
+
if (isGithubIdKey(key)) return githubIdFor(obj, value, self, isPersonRecord);
|
|
179
|
+
if (ctx.ownAccount && isPersonRecord) {
|
|
180
|
+
if (key === 'total_credits') return OWN_CREDITS;
|
|
181
|
+
if (key === 'karma' || key === 'total_karma') return OWN_KARMA;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// A GitHub id names an account anywhere on GitHub, so it follows its person's n.
|
|
188
|
+
function githubIdFor(obj, value, self, isPersonRecord) {
|
|
189
|
+
if (isPersonRecord) return self();
|
|
190
|
+
const n = Number(value);
|
|
191
|
+
// A standalone small id is already a stand-in (real GitHub ids of any account a
|
|
192
|
+
// builder holds today are seven-plus digits).
|
|
193
|
+
if (Number.isInteger(n) && n > 0 && n < 100000) return n;
|
|
194
|
+
return numberFor([`id:${value}`]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The login key that sits beside a `<who>_name` key, for pairing the two.
|
|
198
|
+
function siblingNameKey(loginKey, obj) {
|
|
199
|
+
const who = loginKey.replace(/_login$/, '');
|
|
200
|
+
return [`${who}_name`, `${who}_display_name`].find((k) => typeof obj[k] === 'string' && obj[k]) || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// A profile's links are the person's own sites. Keep the shape, lose the addresses.
|
|
204
|
+
function scrubLinks(value) {
|
|
205
|
+
if (Array.isArray(value)) return value.map((v) => (typeof v === 'string' ? 'https://example.com/' : (v && typeof v === 'object' ? scrubLinks(v) : v)));
|
|
206
|
+
const out = {};
|
|
207
|
+
for (const [k, v] of Object.entries(value)) out[k] = typeof v === 'string' && v ? 'https://example.com/' : (v && typeof v === 'object' ? scrubLinks(v) : v);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// scrub(data, { ownAccount }) — a scrubbed deep copy; the input is not mutated.
|
|
212
|
+
// ownAccount: the fixture IS the snapshotting account (me, me__*), so its own
|
|
213
|
+
// credit balance and karma are replaced too.
|
|
214
|
+
function scrub(data, { ownAccount = false } = {}) {
|
|
215
|
+
const ctx = { ownAccount };
|
|
216
|
+
if (Array.isArray(data)) return scrubArray(data, ctx);
|
|
217
|
+
return data && typeof data === 'object' ? scrubObject(data, ctx) : data;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { scrub };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// A fixture named `me` or `me__…` is the snapshotting account's own record.
|
|
224
|
+
const isOwnAccountFixture = (name) => name === 'me' || String(name).startsWith('me__');
|
|
225
|
+
|
|
226
|
+
// The highest stand-in number already used in some fixture texts, so a new run can
|
|
227
|
+
// start above it (createScrubber's firstNumber).
|
|
228
|
+
function highestStandIn(texts) {
|
|
229
|
+
let max = 0;
|
|
230
|
+
for (const t of texts) {
|
|
231
|
+
for (const m of String(t).matchAll(/"builder-(\d+)(?:@example\.com)?"|"Builder (\d+)"/gi)) {
|
|
232
|
+
max = Math.max(max, Number(m[1] || m[2]));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return max;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// rewriteScalars(text, next) — the JSON text of a hand-written fixture with ONLY the
|
|
239
|
+
// changed scalar values replaced, byte-for-byte everywhere else. A plain
|
|
240
|
+
// JSON.stringify would also reformat every compact array and inline object a person
|
|
241
|
+
// wrote by hand, burying a handful of identity edits in a whole-file rewrite.
|
|
242
|
+
function rewriteScalars(text, next) {
|
|
243
|
+
const splices = [];
|
|
244
|
+
let i = 0;
|
|
245
|
+
const ws = () => { while (i < text.length && /\s/.test(text[i])) i++; };
|
|
246
|
+
const readString = () => {
|
|
247
|
+
const start = i++;
|
|
248
|
+
while (text[i] !== '"') i += text[i] === '\\' ? 2 : 1;
|
|
249
|
+
i++;
|
|
250
|
+
return { start, end: i, value: JSON.parse(text.slice(start, i)) };
|
|
251
|
+
};
|
|
252
|
+
const readScalar = () => {
|
|
253
|
+
if (text[i] === '"') return readString();
|
|
254
|
+
const start = i;
|
|
255
|
+
while (i < text.length && !/[\s,\]}]/.test(text[i])) i++;
|
|
256
|
+
return { start, end: i, value: JSON.parse(text.slice(start, i)) };
|
|
257
|
+
};
|
|
258
|
+
const visit = (after) => {
|
|
259
|
+
ws();
|
|
260
|
+
if (text[i] === '{') {
|
|
261
|
+
i++; ws();
|
|
262
|
+
if (text[i] === '}') { i++; return; }
|
|
263
|
+
for (;;) {
|
|
264
|
+
ws(); const { value: key } = readString(); ws(); i++; // the ':'
|
|
265
|
+
visit(after == null ? undefined : after[key]);
|
|
266
|
+
ws();
|
|
267
|
+
if (text[i] === ',') { i++; continue; }
|
|
268
|
+
i++; return; // the '}'
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (text[i] === '[') {
|
|
272
|
+
i++; ws();
|
|
273
|
+
if (text[i] === ']') { i++; return; }
|
|
274
|
+
for (let idx = 0; ; idx++) {
|
|
275
|
+
visit(after == null ? undefined : after[idx]);
|
|
276
|
+
ws();
|
|
277
|
+
if (text[i] === ',') { i++; continue; }
|
|
278
|
+
i++; return; // the ']'
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const tok = readScalar();
|
|
282
|
+
if (after !== undefined && after !== tok.value) splices.push([tok.start, tok.end, JSON.stringify(after)]);
|
|
283
|
+
};
|
|
284
|
+
visit(next);
|
|
285
|
+
let out = text;
|
|
286
|
+
for (const [start, end, repl] of splices.reverse()) out = out.slice(0, start) + repl + out.slice(end);
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
createScrubber,
|
|
292
|
+
isOwnAccountFixture,
|
|
293
|
+
highestStandIn,
|
|
294
|
+
rewriteScalars,
|
|
295
|
+
GHOST_AVATAR_URL,
|
|
296
|
+
};
|
|
@@ -21,7 +21,7 @@ const fs = require('node:fs/promises');
|
|
|
21
21
|
const auth = require('../auth');
|
|
22
22
|
const db = require('../db');
|
|
23
23
|
const { validateOrRespond } = require('./_helpers');
|
|
24
|
-
const { resolveEnv } = require('../../instance-config');
|
|
24
|
+
const { resolveEnv, envPrefix } = require('../../instance-config');
|
|
25
25
|
|
|
26
26
|
// Slug shape for security_docs (task 991): lowercase kebab, 1–128 chars. Used to
|
|
27
27
|
// validate POST /security/docs and to safely interpolate :slug into the GET.
|
|
@@ -102,8 +102,8 @@ module.exports = function buildSecurityRouter() {
|
|
|
102
102
|
}
|
|
103
103
|
const hint =
|
|
104
104
|
result.reason === 'env_not_set'
|
|
105
|
-
?
|
|
106
|
-
:
|
|
105
|
+
? `${envPrefix()}_SECURITY_ADR_PATH is unset on the server; an Archon must provision the file and the env var.`
|
|
106
|
+
: `${envPrefix()}_SECURITY_ADR_PATH (or its legacy GDS_ spelling) is set but the file is unreadable (missing, wrong perms, or wrong path).`;
|
|
107
107
|
return res.fail('security_adr_not_provisioned', 404, { reason: result.reason, hint });
|
|
108
108
|
}
|
|
109
109
|
);
|
package/src/build-info.js
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
// deployedCommit === mergeCommit before flipping a task to shipped.
|
|
9
9
|
//
|
|
10
10
|
// Resolution order (resolveCommit is pure + exported for the test):
|
|
11
|
-
// 1. env
|
|
11
|
+
// 1. env <PREFIX>_GIT_SHA (BONGOS_GIT_SHA on a vanilla instance; the legacy
|
|
12
|
+
// OTB_GIT_SHA is still read) — set by ~/deploy.sh at restart if/when it chooses to;
|
|
12
13
|
// authoritative + cheap, and survives a checkout with no usable .git.
|
|
13
14
|
// 2. `git rev-parse HEAD` in the checkout — accurate after deploy.sh's
|
|
14
15
|
// `git reset --hard origin/main`.
|
|
15
16
|
// 3. null — unknown (callers must degrade gracefully, never hard-fail).
|
|
16
17
|
|
|
17
18
|
const { execSync } = require('node:child_process');
|
|
19
|
+
const { resolveEnv } = require('./instance-config');
|
|
18
20
|
|
|
19
21
|
const SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
20
22
|
|
|
@@ -62,7 +64,7 @@ let _cache = null;
|
|
|
62
64
|
// restarts the service), so caching is correct and keeps /version allocation-free.
|
|
63
65
|
function buildInfo() {
|
|
64
66
|
if (_cache) return _cache;
|
|
65
|
-
const fromEnv = resolveCommit({ envSha:
|
|
67
|
+
const fromEnv = resolveCommit({ envSha: resolveEnv('GIT_SHA') });
|
|
66
68
|
const commit = fromEnv || resolveCommit({ gitSha: readGitSha() });
|
|
67
69
|
_cache = {
|
|
68
70
|
commit,
|