Files
john b1577a16e9
Memind CI / Test, build, and release guards (push) Failing after 12m3s
fix: harden release gate and page delivery
2026-07-26 14:32:01 +08:00

158 lines
3.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}