158 lines
3.5 KiB
JavaScript
158 lines
3.5 KiB
JavaScript
import fs from 'node:fs/promises';
|
||
import path from 'node:path';
|
||
|
||
export const SCENARIO_GROUP_COUNTS = Object.freeze({
|
||
REL: 11,
|
||
AUTH: 8,
|
||
CHAT: 15,
|
||
MEM: 16,
|
||
FILE: 8,
|
||
IMGPG: 8,
|
||
XLS: 12,
|
||
PAGE: 13,
|
||
DATA: 13,
|
||
SEARCH: 8,
|
||
AGENT: 11,
|
||
SCHED: 5,
|
||
WX: 13,
|
||
BILL: 7,
|
||
MS: 9,
|
||
PLAZA: 5,
|
||
CFG: 8,
|
||
UI: 8,
|
||
COMP: 9,
|
||
});
|
||
|
||
export const SCENARIO_MODE_BY_GROUP = Object.freeze({
|
||
REL: 'upgrade',
|
||
AUTH: 'scenarios',
|
||
CHAT: 'scenarios',
|
||
MEM: 'scenarios',
|
||
FILE: 'scenarios',
|
||
IMGPG: 'scenarios',
|
||
XLS: 'scenarios',
|
||
PAGE: 'scenarios',
|
||
DATA: 'scenarios',
|
||
SEARCH: 'providers',
|
||
AGENT: 'scenarios',
|
||
SCHED: 'scenarios',
|
||
WX: 'scenarios',
|
||
BILL: 'scenarios',
|
||
MS: 'scenarios',
|
||
PLAZA: 'scenarios',
|
||
CFG: 'deterministic',
|
||
UI: 'browser',
|
||
COMP: 'upgrade',
|
||
});
|
||
|
||
const ALWAYS_REQUIRED = new Set([
|
||
...range('REL', 1, 11),
|
||
'AUTH-01',
|
||
'AUTH-04',
|
||
'AUTH-05',
|
||
'AUTH-06',
|
||
'CHAT-01',
|
||
'CHAT-02',
|
||
'CHAT-06',
|
||
'CHAT-07',
|
||
'CHAT-08',
|
||
'CHAT-09',
|
||
'CHAT-11',
|
||
'CHAT-12',
|
||
'CHAT-13',
|
||
'CHAT-15',
|
||
'MEM-08',
|
||
'MEM-13',
|
||
'MEM-14',
|
||
'MEM-15',
|
||
'MEM-16',
|
||
'FILE-07',
|
||
'PAGE-08',
|
||
'PAGE-09',
|
||
'PAGE-10',
|
||
'PAGE-12',
|
||
'DATA-06',
|
||
'DATA-10',
|
||
'DATA-12',
|
||
'AGENT-10',
|
||
'AGENT-11',
|
||
...range('WX', 9, 13),
|
||
'BILL-02',
|
||
'BILL-03',
|
||
'BILL-04',
|
||
'MS-01',
|
||
'MS-03',
|
||
'MS-04',
|
||
'CFG-08',
|
||
...range('COMP', 1, 4),
|
||
'COMP-08',
|
||
'COMP-09',
|
||
]);
|
||
|
||
function range(prefix, start, end) {
|
||
return Array.from(
|
||
{ length: end - start + 1 },
|
||
(_, index) => `${prefix}-${String(start + index).padStart(2, '0')}`,
|
||
);
|
||
}
|
||
|
||
export function expectedScenarioIds() {
|
||
return Object.entries(SCENARIO_GROUP_COUNTS).flatMap(
|
||
([prefix, count]) => range(prefix, 1, count),
|
||
);
|
||
}
|
||
|
||
export function isScenarioExemptable(id) {
|
||
return !ALWAYS_REQUIRED.has(id);
|
||
}
|
||
|
||
export function validateScenarioCatalog(catalog) {
|
||
const expected = expectedScenarioIds();
|
||
const expectedSet = new Set(expected);
|
||
const ids = catalog.map((scenario) => scenario.id);
|
||
const unique = new Set(ids);
|
||
const missing = expected.filter((id) => !unique.has(id));
|
||
const unexpected = ids.filter((id) => !expectedSet.has(id));
|
||
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
||
|
||
if (catalog.length !== expected.length || unique.size !== expected.length) {
|
||
throw new Error(
|
||
`Release gate catalog must contain exactly ${expected.length} unique scenarios `
|
||
+ `(found ${catalog.length}/${unique.size})`,
|
||
);
|
||
}
|
||
if (missing.length || unexpected.length || duplicates.length) {
|
||
throw new Error(
|
||
`Release gate catalog mismatch: missing=${missing.join(',') || '-'} `
|
||
+ `unexpected=${unexpected.join(',') || '-'} duplicates=${[...new Set(duplicates)].join(',') || '-'}`,
|
||
);
|
||
}
|
||
for (const scenario of catalog) {
|
||
if (!scenario.name || !scenario.group || !scenario.mode) {
|
||
throw new Error(`Scenario ${scenario.id} is missing name, group, or mode`);
|
||
}
|
||
}
|
||
return catalog;
|
||
}
|
||
|
||
export async function loadScenarioCatalog({
|
||
root = path.resolve(new URL('..', import.meta.url).pathname),
|
||
guardianPath = path.join(root, 'docs', 'production-release-guardian.md'),
|
||
} = {}) {
|
||
const source = await fs.readFile(guardianPath, 'utf8');
|
||
const catalog = [];
|
||
const scenarioPattern = /^- \*\*([A-Z]+-\d{2})\*\*:(.+)$/gm;
|
||
for (const match of source.matchAll(scenarioPattern)) {
|
||
const id = match[1];
|
||
const group = id.split('-')[0];
|
||
catalog.push({
|
||
id,
|
||
name: match[2].trim(),
|
||
group,
|
||
mode: SCENARIO_MODE_BY_GROUP[group],
|
||
exemptable: isScenarioExemptable(id),
|
||
});
|
||
}
|
||
return validateScenarioCatalog(catalog);
|
||
}
|