@bongos/core 1.19.936 → 1.19.938
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 +69 -44
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +2 -0
- package/docs/api/openapi.json +91 -2
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/self-host.md +1 -1
- package/modules/government/board.js +31 -0
- package/modules/government/db.js +33 -0
- package/modules/government/migrations/government_018_passed_items_index.sql +31 -0
- package/modules/government/routes/government.js +7 -0
- package/modules/hall-ui/public/deploy.js +1 -1
- package/modules/ideas/inbox.js +11 -0
- package/modules/ideas/migrations/ideas_010_goal_ratification.sql +103 -0
- package/modules/ideas/module.json +1 -0
- package/modules/ideas/ratify.js +288 -1
- package/modules/ideas/routes/ratify-goal.js +163 -0
- package/modules/ideas/templates.js +16 -4
- package/modules/lifecycle/goal-from-idea.js +131 -0
- package/modules/lifecycle/lifecycle.js +14 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-authorship.sh +33 -0
- package/scripts/gds/db-backup-nightly.sh +1 -1
- package/scripts/gds/instance-apex.js +11 -1
- package/scripts/gds/provision-config.js +3 -2
- package/scripts/gds/provision-core-upgrade.js +7 -7
- package/scripts/gds/provision-process.js +2 -2
- package/scripts/gds/provision-repo.js +35 -10
- package/scripts/gds/provision-units.js +3 -3
- package/scripts/gds/provision.js +6 -3
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +10 -7
- package/tests/government_seed.mjs +1 -0
- package/tests/idea_goal_ratification.mjs +553 -0
- package/tests/provision.mjs +44 -7
- package/tests/provision_account_identity.mjs +16 -0
package/tests/provision.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import { join } from 'node:path';
|
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const P = require('../scripts/gds/provision.js');
|
|
21
|
+
const AP = require('../scripts/gds/instance-apex.js');
|
|
22
|
+
const RP = require('../scripts/gds/provision-repo.js');
|
|
21
23
|
|
|
22
24
|
// The env-var prefix a provisioned instance resolves = the envPrefix an instance INHERITS from
|
|
23
25
|
// the neutral pack (task 1003740: webEnvBody no longer pins a branding pack anywhere — the
|
|
@@ -40,6 +42,11 @@ process.env.PROVISION_GIT_SSH_ALIAS = 'github-deploy';
|
|
|
40
42
|
// The probe itself is covered directly, with an injected fs.
|
|
41
43
|
process.env.PROVISION_PROCESS_MANAGER = 'systemd';
|
|
42
44
|
|
|
45
|
+
// And the app user (task 1002719): unpinned it is whoever runs the suite. Pinned to a name
|
|
46
|
+
// that is nobody's real account, so an assertion passes only if the value came through the
|
|
47
|
+
// resolver — a leftover literal cannot produce it.
|
|
48
|
+
process.env.PROVISION_APP_USER = 'appsvc';
|
|
49
|
+
|
|
43
50
|
let passed = 0, failed = 0;
|
|
44
51
|
function t(name, fn) {
|
|
45
52
|
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
@@ -167,11 +174,16 @@ t('buildCorePinRefreshCommit (the commit step): git sequence, token-URL push, an
|
|
|
167
174
|
// must not attribute its commits to the hub. Assert against the resolver so
|
|
168
175
|
// this pins the behaviour, not one instance's literal.
|
|
169
176
|
assert.deepEqual(withTok.config, [
|
|
170
|
-
|
|
177
|
+
`git -C /srv/bongos/spike-harbor config user.name "${AP.provisionerBotName()}"`,
|
|
171
178
|
`git -C /srv/bongos/spike-harbor config user.email "${P.provisionerBotEmail()}"`,
|
|
172
179
|
]);
|
|
173
180
|
assert.equal(P.provisionerBotEmail({ PROVISION_BOT_EMAIL: 'ops@acme.test' }), 'ops@acme.test', 'env override wins');
|
|
174
181
|
assert.doesNotMatch(P.provisionerBotEmail({}), /example/i, 'never a founder-host address');
|
|
182
|
+
// task 1002719: the NAME comes from the pack too, and one the shell cannot quote falls back.
|
|
183
|
+
assert.equal(AP.provisionerBotName('Acme Forge'), 'Acme Forge', 'the pack\'s product name');
|
|
184
|
+
for (const unsafe of ['Acme "Forge"', 'a$(id)', 'a`id`', 'a\\b', 'a\nb', '']) {
|
|
185
|
+
assert.equal(AP.provisionerBotName(unsafe), 'Cloud Bongos', `falls back on ${JSON.stringify(unsafe)}`);
|
|
186
|
+
}
|
|
175
187
|
assert.equal(withTok.add, 'git -C /srv/bongos/spike-harbor add -A');
|
|
176
188
|
assert.match(withTok.commit, /@bongos\/core 1\.19\.2 -> 1\.19\.196 \(task 1003050\)/);
|
|
177
189
|
assert.match(withTok.push, /store --file=\/tmp\/cred.*x-access-token@github\.com\/jaxri\/spike-harbor\.git HEAD:main/);
|
|
@@ -199,7 +211,7 @@ t('backup units: per-instance names + the DB set on the unit (fixes the task-221
|
|
|
199
211
|
// the DB is pinned on the unit → no fallback to the nonexistent example default
|
|
200
212
|
assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
|
|
201
213
|
assert.match(svc, /Environment=GDS_BACKUP_DIR=\/var\/backups\/gds/);
|
|
202
|
-
assert.match(svc, new RegExp(`User=${reEsc('
|
|
214
|
+
assert.match(svc, new RegExp(`User=${reEsc('appsvc')}`), 'runs as the app user (mirrors serviceUnit)');
|
|
203
215
|
assert.match(svc, /ReadWritePaths=\/var\/backups\/gds/, 'the only writable path is the backup dir');
|
|
204
216
|
const timer = P.backupTimer(coTenant);
|
|
205
217
|
assert.match(timer, /OnCalendar=\*-\*-\* 04:00:00 UTC/);
|
|
@@ -466,7 +478,7 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
|
|
|
466
478
|
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote systemd unit');
|
|
467
479
|
assert.ok(writes.some((w) => /\/mercury\.caddy$/.test(w)), 'wrote per-instance Caddy snippet');
|
|
468
480
|
// task 2213: EVERY instance is born with backups — dir + per-instance timer/service
|
|
469
|
-
assert.match(joined, /install -d -o
|
|
481
|
+
assert.match(joined, /install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 \/var\/backups\/gds/, 'creates the backup dir');
|
|
470
482
|
assert.match(joined, /systemctl enable --now mercury-db-backup\.timer/, 'enables the per-instance backup timer');
|
|
471
483
|
assert.ok(writes.some((w) => /\/mercury-db-backup\.service$/.test(w)), 'wrote backup service unit');
|
|
472
484
|
assert.ok(writes.some((w) => /\/mercury-db-backup\.timer$/.test(w)), 'wrote backup timer unit');
|
|
@@ -626,7 +638,8 @@ t('systemd manager: emits byte-identical commands + unit paths to the inline str
|
|
|
626
638
|
PM.systemd.installBackup(INST, io2);
|
|
627
639
|
// The backup DIR moved in here with the schedule it serves: a manager that installs no
|
|
628
640
|
// schedule must not create it either. Order is unchanged — dir, units, reload, enable.
|
|
629
|
-
|
|
641
|
+
// The group is the app user's PRIMARY group, looked up on the box (task 1002719).
|
|
642
|
+
assert.match(io2.cmds[0], /^sudo install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 /);
|
|
630
643
|
assert.deepEqual(io2.cmds.slice(1), ['sudo systemctl daemon-reload', 'sudo systemctl enable --now demo-db-backup.timer']);
|
|
631
644
|
assert.deepEqual(io2.files.map((f) => f.path),
|
|
632
645
|
['/etc/systemd/system/demo-db-backup.service', '/etc/systemd/system/demo-db-backup.timer']);
|
|
@@ -1329,7 +1342,7 @@ await ta('dedicatedUserData bakes the nightly backup: unit files (b64) + dir + t
|
|
|
1329
1342
|
const b64 = cloudInit.match(/content: ([A-Za-z0-9+/=]+)/)[1];
|
|
1330
1343
|
assert.match(Buffer.from(b64, 'base64').toString('utf8'), /Environment=GDS_BACKUP_DB=mercury/);
|
|
1331
1344
|
// and the dir is created + the timer enabled on first boot
|
|
1332
|
-
assert.match(cloudInit, /install -d -o
|
|
1345
|
+
assert.match(cloudInit, /install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 \/var\/backups\/gds/);
|
|
1333
1346
|
assert.match(cloudInit, /enable --now mercury mercury-db-backup\.timer/);
|
|
1334
1347
|
});
|
|
1335
1348
|
|
|
@@ -2932,7 +2945,31 @@ t('ensureInstanceUserCmd: idempotent SYSTEM account, nologin, state dir re-asser
|
|
|
2932
2945
|
});
|
|
2933
2946
|
|
|
2934
2947
|
t('grantInstanceRepoReadCmd: adds the instance account to the app-user group so the unit can exec its WorkingDirectory', () => {
|
|
2935
|
-
assert.equal(P.grantInstanceRepoReadCmd(coTenant, { privileged: true }), 'sudo usermod -aG
|
|
2948
|
+
assert.equal(P.grantInstanceRepoReadCmd(coTenant, { privileged: true }), 'sudo usermod -aG $(id -gn appsvc) bongos-mercury');
|
|
2949
|
+
});
|
|
2950
|
+
|
|
2951
|
+
t('appUser: PROVISION_APP_USER, else the account the runner runs as — never a named founder (task 1002719)', () => {
|
|
2952
|
+
const runner = (username) => () => ({ username });
|
|
2953
|
+
assert.equal(RP.appUser({ env: { PROVISION_APP_USER: 'ops' }, userInfo: runner('deploy') }), 'ops', 'the pin wins');
|
|
2954
|
+
assert.equal(RP.appUser({ env: {}, userInfo: runner('deploy') }), 'deploy', "unpinned: the runner's own account");
|
|
2955
|
+
assert.equal(RP.appUser(), 'appsvc', 'the suite pin is what every generator above rendered');
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
t('appUser: refuses root and any name a shell command or unit file could not carry', () => {
|
|
2959
|
+
const runner = (username) => () => ({ username });
|
|
2960
|
+
assert.throws(() => RP.appUser({ env: {}, userInfo: runner('root') }), /is root — set PROVISION_APP_USER/);
|
|
2961
|
+
assert.throws(() => RP.appUser({ env: {}, userInfo: () => { throw new Error('ENOENT'); } }),
|
|
2962
|
+
/"" is not a usable unix account name/, 'a uid with no passwd entry names the knob');
|
|
2963
|
+
for (const bad of ['a b', 'x;rm -rf /', '$(id)', 'Admin', '-o', 'a'.repeat(33)]) {
|
|
2964
|
+
assert.throws(() => RP.appUser({ env: { PROVISION_APP_USER: bad }, userInfo: runner('deploy') }),
|
|
2965
|
+
/not a usable unix account name/, `refuses ${JSON.stringify(bad)}`);
|
|
2966
|
+
}
|
|
2967
|
+
// …and the generators go through it rather than rendering the bad value.
|
|
2968
|
+
process.env.PROVISION_APP_USER = 'root';
|
|
2969
|
+
try {
|
|
2970
|
+
assert.throws(() => P.backupService(coTenant), /is root/);
|
|
2971
|
+
assert.throws(() => P.grantInstanceRepoReadCmd(coTenant), /is root/);
|
|
2972
|
+
} finally { process.env.PROVISION_APP_USER = 'appsvc'; }
|
|
2936
2973
|
});
|
|
2937
2974
|
|
|
2938
2975
|
t('dbRoleCmd: LOGIN + password, upserted — CREATE on a fresh box, ALTER on a re-provision (the rotation path)', () => {
|
|
@@ -3052,7 +3089,7 @@ t('dedicatedUserData: the droplet runs the SAME account + role steps, in the sam
|
|
|
3052
3089
|
const ud = P.dedicatedUserData(coTenant);
|
|
3053
3090
|
const at = (needle) => ud.indexOf(needle);
|
|
3054
3091
|
assert.ok(at('useradd --system') > -1, 'the droplet creates the account too');
|
|
3055
|
-
assert.ok(at('usermod -aG
|
|
3092
|
+
assert.ok(at('usermod -aG $(id -gn appsvc)') > -1);
|
|
3056
3093
|
assert.ok(at('useradd --system') < at('createdb mercury'), 'account before the DB');
|
|
3057
3094
|
assert.ok(at('migrate.sh') < at('CREATE ROLE %I'), 'the role grants must follow the migrate that created the tables');
|
|
3058
3095
|
assert.ok(at('CREATE ROLE %I') < at('GRANT ALL ON ALL TABLES'), 'role before its grants');
|
|
@@ -12,6 +12,9 @@ const require = createRequire(import.meta.url);
|
|
|
12
12
|
const P = require('../scripts/gds/provision.js');
|
|
13
13
|
const { accountPlaneGap, accountPreflight } = require('../scripts/gds/provision-repo.js');
|
|
14
14
|
|
|
15
|
+
// The app user is derived from whoever runs the suite unless pinned (task 1002719).
|
|
16
|
+
process.env.PROVISION_APP_USER = 'appsvc';
|
|
17
|
+
|
|
15
18
|
const PROBE = `sh -c 'test "$(id -u)" = 0'`;
|
|
16
19
|
const isRoot = () => ({ ok: true, stdout: '' });
|
|
17
20
|
// What a shell that RAN reports: exit 1 from `test`, or 127 when sudo is not installed.
|
|
@@ -133,3 +136,16 @@ test('provision: a box where the runner reaches root is unchanged — the live s
|
|
|
133
136
|
const idx = (re) => h.cmds.findIndex((c) => re.test(c));
|
|
134
137
|
assert.ok(idx(/id -u.*= 0/) < idx(/useradd --system/), 'the probe precedes the account it guards');
|
|
135
138
|
});
|
|
139
|
+
|
|
140
|
+
test('provision: an app user that resolves to root is refused before a single write (task 1002719)', async () => {
|
|
141
|
+
const h = harness();
|
|
142
|
+
process.env.PROVISION_APP_USER = 'root'; // what an unpinned runner running as root resolves
|
|
143
|
+
try {
|
|
144
|
+
const r = await P.provisionInstance(coTenant, h.deps);
|
|
145
|
+
assert.equal(r.ok, false);
|
|
146
|
+
assert.match(r.error, /is root — set PROVISION_APP_USER/);
|
|
147
|
+
assert.ok(!h.ran(/useradd/), 'refused before step 1c makes an account the run could not finish');
|
|
148
|
+
assert.equal(h.writes.length, 0);
|
|
149
|
+
assert.deepEqual(h.statuses, [], 'the instance never flips to provisioning');
|
|
150
|
+
} finally { process.env.PROVISION_APP_USER = 'appsvc'; }
|
|
151
|
+
});
|