@geekmidas/cli 0.48.0 → 0.50.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/deploy/sniffer-envkit-patch.cjs +27 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.d.cts +46 -0
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.d.mts +46 -0
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.mjs +20 -0
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -0
- package/dist/deploy/sniffer-hooks.cjs +25 -0
- package/dist/deploy/sniffer-hooks.cjs.map +1 -0
- package/dist/deploy/sniffer-hooks.d.cts +27 -0
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -0
- package/dist/deploy/sniffer-hooks.d.mts +27 -0
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -0
- package/dist/deploy/sniffer-hooks.mjs +24 -0
- package/dist/deploy/sniffer-hooks.mjs.map +1 -0
- package/dist/deploy/sniffer-loader.cjs +16 -0
- package/dist/deploy/sniffer-loader.cjs.map +1 -0
- package/dist/deploy/sniffer-loader.d.cts +1 -0
- package/dist/deploy/sniffer-loader.d.mts +1 -0
- package/dist/deploy/sniffer-loader.mjs +15 -0
- package/dist/deploy/sniffer-loader.mjs.map +1 -0
- package/dist/deploy/sniffer-worker.cjs +42 -0
- package/dist/deploy/sniffer-worker.cjs.map +1 -0
- package/dist/deploy/sniffer-worker.d.cts +9 -0
- package/dist/deploy/sniffer-worker.d.cts.map +1 -0
- package/dist/deploy/sniffer-worker.d.mts +9 -0
- package/dist/deploy/sniffer-worker.d.mts.map +1 -0
- package/dist/deploy/sniffer-worker.mjs +41 -0
- package/dist/deploy/sniffer-worker.mjs.map +1 -0
- package/dist/{dokploy-api-DvzIDxTj.mjs → dokploy-api-94KzmTVf.mjs} +4 -4
- package/dist/dokploy-api-94KzmTVf.mjs.map +1 -0
- package/dist/dokploy-api-CItuaWTq.mjs +3 -0
- package/dist/dokploy-api-DBNE8MDt.cjs +3 -0
- package/dist/{dokploy-api-BDLu0qWi.cjs → dokploy-api-YD8WCQfW.cjs} +4 -4
- package/dist/dokploy-api-YD8WCQfW.cjs.map +1 -0
- package/dist/index.cjs +2415 -1893
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2411 -1889
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
- package/src/build/__tests__/handler-templates.spec.ts +947 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +24 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +24 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +12 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +14 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +10 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +18 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +18 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +16 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +229 -0
- package/src/deploy/__tests__/dokploy-api.spec.ts +2 -3
- package/src/deploy/__tests__/domain.spec.ts +7 -3
- package/src/deploy/__tests__/env-resolver.spec.ts +469 -0
- package/src/deploy/__tests__/index.spec.ts +12 -12
- package/src/deploy/__tests__/secrets.spec.ts +4 -1
- package/src/deploy/__tests__/sniffer.spec.ts +326 -1
- package/src/deploy/__tests__/state.spec.ts +844 -0
- package/src/deploy/dns/hostinger-api.ts +4 -1
- package/src/deploy/dns/index.ts +113 -1
- package/src/deploy/docker.ts +1 -2
- package/src/deploy/dokploy-api.ts +18 -9
- package/src/deploy/domain.ts +5 -4
- package/src/deploy/env-resolver.ts +278 -0
- package/src/deploy/index.ts +525 -119
- package/src/deploy/secrets.ts +7 -2
- package/src/deploy/sniffer-envkit-patch.ts +59 -0
- package/src/deploy/sniffer-hooks.ts +57 -0
- package/src/deploy/sniffer-loader.ts +28 -0
- package/src/deploy/sniffer-worker.ts +74 -0
- package/src/deploy/sniffer.ts +170 -14
- package/src/deploy/state.ts +162 -1
- package/src/init/versions.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +5 -0
- package/dist/dokploy-api-BDLu0qWi.cjs.map +0 -1
- package/dist/dokploy-api-BN3V57z1.mjs +0 -3
- package/dist/dokploy-api-BdCKjFDA.cjs +0 -3
- package/dist/dokploy-api-DvzIDxTj.mjs.map +0 -1
package/src/deploy/index.ts
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Module
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment of GKM workspaces to various providers (Docker, Dokploy).
|
|
5
|
+
*
|
|
6
|
+
* ## Per-App Database Credentials
|
|
7
|
+
*
|
|
8
|
+
* When deploying to Dokploy with Postgres, this module creates per-app database
|
|
9
|
+
* users with isolated schemas. This follows the same pattern as local dev mode
|
|
10
|
+
* (docker/postgres/init.sh).
|
|
11
|
+
*
|
|
12
|
+
* ### How It Works
|
|
13
|
+
*
|
|
14
|
+
* 1. **Provisioning**: Creates Postgres service with master credentials
|
|
15
|
+
* 2. **User Creation**: For each backend app that needs DATABASE_URL:
|
|
16
|
+
* - Generates a unique password (stored in deploy state)
|
|
17
|
+
* - Creates a database user with that password
|
|
18
|
+
* - Assigns schema permissions based on app name
|
|
19
|
+
* 3. **Schema Assignment**:
|
|
20
|
+
* - `api` app: Uses `public` schema (shared tables)
|
|
21
|
+
* - Other apps (e.g., `auth`): Get their own schema with `search_path` set
|
|
22
|
+
* 4. **Environment Injection**: Each app receives its own DATABASE_URL
|
|
23
|
+
*
|
|
24
|
+
* ### Security
|
|
25
|
+
*
|
|
26
|
+
* - External Postgres port is enabled only during user creation, then disabled
|
|
27
|
+
* - Each app can only access its own schema
|
|
28
|
+
* - Credentials are stored in `.gkm/deploy-{stage}.json` (gitignored)
|
|
29
|
+
* - Subsequent deploys reuse existing credentials from state
|
|
30
|
+
*
|
|
31
|
+
* ### Example Flow
|
|
32
|
+
*
|
|
33
|
+
* ```
|
|
34
|
+
* gkm deploy --stage production
|
|
35
|
+
* ├─ Create Postgres (user: postgres, db: myproject)
|
|
36
|
+
* ├─ Enable external port temporarily
|
|
37
|
+
* ├─ Create user "api" → public schema
|
|
38
|
+
* ├─ Create user "auth" → auth schema (search_path=auth)
|
|
39
|
+
* ├─ Disable external port
|
|
40
|
+
* ├─ Deploy "api" with DATABASE_URL=postgresql://api:xxx@postgres:5432/myproject
|
|
41
|
+
* └─ Deploy "auth" with DATABASE_URL=postgresql://auth:yyy@postgres:5432/myproject
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @module deploy
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { randomBytes } from 'node:crypto';
|
|
1
48
|
import { stdin as input, stdout as output } from 'node:process';
|
|
2
49
|
import * as readline from 'node:readline/promises';
|
|
50
|
+
import { Client as PgClient } from 'pg';
|
|
3
51
|
import {
|
|
4
52
|
getDokployCredentials,
|
|
5
53
|
getDokployRegistryId,
|
|
@@ -16,6 +64,11 @@ import {
|
|
|
16
64
|
isDeployTargetSupported,
|
|
17
65
|
} from '../workspace/index.js';
|
|
18
66
|
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
67
|
+
import {
|
|
68
|
+
orchestrateDns,
|
|
69
|
+
resolveHostnameToIp,
|
|
70
|
+
verifyDnsRecords,
|
|
71
|
+
} from './dns/index.js';
|
|
19
72
|
import { deployDocker, resolveDockerConfig } from './docker';
|
|
20
73
|
import { deployDokploy } from './dokploy';
|
|
21
74
|
import {
|
|
@@ -25,29 +78,33 @@ import {
|
|
|
25
78
|
type DokployRedis,
|
|
26
79
|
} from './dokploy-api';
|
|
27
80
|
import {
|
|
81
|
+
generatePublicUrlBuildArgs,
|
|
82
|
+
getPublicUrlArgNames,
|
|
83
|
+
isMainFrontendApp,
|
|
84
|
+
resolveHost,
|
|
85
|
+
} from './domain.js';
|
|
86
|
+
import {
|
|
87
|
+
type EnvResolverContext,
|
|
88
|
+
formatMissingVarsError,
|
|
89
|
+
validateEnvVars,
|
|
90
|
+
} from './env-resolver.js';
|
|
91
|
+
import { updateConfig } from './init';
|
|
92
|
+
import { generateSecretsReport, prepareSecretsForAllApps } from './secrets.js';
|
|
93
|
+
import { sniffAllApps } from './sniffer.js';
|
|
94
|
+
import {
|
|
95
|
+
type AppDbCredentials,
|
|
28
96
|
createEmptyState,
|
|
97
|
+
getAllAppCredentials,
|
|
29
98
|
getApplicationId,
|
|
30
99
|
getPostgresId,
|
|
31
100
|
getRedisId,
|
|
32
101
|
readStageState,
|
|
102
|
+
setAppCredentials,
|
|
33
103
|
setApplicationId,
|
|
34
104
|
setPostgresId,
|
|
35
105
|
setRedisId,
|
|
36
106
|
writeStageState,
|
|
37
107
|
} from './state.js';
|
|
38
|
-
import { orchestrateDns } from './dns/index.js';
|
|
39
|
-
import {
|
|
40
|
-
generatePublicUrlBuildArgs,
|
|
41
|
-
getPublicUrlArgNames,
|
|
42
|
-
isMainFrontendApp,
|
|
43
|
-
resolveHost,
|
|
44
|
-
} from './domain.js';
|
|
45
|
-
import { updateConfig } from './init';
|
|
46
|
-
import {
|
|
47
|
-
generateSecretsReport,
|
|
48
|
-
prepareSecretsForAllApps,
|
|
49
|
-
} from './secrets.js';
|
|
50
|
-
import { sniffAllApps } from './sniffer.js';
|
|
51
108
|
import type {
|
|
52
109
|
AppDeployResult,
|
|
53
110
|
DeployOptions,
|
|
@@ -149,6 +206,211 @@ export interface ProvisionServicesResult {
|
|
|
149
206
|
};
|
|
150
207
|
}
|
|
151
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Configuration for a database user to create during Dokploy deployment.
|
|
211
|
+
*
|
|
212
|
+
* @property name - The database username (typically matches the app name)
|
|
213
|
+
* @property password - The generated password for this user
|
|
214
|
+
* @property usePublicSchema - If true, user gets access to public schema (for api app).
|
|
215
|
+
* If false, user gets their own schema with search_path set.
|
|
216
|
+
*/
|
|
217
|
+
interface DbUserConfig {
|
|
218
|
+
name: string;
|
|
219
|
+
password: string;
|
|
220
|
+
usePublicSchema: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Wait for Postgres to be ready to accept connections.
|
|
225
|
+
*
|
|
226
|
+
* Polls the Postgres server until it accepts a connection or max retries reached.
|
|
227
|
+
* Used after enabling the external port to ensure the database is accessible
|
|
228
|
+
* before creating users.
|
|
229
|
+
*
|
|
230
|
+
* @param host - The Postgres server hostname
|
|
231
|
+
* @param port - The external port (typically 5432)
|
|
232
|
+
* @param user - Master database user (postgres)
|
|
233
|
+
* @param password - Master database password
|
|
234
|
+
* @param database - Database name to connect to
|
|
235
|
+
* @param maxRetries - Maximum number of connection attempts (default: 30)
|
|
236
|
+
* @param retryIntervalMs - Milliseconds between retries (default: 2000)
|
|
237
|
+
* @throws Error if Postgres is not ready after maxRetries
|
|
238
|
+
*/
|
|
239
|
+
async function waitForPostgres(
|
|
240
|
+
host: string,
|
|
241
|
+
port: number,
|
|
242
|
+
user: string,
|
|
243
|
+
password: string,
|
|
244
|
+
database: string,
|
|
245
|
+
maxRetries = 30,
|
|
246
|
+
retryIntervalMs = 2000,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
249
|
+
try {
|
|
250
|
+
const client = new PgClient({ host, port, user, password, database });
|
|
251
|
+
await client.connect();
|
|
252
|
+
await client.end();
|
|
253
|
+
return;
|
|
254
|
+
} catch {
|
|
255
|
+
if (i < maxRetries - 1) {
|
|
256
|
+
logger.log(` Waiting for Postgres... (${i + 1}/${maxRetries})`);
|
|
257
|
+
await new Promise((r) => setTimeout(r, retryIntervalMs));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`Postgres not ready after ${maxRetries} retries`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Initialize Postgres with per-app users and schemas.
|
|
266
|
+
*
|
|
267
|
+
* This function implements the same user/schema isolation pattern used in local
|
|
268
|
+
* dev mode (see docker/postgres/init.sh). It:
|
|
269
|
+
*
|
|
270
|
+
* 1. Temporarily enables the external Postgres port
|
|
271
|
+
* 2. Connects using master credentials
|
|
272
|
+
* 3. Creates each user with appropriate schema permissions
|
|
273
|
+
* 4. Disables the external port for security
|
|
274
|
+
*
|
|
275
|
+
* Schema assignment follows this pattern:
|
|
276
|
+
* - `api` app: Uses `public` schema (shared tables, migrations run here)
|
|
277
|
+
* - Other apps: Get their own schema with `search_path` configured
|
|
278
|
+
*
|
|
279
|
+
* @param api - The Dokploy API client
|
|
280
|
+
* @param postgres - The provisioned Postgres service details
|
|
281
|
+
* @param serverHostname - The Dokploy server hostname (for external connection)
|
|
282
|
+
* @param users - Array of users to create with their schema configuration
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* await initializePostgresUsers(api, postgres, 'dokploy.example.com', [
|
|
287
|
+
* { name: 'api', password: 'xxx', usePublicSchema: true },
|
|
288
|
+
* { name: 'auth', password: 'yyy', usePublicSchema: false },
|
|
289
|
+
* ]);
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
async function initializePostgresUsers(
|
|
293
|
+
api: DokployApi,
|
|
294
|
+
postgres: DokployPostgres,
|
|
295
|
+
serverHostname: string,
|
|
296
|
+
users: DbUserConfig[],
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
logger.log('\n🔧 Initializing database users...');
|
|
299
|
+
|
|
300
|
+
// Enable external port temporarily
|
|
301
|
+
const externalPort = 5432;
|
|
302
|
+
logger.log(` Enabling external port ${externalPort}...`);
|
|
303
|
+
await api.savePostgresExternalPort(postgres.postgresId, externalPort);
|
|
304
|
+
|
|
305
|
+
// Redeploy to apply external port change
|
|
306
|
+
await api.deployPostgres(postgres.postgresId);
|
|
307
|
+
|
|
308
|
+
// Wait for Postgres to be ready with external port
|
|
309
|
+
logger.log(
|
|
310
|
+
` Waiting for Postgres to be accessible at ${serverHostname}:${externalPort}...`,
|
|
311
|
+
);
|
|
312
|
+
await waitForPostgres(
|
|
313
|
+
serverHostname,
|
|
314
|
+
externalPort,
|
|
315
|
+
postgres.databaseUser,
|
|
316
|
+
postgres.databasePassword,
|
|
317
|
+
postgres.databaseName,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Connect and create users
|
|
321
|
+
const client = new PgClient({
|
|
322
|
+
host: serverHostname,
|
|
323
|
+
port: externalPort,
|
|
324
|
+
user: postgres.databaseUser,
|
|
325
|
+
password: postgres.databasePassword,
|
|
326
|
+
database: postgres.databaseName,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await client.connect();
|
|
331
|
+
|
|
332
|
+
for (const user of users) {
|
|
333
|
+
const schemaName = user.usePublicSchema ? 'public' : user.name;
|
|
334
|
+
logger.log(
|
|
335
|
+
` Creating user "${user.name}" with schema "${schemaName}"...`,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Create or update user (handles existing users)
|
|
339
|
+
await client.query(`
|
|
340
|
+
DO $$ BEGIN
|
|
341
|
+
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
342
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
343
|
+
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
344
|
+
END $$;
|
|
345
|
+
`);
|
|
346
|
+
|
|
347
|
+
if (user.usePublicSchema) {
|
|
348
|
+
// API uses public schema
|
|
349
|
+
await client.query(`
|
|
350
|
+
GRANT ALL ON SCHEMA public TO "${user.name}";
|
|
351
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${user.name}";
|
|
352
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${user.name}";
|
|
353
|
+
`);
|
|
354
|
+
} else {
|
|
355
|
+
// Other apps get their own schema
|
|
356
|
+
await client.query(`
|
|
357
|
+
CREATE SCHEMA IF NOT EXISTS "${schemaName}" AUTHORIZATION "${user.name}";
|
|
358
|
+
ALTER USER "${user.name}" SET search_path TO "${schemaName}";
|
|
359
|
+
GRANT USAGE ON SCHEMA "${schemaName}" TO "${user.name}";
|
|
360
|
+
GRANT ALL ON ALL TABLES IN SCHEMA "${schemaName}" TO "${user.name}";
|
|
361
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA "${schemaName}" GRANT ALL ON TABLES TO "${user.name}";
|
|
362
|
+
`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
logger.log(` ✓ User "${user.name}" configured`);
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
await client.end();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Disable external port for security
|
|
372
|
+
logger.log(' Disabling external port...');
|
|
373
|
+
await api.savePostgresExternalPort(postgres.postgresId, null);
|
|
374
|
+
await api.deployPostgres(postgres.postgresId);
|
|
375
|
+
|
|
376
|
+
logger.log(' ✓ Database users initialized');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get the server hostname from the Dokploy endpoint URL
|
|
381
|
+
*/
|
|
382
|
+
function getServerHostname(endpoint: string): string {
|
|
383
|
+
const url = new URL(endpoint);
|
|
384
|
+
return url.hostname;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Build per-app DATABASE_URL for internal Docker network communication.
|
|
389
|
+
*
|
|
390
|
+
* The URL uses the Postgres container name (postgresAppName) as the host,
|
|
391
|
+
* which resolves via Docker's internal DNS when apps are in the same network.
|
|
392
|
+
*
|
|
393
|
+
* @param appName - The database username (matches the app name)
|
|
394
|
+
* @param appPassword - The app's database password
|
|
395
|
+
* @param postgresAppName - The Postgres container/service name in Dokploy
|
|
396
|
+
* @param databaseName - The database name (typically the project name)
|
|
397
|
+
* @returns A properly encoded PostgreSQL connection URL
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```ts
|
|
401
|
+
* const url = buildPerAppDatabaseUrl('api', 'secret123', 'postgres-abc', 'myproject');
|
|
402
|
+
* // Returns: postgresql://api:secret123@postgres-abc:5432/myproject
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
function buildPerAppDatabaseUrl(
|
|
406
|
+
appName: string,
|
|
407
|
+
appPassword: string,
|
|
408
|
+
postgresAppName: string,
|
|
409
|
+
databaseName: string,
|
|
410
|
+
): string {
|
|
411
|
+
return `postgresql://${appName}:${encodeURIComponent(appPassword)}@${postgresAppName}:5432/${databaseName}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
152
414
|
/**
|
|
153
415
|
* Provision docker compose services in Dokploy
|
|
154
416
|
* @internal Exported for testing
|
|
@@ -157,7 +419,7 @@ export async function provisionServices(
|
|
|
157
419
|
api: DokployApi,
|
|
158
420
|
projectId: string,
|
|
159
421
|
environmentId: string | undefined,
|
|
160
|
-
|
|
422
|
+
projectName: string,
|
|
161
423
|
services?: DockerComposeServices,
|
|
162
424
|
existingServiceIds?: { postgresId?: string; redisId?: string },
|
|
163
425
|
): Promise<ProvisionServicesResult | undefined> {
|
|
@@ -193,14 +455,15 @@ export async function provisionServices(
|
|
|
193
455
|
|
|
194
456
|
// If not found by ID, use findOrCreate
|
|
195
457
|
if (!postgres) {
|
|
196
|
-
const { randomBytes } = await import('node:crypto');
|
|
197
458
|
const databasePassword = randomBytes(16).toString('hex');
|
|
459
|
+
// Use project name as database name (replace hyphens with underscores for PostgreSQL)
|
|
460
|
+
const databaseName = projectName.replace(/-/g, '_');
|
|
198
461
|
|
|
199
462
|
const result = await api.findOrCreatePostgres(
|
|
200
463
|
postgresName,
|
|
201
464
|
projectId,
|
|
202
465
|
environmentId,
|
|
203
|
-
{ databasePassword },
|
|
466
|
+
{ databaseName, databasePassword },
|
|
204
467
|
);
|
|
205
468
|
postgres = result.postgres;
|
|
206
469
|
created = result.created;
|
|
@@ -230,8 +493,7 @@ export async function provisionServices(
|
|
|
230
493
|
serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
|
|
231
494
|
logger.log(` ✓ Database credentials configured`);
|
|
232
495
|
} catch (error) {
|
|
233
|
-
const message =
|
|
234
|
-
error instanceof Error ? error.message : 'Unknown error';
|
|
496
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
235
497
|
logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
|
|
236
498
|
}
|
|
237
499
|
}
|
|
@@ -297,8 +559,7 @@ export async function provisionServices(
|
|
|
297
559
|
serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
|
|
298
560
|
logger.log(` ✓ Redis credentials configured`);
|
|
299
561
|
} catch (error) {
|
|
300
|
-
const message =
|
|
301
|
-
error instanceof Error ? error.message : 'Unknown error';
|
|
562
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
302
563
|
logger.log(` ⚠ Failed to provision Redis: ${message}`);
|
|
303
564
|
}
|
|
304
565
|
}
|
|
@@ -319,14 +580,6 @@ async function ensureDokploySetup(
|
|
|
319
580
|
): Promise<DokploySetupResult> {
|
|
320
581
|
logger.log('\n🔧 Checking Dokploy setup...');
|
|
321
582
|
|
|
322
|
-
// Read existing secrets to check if services are already configured
|
|
323
|
-
const { readStageSecrets } = await import('../secrets/storage');
|
|
324
|
-
const existingSecrets = await readStageSecrets(stage);
|
|
325
|
-
const existingUrls: { DATABASE_URL?: string; REDIS_URL?: string } = {
|
|
326
|
-
DATABASE_URL: existingSecrets?.urls?.DATABASE_URL,
|
|
327
|
-
REDIS_URL: existingSecrets?.urls?.REDIS_URL,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
583
|
// Step 1: Ensure we have Dokploy credentials
|
|
331
584
|
let creds = await getDokployCredentials();
|
|
332
585
|
|
|
@@ -714,7 +967,9 @@ export async function workspaceDeployCommand(
|
|
|
714
967
|
const stageSecrets = await readStageSecrets(stage, workspace.root);
|
|
715
968
|
if (!stageSecrets) {
|
|
716
969
|
logger.log(` ⚠️ No secrets found for stage "${stage}"`);
|
|
717
|
-
logger.log(
|
|
970
|
+
logger.log(
|
|
971
|
+
` Run "gkm secrets:init --stage ${stage}" to create secrets`,
|
|
972
|
+
);
|
|
718
973
|
}
|
|
719
974
|
|
|
720
975
|
// Sniff environment variables for all apps
|
|
@@ -729,7 +984,9 @@ export async function workspaceDeployCommand(
|
|
|
729
984
|
if (stageSecrets) {
|
|
730
985
|
const report = generateSecretsReport(encryptedSecrets, sniffedApps);
|
|
731
986
|
if (report.appsWithSecrets.length > 0) {
|
|
732
|
-
logger.log(
|
|
987
|
+
logger.log(
|
|
988
|
+
` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(', ')}`,
|
|
989
|
+
);
|
|
733
990
|
}
|
|
734
991
|
if (report.appsWithMissingSecrets.length > 0) {
|
|
735
992
|
for (const { appName, missing } of report.appsWithMissingSecrets) {
|
|
@@ -883,6 +1140,10 @@ export async function workspaceDeployCommand(
|
|
|
883
1140
|
redis: services.cache !== undefined && services.cache !== false,
|
|
884
1141
|
};
|
|
885
1142
|
|
|
1143
|
+
// Track provisioned postgres info for per-app DATABASE_URL
|
|
1144
|
+
let provisionedPostgres: DokployPostgres | null = null;
|
|
1145
|
+
let provisionedRedis: DokployRedis | null = null;
|
|
1146
|
+
|
|
886
1147
|
if (dockerServices.postgres || dockerServices.redis) {
|
|
887
1148
|
logger.log('\n🔧 Provisioning infrastructure services...');
|
|
888
1149
|
// Pass existing service IDs from state (prefer state over URL sniffing)
|
|
@@ -904,9 +1165,17 @@ export async function workspaceDeployCommand(
|
|
|
904
1165
|
if (provisionResult?.serviceIds) {
|
|
905
1166
|
if (provisionResult.serviceIds.postgresId) {
|
|
906
1167
|
setPostgresId(state, provisionResult.serviceIds.postgresId);
|
|
1168
|
+
// Fetch full postgres info for later use
|
|
1169
|
+
provisionedPostgres = await api.getPostgres(
|
|
1170
|
+
provisionResult.serviceIds.postgresId,
|
|
1171
|
+
);
|
|
907
1172
|
}
|
|
908
1173
|
if (provisionResult.serviceIds.redisId) {
|
|
909
1174
|
setRedisId(state, provisionResult.serviceIds.redisId);
|
|
1175
|
+
// Fetch full redis info for later use
|
|
1176
|
+
provisionedRedis = await api.getRedis(
|
|
1177
|
+
provisionResult.serviceIds.redisId,
|
|
1178
|
+
);
|
|
910
1179
|
}
|
|
911
1180
|
}
|
|
912
1181
|
}
|
|
@@ -921,6 +1190,60 @@ export async function workspaceDeployCommand(
|
|
|
921
1190
|
(name) => workspace.apps[name]!.type === 'frontend',
|
|
922
1191
|
);
|
|
923
1192
|
|
|
1193
|
+
// ==================================================================
|
|
1194
|
+
// Initialize per-app database users if Postgres is provisioned
|
|
1195
|
+
// ==================================================================
|
|
1196
|
+
const perAppDbCredentials = new Map<string, AppDbCredentials>();
|
|
1197
|
+
|
|
1198
|
+
if (provisionedPostgres && backendApps.length > 0) {
|
|
1199
|
+
// Determine which backend apps need DATABASE_URL
|
|
1200
|
+
const appsNeedingDb = backendApps.filter((appName) => {
|
|
1201
|
+
const requirements = sniffedApps.get(appName);
|
|
1202
|
+
return requirements?.requiredEnvVars.includes('DATABASE_URL');
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
if (appsNeedingDb.length > 0) {
|
|
1206
|
+
logger.log(`\n🔐 Setting up per-app database credentials...`);
|
|
1207
|
+
logger.log(` Apps needing DATABASE_URL: ${appsNeedingDb.join(', ')}`);
|
|
1208
|
+
|
|
1209
|
+
// Get or generate credentials for each app
|
|
1210
|
+
const existingCredentials = getAllAppCredentials(state);
|
|
1211
|
+
const usersToCreate: DbUserConfig[] = [];
|
|
1212
|
+
|
|
1213
|
+
for (const appName of appsNeedingDb) {
|
|
1214
|
+
let credentials = existingCredentials[appName];
|
|
1215
|
+
|
|
1216
|
+
if (credentials) {
|
|
1217
|
+
logger.log(` ${appName}: Using existing credentials from state`);
|
|
1218
|
+
} else {
|
|
1219
|
+
// Generate new credentials
|
|
1220
|
+
const password = randomBytes(16).toString('hex');
|
|
1221
|
+
credentials = { dbUser: appName, dbPassword: password };
|
|
1222
|
+
setAppCredentials(state, appName, credentials);
|
|
1223
|
+
logger.log(` ${appName}: Generated new credentials`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
perAppDbCredentials.set(appName, credentials);
|
|
1227
|
+
|
|
1228
|
+
// Always add to users to create (idempotent - will update if exists)
|
|
1229
|
+
usersToCreate.push({
|
|
1230
|
+
name: appName,
|
|
1231
|
+
password: credentials.dbPassword,
|
|
1232
|
+
usePublicSchema: appName === 'api', // API uses public schema, others get their own
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Initialize database users
|
|
1237
|
+
const serverHostname = getServerHostname(creds.endpoint);
|
|
1238
|
+
await initializePostgresUsers(
|
|
1239
|
+
api,
|
|
1240
|
+
provisionedPostgres,
|
|
1241
|
+
serverHostname,
|
|
1242
|
+
usersToCreate,
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
924
1247
|
// Track deployed app public URLs for frontend builds
|
|
925
1248
|
const publicUrls: Record<string, string> = {};
|
|
926
1249
|
const results: AppDeployResult[] = [];
|
|
@@ -930,6 +1253,23 @@ export async function workspaceDeployCommand(
|
|
|
930
1253
|
const appHostnames = new Map<string, string>(); // appName -> hostname
|
|
931
1254
|
const appDomainIds = new Map<string, string>(); // appName -> domainId
|
|
932
1255
|
|
|
1256
|
+
// ==================================================================
|
|
1257
|
+
// PRE-COMPUTE: Frontend URLs for BETTER_AUTH_TRUSTED_ORIGINS
|
|
1258
|
+
// ==================================================================
|
|
1259
|
+
const frontendUrls: string[] = [];
|
|
1260
|
+
for (const appName of frontendApps) {
|
|
1261
|
+
const app = workspace.apps[appName]!;
|
|
1262
|
+
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1263
|
+
const hostname = resolveHost(
|
|
1264
|
+
appName,
|
|
1265
|
+
app,
|
|
1266
|
+
stage,
|
|
1267
|
+
dokployConfig,
|
|
1268
|
+
isMainFrontend,
|
|
1269
|
+
);
|
|
1270
|
+
frontendUrls.push(`https://${hostname}`);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
933
1273
|
// ==================================================================
|
|
934
1274
|
// PHASE 1: Deploy backend apps (with encrypted secrets)
|
|
935
1275
|
// ==================================================================
|
|
@@ -953,7 +1293,9 @@ export async function workspaceDeployCommand(
|
|
|
953
1293
|
logger.log(` Using cached ID: ${cachedAppId}`);
|
|
954
1294
|
application = await api.getApplication(cachedAppId);
|
|
955
1295
|
if (application) {
|
|
956
|
-
logger.log(
|
|
1296
|
+
logger.log(
|
|
1297
|
+
` ✓ Application found: ${application.applicationId}`,
|
|
1298
|
+
);
|
|
957
1299
|
} else {
|
|
958
1300
|
logger.log(` ⚠ Cached ID invalid, will create new`);
|
|
959
1301
|
}
|
|
@@ -969,9 +1311,13 @@ export async function workspaceDeployCommand(
|
|
|
969
1311
|
application = result.application;
|
|
970
1312
|
|
|
971
1313
|
if (result.created) {
|
|
972
|
-
logger.log(
|
|
1314
|
+
logger.log(
|
|
1315
|
+
` Created application: ${application.applicationId}`,
|
|
1316
|
+
);
|
|
973
1317
|
} else {
|
|
974
|
-
logger.log(
|
|
1318
|
+
logger.log(
|
|
1319
|
+
` Found existing application: ${application.applicationId}`,
|
|
1320
|
+
);
|
|
975
1321
|
}
|
|
976
1322
|
}
|
|
977
1323
|
|
|
@@ -1010,12 +1356,63 @@ export async function workspaceDeployCommand(
|
|
|
1010
1356
|
buildArgs,
|
|
1011
1357
|
});
|
|
1012
1358
|
|
|
1013
|
-
//
|
|
1014
|
-
const
|
|
1359
|
+
// Compute hostname first (needed for BETTER_AUTH_URL)
|
|
1360
|
+
const backendHost = resolveHost(
|
|
1361
|
+
appName,
|
|
1362
|
+
app,
|
|
1363
|
+
stage,
|
|
1364
|
+
dokployConfig,
|
|
1365
|
+
false, // Backend apps are not main frontend
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
// Build env resolver context
|
|
1369
|
+
const envContext: EnvResolverContext = {
|
|
1370
|
+
app,
|
|
1371
|
+
appName,
|
|
1372
|
+
stage,
|
|
1373
|
+
state,
|
|
1374
|
+
appCredentials: perAppDbCredentials.get(appName),
|
|
1375
|
+
postgres: provisionedPostgres
|
|
1376
|
+
? {
|
|
1377
|
+
host: provisionedPostgres.appName,
|
|
1378
|
+
port: 5432,
|
|
1379
|
+
database: provisionedPostgres.databaseName,
|
|
1380
|
+
}
|
|
1381
|
+
: undefined,
|
|
1382
|
+
redis: provisionedRedis
|
|
1383
|
+
? {
|
|
1384
|
+
host: provisionedRedis.appName,
|
|
1385
|
+
port: 6379,
|
|
1386
|
+
password: provisionedRedis.databasePassword,
|
|
1387
|
+
}
|
|
1388
|
+
: undefined,
|
|
1389
|
+
appHostname: backendHost,
|
|
1390
|
+
frontendUrls,
|
|
1391
|
+
userSecrets: stageSecrets ?? undefined,
|
|
1392
|
+
masterKey: appSecrets?.masterKey,
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// Resolve all required environment variables
|
|
1396
|
+
const appRequirements = sniffedApps.get(appName);
|
|
1397
|
+
const requiredVars = appRequirements?.requiredEnvVars ?? [];
|
|
1398
|
+
const { valid, missing, resolved } = validateEnvVars(
|
|
1399
|
+
requiredVars,
|
|
1400
|
+
envContext,
|
|
1401
|
+
);
|
|
1015
1402
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1403
|
+
if (!valid) {
|
|
1404
|
+
throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Build env vars string for Dokploy
|
|
1408
|
+
const envVars: string[] = Object.entries(resolved).map(
|
|
1409
|
+
([key, value]) => `${key}=${value}`,
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
if (Object.keys(resolved).length > 0) {
|
|
1413
|
+
logger.log(
|
|
1414
|
+
` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
|
|
1415
|
+
);
|
|
1019
1416
|
}
|
|
1020
1417
|
|
|
1021
1418
|
// Configure and deploy application in Dokploy
|
|
@@ -1031,52 +1428,44 @@ export async function workspaceDeployCommand(
|
|
|
1031
1428
|
logger.log(` Deploying to Dokploy...`);
|
|
1032
1429
|
await api.deployApplication(application.applicationId);
|
|
1033
1430
|
|
|
1034
|
-
//
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
false, // Backend apps are not main frontend
|
|
1431
|
+
// Check if domain already exists (backendHost computed above)
|
|
1432
|
+
const existingDomains = await api.getDomainsByApplicationId(
|
|
1433
|
+
application.applicationId,
|
|
1434
|
+
);
|
|
1435
|
+
const existingDomain = existingDomains.find(
|
|
1436
|
+
(d) => d.host === backendHost,
|
|
1041
1437
|
);
|
|
1042
1438
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
host: backendHost,
|
|
1046
|
-
port: app.port,
|
|
1047
|
-
https: true,
|
|
1048
|
-
certificateType: 'letsencrypt',
|
|
1049
|
-
applicationId: application.applicationId,
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
// Track for DNS orchestration
|
|
1053
|
-
appHostnames.set(appName, backendHost);
|
|
1054
|
-
appDomainIds.set(appName, domain.domainId);
|
|
1055
|
-
|
|
1056
|
-
const publicUrl = `https://${backendHost}`;
|
|
1057
|
-
publicUrls[appName] = publicUrl;
|
|
1058
|
-
logger.log(` ✓ Domain: ${publicUrl}`);
|
|
1059
|
-
} catch (domainError) {
|
|
1060
|
-
// Domain might already exist, try to get the existing domain
|
|
1439
|
+
if (existingDomain) {
|
|
1440
|
+
// Domain already exists
|
|
1061
1441
|
appHostnames.set(appName, backendHost);
|
|
1062
|
-
|
|
1063
|
-
|
|
1442
|
+
appDomainIds.set(appName, existingDomain.domainId);
|
|
1443
|
+
publicUrls[appName] = `https://${backendHost}`;
|
|
1444
|
+
logger.log(` ✓ Domain: https://${backendHost} (existing)`);
|
|
1445
|
+
} else {
|
|
1446
|
+
// Create new domain
|
|
1064
1447
|
try {
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1448
|
+
const domain = await api.createDomain({
|
|
1449
|
+
host: backendHost,
|
|
1450
|
+
port: app.port,
|
|
1451
|
+
https: true,
|
|
1452
|
+
certificateType: 'letsencrypt',
|
|
1453
|
+
applicationId: application.applicationId,
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
appHostnames.set(appName, backendHost);
|
|
1457
|
+
appDomainIds.set(appName, domain.domainId);
|
|
1458
|
+
publicUrls[appName] = `https://${backendHost}`;
|
|
1459
|
+
logger.log(` ✓ Domain: https://${backendHost} (created)`);
|
|
1460
|
+
} catch (domainError) {
|
|
1461
|
+
const message =
|
|
1462
|
+
domainError instanceof Error
|
|
1463
|
+
? domainError.message
|
|
1464
|
+
: 'Unknown error';
|
|
1465
|
+
logger.log(` ⚠ Domain creation failed: ${message}`);
|
|
1466
|
+
appHostnames.set(appName, backendHost);
|
|
1467
|
+
publicUrls[appName] = `https://${backendHost}`;
|
|
1076
1468
|
}
|
|
1077
|
-
|
|
1078
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
1079
|
-
logger.log(` ℹ Domain already configured: https://${backendHost}`);
|
|
1080
1469
|
}
|
|
1081
1470
|
|
|
1082
1471
|
results.push({
|
|
@@ -1089,7 +1478,8 @@ export async function workspaceDeployCommand(
|
|
|
1089
1478
|
|
|
1090
1479
|
logger.log(` ✓ ${appName} deployed successfully`);
|
|
1091
1480
|
} catch (error) {
|
|
1092
|
-
const message =
|
|
1481
|
+
const message =
|
|
1482
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
1093
1483
|
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
1094
1484
|
|
|
1095
1485
|
results.push({
|
|
@@ -1130,7 +1520,9 @@ export async function workspaceDeployCommand(
|
|
|
1130
1520
|
logger.log(` Using cached ID: ${cachedAppId}`);
|
|
1131
1521
|
application = await api.getApplication(cachedAppId);
|
|
1132
1522
|
if (application) {
|
|
1133
|
-
logger.log(
|
|
1523
|
+
logger.log(
|
|
1524
|
+
` ✓ Application found: ${application.applicationId}`,
|
|
1525
|
+
);
|
|
1134
1526
|
} else {
|
|
1135
1527
|
logger.log(` ⚠ Cached ID invalid, will create new`);
|
|
1136
1528
|
}
|
|
@@ -1146,9 +1538,13 @@ export async function workspaceDeployCommand(
|
|
|
1146
1538
|
application = result.application;
|
|
1147
1539
|
|
|
1148
1540
|
if (result.created) {
|
|
1149
|
-
logger.log(
|
|
1541
|
+
logger.log(
|
|
1542
|
+
` Created application: ${application.applicationId}`,
|
|
1543
|
+
);
|
|
1150
1544
|
} else {
|
|
1151
|
-
logger.log(
|
|
1545
|
+
logger.log(
|
|
1546
|
+
` Found existing application: ${application.applicationId}`,
|
|
1547
|
+
);
|
|
1152
1548
|
}
|
|
1153
1549
|
}
|
|
1154
1550
|
|
|
@@ -1199,7 +1595,7 @@ export async function workspaceDeployCommand(
|
|
|
1199
1595
|
logger.log(` Deploying to Dokploy...`);
|
|
1200
1596
|
await api.deployApplication(application.applicationId);
|
|
1201
1597
|
|
|
1202
|
-
// Create domain for this app
|
|
1598
|
+
// Create or find domain for this app
|
|
1203
1599
|
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1204
1600
|
const frontendHost = resolveHost(
|
|
1205
1601
|
appName,
|
|
@@ -1209,43 +1605,44 @@ export async function workspaceDeployCommand(
|
|
|
1209
1605
|
isMainFrontend,
|
|
1210
1606
|
);
|
|
1211
1607
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
// Track for DNS orchestration
|
|
1222
|
-
appHostnames.set(appName, frontendHost);
|
|
1223
|
-
appDomainIds.set(appName, domain.domainId);
|
|
1608
|
+
// Check if domain already exists
|
|
1609
|
+
const existingFrontendDomains = await api.getDomainsByApplicationId(
|
|
1610
|
+
application.applicationId,
|
|
1611
|
+
);
|
|
1612
|
+
const existingFrontendDomain = existingFrontendDomains.find(
|
|
1613
|
+
(d) => d.host === frontendHost,
|
|
1614
|
+
);
|
|
1224
1615
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
logger.log(` ✓ Domain: ${publicUrl}`);
|
|
1228
|
-
} catch (domainError) {
|
|
1229
|
-
// Domain might already exist, try to get the existing domain
|
|
1616
|
+
if (existingFrontendDomain) {
|
|
1617
|
+
// Domain already exists
|
|
1230
1618
|
appHostnames.set(appName, frontendHost);
|
|
1231
|
-
|
|
1232
|
-
|
|
1619
|
+
appDomainIds.set(appName, existingFrontendDomain.domainId);
|
|
1620
|
+
publicUrls[appName] = `https://${frontendHost}`;
|
|
1621
|
+
logger.log(` ✓ Domain: https://${frontendHost} (existing)`);
|
|
1622
|
+
} else {
|
|
1623
|
+
// Create new domain
|
|
1233
1624
|
try {
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1625
|
+
const domain = await api.createDomain({
|
|
1626
|
+
host: frontendHost,
|
|
1627
|
+
port: app.port,
|
|
1628
|
+
https: true,
|
|
1629
|
+
certificateType: 'letsencrypt',
|
|
1630
|
+
applicationId: application.applicationId,
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
appHostnames.set(appName, frontendHost);
|
|
1634
|
+
appDomainIds.set(appName, domain.domainId);
|
|
1635
|
+
publicUrls[appName] = `https://${frontendHost}`;
|
|
1636
|
+
logger.log(` ✓ Domain: https://${frontendHost} (created)`);
|
|
1637
|
+
} catch (domainError) {
|
|
1638
|
+
const message =
|
|
1639
|
+
domainError instanceof Error
|
|
1640
|
+
? domainError.message
|
|
1641
|
+
: 'Unknown error';
|
|
1642
|
+
logger.log(` ⚠ Domain creation failed: ${message}`);
|
|
1643
|
+
appHostnames.set(appName, frontendHost);
|
|
1644
|
+
publicUrls[appName] = `https://${frontendHost}`;
|
|
1245
1645
|
}
|
|
1246
|
-
|
|
1247
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
1248
|
-
logger.log(` ℹ Domain already configured: https://${frontendHost}`);
|
|
1249
1646
|
}
|
|
1250
1647
|
|
|
1251
1648
|
results.push({
|
|
@@ -1258,7 +1655,8 @@ export async function workspaceDeployCommand(
|
|
|
1258
1655
|
|
|
1259
1656
|
logger.log(` ✓ ${appName} deployed successfully`);
|
|
1260
1657
|
} catch (error) {
|
|
1261
|
-
const message =
|
|
1658
|
+
const message =
|
|
1659
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
1262
1660
|
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
1263
1661
|
|
|
1264
1662
|
results.push({
|
|
@@ -1280,7 +1678,7 @@ export async function workspaceDeployCommand(
|
|
|
1280
1678
|
logger.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
|
|
1281
1679
|
|
|
1282
1680
|
// ==================================================================
|
|
1283
|
-
// DNS: Create DNS records and validate
|
|
1681
|
+
// DNS: Create DNS records, verify propagation, and validate for SSL
|
|
1284
1682
|
// ==================================================================
|
|
1285
1683
|
const dnsConfig = workspace.deploy.dns;
|
|
1286
1684
|
if (dnsConfig && appHostnames.size > 0) {
|
|
@@ -1290,6 +1688,14 @@ export async function workspaceDeployCommand(
|
|
|
1290
1688
|
creds.endpoint,
|
|
1291
1689
|
);
|
|
1292
1690
|
|
|
1691
|
+
// Verify DNS records resolve correctly (with state caching)
|
|
1692
|
+
if (dnsResult?.serverIp && appHostnames.size > 0) {
|
|
1693
|
+
await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
|
|
1694
|
+
|
|
1695
|
+
// Save state again to persist DNS verification results
|
|
1696
|
+
await writeStageState(workspace.root, stage, state);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1293
1699
|
// Validate domains to trigger SSL certificate generation
|
|
1294
1700
|
if (dnsResult?.success && appHostnames.size > 0) {
|
|
1295
1701
|
logger.log('\n🔒 Validating domains for SSL certificates...');
|