Move yaml check to use nodejs, limit versions to 0.147.0 and add UI option to redirect to the new upgrade tool

This commit is contained in:
Carlos Monastyrski
2025-09-25 22:24:47 -03:00
parent af085db685
commit 3c80456c32
6 changed files with 277 additions and 258 deletions
+2
View File
@@ -129,6 +129,8 @@ const envSchema = z
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
POSTHOG_PROJECT_API_KEY: zpStr(z.string().optional().default("phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE")),
LOOPS_API_KEY: zpStr(z.string().optional()),
// GitHub API token for upgrade path tool
GITHUB_API_TOKEN: zpStr(z.string().optional()),
// jwt options
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
@@ -1,6 +1,8 @@
/* eslint-disable no-await-in-loop */
import RE2 from "re2";
import { getConfig } from "@app/lib/config/env";
import { FormattedRelease, GitHubApiError, GitHubRelease } from "./types";
interface GitHubClientConfig {
@@ -20,7 +22,7 @@ interface RateLimitInfo {
}
const getDefaultConfig = (): GitHubClientConfig => ({
token: process.env.GITHUB_TOKEN,
token: getConfig().GITHUB_API_TOKEN,
timeout: 30000,
maxRetries: 3,
retryDelay: 1000,
@@ -56,7 +58,15 @@ const isMainInfisicalRelease = (tagName: string): boolean => {
) {
return false;
}
return tagName.startsWith("v") || tagName.startsWith("infisical/v") || new RE2(/^\d+\.\d+\.\d+/).test(tagName);
const patterns = [
new RE2(/^v\d+\.\d+\.\d+/),
new RE2(/^\d+\.\d+\.\d+/),
new RE2(/^infisical\/v?\d+\.\d+\.\d+/),
new RE2(/^infisical\/v?\d+\.\d+\.\d+[-\w]*/)
];
return patterns.some((pattern) => pattern.test(tagName));
};
const normalizeVersion = (tagName: string): string => {
@@ -72,6 +82,39 @@ const normalizeVersion = (tagName: string): string => {
return tagName.replace(new RE2(/-[a-zA-Z]+$/), "");
};
const compareVersions = (v1: string, v2: string): number => {
const normalize = (v: string) => {
const versionMatch = v.match(new RE2(/(\d+\.\d+\.\d+(?:\.\d+)?)/));
if (versionMatch) {
return versionMatch[1];
}
if (v.startsWith("infisical/")) {
return v.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "");
}
return v.replace(new RE2(/^v/), "").replace(new RE2(/-[a-zA-Z]+$/), "");
};
const clean1 = normalize(v1);
const clean2 = normalize(v2);
const parts1 = clean1.split(".").map(Number);
const parts2 = clean2.split(".").map(Number);
const maxLength = Math.max(parts1.length, parts2.length);
while (parts1.length < maxLength) parts1.push(0);
while (parts2.length < maxLength) parts2.push(0);
for (let i = 0; i < maxLength; i += 1) {
if (parts1[i] > parts2[i]) return 1;
if (parts1[i] < parts2[i]) return -1;
}
return 0;
};
const isVersionAtLeastMinimum = (tagName: string, minimumVersion = "0.147.0"): boolean => {
return compareVersions(tagName, minimumVersion) >= 0;
};
const makeRequest = async <T>(
url: string,
config: GitHubClientConfig,
@@ -142,10 +185,11 @@ export const fetchReleases = async (includePrerelease = false): Promise<Formatte
const allReleases: GitHubRelease[] = [];
let page = 1;
let hasMorePages = true;
let reachedMinimumVersion = false;
const maxConcurrentRequests = Math.min(3, config.maxPagesPerRequest);
while (hasMorePages && page <= config.maxPagesPerRequest) {
while (hasMorePages && page <= config.maxPagesPerRequest && !reachedMinimumVersion) {
const requests: Promise<{ data: GitHubRelease[]; rateLimit: RateLimitInfo }>[] = [];
for (let i = 0; i < maxConcurrentRequests && page <= config.maxPagesPerRequest; i += 1, page += 1) {
@@ -160,7 +204,16 @@ export const fetchReleases = async (includePrerelease = false): Promise<Formatte
if (result.status === "fulfilled") {
const { data } = result.value;
if (data.length > 0) {
allReleases.push(...data);
for (const release of data) {
if (!release.draft && isMainInfisicalRelease(release.tag_name)) {
if (isVersionAtLeastMinimum(release.tag_name)) {
allReleases.push(release);
} else {
reachedMinimumVersion = true;
break;
}
}
}
hasData = true;
}
}
@@ -172,8 +225,6 @@ export const fetchReleases = async (includePrerelease = false): Promise<Formatte
}
const formattedReleases = allReleases
.filter((release) => !release.draft)
.filter((release) => isMainInfisicalRelease(release.tag_name))
.map(
(release): FormattedRelease => ({
tagName: release.tag_name,
@@ -120,35 +120,6 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
return version.replace(new RE2(/^v/), "").replace(new RE2(/-[a-zA-Z]+$/), "");
};
const findBreakingChangesForVersion = (
version: FormattedRelease,
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${cleanVersionNumber}`,
cleanVersionNumber,
version.tagName.replace(new RE2(/^infisical\//), ""),
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 as BreakingChange[];
}
}
return [];
};
const validateParams = (params: CalculateUpgradePathParams) => {
const { fromVersion, toVersion } = params;
@@ -182,59 +153,69 @@ export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFacto
const cleanFrom = normalizeVersion(fromVersion);
const cleanTo = normalizeVersion(toVersion);
const compareVersions = (v1: string, v2: string): number => {
const normalize = (v: string) => normalizeVersion(v);
const clean1 = normalize(v1);
const clean2 = normalize(v2);
const parts1 = clean1.split(".").map(Number);
const parts2 = clean2.split(".").map(Number);
const maxLength = Math.max(parts1.length, parts2.length);
while (parts1.length < maxLength) parts1.push(0);
while (parts2.length < maxLength) parts2.push(0);
for (let i = 0; i < maxLength; i += 1) {
if (parts1[i] > parts2[i]) return 1;
if (parts1[i] < parts2[i]) return -1;
}
return 0;
};
if (compareVersions(cleanFrom, cleanTo) >= 0) {
throw new Error("fromVersion must be older than toVersion");
}
const fromIdx = releases.findIndex((r) => normalizeVersion(r.normalizedTagName) === cleanFrom);
const toIdx = releases.findIndex((r) => normalizeVersion(r.normalizedTagName) === cleanTo);
if (fromIdx === -1) throw new Error(`Version ${fromVersion} not found`);
if (toIdx === -1) throw new Error(`Version ${toVersion} not found`);
if (fromIdx <= toIdx) throw new Error("Invalid version order");
let upgradePath: FormattedRelease[] = [];
const filteredPath: FormattedRelease[] = [];
const upgradePath = releases.slice(toIdx, fromIdx + 1).reverse();
const [first, last] = [upgradePath[0], upgradePath[upgradePath.length - 1]];
if (fromIdx !== -1 && toIdx !== -1) {
if (fromIdx <= toIdx) throw new Error("Invalid version order");
upgradePath = releases.slice(toIdx, fromIdx + 1).reverse();
const [first, last] = [upgradePath[0], upgradePath[upgradePath.length - 1]];
// Find all versions with breaking changes in the upgrade path
const withBreakingChanges = upgradePath.filter((version) => {
const breakingChanges = findBreakingChangesForVersion(version, config);
return breakingChanges.length > 0;
});
// Build the filtered path with breaking change versions
const filteredPath = [first];
// Get intermediate versions with breaking changes (excluding first and last)
const allIntermediateWithBreaking = withBreakingChanges
.filter((v) => v !== first && v !== last)
.sort((a, b) => new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime());
// Limit intermediate steps to avoid overly complex upgrade paths
const maxIntermediateSteps = 8;
const intermediate =
allIntermediateWithBreaking.length > maxIntermediateSteps
? allIntermediateWithBreaking.slice(-maxIntermediateSteps)
: allIntermediateWithBreaking;
filteredPath.push(...intermediate);
if (last !== first) filteredPath.push(last);
filteredPath.push(first);
if (last !== first) filteredPath.push(last);
}
const breakingChanges: Array<{ version: string; changes: BreakingChange[] }> = [];
const features: Array<{ version: string; name: string; body: string; publishedAt: string }> = [];
let hasDbMigration = false;
// Process versions in upgrade path
const isVersionInRange = (version: string, fromVer: string, toVer: string): boolean => {
const versionComp = compareVersions(version, fromVer);
const toVersionComp = compareVersions(version, toVer);
return versionComp > 0 && toVersionComp < 0;
};
Object.keys(config).forEach((configVersion) => {
const versionConfig = config[configVersion];
if (versionConfig?.breaking_changes?.length) {
if (isVersionInRange(configVersion, cleanFrom, cleanTo)) {
breakingChanges.push({
version: configVersion,
changes: versionConfig.breaking_changes
});
}
}
});
for (let i = 0; i < upgradePath.length; i += 1) {
const version = upgradePath[i];
const isFromVersion = normalizeVersion(version.normalizedTagName) === cleanFrom;
// 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 (excluding starting version)
if (!isFromVersion) {
const versionNumber = normalizeVersion(version.tagName);
const possibleKeys = [