@bongos/core 1.19.1080 → 1.20.1
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 +573 -188
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +34 -0
- package/clients/bongos-client/index.cjs +34 -0
- package/clients/bongos-client/index.d.ts +53 -4
- package/clients/bongos-client/index.mjs +34 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +2 -0
- package/docs/adr/0347-every-store-module-ships-a-how-to.md +135 -0
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +995 -40
- package/docs/api-reference.md +33 -8
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1674 -1566
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/migrations/provisioning_028_render_standup.sql +46 -0
- package/modules/provisioning/module.json +4 -2
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/provisioning.js +5 -5
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +134 -0
- package/modules/provisioning/routes/render-standup.js +165 -0
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +173 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +189 -0
- package/scripts/gds/provision-teardown.js +9 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +12 -12
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +197 -0
- package/scripts/gds/render-payload.js +84 -0
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +362 -0
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +336 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_api.mjs +153 -0
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
|
@@ -390,6 +390,36 @@ function upsertEnvVars(body, vars) {
|
|
|
390
390
|
return text;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// Env names a rename retired, carried forward ON THE BOX by the settings push (task
|
|
394
|
+
// 1004395). The prelaunch-gate rename (task 1003152, ADR 0182) made PRELAUNCH_PASSWORD
|
|
395
|
+
// primary and kept STEALTH_PASSWORD as an alias; the alias can only be dropped (task
|
|
396
|
+
// 1003830) once every live web.env carries the new name, and before this the only way
|
|
397
|
+
// to add it was the owner's SSH (blocker 1000147). [from, to] pairs.
|
|
398
|
+
const ENV_RENAMES = Object.freeze([['STEALTH_PASSWORD', 'PRELAUNCH_PASSWORD']]);
|
|
399
|
+
|
|
400
|
+
// Which renamed names does this web.env still hold ONLY under the old name? Returns
|
|
401
|
+
// { vars, carried }: `vars` is the new name → the old line's right-hand side, copied
|
|
402
|
+
// verbatim (quotes and all, so systemd reads the same value), for upsertEnvVars;
|
|
403
|
+
// `carried` names the pairs and is the ONLY part that may reach a log or an event —
|
|
404
|
+
// the value is a secret read on the box and it never leaves it. A new name that is
|
|
405
|
+
// already set wins, even when it differs. The last line of a key counts, as systemd
|
|
406
|
+
// reads it. PURE.
|
|
407
|
+
function renamedEnvCarry(body) {
|
|
408
|
+
const lines = String(body == null ? '' : body).split(/\r?\n/);
|
|
409
|
+
const rhs = (key) => {
|
|
410
|
+
let v = null;
|
|
411
|
+
for (const l of lines) if (l.startsWith(`${key}=`)) v = l.slice(key.length + 1);
|
|
412
|
+
return v;
|
|
413
|
+
};
|
|
414
|
+
const isSet = (v) => v !== null && !['', '""', "''"].includes(v.trim());
|
|
415
|
+
const vars = {}, carried = [];
|
|
416
|
+
for (const [from, to] of ENV_RENAMES) {
|
|
417
|
+
const oldV = rhs(from);
|
|
418
|
+
if (isSet(oldV) && !isSet(rhs(to))) { vars[to] = oldV; carried.push(`${from} -> ${to}`); }
|
|
419
|
+
}
|
|
420
|
+
return { vars, carried };
|
|
421
|
+
}
|
|
422
|
+
|
|
393
423
|
// ── Cross-site identity: federate a hosted instance's sign-in through the hub ───
|
|
394
424
|
// (ADR 0141 §4, task #1002109). The control plane IS the hub (cloudbongos.com), so
|
|
395
425
|
// registration is a direct in-process call against the hub pool — no HTTP hop.
|
|
@@ -415,6 +445,7 @@ module.exports = {
|
|
|
415
445
|
serviceUnitPath,
|
|
416
446
|
settingsConsumed,
|
|
417
447
|
settingsEnvVarsFor,
|
|
448
|
+
renamedEnvCarry,
|
|
418
449
|
upsertEnvVars,
|
|
419
450
|
webEnvBody,
|
|
420
451
|
webEnvPath,
|
package/scripts/gds/provision.js
CHANGED
|
@@ -50,7 +50,7 @@ const crypto = require('node:crypto');
|
|
|
50
50
|
|
|
51
51
|
const { CONFIG, MANIFEST_UA, softFailResult, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
52
52
|
const { accountPreflight, appGroupExpr, appUser, alreadyScaffolded, buildCorePinRefreshCommit, coreCheckoutHasGit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
53
|
-
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
53
|
+
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, renamedEnvCarry, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
54
54
|
const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, ingressPlaneGap, ingressPreflight, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
55
55
|
const { resolveProcessManager } = require('./provision-process.js');
|
|
56
56
|
const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.js');
|
|
@@ -374,10 +374,10 @@ async function provisionInstance(inst, deps) {
|
|
|
374
374
|
// throw: the greenfield flow now has no manual clone step. Idempotent + dry-run-safe.
|
|
375
375
|
if (inst.hosting_shape === 'cloud-host') {
|
|
376
376
|
await scaffoldStandaloneRepo(inst, deps);
|
|
377
|
-
// A PRIVATE target_ref gets its own read-only deploy key BEFORE step 2's
|
|
378
|
-
//
|
|
379
|
-
// on every private standup (walk finding N4; ADR 0176).
|
|
377
|
+
// A PRIVATE target_ref gets its own read-only deploy key BEFORE step 2's `git pull
|
|
378
|
+
// --ff-only origin main`, which failed unauthenticated on every private standup (N4; ADR 0176).
|
|
380
379
|
await ensurePrivateRepoAccess(inst, deps);
|
|
380
|
+
await require('./provision-pin-key.js').ensurePinWriteKey(inst, deps); // the hub's WRITE key for later pin pushes, public repos too (task 1004291, ADR 0350)
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
if (!inst.db_name) { patch.db_name = inst.slug; inst = { ...inst, db_name: inst.slug }; }
|
|
@@ -904,7 +904,7 @@ async function settingsApplyInstance(inst, deps) {
|
|
|
904
904
|
// exists to avoid). An unreadable or empty file is refused, never "patched":
|
|
905
905
|
// writing two lines over a missing PORT/PGDATABASE would take the instance down.
|
|
906
906
|
const read = boxExec(`${sudoP}cat ${webEnvPath(inst)}`, { allowFail: true });
|
|
907
|
-
const current = String((read && read.stdout) || '');
|
|
907
|
+
const current = String((read && read.stdout) || ''), carry = renamedEnvCarry(current); // task 1004395: only its NAMES are ever logged
|
|
908
908
|
if (!apply) {
|
|
909
909
|
log(` [config] would patch ${Object.keys(vars).join(', ')} in ${webEnvPath(inst)} in place (every other line untouched)`);
|
|
910
910
|
pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
|
|
@@ -916,8 +916,8 @@ async function settingsApplyInstance(inst, deps) {
|
|
|
916
916
|
return { ok: false, error: `cannot read ${webEnvPath(inst)} — nothing to patch (was this instance provisioned by the runner?)` };
|
|
917
917
|
}
|
|
918
918
|
try {
|
|
919
|
-
log(
|
|
920
|
-
boxWriteFile(webEnvPath(inst), upsertEnvVars(current, settingsEnvVarsFor(inst, current)), { mode: 0o600, sudo: true });
|
|
919
|
+
log(` [config] patch the settings lines of web.env in place (creds + origins untouched)${carry.carried.length ? `; carry the renamed env forward: ${carry.carried.join(', ')} (value copied on the box, never shown)` : ''}`);
|
|
920
|
+
boxWriteFile(webEnvPath(inst), upsertEnvVars(current, { ...settingsEnvVarsFor(inst, current), ...carry.vars }), { mode: 0o600, sudo: true });
|
|
921
921
|
log(' [service] restart so the memoized branding pack re-reads them');
|
|
922
922
|
pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
|
|
923
923
|
} catch (e) {
|
|
@@ -960,7 +960,7 @@ async function settingsApplyInstance(inst, deps) {
|
|
|
960
960
|
if (provisioning.clearErrorNote) await provisioning.clearErrorNote(db, inst.id, 'settings-apply').catch(() => {});
|
|
961
961
|
await provisioning.recordEvent(db, {
|
|
962
962
|
instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'settings-applied',
|
|
963
|
-
detail: `settings pushed (${shown}) — web.env patched in place; ${outcome}`,
|
|
963
|
+
detail: `settings pushed (${shown}) — web.env patched in place${carry.carried.length ? `; carried ${carry.carried.join(', ')}` : ''}; ${outcome}`,
|
|
964
964
|
actor: 'runner',
|
|
965
965
|
}).catch(() => {});
|
|
966
966
|
log(healthy && consumed !== false
|
|
@@ -1285,7 +1285,7 @@ async function cmdRunIntents(deps) {
|
|
|
1285
1285
|
// Dispatch EXPLICITLY per action — an unrecognized action must surface as an
|
|
1286
1286
|
// error, never fall through to the destructive teardown leg (task 1002692).
|
|
1287
1287
|
let result;
|
|
1288
|
-
if (intent.action === 'provision') result = await provisionInstance(inst, deps);
|
|
1288
|
+
if (intent.action === 'provision') result = await require('./provision-render.js').afterProvision(inst, deps, intent, await provisionInstance(inst, deps)); // then the app on Render, when the wizard lent a key (task 1004352)
|
|
1289
1289
|
else if (intent.action === 'teardown') result = await teardownInstance(inst, deps);
|
|
1290
1290
|
else if (intent.action === 'domain-attach') result = await domainAttachInstance(inst, deps);
|
|
1291
1291
|
else if (intent.action === 'settings-apply') result = await settingsApplyInstance(inst, deps);
|
|
@@ -1293,10 +1293,10 @@ async function cmdRunIntents(deps) {
|
|
|
1293
1293
|
else if (intent.action === 'disconnect') result = await disconnectInstance(inst, deps);
|
|
1294
1294
|
else if (intent.action === 'core-upgrade') result = await coreUpgradeInstance(inst, deps, intent); // also reads the INTENT: the version the owner previewed (ADR 0293 D4)
|
|
1295
1295
|
else if (intent.action === 'core-upgrade-preflight') result = await preflightCoreUpgrade(inst, deps, intent); // changes nothing; caches what the move WOULD say (task 1004121)
|
|
1296
|
-
else if (intent.action === 'repo-private') result = await require('./provision-repo-private.js').repoPrivateInstance(inst, deps); // wire the deploy key, THEN flip (task 1004193); required here — this file is at the size ratchet
|
|
1296
|
+
else if (intent.action === 'repo-private' || intent.action === 'render-standup') result = intent.action === 'repo-private' ? await require('./provision-repo-private.js').repoPrivateInstance(inst, deps) : await require('./provision-render.js').renderStandupInstance(inst, deps, intent); // wire the deploy key, THEN flip (task 1004193) · spend the borrowed Render key once (task 1004183); required here — this file is at the size ratchet
|
|
1297
1297
|
else result = { ok: false, error: `unknown_action '${intent.action}' — runner core ${coreVersionSafe() || '?'} does not know it (runner too old?); refusing to guess` };
|
|
1298
1298
|
if (result && result.ok === false) {
|
|
1299
|
-
if (intent.attempts >= MAX_INTENT_ATTEMPTS) {
|
|
1299
|
+
if (intent.attempts >= MAX_INTENT_ATTEMPTS || result.terminal) { // terminal: a leg that must not be retried unattended (the Render leg's key is already gone — task 1004183)
|
|
1300
1300
|
await provisioning.resolveIntent(db, intent.id, 'error', result.error || 'op failed');
|
|
1301
1301
|
// A retired intent stops silently otherwise (F19): record the reason
|
|
1302
1302
|
// on the instance WITHOUT changing its lifecycle status — passing the
|
|
@@ -1489,7 +1489,7 @@ module.exports = {
|
|
|
1489
1489
|
makeExec, makeWriteFile, allocatePort, boundLocalPorts, provisionInstance, teardownInstance,
|
|
1490
1490
|
domainAttachInstance, releasableDomains, cmdRunIntents, cmdReconcile, cmdStatus,
|
|
1491
1491
|
// live settings push (task 1003140) + the in-place env patch + read-back it rides on
|
|
1492
|
-
settingsApplyInstance, upsertEnvVars, settingsEnvVarsFor, settingsConsumed, instanceManifestCmd,
|
|
1492
|
+
settingsApplyInstance, upsertEnvVars, renamedEnvCarry, settingsEnvVarsFor, settingsConsumed, instanceManifestCmd,
|
|
1493
1493
|
// restart an unreachable ACTIVE instance (task 1003238) — the settings leg minus the env patch
|
|
1494
1494
|
restartInstance,
|
|
1495
1495
|
// GitHub App Manifest flow (task 2080, ADR 0133)
|
|
@@ -72,6 +72,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
72
72
|
'modules/provisioning/', // instance-hosting control plane — CORE (its do-api toolchain is core). Reclassified IN so the core CLI scripts/gds/provision.js resolves it from the core package post-split, not a MODULE_NOT_FOUND (task 2135). default:false — shipped ≠ enabled.
|
|
73
73
|
'modules/agents/', // the declarative agent registry + spawn seam (goal 1000038) — CORE: its own manifest declares maintenance.status 'core-maintained', and it consumes the core `grade` port, so a released core without it cannot run the MAS machinery the methodology describes. default:false — shipped is not enabled (ADR 0083), exactly like provisioning + platform-identity below. Omitted at module-creation time (tasks 1002485/1002486): the omission was invisible until tests/agents_spawn.mjs became the first PUBLISHABLE file to import it and the R89 boundary guard caught it (task 1002489).
|
|
74
74
|
'modules/npm-release/', // the deploy page's "where is the work" + version history (task 1002622). default:false — on only in the hall that publishes the core. MUST be here: modules/hall-ui/ ships as one subtree, so a released core without this carries deploy.js's loader and the #nr-work mounts with no routes behind them — the copy-desk asymmetry above.
|
|
75
|
+
'modules/render-deploy/', // the deploy page of a project whose app deploys through Render (task 1004270). default:false. MUST be here for the npm-release reason: modules/hall-ui/ ships as one subtree, so a released core without it carries deploy.js's loader for #rd-panel with no routes behind it.
|
|
75
76
|
'modules/platform-identity/', // hub identity provider (ADR 0141) — ships in core, enabled ONLY on the hub (cloudbongos.com). MUST be here or the hub's enabledModules() throws "unknown module" and provision.js's federation load MODULE_NOT_FOUNDs (task 1002235, same rationale as provisioning above). default:false — shipped ≠ enabled; a vanilla/self-hosted instance never turns it on.
|
|
76
77
|
// --- neutral (customer-0) config starters + their schemas (the filled-in *.json twins are denied) ---
|
|
77
78
|
'config/branding.neutral.json',
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// scripts/gds/render-api.js — a thin Render API v1 client for the control plane, with
|
|
2
|
+
// the workspace (ownerId) guard (task 1004183; ADR 0327).
|
|
3
|
+
//
|
|
4
|
+
// Control-plane code, like its model scripts/gds/do-api.js: only the provisioning runner
|
|
5
|
+
// uses it, never the web tier (ADR 0111 §2). Same skeleton (JsonHttpApi): injectable
|
|
6
|
+
// transport, dry-run by default, and live mode refuses to start without a token.
|
|
7
|
+
//
|
|
8
|
+
// THE GUARD. A Render API key belongs to a USER, not a workspace, and carries that user's
|
|
9
|
+
// rights in every workspace they belong to — measured 2026-09-22: one owner's key reached
|
|
10
|
+
// three workspaces, one of them another builder's, holding live production services. So:
|
|
11
|
+
//
|
|
12
|
+
// - the client is built for ONE workspace (`ownerId`) and refuses to exist without it —
|
|
13
|
+
// never defaulted, and never derived by taking the first of GET /owners;
|
|
14
|
+
// - every call that names an owner sends this one;
|
|
15
|
+
// - every response that carries an owner is ASSERTED to carry this one, and a missing
|
|
16
|
+
// or different owner is a hard error (RenderOwnerMismatch). Assert on the response,
|
|
17
|
+
// not the request: a request you got right says nothing about a response that came
|
|
18
|
+
// from somewhere else. A list is checked element by element.
|
|
19
|
+
//
|
|
20
|
+
// Render's own shapes decide where the owner sits: a service carries `ownerId`, a
|
|
21
|
+
// Postgres carries `owner.id` (both measured by the pilot, task 1004142). A few calls
|
|
22
|
+
// answer with no owner at all (a database's connection-info, an env-var PUT, a deploy
|
|
23
|
+
// trigger). Those are only reachable through a method that first reads the parent
|
|
24
|
+
// resource through the guard, so nothing is ever sent to or read from a resource whose
|
|
25
|
+
// workspace was not checked.
|
|
26
|
+
//
|
|
27
|
+
// NO DELETE. The platform must not hold the ability to destroy infrastructure in an
|
|
28
|
+
// account it does not own (ADR 0327 §3). There is no delete method, and _req refuses the
|
|
29
|
+
// verb outright, so adding one later is a visible change to this file, not a one-liner.
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const { JsonHttpApi, makeApiError } = require('./http-api-client');
|
|
34
|
+
|
|
35
|
+
const RENDER_API_BASE = 'https://api.render.com/v1';
|
|
36
|
+
|
|
37
|
+
const RenderApiError = makeApiError('RenderApiError', 'RENDER_API_ERROR');
|
|
38
|
+
const RenderOwnerMismatch = makeApiError('RenderOwnerMismatch', 'RENDER_OWNER_MISMATCH');
|
|
39
|
+
|
|
40
|
+
// The owner id a Render resource carries, in either of the two shapes Render uses.
|
|
41
|
+
// null when it carries none.
|
|
42
|
+
function ownerOf(resource) {
|
|
43
|
+
if (!resource || typeof resource !== 'object') return null;
|
|
44
|
+
if (typeof resource.ownerId === 'string' && resource.ownerId) return resource.ownerId;
|
|
45
|
+
if (resource.owner && typeof resource.owner.id === 'string' && resource.owner.id) return resource.owner.id;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Env var values are the owner's secrets on their way to Render. A dry-run log is the one
|
|
50
|
+
// place this client would otherwise print a request body, so it prints the names only.
|
|
51
|
+
function redact(body) {
|
|
52
|
+
if (!body || typeof body !== 'object') return body;
|
|
53
|
+
const out = { ...body };
|
|
54
|
+
if (Array.isArray(out.envVars)) out.envVars = out.envVars.map((v) => ({ key: v && v.key, value: '[redacted]' }));
|
|
55
|
+
if ('value' in out) out.value = '[redacted]';
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class RenderApi extends JsonHttpApi {
|
|
60
|
+
// opts: { token, ownerId, dryRun=true, fetchImpl=globalThis.fetch, base, log }
|
|
61
|
+
constructor(opts = {}) {
|
|
62
|
+
super(opts, { base: RENDER_API_BASE, logTag: 'Render', ErrorClass: RenderApiError });
|
|
63
|
+
if (typeof opts.ownerId !== 'string' || !opts.ownerId.trim()) {
|
|
64
|
+
throw new RenderApiError('a Render client needs the workspace it acts in (ownerId) — it is never defaulted');
|
|
65
|
+
}
|
|
66
|
+
this.ownerId = opts.ownerId.trim();
|
|
67
|
+
if (!this.dryRun && !this.token) {
|
|
68
|
+
throw new RenderApiError('live Render call requested with no API key');
|
|
69
|
+
}
|
|
70
|
+
if (!this.dryRun && typeof this.fetchImpl !== 'function') {
|
|
71
|
+
throw new RenderApiError('no fetch implementation available for live mode');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async _req(method, path, body, opts = {}) {
|
|
76
|
+
if (method === 'DELETE') throw new RenderApiError(`refusing ${method} ${path}: the platform never deletes in an account it does not own (ADR 0327 §3)`);
|
|
77
|
+
if (this.dryRun) {
|
|
78
|
+
this.log(`[dry-run] Render ${method} ${path}${body ? ' ' + JSON.stringify(redact(body)) : ''}`);
|
|
79
|
+
return { dryRun: true, ...(opts.dryRunResult || {}) };
|
|
80
|
+
}
|
|
81
|
+
return super._req(method, path, body, opts);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Throws unless `resource` carries exactly this client's workspace.
|
|
85
|
+
assertOwned(resource, what) {
|
|
86
|
+
const got = ownerOf(resource);
|
|
87
|
+
if (got !== this.ownerId) {
|
|
88
|
+
throw new RenderOwnerMismatch(
|
|
89
|
+
got
|
|
90
|
+
? `Render answered ${what} from workspace ${got}, not ${this.ownerId} — refusing to go on`
|
|
91
|
+
: `Render answered ${what} with no workspace on it — refusing to go on`,
|
|
92
|
+
{ body: null });
|
|
93
|
+
}
|
|
94
|
+
return resource;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- workspaces ------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
// The workspaces the key can reach, as [{ id, name, type }]. Used for MEMBERSHIP only —
|
|
100
|
+
// "is the workspace the owner named one this key can act in" — never to pick one.
|
|
101
|
+
async listOwners() {
|
|
102
|
+
const json = await this._req('GET', '/owners?limit=100', null, {
|
|
103
|
+
dryRunResult: { items: [{ owner: { id: this.ownerId, name: '(dry-run)', type: 'team' } }] },
|
|
104
|
+
});
|
|
105
|
+
const items = Array.isArray(json) ? json : (json && json.items) || [];
|
|
106
|
+
return items.map((e) => (e && e.owner) || e).filter((o) => o && typeof o.id === 'string');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- services --------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
async getService(serviceId) {
|
|
112
|
+
const json = await this._req('GET', `/services/${encodeURIComponent(serviceId)}`, null, {
|
|
113
|
+
dryRunResult: { id: serviceId, ownerId: this.ownerId, serviceDetails: { url: null } },
|
|
114
|
+
});
|
|
115
|
+
return this.assertOwned(json && json.service ? json.service : json, `service ${serviceId}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// payload: the POST /services body (render-payload.js). Its ownerId must be ours — the
|
|
119
|
+
// payload builder takes it as an argument, so this is checked, not overwritten.
|
|
120
|
+
async createService(payload) {
|
|
121
|
+
if (!payload || payload.ownerId !== this.ownerId) throw new RenderApiError('a service payload must name this client\'s workspace');
|
|
122
|
+
const json = await this._req('POST', '/services', payload, {
|
|
123
|
+
dryRunResult: { deployId: null, service: { id: `dryrun-srv-${payload.name}`, ownerId: this.ownerId, serviceDetails: { url: null } } },
|
|
124
|
+
});
|
|
125
|
+
return { deployId: (json && json.deployId) || null, service: this.assertOwned(json && json.service, 'the new service') };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Set one env var on a service this workspace owns. The PUT answers with no owner, so
|
|
129
|
+
// the service is read through the guard first.
|
|
130
|
+
async putEnvVar(serviceId, key, value) {
|
|
131
|
+
await this.getService(serviceId);
|
|
132
|
+
return this._req('PUT', `/services/${encodeURIComponent(serviceId)}/env-vars/${encodeURIComponent(key)}`, { value }, { dryRunResult: { key } });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async triggerDeploy(serviceId) {
|
|
136
|
+
await this.getService(serviceId);
|
|
137
|
+
return this._req('POST', `/services/${encodeURIComponent(serviceId)}/deploys`, {}, { dryRunResult: { id: null } });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A service's deploys, newest first, as the bare deploy objects (Render wraps each in
|
|
141
|
+
// `{ deploy, cursor }`; both shapes are read). The list carries no owner, so the
|
|
142
|
+
// service is read through the guard first (the deploy hall's history, task 1004270).
|
|
143
|
+
async listDeploys(serviceId, { limit = 30 } = {}) {
|
|
144
|
+
await this.getService(serviceId);
|
|
145
|
+
const n = Math.max(1, Math.min(100, Number.parseInt(limit, 10) || 30));
|
|
146
|
+
const json = await this._req('GET', `/services/${encodeURIComponent(serviceId)}/deploys?limit=${n}`, null, { dryRunResult: { items: [] } });
|
|
147
|
+
const items = Array.isArray(json) ? json : (json && json.items) || [];
|
|
148
|
+
return items.map((e) => (e && e.deploy) || e).filter((d) => d && typeof d.id === 'string');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Roll the service back to one of its earlier deploys. The answer carries no owner, so
|
|
152
|
+
// the service is read through the guard first. This is a write on the owner's account;
|
|
153
|
+
// the caller (the deploy hall) asks the owner twice before it reaches here.
|
|
154
|
+
async rollbackTo(serviceId, deployId) {
|
|
155
|
+
await this.getService(serviceId);
|
|
156
|
+
return this._req('POST', `/services/${encodeURIComponent(serviceId)}/rollback`, { deployId }, { dryRunResult: { id: null } });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- Postgres --------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
async createPostgres(payload) {
|
|
162
|
+
if (!payload || payload.ownerId !== this.ownerId) throw new RenderApiError('a Postgres payload must name this client\'s workspace');
|
|
163
|
+
const json = await this._req('POST', '/postgres', payload, {
|
|
164
|
+
dryRunResult: { id: `dryrun-pg-${payload.name}`, status: 'creating', owner: { id: this.ownerId } },
|
|
165
|
+
});
|
|
166
|
+
return this.assertOwned(json, 'the new database');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getPostgres(postgresId) {
|
|
170
|
+
const json = await this._req('GET', `/postgres/${encodeURIComponent(postgresId)}`, null, {
|
|
171
|
+
dryRunResult: { id: postgresId, status: 'available', owner: { id: this.ownerId } },
|
|
172
|
+
});
|
|
173
|
+
return this.assertOwned(json, `database ${postgresId}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The database's connection strings. The answer carries no owner, so the database is
|
|
177
|
+
// read through the guard first.
|
|
178
|
+
async postgresConnectionInfo(postgresId) {
|
|
179
|
+
await this.getPostgres(postgresId);
|
|
180
|
+
return this._req('GET', `/postgres/${encodeURIComponent(postgresId)}/connection-info`, null, {
|
|
181
|
+
dryRunResult: { internalConnectionString: 'postgres://dry-run' },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Poll until a new database is 'available'. `sleepFn` lets tests drive it with no clock.
|
|
186
|
+
async settlePostgres(postgresId, { tries = 60, sleepMs = 5000, sleepFn } = {}) {
|
|
187
|
+
const sleep = sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
188
|
+
for (let i = 0; i < tries; i++) {
|
|
189
|
+
const pg = await this.getPostgres(postgresId);
|
|
190
|
+
if (pg.status === 'available') return pg;
|
|
191
|
+
await sleep(sleepMs);
|
|
192
|
+
}
|
|
193
|
+
throw new RenderApiError(`database ${postgresId} did not become available in time`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { RenderApi, RenderApiError, RenderOwnerMismatch, RENDER_API_BASE, ownerOf, redact };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// scripts/gds/render-payload.js — WHAT the Render standup leg creates, kept apart from
|
|
2
|
+
// HOW it creates it (scripts/gds/provision-render.js) (task 1004183; retargeted by
|
|
3
|
+
// task 1004350).
|
|
4
|
+
//
|
|
5
|
+
// Pure functions, no I/O. What goes to the owner's Render account is their APP, not the
|
|
6
|
+
// Bongos hall: we host every project's hall, and the app deploys where its owner
|
|
7
|
+
// chooses (ADR 0345 D1, D3). So the service is built from the project's own repository
|
|
8
|
+
// and started the way that repository says — the owner's runtime and commands, or the
|
|
9
|
+
// repository's own Dockerfile — never the Bongos core.
|
|
10
|
+
//
|
|
11
|
+
// A database is optional (some apps need none). When there is one, its internal
|
|
12
|
+
// connection string is handed to the app as DATABASE_URL, the one name every mainstream
|
|
13
|
+
// framework reads. That is the only env var the platform sets: every other variable is
|
|
14
|
+
// the owner's to set on Render, and Bongos keeps their NAMES only (task 1004062's
|
|
15
|
+
// manifest) — never a value.
|
|
16
|
+
//
|
|
17
|
+
// The plans are per-project settings (ADR 0327 §5), never constants. The defaults are
|
|
18
|
+
// what the Render pilot (task 1004142) found enough at idle: a `starter` web service and
|
|
19
|
+
// a paid `basic_256mb` Postgres, because Render's free Postgres has no backups.
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// The vocabulary lives with the web tier's validation, so the two can never disagree.
|
|
24
|
+
const { WEB_PLANS, DB_PLANS, REGIONS, RUNTIMES } = require('../../modules/provisioning/render-standup.js');
|
|
25
|
+
const DEFAULTS = Object.freeze({ webPlan: 'starter', dbPlan: 'basic_256mb', region: 'virginia' });
|
|
26
|
+
|
|
27
|
+
// A Render resource name from the project's slug. Render allows letters, digits and
|
|
28
|
+
// dashes; the slug already is that, this only makes sure.
|
|
29
|
+
function resourceName(inst, suffix) {
|
|
30
|
+
const base = String(inst.slug || `project-${inst.id}`).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 50);
|
|
31
|
+
return suffix ? `${base}-${suffix}` : base;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The plan and region a project asked for, with the defaults filled in. A value outside
|
|
35
|
+
// the vocabulary is refused rather than passed through for Render to reject.
|
|
36
|
+
function plansFor(appHost) {
|
|
37
|
+
const h = appHost || {};
|
|
38
|
+
const out = {
|
|
39
|
+
webPlan: h.webPlan || DEFAULTS.webPlan,
|
|
40
|
+
dbPlan: h.dbPlan || DEFAULTS.dbPlan,
|
|
41
|
+
region: h.region || DEFAULTS.region,
|
|
42
|
+
};
|
|
43
|
+
if (!WEB_PLANS.includes(out.webPlan)) throw new Error(`unknown Render web plan '${out.webPlan}'`);
|
|
44
|
+
if (!DB_PLANS.includes(out.dbPlan)) throw new Error(`unknown Render database plan '${out.dbPlan}'`);
|
|
45
|
+
if (!REGIONS.includes(out.region)) throw new Error(`unknown Render region '${out.region}'`);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// How the app is built and started, as the owner said. `docker` builds the repository's
|
|
50
|
+
// own Dockerfile and needs no commands; every other runtime needs a start command, and a
|
|
51
|
+
// build command if the app has a build step.
|
|
52
|
+
function buildFor(appHost) {
|
|
53
|
+
const h = appHost || {};
|
|
54
|
+
if (!RUNTIMES.includes(h.runtime)) throw new Error(`unknown Render runtime '${h.runtime || 'none'}'`);
|
|
55
|
+
if (h.runtime === 'docker') return { runtime: 'docker' };
|
|
56
|
+
if (!h.startCommand) throw new Error('the app needs a start command');
|
|
57
|
+
return { runtime: h.runtime, buildCommand: h.buildCommand || null, startCommand: h.startCommand };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// POST /postgres. `plan` is never 'free' — see the header.
|
|
61
|
+
function renderPostgresPayload(inst, { ownerId, dbPlan, region }) {
|
|
62
|
+
return { name: resourceName(inst, 'db'), ownerId, plan: dbPlan, region, version: '16' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// POST /services, for the owner's app from their repository. `dbUrl` is the database's
|
|
66
|
+
// internal connection string, or null when the app has no database. It is a VALUE sent
|
|
67
|
+
// to Render and held there — the platform keeps none.
|
|
68
|
+
function renderServicePayload(inst, { ownerId, repoUrl, branch, webPlan, region, build, dbUrl }) {
|
|
69
|
+
const envSpecificDetails = build.runtime === 'docker'
|
|
70
|
+
? {}
|
|
71
|
+
: { ...(build.buildCommand ? { buildCommand: build.buildCommand } : {}), startCommand: build.startCommand };
|
|
72
|
+
return {
|
|
73
|
+
type: 'web_service',
|
|
74
|
+
name: resourceName(inst),
|
|
75
|
+
ownerId,
|
|
76
|
+
repo: repoUrl,
|
|
77
|
+
branch: branch || 'main',
|
|
78
|
+
autoDeploy: 'yes',
|
|
79
|
+
serviceDetails: { runtime: build.runtime, plan: webPlan, region, envSpecificDetails },
|
|
80
|
+
envVars: dbUrl ? [{ key: 'DATABASE_URL', value: dbUrl }] : [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { DEFAULTS, resourceName, plansFor, buildFor, renderPostgresPayload, renderServicePayload };
|
|
@@ -102,6 +102,13 @@ const INTEGRATION = new Set([
|
|
|
102
102
|
// never was a dedicated consumer-layout-boot workflow, whatever older notes say.
|
|
103
103
|
'consumer_layout_boot',
|
|
104
104
|
'claim_gate_open_blocker', 'claim_gate_rebase',
|
|
105
|
+
// task 1003457: proves the blocker-resolve auto-promotion trigger (migration
|
|
106
|
+
// 009, tightened by core_223) fires on the HALL'S OWN write path — the real
|
|
107
|
+
// buildGdsRouter() HTTP routes, not a static SQL-text pin (blocker_resolve_guard.mjs,
|
|
108
|
+
// blocker_unlink.mjs) or a pure-predicate unit test (blocker_link.mjs, which never
|
|
109
|
+
// touches a database). Only a real transaction engine behind a real router proves
|
|
110
|
+
// that; self-skips without a reachable Postgres.
|
|
111
|
+
'blocker_hall_live_proof',
|
|
105
112
|
// task 1002709: core_221's re-keyed backstop index against a real Postgres.
|
|
106
113
|
// The DB-free sibling pins the guard and the migration's text; this proves the
|
|
107
114
|
// index actually refuses the commingling INSERT and still allows the parallel
|
|
@@ -122,6 +129,9 @@ const INTEGRATION = new Set([
|
|
|
122
129
|
// dev box session. The claims foreign key that failed the live upgrade is a
|
|
123
130
|
// schema fact only a transaction engine enforces. Always rolled back.
|
|
124
131
|
'core_258_box_session_claims_db',
|
|
132
|
+
// task 1004360: core_262 drops the four builders columns migration 104 added, against a real
|
|
133
|
+
// Postgres, with the foreign key live, then re-runs it. Always rolled back.
|
|
134
|
+
'core_262_drop_box_blocked_db',
|
|
125
135
|
'discord_autojoin', 'discord_oauth',
|
|
126
136
|
// task 1003000 (IC6): the three ideator credit streams against a real Postgres.
|
|
127
137
|
// The three DB-free sibling suites stub the client and SIMULATE the
|
|
@@ -14,7 +14,8 @@ const { cliClient, printOnboardingNudge, rewardPolicy, cardFlagsFromMe } = requi
|
|
|
14
14
|
const { closeoutPlan } = require('../../modules/lifecycle/ship-preflight');
|
|
15
15
|
const { syncMemoryOnShip } = require('./ship-memory-sync.js');
|
|
16
16
|
const { uploadSessionOnShip } = require('./ship-session-upload.js');
|
|
17
|
-
const {
|
|
17
|
+
const { execFileSync } = require('node:child_process');
|
|
18
|
+
const { gitOk } = require('./ship-git.js');
|
|
18
19
|
const { fetchDeployedVersion } = require('./ship-deploy-target.js');
|
|
19
20
|
const { landBailed, reconciledCardStatus } = require('./ship-land.js');
|
|
20
21
|
const { autoMerge, publishHeld } = require('./ship-merge.js');
|
|
@@ -142,18 +143,23 @@ async function onboardingNudgeBestEffort() {
|
|
|
142
143
|
// close-out, folded into the delivery process (NOT a separate skill). After a
|
|
143
144
|
// ship lands, scratch often lingers uncommitted — untracked files the preflight
|
|
144
145
|
// lets through, generated artifacts, post-commit edits — making the session
|
|
145
|
-
// hard to archive cleanly. This
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
146
|
+
// hard to archive cleanly. This REPORTS what's left and leaves it in place.
|
|
147
|
+
// task 1004254: it used to `git stash push -u` the leftover, but the stash stack
|
|
148
|
+
// is repo-global across every worktree (task 1003344), so one builder's scratch
|
|
149
|
+
// landed where a parallel agent's pop could apply it to the wrong branch — and
|
|
150
|
+
// it swallowed review renders an owner still needed. It never touches the stash.
|
|
151
|
+
// STRICTLY fail-open: the ship has already succeeded by the time this runs, so
|
|
152
|
+
// nothing here may throw, prompt, or change the exit status — a messy tree must
|
|
153
|
+
// never surface as a ship failure. The caller gates this on a SUCCESSFUL land.
|
|
152
154
|
function postShipCloseout(taskId) {
|
|
153
155
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// Untrimmed: gitOk's trim() eats the first line's leading status column, so
|
|
157
|
+
// " M a.txt" was listed as ".txt" (task 1004254).
|
|
158
|
+
let out;
|
|
159
|
+
try {
|
|
160
|
+
out = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
161
|
+
} catch (_) { return; } // git unavailable / not a repo — nothing to reconcile
|
|
162
|
+
const plan = closeoutPlan(out);
|
|
157
163
|
if (plan.clean) {
|
|
158
164
|
console.log('Close-out: working tree clean — session is archive-ready.');
|
|
159
165
|
return;
|
|
@@ -162,15 +168,7 @@ function postShipCloseout(taskId) {
|
|
|
162
168
|
console.log(`Close-out: ${plan.leftover.length} uncommitted/untracked path(s) remain after the ship:`);
|
|
163
169
|
for (const f of plan.leftover.slice(0, 15)) console.log(` ${f}`);
|
|
164
170
|
if (plan.leftover.length > 15) console.log(` … and ${plan.leftover.length - 15} more`);
|
|
165
|
-
|
|
166
|
-
// Label is `otb-postship-<numericId>` — no shell metacharacters.
|
|
167
|
-
const stashed = shellOk(`git stash push --include-untracked -m "${plan.stashLabel}"`);
|
|
168
|
-
if (stashed) {
|
|
169
|
-
console.log(`Close-out: stashed the leftover as "${plan.stashLabel}" — tree is now clean + archive-ready.`);
|
|
170
|
-
console.log(' Recover anytime: git stash list → git stash pop');
|
|
171
|
-
} else {
|
|
172
|
-
console.log('Close-out: could not stash the leftover; left it in place (the ship already succeeded — tidy up manually when convenient).');
|
|
173
|
-
}
|
|
171
|
+
console.log(`Close-out: left in place (task ${taskId} already shipped) — commit, move or delete them before archiving this worktree.`);
|
|
174
172
|
} catch (err) {
|
|
175
173
|
// Fail-open — a post-ship close-out problem must never look like a ship failure.
|
|
176
174
|
console.log(`Close-out: skipped (${err && err.message ? err.message : 'error'}).`);
|
|
@@ -404,6 +402,7 @@ module.exports = {
|
|
|
404
402
|
finishHeld,
|
|
405
403
|
mergeAndDeploy,
|
|
406
404
|
onboardingNudgeBestEffort,
|
|
405
|
+
postShipCloseout,
|
|
407
406
|
postShipSync,
|
|
408
407
|
printCompletionCard,
|
|
409
408
|
};
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
# 5. Promote A manually (the route allows it; the gate is at claim time)
|
|
11
11
|
# 6. Try to claim A — still 409 DEPS_NOT_SHIPPED
|
|
12
12
|
# 7. Claim B, ship B (active → completed → confirmed → shipped)
|
|
13
|
-
# 8. Verify
|
|
13
|
+
# 8. Verify C — a second dependent of B that NOTHING ever promoted by hand —
|
|
14
|
+
# flipped backlog→ready. This is the only assertion that can fail if the
|
|
15
|
+
# auto-promotion trigger is broken (task 1004191): A was already 'ready'
|
|
16
|
+
# from the manual step, so A's status proves nothing about the trigger.
|
|
14
17
|
# 9. Claim A — succeeds
|
|
15
|
-
# 10. Release A, abandon A (B is shipped and cannot be abandoned)
|
|
18
|
+
# 10. Release A, abandon A and C (B is shipped and cannot be abandoned)
|
|
16
19
|
#
|
|
17
20
|
# Run from any environment with a valid CLI session (/builder-setup).
|
|
18
21
|
# Requires curl.
|
|
@@ -44,7 +47,22 @@ A=$(curl -sS -X POST "$BASE/api/bongos/tasks" \
|
|
|
44
47
|
A_ID=$(echo "$A" | python3 -c "import json,sys; print(json.load(sys.stdin)['task']['id'])")
|
|
45
48
|
echo " A id=$A_ID"
|
|
46
49
|
|
|
47
|
-
echo "===
|
|
50
|
+
echo "=== 2b. Create task C (dependent, status=backlog, NEVER promoted by hand) ==="
|
|
51
|
+
# task 1004191 (finding 62 of the task 1000379 audit): A is promoted manually in
|
|
52
|
+
# step 4, so "A is ready after B ships" held whether or not the auto-promotion
|
|
53
|
+
# trigger fired. C exists to be the dependent only the trigger can flip.
|
|
54
|
+
C=$(curl -sS -X POST "$BASE/api/bongos/tasks" \
|
|
55
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
56
|
+
--data "{\"version_id\":\"GDS-V3\",\"title\":\"__smoke__ dep-test C $SUFFIX\",\"description\":\"smoke\",\"status\":\"backlog\",\"kind\":\"feature\",\"discipline\":\"engineering\",\"priority\":5,\"touches\":[\"smoke-dep-C-$SUFFIX/\"],\"source\":\"smoke\",\"source_ref\":\"smoke-dep-C-$SUFFIX\"}")
|
|
57
|
+
C_ID=$(echo "$C" | python3 -c "import json,sys; print(json.load(sys.stdin)['task']['id'])")
|
|
58
|
+
echo " C id=$C_ID"
|
|
59
|
+
|
|
60
|
+
echo "=== 3. Wire A → depends_on B, and C → depends_on B ==="
|
|
61
|
+
curl -sS -X POST "$BASE/api/bongos/tasks/$C_ID/dependencies" \
|
|
62
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
63
|
+
--data "{\"depends_on_task_id\": $B_ID}" >/dev/null
|
|
64
|
+
C_PRE=$(curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/api/bongos/tasks/$C_ID" | python3 -c "import json,sys; print(json.load(sys.stdin)['task']['status'])")
|
|
65
|
+
[ "$C_PRE" = "backlog" ] && echo " PASS: C is backlog before B ships" || (echo " FAIL: C is '$C_PRE' before B ships — the auto-promotion check below would prove nothing"; exit 1)
|
|
48
66
|
curl -sS -X POST "$BASE/api/bongos/tasks/$A_ID/dependencies" \
|
|
49
67
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
50
68
|
--data "{\"depends_on_task_id\": $B_ID}" | python3 -m json.tool
|
|
@@ -69,15 +87,18 @@ curl -sS -X POST "$BASE/api/bongos/claims/by-task/$B_ID/resolve" \
|
|
|
69
87
|
curl -sS -X POST "$BASE/api/bongos/tasks/$B_ID/confirm" -H "Authorization: Bearer $TOKEN" >/dev/null
|
|
70
88
|
curl -sS -X POST "$BASE/api/bongos/tasks/$B_ID/ship" -H "Authorization: Bearer $TOKEN" >/dev/null
|
|
71
89
|
|
|
72
|
-
echo "=== 7. Verify
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
echo "=== 7. Verify the auto-promotion trigger fired: C backlog → ready ==="
|
|
91
|
+
# C was never promoted by hand, so only promote_dependents_on_ship can have made
|
|
92
|
+
# it 'ready'. (A is 'ready' from the manual step 4 regardless, so A is not asserted
|
|
93
|
+
# here — that is the assertion that used to pass with the trigger broken.)
|
|
94
|
+
C_STATUS=$(curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/api/bongos/tasks/$C_ID" | python3 -c "import json,sys; print(json.load(sys.stdin)['task']['status'])")
|
|
95
|
+
echo " C status: $C_STATUS"
|
|
96
|
+
[ "$C_STATUS" = "ready" ] && echo " PASS: C auto-promoted" || (echo " FAIL: C is '$C_STATUS' — B shipped but the auto-promotion trigger did not flip its dependent"; exit 1)
|
|
76
97
|
|
|
77
98
|
echo "=== 8. Claim A — must succeed now ==="
|
|
78
99
|
node scripts/gds/claim.js "$A_ID" --worktree "smoke-$SUFFIX-A"
|
|
79
100
|
|
|
80
|
-
echo "=== 9. Cleanup — abandon A; B is shipped and stays ==="
|
|
101
|
+
echo "=== 9. Cleanup — abandon A and C; B is shipped and stays ==="
|
|
81
102
|
# task 1004026: this step used to ANNOUNCE "release A; mark both abandoned" and then
|
|
82
103
|
# only release A. Neither task reached a terminal state, so every run leaked two
|
|
83
104
|
# __smoke__ rows into the live tasks table — invisibly, because anyone grepping the
|
|
@@ -108,6 +129,14 @@ case "${A_CLEANUP_CODE:-000}" in
|
|
|
108
129
|
000) echo " SKIP cleanup — POST /tasks/$A_ID/abandon got no response; task #$A_ID left in the ledger" ;;
|
|
109
130
|
*) echo " SKIP cleanup — POST /tasks/$A_ID/abandon returned HTTP $A_CLEANUP_CODE; task #$A_ID left in the ledger" ;;
|
|
110
131
|
esac
|
|
132
|
+
C_CLEANUP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
133
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
134
|
+
--data '{"reason":"smoke cleanup (task 1004191) - auto-promotion witness; the throwaway row has served its purpose"}' \
|
|
135
|
+
"$BASE/api/bongos/tasks/$C_ID/abandon") || true
|
|
136
|
+
case "${C_CLEANUP_CODE:-000}" in
|
|
137
|
+
2*) echo " PASS cleaned up throwaway task #$C_ID (abandoned)" ;;
|
|
138
|
+
*) echo " SKIP cleanup — POST /tasks/$C_ID/abandon returned HTTP ${C_CLEANUP_CODE:-000}; task #$C_ID left in the ledger" ;;
|
|
139
|
+
esac
|
|
111
140
|
echo " NOTE task #$B_ID is SHIPPED and is deliberately left in the ledger — the abandon route refuses shipped work (cannot_abandon_shipped)."
|
|
112
141
|
|
|
113
142
|
echo
|