Skip to content

feat(build): fetch Railpack versions dynamically from GitHub releases#4070

Open
s24407-pj wants to merge 3 commits intoDokploy:canaryfrom
s24407-pj:feat/dynamic-railpack-versions
Open

feat(build): fetch Railpack versions dynamically from GitHub releases#4070
s24407-pj wants to merge 3 commits intoDokploy:canaryfrom
s24407-pj:feat/dynamic-railpack-versions

Conversation

@s24407-pj
Copy link

@s24407-pj s24407-pj commented Mar 25, 2026

Summary

  • Removes the hardcoded RAILPACK_VERSIONS array and replaces it with a tRPC query (getRailpackVersions) that fetches releases directly from the GitHub API
  • Versions are only fetched when the Railpack build type is selected, and cached for 24 hours to avoid hitting GitHub rate limits
  • The "Latest" badge is now dynamic — always reflects the actual latest release
  • Strips v prefix from manually entered versions before saving to prevent build failures
  • Fixed a bug where switching to Railpack for the first time would silently save the hardcoded "0.15.4" instead of the actual latest version

🤖 Generated with Claude Code

Greptile Summary

This PR replaces the hardcoded RAILPACK_VERSIONS array with a live GitHub Releases API call via a new getRailpackVersions tRPC procedure, making the version list and "Latest" badge always up-to-date. The client uses React Query with a 24-hour staleTime to avoid redundant requests within a session.

Key concerns found:

  • GitHub rate limiting (P1): The server-side getRailpackVersions query 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 optional GITHUB_TOKEN env 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 getRailpackVersions is still loading or has errored, both data.railpackVersion and railpackVersions?.[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): isError is 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=100 is a trivial fix.

Confidence Score: 2/5

  • Not safe to merge — the missing server-side cache can cause rate-limit failures for all users, and submitting while loading can silently persist an empty version string that breaks builds.
  • There are two P1 issues that affect the primary user path in normal production usage: (1) unmitigated GitHub API rate limits on the shared server IP will break the version list for all users after ~60 page loads per hour, and (2) a race condition can save an empty string as 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) and apps/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

Greptile also left 4 inline comments on this PR.

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

s24407-pj and others added 3 commits March 25, 2026 16:59
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>
@s24407-pj s24407-pj requested a review from Siumauricio as a code owner March 25, 2026 16:45
@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. enhancement New feature or request labels Mar 25, 2026
@dosubot
Copy link

dosubot bot commented Mar 25, 2026

Related Documentation

1 document(s) may need updating based on files changed in this PR:

Dokploy's Space

README /dokploy/blob/canary/apps/api/README.md
View Suggested Changes
@@ -113,6 +113,41 @@
   -F "zip=@/path/to/your/app.zip" \
   -F "dropBuildPath=optional/build/path"
 ```
+
+## Application Endpoints
+
+### application.getRailpackVersions
+
+Fetches available Railpack versions dynamically from the GitHub API.
+
+**Type:** Protected procedure query (requires authentication)
+
+**Parameters:** None
+
+**Response:**
+Returns an array of version strings (tag names from GitHub releases with the 'v' prefix stripped):
+```json
+[
+  "0.15.4",
+  "0.15.3",
+  "0.15.2"
+]
+```
+
+**Error Responses:**
+- `500` - INTERNAL_SERVER_ERROR: "Failed to fetch Railpack versions from GitHub" if the GitHub API request fails
+
+**Caching:**
+When calling from the client, use a `staleTime` of 24 hours (1000 * 60 * 60 * 24) to avoid hitting GitHub API rate limits.
+
+**Example:**
+```typescript
+const { data: railpackVersions } = api.application.getRailpackVersions.useQuery(undefined, {
+  staleTime: 1000 * 60 * 60 * 24, // 24 hours
+});
+```
+
+**Data Source:** https://api.github.com/repos/railwayapp/railpack/releases
 
 ## Search Endpoints
 

[Accept] [Decline]

Note: You must be authenticated to accept/decline updates.

How did I do? Any feedback?  Join Discord

Comment on lines +1083 to +1096
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/, ""));
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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}` }
                : {}),
        },
    },
);

Comment on lines 215 to 218
railpackVersion:
data.buildType === BuildType.railpack
? data.railpackVersion || "0.15.4"
? (data.railpackVersion || railpackVersions?.[0] || "").replace(/^v/, "")
: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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:

Suggested change
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.

Comment on lines +158 to +162
const { data: railpackVersions, isLoading: isLoadingRailpackVersions } =
api.application.getRailpackVersions.useQuery(undefined, {
enabled: buildType === BuildType.railpack,
staleTime: 1000 * 60 * 60 * 24, // 24 hours
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +1084 to +1086
const res = await fetch(
"https://api.github.com/repos/railwayapp/railpack/releases",
{ headers: { Accept: "application/vnd.github+json" } },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant