fix(config): migrate legacy release arrays during config fixes and validate release config shape
This commit is contained in:
@@ -19,6 +19,38 @@ const ensureObject = (parent: Record<string, any>, key: string): Record<string,
|
||||
return parent[key];
|
||||
};
|
||||
|
||||
const normalizeRegistryList = (registries: unknown[]): string[] => {
|
||||
const result: string[] = [];
|
||||
for (const registry of registries) {
|
||||
if (typeof registry !== "string" || !registry.trim()) {
|
||||
continue;
|
||||
}
|
||||
const normalizedRegistry = normalizeRegistryUrl(registry);
|
||||
if (!result.includes(normalizedRegistry)) {
|
||||
result.push(normalizedRegistry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const migrateLegacyReleaseArray = (smartconfigJson: Record<string, any>): boolean => {
|
||||
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||
if (!Array.isArray(cliConfig.release)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const registries = normalizeRegistryList(cliConfig.release);
|
||||
cliConfig.release = {
|
||||
targets: {
|
||||
npm: {
|
||||
enabled: registries.length > 0,
|
||||
registries,
|
||||
},
|
||||
},
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
|
||||
let migrated = false;
|
||||
const migrations = [
|
||||
@@ -50,9 +82,9 @@ const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean =>
|
||||
|
||||
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
||||
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||
let migrated = migrateLegacyReleaseArray(smartconfigJson);
|
||||
const releaseConfig = ensureObject(cliConfig, "release");
|
||||
|
||||
let migrated = false;
|
||||
const targets = ensureObject(releaseConfig, "targets");
|
||||
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
|
||||
|
||||
@@ -192,6 +224,10 @@ export const migrateSmartconfigData = (
|
||||
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
|
||||
let currentVersion = fromVersion;
|
||||
|
||||
if (targetVersion >= 2) {
|
||||
migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
|
||||
}
|
||||
|
||||
if (currentVersion < 2 && targetVersion >= 2) {
|
||||
migrated = migrateToV2(smartconfigJson) || migrated;
|
||||
currentVersion = 2;
|
||||
|
||||
+60
-9
@@ -168,7 +168,7 @@ async function handleInteractiveMenu(): Promise<void> {
|
||||
{ name: "Configure release workflow", value: "release" },
|
||||
{ name: "Configure services", value: "services" },
|
||||
{ name: "Validate configuration (doctor)", value: "doctor" },
|
||||
{ name: "Fix configuration with opencode", value: "fix" },
|
||||
{ name: "Fix configuration", value: "fix" },
|
||||
{ name: "Add an npm target registry", value: "add" },
|
||||
{ name: "Remove an npm target registry", value: "remove" },
|
||||
{ name: "Clear npm target registries", value: "clear" },
|
||||
@@ -939,8 +939,8 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const findings = await collectDoctorFindings();
|
||||
const counts = countDoctorFindings(findings);
|
||||
let findings = await collectDoctorFindings();
|
||||
let counts = countDoctorFindings(findings);
|
||||
const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim();
|
||||
const force = Boolean(argvArg.force);
|
||||
|
||||
@@ -954,10 +954,10 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
|
||||
if (!mode.yes) {
|
||||
if (!mode.interactive) {
|
||||
throw new Error("Config fix requires an interactive terminal or `-y` to run opencode non-interactively.");
|
||||
throw new Error("Config fix requires an interactive terminal or `-y` to run non-interactively.");
|
||||
}
|
||||
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||
`Run opencode to fix .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`,
|
||||
`Run configuration fixes for .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`,
|
||||
true,
|
||||
);
|
||||
if (!confirmed) {
|
||||
@@ -966,6 +966,16 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const appliedKnownFixes = await applyKnownConfigFixes(mode);
|
||||
if (appliedKnownFixes) {
|
||||
findings = await collectDoctorFindings();
|
||||
counts = countDoctorFindings(findings);
|
||||
if (counts.error === 0 && counts.warn === 0 && !extraInstructions && !force) {
|
||||
printDoctorResult(findings, mode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const opencodeArgs = [
|
||||
"run",
|
||||
"--title",
|
||||
@@ -1004,6 +1014,33 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
printDoctorResult(finalFindings, mode);
|
||||
}
|
||||
|
||||
async function applyKnownConfigFixes(mode: ICliMode): Promise<boolean> {
|
||||
const smartconfigPath = getSmartconfigPath();
|
||||
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let smartconfigData: Record<string, any>;
|
||||
try {
|
||||
smartconfigData = await readSmartconfigFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = migrateSmartconfigData(smartconfigData);
|
||||
if (!result.migrated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await writeSmartconfigFile(smartconfigData);
|
||||
plugins.logger.log(
|
||||
"success",
|
||||
`Applied known .smartconfig.json migrations to schema v${result.toVersion}`,
|
||||
);
|
||||
await formatSmartconfigWithDiff(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
|
||||
const findings: IDoctorFinding[] = [];
|
||||
const smartconfigPath = getSmartconfigPath();
|
||||
@@ -1071,7 +1108,7 @@ async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
|
||||
await validateDetectedProjectType(cliConfig, findings);
|
||||
|
||||
validateCommitConfig(cliConfig.commit || {}, findings);
|
||||
await validateReleaseConfig(cliConfig.release || {}, smartconfigData, findings);
|
||||
await validateReleaseConfig(cliConfig.release, smartconfigData, findings);
|
||||
|
||||
return findings;
|
||||
}
|
||||
@@ -1570,10 +1607,24 @@ function validateCommitConfig(
|
||||
}
|
||||
|
||||
async function validateReleaseConfig(
|
||||
releaseConfig: Record<string, any>,
|
||||
rawReleaseConfig: unknown,
|
||||
smartconfigData: Record<string, any>,
|
||||
findings: IDoctorFinding[],
|
||||
): Promise<void> {
|
||||
const releaseConfig = rawReleaseConfig === undefined ? {} : rawReleaseConfig;
|
||||
if (!isPlainObject(releaseConfig)) {
|
||||
findings.push({
|
||||
level: "error",
|
||||
message: `Release config must be an object, found ${
|
||||
Array.isArray(releaseConfig) ? "array" : typeof releaseConfig
|
||||
}`,
|
||||
fix: Array.isArray(releaseConfig)
|
||||
? "Run `gitzone config migrate` to move legacy registry arrays into release.targets.npm.registries."
|
||||
: "Set @git.zone/cli.release to an object or remove it.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmation = releaseConfig.confirmation;
|
||||
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
|
||||
findings.push({ level: "ok", message: "Release confirmation mode is valid" });
|
||||
@@ -1993,7 +2044,7 @@ export function showHelp(mode?: ICliMode): void {
|
||||
{ name: "cli", description: "Configure CLI behavior interactively" },
|
||||
{ name: "release", description: "Configure release workflow interactively" },
|
||||
{ name: "doctor", description: "Validate .smartconfig.json" },
|
||||
{ name: "fix [instructions]", description: "Use opencode to repair .smartconfig.json" },
|
||||
{ name: "fix [instructions]", description: "Repair .smartconfig.json" },
|
||||
{ name: "get <path>", description: "Read a single config value" },
|
||||
{ name: "set <path> <value>", description: "Write a config value" },
|
||||
{ name: "unset <path>", description: "Delete a config value" },
|
||||
@@ -2038,7 +2089,7 @@ export function showHelp(mode?: ICliMode): void {
|
||||
console.log(" cli Configure CLI behavior interactively");
|
||||
console.log(" release Configure release workflow interactively");
|
||||
console.log(" doctor Validate .smartconfig.json");
|
||||
console.log(" fix [instructions] Use opencode to repair .smartconfig.json");
|
||||
console.log(" fix [instructions] Repair .smartconfig.json");
|
||||
console.log(" get <path> Read a single config value");
|
||||
console.log(" set <path> <value> Write a config value");
|
||||
console.log(" unset <path> Delete a config value");
|
||||
|
||||
Reference in New Issue
Block a user