@akash-chowdhury-24/deployhub 1.0.16 → 2.0.2
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/README.md +264 -59
- package/package.json +4 -3
- package/src/commands/doctor.js +168 -170
- package/src/commands/init.js +80 -371
- package/src/commands/rollback.js +1 -1
- package/src/core/config.js +17 -13
- package/src/core/stages.js +0 -11
- package/src/deployment/deployment-env.js +625 -0
- package/src/deployment/index.js +0 -72
- package/src/deployment/init-helpers.js +389 -0
- package/src/deployment/init-prompts.js +432 -0
- package/src/deployment/providers/azure-vm.js +85 -2
- package/src/deployment/providers/docker.js +101 -8
- package/src/deployment/providers/ec2.js +88 -2
- package/src/deployment/providers/gcp-vm.js +89 -2
- package/src/deployment/providers/kubernetes.js +104 -7
- package/src/deployment/providers/ssh.js +21 -4
- package/src/utils/github-actions.js +28 -131
- package/src/utils/rollback/engine.js +69 -0
- package/src/deployment/providers/platforms/_shared.js +0 -167
- package/src/deployment/providers/platforms/aws-amplify.js +0 -367
- package/src/deployment/providers/platforms/azure-static-web-apps.js +0 -68
- package/src/deployment/providers/platforms/cloudflare-pages.js +0 -103
- package/src/deployment/providers/platforms/firebase-app-hosting.js +0 -95
- package/src/deployment/providers/platforms/firebase-hosting.js +0 -99
- package/src/deployment/providers/platforms/index.js +0 -44
- package/src/deployment/providers/platforms/netlify.js +0 -102
- package/src/deployment/providers/platforms/vercel.js +0 -92
- package/src/rollback/engine.js +0 -102
- package/src/utils/firebase-config-generator.js +0 -35
- package/src/utils/init-platform.js +0 -236
- package/src/utils/platform-env.js +0 -132
package/src/commands/doctor.js
CHANGED
|
@@ -7,11 +7,13 @@ import { loadConfig, loadEnv } from '../core/config.js';
|
|
|
7
7
|
import { testProvider } from '../storage/index.js';
|
|
8
8
|
import { getDeploymentProvider } from '../deployment/index.js';
|
|
9
9
|
import { PROVIDER_ENV_MAP } from '../utils/github-actions.js';
|
|
10
|
-
import { PLATFORM_ENV_MAP, PLATFORM_CLI_MAP } from '../utils/platform-env.js';
|
|
11
|
-
import { createPlatformProvider } from '../deployment/providers/platforms/index.js';
|
|
12
|
-
import { isCliInstalled } from '../deployment/providers/platforms/_shared.js';
|
|
13
10
|
import { printDoctorFooter } from '../utils/author.js';
|
|
14
11
|
import { createLocalProvider } from '../storage/providers/local.js';
|
|
12
|
+
import {
|
|
13
|
+
getDeploymentEnvKeys,
|
|
14
|
+
getDeploymentSecretKeys,
|
|
15
|
+
} from '../deployment/deployment-env.js';
|
|
16
|
+
import { testSshConnectivity, validateSshKeyForDoctor, testSshHostReachability } from '../deployment/init-helpers.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* @typedef {{ name: string, pass: boolean, message: string }} CheckResult
|
|
@@ -51,14 +53,165 @@ function resolveBackendFramework(config) {
|
|
|
51
53
|
return config.backend?.framework || config.framework || 'express';
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/** @type {Set<string>} */
|
|
57
|
+
const SSH_DEPLOY_TYPES = new Set(['ssh', 'ec2', 'azure-vm', 'gcp-vm']);
|
|
58
|
+
|
|
54
59
|
/**
|
|
55
60
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
56
61
|
* @param {string} envName
|
|
62
|
+
* @param {Record<string, unknown>} envConfig
|
|
57
63
|
* @returns {Promise<CheckResult[]>}
|
|
58
64
|
*/
|
|
59
|
-
async function
|
|
65
|
+
async function runDeploymentChecks(config, envName, envConfig) {
|
|
66
|
+
const deployType = envConfig.type;
|
|
67
|
+
if (!deployType || typeof deployType !== 'string') return [];
|
|
68
|
+
|
|
69
|
+
/** @type {CheckResult[]} */
|
|
70
|
+
const checks = [];
|
|
71
|
+
const requiredKeys = getDeploymentEnvKeys(deployType, config);
|
|
72
|
+
|
|
73
|
+
checks.push(
|
|
74
|
+
await runCheck(`${deployType} env vars`, async () => {
|
|
75
|
+
const missing = requiredKeys.filter((k) => {
|
|
76
|
+
if (k === 'SSH_KEY_PATH') {
|
|
77
|
+
return !process.env.SSH_KEY_PATH && !process.env.SSH_KEY;
|
|
78
|
+
}
|
|
79
|
+
return !process.env[k];
|
|
80
|
+
});
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
return {
|
|
83
|
+
name: `${deployType} env vars`,
|
|
84
|
+
pass: false,
|
|
85
|
+
message: `Missing required variables: ${missing.join(', ')} — copy .env.example to .env and fill in values (see inline comments).`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
name: `${deployType} env vars`,
|
|
90
|
+
pass: true,
|
|
91
|
+
message: 'All required deployment variables present',
|
|
92
|
+
};
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (SSH_DEPLOY_TYPES.has(deployType)) {
|
|
97
|
+
const host = envConfig.host || process.env.SSH_HOST;
|
|
98
|
+
const user = envConfig.user || process.env.SSH_USER;
|
|
99
|
+
const keyPath = envConfig.keyPath || process.env.SSH_KEY_PATH;
|
|
100
|
+
const sshPort = Number(process.env.SSH_SSH_PORT || envConfig.sshPort) || 22;
|
|
101
|
+
|
|
102
|
+
checks.push(
|
|
103
|
+
await runCheck('SSH key', async () => {
|
|
104
|
+
const result = await validateSshKeyForDoctor(
|
|
105
|
+
keyPath ? String(keyPath) : undefined,
|
|
106
|
+
process.env.SSH_KEY
|
|
107
|
+
);
|
|
108
|
+
return {
|
|
109
|
+
name: 'SSH key',
|
|
110
|
+
pass: result.ok,
|
|
111
|
+
message: result.message,
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
checks.push(
|
|
117
|
+
await runCheck('SSH host reachability', async () => {
|
|
118
|
+
if (!host) {
|
|
119
|
+
return {
|
|
120
|
+
name: 'SSH host reachability',
|
|
121
|
+
pass: false,
|
|
122
|
+
message: 'SSH_HOST is required — set it in .env to your server IP or hostname.',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const result = await testSshHostReachability(String(host), sshPort);
|
|
126
|
+
return {
|
|
127
|
+
name: 'SSH host reachability',
|
|
128
|
+
pass: result.ok,
|
|
129
|
+
message: result.message,
|
|
130
|
+
};
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const isBackend = config.projectType === 'backend' || config.projectType === 'both';
|
|
135
|
+
if (isBackend) {
|
|
136
|
+
const backendChecks = await runBackendProcessChecks(config, envName, deployType);
|
|
137
|
+
checks.push(...backendChecks);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (deployType === 'docker') {
|
|
142
|
+
checks.push(
|
|
143
|
+
await runCheck('Docker daemon', async () => {
|
|
144
|
+
try {
|
|
145
|
+
const provider = getDeploymentProvider('docker', config, envName);
|
|
146
|
+
await provider.testConnection();
|
|
147
|
+
return { name: 'Docker daemon', pass: true, message: 'Docker daemon reachable' };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
return {
|
|
151
|
+
name: 'Docker daemon',
|
|
152
|
+
pass: false,
|
|
153
|
+
message: `Docker not reachable — ${msg}. Install Docker or set DOCKER_HOST for a remote daemon.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (deployType === 'kubernetes') {
|
|
161
|
+
checks.push(
|
|
162
|
+
await runCheck('Kubernetes cluster', async () => {
|
|
163
|
+
try {
|
|
164
|
+
const provider = getDeploymentProvider('kubernetes', config, envName);
|
|
165
|
+
await provider.testConnection();
|
|
166
|
+
const ctx = process.env.KUBE_CONTEXT || 'current';
|
|
167
|
+
const ns = process.env.KUBE_NAMESPACE || config.project || 'default';
|
|
168
|
+
return {
|
|
169
|
+
name: 'Kubernetes cluster',
|
|
170
|
+
pass: true,
|
|
171
|
+
message: `kubectl cluster-info OK (context: ${ctx}, namespace: ${ns})`,
|
|
172
|
+
};
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
return {
|
|
176
|
+
name: 'Kubernetes cluster',
|
|
177
|
+
pass: false,
|
|
178
|
+
message: `kubectl cluster-info failed — ${msg}. Check KUBECONFIG path and KUBE_CONTEXT.`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (deployType === 'ec2' && process.env.EC2_INSTANCE_ID) {
|
|
186
|
+
checks.push(
|
|
187
|
+
await runCheck('EC2 API', async () => {
|
|
188
|
+
const missing = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'].filter(
|
|
189
|
+
(k) => !process.env[k]
|
|
190
|
+
);
|
|
191
|
+
if (missing.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
name: 'EC2 API',
|
|
194
|
+
pass: false,
|
|
195
|
+
message: `EC2_INSTANCE_ID is set but missing: ${missing.join(', ')} — needed for dynamic IP lookup.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { name: 'EC2 API', pass: true, message: 'AWS credentials present for EC2 lookup' };
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return checks;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
208
|
+
* @param {string} envName
|
|
209
|
+
* @param {string} [deployType]
|
|
210
|
+
* @returns {Promise<CheckResult[]>}
|
|
211
|
+
*/
|
|
212
|
+
async function runBackendProcessChecks(config, envName, deployType = 'ssh') {
|
|
60
213
|
const framework = resolveBackendFramework(config);
|
|
61
|
-
const provider = getDeploymentProvider(
|
|
214
|
+
const provider = getDeploymentProvider(deployType, config, envName);
|
|
62
215
|
|
|
63
216
|
if (!provider.runRemoteCheck) {
|
|
64
217
|
return [];
|
|
@@ -163,132 +316,6 @@ async function runBackendProcessChecks(config, envName) {
|
|
|
163
316
|
return checks;
|
|
164
317
|
}
|
|
165
318
|
|
|
166
|
-
/**
|
|
167
|
-
* @param {import('../core/config.js').DeployHubConfig} config
|
|
168
|
-
* @param {string} envName
|
|
169
|
-
* @param {string} [cwd]
|
|
170
|
-
* @returns {Promise<CheckResult[]>}
|
|
171
|
-
*/
|
|
172
|
-
async function runPlatformChecks(config, envName, cwd = process.cwd()) {
|
|
173
|
-
const env = config.environments[envName];
|
|
174
|
-
const platform = env?.platform;
|
|
175
|
-
if (!platform) return [];
|
|
176
|
-
|
|
177
|
-
const cli = PLATFORM_CLI_MAP[platform];
|
|
178
|
-
const envKeys = PLATFORM_ENV_MAP[platform] || [];
|
|
179
|
-
/** @type {CheckResult[]} */
|
|
180
|
-
const checks = [];
|
|
181
|
-
|
|
182
|
-
if (cli?.binary) {
|
|
183
|
-
const cliLabel = cli.binary.charAt(0).toUpperCase() + cli.binary.slice(1);
|
|
184
|
-
checks.push(
|
|
185
|
-
await runCheck(`${cliLabel} CLI`, async () => {
|
|
186
|
-
const installed = await isCliInstalled(cli.binary);
|
|
187
|
-
if (installed) {
|
|
188
|
-
return { name: `${cliLabel} CLI`, pass: true, message: `${cli.binary} CLI installed` };
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
name: `${cliLabel} CLI`,
|
|
192
|
-
pass: false,
|
|
193
|
-
message: `not found — run: ${cli.globalInstall || `npm install -g ${cli.install}`}`,
|
|
194
|
-
};
|
|
195
|
-
})
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const key of envKeys) {
|
|
200
|
-
const isToken = key.includes('TOKEN') || key.includes('KEY');
|
|
201
|
-
checks.push(
|
|
202
|
-
await runCheck(key, async () => {
|
|
203
|
-
if (!process.env[key]) {
|
|
204
|
-
return { name: key, pass: false, message: 'not set in .env' };
|
|
205
|
-
}
|
|
206
|
-
if (isToken && key === envKeys[0]) {
|
|
207
|
-
try {
|
|
208
|
-
const provider = createPlatformProvider(platform, config, envName);
|
|
209
|
-
if (provider.testConnection) {
|
|
210
|
-
await provider.testConnection();
|
|
211
|
-
return { name: key, pass: true, message: 'token valid' };
|
|
212
|
-
}
|
|
213
|
-
} catch (err) {
|
|
214
|
-
return {
|
|
215
|
-
name: key,
|
|
216
|
-
pass: false,
|
|
217
|
-
message: err instanceof Error ? err.message : 'token invalid',
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return { name: key, pass: true, message: 'present' };
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (platform === 'vercel') {
|
|
227
|
-
checks.push(
|
|
228
|
-
await runCheck('Vercel project link', async () => {
|
|
229
|
-
const vercelJson = path.join(cwd, '.vercel', 'project.json');
|
|
230
|
-
if (await fs.pathExists(vercelJson)) {
|
|
231
|
-
return { name: 'Vercel project link', pass: true, message: '.vercel/project.json found' };
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
name: 'Vercel project link',
|
|
235
|
-
pass: false,
|
|
236
|
-
message: 'not found — run: vercel link',
|
|
237
|
-
};
|
|
238
|
-
})
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (platform === 'firebase-hosting') {
|
|
243
|
-
checks.push(
|
|
244
|
-
await runCheck('firebase.json', async () => {
|
|
245
|
-
if (await fs.pathExists(path.join(cwd, 'firebase.json'))) {
|
|
246
|
-
return { name: 'firebase.json', pass: true, message: 'found' };
|
|
247
|
-
}
|
|
248
|
-
return {
|
|
249
|
-
name: 'firebase.json',
|
|
250
|
-
pass: false,
|
|
251
|
-
message: 'missing — run deployhub init or create manually',
|
|
252
|
-
};
|
|
253
|
-
})
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const provider = createPlatformProvider(platform, config, envName);
|
|
259
|
-
if (provider.testConnection && envKeys.every((k) => process.env[k])) {
|
|
260
|
-
checks.push(
|
|
261
|
-
await runCheck(`${platform} connection`, async () => {
|
|
262
|
-
await provider.testConnection();
|
|
263
|
-
if (platform === 'netlify') {
|
|
264
|
-
return { name: 'NETLIFY_SITE_ID', pass: true, message: 'site found' };
|
|
265
|
-
}
|
|
266
|
-
if (platform === 'cloudflare-pages') {
|
|
267
|
-
return { name: 'CF project', pass: true, message: 'project exists' };
|
|
268
|
-
}
|
|
269
|
-
if (platform === 'aws-amplify') {
|
|
270
|
-
return { name: 'AMPLIFY_APP_ID', pass: true, message: 'app found in AWS' };
|
|
271
|
-
}
|
|
272
|
-
if (platform.startsWith('firebase')) {
|
|
273
|
-
return { name: 'Firebase project', pass: true, message: 'project ID valid' };
|
|
274
|
-
}
|
|
275
|
-
return { name: `${platform} connection`, pass: true, message: 'connected' };
|
|
276
|
-
})
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
} catch (err) {
|
|
280
|
-
if (envKeys.every((k) => process.env[k])) {
|
|
281
|
-
checks.push({
|
|
282
|
-
name: `${platform} connection`,
|
|
283
|
-
pass: false,
|
|
284
|
-
message: err instanceof Error ? err.message : String(err),
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return checks;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
319
|
/**
|
|
293
320
|
* @param {import('commander').Command} program
|
|
294
321
|
*/
|
|
@@ -467,32 +494,8 @@ export function registerDoctorCommand(program) {
|
|
|
467
494
|
const env = config.environments[envName];
|
|
468
495
|
if (!env) continue;
|
|
469
496
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
results.push(...platformChecks);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (env.type && ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(env.type)) {
|
|
476
|
-
results.push(
|
|
477
|
-
await runCheck('SSH target', async () => {
|
|
478
|
-
const provider = getDeploymentProvider(env.type, config, envName);
|
|
479
|
-
await provider.testConnection();
|
|
480
|
-
const host = env.host || process.env.SSH_HOST;
|
|
481
|
-
return {
|
|
482
|
-
name: 'SSH target',
|
|
483
|
-
pass: true,
|
|
484
|
-
message: `Can reach ${host || 'host'}`,
|
|
485
|
-
};
|
|
486
|
-
})
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
const isBackend =
|
|
490
|
-
config.projectType === 'backend' || config.projectType === 'both';
|
|
491
|
-
if (isBackend && env.type === 'ssh') {
|
|
492
|
-
const backendChecks = await runBackendProcessChecks(config, envName);
|
|
493
|
-
results.push(...backendChecks);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
497
|
+
const deployChecks = await runDeploymentChecks(config, envName, env);
|
|
498
|
+
results.push(...deployChecks);
|
|
496
499
|
}
|
|
497
500
|
|
|
498
501
|
results.push(
|
|
@@ -539,22 +542,17 @@ export function registerDoctorCommand(program) {
|
|
|
539
542
|
}
|
|
540
543
|
for (const envName of config.deploy || []) {
|
|
541
544
|
const env = config.environments[envName];
|
|
542
|
-
if (!env) continue;
|
|
543
|
-
|
|
544
|
-
if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
|
|
545
|
-
const platform = env.platform;
|
|
546
|
-
if (platform) {
|
|
547
|
-
const keys = PLATFORM_ENV_MAP[platform] || [];
|
|
548
|
-
required.push(...keys);
|
|
549
|
-
}
|
|
550
|
-
} else if (env.type) {
|
|
551
|
-
const keys = PROVIDER_ENV_MAP[env.type] || [];
|
|
552
|
-
required.push(...keys);
|
|
553
|
-
}
|
|
545
|
+
if (!env?.type) continue;
|
|
546
|
+
required.push(...getDeploymentSecretKeys(env.type, config));
|
|
554
547
|
}
|
|
555
548
|
|
|
556
549
|
const unique = [...new Set(required)];
|
|
557
|
-
const missing = unique.filter((k) =>
|
|
550
|
+
const missing = unique.filter((k) => {
|
|
551
|
+
if (k === 'SSH_KEY') {
|
|
552
|
+
return !process.env.SSH_KEY && !process.env.SSH_KEY_PATH;
|
|
553
|
+
}
|
|
554
|
+
return !process.env[k];
|
|
555
|
+
});
|
|
558
556
|
if (missing.length > 0) {
|
|
559
557
|
return {
|
|
560
558
|
name: 'Secrets',
|