feat(release): add reversible production cutover gates

This commit is contained in:
chick
2026-07-18 17:24:51 +08:00
parent 3718c91706
commit 4a7e29cf06
18 changed files with 1468 additions and 8 deletions
+55
View File
@@ -8,6 +8,10 @@ export interface ProductionReadinessOptions {
readonly gatewayToken: string;
/** Metadata/availability probe only. This callback must never retrieve a secret. */
readonly keychainMetadataCheck: () => boolean | Promise<boolean>;
/** Release gates are deliberately required: omitting one fails readiness closed. */
readonly assetsCheck?: () => boolean | Promise<boolean>;
readonly recoveryMarkersCheck?: () => number | Promise<number>;
readonly upstreamGatewayCheck?: () => boolean | Promise<boolean>;
}
interface ExecutableMetadata {
isFile(): boolean;
@@ -57,6 +61,57 @@ export function createProductionReadiness(
checks.keychain = 'unavailable';
}
if (checks.keychain !== 'ready') ready = false;
try {
const row = options.db
.prepare(
"SELECT value_json FROM app_settings WHERE key = 'legacyImport.pendingCredentials'",
)
.get() as { value_json: string } | undefined;
const value: unknown = row === undefined ? 0 : JSON.parse(row.value_json);
checks.pendingCredentials =
Number.isSafeInteger(value) && (value as number) >= 0
? value === 0
? 'ready'
: `pending:${String(value)}`
: 'unavailable';
} catch {
checks.pendingCredentials = 'unavailable';
}
if (checks.pendingCredentials !== 'ready') ready = false;
if (options.recoveryMarkersCheck === undefined) {
checks.recoveryMarkers = 'unconfigured';
} else {
try {
const count = await options.recoveryMarkersCheck();
checks.recoveryMarkers =
Number.isSafeInteger(count) && count >= 0
? count === 0
? 'ready'
: `present:${String(count)}`
: 'unavailable';
} catch {
checks.recoveryMarkers = 'unavailable';
}
}
if (checks.recoveryMarkers !== 'ready') ready = false;
for (const [name, check] of [
['assets', options.assetsCheck],
['upstreamGateway', options.upstreamGatewayCheck],
] as const) {
if (check === undefined) {
checks[name] = 'unconfigured';
} else {
try {
checks[name] = (await check()) ? 'ready' : 'unavailable';
} catch {
checks[name] = 'unavailable';
}
}
if (checks[name] !== 'ready') ready = false;
}
return { ready, checks };
};
}