@geekmidas/cli 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bundler-DQIuE3Kn.mjs → bundler-Db83tLti.mjs} +2 -2
- package/dist/{bundler-DQIuE3Kn.mjs.map → bundler-Db83tLti.mjs.map} +1 -1
- package/dist/{bundler-CyHg1v_T.cjs → bundler-DsXfFSCU.cjs} +2 -2
- package/dist/{bundler-CyHg1v_T.cjs.map → bundler-DsXfFSCU.cjs.map} +1 -1
- package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
- package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
- package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
- package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
- package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
- package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
- package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
- package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
- package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
- package/dist/encryption-Biq0EZ4m.cjs +4 -0
- package/dist/encryption-CQXBZGkt.mjs +3 -0
- package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
- package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
- package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
- package/dist/index-CXa3odEw.d.mts.map +1 -0
- package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
- package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
- package/dist/index.cjs +787 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +767 -125
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
- package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
- package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
- package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
- package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
- package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
- package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
- package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/build/index.ts +23 -6
- package/src/deploy/__tests__/domain.spec.ts +231 -0
- package/src/deploy/__tests__/secrets.spec.ts +300 -0
- package/src/deploy/__tests__/sniffer.spec.ts +221 -0
- package/src/deploy/docker.ts +58 -29
- package/src/deploy/dokploy-api.ts +99 -0
- package/src/deploy/domain.ts +125 -0
- package/src/deploy/index.ts +364 -145
- package/src/deploy/secrets.ts +182 -0
- package/src/deploy/sniffer.ts +180 -0
- package/src/dev/index.ts +155 -9
- package/src/docker/index.ts +17 -2
- package/src/docker/templates.ts +171 -1
- package/src/index.ts +18 -1
- package/src/init/generators/auth.ts +2 -0
- package/src/init/versions.ts +2 -2
- package/src/workspace/index.ts +2 -0
- package/src/workspace/schema.ts +32 -6
- package/src/workspace/types.ts +64 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-B0w17y4_.mjs +0 -3
- package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
- package/dist/index-C7TkoYmt.d.mts.map +0 -1
- package/dist/index-CpchsC9w.d.cts.map +0 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { NormalizedAppConfig } from '../../workspace/types';
|
|
3
|
+
import { sniffAllApps, sniffAppEnvironment, type SniffResult } from '../sniffer';
|
|
4
|
+
|
|
5
|
+
describe('sniffAppEnvironment', () => {
|
|
6
|
+
const workspacePath = '/test/workspace';
|
|
7
|
+
|
|
8
|
+
const createApp = (
|
|
9
|
+
overrides: Partial<NormalizedAppConfig> = {},
|
|
10
|
+
): NormalizedAppConfig => ({
|
|
11
|
+
type: 'backend',
|
|
12
|
+
path: 'apps/api',
|
|
13
|
+
port: 3000,
|
|
14
|
+
dependencies: [],
|
|
15
|
+
resolvedDeployTarget: 'dokploy',
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('frontend apps', () => {
|
|
20
|
+
it('should return empty env vars for frontend apps', async () => {
|
|
21
|
+
const app = createApp({ type: 'frontend' });
|
|
22
|
+
|
|
23
|
+
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
24
|
+
|
|
25
|
+
expect(result.appName).toBe('web');
|
|
26
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should ignore requiredEnv for frontend apps', async () => {
|
|
30
|
+
const app = createApp({
|
|
31
|
+
type: 'frontend',
|
|
32
|
+
requiredEnv: ['API_KEY', 'SECRET'], // Should be ignored
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
36
|
+
|
|
37
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('entry-based apps with requiredEnv', () => {
|
|
42
|
+
it('should return requiredEnv list for entry-based apps', async () => {
|
|
43
|
+
const app = createApp({
|
|
44
|
+
entry: './src/index.ts',
|
|
45
|
+
requiredEnv: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await sniffAppEnvironment(app, 'auth', workspacePath);
|
|
49
|
+
|
|
50
|
+
expect(result.appName).toBe('auth');
|
|
51
|
+
expect(result.requiredEnvVars).toEqual([
|
|
52
|
+
'DATABASE_URL',
|
|
53
|
+
'BETTER_AUTH_SECRET',
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return copy of requiredEnv (not reference)', async () => {
|
|
58
|
+
const requiredEnv = ['DATABASE_URL'];
|
|
59
|
+
const app = createApp({ requiredEnv });
|
|
60
|
+
|
|
61
|
+
const result = await sniffAppEnvironment(app, 'api', workspacePath);
|
|
62
|
+
|
|
63
|
+
// Modify the result and verify original is unchanged
|
|
64
|
+
result.requiredEnvVars.push('MODIFIED');
|
|
65
|
+
expect(requiredEnv).toEqual(['DATABASE_URL']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return empty when requiredEnv is empty array', async () => {
|
|
69
|
+
const app = createApp({ requiredEnv: [] });
|
|
70
|
+
|
|
71
|
+
const result = await sniffAppEnvironment(app, 'api', workspacePath);
|
|
72
|
+
|
|
73
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('apps with envParser', () => {
|
|
78
|
+
it('should return empty when envParser module cannot be loaded', async () => {
|
|
79
|
+
const app = createApp({
|
|
80
|
+
envParser: './src/nonexistent/env#parser',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// This will fail to load the module and return empty
|
|
84
|
+
// Suppress warnings for this test
|
|
85
|
+
const result = await sniffAppEnvironment(app, 'api', workspacePath, {
|
|
86
|
+
logWarnings: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should gracefully handle errors without failing the build', async () => {
|
|
93
|
+
const app = createApp({
|
|
94
|
+
envParser: './src/invalid/path#nonexistent',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Should not throw, just return empty
|
|
98
|
+
const result = await sniffAppEnvironment(app, 'api', workspacePath, {
|
|
99
|
+
logWarnings: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.appName).toBe('api');
|
|
103
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('apps without env detection', () => {
|
|
108
|
+
it('should return empty when no envParser or requiredEnv', async () => {
|
|
109
|
+
const app = createApp({
|
|
110
|
+
// No envParser or requiredEnv
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await sniffAppEnvironment(app, 'api', workspacePath);
|
|
114
|
+
|
|
115
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('sniffAllApps', () => {
|
|
121
|
+
const workspacePath = '/test/workspace';
|
|
122
|
+
|
|
123
|
+
it('should sniff all apps in workspace', async () => {
|
|
124
|
+
const apps: Record<string, NormalizedAppConfig> = {
|
|
125
|
+
api: {
|
|
126
|
+
type: 'backend',
|
|
127
|
+
path: 'apps/api',
|
|
128
|
+
port: 3000,
|
|
129
|
+
dependencies: [],
|
|
130
|
+
resolvedDeployTarget: 'dokploy',
|
|
131
|
+
requiredEnv: ['DATABASE_URL', 'REDIS_URL'],
|
|
132
|
+
},
|
|
133
|
+
auth: {
|
|
134
|
+
type: 'backend',
|
|
135
|
+
path: 'apps/auth',
|
|
136
|
+
port: 3002,
|
|
137
|
+
dependencies: [],
|
|
138
|
+
resolvedDeployTarget: 'dokploy',
|
|
139
|
+
requiredEnv: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
|
|
140
|
+
},
|
|
141
|
+
web: {
|
|
142
|
+
type: 'frontend',
|
|
143
|
+
path: 'apps/web',
|
|
144
|
+
port: 3001,
|
|
145
|
+
dependencies: ['api', 'auth'],
|
|
146
|
+
resolvedDeployTarget: 'dokploy',
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const results = await sniffAllApps(apps, workspacePath);
|
|
151
|
+
|
|
152
|
+
expect(results.size).toBe(3);
|
|
153
|
+
|
|
154
|
+
expect(results.get('api')).toEqual({
|
|
155
|
+
appName: 'api',
|
|
156
|
+
requiredEnvVars: ['DATABASE_URL', 'REDIS_URL'],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(results.get('auth')).toEqual({
|
|
160
|
+
appName: 'auth',
|
|
161
|
+
requiredEnvVars: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(results.get('web')).toEqual({
|
|
165
|
+
appName: 'web',
|
|
166
|
+
requiredEnvVars: [], // Frontend - no secrets
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle empty apps record', async () => {
|
|
171
|
+
const apps: Record<string, NormalizedAppConfig> = {};
|
|
172
|
+
|
|
173
|
+
const results = await sniffAllApps(apps, workspacePath);
|
|
174
|
+
|
|
175
|
+
expect(results.size).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should pass options to individual app sniffing', async () => {
|
|
179
|
+
const apps: Record<string, NormalizedAppConfig> = {
|
|
180
|
+
api: {
|
|
181
|
+
type: 'backend',
|
|
182
|
+
path: 'apps/api',
|
|
183
|
+
port: 3000,
|
|
184
|
+
dependencies: [],
|
|
185
|
+
resolvedDeployTarget: 'dokploy',
|
|
186
|
+
envParser: './src/nonexistent/env#parser', // Will fail but shouldn't log
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Should not throw, and suppress warnings
|
|
191
|
+
const results = await sniffAllApps(apps, workspacePath, {
|
|
192
|
+
logWarnings: false,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(results.size).toBe(1);
|
|
196
|
+
expect(results.get('api')?.requiredEnvVars).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('fire-and-forget handling', () => {
|
|
201
|
+
it('captures environment variables even when envParser throws', async () => {
|
|
202
|
+
// The sniffer should capture env vars that were accessed before an error
|
|
203
|
+
// This is the "fire and forget" pattern - errors don't stop env detection
|
|
204
|
+
const app: NormalizedAppConfig = {
|
|
205
|
+
type: 'backend',
|
|
206
|
+
path: 'apps/api',
|
|
207
|
+
port: 3000,
|
|
208
|
+
dependencies: [],
|
|
209
|
+
resolvedDeployTarget: 'dokploy',
|
|
210
|
+
envParser: './src/invalid#missing',
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = await sniffAppEnvironment(app, 'api', '/test/workspace', {
|
|
214
|
+
logWarnings: false,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Should return gracefully without throwing
|
|
218
|
+
expect(result.appName).toBe('api');
|
|
219
|
+
expect(Array.isArray(result.requiredEnvVars)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/deploy/docker.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import type { GkmConfig } from '../config';
|
|
5
|
-
import { dockerCommand, findLockfilePath
|
|
5
|
+
import { dockerCommand, findLockfilePath } from '../docker';
|
|
6
6
|
import type { DeployResult, DockerDeployConfig } from './types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -76,6 +76,16 @@ export interface DockerDeployOptions {
|
|
|
76
76
|
masterKey?: string;
|
|
77
77
|
/** Docker config from gkm.config */
|
|
78
78
|
config: DockerDeployConfig;
|
|
79
|
+
/**
|
|
80
|
+
* Build arguments to pass to docker build.
|
|
81
|
+
* Format: ['KEY=value', 'KEY2=value2']
|
|
82
|
+
*/
|
|
83
|
+
buildArgs?: string[];
|
|
84
|
+
/**
|
|
85
|
+
* Public URL argument names for frontend Dockerfile generation.
|
|
86
|
+
* Used to ensure the Dockerfile declares these as ARG/ENV.
|
|
87
|
+
*/
|
|
88
|
+
publicUrlArgs?: string[];
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
|
@@ -94,15 +104,24 @@ export function getImageRef(
|
|
|
94
104
|
|
|
95
105
|
/**
|
|
96
106
|
* Build Docker image
|
|
107
|
+
* @param imageRef - Full image reference (registry/name:tag)
|
|
108
|
+
* @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
|
|
109
|
+
* @param buildArgs - Build arguments to pass to docker build
|
|
97
110
|
*/
|
|
98
|
-
async function buildImage(
|
|
111
|
+
async function buildImage(
|
|
112
|
+
imageRef: string,
|
|
113
|
+
appName?: string,
|
|
114
|
+
buildArgs?: string[],
|
|
115
|
+
): Promise<void> {
|
|
99
116
|
logger.log(`\n🔨 Building Docker image: ${imageRef}`);
|
|
100
117
|
|
|
101
118
|
const cwd = process.cwd();
|
|
102
|
-
const
|
|
119
|
+
const lockfilePath = findLockfilePath(cwd);
|
|
120
|
+
const lockfileDir = lockfilePath ? dirname(lockfilePath) : cwd;
|
|
121
|
+
const inMonorepo = lockfileDir !== cwd;
|
|
103
122
|
|
|
104
123
|
// Generate appropriate Dockerfile
|
|
105
|
-
if (inMonorepo) {
|
|
124
|
+
if (appName || inMonorepo) {
|
|
106
125
|
logger.log(' Generating Dockerfile for monorepo (turbo prune)...');
|
|
107
126
|
} else {
|
|
108
127
|
logger.log(' Generating Dockerfile...');
|
|
@@ -110,31 +129,41 @@ async function buildImage(imageRef: string): Promise<void> {
|
|
|
110
129
|
await dockerCommand({});
|
|
111
130
|
|
|
112
131
|
// Determine build context and Dockerfile path
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
dockerfilePath = join(appRelPath, '.gkm/docker/Dockerfile');
|
|
123
|
-
buildCwd = monorepoRoot;
|
|
124
|
-
logger.log(` Building from monorepo root: ${monorepoRoot}`);
|
|
125
|
-
}
|
|
132
|
+
// For workspaces with multiple apps, use per-app Dockerfile (Dockerfile.api, etc.)
|
|
133
|
+
const dockerfileSuffix = appName ? `.${appName}` : '';
|
|
134
|
+
const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
|
|
135
|
+
|
|
136
|
+
// Build from workspace/monorepo root when we have a lockfile elsewhere or appName is provided
|
|
137
|
+
const buildCwd =
|
|
138
|
+
lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
|
|
139
|
+
if (buildCwd !== cwd) {
|
|
140
|
+
logger.log(` Building from workspace root: ${buildCwd}`);
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
// Build the build args string
|
|
144
|
+
const buildArgsString =
|
|
145
|
+
buildArgs && buildArgs.length > 0
|
|
146
|
+
? buildArgs.map((arg) => `--build-arg "${arg}"`).join(' ')
|
|
147
|
+
: '';
|
|
148
|
+
|
|
128
149
|
try {
|
|
129
150
|
// Build for linux/amd64 to ensure compatibility with most cloud servers
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
const cmd = [
|
|
152
|
+
'DOCKER_BUILDKIT=1 docker build',
|
|
153
|
+
'--platform linux/amd64',
|
|
154
|
+
`-f ${dockerfilePath}`,
|
|
155
|
+
`-t ${imageRef}`,
|
|
156
|
+
buildArgsString,
|
|
157
|
+
'.',
|
|
158
|
+
]
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.join(' ');
|
|
161
|
+
|
|
162
|
+
execSync(cmd, {
|
|
163
|
+
cwd: buildCwd,
|
|
164
|
+
stdio: 'inherit',
|
|
165
|
+
env: { ...process.env, DOCKER_BUILDKIT: '1' },
|
|
166
|
+
});
|
|
138
167
|
logger.log(`✅ Image built: ${imageRef}`);
|
|
139
168
|
} catch (error) {
|
|
140
169
|
throw new Error(
|
|
@@ -168,14 +197,14 @@ async function pushImage(imageRef: string): Promise<void> {
|
|
|
168
197
|
export async function deployDocker(
|
|
169
198
|
options: DockerDeployOptions,
|
|
170
199
|
): Promise<DeployResult> {
|
|
171
|
-
const { stage, tag, skipPush, masterKey, config } = options;
|
|
200
|
+
const { stage, tag, skipPush, masterKey, config, buildArgs } = options;
|
|
172
201
|
|
|
173
202
|
// imageName should always be set by resolveDockerConfig
|
|
174
203
|
const imageName = config.imageName!;
|
|
175
204
|
const imageRef = getImageRef(config.registry, imageName, tag);
|
|
176
205
|
|
|
177
|
-
// Build image
|
|
178
|
-
await buildImage(imageRef);
|
|
206
|
+
// Build image (pass appName for workspace Dockerfile selection)
|
|
207
|
+
await buildImage(imageRef, config.appName, buildArgs);
|
|
179
208
|
|
|
180
209
|
// Push to registry if not skipped
|
|
181
210
|
if (!skipPush) {
|
|
@@ -458,6 +458,68 @@ export class DokployApi {
|
|
|
458
458
|
): Promise<void> {
|
|
459
459
|
await this.post('redis.update', { redisId, ...updates });
|
|
460
460
|
}
|
|
461
|
+
|
|
462
|
+
// ============================================
|
|
463
|
+
// Domain endpoints
|
|
464
|
+
// ============================================
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Create a new domain for an application
|
|
468
|
+
*/
|
|
469
|
+
async createDomain(options: DokployDomainCreate): Promise<DokployDomain> {
|
|
470
|
+
return this.post<DokployDomain>(
|
|
471
|
+
'domain.create',
|
|
472
|
+
options as unknown as Record<string, unknown>,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Update an existing domain
|
|
478
|
+
*/
|
|
479
|
+
async updateDomain(
|
|
480
|
+
domainId: string,
|
|
481
|
+
updates: Partial<DokployDomainCreate>,
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
await this.post('domain.update', { domainId, ...updates });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Delete a domain
|
|
488
|
+
*/
|
|
489
|
+
async deleteDomain(domainId: string): Promise<void> {
|
|
490
|
+
await this.post('domain.delete', { domainId });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get a domain by ID
|
|
495
|
+
*/
|
|
496
|
+
async getDomain(domainId: string): Promise<DokployDomain> {
|
|
497
|
+
return this.get<DokployDomain>(`domain.one?domainId=${domainId}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get all domains for an application
|
|
502
|
+
*/
|
|
503
|
+
async getDomainsByApplicationId(
|
|
504
|
+
applicationId: string,
|
|
505
|
+
): Promise<DokployDomain[]> {
|
|
506
|
+
return this.get<DokployDomain[]>(
|
|
507
|
+
`domain.byApplicationId?applicationId=${applicationId}`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Auto-generate a domain name for an application
|
|
513
|
+
*/
|
|
514
|
+
async generateDomain(
|
|
515
|
+
appName: string,
|
|
516
|
+
serverId?: string,
|
|
517
|
+
): Promise<{ domain: string }> {
|
|
518
|
+
return this.post<{ domain: string }>('domain.generateDomain', {
|
|
519
|
+
appName,
|
|
520
|
+
serverId,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
461
523
|
}
|
|
462
524
|
|
|
463
525
|
// ============================================
|
|
@@ -552,6 +614,43 @@ export interface DokployRedisUpdate {
|
|
|
552
614
|
description: string;
|
|
553
615
|
}
|
|
554
616
|
|
|
617
|
+
export type DokployCertificateType = 'letsencrypt' | 'none' | 'custom';
|
|
618
|
+
export type DokployDomainType = 'application' | 'compose' | 'preview';
|
|
619
|
+
|
|
620
|
+
export interface DokployDomainCreate {
|
|
621
|
+
/** Domain hostname (e.g., 'api.example.com') */
|
|
622
|
+
host: string;
|
|
623
|
+
/** URL path (optional, e.g., '/api') */
|
|
624
|
+
path?: string | null;
|
|
625
|
+
/** Container port to route to (1-65535) */
|
|
626
|
+
port?: number | null;
|
|
627
|
+
/** Enable HTTPS */
|
|
628
|
+
https?: boolean;
|
|
629
|
+
/** Associated application ID */
|
|
630
|
+
applicationId?: string | null;
|
|
631
|
+
/** Certificate type for HTTPS */
|
|
632
|
+
certificateType?: DokployCertificateType;
|
|
633
|
+
/** Custom certificate resolver name */
|
|
634
|
+
customCertResolver?: string | null;
|
|
635
|
+
/** Docker Compose service ID */
|
|
636
|
+
composeId?: string | null;
|
|
637
|
+
/** Service name for compose */
|
|
638
|
+
serviceName?: string | null;
|
|
639
|
+
/** Domain type */
|
|
640
|
+
domainType?: DokployDomainType | null;
|
|
641
|
+
/** Preview deployment ID */
|
|
642
|
+
previewDeploymentId?: string | null;
|
|
643
|
+
/** Internal routing path */
|
|
644
|
+
internalPath?: string | null;
|
|
645
|
+
/** Strip path from forwarded requests */
|
|
646
|
+
stripPath?: boolean;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export interface DokployDomain extends DokployDomainCreate {
|
|
650
|
+
domainId: string;
|
|
651
|
+
createdAt?: string;
|
|
652
|
+
}
|
|
653
|
+
|
|
555
654
|
/**
|
|
556
655
|
* Create a Dokploy API client from stored credentials or environment
|
|
557
656
|
*/
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { DokployWorkspaceConfig, NormalizedAppConfig } from '../workspace/types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the hostname for an app based on stage configuration.
|
|
5
|
+
*
|
|
6
|
+
* Domain resolution priority:
|
|
7
|
+
* 1. Explicit app.domain override (string or stage-specific)
|
|
8
|
+
* 2. Default pattern based on app type:
|
|
9
|
+
* - Main frontend app gets base domain (e.g., 'myapp.com')
|
|
10
|
+
* - Other apps get prefixed domain (e.g., 'api.myapp.com')
|
|
11
|
+
*
|
|
12
|
+
* @param appName - The name of the app
|
|
13
|
+
* @param app - The normalized app configuration
|
|
14
|
+
* @param stage - The deployment stage (e.g., 'production', 'development')
|
|
15
|
+
* @param dokployConfig - Dokploy workspace configuration with domain mappings
|
|
16
|
+
* @param isMainFrontend - Whether this is the main frontend app
|
|
17
|
+
* @returns The resolved hostname for the app
|
|
18
|
+
* @throws Error if no domain configuration is found for the stage
|
|
19
|
+
*/
|
|
20
|
+
export function resolveHost(
|
|
21
|
+
appName: string,
|
|
22
|
+
app: NormalizedAppConfig,
|
|
23
|
+
stage: string,
|
|
24
|
+
dokployConfig: DokployWorkspaceConfig | undefined,
|
|
25
|
+
isMainFrontend: boolean,
|
|
26
|
+
): string {
|
|
27
|
+
// 1. Check for explicit app domain override
|
|
28
|
+
if (app.domain) {
|
|
29
|
+
if (typeof app.domain === 'string') {
|
|
30
|
+
return app.domain;
|
|
31
|
+
}
|
|
32
|
+
if (app.domain[stage]) {
|
|
33
|
+
return app.domain[stage]!;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Get base domain for this stage
|
|
38
|
+
const baseDomain = dokployConfig?.domains?.[stage];
|
|
39
|
+
if (!baseDomain) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No domain configured for stage "${stage}". ` +
|
|
42
|
+
`Add deploy.dokploy.domains.${stage} to gkm.config.ts`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Main frontend app gets base domain, others get prefix
|
|
47
|
+
if (isMainFrontend) {
|
|
48
|
+
return baseDomain;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return `${appName}.${baseDomain}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Determine if an app is the "main" frontend (gets base domain).
|
|
56
|
+
*
|
|
57
|
+
* An app is considered the main frontend if:
|
|
58
|
+
* 1. It's named 'web' and is a frontend type
|
|
59
|
+
* 2. It's the first frontend app in the apps list
|
|
60
|
+
*
|
|
61
|
+
* @param appName - The name of the app to check
|
|
62
|
+
* @param app - The app configuration
|
|
63
|
+
* @param allApps - All apps in the workspace
|
|
64
|
+
* @returns True if this is the main frontend app
|
|
65
|
+
*/
|
|
66
|
+
export function isMainFrontendApp(
|
|
67
|
+
appName: string,
|
|
68
|
+
app: NormalizedAppConfig,
|
|
69
|
+
allApps: Record<string, NormalizedAppConfig>,
|
|
70
|
+
): boolean {
|
|
71
|
+
if (app.type !== 'frontend') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// App named 'web' is always main
|
|
76
|
+
if (appName === 'web') {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Otherwise, check if this is the first frontend
|
|
81
|
+
for (const [name, a] of Object.entries(allApps)) {
|
|
82
|
+
if (a.type === 'frontend') {
|
|
83
|
+
return name === appName;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate public URL build args for a frontend app based on its dependencies.
|
|
92
|
+
*
|
|
93
|
+
* @param app - The frontend app configuration
|
|
94
|
+
* @param deployedUrls - Map of app name to deployed public URL
|
|
95
|
+
* @returns Array of build args like 'NEXT_PUBLIC_API_URL=https://api.example.com'
|
|
96
|
+
*/
|
|
97
|
+
export function generatePublicUrlBuildArgs(
|
|
98
|
+
app: NormalizedAppConfig,
|
|
99
|
+
deployedUrls: Record<string, string>,
|
|
100
|
+
): string[] {
|
|
101
|
+
const buildArgs: string[] = [];
|
|
102
|
+
|
|
103
|
+
for (const dep of app.dependencies) {
|
|
104
|
+
const publicUrl = deployedUrls[dep];
|
|
105
|
+
if (publicUrl) {
|
|
106
|
+
// Convert app name to UPPER_SNAKE_CASE for env var
|
|
107
|
+
const envVarName = `NEXT_PUBLIC_${dep.toUpperCase()}_URL`;
|
|
108
|
+
buildArgs.push(`${envVarName}=${publicUrl}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return buildArgs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get public URL arg names from app dependencies.
|
|
117
|
+
*
|
|
118
|
+
* @param app - The frontend app configuration
|
|
119
|
+
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
|
|
120
|
+
*/
|
|
121
|
+
export function getPublicUrlArgNames(app: NormalizedAppConfig): string[] {
|
|
122
|
+
return app.dependencies.map(
|
|
123
|
+
(dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`,
|
|
124
|
+
);
|
|
125
|
+
}
|