@bongos/core 1.17.3 → 1.18.3
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 +212 -332
- package/.devcontainer/healthcheck.sh +0 -0
- package/.devcontainer/init-firewall.sh +0 -0
- package/.devcontainer/post-create.sh +0 -0
- package/GOVERNANCE.md +1 -1
- package/README.md +26 -22
- package/bin/bongos.js +0 -0
- package/config/branding.neutral.json +5 -1
- package/docker-entrypoint.sh +0 -0
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/README.md +6 -0
- package/docs/api/openapi.json +1630 -632
- package/docs/api-reference.md +50 -15
- package/docs/file-map.md +0 -18
- package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
- package/docs/onboarding/diagrams/assertions.json +5 -9
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/modules/hall-ui/public/builders.js +37 -0
- package/modules/hall-ui/public/goals-page.js +98 -176
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -261
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-widgets.js +12 -4
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/settings.css +355 -0
- package/modules/hall-ui/public/settings.html +86 -414
- package/modules/hall-ui/public/settings.js +96 -35
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/watch.css +184 -0
- package/modules/hall-ui/public/watch.html +111 -285
- package/modules/hall-ui/public/watch.js +59 -21
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +292 -19
- package/public-landing/index.html +11 -76
- package/scripts/cron/style_guide_audit.sh +0 -0
- package/scripts/gds/api.js +0 -0
- package/scripts/gds/architect-audit.js +0 -0
- package/scripts/gds/audit-authorship.sh +0 -0
- package/scripts/gds/audit-replay.sh +0 -0
- package/scripts/gds/box.js +0 -0
- package/scripts/gds/capture.js +0 -0
- package/scripts/gds/cli-lib.js +11 -5
- package/scripts/gds/conductor-dispatch.js +0 -0
- package/scripts/gds/coreB-carve-proof.sh +0 -0
- package/scripts/gds/db-backup-nightly.sh +0 -0
- package/scripts/gds/db-backup-offsite.sh +0 -0
- package/scripts/gds/doctor.js +0 -0
- package/scripts/gds/grade-smoke.js +0 -0
- package/scripts/gds/hooks/pre-push.js +0 -0
- package/scripts/gds/install-git-hooks.js +0 -0
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +42 -10
- package/scripts/gds/main-audit.js +0 -0
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/oauth-secret.js +0 -0
- package/scripts/gds/onboarding-analyze.js +0 -0
- package/scripts/gds/onboarding-ship.js +0 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +0 -0
- package/scripts/gds/portability-smoke-art.sh +0 -0
- package/scripts/gds/portability-smoke-memory.sh +0 -0
- package/scripts/gds/provision.js +270 -11
- package/scripts/gds/publish-manifest.js +15 -0
- package/scripts/gds/push-main.js +0 -0
- package/scripts/gds/render-ideas.js +0 -0
- package/scripts/gds/run-unit-tests.js +38 -14
- package/scripts/gds/security-baseline-check.sh +0 -0
- package/scripts/gds/session.js +0 -0
- package/scripts/gds/setup.js +41 -0
- package/scripts/gds/ship.js +126 -32
- package/scripts/gds/smoke-batch-claim.sh +0 -0
- package/scripts/gds/smoke-batch-release.sh +0 -0
- package/scripts/gds/smoke-box.sh +0 -0
- package/scripts/gds/smoke-claim-consistency.sh +0 -0
- package/scripts/gds/smoke-dependencies.sh +0 -0
- package/scripts/gds/smoke-write.sh +0 -0
- package/scripts/gds/upgrade.js +147 -5
- package/scripts/hall-preview/server.js +18 -1
- package/scripts/migrate.sh +10 -0
- package/scripts/migrate_cost_ledger.sh +0 -0
- package/scripts/render-diagrams.sh +0 -0
- package/scripts/security/dep-audit.js +0 -0
- package/scripts/setup-builder.js +0 -0
- package/scripts/setup-builder.sh +0 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
- package/src/bongos/auth.js +362 -5
- package/src/bongos/db.js +66 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/routes/auth.js +93 -11
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes.js +2 -0
- package/src/branding.js +17 -2
- package/src/instance-config.js +16 -0
- package/src/module-api.js +48 -1
- package/tests/branding.mjs +29 -23
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/idp_device_exchange.mjs +105 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/package_core.mjs +246 -1
- package/tests/provision.mjs +226 -0
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/ship_ci_deploy.mjs +60 -0
- package/tests/upgrade.mjs +254 -5
- package/.gitignore +0 -91
- package/scripts/hall-preview/fixtures/access-requests.json +0 -1
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -1
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/override-requests.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -1
- package/scripts/hall-preview/fixtures/security__reports.json +0 -1
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
package/scripts/gds/provision.js
CHANGED
|
@@ -47,6 +47,7 @@ const fs = require('node:fs');
|
|
|
47
47
|
const os = require('node:os');
|
|
48
48
|
const path = require('node:path');
|
|
49
49
|
const cp = require('node:child_process');
|
|
50
|
+
const crypto = require('node:crypto');
|
|
50
51
|
|
|
51
52
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
52
53
|
|
|
@@ -98,6 +99,11 @@ const CONFIG = {
|
|
|
98
99
|
// <slug>.caddy file (idempotent overwrite, no fragile append to a shared file);
|
|
99
100
|
// the main Caddyfile carries a one-time `import <dir>/*.caddy` (operator setup).
|
|
100
101
|
caddySnippetDir: process.env.PROVISION_CADDY_SNIPPET_DIR || '/etc/caddy/sites',
|
|
102
|
+
// Where nightly pg_dump backups land (db-backup-nightly.sh's GDS_BACKUP_DIR
|
|
103
|
+
// default). Shared across co-tenant instances on one box — dumps are named
|
|
104
|
+
// `<db>-<ts>.sql.gz`, so each instance's files + retention prune stay scoped to
|
|
105
|
+
// its own DB. Owned by the app user, mode 0750. (task 2213, productizes 2210.)
|
|
106
|
+
backupDir: process.env.PROVISION_BACKUP_DIR || '/var/backups/gds',
|
|
101
107
|
// Dedicated-droplet defaults (only used for hosting_shape='dedicated').
|
|
102
108
|
region: process.env.PROVISION_REGION || 'nyc3',
|
|
103
109
|
baseImage: process.env.PROVISION_BASE_IMAGE || 'ubuntu-24-04-x64',
|
|
@@ -175,7 +181,23 @@ function loadDeps() {
|
|
|
175
181
|
const provisioning = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning'));
|
|
176
182
|
// eslint-disable-next-line global-require
|
|
177
183
|
const { DoApi } = require(path.join(REPO_ROOT, 'scripts/gds/do-api'));
|
|
178
|
-
|
|
184
|
+
// Cross-site identity (ADR 0141 §4, task #1002109). Resolve the hub-side module by a
|
|
185
|
+
// VARIABLE key, gated on isModuleEnabled — the module-loader's own dynamic-dispatch
|
|
186
|
+
// pattern (the run-unit-tests.js precedent), NOT a static 'modules/platform-identity/'
|
|
187
|
+
// literal that would couple this publishable core file to an instance-EXCLUDED module
|
|
188
|
+
// (fitness Check 16b / task 2135). A vanilla core that neither ships nor enables the
|
|
189
|
+
// module never resolves it → no MODULE_NOT_FOUND on a fresh boot. Null here → federation
|
|
190
|
+
// dormant (shouldFederate skips) → the instance owns its auth, byte-identical to today.
|
|
191
|
+
let platformIdentity = null, hubKeys = null;
|
|
192
|
+
const PI_KEY = 'platform-identity';
|
|
193
|
+
let piEnabled = false;
|
|
194
|
+
try { piEnabled = require(path.join(REPO_ROOT, 'src/modules')).isModuleEnabled(PI_KEY); } catch { piEnabled = false; }
|
|
195
|
+
if (piEnabled) {
|
|
196
|
+
const piBase = path.join(REPO_ROOT, 'modules', PI_KEY);
|
|
197
|
+
try { platformIdentity = require(path.join(piBase, 'platform-identity')); hubKeys = require(path.join(piBase, 'hub-keys')); }
|
|
198
|
+
catch { platformIdentity = null; hubKeys = null; }
|
|
199
|
+
}
|
|
200
|
+
_deps = { pool, provisioning, DoApi, platformIdentity, hubKeys };
|
|
179
201
|
return _deps;
|
|
180
202
|
}
|
|
181
203
|
|
|
@@ -252,6 +274,10 @@ function seedFirstVersionCmd(inst, { privileged = false } = {}) {
|
|
|
252
274
|
// It is the ADR 0108 DESTINATION model; these generators make provision.js drive the
|
|
253
275
|
// scripts/deploy/deploy-instance.sh runbook API-driven instead of by hand.
|
|
254
276
|
const CORE_PKG_DIR = 'node_modules/@bongos/core';
|
|
277
|
+
// The unix account the instance's Node process + its backup timer run as. Mirrors
|
|
278
|
+
// serviceUnit's hardcoded `User=lars`/`Group=lars` (the droplet's app user); kept as
|
|
279
|
+
// one constant so the backup dir owner + the backup unit's User= can't drift from it.
|
|
280
|
+
const APP_USER = 'lars';
|
|
255
281
|
function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return path.posix.join(base, inst.slug); }
|
|
256
282
|
// Sync the instance's own repo (host content + the core pin). Mirrors deploy-instance.sh
|
|
257
283
|
// step 1 — assumes the repo is already cloned at standaloneRoot (initial clone is a
|
|
@@ -447,6 +473,80 @@ WantedBy=multi-user.target
|
|
|
447
473
|
}
|
|
448
474
|
function serviceUnitPath(inst) { return `/etc/systemd/system/${inst.slug}.service`; }
|
|
449
475
|
|
|
476
|
+
// ── nightly DB backup: per-instance timer + service (task 2213) ─────────────
|
|
477
|
+
// PRODUCTIZES the manual task-2210 fix: a newly-provisioned instance was born with
|
|
478
|
+
// NO backups (dir absent, GDS_BACKUP_DB unset → the script fell back to the
|
|
479
|
+
// nonexistent `amazonprimea` default, and the repo's global gds-db-backup units
|
|
480
|
+
// hardcode /home/lars/amazonprimea — wrong for the consumer node_modules/@bongos/
|
|
481
|
+
// core layout). These generators emit a PER-INSTANCE unit pair that fixes all three:
|
|
482
|
+
// • name → `<slug>-db-backup.{service,timer}` so co-tenant instances on one box
|
|
483
|
+
// never collide (each dumps its OWN db; the script's retention prune is
|
|
484
|
+
// already `<db>-*.sql.gz`-scoped, so a shared backupDir is safe).
|
|
485
|
+
// • DB → Environment=GDS_BACKUP_DB=<instance db>, set on the unit (not read from
|
|
486
|
+
// the web.env) so the dump targets the right database with no fallback.
|
|
487
|
+
// • path → ExecStart resolves the canonical script INSIDE the instance's pinned
|
|
488
|
+
// @bongos/core (standalone) or this control-plane checkout (co-tenant),
|
|
489
|
+
// the SAME workDir/CORE_PKG_DIR split serviceUnit uses — never the old
|
|
490
|
+
// monorepo path.
|
|
491
|
+
function backupUnitName(inst) { return `${inst.slug}-db-backup`; }
|
|
492
|
+
function backupServicePath(inst) { return `/etc/systemd/system/${backupUnitName(inst)}.service`; }
|
|
493
|
+
function backupTimerPath(inst) { return `/etc/systemd/system/${backupUnitName(inst)}.timer`; }
|
|
494
|
+
// The consumer-layout-correct absolute path to db-backup-nightly.sh. Standalone runs
|
|
495
|
+
// it from the instance's own pinned core; co-tenant/dedicated from this checkout's
|
|
496
|
+
// scripts/gds/ — mirrors serviceUnit's execStart resolution so they can't diverge.
|
|
497
|
+
function backupScriptPath(inst) {
|
|
498
|
+
const rel = 'scripts/gds/db-backup-nightly.sh';
|
|
499
|
+
return inst.hosting_shape === 'standalone'
|
|
500
|
+
? path.posix.join(standaloneRoot(inst), CORE_PKG_DIR, rel)
|
|
501
|
+
: path.posix.join(REPO_ROOT, rel);
|
|
502
|
+
}
|
|
503
|
+
function backupService(inst, { backupDir = CONFIG.backupDir } = {}) {
|
|
504
|
+
return `[Unit]
|
|
505
|
+
Description=Nightly pg_dump of the ${inst.slug} GDS DB (provisioned by provision.js, task 2213)
|
|
506
|
+
After=postgresql.service
|
|
507
|
+
Requires=postgresql.service
|
|
508
|
+
|
|
509
|
+
[Service]
|
|
510
|
+
Type=oneshot
|
|
511
|
+
User=${APP_USER}
|
|
512
|
+
Group=${APP_USER}
|
|
513
|
+
# The DB to dump + where dumps land — set on the unit (not via the instance web.env)
|
|
514
|
+
# so the dump targets the right database with no fallback to a stale default (root
|
|
515
|
+
# cause of the task-2210 breakage). db-backup-nightly.sh reads both as env.
|
|
516
|
+
Environment=GDS_BACKUP_DB=${dbName(inst)}
|
|
517
|
+
Environment=GDS_BACKUP_DIR=${backupDir}
|
|
518
|
+
# Consumer-layout path: the canonical script inside this instance's pinned core.
|
|
519
|
+
ExecStart=/bin/bash ${backupScriptPath(inst)}
|
|
520
|
+
# Modest hardening — the script only needs read on the pg socket + write on the
|
|
521
|
+
# backup dir. Mirrors the repo's gds-db-backup.service.
|
|
522
|
+
NoNewPrivileges=true
|
|
523
|
+
ProtectSystem=strict
|
|
524
|
+
ReadWritePaths=${backupDir}
|
|
525
|
+
ProtectHome=read-only
|
|
526
|
+
PrivateTmp=true
|
|
527
|
+
|
|
528
|
+
[Install]
|
|
529
|
+
# Not socket-activated; the timer drives this. The timer's WantedBy is what enables.
|
|
530
|
+
`;
|
|
531
|
+
}
|
|
532
|
+
function backupTimer(inst) {
|
|
533
|
+
return `[Unit]
|
|
534
|
+
Description=Daily timer for the ${inst.slug} GDS DB pg_dump (provisioned by provision.js, task 2213)
|
|
535
|
+
|
|
536
|
+
[Timer]
|
|
537
|
+
# 04:00 UTC daily, mirroring the repo's gds-db-backup.timer (low-traffic window).
|
|
538
|
+
OnCalendar=*-*-* 04:00:00 UTC
|
|
539
|
+
# Catch up on next boot if the box was off when the timer was due.
|
|
540
|
+
Persistent=true
|
|
541
|
+
# Jitter so simultaneous timers don't dogpile the disk at exactly 04:00.
|
|
542
|
+
RandomizedDelaySec=300
|
|
543
|
+
Unit=${backupUnitName(inst)}.service
|
|
544
|
+
|
|
545
|
+
[Install]
|
|
546
|
+
WantedBy=timers.target
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
|
|
450
550
|
// The branding pack a provisioned CO-TENANT instance loads: the shared, committed
|
|
451
551
|
// neutral starter (the co-tenant model shares ONE checkout, differentiated only by
|
|
452
552
|
// env — src/branding.js's GDS_BRANDING_FILE knob). Per-instance identity/domains
|
|
@@ -479,7 +579,7 @@ function provisionedEnvPrefix() {
|
|
|
479
579
|
// the hall on this host + OAuth uses the real callback origin — without them the pack
|
|
480
580
|
// stays at neutral localhost and the hall never loads (task 1972). No domain yet
|
|
481
581
|
// (bring-your-own deferred, ADR 0111 §4) ⇒ origins stay neutral until one is set.
|
|
482
|
-
function webEnvBody(inst, { clientId = '', clientSecret = '' } = {}) {
|
|
582
|
+
function webEnvBody(inst, { clientId = '', clientSecret = '', federation = null } = {}) {
|
|
483
583
|
const prefix = provisionedEnvPrefix();
|
|
484
584
|
const origin = inst.domain ? `https://${inst.domain}` : '';
|
|
485
585
|
const originLines = origin
|
|
@@ -500,7 +600,11 @@ ${prefix}_OAUTH_ORIGIN=${origin}
|
|
|
500
600
|
// Manifest flow (placeManifestCreds fetches them control-plane-side + re-writes this
|
|
501
601
|
// file with clientId/clientSecret set), or the manual oauth-secret.js placement. On
|
|
502
602
|
// the first provision write they are blank placeholders the flow fills later.
|
|
503
|
-
const credComment =
|
|
603
|
+
const credComment = federation
|
|
604
|
+
? `# Provisioned by scripts/gds/provision.js (ADR 0111). This instance FEDERATES sign-in
|
|
605
|
+
# through the Cloud Bongos hub (ADR 0141 §4) — it needs no GitHub app of its own, so the
|
|
606
|
+
# GitHub client id/secret below stay blank; the ${prefix}_HUB_* block drives sign-in.`
|
|
607
|
+
: clientId || clientSecret
|
|
504
608
|
? `# Provisioned by scripts/gds/provision.js (ADR 0111). OAuth app creds placed via the
|
|
505
609
|
# GitHub App Manifest flow (ADR 0133) — the owner clicked one link; no secret was pasted.`
|
|
506
610
|
: `# Provisioned by scripts/gds/provision.js (ADR 0111). Fill the OAuth app creds via the
|
|
@@ -519,6 +623,20 @@ ${prefix}_OAUTH_ORIGIN=${origin}
|
|
|
519
623
|
const firstAdminLine = ownerRepo && ownerRepo.owner
|
|
520
624
|
? `${prefix}_FIRST_ADMIN=${ownerRepo.owner}\n`
|
|
521
625
|
: '';
|
|
626
|
+
// Cross-site identity federation block (ADR 0141 §4, task #1002109). Present only for
|
|
627
|
+
// a HOSTED (federated) instance: flips auth.idp ON via the env override branding.js
|
|
628
|
+
// reads (idpEnabled), and carries the hub origin, this instance's per-client secret
|
|
629
|
+
// (authenticates it TO the hub, server-to-server), and the hub's PUBLIC key (verifies
|
|
630
|
+
// the CLI's hub-signed assertion offline). All written straight to this chmod-600
|
|
631
|
+
// file — never through a browser or chat (the ADR 0133 secret-placement posture).
|
|
632
|
+
const fedLines = federation
|
|
633
|
+
? `${prefix}_AUTH_IDP=true
|
|
634
|
+
${prefix}_HUB_ORIGIN=${federation.hubOrigin}
|
|
635
|
+
${prefix}_HUB_CLIENT_ID=${federation.clientId}
|
|
636
|
+
${prefix}_HUB_CLIENT_SECRET=${federation.clientSecret}
|
|
637
|
+
${prefix}_HUB_PUBLIC_KEY=${federation.hubPublicKey}
|
|
638
|
+
`
|
|
639
|
+
: '';
|
|
522
640
|
return `# /etc/${inst.slug}/web.env — per-instance secrets (chmod 600, OUTSIDE the repo).
|
|
523
641
|
${credComment}
|
|
524
642
|
${prefix}_GITHUB_CLIENT_ID=${clientId || ''}
|
|
@@ -526,12 +644,88 @@ ${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
|
|
|
526
644
|
${firstAdminLine}GDS_BRANDING_FILE=${PROVISIONED_BRANDING_FILE}
|
|
527
645
|
${originLines}PORT=${inst.port}
|
|
528
646
|
PGDATABASE=${dbName(inst)}
|
|
529
|
-
`;
|
|
647
|
+
${fedLines}`;
|
|
530
648
|
}
|
|
531
649
|
function webEnvPath(inst, { etcBase = CONFIG.etcBase } = {}) {
|
|
532
650
|
return path.posix.join(etcBase, inst.slug, 'web.env');
|
|
533
651
|
}
|
|
534
652
|
|
|
653
|
+
// ── Cross-site identity: federate a hosted instance's sign-in through the hub ───
|
|
654
|
+
// (ADR 0141 §4, task #1002109). The control plane IS the hub (cloudbongos.com), so
|
|
655
|
+
// registration is a direct in-process call against the hub pool — no HTTP hop.
|
|
656
|
+
|
|
657
|
+
// The hub's own public origin, written into each federated instance as
|
|
658
|
+
// <PREFIX>_HUB_ORIGIN (what auth.js loadIdpConfig reads). Explicit PROVISION_HUB_ORIGIN
|
|
659
|
+
// wins; otherwise the control plane's OWN public origin (<PREFIX>_PUBLIC_ORIGIN, the
|
|
660
|
+
// env the hub already runs under). Null → not hub-configured → shouldFederate is false.
|
|
661
|
+
function federationHubOrigin() {
|
|
662
|
+
const explicit = process.env.PROVISION_HUB_ORIGIN;
|
|
663
|
+
if (explicit) return String(explicit).replace(/\/+$/, '');
|
|
664
|
+
const own = process.env[`${provisionedEnvPrefix()}_PUBLIC_ORIGIN`];
|
|
665
|
+
return own ? String(own).replace(/\/+$/, '') : null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Whether provisioning should federate this instance's sign-in through the hub.
|
|
669
|
+
// Dormant-by-default: true ONLY when the control plane is hub-configured (the
|
|
670
|
+
// platform-identity module is present, a signing key is loadable, AND a hub origin
|
|
671
|
+
// resolves), the instance is a CO-TENANT (the mothership-hosted shape whose web.env is
|
|
672
|
+
// written locally — a `standalone` is owner-hosted and keeps its own GitHub app, the
|
|
673
|
+
// self-hosted path; a `dedicated` droplet has no local web.env write yet, same gap the
|
|
674
|
+
// ADR 0133 manifest flow leaves — see placeManifestCreds), and it has a public domain
|
|
675
|
+
// (the origin the hub binds the CLI mint audience to — the #1002108 account-takeover
|
|
676
|
+
// fix). Returns { federate, reason } so the caller can log WHY it skipped. PURE-ish
|
|
677
|
+
// (reads env + the injected module handles; no DB, no I/O).
|
|
678
|
+
function shouldFederate(inst, deps) {
|
|
679
|
+
const { platformIdentity, hubKeys } = deps;
|
|
680
|
+
if (!platformIdentity || !hubKeys) return { federate: false, reason: 'platform-identity module not present (control plane is not the hub)' };
|
|
681
|
+
if (!hubKeys.hubSigningConfigured()) return { federate: false, reason: 'hub signing key not configured (<PREFIX>_HUB_SIGNING_KEY unset)' };
|
|
682
|
+
if (!federationHubOrigin()) return { federate: false, reason: 'hub origin not resolvable (set PROVISION_HUB_ORIGIN or <PREFIX>_PUBLIC_ORIGIN)' };
|
|
683
|
+
if (inst.hosting_shape !== 'co-tenant') return { federate: false, reason: `${inst.hosting_shape} keeps its own GitHub app (not the co-tenant hosted path)` };
|
|
684
|
+
if (!inst.domain) return { federate: false, reason: 'no domain yet — federation needs the instance public origin' };
|
|
685
|
+
return { federate: true, reason: null };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Register this hosted instance as an sso_clients row on the HUB (fresh per-client
|
|
689
|
+
// secret) and return the federation block to bake into its web.env (ADR 0141 §4).
|
|
690
|
+
// SECURITY: `origin` MUST be the instance's EXACT public origin — the hub binds the
|
|
691
|
+
// CLI-federation mint audience to it (the #1002108 fix), so a mismatch would break
|
|
692
|
+
// device sign-in (and a wrong origin would be a takeover vector). client_id is the
|
|
693
|
+
// (unique, DNS-safe) slug, so a re-provision rotates the SAME row's secret via
|
|
694
|
+
// registerClient's ON CONFLICT rather than orphaning a client. redirect_uri lists both
|
|
695
|
+
// callback prefixes the core may emit (/api/gds is what auth.js builds today) so the
|
|
696
|
+
// hub's exact-match allow-list accepts the browser leg. --apply gates the real hub
|
|
697
|
+
// write; dry-run returns a placeholder block (so webEnvBody can still be composed) + logs.
|
|
698
|
+
async function federateInstance(inst, deps) {
|
|
699
|
+
const { db, platformIdentity, hubKeys, log, apply } = deps;
|
|
700
|
+
const origin = `https://${inst.domain}`;
|
|
701
|
+
const clientId = inst.slug;
|
|
702
|
+
const hubOrigin = federationHubOrigin();
|
|
703
|
+
const hubPublicKey = hubKeys.hubPublicKeyB64();
|
|
704
|
+
if (!hubPublicKey) throw new Error('hub signing key is set but unreadable — cannot derive HUB_PUBLIC_KEY');
|
|
705
|
+
const redirectUris = [
|
|
706
|
+
`${origin}/api/gds/auth/web/callback`,
|
|
707
|
+
`${origin}/api/bongos/auth/web/callback`,
|
|
708
|
+
];
|
|
709
|
+
if (!apply) {
|
|
710
|
+
log(` [federate] would register client '${clientId}' on the hub (origin ${origin}) + write ${provisionedEnvPrefix()}_HUB_* + AUTH_IDP into web.env`);
|
|
711
|
+
return { hubOrigin, clientId, clientSecret: '0'.repeat(43), hubPublicKey };
|
|
712
|
+
}
|
|
713
|
+
const clientSecret = crypto.randomBytes(32).toString('base64url');
|
|
714
|
+
// Best-effort owner github_id — nice for the hub catalog's ownership link, but
|
|
715
|
+
// nullable in registerClient, so a lookup miss must never fail the provision.
|
|
716
|
+
let ownerGithubId = null;
|
|
717
|
+
try {
|
|
718
|
+
const { rows } = await db.query('SELECT github_id FROM builders WHERE id = $1', [inst.owner_builder_id]);
|
|
719
|
+
ownerGithubId = rows[0] ? rows[0].github_id : null;
|
|
720
|
+
} catch { /* non-fatal */ }
|
|
721
|
+
await platformIdentity.registerClient(
|
|
722
|
+
{ clientId, secret: clientSecret, name: inst.slug, redirectUris, origin, ownerGithubId },
|
|
723
|
+
{ pool: db },
|
|
724
|
+
);
|
|
725
|
+
log(` [federate] registered client '${clientId}' on the hub (origin ${origin})`);
|
|
726
|
+
return { hubOrigin, clientId, clientSecret, hubPublicKey };
|
|
727
|
+
}
|
|
728
|
+
|
|
535
729
|
// The Cloudflare zone a hostname's A record lives in. Cloudflare zones are
|
|
536
730
|
// registrable domains, which for the common case is the last two DNS labels
|
|
537
731
|
// (mercury.example.com → example.com; apex example.com → example.com). Multi-part
|
|
@@ -842,6 +1036,28 @@ async function provisionInstance(inst, deps) {
|
|
|
842
1036
|
if (apply) await provisioning.setInstanceStatus(db, inst.id, 'provisioning');
|
|
843
1037
|
const patch = {};
|
|
844
1038
|
|
|
1039
|
+
// ── cross-site identity: register hub federation (ADR 0141 §4, task #1002109) ──
|
|
1040
|
+
// Computed BEFORE the shape steps so the co-tenant web.env write (step 3) bakes it
|
|
1041
|
+
// in — the instance comes up already federated, no post-hoc restart. Dormant unless
|
|
1042
|
+
// the control plane is hub-configured AND the shape/domain qualify (shouldFederate);
|
|
1043
|
+
// otherwise the instance owns its auth (direct GitHub), byte-identically to today.
|
|
1044
|
+
let federation = null;
|
|
1045
|
+
{
|
|
1046
|
+
const fed = shouldFederate(inst, deps);
|
|
1047
|
+
if (fed.federate) {
|
|
1048
|
+
federation = await federateInstance(inst, deps);
|
|
1049
|
+
log(` [federate] ${inst.slug} → hub sign-in (auth.idp on) — no GitHub app of its own`);
|
|
1050
|
+
if (apply) {
|
|
1051
|
+
await provisioning.recordEvent(db, {
|
|
1052
|
+
instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'federated',
|
|
1053
|
+
detail: `sign-in federated to the hub (client ${inst.slug}, origin https://${inst.domain})`, actor: 'runner',
|
|
1054
|
+
}).catch(() => {});
|
|
1055
|
+
}
|
|
1056
|
+
} else {
|
|
1057
|
+
log(` [federate] skipped (${fed.reason}) — instance owns its auth (direct GitHub)`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
845
1061
|
// ── step 1: server ───────────────────────────────────────────────────────
|
|
846
1062
|
// The A-record target for step 5's DNS UPSERT:
|
|
847
1063
|
// • co-tenant → PROVISION_PUBLIC_IP (the box it runs on — under remote-exec that
|
|
@@ -939,7 +1155,7 @@ async function provisionInstance(inst, deps) {
|
|
|
939
1155
|
|
|
940
1156
|
// ── step 3: config + per-instance secrets ─────────────────────────────────
|
|
941
1157
|
log(' [config] per-instance web.env');
|
|
942
|
-
if (local) boxWriteFile(webEnvPath(inst), webEnvBody(inst), { mode: 0o600, sudo: true });
|
|
1158
|
+
if (local) boxWriteFile(webEnvPath(inst), webEnvBody(inst, { federation }), { mode: 0o600, sudo: true });
|
|
943
1159
|
else log(' (baked into cloud-init)');
|
|
944
1160
|
|
|
945
1161
|
// ── step 4: systemd unit + enable ─────────────────────────────────────────
|
|
@@ -950,6 +1166,21 @@ async function provisionInstance(inst, deps) {
|
|
|
950
1166
|
boxExec(`${sudoP}systemctl enable --now ${inst.slug}`);
|
|
951
1167
|
} else { log(' (unit baked into cloud-init)'); }
|
|
952
1168
|
|
|
1169
|
+
// ── step 4b: nightly DB backup — dir + consumer-layout timer/service (task 2213) ──
|
|
1170
|
+
// Every instance must be born WITH backups (the task-2210 gap: cloudbongos was
|
|
1171
|
+
// provisioned without any). Create the app-user-owned backup dir, install the
|
|
1172
|
+
// per-instance timer/service (correct db + consumer-layout script path), enable
|
|
1173
|
+
// the timer. Idempotent: `install -d` no-ops on an existing dir, unit writes are
|
|
1174
|
+
// overwrites, `enable --now` is idempotent. Dedicated bakes the same into cloud-init.
|
|
1175
|
+
log(' [backup] create backup dir + install + enable nightly DB backup timer');
|
|
1176
|
+
if (local) {
|
|
1177
|
+
boxExec(`${sudoP}install -d -o ${APP_USER} -g ${APP_USER} -m 0750 ${CONFIG.backupDir}`);
|
|
1178
|
+
boxWriteFile(backupServicePath(inst), backupService(inst), { mode: 0o644, sudo: true });
|
|
1179
|
+
boxWriteFile(backupTimerPath(inst), backupTimer(inst), { mode: 0o644, sudo: true });
|
|
1180
|
+
boxExec(`${sudoP}systemctl daemon-reload`);
|
|
1181
|
+
boxExec(`${sudoP}systemctl enable --now ${backupUnitName(inst)}.timer`);
|
|
1182
|
+
} else { log(' (backup dir + units baked into cloud-init)'); }
|
|
1183
|
+
|
|
953
1184
|
// ── step 5: DNS — Cloudflare A-record UPSERT for the instance's domain (P4) ──
|
|
954
1185
|
// Formalized in P4 (#1945): the shared box-dns-cloudflare.sh hook UPSERTs an A
|
|
955
1186
|
// record pointing the domain at its target IP (recordIp), in the domain's own
|
|
@@ -1009,12 +1240,28 @@ async function provisionInstance(inst, deps) {
|
|
|
1009
1240
|
// SAME pure command generators so local + dedicated never diverge.
|
|
1010
1241
|
function dedicatedUserData(inst) {
|
|
1011
1242
|
const i = { ...inst, db_name: inst.db_name || inst.slug };
|
|
1243
|
+
// task 2213: a dedicated droplet gets the same nightly backup as local shapes —
|
|
1244
|
+
// write the per-instance unit pair (base64-encoded to sidestep YAML block-scalar
|
|
1245
|
+
// escaping of the multi-line bodies), create the app-user-owned backup dir, and
|
|
1246
|
+
// enable the timer alongside the web unit. Mirrors provisionInstance's step 4b.
|
|
1247
|
+
const svcB64 = Buffer.from(backupService(i), 'utf8').toString('base64');
|
|
1248
|
+
const timerB64 = Buffer.from(backupTimer(i), 'utf8').toString('base64');
|
|
1012
1249
|
return `#cloud-config
|
|
1250
|
+
write_files:
|
|
1251
|
+
- path: ${backupServicePath(i)}
|
|
1252
|
+
encoding: b64
|
|
1253
|
+
permissions: '0644'
|
|
1254
|
+
content: ${svcB64}
|
|
1255
|
+
- path: ${backupTimerPath(i)}
|
|
1256
|
+
encoding: b64
|
|
1257
|
+
permissions: '0644'
|
|
1258
|
+
content: ${timerB64}
|
|
1013
1259
|
runcmd:
|
|
1014
1260
|
- [ bash, -lc, "${dbCreateCmd(i)} || true" ]
|
|
1015
1261
|
- [ bash, -lc, "cd ${REPO_ROOT} && ${migrateCmd(i)}" ]
|
|
1016
1262
|
- [ bash, -lc, "${seedFirstVersionCmd(i)}" ]
|
|
1017
|
-
- [ bash, -lc, "
|
|
1263
|
+
- [ bash, -lc, "install -d -o ${APP_USER} -g ${APP_USER} -m 0750 ${CONFIG.backupDir}" ]
|
|
1264
|
+
- [ bash, -lc, "systemctl daemon-reload && systemctl enable --now ${i.slug} ${backupUnitName(i)}.timer" ]
|
|
1018
1265
|
`;
|
|
1019
1266
|
}
|
|
1020
1267
|
|
|
@@ -1034,10 +1281,16 @@ async function teardownInstance(inst, deps) {
|
|
|
1034
1281
|
if (inst.hosting_shape !== 'dedicated') {
|
|
1035
1282
|
boxExec(`${sudoP}systemctl disable --now ${inst.slug}`, { allowFail: true });
|
|
1036
1283
|
boxExec(`${sudoP}rm -f ${serviceUnitPath(inst)}`, { allowFail: true });
|
|
1284
|
+
// Stop + remove the nightly backup units too (task 2213). The backup DIR + its
|
|
1285
|
+
// dumps are left in place — like the DB below, they are precious data a teardown
|
|
1286
|
+
// must not silently destroy.
|
|
1287
|
+
boxExec(`${sudoP}systemctl disable --now ${backupUnitName(inst)}.timer`, { allowFail: true });
|
|
1288
|
+
boxExec(`${sudoP}rm -f ${backupServicePath(inst)} ${backupTimerPath(inst)}`, { allowFail: true });
|
|
1037
1289
|
boxExec(`${sudoP}systemctl daemon-reload`, { allowFail: true });
|
|
1038
|
-
// DB + Caddy block are left for an explicit operator (data is
|
|
1039
|
-
// teardown should not silently dropdb).
|
|
1040
|
-
|
|
1290
|
+
// DB + Caddy block + backup dumps are left for an explicit operator (data is
|
|
1291
|
+
// precious; a teardown should not silently dropdb or delete backups). Log the
|
|
1292
|
+
// manual follow-ups.
|
|
1293
|
+
log(` (left for the operator: dropdb ${dbName(inst)}, remove the Caddy block for ${inst.slug}, prune ${CONFIG.backupDir}/${dbName(inst)}-*.sql.gz)`);
|
|
1041
1294
|
} else if (apply && deps.doApi && inst.droplet_id) {
|
|
1042
1295
|
await deps.doApi.deleteDroplet(inst.droplet_id);
|
|
1043
1296
|
}
|
|
@@ -1356,8 +1609,8 @@ async function main() {
|
|
|
1356
1609
|
const slug = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
1357
1610
|
if ((cmd === 'provision' || cmd === 'teardown') && !slug) { console.error(`${cmd} needs a <slug>`); process.exit(2); }
|
|
1358
1611
|
|
|
1359
|
-
let pool, provisioning, DoApi;
|
|
1360
|
-
try { ({ pool, provisioning, DoApi } = loadDeps()); }
|
|
1612
|
+
let pool, provisioning, DoApi, platformIdentity, hubKeys;
|
|
1613
|
+
try { ({ pool, provisioning, DoApi, platformIdentity, hubKeys } = loadDeps()); }
|
|
1361
1614
|
catch (e) { console.error(`cannot load DB layer (run this on the control plane): ${e.message}`); process.exit(1); }
|
|
1362
1615
|
|
|
1363
1616
|
// Box-target executor (remote over SSH when PROVISION_REMOTE_HOST is set, else
|
|
@@ -1372,6 +1625,9 @@ async function main() {
|
|
|
1372
1625
|
writeFile: makeWriteFile({ apply, privileged: CONFIG.privileged, exec, remoteHost: CONFIG.remoteHost }),
|
|
1373
1626
|
controlWriteFile: makeWriteFile({ apply, privileged: CONFIG.privileged, exec: controlExec }),
|
|
1374
1627
|
doApi: DoApi ? new DoApi({ dryRun: !apply, token: CONFIG.doToken, log: (m) => console.log(' ' + m) }) : null,
|
|
1628
|
+
// Cross-site identity (ADR 0141 §4). Null on a non-hub control plane → federation
|
|
1629
|
+
// dormant; shouldFederate() then skips and the instance owns its auth (as today).
|
|
1630
|
+
platformIdentity, hubKeys,
|
|
1375
1631
|
};
|
|
1376
1632
|
|
|
1377
1633
|
let result;
|
|
@@ -1396,6 +1652,7 @@ module.exports = {
|
|
|
1396
1652
|
CONFIG, MAX_INTENT_ATTEMPTS,
|
|
1397
1653
|
// pure step generators (tested directly)
|
|
1398
1654
|
dbName, dbCreateCmd, migrateCmd, seedFirstVersionCmd, serviceUnit, serviceUnitPath, webEnvBody, webEnvPath,
|
|
1655
|
+
backupUnitName, backupServicePath, backupTimerPath, backupScriptPath, backupService, backupTimer,
|
|
1399
1656
|
standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
|
|
1400
1657
|
// standalone scaffold (ADR 0108 / 0125, task 2055)
|
|
1401
1658
|
parseTargetRef, instanceRepoRemote, instanceInitSpec, scaffoldStandaloneRepo, coreVersionSafe,
|
|
@@ -1406,4 +1663,6 @@ module.exports = {
|
|
|
1406
1663
|
cmdRunIntents, cmdReconcile, cmdStatus,
|
|
1407
1664
|
// GitHub App Manifest flow (task 2080, ADR 0133)
|
|
1408
1665
|
convertManifestCode, placeManifestCreds, drainOAuthManifests, MANIFEST_UA,
|
|
1666
|
+
// cross-site identity federation (ADR 0141 §4, task #1002109)
|
|
1667
|
+
federationHubOrigin, shouldFederate, federateInstance,
|
|
1409
1668
|
};
|
|
@@ -250,6 +250,21 @@ const INSTANCE_EXCLUDE = [
|
|
|
250
250
|
'docs/design/container-exterior-tiles.md',
|
|
251
251
|
'docs/design/container-interior.md',
|
|
252
252
|
'docs/design/world-map-scroll.md',
|
|
253
|
+
// instance-HISTORY ADRs (task 1002219 / audit H5). These record THIS platform-family's own
|
|
254
|
+
// migration + identity history (the amazonprimea→cloudbongos lift, the standalone reprovision,
|
|
255
|
+
// the cloudbongos identity hub) — they are inherently full of the prior instance's name/owner and
|
|
256
|
+
// genericize to nothing (unlike a portable methodology ADR). docs/adr/ is allowlisted wholesale for
|
|
257
|
+
// the portable decision records; these three stay in this repo and never ship in the core, mirroring
|
|
258
|
+
// the instance-recipe / game-design carve-outs above. (The portable ADRs + the README index still publish.)
|
|
259
|
+
'docs/adr/0139-platform-history-migration-to-cloudbongos.md',
|
|
260
|
+
'docs/adr/0140-cloudbongos-standalone-reprovision.md',
|
|
261
|
+
'docs/adr/0141-cross-site-identity-federated-hub-idp.md',
|
|
262
|
+
// hall-preview FIXTURES (task 1002219 / audit H5) — the local hall-UI preview harness's sample
|
|
263
|
+
// data is a snapshot of THIS instance's builders/goals/tasks (owner login, world name, prior
|
|
264
|
+
// instance name), pure instance identity. The harness code stays portable; only the OTB data is
|
|
265
|
+
// carved out, so a consumer core never ships customer-zero's directory. (Fixtures remain in-repo
|
|
266
|
+
// for local dev — this only affects what package-core publishes.)
|
|
267
|
+
'scripts/hall-preview/fixtures/',
|
|
253
268
|
'blockers/',
|
|
254
269
|
'limitations/',
|
|
255
270
|
'generative-ideas.md',
|
package/scripts/gds/push-main.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -31,6 +31,10 @@ const { isModuleEnabled } = require('../../src/modules');
|
|
|
31
31
|
const TESTS_DIR = path.resolve(__dirname, '..', '..', 'tests');
|
|
32
32
|
const MODULES_DIR = path.resolve(__dirname, '..', '..', 'modules');
|
|
33
33
|
const PER_TEST_TIMEOUT_MS = 90_000;
|
|
34
|
+
// Integration tests (package build + `npm install` + a spawned server) are far heavier than a
|
|
35
|
+
// DB-free unit file, so the `--integration` gate (task 1002221 / audit H1) gives each one a much
|
|
36
|
+
// longer budget than the 90s unit cap.
|
|
37
|
+
const INTEGRATION_TIMEOUT_MS = 300_000;
|
|
34
38
|
|
|
35
39
|
// Feature-module test sets (R59 / task 1198). A test that exercises a feature
|
|
36
40
|
// MODULE runs only when this instance enables that module — a vanilla Cloud
|
|
@@ -128,31 +132,50 @@ function discoverModuleTests({ modulesDir = MODULES_DIR, isEnabled = isModuleEna
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
function main() {
|
|
135
|
+
// Two gates from one runner (task 1002221 / audit H1):
|
|
136
|
+
// default — the DB-free UNIT gate: every test file MINUS the integration set (+ known-
|
|
137
|
+
// failing / support / disabled-module). Fast, runs in a clean checkout / per PR.
|
|
138
|
+
// --integration — the standing INTEGRATION-SMOKE gate: ONLY the integration set (package build +
|
|
139
|
+
// `npm install` + live boot etc.). Heavier, longer per-test budget; run per
|
|
140
|
+
// release / on a schedule in an env that provides the services (DB/network). This
|
|
141
|
+
// is what promotes tests/consumer_layout_boot.mjs from "excluded + ad-hoc" to a
|
|
142
|
+
// first-class gate a release step can invoke with one command.
|
|
143
|
+
const integrationMode = process.argv.includes('--integration');
|
|
144
|
+
const perTestTimeout = integrationMode ? INTEGRATION_TIMEOUT_MS : PER_TEST_TIMEOUT_MS;
|
|
145
|
+
|
|
131
146
|
const files = fs
|
|
132
147
|
.readdirSync(TESTS_DIR)
|
|
133
148
|
.filter((f) => f.endsWith('.mjs'))
|
|
134
149
|
.map((f) => f.replace(/\.mjs$/, ''))
|
|
135
|
-
.filter((name) =>
|
|
136
|
-
&& !
|
|
150
|
+
.filter((name) => integrationMode
|
|
151
|
+
? (INTEGRATION.has(name) && !KNOWN_FAILING.has(name) && !SUPPORT_MODULES.has(name))
|
|
152
|
+
: (!INTEGRATION.has(name) && !KNOWN_FAILING.has(name) && !SUPPORT_MODULES.has(name) && !MODULE_DISABLED.has(name)))
|
|
137
153
|
.sort();
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
// Module-owned tests are DB-free unit tests — only the default gate runs them.
|
|
156
|
+
const { included: moduleTests, skippedDisabled: moduleTestsSkipped } = integrationMode
|
|
157
|
+
? { included: [], skippedDisabled: [] }
|
|
158
|
+
: discoverModuleTests();
|
|
159
|
+
|
|
160
|
+
if (integrationMode) {
|
|
161
|
+
console.log(`run-unit-tests --integration: ${files.length} integration-smoke test file(s) (${files.join(', ') || 'none found'}); per-test budget ${INTEGRATION_TIMEOUT_MS / 1000}s`);
|
|
162
|
+
} else {
|
|
163
|
+
const moduleNote = MODULE_DISABLED.size
|
|
164
|
+
? ` + ${MODULE_DISABLED.size} disabled-module (${[...MODULE_DISABLED].sort().join(', ')})`
|
|
165
|
+
: '';
|
|
166
|
+
const modOwnedSkipNote = moduleTestsSkipped.length
|
|
167
|
+
? ` + ${moduleTestsSkipped.length} module-owned test(s) skipped (module disabled: ${[...new Set(moduleTestsSkipped.map((t) => t.key))].sort().join(', ')})`
|
|
168
|
+
: '';
|
|
169
|
+
console.log(`run-unit-tests: ${files.length + moduleTests.length} DB-free unit test file(s) (${files.length} core + ${moduleTests.length} module-owned; excluded ${INTEGRATION.size} integration + ${KNOWN_FAILING.size} known-failing${moduleNote}${modOwnedSkipNote})`);
|
|
170
|
+
}
|
|
147
171
|
const total = files.length + moduleTests.length;
|
|
148
|
-
console.log(`run-unit-tests: ${total} DB-free unit test file(s) (${files.length} core + ${moduleTests.length} module-owned; excluded ${INTEGRATION.size} integration + ${KNOWN_FAILING.size} known-failing${moduleNote}${modOwnedSkipNote})`);
|
|
149
172
|
|
|
150
173
|
const failures = [];
|
|
151
174
|
const runOne = (displayName, filePath) => {
|
|
152
175
|
try {
|
|
153
176
|
execFileSync(process.execPath, [filePath], {
|
|
154
177
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
155
|
-
timeout:
|
|
178
|
+
timeout: perTestTimeout,
|
|
156
179
|
env: { ...process.env, NODE_ENV: 'test' },
|
|
157
180
|
});
|
|
158
181
|
console.log(` ok ${displayName}`);
|
|
@@ -168,12 +191,13 @@ function main() {
|
|
|
168
191
|
for (const name of files) runOne(name, path.join(TESTS_DIR, `${name}.mjs`));
|
|
169
192
|
for (const { key, name, file } of moduleTests) runOne(`${key}/${name}`, file);
|
|
170
193
|
|
|
194
|
+
const label = integrationMode ? 'run-unit-tests --integration' : 'run-unit-tests';
|
|
171
195
|
console.log('');
|
|
172
196
|
if (failures.length === 0) {
|
|
173
|
-
console.log(
|
|
197
|
+
console.log(`${label}: all ${total} passed.`);
|
|
174
198
|
process.exit(0);
|
|
175
199
|
}
|
|
176
|
-
console.log(
|
|
200
|
+
console.log(`${label}: ${failures.length} FAILED — ${failures.join(', ')}`);
|
|
177
201
|
process.exit(1);
|
|
178
202
|
}
|
|
179
203
|
|
|
File without changes
|
package/scripts/gds/session.js
CHANGED
|
File without changes
|
package/scripts/gds/setup.js
CHANGED
|
@@ -378,6 +378,47 @@ async function promptOnboardingConsent() {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
// Federation (ADR 0141 §1, task #1002108): a HOSTED instance has no GitHub app of
|
|
382
|
+
// its own — it federates sign-in through the Cloud Bongos hub. If the instance
|
|
383
|
+
// manifest advertises auth.idp, run the hub device flow + assertion exchange via
|
|
384
|
+
// the shared login.js runDeviceFlow, then the SAME onboarding prompts. The direct
|
|
385
|
+
// GitHub device flow below (auth.idp unset) is untouched — the self-hosted default.
|
|
386
|
+
let idpCfg = null;
|
|
387
|
+
try {
|
|
388
|
+
const m = await fetch(`${API_BASE}/api/gds/instance`);
|
|
389
|
+
const mj = m.ok ? await m.json() : null;
|
|
390
|
+
idpCfg = (mj && mj.auth && mj.auth.idp) || null;
|
|
391
|
+
} catch (_) { /* manifest unreachable → fall through to the direct flow */ }
|
|
392
|
+
if (idpCfg) {
|
|
393
|
+
console.log('Signing in via the Cloud Bongos platform (hub)...');
|
|
394
|
+
const { runDeviceFlow } = require('./login');
|
|
395
|
+
const flow = await runDeviceFlow(API_BASE, { log: console.log, idp: idpCfg });
|
|
396
|
+
if (flow.state === 'not_approved') {
|
|
397
|
+
console.error('');
|
|
398
|
+
console.error('This GitHub account has not been approved yet.');
|
|
399
|
+
console.error('An Archon must approve your access request before you can enroll.');
|
|
400
|
+
console.error(' → Ask an Archon to invite you, then re-run /builder-setup.');
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
if (flow.state !== 'ok') {
|
|
404
|
+
console.error(`\nSign-in did not complete (${flow.state}). Re-run /builder-setup.`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const { token, builder } = flow;
|
|
408
|
+
await saveSession({ token, builder, api_base: API_BASE });
|
|
409
|
+
console.log('');
|
|
410
|
+
console.log(`Authenticated as ${builder.github_login} (#${builder.id}). Session saved to ${SESSION_PATH}.`);
|
|
411
|
+
try {
|
|
412
|
+
const { activate } = require('./install-git-hooks');
|
|
413
|
+
activate();
|
|
414
|
+
} catch (_) { /* hook activation is best-effort */ }
|
|
415
|
+
await promptGeminiKey();
|
|
416
|
+
await promptAnthropicLink();
|
|
417
|
+
await promptDisciplines();
|
|
418
|
+
await promptOnboardingConsent();
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
|
|
381
422
|
console.log('Starting GitHub Device Flow...');
|
|
382
423
|
const start = await (await cliClient()).auth.postAuthDeviceStart();
|
|
383
424
|
if (!start.ok) {
|