-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Add Dokploy deployment with comprehensive linking and control options #324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| import { ipcMain } from 'electron'; | ||
| import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; | ||
| import path from 'path'; | ||
| import { IPC_CHANNELS } from '../../shared/constants'; | ||
| import type { DokployApiRequest, DokployApiResponse, AppSettings, DokployAccount, DokployProjectDeployment } from '../../shared/types'; | ||
| import { readSettingsFile } from '../settings-utils'; | ||
| import { parseEnvFile } from './utils'; | ||
|
|
||
| const DEPLOYMENT_FILE = '.dokploy.json'; | ||
|
|
||
| /** | ||
| * Make a request to the Dokploy API | ||
| */ | ||
| async function makeDokployRequest<T>( | ||
| baseUrl: string, | ||
| apiKey: string, | ||
| endpoint: string, | ||
| method: 'GET' | 'POST', | ||
| body?: Record<string, unknown>, | ||
| query?: Record<string, string> | ||
| ): Promise<DokployApiResponse<T>> { | ||
| try { | ||
| // Build URL - Dokploy uses TRPC format | ||
| // baseUrl should be like https://dokploy.example.com/api | ||
| let url = `${baseUrl}/${endpoint}`; | ||
|
|
||
| // For GET requests with TRPC, we need to pass input as JSON in query string | ||
| if (method === 'GET') { | ||
| const input = query && Object.keys(query).length > 0 ? query : {}; | ||
| url += `?input=${encodeURIComponent(JSON.stringify(input))}`; | ||
| } | ||
|
|
||
| const options: RequestInit = { | ||
| method, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'x-api-key': apiKey | ||
| } | ||
| }; | ||
|
|
||
| if (method === 'POST' && body) { | ||
| options.body = JSON.stringify(body); | ||
| } | ||
|
|
||
| const response = await fetch(url, options); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| return { | ||
| success: false, | ||
| error: `API error: ${response.status} - ${errorText}` | ||
| }; | ||
| } | ||
|
|
||
| const responseData = await response.json(); | ||
| // TRPC returns data in { result: { data: ... } } format for queries | ||
| // or just the data directly for mutations | ||
| const data = responseData?.result?.data ?? responseData; | ||
| return { | ||
| success: true, | ||
| data: data as T | ||
| }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Request failed' | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Read environment variables from env files at a given path | ||
| * Checks multiple common env file names | ||
| */ | ||
| function readEnvFile(servicePath: string): Record<string, string> { | ||
| console.log('[Dokploy Backend] readEnvFile called with:', servicePath); | ||
|
|
||
| // Common env file names to check (in priority order) | ||
| const envFileNames = ['.env', '.env.local', '.env.development', '.env.production']; | ||
| let allEnvVars: Record<string, string> = {}; | ||
|
|
||
| for (const fileName of envFileNames) { | ||
| const envPath = path.join(servicePath, fileName); | ||
| console.log('[Dokploy Backend] Checking:', envPath, 'exists:', existsSync(envPath)); | ||
|
|
||
| if (existsSync(envPath)) { | ||
| try { | ||
| const content = readFileSync(envPath, 'utf-8'); | ||
| console.log(`[Dokploy Backend] Found ${fileName}, length:`, content.length); | ||
| const parsed = parseEnvFile(content); | ||
| console.log(`[Dokploy Backend] Parsed from ${fileName}:`, Object.keys(parsed)); | ||
| // Merge (earlier files take precedence) | ||
| allEnvVars = { ...parsed, ...allEnvVars }; | ||
| } catch (err) { | ||
| console.error(`[Dokploy Backend] Error reading ${fileName}:`, err); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log('[Dokploy Backend] Total env vars found:', Object.keys(allEnvVars)); | ||
| return allEnvVars; | ||
|
Comment on lines
+76
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§Ή Nitpick | π΅ Trivial Remove or conditionally enable verbose logging in production. The π Suggested approach+const DEBUG_DOKPLOY = process.env.NODE_ENV === 'development';
function readEnvFile(servicePath: string): Record<string, string> {
- console.log('[Dokploy Backend] readEnvFile called with:', servicePath);
+ if (DEBUG_DOKPLOY) console.log('[Dokploy Backend] readEnvFile called with:', servicePath);
// Common env file names to check (in priority order)
const envFileNames = ['.env', '.env.local', '.env.development', '.env.production'];
let allEnvVars: Record<string, string> = {};
for (const fileName of envFileNames) {
const envPath = path.join(servicePath, fileName);
- console.log('[Dokploy Backend] Checking:', envPath, 'exists:', existsSync(envPath));
+ if (DEBUG_DOKPLOY) console.log('[Dokploy Backend] Checking:', envPath, 'exists:', existsSync(envPath));
// ... apply similar changes to other log statements
}
π€ Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Register Dokploy-related IPC handlers | ||
| */ | ||
| export function registerDokployHandlers(): void { | ||
| // Main Dokploy API handler | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.DOKPLOY_API, | ||
| async (_, request: DokployApiRequest): Promise<DokployApiResponse<unknown>> => { | ||
| try { | ||
| // Get settings to find the Dokploy account | ||
| const settings = readSettingsFile() as unknown as AppSettings; | ||
| const dokployAccounts: DokployAccount[] = settings?.deploymentProviders?.dokploy || []; | ||
|
|
||
| // Find the account by ID | ||
| const account = dokployAccounts.find((a: DokployAccount) => a.id === request.accountId); | ||
| if (!account) { | ||
| return { | ||
| success: false, | ||
| error: 'Dokploy account not found' | ||
| }; | ||
| } | ||
|
|
||
| // Make the API request | ||
| return await makeDokployRequest( | ||
| account.baseUrl, | ||
| account.apiKey, | ||
| request.endpoint, | ||
| request.method, | ||
| request.body, | ||
| request.query | ||
| ); | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Unknown error' | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| // Read env variables from a service path | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.DOKPLOY_READ_ENV, | ||
| async (_, servicePath: string): Promise<DokployApiResponse<Record<string, string>>> => { | ||
| try { | ||
| const envVars = readEnvFile(servicePath); | ||
| return { success: true, data: envVars }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Failed to read env file' | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| // Save deployment info to project directory | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.DOKPLOY_SAVE_DEPLOYMENT, | ||
| async (_, projectPath: string, deployment: DokployProjectDeployment): Promise<DokployApiResponse<void>> => { | ||
| try { | ||
| const filePath = path.join(projectPath, DEPLOYMENT_FILE); | ||
| writeFileSync(filePath, JSON.stringify(deployment, null, 2), 'utf-8'); | ||
| return { success: true }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Failed to save deployment info' | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| // Get deployment info from project directory | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.DOKPLOY_GET_DEPLOYMENT, | ||
| async (_, projectPath: string): Promise<DokployApiResponse<DokployProjectDeployment | null>> => { | ||
| try { | ||
| const filePath = path.join(projectPath, DEPLOYMENT_FILE); | ||
| if (!existsSync(filePath)) { | ||
| return { success: true, data: null }; | ||
| } | ||
| const content = readFileSync(filePath, 'utf-8'); | ||
| const deployment = JSON.parse(content) as DokployProjectDeployment; | ||
| return { success: true, data: deployment }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Failed to read deployment info' | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
|
Comment on lines
+177
to
+196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§Ή Nitpick | π΅ Trivial Handle JSON parse errors gracefully for corrupted deployment files. If π Proposed improvement ipcMain.handle(
IPC_CHANNELS.DOKPLOY_GET_DEPLOYMENT,
async (_, projectPath: string): Promise<DokployApiResponse<DokployProjectDeployment | null>> => {
try {
const filePath = path.join(projectPath, DEPLOYMENT_FILE);
if (!existsSync(filePath)) {
return { success: true, data: null };
}
const content = readFileSync(filePath, 'utf-8');
- const deployment = JSON.parse(content) as DokployProjectDeployment;
- return { success: true, data: deployment };
+ try {
+ const deployment = JSON.parse(content) as DokployProjectDeployment;
+ return { success: true, data: deployment };
+ } catch (parseError) {
+ return {
+ success: false,
+ error: `Invalid deployment file format: ${parseError instanceof Error ? parseError.message : 'parse error'}`
+ };
+ }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read deployment info'
};
}
}
);π€ Prompt for AI Agents |
||
|
|
||
| // Delete deployment info from project directory | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.DOKPLOY_DELETE_DEPLOYMENT, | ||
| async (_, projectPath: string): Promise<DokployApiResponse<void>> => { | ||
| try { | ||
| const filePath = path.join(projectPath, DEPLOYMENT_FILE); | ||
| if (existsSync(filePath)) { | ||
| unlinkSync(filePath); | ||
| } | ||
| return { success: true }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Failed to delete deployment info' | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { ipcRenderer } from 'electron'; | ||
| import { IPC_CHANNELS } from '../../../shared/constants'; | ||
| import type { DokployApiRequest, DokployApiResponse, DokployProjectDeployment } from '../../../shared/types'; | ||
|
|
||
| export interface DokployAPI { | ||
| dokployApi: <T = unknown>(request: DokployApiRequest) => Promise<DokployApiResponse<T>>; | ||
| dokployReadEnv: (servicePath: string) => Promise<DokployApiResponse<Record<string, string>>>; | ||
| dokploySaveDeployment: (projectPath: string, deployment: DokployProjectDeployment) => Promise<DokployApiResponse<void>>; | ||
| dokployGetDeployment: (projectPath: string) => Promise<DokployApiResponse<DokployProjectDeployment | null>>; | ||
| dokployDeleteDeployment: (projectPath: string) => Promise<DokployApiResponse<void>>; | ||
| } | ||
|
|
||
| export const createDokployAPI = (): DokployAPI => ({ | ||
| dokployApi: <T = unknown>(request: DokployApiRequest): Promise<DokployApiResponse<T>> => | ||
| ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_API, request), | ||
| dokployReadEnv: (servicePath: string): Promise<DokployApiResponse<Record<string, string>>> => | ||
| ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_READ_ENV, servicePath), | ||
| dokploySaveDeployment: (projectPath: string, deployment: DokployProjectDeployment): Promise<DokployApiResponse<void>> => | ||
| ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_SAVE_DEPLOYMENT, projectPath, deployment), | ||
| dokployGetDeployment: (projectPath: string): Promise<DokployApiResponse<DokployProjectDeployment | null>> => | ||
| ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_GET_DEPLOYMENT, projectPath), | ||
| dokployDeleteDeployment: (projectPath: string): Promise<DokployApiResponse<void>> => | ||
| ipcRenderer.invoke(IPC_CHANNELS.DOKPLOY_DELETE_DEPLOYMENT, projectPath) | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a timeout to the fetch request to prevent indefinite hangs.
The
fetchcall has no timeout configured. If the Dokploy server is unresponsive, the request will hang indefinitely, potentially blocking the UI or causing resource exhaustion.π Proposed fix using AbortController
π€ Prompt for AI Agents