@geekmidas/cli 0.47.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-CMWlWq7-.mjs → dokploy-api-94KzmTVf.mjs} +7 -7
- 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-BnX2OxyF.cjs → dokploy-api-YD8WCQfW.cjs} +7 -7
- package/dist/dokploy-api-YD8WCQfW.cjs.map +1 -0
- package/dist/index.cjs +2390 -1890
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2387 -1887
- 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 +9 -6
- package/src/deploy/dns/index.ts +115 -4
- package/src/deploy/docker.ts +1 -2
- package/src/deploy/dokploy-api.ts +20 -11
- package/src/deploy/domain.ts +5 -4
- package/src/deploy/env-resolver.ts +278 -0
- package/src/deploy/index.ts +534 -124
- 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/docker/templates.ts +10 -14
- package/src/init/versions.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-4a6h35VY.cjs +0 -3
- package/dist/dokploy-api-BnX2OxyF.cjs.map +0 -1
- package/dist/dokploy-api-CMWlWq7-.mjs.map +0 -1
- package/dist/dokploy-api-DQvi9iZa.mjs +0 -3
package/src/deploy/secrets.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { encryptSecrets } from '../secrets/encryption.js';
|
|
2
2
|
import { toEmbeddableSecrets } from '../secrets/storage.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
EmbeddableSecrets,
|
|
5
|
+
EncryptedPayload,
|
|
6
|
+
StageSecrets,
|
|
7
|
+
} from '../secrets/types.js';
|
|
4
8
|
import type { SniffedEnvironment } from './sniffer.js';
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -152,7 +156,8 @@ export function generateSecretsReport(
|
|
|
152
156
|
): SecretsReport {
|
|
153
157
|
const appsWithSecrets: string[] = [];
|
|
154
158
|
const appsWithoutSecrets: string[] = [];
|
|
155
|
-
const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> =
|
|
159
|
+
const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> =
|
|
160
|
+
[];
|
|
156
161
|
|
|
157
162
|
for (const [appName, sniffedEnv] of sniffedApps) {
|
|
158
163
|
if (sniffedEnv.requiredEnvVars.length === 0) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patched @geekmidas/envkit module for entry app sniffing.
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the SnifferEnvironmentParser as EnvironmentParser,
|
|
5
|
+
* allowing entry apps to be imported while capturing their env var usage.
|
|
6
|
+
*
|
|
7
|
+
* The actual sniffer instance is stored in globalThis.__envSniffer
|
|
8
|
+
* so the worker script can retrieve the captured variables.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
12
|
+
|
|
13
|
+
// Extend globalThis type for the sniffer instance
|
|
14
|
+
declare global {
|
|
15
|
+
// eslint-disable-next-line no-var
|
|
16
|
+
var __envSniffer: SnifferEnvironmentParser | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Create a shared sniffer instance that will be used by all imports
|
|
20
|
+
// This is stored globally so the worker script can access it
|
|
21
|
+
if (!globalThis.__envSniffer) {
|
|
22
|
+
globalThis.__envSniffer = new SnifferEnvironmentParser();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Patched EnvironmentParser that uses the global sniffer instance.
|
|
27
|
+
*
|
|
28
|
+
* This class wraps the global sniffer to maintain API compatibility
|
|
29
|
+
* with the real EnvironmentParser. The constructor accepts an env
|
|
30
|
+
* parameter for API compatibility but ignores it since we're sniffing.
|
|
31
|
+
*/
|
|
32
|
+
class PatchedEnvironmentParser {
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
create(builder: (get: any) => any) {
|
|
35
|
+
return globalThis.__envSniffer!.create(builder);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Export the patched parser as EnvironmentParser
|
|
40
|
+
export { PatchedEnvironmentParser as EnvironmentParser };
|
|
41
|
+
|
|
42
|
+
// Re-export other envkit exports that entry apps might use
|
|
43
|
+
export { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module loader hooks for entry app sniffing.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the resolve hook that intercepts '@geekmidas/envkit'
|
|
5
|
+
* imports and redirects them to the patched sniffer version.
|
|
6
|
+
*
|
|
7
|
+
* This file is registered via module.register() from sniffer-loader.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
// Resolve path to the patched envkit module
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const patchedEnvkitPath = join(__dirname, 'sniffer-envkit-patch.ts');
|
|
17
|
+
|
|
18
|
+
type ResolveContext = {
|
|
19
|
+
conditions: string[];
|
|
20
|
+
importAttributes: Record<string, string>;
|
|
21
|
+
parentURL?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ResolveResult = {
|
|
25
|
+
url: string;
|
|
26
|
+
shortCircuit?: boolean;
|
|
27
|
+
format?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type NextResolve = (
|
|
31
|
+
specifier: string,
|
|
32
|
+
context: ResolveContext,
|
|
33
|
+
) => Promise<ResolveResult>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve hook - intercepts module resolution for @geekmidas/envkit
|
|
37
|
+
*/
|
|
38
|
+
export async function resolve(
|
|
39
|
+
specifier: string,
|
|
40
|
+
context: ResolveContext,
|
|
41
|
+
nextResolve: NextResolve,
|
|
42
|
+
): Promise<ResolveResult> {
|
|
43
|
+
// Intercept @geekmidas/envkit imports
|
|
44
|
+
if (specifier === '@geekmidas/envkit') {
|
|
45
|
+
return {
|
|
46
|
+
url: `file://${patchedEnvkitPath}`,
|
|
47
|
+
shortCircuit: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return nextResolve(specifier, context);
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js module loader registration for entry app sniffing.
|
|
3
|
+
*
|
|
4
|
+
* This module registers a custom loader hook that intercepts imports of
|
|
5
|
+
* '@geekmidas/envkit' and replaces the EnvironmentParser with
|
|
6
|
+
* SnifferEnvironmentParser, allowing us to capture which environment
|
|
7
|
+
* variables an entry app accesses.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node --import tsx --import ./sniffer-loader.ts ./sniffer-worker.ts /path/to/entry.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { register } from 'node:module';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
|
|
17
|
+
// Resolve path to the loader hooks module
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const hooksPath = join(__dirname, 'sniffer-hooks.ts');
|
|
21
|
+
|
|
22
|
+
// Register the loader hooks
|
|
23
|
+
register(pathToFileURL(hooksPath).href, import.meta.url);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess worker for entry app sniffing.
|
|
3
|
+
*
|
|
4
|
+
* This script is executed in a subprocess with the sniffer-loader.ts
|
|
5
|
+
* registered, which intercepts @geekmidas/envkit imports.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node --import tsx --import ./sniffer-loader.ts ./sniffer-worker.ts /path/to/entry.ts
|
|
9
|
+
*
|
|
10
|
+
* Output (JSON to stdout):
|
|
11
|
+
* { "envVars": ["PORT", "DATABASE_URL", ...], "error": null }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
16
|
+
|
|
17
|
+
// Extend globalThis type for the sniffer instance
|
|
18
|
+
declare global {
|
|
19
|
+
// eslint-disable-next-line no-var
|
|
20
|
+
var __envSniffer: SnifferEnvironmentParser | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get the entry file path from command line args
|
|
24
|
+
const entryPath = process.argv[2] as string | undefined;
|
|
25
|
+
|
|
26
|
+
if (!entryPath) {
|
|
27
|
+
console.log(
|
|
28
|
+
JSON.stringify({ envVars: [], error: 'No entry file path provided' }),
|
|
29
|
+
);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// entryPath is guaranteed to be defined after the check above
|
|
34
|
+
const validEntryPath: string = entryPath;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Main sniffing function
|
|
38
|
+
*/
|
|
39
|
+
async function sniff(): Promise<void> {
|
|
40
|
+
let error: string | null = null;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Import the entry file - this triggers:
|
|
44
|
+
// 1. Entry imports config module
|
|
45
|
+
// 2. Config module imports @geekmidas/envkit (intercepted by loader)
|
|
46
|
+
// 3. Config creates EnvironmentParser (actually SnifferEnvironmentParser)
|
|
47
|
+
// 4. Config calls .create() and .parse()
|
|
48
|
+
// 5. Sniffer captures all accessed env var names
|
|
49
|
+
const entryUrl = pathToFileURL(validEntryPath).href;
|
|
50
|
+
await import(entryUrl);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Entry may fail due to missing env vars or other runtime issues.
|
|
53
|
+
// This is expected - we still capture the env vars that were accessed.
|
|
54
|
+
error = e instanceof Error ? e.message : String(e);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Retrieve captured env vars from the global sniffer
|
|
58
|
+
const sniffer = globalThis.__envSniffer;
|
|
59
|
+
const envVars = sniffer ? sniffer.getEnvironmentVariables() : [];
|
|
60
|
+
|
|
61
|
+
// Output result as JSON
|
|
62
|
+
console.log(JSON.stringify({ envVars, error }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle unhandled rejections (fire-and-forget promises)
|
|
66
|
+
process.on('unhandledRejection', () => {
|
|
67
|
+
// Silently ignore - we only care about env var capture
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Run the sniffer
|
|
71
|
+
sniff().catch((e) => {
|
|
72
|
+
console.log(JSON.stringify({ envVars: [], error: e.message || String(e) }));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
4
|
import type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
4
5
|
import type { NormalizedAppConfig } from '../workspace/types.js';
|
|
5
6
|
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
6
10
|
// Re-export SniffResult for consumers
|
|
7
11
|
export type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
8
12
|
|
|
@@ -25,11 +29,12 @@ export interface SniffAppOptions {
|
|
|
25
29
|
/**
|
|
26
30
|
* Get required environment variables for an app.
|
|
27
31
|
*
|
|
28
|
-
* Detection strategy:
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* Detection strategy (in order):
|
|
33
|
+
* 1. Frontend apps: Returns empty (no server secrets)
|
|
34
|
+
* 2. Apps with `requiredEnv`: Uses explicit list from config
|
|
35
|
+
* 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
|
|
36
|
+
* 4. Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
|
|
37
|
+
* 5. Apps with neither: Returns empty
|
|
33
38
|
*
|
|
34
39
|
* This function handles "fire and forget" async operations gracefully,
|
|
35
40
|
* capturing errors and unhandled rejections without failing the build.
|
|
@@ -48,17 +53,30 @@ export async function sniffAppEnvironment(
|
|
|
48
53
|
): Promise<SniffedEnvironment> {
|
|
49
54
|
const { logWarnings = true } = options;
|
|
50
55
|
|
|
51
|
-
// Frontend apps don't have server-side secrets
|
|
56
|
+
// 1. Frontend apps don't have server-side secrets
|
|
52
57
|
if (app.type === 'frontend') {
|
|
53
58
|
return { appName, requiredEnvVars: [] };
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
// Entry-based apps with explicit env list
|
|
61
|
+
// 2. Entry-based apps with explicit env list
|
|
57
62
|
if (app.requiredEnv && app.requiredEnv.length > 0) {
|
|
58
63
|
return { appName, requiredEnvVars: [...app.requiredEnv] };
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
//
|
|
66
|
+
// 3. Entry apps - import entry file in subprocess to trigger config.parse()
|
|
67
|
+
if (app.entry) {
|
|
68
|
+
const result = await sniffEntryFile(app.entry, app.path, workspacePath);
|
|
69
|
+
|
|
70
|
+
if (logWarnings && result.error) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { appName, requiredEnvVars: result.envVars };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Apps with envParser - run sniffer to detect env var usage
|
|
62
80
|
if (app.envParser) {
|
|
63
81
|
const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
|
|
64
82
|
|
|
@@ -79,10 +97,107 @@ export async function sniffAppEnvironment(
|
|
|
79
97
|
return { appName, requiredEnvVars: result.envVars };
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
// No env detection method available
|
|
100
|
+
// 5. No env detection method available
|
|
83
101
|
return { appName, requiredEnvVars: [] };
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Result from sniffing an entry file.
|
|
106
|
+
*/
|
|
107
|
+
interface EntrySniffResult {
|
|
108
|
+
envVars: string[];
|
|
109
|
+
error?: Error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sniff an entry file by importing it in a subprocess.
|
|
114
|
+
*
|
|
115
|
+
* Entry apps call `config.parse()` at module load time. To capture which
|
|
116
|
+
* env vars are accessed, we:
|
|
117
|
+
* 1. Spawn a subprocess with a module loader hook
|
|
118
|
+
* 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
|
|
119
|
+
* with SnifferEnvironmentParser
|
|
120
|
+
* 3. Import the entry file (triggers config.parse())
|
|
121
|
+
* 4. Capture and return the accessed env var names
|
|
122
|
+
*
|
|
123
|
+
* This approach provides process isolation - each app is sniffed in its own
|
|
124
|
+
* subprocess, preventing module cache pollution.
|
|
125
|
+
*
|
|
126
|
+
* @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
|
|
127
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
|
|
128
|
+
* @param workspacePath - Absolute path to workspace root
|
|
129
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
130
|
+
*/
|
|
131
|
+
async function sniffEntryFile(
|
|
132
|
+
entryPath: string,
|
|
133
|
+
appPath: string,
|
|
134
|
+
workspacePath: string,
|
|
135
|
+
): Promise<EntrySniffResult> {
|
|
136
|
+
const fullEntryPath = resolve(workspacePath, appPath, entryPath);
|
|
137
|
+
const loaderPath = resolve(__dirname, 'sniffer-loader.ts');
|
|
138
|
+
const workerPath = resolve(__dirname, 'sniffer-worker.ts');
|
|
139
|
+
|
|
140
|
+
return new Promise((resolvePromise) => {
|
|
141
|
+
const child = spawn(
|
|
142
|
+
'node',
|
|
143
|
+
['--import', loaderPath, workerPath, fullEntryPath],
|
|
144
|
+
{
|
|
145
|
+
cwd: resolve(workspacePath, appPath),
|
|
146
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
147
|
+
env: {
|
|
148
|
+
...process.env,
|
|
149
|
+
// Ensure tsx is available for TypeScript entry files
|
|
150
|
+
NODE_OPTIONS: '--import tsx',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let stdout = '';
|
|
156
|
+
let stderr = '';
|
|
157
|
+
|
|
158
|
+
child.stdout.on('data', (data) => {
|
|
159
|
+
stdout += data.toString();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
child.stderr.on('data', (data) => {
|
|
163
|
+
stderr += data.toString();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
child.on('close', (code) => {
|
|
167
|
+
// Try to parse the JSON output from the worker
|
|
168
|
+
try {
|
|
169
|
+
// Find the last JSON object in stdout (worker may emit other output)
|
|
170
|
+
const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
|
|
171
|
+
if (jsonMatch) {
|
|
172
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
173
|
+
resolvePromise({
|
|
174
|
+
envVars: result.envVars || [],
|
|
175
|
+
error: result.error ? new Error(result.error) : undefined,
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// JSON parse failed
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// If we couldn't parse the output, return empty with error info
|
|
184
|
+
resolvePromise({
|
|
185
|
+
envVars: [],
|
|
186
|
+
error: new Error(
|
|
187
|
+
`Failed to sniff entry file (exit code ${code}): ${stderr || stdout || 'No output'}`,
|
|
188
|
+
),
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
child.on('error', (err) => {
|
|
193
|
+
resolvePromise({
|
|
194
|
+
envVars: [],
|
|
195
|
+
error: err,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
86
201
|
/**
|
|
87
202
|
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
88
203
|
* which environment variables it accesses.
|
|
@@ -118,7 +233,9 @@ async function sniffEnvParser(
|
|
|
118
233
|
sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
|
|
119
234
|
} catch (error) {
|
|
120
235
|
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
-
console.warn(
|
|
236
|
+
console.warn(
|
|
237
|
+
`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`,
|
|
238
|
+
);
|
|
122
239
|
return { envVars: [], unhandledRejections: [] };
|
|
123
240
|
}
|
|
124
241
|
|
|
@@ -169,7 +286,12 @@ export async function sniffAllApps(
|
|
|
169
286
|
const results = new Map<string, SniffedEnvironment>();
|
|
170
287
|
|
|
171
288
|
for (const [appName, app] of Object.entries(apps)) {
|
|
172
|
-
const sniffed = await sniffAppEnvironment(
|
|
289
|
+
const sniffed = await sniffAppEnvironment(
|
|
290
|
+
app,
|
|
291
|
+
appName,
|
|
292
|
+
workspacePath,
|
|
293
|
+
options,
|
|
294
|
+
);
|
|
173
295
|
results.set(appName, sniffed);
|
|
174
296
|
}
|
|
175
297
|
|
|
@@ -177,4 +299,4 @@ export async function sniffAllApps(
|
|
|
177
299
|
}
|
|
178
300
|
|
|
179
301
|
// Export for testing
|
|
180
|
-
export { sniffEnvParser as _sniffEnvParser };
|
|
302
|
+
export { sniffEnvParser as _sniffEnvParser, sniffEntryFile as _sniffEntryFile };
|
package/src/deploy/state.ts
CHANGED
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Per-app database credentials
|
|
13
|
+
*/
|
|
14
|
+
export interface AppDbCredentials {
|
|
15
|
+
dbUser: string;
|
|
16
|
+
dbPassword: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DNS verification record for a hostname
|
|
21
|
+
*/
|
|
22
|
+
export interface DnsVerificationRecord {
|
|
23
|
+
serverIp: string;
|
|
24
|
+
verifiedAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
/**
|
|
12
28
|
* State for a single stage deployment
|
|
13
29
|
*/
|
|
@@ -20,6 +36,12 @@ export interface DokployStageState {
|
|
|
20
36
|
postgresId?: string;
|
|
21
37
|
redisId?: string;
|
|
22
38
|
};
|
|
39
|
+
/** Per-app database credentials for reuse on subsequent deploys */
|
|
40
|
+
appCredentials?: Record<string, AppDbCredentials>;
|
|
41
|
+
/** Auto-generated secrets per app (e.g., BETTER_AUTH_SECRET) */
|
|
42
|
+
generatedSecrets?: Record<string, Record<string, string>>;
|
|
43
|
+
/** DNS verification state per hostname */
|
|
44
|
+
dnsVerified?: Record<string, DnsVerificationRecord>;
|
|
23
45
|
lastDeployedAt: string;
|
|
24
46
|
}
|
|
25
47
|
|
|
@@ -134,7 +156,9 @@ export function setPostgresId(
|
|
|
134
156
|
/**
|
|
135
157
|
* Get redis ID from state
|
|
136
158
|
*/
|
|
137
|
-
export function getRedisId(
|
|
159
|
+
export function getRedisId(
|
|
160
|
+
state: DokployStageState | null,
|
|
161
|
+
): string | undefined {
|
|
138
162
|
return state?.services.redisId;
|
|
139
163
|
}
|
|
140
164
|
|
|
@@ -144,3 +168,140 @@ export function getRedisId(state: DokployStageState | null): string | undefined
|
|
|
144
168
|
export function setRedisId(state: DokployStageState, redisId: string): void {
|
|
145
169
|
state.services.redisId = redisId;
|
|
146
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get app credentials from state
|
|
174
|
+
*/
|
|
175
|
+
export function getAppCredentials(
|
|
176
|
+
state: DokployStageState | null,
|
|
177
|
+
appName: string,
|
|
178
|
+
): AppDbCredentials | undefined {
|
|
179
|
+
return state?.appCredentials?.[appName];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set app credentials in state (mutates state)
|
|
184
|
+
*/
|
|
185
|
+
export function setAppCredentials(
|
|
186
|
+
state: DokployStageState,
|
|
187
|
+
appName: string,
|
|
188
|
+
credentials: AppDbCredentials,
|
|
189
|
+
): void {
|
|
190
|
+
if (!state.appCredentials) {
|
|
191
|
+
state.appCredentials = {};
|
|
192
|
+
}
|
|
193
|
+
state.appCredentials[appName] = credentials;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get all app credentials from state
|
|
198
|
+
*/
|
|
199
|
+
export function getAllAppCredentials(
|
|
200
|
+
state: DokployStageState | null,
|
|
201
|
+
): Record<string, AppDbCredentials> {
|
|
202
|
+
return state?.appCredentials ?? {};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// Generated Secrets
|
|
207
|
+
// ============================================================================
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get a generated secret for an app
|
|
211
|
+
*/
|
|
212
|
+
export function getGeneratedSecret(
|
|
213
|
+
state: DokployStageState | null,
|
|
214
|
+
appName: string,
|
|
215
|
+
secretName: string,
|
|
216
|
+
): string | undefined {
|
|
217
|
+
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Set a generated secret for an app (mutates state)
|
|
222
|
+
*/
|
|
223
|
+
export function setGeneratedSecret(
|
|
224
|
+
state: DokployStageState,
|
|
225
|
+
appName: string,
|
|
226
|
+
secretName: string,
|
|
227
|
+
value: string,
|
|
228
|
+
): void {
|
|
229
|
+
if (!state.generatedSecrets) {
|
|
230
|
+
state.generatedSecrets = {};
|
|
231
|
+
}
|
|
232
|
+
if (!state.generatedSecrets[appName]) {
|
|
233
|
+
state.generatedSecrets[appName] = {};
|
|
234
|
+
}
|
|
235
|
+
state.generatedSecrets[appName][secretName] = value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get all generated secrets for an app
|
|
240
|
+
*/
|
|
241
|
+
export function getAppGeneratedSecrets(
|
|
242
|
+
state: DokployStageState | null,
|
|
243
|
+
appName: string,
|
|
244
|
+
): Record<string, string> {
|
|
245
|
+
return state?.generatedSecrets?.[appName] ?? {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get all generated secrets from state
|
|
250
|
+
*/
|
|
251
|
+
export function getAllGeneratedSecrets(
|
|
252
|
+
state: DokployStageState | null,
|
|
253
|
+
): Record<string, Record<string, string>> {
|
|
254
|
+
return state?.generatedSecrets ?? {};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// DNS Verification
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get DNS verification record for a hostname
|
|
263
|
+
*/
|
|
264
|
+
export function getDnsVerification(
|
|
265
|
+
state: DokployStageState | null,
|
|
266
|
+
hostname: string,
|
|
267
|
+
): DnsVerificationRecord | undefined {
|
|
268
|
+
return state?.dnsVerified?.[hostname];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Set DNS verification record for a hostname (mutates state)
|
|
273
|
+
*/
|
|
274
|
+
export function setDnsVerification(
|
|
275
|
+
state: DokployStageState,
|
|
276
|
+
hostname: string,
|
|
277
|
+
serverIp: string,
|
|
278
|
+
): void {
|
|
279
|
+
if (!state.dnsVerified) {
|
|
280
|
+
state.dnsVerified = {};
|
|
281
|
+
}
|
|
282
|
+
state.dnsVerified[hostname] = {
|
|
283
|
+
serverIp,
|
|
284
|
+
verifiedAt: new Date().toISOString(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a hostname is already verified with the given IP
|
|
290
|
+
*/
|
|
291
|
+
export function isDnsVerified(
|
|
292
|
+
state: DokployStageState | null,
|
|
293
|
+
hostname: string,
|
|
294
|
+
serverIp: string,
|
|
295
|
+
): boolean {
|
|
296
|
+
const record = state?.dnsVerified?.[hostname];
|
|
297
|
+
return record?.serverIp === serverIp;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get all DNS verification records from state
|
|
302
|
+
*/
|
|
303
|
+
export function getAllDnsVerifications(
|
|
304
|
+
state: DokployStageState | null,
|
|
305
|
+
): Record<string, DnsVerificationRecord> {
|
|
306
|
+
return state?.dnsVerified ?? {};
|
|
307
|
+
}
|
package/src/docker/templates.ts
CHANGED
|
@@ -321,8 +321,8 @@ ENV NODE_ENV=production
|
|
|
321
321
|
ENV PORT=${port}
|
|
322
322
|
|
|
323
323
|
# Health check
|
|
324
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
325
|
-
CMD wget -
|
|
324
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
325
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
326
326
|
|
|
327
327
|
# Switch to non-root user
|
|
328
328
|
USER hono
|
|
@@ -413,8 +413,8 @@ COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
|
413
413
|
ENV NODE_ENV=production
|
|
414
414
|
ENV PORT=${port}
|
|
415
415
|
|
|
416
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
417
|
-
CMD wget -
|
|
416
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
417
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
418
418
|
|
|
419
419
|
USER hono
|
|
420
420
|
|
|
@@ -452,8 +452,8 @@ ENV NODE_ENV=production
|
|
|
452
452
|
ENV PORT=${port}
|
|
453
453
|
|
|
454
454
|
# Health check
|
|
455
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
456
|
-
CMD wget -
|
|
455
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
456
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
457
457
|
|
|
458
458
|
# Switch to non-root user
|
|
459
459
|
USER hono
|
|
@@ -682,10 +682,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
|
682
682
|
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
683
683
|
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
684
684
|
|
|
685
|
-
# Health check
|
|
686
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
|
|
687
|
-
CMD wget -q --spider http://localhost:${port}/ || exit 1
|
|
688
|
-
|
|
689
685
|
USER nextjs
|
|
690
686
|
|
|
691
687
|
EXPOSE ${port}
|
|
@@ -790,8 +786,8 @@ COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.
|
|
|
790
786
|
ENV NODE_ENV=production
|
|
791
787
|
ENV PORT=${port}
|
|
792
788
|
|
|
793
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
794
|
-
CMD wget -
|
|
789
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
790
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
795
791
|
|
|
796
792
|
USER hono
|
|
797
793
|
|
|
@@ -930,8 +926,8 @@ COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
|
|
|
930
926
|
ENV NODE_ENV=production
|
|
931
927
|
ENV PORT=${port}
|
|
932
928
|
|
|
933
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
934
|
-
CMD wget -
|
|
929
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
930
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
935
931
|
|
|
936
932
|
USER app
|
|
937
933
|
|
package/src/init/versions.ts
CHANGED
|
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
32
32
|
'@geekmidas/cli': CLI_VERSION,
|
|
33
33
|
'@geekmidas/client': '~0.5.0',
|
|
34
34
|
'@geekmidas/cloud': '~0.2.0',
|
|
35
|
-
'@geekmidas/constructs': '~0.
|
|
35
|
+
'@geekmidas/constructs': '~0.8.0',
|
|
36
36
|
'@geekmidas/db': '~0.3.0',
|
|
37
37
|
'@geekmidas/emailkit': '~0.2.0',
|
|
38
|
-
'@geekmidas/envkit': '~0.
|
|
38
|
+
'@geekmidas/envkit': '~0.7.0',
|
|
39
39
|
'@geekmidas/errors': '~0.1.0',
|
|
40
40
|
'@geekmidas/events': '~0.2.0',
|
|
41
41
|
'@geekmidas/logger': '~0.4.0',
|
|
@@ -44,7 +44,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
44
44
|
'@geekmidas/services': '~0.2.0',
|
|
45
45
|
'@geekmidas/storage': '~0.1.0',
|
|
46
46
|
'@geekmidas/studio': '~0.4.0',
|
|
47
|
-
'@geekmidas/telescope': '~0.
|
|
47
|
+
'@geekmidas/telescope': '~0.6.0',
|
|
48
48
|
'@geekmidas/testkit': '~0.6.0',
|
|
49
49
|
};
|
|
50
50
|
|