diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 32aee23d37..bb40c16baa 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -29,30 +29,6 @@ import { } from "@/components/ui/select"; import { api } from "@/utils/api"; -// Railpack versions from https://github.com/railwayapp/railpack/releases -export const RAILPACK_VERSIONS = [ - "0.15.4", - "0.15.3", - "0.15.2", - "0.15.1", - "0.15.0", - "0.14.0", - "0.13.0", - "0.12.0", - "0.11.0", - "0.10.0", - "0.9.2", - "0.9.1", - "0.9.0", - "0.8.0", - "0.7.0", - "0.6.0", - "0.5.0", - "0.4.0", - "0.3.0", - "0.2.2", -] as const; - export enum BuildType { dockerfile = "dockerfile", heroku_buildpacks = "heroku_buildpacks", @@ -91,7 +67,7 @@ const mySchema = z.discriminatedUnion("buildType", [ }), z.object({ buildType: z.literal(BuildType.railpack), - railpackVersion: z.string().nullable().default("0.15.4"), + railpackVersion: z.string().nullable().default(null), }), z.object({ buildType: z.literal(BuildType.static), @@ -162,6 +138,9 @@ const resetData = (data: ApplicationData): AddTemplate => { } }; +const normalizeRailpackVersion = (version: string) => + version.trim().replace(/^v/i, ""); + export const ShowBuildChooseForm = ({ applicationId }: Props) => { const { mutateAsync, isPending } = api.application.saveBuildType.useMutation(); @@ -169,7 +148,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { { applicationId }, { enabled: !!applicationId }, ); - const form = useForm({ defaultValues: { buildType: BuildType.nixpacks, @@ -178,7 +156,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { }); const buildType = form.watch("buildType"); - const railpackVersion = form.watch("railpackVersion"); + + const { + data: railpackVersions, + isLoading: isLoadingRailpackVersions, + isError: isErrorRailpackVersions, + } = api.application.getRailpackVersions.useQuery(undefined, { + enabled: buildType === BuildType.railpack, + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }); const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false); useEffect(() => { @@ -191,16 +177,28 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { }; form.reset(resetData(typedData)); + } + }, [data, form]); - // Check if railpack version is manual (not in the predefined list) + useEffect(() => { + if (railpackVersions?.length) { if ( - data.railpackVersion && - !RAILPACK_VERSIONS.includes(data.railpackVersion as any) + data?.railpackVersion && + !railpackVersions.includes(data.railpackVersion) ) { setIsManualRailpackVersion(true); } + if (!form.getValues("railpackVersion")) { + form.setValue("railpackVersion", railpackVersions[0]); + } } - }, [data, form]); + }, [railpackVersions, data?.railpackVersion, form]); + + useEffect(() => { + if (buildType === BuildType.railpack && isErrorRailpackVersions) { + setIsManualRailpackVersion(true); + } + }, [buildType, isErrorRailpackVersions]); // Hide builder section when Docker provider is selected if (data?.sourceType === "docker") { @@ -208,11 +206,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { } const onSubmit = async (data: AddTemplate) => { + const normalizedRailpackVersion = + data.buildType === BuildType.railpack + ? normalizeRailpackVersion( + data.railpackVersion || railpackVersions?.[0] || "", + ) + : null; + + if (data.buildType === BuildType.railpack && !normalizedRailpackVersion) { + toast.error("Railpack version is required"); + return; + } + await mutateAsync({ applicationId, buildType: data.buildType, publishDirectory: - data.buildType === BuildType.nixpacks ? data.publishDirectory : null, + data.buildType === BuildType.nixpacks + ? (data.publishDirectory ?? null) + : null, dockerfile: data.buildType === BuildType.dockerfile ? data.dockerfile : null, dockerContextPath: @@ -227,7 +239,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { data.buildType === BuildType.static ? data.isStaticSpa : null, railpackVersion: data.buildType === BuildType.railpack - ? data.railpackVersion || "0.15.4" + ? normalizedRailpackVersion : null, }) .then(async () => { @@ -465,7 +477,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { size="sm" onClick={() => { setIsManualRailpackVersion(false); - field.onChange("0.15.4"); + field.onChange(railpackVersions?.[0] ?? ""); }} > Use predefined versions @@ -481,10 +493,16 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { field.onChange(value); } }} - value={field.value ?? "0.15.4"} + value={field.value ?? railpackVersions?.[0] ?? ""} > - - + + {isLoadingRailpackVersions ? ( + + Loading versions... + + ) : ( + + )} @@ -492,10 +510,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { ✏️ Manual (Custom Version) - {RAILPACK_VERSIONS.map((version) => ( + {railpackVersions?.map((version) => ( v{version} - {version === "0.15.4" && ( + {version === railpackVersions?.[0] && ( { View releases + {isErrorRailpackVersions && !isManualRailpackVersion && ( +

+ Failed to load Railpack versions. Switch to manual + mode to set a custom version. +

+ )} )} @@ -528,7 +552,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { )}
-
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 228c986567..16d428638f 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -77,6 +77,11 @@ import { } from "@/server/queues/queueSetup"; import { cancelDeployment, deploy } from "@/server/utils/deploy"; +const RAILPACK_VERSIONS_CACHE_TTL = 1000 * 60 * 60 * 24; +let railpackVersionsCache: + | { versions: string[]; fetchedAt: number } + | undefined; + export const applicationRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateApplication) @@ -1103,6 +1108,37 @@ export const applicationRouter = createTRPCRouter({ }; }), + getRailpackVersions: protectedProcedure.query(async () => { + if ( + railpackVersionsCache && + Date.now() - railpackVersionsCache.fetchedAt < RAILPACK_VERSIONS_CACHE_TTL + ) { + return railpackVersionsCache.versions; + } + + const res = await fetch( + "https://api.github.com/repos/railwayapp/railpack/releases", + { + headers: { + Accept: "application/vnd.github+json", + ...(process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}), + }, + }, + ); + if (!res.ok) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch Railpack versions from GitHub", + }); + } + const releases = (await res.json()) as Array<{ tag_name: string }>; + const versions = releases.map((r) => r.tag_name.replace(/^v/i, "")); + railpackVersionsCache = { versions, fetchedAt: Date.now() }; + return versions; + }), + readLogs: protectedProcedure .input( apiFindOneApplication.extend({ diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index a7067f63f3..c13b9aa702 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -428,9 +428,19 @@ export const apiSaveBuildType = createSchema dockerBuildStage: true, herokuVersion: true, railpackVersion: true, + publishDirectory: true, + isStaticSpa: true, }) .required() - .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true })); + .superRefine((data, ctx) => { + if (data.buildType === "railpack" && !data.railpackVersion?.trim()) { + ctx.addIssue({ + code: "custom", + path: ["railpackVersion"], + message: "Railpack version is required", + }); + } + }); export const apiSaveGithubProvider = createSchema .pick({