@geekmidas/cli 0.48.0 → 0.49.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/{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 +2392 -1888
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2389 -1885
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
- 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 +43 -0
- package/src/deploy/sniffer-hooks.ts +52 -0
- package/src/deploy/sniffer-loader.ts +23 -0
- package/src/deploy/sniffer-worker.ts +74 -0
- package/src/deploy/sniffer.ts +136 -14
- package/src/deploy/state.ts +162 -1
- package/src/init/versions.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- 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
|
@@ -185,7 +185,10 @@ export class HostingerApi {
|
|
|
185
185
|
* @param records - Records to validate
|
|
186
186
|
* @returns true if valid, throws if invalid
|
|
187
187
|
*/
|
|
188
|
-
async validateRecords(
|
|
188
|
+
async validateRecords(
|
|
189
|
+
domain: string,
|
|
190
|
+
records: DnsRecord[],
|
|
191
|
+
): Promise<boolean> {
|
|
189
192
|
await this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {
|
|
190
193
|
overwrite: false,
|
|
191
194
|
zone: records,
|
package/src/deploy/dns/index.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import { lookup } from 'node:dns/promises';
|
|
8
8
|
import { getHostingerToken, storeHostingerToken } from '../../auth/credentials';
|
|
9
9
|
import type { DnsConfig } from '../../workspace/types';
|
|
10
|
+
import {
|
|
11
|
+
type DokployStageState,
|
|
12
|
+
isDnsVerified,
|
|
13
|
+
setDnsVerification,
|
|
14
|
+
} from '../state';
|
|
10
15
|
import { HostingerApi } from './hostinger-api';
|
|
11
16
|
|
|
12
17
|
const logger = console;
|
|
@@ -105,7 +110,7 @@ export function printDnsRecordsTable(
|
|
|
105
110
|
records: RequiredDnsRecord[],
|
|
106
111
|
rootDomain: string,
|
|
107
112
|
): void {
|
|
108
|
-
logger.log(
|
|
113
|
+
logger.log(`\n 📋 DNS Records for ${rootDomain}:`);
|
|
109
114
|
logger.log(
|
|
110
115
|
' ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐',
|
|
111
116
|
);
|
|
@@ -396,3 +401,110 @@ export async function orchestrateDns(
|
|
|
396
401
|
serverIp,
|
|
397
402
|
};
|
|
398
403
|
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Result of DNS verification for a single hostname
|
|
407
|
+
*/
|
|
408
|
+
export interface DnsVerificationResult {
|
|
409
|
+
hostname: string;
|
|
410
|
+
appName: string;
|
|
411
|
+
verified: boolean;
|
|
412
|
+
resolvedIp?: string;
|
|
413
|
+
expectedIp: string;
|
|
414
|
+
error?: string;
|
|
415
|
+
skipped?: boolean; // True if already verified in state
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Verify DNS records resolve correctly after deployment.
|
|
420
|
+
*
|
|
421
|
+
* This function:
|
|
422
|
+
* 1. Checks state for previously verified hostnames (skips if already verified with same IP)
|
|
423
|
+
* 2. Attempts to resolve each hostname to an IP
|
|
424
|
+
* 3. Compares resolved IP with expected server IP
|
|
425
|
+
* 4. Updates state with verification results
|
|
426
|
+
*
|
|
427
|
+
* @param appHostnames - Map of app names to hostnames
|
|
428
|
+
* @param serverIp - Expected IP address the hostnames should resolve to
|
|
429
|
+
* @param state - Deploy state for caching verification results
|
|
430
|
+
* @returns Array of verification results
|
|
431
|
+
*/
|
|
432
|
+
export async function verifyDnsRecords(
|
|
433
|
+
appHostnames: Map<string, string>,
|
|
434
|
+
serverIp: string,
|
|
435
|
+
state: DokployStageState,
|
|
436
|
+
): Promise<DnsVerificationResult[]> {
|
|
437
|
+
const results: DnsVerificationResult[] = [];
|
|
438
|
+
|
|
439
|
+
logger.log('\n🔍 Verifying DNS records...');
|
|
440
|
+
|
|
441
|
+
for (const [appName, hostname] of appHostnames) {
|
|
442
|
+
// Check if already verified with same IP
|
|
443
|
+
if (isDnsVerified(state, hostname, serverIp)) {
|
|
444
|
+
logger.log(` ✓ ${hostname} (previously verified)`);
|
|
445
|
+
results.push({
|
|
446
|
+
hostname,
|
|
447
|
+
appName,
|
|
448
|
+
verified: true,
|
|
449
|
+
expectedIp: serverIp,
|
|
450
|
+
skipped: true,
|
|
451
|
+
});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Attempt to resolve hostname
|
|
456
|
+
try {
|
|
457
|
+
const resolvedIp = await resolveHostnameToIp(hostname);
|
|
458
|
+
|
|
459
|
+
if (resolvedIp === serverIp) {
|
|
460
|
+
// DNS verified successfully
|
|
461
|
+
setDnsVerification(state, hostname, serverIp);
|
|
462
|
+
logger.log(` ✓ ${hostname} → ${resolvedIp}`);
|
|
463
|
+
results.push({
|
|
464
|
+
hostname,
|
|
465
|
+
appName,
|
|
466
|
+
verified: true,
|
|
467
|
+
resolvedIp,
|
|
468
|
+
expectedIp: serverIp,
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
// DNS resolves but to wrong IP
|
|
472
|
+
logger.log(
|
|
473
|
+
` ⚠ ${hostname} resolves to ${resolvedIp}, expected ${serverIp}`,
|
|
474
|
+
);
|
|
475
|
+
results.push({
|
|
476
|
+
hostname,
|
|
477
|
+
appName,
|
|
478
|
+
verified: false,
|
|
479
|
+
resolvedIp,
|
|
480
|
+
expectedIp: serverIp,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// DNS resolution failed
|
|
485
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
486
|
+
logger.log(` ⚠ ${hostname} DNS not propagated (${message})`);
|
|
487
|
+
results.push({
|
|
488
|
+
hostname,
|
|
489
|
+
appName,
|
|
490
|
+
verified: false,
|
|
491
|
+
expectedIp: serverIp,
|
|
492
|
+
error: message,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Summary
|
|
498
|
+
const verified = results.filter((r) => r.verified).length;
|
|
499
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
500
|
+
const pending = results.filter((r) => !r.verified).length;
|
|
501
|
+
|
|
502
|
+
if (pending > 0) {
|
|
503
|
+
logger.log(`\n ${verified} verified, ${pending} pending propagation`);
|
|
504
|
+
logger.log(' DNS changes may take 5-30 minutes to propagate');
|
|
505
|
+
} else if (skipped > 0) {
|
|
506
|
+
logger.log(` ${verified} verified (${skipped} from cache)`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return results;
|
|
510
|
+
}
|
package/src/deploy/docker.ts
CHANGED
|
@@ -134,8 +134,7 @@ async function buildImage(
|
|
|
134
134
|
const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
|
|
135
135
|
|
|
136
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;
|
|
137
|
+
const buildCwd = lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
|
|
139
138
|
if (buildCwd !== cwd) {
|
|
140
139
|
logger.log(` Building from workspace root: ${buildCwd}`);
|
|
141
140
|
}
|
|
@@ -431,10 +431,10 @@ export class DokployApi {
|
|
|
431
431
|
environmentId,
|
|
432
432
|
appName:
|
|
433
433
|
options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
434
|
-
databaseName: options?.databaseName
|
|
434
|
+
databaseName: options?.databaseName,
|
|
435
435
|
databaseUser: options?.databaseUser ?? 'postgres',
|
|
436
436
|
databasePassword: options?.databasePassword,
|
|
437
|
-
dockerImage: options?.dockerImage ?? 'postgres:
|
|
437
|
+
dockerImage: options?.dockerImage ?? 'postgres:18',
|
|
438
438
|
description: options?.description ?? `Postgres database for ${name}`,
|
|
439
439
|
});
|
|
440
440
|
}
|
|
@@ -447,6 +447,7 @@ export class DokployApi {
|
|
|
447
447
|
projectId: string,
|
|
448
448
|
environmentId: string,
|
|
449
449
|
options?: {
|
|
450
|
+
databaseName?: string;
|
|
450
451
|
databasePassword?: string;
|
|
451
452
|
},
|
|
452
453
|
): Promise<{ postgres: DokployPostgres; created: boolean }> {
|
|
@@ -513,9 +514,7 @@ export class DokployApi {
|
|
|
513
514
|
*/
|
|
514
515
|
async listRedis(projectId: string): Promise<DokployRedis[]> {
|
|
515
516
|
try {
|
|
516
|
-
return await this.get<DokployRedis[]>(
|
|
517
|
-
`redis.all?projectId=${projectId}`,
|
|
518
|
-
);
|
|
517
|
+
return await this.get<DokployRedis[]>(`redis.all?projectId=${projectId}`);
|
|
519
518
|
} catch {
|
|
520
519
|
// Fallback: endpoint might not exist in older Dokploy versions
|
|
521
520
|
return [];
|
|
@@ -557,7 +556,7 @@ export class DokployApi {
|
|
|
557
556
|
appName:
|
|
558
557
|
options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
559
558
|
databasePassword: options?.databasePassword,
|
|
560
|
-
dockerImage: options?.dockerImage ?? 'redis:
|
|
559
|
+
dockerImage: options?.dockerImage ?? 'redis:8',
|
|
561
560
|
description: options?.description ?? `Redis instance for ${name}`,
|
|
562
561
|
});
|
|
563
562
|
}
|
|
@@ -577,7 +576,12 @@ export class DokployApi {
|
|
|
577
576
|
if (existing) {
|
|
578
577
|
return { redis: existing, created: false };
|
|
579
578
|
}
|
|
580
|
-
const redis = await this.createRedis(
|
|
579
|
+
const redis = await this.createRedis(
|
|
580
|
+
name,
|
|
581
|
+
projectId,
|
|
582
|
+
environmentId,
|
|
583
|
+
options,
|
|
584
|
+
);
|
|
581
585
|
return { redis, created: true };
|
|
582
586
|
}
|
|
583
587
|
|
|
@@ -679,8 +683,13 @@ export class DokployApi {
|
|
|
679
683
|
*
|
|
680
684
|
* @param domain - The domain hostname to validate (e.g., 'api.example.com')
|
|
681
685
|
*/
|
|
682
|
-
async validateDomain(
|
|
683
|
-
|
|
686
|
+
async validateDomain(
|
|
687
|
+
domain: string,
|
|
688
|
+
): Promise<{ isValid: boolean; resolvedIp: string }> {
|
|
689
|
+
return this.post<{ isValid: boolean; resolvedIp: string }>(
|
|
690
|
+
'domain.validateDomain',
|
|
691
|
+
{ domain },
|
|
692
|
+
);
|
|
684
693
|
}
|
|
685
694
|
|
|
686
695
|
/**
|
package/src/deploy/domain.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DokployWorkspaceConfig,
|
|
3
|
+
NormalizedAppConfig,
|
|
4
|
+
} from '../workspace/types.js';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Resolve the hostname for an app based on stage configuration.
|
|
@@ -119,7 +122,5 @@ export function generatePublicUrlBuildArgs(
|
|
|
119
122
|
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
|
|
120
123
|
*/
|
|
121
124
|
export function getPublicUrlArgNames(app: NormalizedAppConfig): string[] {
|
|
122
|
-
return app.dependencies.map(
|
|
123
|
-
(dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`,
|
|
124
|
-
);
|
|
125
|
+
return app.dependencies.map((dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`);
|
|
125
126
|
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Resolution for Dokploy Deployments
|
|
3
|
+
*
|
|
4
|
+
* Resolves sniffed environment variables to actual values during deployment.
|
|
5
|
+
* Auto-supports common variables like DATABASE_URL, REDIS_URL, BETTER_AUTH_*,
|
|
6
|
+
* and falls back to user-provided secrets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomBytes } from 'node:crypto';
|
|
10
|
+
import type { StageSecrets } from '../secrets/types';
|
|
11
|
+
import type { NormalizedAppConfig } from '../workspace/types';
|
|
12
|
+
import {
|
|
13
|
+
getGeneratedSecret,
|
|
14
|
+
setGeneratedSecret,
|
|
15
|
+
type AppDbCredentials,
|
|
16
|
+
type DokployStageState,
|
|
17
|
+
} from './state';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Context needed for environment variable resolution
|
|
21
|
+
*/
|
|
22
|
+
export interface EnvResolverContext {
|
|
23
|
+
/** The app being deployed */
|
|
24
|
+
app: NormalizedAppConfig;
|
|
25
|
+
/** The app name */
|
|
26
|
+
appName: string;
|
|
27
|
+
/** Deployment stage (production, staging, development) */
|
|
28
|
+
stage: string;
|
|
29
|
+
/** Deploy state (for persisting generated secrets) */
|
|
30
|
+
state: DokployStageState;
|
|
31
|
+
/** Per-app database credentials (if postgres is enabled) */
|
|
32
|
+
appCredentials?: AppDbCredentials;
|
|
33
|
+
/** Postgres connection info (internal hostname) */
|
|
34
|
+
postgres?: {
|
|
35
|
+
host: string;
|
|
36
|
+
port: number;
|
|
37
|
+
database: string;
|
|
38
|
+
};
|
|
39
|
+
/** Redis connection info (internal hostname) */
|
|
40
|
+
redis?: {
|
|
41
|
+
host: string;
|
|
42
|
+
port: number;
|
|
43
|
+
password?: string;
|
|
44
|
+
};
|
|
45
|
+
/** Public hostname for this app */
|
|
46
|
+
appHostname: string;
|
|
47
|
+
/** All frontend app URLs (for BETTER_AUTH_TRUSTED_ORIGINS) */
|
|
48
|
+
frontendUrls: string[];
|
|
49
|
+
/** User-provided secrets from secrets store */
|
|
50
|
+
userSecrets?: StageSecrets;
|
|
51
|
+
/** Master key for runtime decryption (optional) */
|
|
52
|
+
masterKey?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of environment variable resolution
|
|
57
|
+
*/
|
|
58
|
+
export interface EnvResolutionResult {
|
|
59
|
+
/** Successfully resolved environment variables */
|
|
60
|
+
resolved: Record<string, string>;
|
|
61
|
+
/** Environment variable names that could not be resolved */
|
|
62
|
+
missing: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Auto-supported environment variable names
|
|
67
|
+
*/
|
|
68
|
+
export const AUTO_SUPPORTED_VARS = [
|
|
69
|
+
'PORT',
|
|
70
|
+
'NODE_ENV',
|
|
71
|
+
'DATABASE_URL',
|
|
72
|
+
'REDIS_URL',
|
|
73
|
+
'BETTER_AUTH_URL',
|
|
74
|
+
'BETTER_AUTH_SECRET',
|
|
75
|
+
'BETTER_AUTH_TRUSTED_ORIGINS',
|
|
76
|
+
'GKM_MASTER_KEY',
|
|
77
|
+
] as const;
|
|
78
|
+
|
|
79
|
+
export type AutoSupportedVar = (typeof AUTO_SUPPORTED_VARS)[number];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a variable name is auto-supported
|
|
83
|
+
*/
|
|
84
|
+
export function isAutoSupportedVar(varName: string): varName is AutoSupportedVar {
|
|
85
|
+
return AUTO_SUPPORTED_VARS.includes(varName as AutoSupportedVar);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate a secure random secret (64 hex characters = 32 bytes)
|
|
90
|
+
*/
|
|
91
|
+
export function generateSecret(): string {
|
|
92
|
+
return randomBytes(32).toString('hex');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get or generate a secret for an app.
|
|
97
|
+
* If the secret already exists in state, returns it.
|
|
98
|
+
* Otherwise generates a new one and stores it.
|
|
99
|
+
*/
|
|
100
|
+
export function getOrGenerateSecret(
|
|
101
|
+
state: DokployStageState,
|
|
102
|
+
appName: string,
|
|
103
|
+
secretName: string,
|
|
104
|
+
): string {
|
|
105
|
+
// Check if already generated
|
|
106
|
+
const existing = getGeneratedSecret(state, appName, secretName);
|
|
107
|
+
if (existing) {
|
|
108
|
+
return existing;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Generate new secret
|
|
112
|
+
const generated = generateSecret();
|
|
113
|
+
|
|
114
|
+
// Store in state for persistence
|
|
115
|
+
setGeneratedSecret(state, appName, secretName, generated);
|
|
116
|
+
|
|
117
|
+
return generated;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build a DATABASE_URL for an app with per-app credentials
|
|
122
|
+
*/
|
|
123
|
+
export function buildDatabaseUrl(
|
|
124
|
+
credentials: AppDbCredentials,
|
|
125
|
+
postgres: { host: string; port: number; database: string },
|
|
126
|
+
): string {
|
|
127
|
+
const { dbUser, dbPassword } = credentials;
|
|
128
|
+
const { host, port, database } = postgres;
|
|
129
|
+
return `postgresql://${encodeURIComponent(dbUser)}:${encodeURIComponent(dbPassword)}@${host}:${port}/${database}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build a REDIS_URL
|
|
134
|
+
*/
|
|
135
|
+
export function buildRedisUrl(redis: {
|
|
136
|
+
host: string;
|
|
137
|
+
port: number;
|
|
138
|
+
password?: string;
|
|
139
|
+
}): string {
|
|
140
|
+
const { host, port, password } = redis;
|
|
141
|
+
if (password) {
|
|
142
|
+
return `redis://:${encodeURIComponent(password)}@${host}:${port}`;
|
|
143
|
+
}
|
|
144
|
+
return `redis://${host}:${port}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a single environment variable
|
|
149
|
+
*/
|
|
150
|
+
export function resolveEnvVar(
|
|
151
|
+
varName: string,
|
|
152
|
+
context: EnvResolverContext,
|
|
153
|
+
): string | undefined {
|
|
154
|
+
// Auto-supported variables
|
|
155
|
+
switch (varName) {
|
|
156
|
+
case 'PORT':
|
|
157
|
+
return String(context.app.port);
|
|
158
|
+
|
|
159
|
+
case 'NODE_ENV':
|
|
160
|
+
return context.stage === 'production' ? 'production' : 'development';
|
|
161
|
+
|
|
162
|
+
case 'DATABASE_URL':
|
|
163
|
+
if (context.appCredentials && context.postgres) {
|
|
164
|
+
return buildDatabaseUrl(context.appCredentials, context.postgres);
|
|
165
|
+
}
|
|
166
|
+
// Fall through to check user secrets
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'REDIS_URL':
|
|
170
|
+
if (context.redis) {
|
|
171
|
+
return buildRedisUrl(context.redis);
|
|
172
|
+
}
|
|
173
|
+
// Fall through to check user secrets
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'BETTER_AUTH_URL':
|
|
177
|
+
return `https://${context.appHostname}`;
|
|
178
|
+
|
|
179
|
+
case 'BETTER_AUTH_SECRET':
|
|
180
|
+
return getOrGenerateSecret(
|
|
181
|
+
context.state,
|
|
182
|
+
context.appName,
|
|
183
|
+
'BETTER_AUTH_SECRET',
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
case 'BETTER_AUTH_TRUSTED_ORIGINS':
|
|
187
|
+
if (context.frontendUrls.length > 0) {
|
|
188
|
+
return context.frontendUrls.join(',');
|
|
189
|
+
}
|
|
190
|
+
// Fall through to check user secrets
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case 'GKM_MASTER_KEY':
|
|
194
|
+
if (context.masterKey) {
|
|
195
|
+
return context.masterKey;
|
|
196
|
+
}
|
|
197
|
+
// Fall through to check user secrets
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check user-provided secrets
|
|
202
|
+
if (context.userSecrets) {
|
|
203
|
+
// Check custom secrets first
|
|
204
|
+
if (context.userSecrets.custom[varName]) {
|
|
205
|
+
return context.userSecrets.custom[varName];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check URLs (DATABASE_URL, REDIS_URL, RABBITMQ_URL)
|
|
209
|
+
if (varName in context.userSecrets.urls) {
|
|
210
|
+
return context.userSecrets.urls[varName as keyof typeof context.userSecrets.urls];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check service-specific vars
|
|
214
|
+
if (varName === 'POSTGRES_PASSWORD' && context.userSecrets.services.postgres) {
|
|
215
|
+
return context.userSecrets.services.postgres.password;
|
|
216
|
+
}
|
|
217
|
+
if (varName === 'REDIS_PASSWORD' && context.userSecrets.services.redis) {
|
|
218
|
+
return context.userSecrets.services.redis.password;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resolve all environment variables for an app
|
|
227
|
+
*/
|
|
228
|
+
export function resolveEnvVars(
|
|
229
|
+
requiredVars: string[],
|
|
230
|
+
context: EnvResolverContext,
|
|
231
|
+
): EnvResolutionResult {
|
|
232
|
+
const resolved: Record<string, string> = {};
|
|
233
|
+
const missing: string[] = [];
|
|
234
|
+
|
|
235
|
+
for (const varName of requiredVars) {
|
|
236
|
+
const value = resolveEnvVar(varName, context);
|
|
237
|
+
if (value !== undefined) {
|
|
238
|
+
resolved[varName] = value;
|
|
239
|
+
} else {
|
|
240
|
+
missing.push(varName);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { resolved, missing };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Format missing variables error message
|
|
249
|
+
*/
|
|
250
|
+
export function formatMissingVarsError(
|
|
251
|
+
appName: string,
|
|
252
|
+
missing: string[],
|
|
253
|
+
stage: string,
|
|
254
|
+
): string {
|
|
255
|
+
const varList = missing.map((v) => ` - ${v}`).join('\n');
|
|
256
|
+
return (
|
|
257
|
+
`Deployment failed: ${appName} is missing required environment variables:\n` +
|
|
258
|
+
`${varList}\n\n` +
|
|
259
|
+
`Add them with:\n` +
|
|
260
|
+
` gkm secrets:set <VAR_NAME> <value> --stage ${stage}\n\n` +
|
|
261
|
+
`Or add them to the app's requiredEnv in gkm.config.ts to have them auto-resolved.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate that all required environment variables can be resolved
|
|
267
|
+
*/
|
|
268
|
+
export function validateEnvVars(
|
|
269
|
+
requiredVars: string[],
|
|
270
|
+
context: EnvResolverContext,
|
|
271
|
+
): { valid: boolean; missing: string[]; resolved: Record<string, string> } {
|
|
272
|
+
const { resolved, missing } = resolveEnvVars(requiredVars, context);
|
|
273
|
+
return {
|
|
274
|
+
valid: missing.length === 0,
|
|
275
|
+
missing,
|
|
276
|
+
resolved,
|
|
277
|
+
};
|
|
278
|
+
}
|