feat(build): fetch Railpack versions dynamically from GitHub releases#4070
feat(build): fetch Railpack versions dynamically from GitHub releases#4070s24407-pj wants to merge 3 commits intoDokploy:canaryfrom
Conversation
Replace the hardcoded RAILPACK_VERSIONS array with a tRPC query that fetches releases from the GitHub API. Versions are cached for 24 hours and only fetched when the Railpack build type is selected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ection - Change Zod schema default from hardcoded "0.15.4" to null so Zod does not silently override the form field before the fetched versions arrive - Set form value to latest fetched version when no version is selected - Strip v prefix from manually entered versions before saving - Fix form reset loop by splitting useEffect into separate concerns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Related Documentation 1 document(s) may need updating based on files changed in this PR: Dokploy's Space README
|
| getRailpackVersions: protectedProcedure.query(async () => { | ||
| const res = await fetch( | ||
| "https://api.github.com/repos/railwayapp/railpack/releases", | ||
| { headers: { Accept: "application/vnd.github+json" } }, | ||
| ); | ||
| 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 }>; | ||
| return releases.map((r) => r.tag_name.replace(/^v/, "")); | ||
| }), |
There was a problem hiding this comment.
No server-side caching — GitHub rate limits will be hit quickly
The endpoint makes a fresh GitHub API call on every invocation. GitHub's unauthenticated rate limit is 60 requests/hour per IP address. Because the Dokploy server makes these calls (all users share one IP), a busy instance with multiple users opening the Railpack build tab can exhaust this limit very quickly, causing every subsequent call to fail with a 403/429.
The client-side staleTime: 1000 * 60 * 60 * 24 only helps within a single user's session — it won't help when different users open the page, or when the server restarts and all client caches are cold.
A simple fix is to add a server-side in-memory cache (e.g. a module-level variable with a timestamp) or use a Cache-Control response header from GitHub to drive conditional requests. At minimum, a GITHUB_TOKEN environment variable should be optionally read and forwarded so authenticated rate limits (5 000 req/hour) can be used:
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}` }
: {}),
},
},
);| railpackVersion: | ||
| data.buildType === BuildType.railpack | ||
| ? data.railpackVersion || "0.15.4" | ||
| ? (data.railpackVersion || railpackVersions?.[0] || "").replace(/^v/, "") | ||
| : null, |
There was a problem hiding this comment.
Empty string saved as version if API is still loading or failed
If the user switches to Railpack and submits before getRailpackVersions resolves (or if the GitHub API request fails entirely), data.railpackVersion is null (the schema default) and railpackVersions?.[0] is undefined. The expression evaluates to "".replace(/^v/, "") → "", which silently saves an empty string as the build version, causing the next build to fail without a clear error.
The Save button should be disabled while versions are loading, or a validation should reject an empty version:
| railpackVersion: | |
| data.buildType === BuildType.railpack | |
| ? data.railpackVersion || "0.15.4" | |
| ? (data.railpackVersion || railpackVersions?.[0] || "").replace(/^v/, "") | |
| : null, | |
| ? (data.railpackVersion || railpackVersions?.[0] || null)?.replace(/^v/, "") ?? null |
Pairing this with disabling the Save button while isLoadingRailpackVersions is true would fully close the gap.
| const { data: railpackVersions, isLoading: isLoadingRailpackVersions } = | ||
| api.application.getRailpackVersions.useQuery(undefined, { | ||
| enabled: buildType === BuildType.railpack, | ||
| staleTime: 1000 * 60 * 60 * 24, // 24 hours | ||
| }); |
There was a problem hiding this comment.
No error state handled for
getRailpackVersions
isError is not destructured from the query. When the GitHub API call fails (rate-limited, network error, etc.), isLoadingRailpackVersions flips back to false but railpackVersions remains undefined. The SelectTrigger becomes enabled again, the dropdown renders no options, and there is no error message anywhere — the user sees a blank selector with no explanation.
At minimum, destructure isError and show an error message:
const {
data: railpackVersions,
isLoading: isLoadingRailpackVersions,
isError: isErrorRailpackVersions,
} = api.application.getRailpackVersions.useQuery(undefined, {
enabled: buildType === BuildType.railpack,
staleTime: 1000 * 60 * 60 * 24,
});Then in the SelectTrigger (or just below the Select), show feedback when isErrorRailpackVersions is true so users understand they can still enter a version manually.
| const res = await fetch( | ||
| "https://api.github.com/repos/railwayapp/railpack/releases", | ||
| { headers: { Accept: "application/vnd.github+json" } }, |
There was a problem hiding this comment.
GitHub API pagination — only first 30 releases returned
The /releases endpoint defaults to per_page=30. Railpack already has more than 20 releases listed in the old hardcoded array, so future releases beyond 30 would be silently omitted. Adding ?per_page=100 would cover the foreseeable future without requiring pagination logic.
| const res = await fetch( | |
| "https://api.github.com/repos/railwayapp/railpack/releases", | |
| { headers: { Accept: "application/vnd.github+json" } }, | |
| "https://api.github.com/repos/railwayapp/railpack/releases?per_page=100", |
Summary
RAILPACK_VERSIONSarray and replaces it with a tRPC query (getRailpackVersions) that fetches releases directly from the GitHub APIvprefix from manually entered versions before saving to prevent build failures"0.15.4"instead of the actual latest version🤖 Generated with Claude Code
Greptile Summary
This PR replaces the hardcoded
RAILPACK_VERSIONSarray with a live GitHub Releases API call via a newgetRailpackVersionstRPC procedure, making the version list and "Latest" badge always up-to-date. The client uses React Query with a 24-hourstaleTimeto avoid redundant requests within a session.Key concerns found:
GitHub rate limiting (P1): The server-side
getRailpackVersionsquery has no server-level cache. GitHub's unauthenticated API rate limit is 60 requests/hour per server IP. In a multi-user Dokploy instance all users share the same IP, so this limit can be exhausted quickly. Adding an optionalGITHUB_TOKENenv variable and/or a module-level in-memory cache on the server side would be necessary for production reliability.Empty string saved as version (P1): When the form is submitted while
getRailpackVersionsis still loading or has errored, bothdata.railpackVersionandrailpackVersions?.[0]are falsy, causing""to be persisted as the build version. This would silently break any subsequent build triggered for that application.No error state in the UI (P1):
isErroris not destructured from the query, so if the GitHub API call fails the select becomes enabled but renders no options, with no message explaining what happened or pointing users to the manual entry mode.Pagination truncation (P2): The GitHub API call uses the default
per_page=30, which silently omits releases beyond the first page. Adding?per_page=100is a trivial fix.Confidence Score: 2/5
railpackVersion, causing silent build failures. Both issues need to be resolved before this is safe to ship.apps/dokploy/server/api/routers/application.ts(server-side caching and rate limits) andapps/dokploy/components/dashboard/application/build/show.tsx(empty-version guard and error state).Reviews (1): Last reviewed commit: "fix(build): correctly default railpack v..." | Re-trigger Greptile
(2/5) Greptile learns from your feedback when you react with thumbs up/down!