fix: accept published installer migration checksums

This commit is contained in:
Colin
2026-06-04 12:23:29 -04:00
parent e8dc420b4d
commit b8e15c7a98
12 changed files with 198 additions and 3 deletions

View File

@@ -91,6 +91,7 @@ export function validateInstallerMigrationRecord(record: unknown, source?: strin
throw new Error(`migration record must declare destructive as a boolean: ${displaySource}`);
}
validateStringArray(rec, 'runtimes', displaySource);
validateStringArray(rec, 'legacyChecksums', displaySource);
requireStringArray(rec, 'scopes', displaySource);
if (typeof rec['plan'] !== 'function') {
throw new Error(`migration record must include a plan function: ${displaySource}`);

View File

@@ -208,12 +208,22 @@ function migrationChecksum(migration: MigrationRecord): string {
return `sha256:${sha256Text(JSON.stringify(serializable))}`;
}
function migrationLegacyChecksums(migration: MigrationRecord): string[] {
return Array.isArray(migration.legacyChecksums)
? migration.legacyChecksums.filter((checksum): checksum is string => typeof checksum === 'string' && checksum.length > 0)
: [];
}
function migrationAcceptedChecksums(migration: MigrationRecord): Set<string> {
return new Set([migrationChecksum(migration), ...migrationLegacyChecksums(migration)]);
}
function assertAppliedMigrationChecksums(applied: Map<string, Record<string, unknown>>, migrations: MigrationRecord[]): void {
for (const migration of migrations) {
const entry = applied.get(migration.id as string);
if (!entry || !entry.checksum) continue;
const checksum = migrationChecksum(migration);
if (entry.checksum !== checksum) {
const accepted = migrationAcceptedChecksums(migration);
if (!accepted.has(entry.checksum as string)) {
throw new Error(
`applied migration checksum changed for ${migration.id as string}; create a new fix-forward migration id`
);
@@ -923,6 +933,7 @@ export = {
acquireInstallMigrationLock,
applyInstallerMigrationPlan,
classifyArtifact,
computeInstallerMigrationChecksum: migrationChecksum,
discoverInstallerMigrations,
planInstallerMigrations,
readInstallManifest,

View File

@@ -171,6 +171,8 @@ interface InstallerMigration {
title: string;
description: string;
introducedIn: string;
checksum: string;
legacyChecksums?: string[];
scopes: string[];
destructive: boolean;
plan: (ctx: PlanContext) => BaselineAction[];
@@ -181,6 +183,10 @@ const migration: InstallerMigration = {
title: 'Record first-time installer migration baseline',
description: 'Classify existing install surfaces before destructive installer migrations run.',
introducedIn: '1.50.0',
checksum: 'sha256:4ec58d35b30dbf39cc56e3972146086d8d31861ecd800cf0b37a7aa94fe74c2a',
legacyChecksums: [
'sha256:34608ea4e2f4e1c53b069604892860e603600d8573cc6a5584e4194044b48e67',
],
scopes: ['global', 'local'],
destructive: false,
plan: ({ configDir, runtime, baselineScan, classifyArtifact }: PlanContext): BaselineAction[] => {

View File

@@ -31,6 +31,8 @@ interface InstallerMigration {
title: string;
description: string;
introducedIn: string;
checksum: string;
legacyChecksums?: string[];
scopes: string[];
destructive: boolean;
plan: (ctx: MigrationPlanContext) => MigrationAction[];
@@ -46,6 +48,10 @@ const migration: InstallerMigration = {
title: 'Remove manifest-managed legacy orphan hook files',
description: 'Remove legacy orphan hook files that are still manifest-managed.',
introducedIn: '1.50.0',
checksum: 'sha256:e492698748a2436a12a55f0940f539b9bf651d8ffcac6f60cd856a6dabd6788c',
legacyChecksums: [
'sha256:4488e38c127a5225b31016918bcbc85ba3fd3139291ad407b94e76c03c0b89d3',
],
scopes: ['global', 'local'],
destructive: true,
// Retired generated hook files are removed only with manifest-managed

View File

@@ -48,6 +48,8 @@ interface InstallerMigration {
title: string;
description: string;
introducedIn: string;
checksum: string;
legacyChecksums?: string[];
runtimes: string[];
scopes: string[];
destructive: boolean;
@@ -110,6 +112,10 @@ const migration: InstallerMigration = {
title: 'Remove legacy Codex hooks.json GSD hook registrations',
description: 'Remove legacy Codex hooks.json GSD hook registrations after config.toml migration.',
introducedIn: '1.50.0',
checksum: 'sha256:5ce55294aa02f25758f604a569c899a6d2d060299189f5f447f68d8033157058',
legacyChecksums: [
'sha256:41f1545704dc72dfc3ab019207677a8652e389b200e8c450d52317df5bc198da',
],
runtimes: ['codex'],
scopes: ['global', 'local'],
destructive: true,

View File

@@ -48,6 +48,7 @@ interface InstallerMigration {
title: string;
description: string;
introducedIn: string;
checksum: string;
scopes: string[];
destructive: boolean;
plan(ctx: MigrationPlanContext): MigrationAction[];
@@ -80,6 +81,7 @@ const migration: InstallerMigration = {
'After the config dir rename from get-shit-done/ to gsd-core/ (#604), remove prior-manifest-managed files ' + // gsd-allow-legacy-name
'from the stale legacy directory during install (framework rollback restores them if install fails). User-added files are preserved.',
introducedIn: '1.2.0',
checksum: 'sha256:3a9f1d97f64097fb313203d19c6d93a187a38df61dd299afa5eef73e16124e95',
scopes: ['global', 'local'],
destructive: true,
plan(ctx: MigrationPlanContext): MigrationAction[] {