Few improvements on version upgrade tool

This commit is contained in:
Carlos Monastyrski
2025-09-23 11:40:02 -03:00
parent a1aab68461
commit e29ff3bbe4
4 changed files with 266 additions and 28 deletions
@@ -2,6 +2,7 @@ import RE2 from "re2";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { publicEndpointLimit } from "@app/server/config/rateLimiter";
const versionSchema = z
@@ -40,7 +41,7 @@ export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
versions
};
} catch (error) {
req.log.error(error, "Failed to fetch versions");
logger.error(error, "Failed to fetch versions");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: "Invalid query parameters" });
}
@@ -101,14 +102,14 @@ export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
const result = await req.server.services.upgradePath.calculateUpgradePath(fromVersion, toVersion);
req.log.info(
logger.info(
{ pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 },
"Upgrade path calculated"
);
return result;
} catch (error) {
req.log.error(error, "Failed to calculate upgrade path");
logger.error(error, "Failed to calculate upgrade path");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: `Invalid input: ${error.errors.map((e) => e.message).join(", ")}` });
}
@@ -121,6 +121,10 @@ const makeRequest = async <T>(
clearTimeout(timeout);
if (error instanceof Error && error.name === "AbortError") {
if (retryCount < config.maxRetries) {
await delay(config.retryDelay * 2 ** retryCount);
return await makeRequest<T>(url, config, retryCount + 1);
}
throw new Error(`Request timeout after ${config.timeout}ms`);
}
@@ -5,6 +5,7 @@ import RE2 from "re2";
import { z } from "zod";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { logger } from "@app/lib/logger";
import { fetchReleases } from "./github-client";
import { BreakingChange, FormattedRelease, UpgradePathConfig, UpgradePathResult, VersionConfig } from "./types";
@@ -20,25 +21,44 @@ const versionSchema = z
.max(50)
.regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format");
const breakingChangeSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().min(1).max(1000),
action: z.string().min(1).max(500)
});
const versionConfigSchema = z.object({
breaking_changes: z.array(breakingChangeSchema).optional(),
db_schema_changes: z.string().max(1000).optional(),
notes: z.string().max(2000).optional()
});
interface CalculateUpgradePathParams {
fromVersion: string;
toVersion: string;
}
export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFactory) => {
const sanitizeCacheKey = (key: string): string => {
return key.replace(new RE2(/[^a-zA-Z0-9\-:._]/g), "_");
};
const getGitHubReleases = async (): Promise<FormattedRelease[]> => {
const cacheKey = "upgrade-path:releases";
try {
const cached = await keyStore.getItem(cacheKey);
if (cached) return JSON.parse(cached) as FormattedRelease[];
if (cached) {
const cachedReleases = JSON.parse(cached) as FormattedRelease[];
if (cachedReleases.length > 0) {
return cachedReleases;
}
}
} catch (error) {
// Cache miss, continue to fetch from source
logger.error(error, "Failed to retrieve releases from cache");
}
try {
const releases = await fetchReleases(false);
const filteredReleases = releases.filter((v) => !v.tagName.includes("nightly"));
await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(filteredReleases));
@@ -48,14 +68,14 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
}
};
const getUpgradePathConfig = async (): Promise<Record<string, VersionConfig>> => {
const getUpgradePathConfig = async (): Promise<Record<string, z.infer<typeof versionConfigSchema>>> => {
const cacheKey = "upgrade-path:config";
try {
const cached = await keyStore.getItem(cacheKey);
if (cached) return JSON.parse(cached) as Record<string, VersionConfig>;
} catch (error) {
// Cache miss, continue to fetch from source
logger.error(error, "Failed to retrieve config from cache");
}
try {
@@ -88,14 +108,12 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
};
const normalizeVersion = (version: string): string => {
// Extract just the X.X.X.X part from any version format
const versionRegex = new RE2(/(\d+\.\d+\.\d+(?:\.\d+)?)/);
const versionMatch = version.match(versionRegex);
if (versionMatch) {
return versionMatch[1];
}
// Handle legacy version formats
if (version.startsWith("infisical/")) {
return version.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "");
}
@@ -104,23 +122,28 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
const findBreakingChangesForVersion = (
version: FormattedRelease,
config: Record<string, VersionConfig>
config: Record<string, z.infer<typeof versionConfigSchema>>
): BreakingChange[] => {
// Check multiple key variations for breaking changes configuration
const versionNumber = normalizeVersion(version.tagName);
const cleanVersionNumber = versionNumber.replace(new RE2(/^v/), "");
const possibleKeys = [
version.tagName,
version.normalizedTagName,
versionNumber,
`v${versionNumber}`,
`v${cleanVersionNumber}`,
cleanVersionNumber,
version.tagName.replace(new RE2(/^infisical\//), ""),
version.tagName.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "")
version.tagName.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), ""),
`v${cleanVersionNumber}`,
cleanVersionNumber
];
for (const key of possibleKeys) {
const versionConfig = config[key];
if (versionConfig?.breaking_changes?.length) {
return versionConfig.breaking_changes;
return versionConfig.breaking_changes as BreakingChange[];
}
}
return [];
@@ -145,13 +168,13 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
const calculateUpgradePath = async (params: CalculateUpgradePathParams): Promise<UpgradePathResult> => {
const { fromVersion, toVersion } = validateParams(params);
const cacheKey = `upgrade-path:${fromVersion}:${toVersion}`;
const cacheKey = sanitizeCacheKey(`upgrade-path:${fromVersion}:${toVersion}`);
try {
const cached = await keyStore.getItem(cacheKey);
if (cached) return JSON.parse(cached) as UpgradePathResult;
} catch (error) {
// Cache miss, continue to fetch from source
logger.error(error, "Failed to retrieve upgrade path from cache");
}
const [releases, config] = await Promise.all([getGitHubReleases(), getUpgradePathConfig()]);
@@ -197,23 +220,21 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
const features: Array<{ version: string; name: string; body: string; publishedAt: string }> = [];
let hasDbMigration = false;
// Process versions in upgrade path, excluding starting version
for (let i = 1; i < upgradePath.length; i += 1) {
// Process versions in upgrade path
for (let i = 0; i < upgradePath.length; i += 1) {
const version = upgradePath[i];
const isFromVersion = normalizeVersion(version.normalizedTagName) === cleanFrom;
// Process breaking changes for intermediate versions only
if (!isFromVersion) {
const versionBreakingChanges = findBreakingChangesForVersion(version, config);
if (versionBreakingChanges.length > 0) {
breakingChanges.push({
version: version.tagName,
changes: versionBreakingChanges
});
}
// Process breaking changes for all versions in the upgrade path
const versionBreakingChanges = findBreakingChangesForVersion(version, config);
if (versionBreakingChanges.length > 0) {
breakingChanges.push({
version: version.tagName,
changes: versionBreakingChanges
});
}
// Process database migrations for intermediate versions only
// Process database migrations for intermediate versions only (excluding starting version)
if (!isFromVersion) {
const versionNumber = normalizeVersion(version.tagName);
const possibleKeys = [