From 8578265b0f884dff74f9e04110dc389fc92952c2 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 15 Sep 2025 12:15:59 -0300 Subject: [PATCH 01/12] Infisical Version Upgrade Tool --- backend/package-lock.json | 10 + backend/package.json | 2 + backend/src/@types/fastify.d.ts | 2 + backend/src/server/routes/index.ts | 6 +- backend/src/server/routes/v1/index.ts | 2 + .../server/routes/v1/upgrade-path-router.ts | 143 ++++ .../services/upgrade-path/github-client.ts | 187 +++++ backend/src/services/upgrade-path/index.ts | 2 + backend/src/services/upgrade-path/types.ts | 66 ++ .../upgrade-path/upgrade-path-service.ts | 270 +++++++ backend/upgrade-path.yaml | 78 ++ .../self-hosting/helper/upgrade-path-tool.png | Bin 0 -> 210513 bytes .../guides/upgrading-infisical.mdx | 13 +- frontend/src/hooks/api/upgradePath/index.ts | 7 + .../src/hooks/api/upgradePath/queries.tsx | 83 +++ .../UpgradePathPage/UpgradePathPage.tsx | 671 ++++++++++++++++++ .../pages/public/UpgradePathPage/route.tsx | 7 + frontend/src/routeTree.gen.ts | 28 + frontend/src/routes.ts | 1 + package-lock.json | 7 +- package.json | 1 + 21 files changed, 1577 insertions(+), 9 deletions(-) create mode 100644 backend/src/server/routes/v1/upgrade-path-router.ts create mode 100644 backend/src/services/upgrade-path/github-client.ts create mode 100644 backend/src/services/upgrade-path/index.ts create mode 100644 backend/src/services/upgrade-path/types.ts create mode 100644 backend/src/services/upgrade-path/upgrade-path-service.ts create mode 100644 backend/upgrade-path.yaml create mode 100644 docs/images/self-hosting/helper/upgrade-path-tool.png create mode 100644 frontend/src/hooks/api/upgradePath/index.ts create mode 100644 frontend/src/hooks/api/upgradePath/queries.tsx create mode 100644 frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx create mode 100644 frontend/src/pages/public/UpgradePathPage/route.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index c6ac0b147..db4e9e7dd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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" }, diff --git a/backend/package.json b/backend/package.json index 0c8464eaf..8fece4ebe 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8ca4288b5..d075d21a8 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -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 diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eccad2956..6a4c1d995 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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[] = []; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 6108be32b..78ed50a76 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -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" }); }; diff --git a/backend/src/server/routes/v1/upgrade-path-router.ts b/backend/src/server/routes/v1/upgrade-path-router.ts new file mode 100644 index 000000000..30b5e9561 --- /dev/null +++ b/backend/src/server/routes/v1/upgrade-path-router.ts @@ -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" }); + } + } + }); +}; diff --git a/backend/src/services/upgrade-path/github-client.ts b/backend/src/services/upgrade-path/github-client.ts new file mode 100644 index 000000000..2982e297a --- /dev/null +++ b/backend/src/services/upgrade-path/github-client.ts @@ -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 => { + const headers: Record = { + 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 => { + 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 ( + 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(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(url, config, retryCount + 1); + } + + throw error; + } +}; + +export const fetchReleases = async (includePrerelease = false): Promise => { + 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(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); +}; diff --git a/backend/src/services/upgrade-path/index.ts b/backend/src/services/upgrade-path/index.ts new file mode 100644 index 000000000..1000e2bfa --- /dev/null +++ b/backend/src/services/upgrade-path/index.ts @@ -0,0 +1,2 @@ +export type { TUpgradePathService, TUpgradePathServiceFactory } from "./upgrade-path-service"; +export { upgradePathServiceFactory } from "./upgrade-path-service"; diff --git a/backend/src/services/upgrade-path/types.ts b/backend/src/services/upgrade-path/types.ts new file mode 100644 index 000000000..83d3d1546 --- /dev/null +++ b/backend/src/services/upgrade-path/types.ts @@ -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; +} + +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; +} + +export interface GitHubApiError extends Error { + status?: number; + headers?: Headers; +} + +export interface CacheEntry { + data: T; + timestamp: number; + ttl: number; +} diff --git a/backend/src/services/upgrade-path/upgrade-path-service.ts b/backend/src/services/upgrade-path/upgrade-path-service.ts new file mode 100644 index 000000000..05bd44e25 --- /dev/null +++ b/backend/src/services/upgrade-path/upgrade-path-service.ts @@ -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; + +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 => { + 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> => { + const cacheKey = "upgrade-path:config"; + + try { + const cached = await keyStore.getItem(cacheKey); + if (cached) return JSON.parse(cached) as Record; + } 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 + ): 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 => { + 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 }) + }; +}; diff --git a/backend/upgrade-path.yaml b/backend/upgrade-path.yaml new file mode 100644 index 000000000..87aa5a53f --- /dev/null +++ b/backend/upgrade-path.yaml @@ -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." diff --git a/docs/images/self-hosting/helper/upgrade-path-tool.png b/docs/images/self-hosting/helper/upgrade-path-tool.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a538aaf40e35895efbb7b4531455ef57c3f8ba GIT binary patch literal 210513 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

WKd@ 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%0DH!>)CaFAx-D$)z2R)X6wy{R*a^NXXxwwa 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

  • t=R3r|$E;cEtvRHn@$TMYnaxQ2 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 SF+daCB^3ot`O>H6Z~q@Bx=)Y* literal 0 HcmV?d00001 diff --git a/docs/self-hosting/guides/upgrading-infisical.mdx b/docs/self-hosting/guides/upgrading-infisical.mdx index 60c6edbff..f91b904b5 100644 --- a/docs/self-hosting/guides/upgrading-infisical.mdx +++ b/docs/self-hosting/guides/upgrading-infisical.mdx @@ -41,9 +41,16 @@ Now, migrations run automatically during boot-up. This improvement streamlines t - Ensure you have a complete backup of your Postgres database. - Verify that your backup is current and accessible. -2. **Select the Upgrade Version:** - - Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a list of available versions. - - Look for releases with the prefix `infisical/` as there are other releases that are not related to the Infisical instance. +2. **Plan Your Upgrade Path:** + - Use our [Upgrade Path Tool](https://app.infisical.com/upgrade-path) to analyze your upgrade path between your current version and target version. + - The tool will show you: + - **Breaking changes** that require action before upgrading + - **Database migrations** that may require additional settings or precautions + - **Step-by-step upgrade path** with intermediate versions if needed + - Review any breaking changes and plan necessary configuration updates before proceeding. + - Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a complete list of available versions. + +![Upgrade Path Tool showing breaking changes and migration information](/images/self-hosting/helper/upgrade-path-tool.png) 3. **Start the Upgrade Process:** - Launch the new version of Infisical. During startup, the application will automatically compare the current database schema with the updated schema in the code. diff --git a/frontend/src/hooks/api/upgradePath/index.ts b/frontend/src/hooks/api/upgradePath/index.ts new file mode 100644 index 000000000..73c6d1254 --- /dev/null +++ b/frontend/src/hooks/api/upgradePath/index.ts @@ -0,0 +1,7 @@ +export type { + CalculateUpgradePathParams, + GetUpgradePathVersionsParams, + GitHubVersion, + UpgradePathResult +} from "./queries"; +export { useCalculateUpgradePath, useGetUpgradePathVersions } from "./queries"; diff --git a/frontend/src/hooks/api/upgradePath/queries.tsx b/frontend/src/hooks/api/upgradePath/queries.tsx new file mode 100644 index 000000000..aa4475c74 --- /dev/null +++ b/frontend/src/hooks/api/upgradePath/queries.tsx @@ -0,0 +1,83 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +export interface GitHubVersion { + tagName: string; + name: string; + publishedAt: string; + prerelease: boolean; + draft: boolean; +} + +export interface UpgradePathResult { + path: Array<{ + version: string; + name: string; + publishedAt: string; + prerelease: boolean; + }>; + breakingChanges: Array<{ + version: string; + changes: Array<{ + title: string; + description: string; + action: string; + }>; + }>; + features: Array<{ + version: string; + name: string; + body: string; + publishedAt: string; + }>; + hasDbMigration: boolean; + config: Record; +} + +export interface GetUpgradePathVersionsParams { + includePrerelease?: boolean; +} + +export interface CalculateUpgradePathParams { + fromVersion: string; + toVersion: string; + includePrerelease?: boolean; +} + +const upgradePathKeys = { + all: ["upgrade-path"] as const, + versions: (params: GetUpgradePathVersionsParams) => + [...upgradePathKeys.all, "versions", params] as const, + calculate: (params: CalculateUpgradePathParams) => + [...upgradePathKeys.all, "calculate", params] as const +}; + +export const useGetUpgradePathVersions = ( + params: GetUpgradePathVersionsParams, + options?: Omit, "queryKey" | "queryFn"> +) => { + return useQuery({ + queryKey: upgradePathKeys.versions(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ versions: GitHubVersion[] }>( + "/api/v1/upgrade-path/versions", + { + params + } + ); + return data; + }, + ...options + }); +}; + +export const useCalculateUpgradePath = () => { + return async (params: CalculateUpgradePathParams): Promise => { + const { data } = await apiRequest.post( + "/api/v1/upgrade-path/calculate", + params + ); + return data; + }; +}; diff --git a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx new file mode 100644 index 000000000..a8a8551f2 --- /dev/null +++ b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx @@ -0,0 +1,671 @@ +/* eslint-disable no-nested-ternary */ +import { useEffect, useMemo, useRef, useState } from "react"; +import { Helmet } from "react-helmet"; +import { faExternalLink, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useMutation } from "@tanstack/react-query"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; + +import { + useCalculateUpgradePath, + useGetUpgradePathVersions +} from "../../../hooks/api/upgradePath/queries"; + +interface UpgradeResult { + path: Array<{ + version: string; + name: string; + publishedAt: string; + prerelease: boolean; + }>; + breakingChanges: Array<{ + version: string; + changes: Array<{ + title: string; + description: string; + action: string; + }>; + }>; + features: Array<{ + version: string; + name: string; + body: string; + publishedAt: string; + }>; + hasDbMigration: boolean; + config: Record; +} + +export const UpgradePathPage = () => { + const [fromVersion, setFromVersion] = useState(""); + const [toVersion, setToVersion] = useState(""); + const [fromSearch, setFromSearch] = useState(""); + const [toSearch, setToSearch] = useState(""); + const [showFromDropdown, setShowFromDropdown] = useState(false); + const [showToDropdown, setShowToDropdown] = useState(false); + const [upgradeResult, setUpgradeResult] = useState(null); + const [debouncedFromSearch, setDebouncedFromSearch] = useState(""); + const [debouncedToSearch, setDebouncedToSearch] = useState(""); + + const fromDropdownRef = useRef(null); + const toDropdownRef = useRef(null); + const fromSearchTimeoutRef = useRef(); + const toSearchTimeoutRef = useRef(); + + useEffect(() => { + if (fromSearchTimeoutRef.current) { + clearTimeout(fromSearchTimeoutRef.current); + } + fromSearchTimeoutRef.current = setTimeout(() => { + setDebouncedFromSearch(fromSearch); + }, 300); + + return () => { + if (fromSearchTimeoutRef.current) { + clearTimeout(fromSearchTimeoutRef.current); + } + }; + }, [fromSearch]); + + useEffect(() => { + if (toSearchTimeoutRef.current) { + clearTimeout(toSearchTimeoutRef.current); + } + toSearchTimeoutRef.current = setTimeout(() => { + setDebouncedToSearch(toSearch); + }, 300); + + return () => { + if (toSearchTimeoutRef.current) { + clearTimeout(toSearchTimeoutRef.current); + } + }; + }, [toSearch]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) { + setShowFromDropdown(false); + } + if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) { + setShowToDropdown(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const { + data: versions, + isLoading: versionsLoading, + isFetching: versionsFetching + } = useGetUpgradePathVersions( + { + includePrerelease: false + }, + { + enabled: true, + staleTime: 24 * 60 * 60 * 1000, + refetchOnWindowFocus: false + } + ); + + const calculateMutation = useMutation({ + mutationFn: useCalculateUpgradePath(), + onSuccess: (data) => { + setUpgradeResult(data); + }, + onError: (error: unknown) => { + createNotification({ + text: (error as any)?.response?.data?.message || "Failed to calculate upgrade path", + type: "error" + }); + } + }); + + const filteredFromVersions = useMemo(() => { + if (!versions?.versions) return []; + + const filtered = versions.versions + .filter((version) => !version.tagName.includes("nightly")) + .filter((version) => { + if (!debouncedFromSearch) return true; + const searchTerm = debouncedFromSearch.toLowerCase(); + return version.tagName.toLowerCase().includes(searchTerm); + }); + + return filtered.slice(0, 25); + }, [versions?.versions, debouncedFromSearch]); + + const filteredToVersions = useMemo(() => { + if (!versions?.versions) return []; + + const filtered = versions.versions + .filter((version) => !version.tagName.includes("nightly")) + .filter((version) => { + if (!debouncedToSearch) return true; + const searchTerm = debouncedToSearch.toLowerCase(); + return version.tagName.toLowerCase().includes(searchTerm); + }); + + return filtered.slice(0, 25); + }, [versions?.versions, debouncedToSearch]); + + const handleFromVersionSelect = (version: string) => { + setFromVersion(version); + setFromSearch(version); + setShowFromDropdown(false); + }; + + const handleToVersionSelect = (version: string) => { + setToVersion(version); + setToSearch(version); + setShowToDropdown(false); + }; + + const handleCalculate = () => { + if (!fromVersion || !toVersion) { + createNotification({ + text: "Please select both from and to versions", + type: "error" + }); + return; + } + + if (fromVersion === toVersion) { + createNotification({ + text: "From and To versions cannot be the same", + type: "error" + }); + return; + } + + calculateMutation.mutate({ + fromVersion, + toVersion, + includePrerelease: false + }); + }; + + return ( + <> + + Infisical Upgrade Path Tool | Infisical + + + + + +
    +
    +
    +
    + {/* Header */} +
    +
    + + Infisical logo + +
    +

    + Upgrade your Infisical Version +

    +
    + + {/* Calculator Card */} +
    +

    Calculate Upgrade Path

    +
    +
    + {/* From Version Selector */} +
    + +
    + { + setFromSearch(e.target.value); + setFromVersion(""); + setShowFromDropdown(true); + }} + onFocus={() => setShowFromDropdown(true)} + placeholder="Search or select version..." + className="border-mineshaft-600 bg-mineshaft-900" + leftIcon={} + isDisabled={versionsLoading || versionsFetching} + /> + {showFromDropdown && ( +
    + {(() => { + if (versionsLoading || versionsFetching) { + return ( +
    +
    + + Loading versions... + +
    + ); + } + if (filteredFromVersions.length > 0) { + return filteredFromVersions.slice(0, 8).map((version) => ( + + )); + } + return ( +
    + {debouncedFromSearch + ? `No versions found matching "${debouncedFromSearch}"` + : "No versions available"} +
    + ); + })()} +
    + )} +
    + +
    + + {/* To Version Selector */} +
    + +
    + { + setToSearch(e.target.value); + setToVersion(""); + setShowToDropdown(true); + }} + onFocus={() => setShowToDropdown(true)} + placeholder="Search or select version..." + className="border-mineshaft-600 bg-mineshaft-900" + leftIcon={} + isDisabled={versionsLoading || versionsFetching} + /> + {showToDropdown && ( +
    + {(() => { + if (versionsLoading || versionsFetching) { + return ( +
    +
    + + Loading versions... + +
    + ); + } + if (filteredToVersions.length > 0) { + return filteredToVersions.slice(0, 8).map((version) => ( + + )); + } + return ( +
    + {debouncedToSearch + ? `No versions found matching "${debouncedToSearch}"` + : "No versions available"} +
    + ); + })()} +
    + )} +
    + +
    +
    + + +
    +
    + + {/* Results Section */} + {upgradeResult && ( +
    + {/* Action Required Banner */} + {(() => { + const versionsWithBreakingChanges = upgradeResult.breakingChanges + .filter((bc) => bc.changes.length > 0) + .map((bc) => bc.version); + + const versionsWithDbMigrations = upgradeResult.path + .filter((step, index) => { + const isStartingVersion = index === 0; + if (isStartingVersion) return false; + + const versionConfig = upgradeResult.config as Record; + + const possibleKeys = [ + step.version, + step.version.replace(/^v/, ""), + step.version.replace(/^infisical\/v?/, ""), + step.version.replace(/^infisical\/v?/, "").replace(/-[a-zA-Z]+$/, "") + ]; + + const dbSchemaChanges = possibleKeys + .map((key) => versionConfig?.[key]?.db_schema_changes) + .find((changes) => changes); + + return ( + dbSchemaChanges && + (typeof dbSchemaChanges === "string" + ? dbSchemaChanges.trim() + : dbSchemaChanges) + ); + }) + .map((step) => step.version); + + const allConflictVersions = [ + ...new Set([...versionsWithBreakingChanges, ...versionsWithDbMigrations]) + ]; + const hasIssues = allConflictVersions.length > 0; + + return ( +
    +
    +
    + {hasIssues ? ( + + + + ) : ( + + + + )} +
    +
    +

    + {hasIssues ? "Action Required:" : "Ready to Upgrade:"} +

    +

    + {hasIssues + ? `Your upgrade path contains conflicts in the following versions: ${allConflictVersions.join(", ")}. Please review and resolve each item before proceeding to the next version.` + : "Your upgrade path is clear with no breaking changes or conflicts. You can proceed with the upgrade."} +

    +
    +
    +
    + ); + })()} + + {/* Upgrade Steps */} +
    +
    + + + +

    Upgrade Steps

    +
    + +
    + {upgradeResult.path.map((step, index) => { + const isFirst = index === 0; + const isLast = index === upgradeResult.path.length - 1; + + const versionChanges = upgradeResult.breakingChanges.find((bc) => { + if (bc.version === step.version) return true; + + const normalizeVersion = (v: string) => { + return v.replace(/^(infisical\/)?v?/, "").replace(/-[a-zA-Z]+$/, ""); + }; + + const normalizedStep = normalizeVersion(step.version); + const normalizedBC = normalizeVersion(bc.version); + + return normalizedStep === normalizedBC; + }); + + const versionConfig = upgradeResult.config as Record; + + const possibleKeys = [ + step.version, + step.version.replace(/^v/, ""), + step.version.replace(/^infisical\/v?/, ""), + step.version.replace(/^infisical\/v?/, "").replace(/-[a-zA-Z]+$/, "") + ]; + + const dbMigrationDescription = possibleKeys + .map((key) => versionConfig?.[key]?.db_schema_changes) + .find((changes) => changes); + + const hasDbMigration = + !isFirst && + dbMigrationDescription && + (typeof dbMigrationDescription === "string" + ? dbMigrationDescription.trim() + : dbMigrationDescription); + + const hasBreakingChanges = + versionChanges && versionChanges.changes.length > 0; + + return ( +
    + {/* Timeline Column */} +
    + {/* Timeline Circle */} +
    + {isFirst || isLast ? ( +
    + ) : hasBreakingChanges || hasDbMigration ? ( + + + + ) : ( +
    + )} +
    + + {/* Timeline Line */} + {!isLast &&
    } +
    + + {/* Content Column */} +
    + {/* Version Header */} +
    +

    {step.version}

    + {isFirst && ( + + Starting Version + + )} + {isLast && ( + + Target Version + + )} + + + View Changelog + +
    + + {/* Version Notes */} + {(() => { + if (isFirst) return null; + + const notes = possibleKeys + .map((key) => versionConfig?.[key]?.notes) + .find((note) => note); + + if (!notes) return null; + + return ( +
    +
    {notes}
    +
    + ); + })()} + + {/* Database Schema Changes */} + {hasDbMigration && ( +
    +
    + Database Schema Changes Required +
    +
    + {typeof dbMigrationDescription === "string" + ? dbMigrationDescription + : "This version includes database schema changes that require migrations."} +
    +
    + Action:{" "} + + Make sure to backup your database before proceeding + +
    +
    + )} + + {/* Breaking Changes */} + {hasBreakingChanges && ( +
    +
    + + + + + Breaking Changes ({versionChanges.changes.length}) + +
    + {versionChanges.changes.map((change) => ( +
    +
    + {change.title} +
    +
    + {change.description} +
    +
    + Action:{" "} + {change.action} +
    +
    + ))} +
    + )} +
    +
    + ); + })} +
    +
    +
    + )} +
    +
    +

    + Made with ❤️ by{" "} + + Infisical + +
    + 235 2nd st, San Francisco, California, 94105, United States. 🇺🇸 +

    +
    +
    +
    + + ); +}; diff --git a/frontend/src/pages/public/UpgradePathPage/route.tsx b/frontend/src/pages/public/UpgradePathPage/route.tsx new file mode 100644 index 000000000..cc91b74e4 --- /dev/null +++ b/frontend/src/pages/public/UpgradePathPage/route.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { UpgradePathPage } from "./UpgradePathPage"; + +export const Route = createFileRoute("/upgrade-path")({ + component: UpgradePathPage +}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 1f62ba7b9..d4ecc024d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './pages/root' import { Route as middlewaresRestrictLoginSignupImport } from './pages/middlewares/restrict-login-signup' import { Route as middlewaresAuthenticateImport } from './pages/middlewares/authenticate' +import { Route as publicUpgradePathPageRouteImport } from './pages/public/UpgradePathPage/route' import { Route as publicShareSecretPageRouteImport } from './pages/public/ShareSecretPage/route' import { Route as authCliRedirectPageRouteImport } from './pages/auth/CliRedirectPage/route' import { Route as indexImport } from './pages/index' @@ -318,6 +319,14 @@ const middlewaresAuthenticateRoute = middlewaresAuthenticateImport.update({ getParentRoute: () => rootRoute, } as any) +const publicUpgradePathPageRouteRoute = publicUpgradePathPageRouteImport.update( + { + id: '/upgrade-path', + path: '/upgrade-path', + getParentRoute: () => rootRoute, + } as any, +) + const publicShareSecretPageRouteRoute = publicShareSecretPageRouteImport.update( { id: '/share-secret', @@ -2037,6 +2046,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof publicShareSecretPageRouteImport parentRoute: typeof rootRoute } + '/upgrade-path': { + id: '/upgrade-path' + path: '/upgrade-path' + fullPath: '/upgrade-path' + preLoaderRoute: typeof publicUpgradePathPageRouteImport + parentRoute: typeof rootRoute + } '/_authenticate': { id: '/_authenticate' path: '' @@ -4600,6 +4616,7 @@ export interface FileRoutesByFullPath { '/': typeof indexRoute '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute + '/upgrade-path': typeof publicUpgradePathPageRouteRoute '': typeof organizationLayoutRouteWithChildren '/password-setup': typeof authPasswordSetupPageRouteRoute '/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute @@ -4821,6 +4838,7 @@ export interface FileRoutesByTo { '/': typeof indexRoute '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute + '/upgrade-path': typeof publicUpgradePathPageRouteRoute '': typeof organizationLayoutRouteWithChildren '/password-setup': typeof authPasswordSetupPageRouteRoute '/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute @@ -5029,6 +5047,7 @@ export interface FileRoutesById { '/': typeof indexRoute '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute + '/upgrade-path': typeof publicUpgradePathPageRouteRoute '/_authenticate': typeof middlewaresAuthenticateRouteWithChildren '/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren '/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute @@ -5262,6 +5281,7 @@ export interface FileRouteTypes { | '/' | '/cli-redirect' | '/share-secret' + | '/upgrade-path' | '' | '/password-setup' | '/email-not-verified' @@ -5482,6 +5502,7 @@ export interface FileRouteTypes { | '/' | '/cli-redirect' | '/share-secret' + | '/upgrade-path' | '' | '/password-setup' | '/email-not-verified' @@ -5688,6 +5709,7 @@ export interface FileRouteTypes { | '/' | '/cli-redirect' | '/share-secret' + | '/upgrade-path' | '/_authenticate' | '/_restrict-login-signup' | '/_authenticate/password-setup' @@ -5920,6 +5942,7 @@ export interface RootRouteChildren { indexRoute: typeof indexRoute authCliRedirectPageRouteRoute: typeof authCliRedirectPageRouteRoute publicShareSecretPageRouteRoute: typeof publicShareSecretPageRouteRoute + publicUpgradePathPageRouteRoute: typeof publicUpgradePathPageRouteRoute middlewaresAuthenticateRoute: typeof middlewaresAuthenticateRouteWithChildren middlewaresRestrictLoginSignupRoute: typeof middlewaresRestrictLoginSignupRouteWithChildren publicViewSecretRequestByIDPageRouteRoute: typeof publicViewSecretRequestByIDPageRouteRoute @@ -5930,6 +5953,7 @@ const rootRouteChildren: RootRouteChildren = { indexRoute: indexRoute, authCliRedirectPageRouteRoute: authCliRedirectPageRouteRoute, publicShareSecretPageRouteRoute: publicShareSecretPageRouteRoute, + publicUpgradePathPageRouteRoute: publicUpgradePathPageRouteRoute, middlewaresAuthenticateRoute: middlewaresAuthenticateRouteWithChildren, middlewaresRestrictLoginSignupRoute: middlewaresRestrictLoginSignupRouteWithChildren, @@ -5952,6 +5976,7 @@ export const routeTree = rootRoute "/", "/cli-redirect", "/share-secret", + "/upgrade-path", "/_authenticate", "/_restrict-login-signup", "/secret-request/secret/$secretRequestId", @@ -5967,6 +5992,9 @@ export const routeTree = rootRoute "/share-secret": { "filePath": "public/ShareSecretPage/route.tsx" }, + "/upgrade-path": { + "filePath": "public/UpgradePathPage/route.tsx" + }, "/_authenticate": { "filePath": "middlewares/authenticate.tsx", "children": [ diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 70c34edc6..1db098b71 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -373,6 +373,7 @@ export const routes = rootRoute("root.tsx", [ route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"), route("/secret-request/secret/$secretRequestId", "public/ViewSecretRequestByIDPage/route.tsx"), route("/share-secret", "public/ShareSecretPage/route.tsx"), + route("/upgrade-path", "public/UpgradePathPage/route.tsx"), route("/cli-redirect", "auth/CliRedirectPage/route.tsx"), middleware("restrict-login-signup.tsx", [ route("/admin/signup", "admin/SignUpPage/route.tsx"), diff --git a/package-lock.json b/package-lock.json index 4d72220af..6e37ffeab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "license": "ISC", "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", + "js-yaml": "^4.1.0", "secrets.js-grempe": "^2.0.0" }, "devDependencies": { @@ -561,7 +562,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { @@ -1104,7 +1104,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1891,8 +1890,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "balanced-match": { "version": "1.0.2", @@ -2284,7 +2282,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } diff --git a/package.json b/package.json index db15de3fb..2efd0def8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", + "js-yaml": "^4.1.0", "secrets.js-grempe": "^2.0.0" } } From ecb9466617b755c5d1fe22021b5d1707c5c77d6a Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 15 Sep 2025 14:26:14 -0300 Subject: [PATCH 02/12] Fix upgrade-path.yaml, removed test data --- backend/upgrade-path.yaml | 102 ++++++++++---------------------------- 1 file changed, 25 insertions(+), 77 deletions(-) diff --git a/backend/upgrade-path.yaml b/backend/upgrade-path.yaml index 87aa5a53f..4ccb7e775 100644 --- a/backend/upgrade-path.yaml +++ b/backend/upgrade-path.yaml @@ -1,78 +1,26 @@ -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." +# Upgrade Path Configuration File +# +# This file defines breaking changes and database migration information for Infisical versions. +# Used by the upgrade path tool to help users understand what changes are required between versions. +# +# Expected format: +# versions: +# "version_key": # Can be "v1.2.3", "1.2.3", or "infisical/v1.2.3-postgres" +# breaking_changes: # Optional: list of breaking changes for this version +# - title: "Short descriptive title" +# description: "Detailed description of what changed" +# action: "Specific steps users need to take" +# db_schema_changes: "Optional: Description of database changes and migration details" +# notes: "Optional: Additional notes or important information about this version" +# +# Example: +# versions: +# "v1.2.3": +# breaking_changes: +# - title: "API Endpoint Changes" +# description: "Authentication endpoints have been restructured" +# action: "Update all API calls to use new /auth/v2/ endpoints" +# db_schema_changes: "Major schema restructuring with table reorganization. Extended migration time: 3 minutes." +# notes: "Critical update requiring maintenance window. Test thoroughly before production deployment." - - "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." +versions: \ No newline at end of file From 5fc88610e4541b5611877413e87fd98c248e88ad Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 15 Sep 2025 14:42:14 -0300 Subject: [PATCH 03/12] Improve UI dropdown --- .../UpgradePathPage/UpgradePathPage.tsx | 267 ++++-------------- 1 file changed, 60 insertions(+), 207 deletions(-) diff --git a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx index a8a8551f2..74317d02f 100644 --- a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx +++ b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx @@ -1,18 +1,31 @@ /* eslint-disable no-nested-ternary */ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useState } from "react"; import { Helmet } from "react-helmet"; -import { faExternalLink, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { SingleValue } from "react-select"; +import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMutation } from "@tanstack/react-query"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl } from "@app/components/v2"; import { useCalculateUpgradePath, useGetUpgradePathVersions } from "../../../hooks/api/upgradePath/queries"; +type VersionOption = { + label: string; + value: string; + isLatest: boolean; +}; + +const formatVersionOption = (option: VersionOption) => ( +
    + {option.label} + {option.isLatest && (Latest)} +
    +); interface UpgradeResult { path: Array<{ version: string; @@ -39,66 +52,9 @@ interface UpgradeResult { } export const UpgradePathPage = () => { - const [fromVersion, setFromVersion] = useState(""); - const [toVersion, setToVersion] = useState(""); - const [fromSearch, setFromSearch] = useState(""); - const [toSearch, setToSearch] = useState(""); - const [showFromDropdown, setShowFromDropdown] = useState(false); - const [showToDropdown, setShowToDropdown] = useState(false); + const [fromVersion, setFromVersion] = useState(null); + const [toVersion, setToVersion] = useState(null); const [upgradeResult, setUpgradeResult] = useState(null); - const [debouncedFromSearch, setDebouncedFromSearch] = useState(""); - const [debouncedToSearch, setDebouncedToSearch] = useState(""); - - const fromDropdownRef = useRef(null); - const toDropdownRef = useRef(null); - const fromSearchTimeoutRef = useRef(); - const toSearchTimeoutRef = useRef(); - - useEffect(() => { - if (fromSearchTimeoutRef.current) { - clearTimeout(fromSearchTimeoutRef.current); - } - fromSearchTimeoutRef.current = setTimeout(() => { - setDebouncedFromSearch(fromSearch); - }, 300); - - return () => { - if (fromSearchTimeoutRef.current) { - clearTimeout(fromSearchTimeoutRef.current); - } - }; - }, [fromSearch]); - - useEffect(() => { - if (toSearchTimeoutRef.current) { - clearTimeout(toSearchTimeoutRef.current); - } - toSearchTimeoutRef.current = setTimeout(() => { - setDebouncedToSearch(toSearch); - }, 300); - - return () => { - if (toSearchTimeoutRef.current) { - clearTimeout(toSearchTimeoutRef.current); - } - }; - }, [toSearch]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) { - setShowFromDropdown(false); - } - if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) { - setShowToDropdown(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); const { data: versions, @@ -128,44 +84,26 @@ export const UpgradePathPage = () => { } }); - const filteredFromVersions = useMemo(() => { + const versionOptions = useMemo(() => { if (!versions?.versions) return []; - const filtered = versions.versions + return versions.versions .filter((version) => !version.tagName.includes("nightly")) - .filter((version) => { - if (!debouncedFromSearch) return true; - const searchTerm = debouncedFromSearch.toLowerCase(); - return version.tagName.toLowerCase().includes(searchTerm); - }); + .map((version) => ({ + label: version.tagName, + value: version.tagName, + isLatest: versions.versions[0]?.tagName === version.tagName + })); + }, [versions?.versions]); - return filtered.slice(0, 25); - }, [versions?.versions, debouncedFromSearch]); - - const filteredToVersions = useMemo(() => { - if (!versions?.versions) return []; - - const filtered = versions.versions - .filter((version) => !version.tagName.includes("nightly")) - .filter((version) => { - if (!debouncedToSearch) return true; - const searchTerm = debouncedToSearch.toLowerCase(); - return version.tagName.toLowerCase().includes(searchTerm); - }); - - return filtered.slice(0, 25); - }, [versions?.versions, debouncedToSearch]); - - const handleFromVersionSelect = (version: string) => { - setFromVersion(version); - setFromSearch(version); - setShowFromDropdown(false); + const handleFromVersionSelect = (value: unknown) => { + const selected = value as SingleValue; + setFromVersion(selected?.value || null); }; - const handleToVersionSelect = (version: string) => { - setToVersion(version); - setToSearch(version); - setShowToDropdown(false); + const handleToVersionSelect = (value: unknown) => { + const selected = value as SingleValue; + setToVersion(selected?.value || null); }; const handleCalculate = () => { @@ -232,121 +170,36 @@ export const UpgradePathPage = () => {
    {/* From Version Selector */} -
    - -
    - { - setFromSearch(e.target.value); - setFromVersion(""); - setShowFromDropdown(true); - }} - onFocus={() => setShowFromDropdown(true)} - placeholder="Search or select version..." - className="border-mineshaft-600 bg-mineshaft-900" - leftIcon={} - isDisabled={versionsLoading || versionsFetching} - /> - {showFromDropdown && ( -
    - {(() => { - if (versionsLoading || versionsFetching) { - return ( -
    -
    - - Loading versions... - -
    - ); - } - if (filteredFromVersions.length > 0) { - return filteredFromVersions.slice(0, 8).map((version) => ( - - )); - } - return ( -
    - {debouncedFromSearch - ? `No versions found matching "${debouncedFromSearch}"` - : "No versions available"} -
    - ); - })()} -
    - )} -
    - -
    + + opt.value === fromVersion) || null} + onChange={handleFromVersionSelect} + placeholder="Search or select version..." + isLoading={versionsLoading || versionsFetching} + isDisabled={versionsLoading || versionsFetching} + isSearchable + isClearable + menuPortalTarget={document.body} + formatOptionLabel={formatVersionOption} + /> + {/* To Version Selector */} -
    - -
    - { - setToSearch(e.target.value); - setToVersion(""); - setShowToDropdown(true); - }} - onFocus={() => setShowToDropdown(true)} - placeholder="Search or select version..." - className="border-mineshaft-600 bg-mineshaft-900" - leftIcon={} - isDisabled={versionsLoading || versionsFetching} - /> - {showToDropdown && ( -
    - {(() => { - if (versionsLoading || versionsFetching) { - return ( -
    -
    - - Loading versions... - -
    - ); - } - if (filteredToVersions.length > 0) { - return filteredToVersions.slice(0, 8).map((version) => ( - - )); - } - return ( -
    - {debouncedToSearch - ? `No versions found matching "${debouncedToSearch}"` - : "No versions available"} -
    - ); - })()} -
    - )} -
    - -
    + + opt.value === toVersion) || null} + onChange={handleToVersionSelect} + placeholder="Search or select version..." + isLoading={versionsLoading || versionsFetching} + isDisabled={versionsLoading || versionsFetching} + isSearchable + isClearable + menuPortalTarget={document.body} + formatOptionLabel={formatVersionOption} + /> +
    - {upgradeResult.path.map((step, index) => { + {(() => { + const pathSteps = upgradeResult.path.map((step) => ({ + ...step, + hasGithubRelease: true + })); + + const breakingChangeSteps = upgradeResult.breakingChanges + .filter( + (bc) => + !upgradeResult.path.some((step) => { + const normalizeVersion = (v: string) => + v.replace(/^(infisical\/)?v?/, "").replace(/-[a-zA-Z]+$/, ""); + return ( + normalizeVersion(step.version) === normalizeVersion(bc.version) + ); + }) + ) + .map((bc) => ({ + version: bc.version, + name: bc.version, + publishedAt: new Date().toISOString(), + prerelease: false, + hasGithubRelease: false + })); + + const allSteps = [...pathSteps, ...breakingChangeSteps]; + + allSteps.sort((a, b) => { + const normalizeForSort = (v: string) => { + const cleaned = v + .replace(/^(infisical\/)?v?/, "") + .replace(/-[a-zA-Z]+$/, ""); + const parts = cleaned.split(".").map(Number); + return parts[0] * 1000000 + (parts[1] || 0) * 1000 + (parts[2] || 0); + }; + return normalizeForSort(a.version) - normalizeForSort(b.version); + }); + + return allSteps; + })().map((step, index, allSteps) => { const isFirst = index === 0; - const isLast = index === upgradeResult.path.length - 1; + const isLast = index === allSteps.length - 1; const versionChanges = upgradeResult.breakingChanges.find((bc) => { if (bc.version === step.version) return true; @@ -412,15 +451,21 @@ export const UpgradePathPage = () => { Target Version )} - - - View Changelog - + {step.hasGithubRelease ? ( + + + View Changelog + + ) : ( + + No GitHub Release + + )}
    {/* Version Notes */} From 62c3092942d2faacc3cc2544e2404d9cd4ffbe98 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 26 Sep 2025 15:19:03 -0300 Subject: [PATCH 09/12] Improve timeline UI for upgrade tool --- .../UpgradePathPage/UpgradePathPage.tsx | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx index 79dbed9ef..4e81afcd5 100644 --- a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx +++ b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx @@ -411,29 +411,14 @@ export const UpgradePathPage = () => {
    {/* Timeline Column */}
    - {/* Timeline Circle */} -
    - {isFirst || isLast ? ( -
    - ) : hasBreakingChanges || hasDbMigration ? ( - - - - ) : ( -
    - )} +
    + {index + 1}
    {/* Timeline Line */} - {!isLast &&
    } + {!isLast && ( +
    + )}
    {/* Content Column */} From 44e7fb1989c0c6525db5a7792e05c4870d572f6d Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 26 Sep 2025 18:46:18 -0300 Subject: [PATCH 10/12] Remove js-yaml from root package json --- .github/workflows/validate-upgrade-path.yml | 166 ++---------------- backend/package.json | 3 +- backend/scripts/validate-upgrade-path-file.ts | 111 ++++++++++++ .../server/routes/v1/upgrade-path-router.ts | 8 +- .../upgrade-path/upgrade-path-schemas.ts | 24 +++ .../upgrade-path/upgrade-path-service.ts | 19 +- package-lock.json | 7 +- package.json | 1 - 8 files changed, 156 insertions(+), 183 deletions(-) create mode 100644 backend/scripts/validate-upgrade-path-file.ts create mode 100644 backend/src/services/upgrade-path/upgrade-path-schemas.ts diff --git a/.github/workflows/validate-upgrade-path.yml b/.github/workflows/validate-upgrade-path.yml index edc494dc3..10ec4fec1 100644 --- a/.github/workflows/validate-upgrade-path.yml +++ b/.github/workflows/validate-upgrade-path.yml @@ -5,6 +5,8 @@ on: types: [opened, synchronize] paths: - "backend/upgrade-path.yaml" + - "backend/scripts/validate-upgrade-path-file.ts" + - "backend/src/services/upgrade-path/upgrade-path-schemas.ts" workflow_call: @@ -12,170 +14,26 @@ jobs: validate-upgrade-path: name: Validate upgrade-path.yaml runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 3 steps: - name: Checkout source uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - - name: Check for changes in upgrade-path.yaml - id: check-changes - run: | - # For local testing with act, always run validation - if [ "${ACT:-false}" = "true" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - echo "Running validation (local act mode)" - else - # Check if upgrade-path.yaml was modified in this PR - if git diff --name-only HEAD^ HEAD | grep -q "backend/upgrade-path.yaml"; then - echo "changed=true" >> $GITHUB_OUTPUT - echo "Changes detected in backend/upgrade-path.yaml" - else - echo "changed=false" >> $GITHUB_OUTPUT - echo "No changes detected in backend/upgrade-path.yaml" - fi - fi - - - name: Setup Node.js for YAML validation - if: steps.check-changes.outputs.changed == 'true' + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' + cache: 'npm' + cache-dependency-path: 'backend/package-lock.json' - - name: Create lightweight validation script - if: steps.check-changes.outputs.changed == 'true' + - name: Install minimal dependencies + working-directory: backend run: | - # Create a temporary package.json with only the required dependencies - cat > package.json << 'EOF' - { - "name": "upgrade-path-validator", - "version": "1.0.0", - "dependencies": { - "js-yaml": "^4.1.0", - "zod": "^3.22.0" - } - } - EOF - - - name: Install minimal validation dependencies - if: steps.check-changes.outputs.changed == 'true' - run: | - npm install --no-package-lock --production + npm install --no-package-lock js-yaml@^4.1.0 zod@^3.22.0 tsx@^4.0.0 @types/js-yaml@^4.0.0 re2@^1.20.0 - name: Validate upgrade-path.yaml format - if: steps.check-changes.outputs.changed == 'true' - run: | - echo "Running upgrade-path.yaml validation..." - node << 'EOF' - const fs = require('fs'); - const yaml = require('js-yaml'); - const { z } = require('zod'); - - // Validation schemas matching backend service - const versionSchema = z - .string() - .min(1) - .max(50) - .regex(/^[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() - }); - - const upgradePathConfigSchema = z.object({ - versions: z.record(versionSchema, versionConfigSchema).optional().nullable() - }); - - function validateUpgradePathConfig() { - try { - const yamlPath = './backend/upgrade-path.yaml'; - - if (!fs.existsSync(yamlPath)) { - console.log('Warning: No upgrade-path.yaml file found'); - return true; - } - - const yamlContent = fs.readFileSync(yamlPath, 'utf8'); - - if (yamlContent.length > 1024 * 1024) { - throw new Error('Config file too large (>1MB)'); - } - - // Parse YAML safely - const config = yaml.load(yamlContent, { schema: yaml.FAILSAFE_SCHEMA }); - - if (!config) { - console.log('Warning: Empty configuration file'); - return true; - } - - // Validate schema - const result = upgradePathConfigSchema.safeParse(config); - - if (!result.success) { - console.log('Validation failed with the following errors:'); - result.error.issues.forEach(issue => { - const path = issue.path.length > 0 ? `[${issue.path.join('.')}]` : ''; - console.log(` - ${path}: ${issue.message}`); - }); - return false; - } - - const versions = config.versions || {}; - const versionCount = Object.keys(versions).length; - - if (versionCount === 0) { - console.log('Warning: No versions found in the configuration'); - } else { - console.log(`Validated ${versionCount} version configuration(s)`); - - // Check for common version patterns - const commonPatterns = [ - /^v?\d+\.\d+\.\d+$/, // v1.2.3 or 1.2.3 - /^v?\d+\.\d+\.\d+\.\d+$/, // v1.2.3.4 or 1.2.3.4 - /^infisical\/v?\d+\.\d+\.\d+$/, // infisical/v1.2.3 - /^infisical\/v?\d+\.\d+\.\d+-\w+$/ // infisical/v1.2.3-postgres - ]; - - for (const versionKey of Object.keys(versions)) { - const isCommonPattern = commonPatterns.some(pattern => pattern.test(versionKey)); - if (!isCommonPattern) { - console.log(`Warning: Version key '${versionKey}' doesn't match common patterns. This may be intentional.`); - } - } - } - - console.log('upgrade-path.yaml format is valid'); - return true; - - } catch (error) { - console.log(`Validation failed: ${error.message}`); - return false; - } - } - - if (!validateUpgradePathConfig()) { - process.exit(1); - } - EOF - - - name: Validation completed - if: steps.check-changes.outputs.changed == 'true' - run: | - echo "upgrade-path.yaml validation passed!" - echo "The configuration file follows the expected format and all version entries are valid." - - - name: Skipping validation - if: steps.check-changes.outputs.changed == 'false' - run: | - echo "Skipping validation - no changes detected in backend/upgrade-path.yaml" \ No newline at end of file + working-directory: backend + run: npx tsx ./scripts/validate-upgrade-path-file.ts \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 8fece4ebe..f06c1d69f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -73,7 +73,8 @@ "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", "seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest", - "email:dev": "email dev --dir src/services/smtp/emails" + "email:dev": "email dev --dir src/services/smtp/emails", + "validate-upgrade-path": "tsx ./scripts/validate-upgrade-path-file.ts" }, "keywords": [], "author": "", diff --git a/backend/scripts/validate-upgrade-path-file.ts b/backend/scripts/validate-upgrade-path-file.ts new file mode 100644 index 000000000..934d5d822 --- /dev/null +++ b/backend/scripts/validate-upgrade-path-file.ts @@ -0,0 +1,111 @@ +/* eslint-disable no-console */ +import { readFile } from "fs/promises"; +import * as yaml from "js-yaml"; +import * as path from "path"; +import { z } from "zod"; + +import { upgradePathConfigSchema } from "../src/services/upgrade-path/upgrade-path-schemas"; + +async function validateUpgradePathConfig(): Promise { + try { + const yamlPath = path.join(__dirname, "..", "upgrade-path.yaml"); + const resolvedPath = path.resolve(yamlPath); + const expectedBaseDir = path.resolve(__dirname, ".."); + + if (!resolvedPath.startsWith(expectedBaseDir)) { + throw new Error("Invalid configuration file path"); + } + + try { + await readFile(yamlPath, "utf8"); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + console.log("Warning: No upgrade-path.yaml file found"); + return; + } + throw error; + } + + const yamlContent = await readFile(yamlPath, "utf8"); + + if (yamlContent.length > 1024 * 1024) { + throw new Error("Config file too large (>1MB)"); + } + + let config: unknown; + try { + config = yaml.load(yamlContent, { + schema: yaml.FAILSAFE_SCHEMA, + filename: yamlPath, + onWarning: (warning) => { + console.log(`YAML Warning: ${warning.message}`); + } + }); + } catch (yamlError) { + if (yamlError instanceof yaml.YAMLException) { + throw new Error( + `YAML parsing failed: ${yamlError.message} at line ${yamlError.mark?.line}, column ${yamlError.mark?.column}` + ); + } + throw new Error(`YAML parsing failed: ${yamlError instanceof Error ? yamlError.message : "Unknown YAML error"}`); + } + + if (!config) { + console.log("Warning: Empty configuration file"); + return; + } + + if (typeof config !== "object" || config === null) { + throw new Error("Configuration must be a valid YAML object"); + } + + const result = upgradePathConfigSchema.safeParse(config); + + if (!result.success) { + console.log("Validation failed with the following errors:"); + result.error.issues.forEach((issue: z.ZodIssue) => { + const issuePath = issue.path.length > 0 ? `[${issue.path.join(".")}]` : ""; + console.log(` - ${issuePath}: ${issue.message}`); + }); + throw new Error("Schema validation failed"); + } + + const validatedConfig = result.data; + const versions = validatedConfig?.versions || {}; + const versionCount = Object.keys(versions).length; + + if (versionCount === 0) { + console.log("Warning: No versions found in the configuration"); + } else { + console.log(`Validated ${versionCount} version configuration(s)`); + + const commonPatterns = [ + /^v?\d+\.\d+\.\d+$/, + /^v?\d+\.\d+\.\d+\.\d+$/, + /^infisical\/v?\d+\.\d+\.\d+$/, + /^infisical\/v?\d+\.\d+\.\d+-\w+$/ + ]; + + for (const versionKey of Object.keys(versions)) { + const isCommonPattern = commonPatterns.some((pattern) => pattern.test(versionKey)); + if (!isCommonPattern) { + console.log(`Warning: Version key '${versionKey}' doesn't match common patterns. This may be intentional.`); + } + } + } + + console.log("upgrade-path.yaml format is valid"); + } catch (error) { + console.error(`Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`); + process.exit(1); + } +} + +if (require.main === module) { + validateUpgradePathConfig().catch((error) => { + console.error("Unexpected error:", error); + process.exit(1); + }); +} + +export { validateUpgradePathConfig }; diff --git a/backend/src/server/routes/v1/upgrade-path-router.ts b/backend/src/server/routes/v1/upgrade-path-router.ts index 484b12f53..481bee5e6 100644 --- a/backend/src/server/routes/v1/upgrade-path-router.ts +++ b/backend/src/server/routes/v1/upgrade-path-router.ts @@ -1,15 +1,9 @@ -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 - .string() - .min(1) - .max(50) - .regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format"); +import { versionSchema } from "@app/services/upgrade-path/upgrade-path-schemas"; export const registerUpgradePathRouter = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend/src/services/upgrade-path/upgrade-path-schemas.ts b/backend/src/services/upgrade-path/upgrade-path-schemas.ts new file mode 100644 index 000000000..a283505c7 --- /dev/null +++ b/backend/src/services/upgrade-path/upgrade-path-schemas.ts @@ -0,0 +1,24 @@ +import RE2 from "re2"; +import { z } from "zod"; + +export const versionSchema = z + .string() + .min(1) + .max(50) + .regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format"); + +export 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) +}); + +export const versionConfigSchema = z.object({ + breaking_changes: z.array(breakingChangeSchema).optional(), + db_schema_changes: z.string().max(1000).optional(), + notes: z.string().max(2000).optional() +}); + +export const upgradePathConfigSchema = z.object({ + versions: z.record(versionSchema, versionConfigSchema).optional().nullable() +}); diff --git a/backend/src/services/upgrade-path/upgrade-path-service.ts b/backend/src/services/upgrade-path/upgrade-path-service.ts index ebba09de6..45c602d78 100644 --- a/backend/src/services/upgrade-path/upgrade-path-service.ts +++ b/backend/src/services/upgrade-path/upgrade-path-service.ts @@ -9,30 +9,13 @@ import { logger } from "@app/lib/logger"; import { fetchReleases } from "./github-client"; import { BreakingChange, FormattedRelease, UpgradePathConfig, UpgradePathResult, VersionConfig } from "./types"; +import { versionConfigSchema, versionSchema } from "./upgrade-path-schemas"; export type TUpgradePathServiceFactory = { keyStore: TKeyStoreFactory; }; export type TUpgradePathService = ReturnType; -const versionSchema = z - .string() - .min(1) - .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; diff --git a/package-lock.json b/package-lock.json index 6e37ffeab..4d72220af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "license": "ISC", "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", - "js-yaml": "^4.1.0", "secrets.js-grempe": "^2.0.0" }, "devDependencies": { @@ -562,6 +561,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { @@ -1104,6 +1104,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1890,7 +1891,8 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "balanced-match": { "version": "1.0.2", @@ -2282,6 +2284,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "requires": { "argparse": "^2.0.1" } diff --git a/package.json b/package.json index 2efd0def8..db15de3fb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ }, "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", - "js-yaml": "^4.1.0", "secrets.js-grempe": "^2.0.0" } } From aba41312e12a93b994bbb31547559f6e71b504cb Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 26 Sep 2025 18:50:55 -0300 Subject: [PATCH 11/12] Increase action timeout --- .github/workflows/validate-upgrade-path.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-upgrade-path.yml b/.github/workflows/validate-upgrade-path.yml index 10ec4fec1..dabdccae0 100644 --- a/.github/workflows/validate-upgrade-path.yml +++ b/.github/workflows/validate-upgrade-path.yml @@ -14,7 +14,7 @@ jobs: validate-upgrade-path: name: Validate upgrade-path.yaml runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 15 steps: - name: Checkout source From fd02746579504b8abf673068341dd98c644c0f72 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 26 Sep 2025 19:26:42 -0300 Subject: [PATCH 12/12] Minor nit fix on validate yaml file format script --- backend/scripts/validate-upgrade-path-file.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/scripts/validate-upgrade-path-file.ts b/backend/scripts/validate-upgrade-path-file.ts index 934d5d822..566300cde 100644 --- a/backend/scripts/validate-upgrade-path-file.ts +++ b/backend/scripts/validate-upgrade-path-file.ts @@ -101,11 +101,7 @@ async function validateUpgradePathConfig(): Promise { } } -if (require.main === module) { - validateUpgradePathConfig().catch((error) => { - console.error("Unexpected error:", error); - process.exit(1); - }); -} - -export { validateUpgradePathConfig }; +validateUpgradePathConfig().catch((error) => { + console.error("Unexpected error:", error); + process.exit(1); +});