@bongos/core 1.20.28 → 1.20.30
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 +47 -27
- 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 +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +49 -1
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +4 -0
- package/modules/provisioning/core-upgrade.js +75 -1
- package/modules/provisioning/migrations/provisioning_029_update_channel.sql +24 -0
- package/modules/provisioning/migrations/provisioning_030_intent_failure_class.sql +28 -0
- package/modules/provisioning/routes/core-upgrade.js +21 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-core-upgrade.js +40 -20
- package/scripts/gds/provision.js +11 -0
- package/scripts/gds/upgrade-outcome.js +155 -0
- package/scripts/gds/upgrade.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_door.mjs +89 -1
- package/tests/core_upgrade_runner.mjs +120 -1
- package/tests/provision.mjs +34 -0
- package/tests/upgrade_outcome.mjs +129 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// tests/upgrade_outcome.mjs — every failed core move gets a class and honest wording
|
|
2
|
+
// (task 1004446, goal 1000090 deploy self-heal).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IS AT RISK. The deploy page said "failed and was rolled back" for every exit of
|
|
5
|
+
// `bongos upgrade`, including refusals that changed nothing and rollbacks that themselves
|
|
6
|
+
// failed. The rest of the self-heal chain (retry later, fix it, escalate) keys on the class
|
|
7
|
+
// this file assigns, so a mislabel there is a retry of something that must not be retried,
|
|
8
|
+
// or an automatic "fix" for something that needed a person.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/upgrade_outcome.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import {
|
|
15
|
+
outcomeLine, parseOutcomeLine, upgradeReason, upgradeStage, classifyFailure, runnerReason,
|
|
16
|
+
describeUpgradeFailure, FAILURE_CLASSES, REASON_CLASS, STAGES, OUTCOME_TAG,
|
|
17
|
+
} from '../scripts/gds/upgrade-outcome.js';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const R = require('../scripts/gds/provision-core-upgrade.js');
|
|
21
|
+
|
|
22
|
+
let passed = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
const t = async (name, fn) => {
|
|
25
|
+
try { await fn(); passed += 1; console.log(` PASS ${name}`); }
|
|
26
|
+
catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Each row: an upgrade.js failure result as runUpgrade returns it → the stage, reason and
|
|
30
|
+
// class it must get. These are the branches of runUpgrade's failure table (upgrade.js).
|
|
31
|
+
const OK_RB = { restoreOk: true, reinstallOk: true, restartOk: true, healthOk: true };
|
|
32
|
+
const TABLE = [
|
|
33
|
+
['downgrade refusal', { ok: false, downgrade: true, error: 'refusing to DOWNGRADE: --to 1.19.1 is older' }, 'refused', 'downgrade', 'needs_decision'],
|
|
34
|
+
['already on the target', { ok: false, error: 'already on 1.20.1 (pass --force to re-run the steps anyway)' }, 'refused', 'already_on', 'unknown'],
|
|
35
|
+
['dirty working tree', { ok: false, error: 'working tree not clean — commit/stash first, or pass --force.\n M package.json' }, 'refused', 'dirty_tree', 'known_wedge'],
|
|
36
|
+
['module pre-check', { ok: false, error: 'module pre-check failed: game incompatible with core 1.20.1' }, 'refused', 'module_incompatible', 'needs_decision'],
|
|
37
|
+
['database unresolvable', { ok: false, error: 'cannot determine which database to migrate.\n Set PGDATABASE' }, 'refused', 'db_unresolvable', 'unknown'],
|
|
38
|
+
['database identity', { ok: false, error: 'database "cb" is not reachable as the current user: role "root" does not exist' }, 'refused', 'db_identity', 'unknown'],
|
|
39
|
+
['artist gate hold', { ok: false, artistGate: true, error: 'a visual review is holding this deploy' }, 'refused', 'artist_gate', 'needs_decision'],
|
|
40
|
+
['health url missing', { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service' }, 'refused', 'health_url_missing', 'unknown'],
|
|
41
|
+
['npm install failed, rolled back', { ok: false, error: 'npm install failed (exit 1)', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'npm_install', 'transient'],
|
|
42
|
+
['disk full, rolled back', { ok: false, error: 'npm install failed (ENOSPC: no space left on device)', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'disk_full', 'known_wedge'],
|
|
43
|
+
['integrity pin mismatch', { ok: false, error: 'integrity pin MISMATCH (side-car) — installed core does not match its pin: x', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'integrity_pin', 'unknown'],
|
|
44
|
+
['migrate failed', { ok: false, error: 'migrate failed (exit 3)', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'migrate', 'unknown'],
|
|
45
|
+
['restart failed', { ok: false, error: 'restart failed (exit 1) — the instance is still running the previous core', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'restart', 'unknown'],
|
|
46
|
+
['health failed', { ok: false, error: 'health check failed after restart — rolled back to 1.19.1081', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'health', 'unknown'],
|
|
47
|
+
['served version unchanged', { ok: false, error: 'served core version did not change (still 1.19.1081, expected 1.20.1) — rolled back', rolledBack: true, rollback: OK_RB }, 'rolled_back', 'health', 'unknown'],
|
|
48
|
+
['ROLLBACK FAILED (health still bad)', { ok: false, error: 'health check failed after restart — rolled back', rolledBack: true, rollback: { ...OK_RB, healthOk: false } }, 'rollback_failed', 'rollback_failed', 'unknown'],
|
|
49
|
+
['ROLLBACK FAILED (reinstall)', { ok: false, error: 'migrate failed (exit 3)', rolledBack: true, rollback: { ...OK_RB, reinstallOk: false } }, 'rollback_failed', 'rollback_failed', 'unknown'],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
console.log('\nevery branch of the upgrade failure table:');
|
|
53
|
+
|
|
54
|
+
for (const [name, res, stage, reason, cls] of TABLE) {
|
|
55
|
+
await t(`${name} → ${stage} / ${reason} / ${cls}`, () => {
|
|
56
|
+
const o = parseOutcomeLine(outcomeLine(res));
|
|
57
|
+
assert.deepEqual(o, { stage, reason });
|
|
58
|
+
assert.equal(classifyFailure(o.reason).class, cls);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('\nthe vocabulary:');
|
|
63
|
+
|
|
64
|
+
await t('every reason maps to one of the four classes', () => {
|
|
65
|
+
for (const [r, c] of Object.entries(REASON_CLASS)) assert.ok(FAILURE_CLASSES.includes(c), `${r} → ${c}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await t('an unlisted reason is unknown — the safe direction', () => {
|
|
69
|
+
assert.deepEqual(classifyFailure('something_new'), { class: 'unknown', reason: 'unknown' });
|
|
70
|
+
assert.deepEqual(classifyFailure(undefined), { class: 'unknown', reason: 'unknown' });
|
|
71
|
+
assert.deepEqual(classifyFailure('__proto__'), { class: 'unknown', reason: 'unknown' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await t('the migration CHECK carries exactly the four classes', async () => {
|
|
75
|
+
const { readFileSync } = await import('node:fs');
|
|
76
|
+
const sql = readFileSync(new URL('../modules/provisioning/migrations/provisioning_030_intent_failure_class.sql', import.meta.url), 'utf8');
|
|
77
|
+
for (const c of FAILURE_CLASSES) assert.match(sql, new RegExp(`'${c}'`));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await t('a stage is only what upgrade.js reports: no rollback flag means nothing moved', () => {
|
|
81
|
+
assert.equal(upgradeStage({ ok: false, error: 'x' }), 'refused');
|
|
82
|
+
assert.equal(upgradeStage({ ok: false, rolledBack: true, rollback: OK_RB }), 'rolled_back');
|
|
83
|
+
assert.equal(upgradeStage({ ok: false, rolledBack: true, rollback: { restartOk: false } }), 'rollback_failed');
|
|
84
|
+
assert.equal(upgradeReason(null), 'unknown');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await t('parseOutcomeLine takes the LAST line, and refuses anything off-vocabulary', () => {
|
|
88
|
+
const two = `${OUTCOME_TAG} {"stage":"refused","reason":"dirty_tree"}\nnoise\n${OUTCOME_TAG} {"stage":"rolled_back","reason":"migrate"}\n`;
|
|
89
|
+
assert.deepEqual(parseOutcomeLine(two), { stage: 'rolled_back', reason: 'migrate' });
|
|
90
|
+
assert.equal(parseOutcomeLine('no tag here'), null);
|
|
91
|
+
assert.equal(parseOutcomeLine(`${OUTCOME_TAG} {"stage":"exploded","reason":"x"}`), null);
|
|
92
|
+
assert.equal(parseOutcomeLine(`${OUTCOME_TAG} {"stage":"refused","reason":"<script>"}`), null);
|
|
93
|
+
assert.equal(parseOutcomeLine(`${OUTCOME_TAG} not json`), null);
|
|
94
|
+
assert.deepEqual([...STAGES], ['refused', 'rolled_back', 'rollback_failed']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await t('the runner\'s own refusals get their reason from targetFault\'s sentences', () => {
|
|
98
|
+
assert.equal(runnerReason(R.targetFault('1.20.1', [], '1.19.1', 'patch', () => true)), 'registry_unreadable');
|
|
99
|
+
assert.equal(runnerReason(R.targetFault('1.20.1', ['1.19.1'], '1.19.1', 'patch', () => true)), 'not_published');
|
|
100
|
+
assert.equal(runnerReason(R.targetFault('1.20.1', ['1.20.1'], null, 'patch', () => true)), 'installed_unreadable');
|
|
101
|
+
assert.equal(runnerReason(R.targetFault('1.20.1', ['1.20.1'], '1.19.1', 'patch', () => false)), 'outside_channel');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log('\nthe wording says what happened, and no more:');
|
|
105
|
+
|
|
106
|
+
await t('a refusal never says "rolled back"', () => {
|
|
107
|
+
const s = describeUpgradeFailure('1.20.1', { stage: 'refused', reason: 'dirty_tree' }, 'working tree not clean');
|
|
108
|
+
assert.doesNotMatch(s, /rolled back/);
|
|
109
|
+
assert.match(s, /refused before anything changed/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await t('a real rollback says so', () => {
|
|
113
|
+
assert.match(describeUpgradeFailure('1.20.1', { stage: 'rolled_back', reason: 'migrate' }, 'migrate failed'), /failed and was rolled back/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await t('a failed rollback says the project may be down and needs a person', () => {
|
|
117
|
+
const s = describeUpgradeFailure('1.20.1', { stage: 'rollback_failed', reason: 'rollback_failed' }, '');
|
|
118
|
+
assert.match(s, /rollback did not finish/);
|
|
119
|
+
assert.match(s, /needs a person/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t('no outcome line claims nothing about a rollback', () => {
|
|
123
|
+
const s = describeUpgradeFailure('1.20.1', null, 'sudo: unknown user bongos-test4');
|
|
124
|
+
assert.doesNotMatch(s, /rolled back/);
|
|
125
|
+
assert.match(s, /did not report how far it got/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log(`\nupgrade_outcome: ${passed} passed, ${failed} failed`);
|
|
129
|
+
if (failed) process.exit(1);
|