Infisical Version Upgrade Tool

This commit is contained in:
Carlos Monastyrski
2025-09-15 12:15:59 -03:00
parent c24b222ff0
commit 8578265b0f
21 changed files with 1577 additions and 9 deletions
+10
View File
@@ -83,6 +83,7 @@
"ioredis": "^5.3.2",
"isomorphic-dompurify": "^2.22.0",
"jmespath": "^0.16.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0",
@@ -143,6 +144,7 @@
"@smithy/types": "^4.3.1",
"@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
@@ -13160,6 +13162,13 @@
"integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==",
"dev": true
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -20452,6 +20461,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
+2
View File
@@ -87,6 +87,7 @@
"@smithy/types": "^4.3.1",
"@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
@@ -203,6 +204,7 @@
"ioredis": "^5.3.2",
"isomorphic-dompurify": "^2.22.0",
"jmespath": "^0.16.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0",
+2
View File
@@ -114,6 +114,7 @@ import { TSlackServiceFactory } from "@app/services/slack/slack-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
import { TUpgradePathService } from "@app/services/upgrade-path/upgrade-path-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
@@ -312,6 +313,7 @@ declare module "fastify" {
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
notification: TNotificationServiceFactory;
offlineUsageReport: TOfflineUsageReportServiceFactory;
upgradePath: TUpgradePathService;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
+5 -1
View File
@@ -309,6 +309,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
import { totpServiceFactory } from "@app/services/totp/totp-service";
import { upgradePathServiceFactory } from "@app/services/upgrade-path/upgrade-path-service";
import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -759,6 +760,8 @@ export const registerRoutes = async (
userAliasDAL
});
const upgradePathService = upgradePathServiceFactory({ keyStore });
const totpService = totpServiceFactory({
totpConfigDAL,
userDAL,
@@ -2174,7 +2177,8 @@ export const registerRoutes = async (
reminder: reminderService,
bus: eventBusService,
sse: sseService,
notification: notificationService
notification: notificationService,
upgradePath: upgradePathService
});
const cronJobs: CronJob[] = [];
+2
View File
@@ -51,6 +51,7 @@ import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSlackRouter } from "./slack-router";
import { registerUpgradePathRouter } from "./upgrade-path-router";
import { registerSsoRouter } from "./sso-router";
import { registerUserActionRouter } from "./user-action-router";
import { registerUserEngagementRouter } from "./user-engagement-router";
@@ -188,4 +189,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
);
await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
};
@@ -0,0 +1,143 @@
import RE2 from "re2";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { publicEndpointLimit } from "@app/server/config/rateLimiter";
const versionSchema = z
.string()
.min(1)
.max(50)
.regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format");
const booleanSchema = z.boolean().default(false);
const queryBooleanSchema = z
.union([z.boolean(), z.string()])
.transform((val) => {
if (typeof val === "string") {
return val === "true" || val === "1";
}
return val;
})
.default(false);
export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/versions",
config: {
rateLimit: publicEndpointLimit
},
schema: {
querystring: z.object({
includePrerelease: queryBooleanSchema
}),
response: {
200: z.object({
versions: z.array(
z.object({
tagName: z.string(),
name: z.string(),
publishedAt: z.string(),
prerelease: z.boolean(),
draft: z.boolean()
})
)
})
}
},
handler: async (req) => {
try {
const { includePrerelease } = req.query;
const versions = await req.server.services.upgradePath.getGitHubReleases(includePrerelease);
return {
versions
};
} catch (error) {
req.log.error(error, "Failed to fetch versions");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: "Invalid query parameters" });
}
throw new BadRequestError({ message: "Failed to fetch GitHub releases" });
}
}
});
server.route({
method: "POST",
url: "/calculate",
config: {
rateLimit: publicEndpointLimit
},
schema: {
body: z.object({
fromVersion: versionSchema,
toVersion: versionSchema,
includePrerelease: booleanSchema
}),
response: {
200: z.object({
path: z.array(
z.object({
version: z.string(),
name: z.string(),
publishedAt: z.string(),
prerelease: z.boolean()
})
),
breakingChanges: z.array(
z.object({
version: z.string(),
changes: z.array(
z.object({
title: z.string(),
description: z.string(),
action: z.string()
})
)
})
),
features: z.array(
z.object({
version: z.string(),
name: z.string(),
body: z.string(),
publishedAt: z.string()
})
),
hasDbMigration: z.boolean(),
config: z.record(z.unknown())
})
}
},
handler: async (req) => {
try {
const { fromVersion, toVersion, includePrerelease } = req.body;
req.log.info({ fromVersion, toVersion, includePrerelease }, "Calculating upgrade path");
const result = await req.server.services.upgradePath.calculateUpgradePath(
fromVersion,
toVersion,
includePrerelease
);
req.log.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");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: `Invalid input: ${error.errors.map((e) => e.message).join(", ")}` });
}
if (error instanceof Error) {
throw new BadRequestError({ message: error.message });
}
throw new BadRequestError({ message: "Failed to calculate upgrade path" });
}
}
});
};
@@ -0,0 +1,187 @@
/* eslint-disable no-await-in-loop */
import RE2 from "re2";
import { FormattedRelease, GitHubApiError, GitHubRelease } from "./types";
interface GitHubClientConfig {
token?: string;
timeout: number;
maxRetries: number;
retryDelay: number;
maxPagesPerRequest: number;
perPage: number;
}
interface RateLimitInfo {
remaining: number;
reset: Date;
used: number;
limit: number;
}
const getDefaultConfig = (): GitHubClientConfig => ({
token: process.env.GITHUB_TOKEN,
timeout: 30000,
maxRetries: 3,
retryDelay: 1000,
maxPagesPerRequest: 10,
perPage: 100
});
const getHeaders = (token?: string): Record<string, string> => {
const headers: Record<string, string> = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Infisical-Upgrade-Path-Tool/1.0",
"X-GitHub-Api-Version": "2022-11-28"
};
if (token) {
headers.Authorization = `token ${token}`;
}
return headers;
};
const delay = (ms: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
const isMainInfisicalRelease = (tagName: string): boolean => {
if (
tagName.startsWith("infisical-cli/") ||
tagName.startsWith("infisical-k8-operator/") ||
tagName.startsWith("infisical-k8s-operator/")
) {
return false;
}
return tagName.startsWith("v") || tagName.startsWith("infisical/v") || new RE2(/^\d+\.\d+\.\d+/).test(tagName);
};
const normalizeVersion = (tagName: string): string => {
const versionMatch = tagName.match(new RE2(/(\d+\.\d+\.\d+(?:\.\d+)?)/));
if (versionMatch) {
return `v${versionMatch[1]}`;
}
if (tagName.startsWith("infisical/")) {
const withoutPrefix = tagName.replace(new RE2(/^infisical\//), "");
return withoutPrefix.replace(new RE2(/-[a-zA-Z]+$/), "");
}
return tagName.replace(new RE2(/-[a-zA-Z]+$/), "");
};
const makeRequest = async <T>(
url: string,
config: GitHubClientConfig,
retryCount = 0
): Promise<{ data: T; rateLimit: RateLimitInfo }> => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeout);
try {
const response = await fetch(url, {
headers: getHeaders(config.token),
signal: controller.signal
});
clearTimeout(timeout);
const rateLimit: RateLimitInfo = {
remaining: parseInt(response.headers.get("X-RateLimit-Remaining") || "0", 10),
reset: new Date(parseInt(response.headers.get("X-RateLimit-Reset") || "0", 10) * 1000),
used: parseInt(response.headers.get("X-RateLimit-Used") || "0", 10),
limit: parseInt(response.headers.get("X-RateLimit-Limit") || "5000", 10)
};
if (!response.ok) {
const error: GitHubApiError = new Error(`GitHub API error: ${response.status}`);
error.status = response.status;
error.headers = response.headers;
if (response.status === 403) {
const resetTime = rateLimit.reset.toISOString();
error.message = `GitHub API rate limit exceeded. Remaining: ${rateLimit.remaining}, Reset at: ${resetTime}. ${
!config.token ? "Consider setting GITHUB_TOKEN environment variable." : ""
}`;
}
if (retryCount < config.maxRetries && (response.status >= 500 || response.status === 403)) {
await delay(config.retryDelay * 2 ** retryCount);
return await makeRequest<T>(url, config, retryCount + 1);
}
throw error;
}
const data = (await response.json()) as T;
return { data, rateLimit };
} catch (error) {
clearTimeout(timeout);
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Request timeout after ${config.timeout}ms`);
}
if (retryCount < config.maxRetries && !(error as GitHubApiError).status) {
await delay(config.retryDelay * 2 ** retryCount);
return await makeRequest<T>(url, config, retryCount + 1);
}
throw error;
}
};
export const fetchReleases = async (includePrerelease = false): Promise<FormattedRelease[]> => {
const config = getDefaultConfig();
const allReleases: GitHubRelease[] = [];
let page = 1;
let hasMorePages = true;
const maxConcurrentRequests = Math.min(3, config.maxPagesPerRequest);
while (hasMorePages && page <= config.maxPagesPerRequest) {
const requests: Promise<{ data: GitHubRelease[]; rateLimit: RateLimitInfo }>[] = [];
for (let i = 0; i < maxConcurrentRequests && page <= config.maxPagesPerRequest; i += 1, page += 1) {
const url = `https://api.github.com/repos/Infisical/infisical/releases?page=${page}&per_page=${config.perPage}`;
requests.push(makeRequest<GitHubRelease[]>(url, config));
}
const results = await Promise.allSettled(requests);
let hasData = false;
for (const result of results) {
if (result.status === "fulfilled") {
const { data } = result.value;
if (data.length > 0) {
allReleases.push(...data);
hasData = true;
}
}
}
if (!hasData || results.every((r) => r.status === "fulfilled" && r.value.data.length < config.perPage)) {
hasMorePages = false;
}
}
const formattedReleases = allReleases
.filter((release) => !release.draft)
.filter((release) => isMainInfisicalRelease(release.tag_name))
.map(
(release): FormattedRelease => ({
tagName: release.tag_name,
normalizedTagName: normalizeVersion(release.tag_name),
name: release.name,
body: release.body,
publishedAt: release.published_at,
prerelease: release.prerelease,
draft: release.draft
})
)
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
return formattedReleases.filter((release) => includePrerelease || !release.prerelease);
};
@@ -0,0 +1,2 @@
export type { TUpgradePathService, TUpgradePathServiceFactory } from "./upgrade-path-service";
export { upgradePathServiceFactory } from "./upgrade-path-service";
@@ -0,0 +1,66 @@
export interface GitHubRelease {
tag_name: string;
name: string;
body: string;
published_at: string;
prerelease: boolean;
draft: boolean;
}
export interface FormattedRelease {
tagName: string;
normalizedTagName: string;
name: string;
body: string;
publishedAt: string;
prerelease: boolean;
draft: boolean;
}
export interface BreakingChange {
title: string;
description: string;
action: string;
}
export interface VersionConfig {
breaking_changes?: BreakingChange[];
db_schema_changes?: string;
notes?: string;
}
export interface UpgradePathConfig {
versions?: Record<string, VersionConfig>;
}
export interface UpgradePathResult {
path: Array<{
version: string;
name: string;
publishedAt: string;
prerelease: boolean;
}>;
breakingChanges: Array<{
version: string;
changes: BreakingChange[];
}>;
features: Array<{
version: string;
name: string;
body: string;
publishedAt: string;
}>;
hasDbMigration: boolean;
config: Record<string, unknown>;
}
export interface GitHubApiError extends Error {
status?: number;
headers?: Headers;
}
export interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
@@ -0,0 +1,270 @@
import { readFile } from "fs/promises";
import * as yaml from "js-yaml";
import * as path from "path";
import RE2 from "re2";
import { z } from "zod";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { fetchReleases } from "./github-client";
import { BreakingChange, FormattedRelease, UpgradePathConfig, UpgradePathResult, VersionConfig } from "./types";
export type TUpgradePathServiceFactory = {
keyStore: TKeyStoreFactory;
};
export type TUpgradePathService = ReturnType<typeof upgradePathServiceFactory>;
const versionSchema = z
.string()
.min(1)
.max(50)
.regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format");
const booleanSchema = z.boolean().default(false);
interface CalculateUpgradePathParams {
fromVersion: string;
toVersion: string;
includePrerelease?: boolean;
}
export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFactory) => {
const getGitHubReleases = async (includePrerelease = false): Promise<FormattedRelease[]> => {
const cacheKey = `upgrade-path:releases:${includePrerelease}`;
try {
const cached = await keyStore.getItem(cacheKey);
if (cached) return JSON.parse(cached) as FormattedRelease[];
} catch (error) {
// Cache miss, continue to fetch from source
}
try {
const releases = await fetchReleases(booleanSchema.parse(includePrerelease));
const filteredReleases = releases.filter((v) => !v.tagName.includes("nightly"));
await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(filteredReleases));
return filteredReleases;
} catch (error) {
throw new Error(`GitHub releases unavailable: ${error instanceof Error ? error.message : "Unknown error"}`);
}
};
const getUpgradePathConfig = async (): Promise<Record<string, VersionConfig>> => {
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
}
try {
const yamlPath = path.join(__dirname, "..", "..", "..", "upgrade-path.yaml");
const yamlContent = await readFile(yamlPath, "utf8");
if (yamlContent.length > 1024 * 1024) {
throw new Error("Config file too large");
}
const config = yaml.load(yamlContent) as UpgradePathConfig;
const versionConfig = config?.versions || {};
await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(versionConfig));
return versionConfig;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
const empty = {};
await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(empty));
return empty;
}
throw new Error(`Config load failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
};
const normalizeVersion = (version: string): string => {
// Extract just the X.X.X.X part from any version format
const versionMatch = version.match(/(\d+\.\d+\.\d+(?:\.\d+)?)/);
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]+$/), "");
}
return version.replace(new RE2(/^v/), "").replace(new RE2(/-[a-zA-Z]+$/), "");
};
const findBreakingChangesForVersion = (
version: FormattedRelease,
config: Record<string, VersionConfig>
): BreakingChange[] => {
// Check multiple key variations for breaking changes configuration
const versionNumber = normalizeVersion(version.tagName);
const possibleKeys = [
version.tagName,
version.normalizedTagName,
versionNumber,
`v${versionNumber}`,
version.tagName.replace(new RE2(/^infisical\//), ""),
version.tagName.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "")
];
for (const key of possibleKeys) {
const versionConfig = config[key];
if (versionConfig?.breaking_changes?.length) {
return versionConfig.breaking_changes;
}
}
return [];
};
const validateParams = (params: CalculateUpgradePathParams) => {
const { fromVersion, toVersion, includePrerelease = false } = params;
versionSchema.parse(fromVersion);
versionSchema.parse(toVersion);
if (fromVersion === toVersion) {
throw new Error("Versions cannot be identical");
}
if (fromVersion.includes("nightly") || toVersion.includes("nightly")) {
throw new Error("Nightly releases are not supported for upgrade path calculation");
}
return { fromVersion, toVersion, includePrerelease: booleanSchema.parse(includePrerelease) };
};
const calculateUpgradePath = async (params: CalculateUpgradePathParams): Promise<UpgradePathResult> => {
const { fromVersion, toVersion, includePrerelease } = validateParams(params);
const cacheKey = `upgrade-path:${fromVersion}:${toVersion}:${includePrerelease}`;
try {
const cached = await keyStore.getItem(cacheKey);
if (cached) return JSON.parse(cached) as UpgradePathResult;
} catch (error) {
// Cache miss, continue to fetch from source
}
const [releases, config] = await Promise.all([getGitHubReleases(includePrerelease), getUpgradePathConfig()]);
const cleanFrom = normalizeVersion(fromVersion);
const cleanTo = normalizeVersion(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");
const 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);
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, excluding starting version
for (let i = 1; 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 database migrations for intermediate versions only
if (!isFromVersion) {
const versionNumber = normalizeVersion(version.tagName);
const possibleKeys = [
version.tagName,
version.normalizedTagName,
versionNumber,
`v${versionNumber}`,
version.tagName.replace(new RE2(/^infisical\//), ""),
version.tagName.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "")
];
for (const key of possibleKeys) {
const versionConfig = config[key];
if (
versionConfig?.db_schema_changes &&
typeof versionConfig.db_schema_changes === "string" &&
versionConfig.db_schema_changes.trim()
) {
hasDbMigration = true;
break;
}
}
}
// Collect release notes and features
if (version.body) {
features.push({
version: version.tagName,
name: version.name,
body: version.body,
publishedAt: version.publishedAt
});
}
}
const result: UpgradePathResult = {
path: filteredPath.map((r) => ({
version: r.tagName,
name: r.name,
publishedAt: r.publishedAt,
prerelease: r.prerelease
})),
breakingChanges,
features,
hasDbMigration,
config
};
await keyStore.setItemWithExpiry(cacheKey, 60 * 60, JSON.stringify(result));
return result;
};
return {
getGitHubReleases,
getUpgradePathConfig,
calculateUpgradePath: (fromVersion: string, toVersion: string, includePrerelease = false) =>
calculateUpgradePath({ fromVersion, toVersion, includePrerelease })
};
};
+78
View File
@@ -0,0 +1,78 @@
versions:
"infisical/v0.130.0-postgres":
breaking_changes:
- title: "API Key Authentication Deprecation"
description: "Legacy API key authentication method has been deprecated and will be removed in future versions"
action: "Migrate all integrations to use Machine Identity authentication with JWT tokens. Update your CI/CD pipelines and automation scripts"
impact: "high"
- title: "Environment Variable Structure Changes"
description: "Environment variable naming convention has changed from INFISICAL_ prefix to INF_ for better compatibility"
action: "Update all environment variable references in your deployment configurations, Docker files, and Kubernetes manifests"
impact: "medium"
- title: "RBAC Permission Model Updates"
description: "Role-based access control has been restructured with new permission granularity affecting existing role assignments"
action: "Review and reassign user roles and permissions. Test access to sensitive resources before production deployment"
impact: "high"
db_schema_changes: "Extensive database schema restructuring for authentication and RBAC systems. Requires table reorganization and reindexing which may cause extended downtime."
notes: "Critical authentication and permission system overhaul. Database migration is extensive and may cause extended downtime. Plan maintenance window accordingly and ensure health checks are adjusted for longer migration time."
"infisical/v0.131.0-postgres":
breaking_changes:
- title: "Webhook Payload Format Changes"
description: "Webhook event payloads now use a new standardized format that is incompatible with previous versions"
action: "Update all webhook consumers to handle the new payload structure. Test webhook integrations with Slack, Discord, and custom endpoints"
impact: "high"
- title: "Secret Versioning API Breaking Changes"
description: "Secret versioning endpoints have changed from /api/v2/secrets to /api/v3/secrets with modified request/response schemas"
action: "Update all API clients and SDKs to use the new v3 endpoints. Modify any custom integrations or scripts"
impact: "medium"
- title: "CLI Authentication Method Changes"
description: "Infisical CLI now requires explicit authentication method specification and no longer supports legacy token formats"
action: "Update CLI installation in all environments. Re-authenticate CLI instances using 'infisical login' command"
impact: "medium"
db_schema_changes: "Major database schema changes for API restructuring. Includes reindexing large tables and webhook payload modifications which significantly impact performance during migration."
notes: "Major API restructure with extensive database changes. Migration involves reindexing large tables and may significantly impact instance performance. Health checks will likely fail during migration. Schedule during lowest traffic period."
"v0.147.0":
breaking_changes:
- title: "Docker Tag Format Changes"
description: "Docker tags no longer contain the -postgres suffix. This affects deployment configurations"
action: "Update all deployment scripts, Docker Compose files, and Kubernetes manifests to use new tag format without -postgres suffix"
impact: "high"
- title: "Release Channel System Introduction"
description: "Formal release channels introduced with breaking changes to update mechanisms"
action: "Review release channel documentation and update your deployment strategy to align with new release channels"
impact: "medium"
db_schema_changes: "Database schema updates for release channel system implementation. Adds new tables for channel tracking and version management."
notes: "Docker tag format change requires deployment configuration updates. Review release channel documentation."
"0.147.0":
breaking_changes:
- title: "Docker Tag Format Changes"
description: "Docker tags no longer contain the -postgres suffix. This affects deployment configurations"
action: "Update all deployment scripts, Docker Compose files, and Kubernetes manifests to use new tag format without -postgres suffix"
impact: "high"
- title: "Release Channel System Introduction"
description: "Formal release channels introduced with breaking changes to update mechanisms"
action: "Review release channel documentation and update your deployment strategy to align with new release channels"
impact: "medium"
db_schema_changes: "Database schema updates for release channel system implementation. Adds new tables for channel tracking and version management."
notes: "Docker tag format change requires deployment configuration updates. Review release channel documentation."
"v0.148.0":
breaking_changes:
- title: "Secret Overview Page Removal"
description: "Secret overview page has been removed and replaced with revamped secret dashboard"
action: "Update any bookmarks, documentation, or automation that references the old overview page URLs"
impact: "medium"
- title: "Universal Auth Login Lockout"
description: "New lockout mechanism for Universal Auth that may affect existing authentication flows"
action: "Review and test authentication flows. Update monitoring and alerting for lockout scenarios"
impact: "high"
- title: "SAML Duplicate Account Handling Changes"
description: "Changes to how duplicate SAML accounts are handled during first-time sign-in"
action: "Test SAML authentication flows and ensure proper account linking procedures are in place"
impact: "medium"
db_schema_changes: "Database schema changes for authentication system improvements and UI restructuring. Includes new lockout mechanism tables and SAML account handling modifications."
notes: "UI changes and authentication flow updates. Test all authentication methods thoroughly."