fix(config): migrate legacy release arrays during config fixes and validate release config shape
This commit is contained in:
@@ -3,6 +3,14 @@
|
|||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- migrate legacy release arrays during config fixes and validate release config shape (config)
|
||||||
|
- Automatically converts legacy release registry arrays into release.targets.npm.registries during smartconfig migration and config fix runs.
|
||||||
|
- Re-runs doctor checks after applying known migrations so resolved issues do not require the external fixer.
|
||||||
|
- Reports an explicit validation error when release config is not an object.
|
||||||
|
- Updates config fix prompts and help text to use generic configuration repair wording.
|
||||||
|
|
||||||
## 2026-05-13 - 2.19.0
|
## 2026-05-13 - 2.19.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -19,6 +19,38 @@ const ensureObject = (parent: Record<string, any>, key: string): Record<string,
|
|||||||
return parent[key];
|
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 => {
|
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
let migrated = false;
|
let migrated = false;
|
||||||
const migrations = [
|
const migrations = [
|
||||||
@@ -50,9 +82,9 @@ const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean =>
|
|||||||
|
|
||||||
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
||||||
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
||||||
|
let migrated = migrateLegacyReleaseArray(smartconfigJson);
|
||||||
const releaseConfig = ensureObject(cliConfig, "release");
|
const releaseConfig = ensureObject(cliConfig, "release");
|
||||||
|
|
||||||
let migrated = false;
|
|
||||||
const targets = ensureObject(releaseConfig, "targets");
|
const targets = ensureObject(releaseConfig, "targets");
|
||||||
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
|
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
|
||||||
|
|
||||||
@@ -192,6 +224,10 @@ export const migrateSmartconfigData = (
|
|||||||
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
|
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
|
||||||
let currentVersion = fromVersion;
|
let currentVersion = fromVersion;
|
||||||
|
|
||||||
|
if (targetVersion >= 2) {
|
||||||
|
migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentVersion < 2 && targetVersion >= 2) {
|
if (currentVersion < 2 && targetVersion >= 2) {
|
||||||
migrated = migrateToV2(smartconfigJson) || migrated;
|
migrated = migrateToV2(smartconfigJson) || migrated;
|
||||||
currentVersion = 2;
|
currentVersion = 2;
|
||||||
|
|||||||
+60
-9
@@ -168,7 +168,7 @@ async function handleInteractiveMenu(): Promise<void> {
|
|||||||
{ name: "Configure release workflow", value: "release" },
|
{ name: "Configure release workflow", value: "release" },
|
||||||
{ name: "Configure services", value: "services" },
|
{ name: "Configure services", value: "services" },
|
||||||
{ name: "Validate configuration (doctor)", value: "doctor" },
|
{ 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: "Add an npm target registry", value: "add" },
|
||||||
{ name: "Remove an npm target registry", value: "remove" },
|
{ name: "Remove an npm target registry", value: "remove" },
|
||||||
{ name: "Clear npm target registries", value: "clear" },
|
{ name: "Clear npm target registries", value: "clear" },
|
||||||
@@ -939,8 +939,8 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const findings = await collectDoctorFindings();
|
let findings = await collectDoctorFindings();
|
||||||
const counts = countDoctorFindings(findings);
|
let counts = countDoctorFindings(findings);
|
||||||
const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim();
|
const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim();
|
||||||
const force = Boolean(argvArg.force);
|
const force = Boolean(argvArg.force);
|
||||||
|
|
||||||
@@ -954,10 +954,10 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|||||||
|
|
||||||
if (!mode.yes) {
|
if (!mode.yes) {
|
||||||
if (!mode.interactive) {
|
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(
|
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,
|
true,
|
||||||
);
|
);
|
||||||
if (!confirmed) {
|
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 = [
|
const opencodeArgs = [
|
||||||
"run",
|
"run",
|
||||||
"--title",
|
"--title",
|
||||||
@@ -1004,6 +1014,33 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|||||||
printDoctorResult(finalFindings, mode);
|
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[]> {
|
async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
|
||||||
const findings: IDoctorFinding[] = [];
|
const findings: IDoctorFinding[] = [];
|
||||||
const smartconfigPath = getSmartconfigPath();
|
const smartconfigPath = getSmartconfigPath();
|
||||||
@@ -1071,7 +1108,7 @@ async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
|
|||||||
await validateDetectedProjectType(cliConfig, findings);
|
await validateDetectedProjectType(cliConfig, findings);
|
||||||
|
|
||||||
validateCommitConfig(cliConfig.commit || {}, findings);
|
validateCommitConfig(cliConfig.commit || {}, findings);
|
||||||
await validateReleaseConfig(cliConfig.release || {}, smartconfigData, findings);
|
await validateReleaseConfig(cliConfig.release, smartconfigData, findings);
|
||||||
|
|
||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
@@ -1570,10 +1607,24 @@ function validateCommitConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function validateReleaseConfig(
|
async function validateReleaseConfig(
|
||||||
releaseConfig: Record<string, any>,
|
rawReleaseConfig: unknown,
|
||||||
smartconfigData: Record<string, any>,
|
smartconfigData: Record<string, any>,
|
||||||
findings: IDoctorFinding[],
|
findings: IDoctorFinding[],
|
||||||
): Promise<void> {
|
): 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;
|
const confirmation = releaseConfig.confirmation;
|
||||||
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
|
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
|
||||||
findings.push({ level: "ok", message: "Release confirmation mode is valid" });
|
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: "cli", description: "Configure CLI behavior interactively" },
|
||||||
{ name: "release", description: "Configure release workflow interactively" },
|
{ name: "release", description: "Configure release workflow interactively" },
|
||||||
{ name: "doctor", description: "Validate .smartconfig.json" },
|
{ 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: "get <path>", description: "Read a single config value" },
|
||||||
{ name: "set <path> <value>", description: "Write a config value" },
|
{ name: "set <path> <value>", description: "Write a config value" },
|
||||||
{ name: "unset <path>", description: "Delete 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(" cli Configure CLI behavior interactively");
|
||||||
console.log(" release Configure release workflow interactively");
|
console.log(" release Configure release workflow interactively");
|
||||||
console.log(" doctor Validate .smartconfig.json");
|
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(" get <path> Read a single config value");
|
||||||
console.log(" set <path> <value> Write a config value");
|
console.log(" set <path> <value> Write a config value");
|
||||||
console.log(" unset <path> Delete a config value");
|
console.log(" unset <path> Delete a config value");
|
||||||
|
|||||||
Reference in New Issue
Block a user