@akash-chowdhury-24/deployhub 2.0.0 → 2.0.3
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 +227 -5
- package/package.json +4 -3
- package/src/commands/deploy.js +1 -1
- package/src/commands/doctor.js +244 -30
- package/src/commands/init.js +66 -223
- package/src/commands/rollback.js +1 -1
- package/src/core/config.js +15 -0
- package/src/deployment/deployment-env.js +625 -0
- package/src/deployment/init-helpers.js +471 -0
- package/src/deployment/init-prompts.js +434 -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 +70 -42
- package/src/utils/github-actions.js +24 -37
- package/src/{rollback → utils/rollback}/engine.js +6 -6
- package/src/utils/shell-quote.js +64 -0
package/src/commands/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { saveConfig } from '../core/config.js';
|
|
5
|
+
import { saveConfig, appendEnv } from '../core/config.js';
|
|
6
6
|
import { detectFrontend, detectBackend } from '../detectors/index.js';
|
|
7
7
|
import {
|
|
8
8
|
getFrontendInfo,
|
|
@@ -20,6 +20,14 @@ import {
|
|
|
20
20
|
} from '../utils/github-actions.js';
|
|
21
21
|
import { printAuthorFooter } from '../utils/author.js';
|
|
22
22
|
import { generateNginxConfig } from '../utils/nginx.js';
|
|
23
|
+
import {
|
|
24
|
+
promptServerDeployment,
|
|
25
|
+
buildServerEnvEntry,
|
|
26
|
+
getDockerEnvSecrets,
|
|
27
|
+
SSH_BASED,
|
|
28
|
+
} from '../deployment/init-prompts.js';
|
|
29
|
+
import { printDeploymentNextSteps } from '../deployment/deployment-env.js';
|
|
30
|
+
import { confirmValueIfContainsSpaces } from '../deployment/init-helpers.js';
|
|
23
31
|
|
|
24
32
|
const FRONTEND_CHOICES = [
|
|
25
33
|
{ name: 'React', value: 'react' },
|
|
@@ -151,136 +159,6 @@ async function promptBuildSettings(defaults, side) {
|
|
|
151
159
|
return inquirer.prompt(questions);
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
const SERVER_DEPLOY_TYPES = [
|
|
155
|
-
'ssh',
|
|
156
|
-
'docker',
|
|
157
|
-
'ec2',
|
|
158
|
-
'azure-vm',
|
|
159
|
-
'gcp-vm',
|
|
160
|
-
'kubernetes',
|
|
161
|
-
];
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @param {string} projectName
|
|
165
|
-
* @param {'frontend'|'backend'|'both'} projectType
|
|
166
|
-
* @param {Record<string, unknown>|null} backendConfig
|
|
167
|
-
* @param {Record<string, unknown>|null} singleConfig
|
|
168
|
-
*/
|
|
169
|
-
async function promptServerDeployment(projectName, projectType, backendConfig, singleConfig) {
|
|
170
|
-
return inquirer.prompt([
|
|
171
|
-
{
|
|
172
|
-
type: 'list',
|
|
173
|
-
name: 'deployType',
|
|
174
|
-
message: 'Deployment type:',
|
|
175
|
-
choices: SERVER_DEPLOY_TYPES,
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
type: 'input',
|
|
179
|
-
name: 'envName',
|
|
180
|
-
message: 'Environment name:',
|
|
181
|
-
default: 'production',
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
type: 'input',
|
|
185
|
-
name: 'host',
|
|
186
|
-
message: 'Host (for SSH-based targets):',
|
|
187
|
-
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
type: 'input',
|
|
191
|
-
name: 'user',
|
|
192
|
-
message: 'SSH user:',
|
|
193
|
-
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
type: 'input',
|
|
197
|
-
name: 'deployPath',
|
|
198
|
-
message: 'Deploy path:',
|
|
199
|
-
default: `/var/www/${projectName}`,
|
|
200
|
-
when: (a) =>
|
|
201
|
-
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
202
|
-
projectType !== 'both',
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
type: 'input',
|
|
206
|
-
name: 'frontendDeployPath',
|
|
207
|
-
message: 'Frontend deploy path:',
|
|
208
|
-
default: `/var/www/${projectName}/public`,
|
|
209
|
-
when: (a) =>
|
|
210
|
-
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
211
|
-
projectType === 'both',
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
type: 'input',
|
|
215
|
-
name: 'backendDeployPath',
|
|
216
|
-
message: 'Backend deploy path:',
|
|
217
|
-
default: `/var/www/${projectName}/api`,
|
|
218
|
-
when: (a) =>
|
|
219
|
-
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
220
|
-
projectType === 'both',
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
type: 'input',
|
|
224
|
-
name: 'appName',
|
|
225
|
-
message: 'App name (for PM2):',
|
|
226
|
-
default: projectType === 'both' ? `${projectName}-api` : projectName,
|
|
227
|
-
when: (a) =>
|
|
228
|
-
['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
|
|
229
|
-
(projectType === 'backend' || projectType === 'both'),
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
type: 'input',
|
|
233
|
-
name: 'healthUrl',
|
|
234
|
-
message: 'Health check URL (optional):',
|
|
235
|
-
},
|
|
236
|
-
]);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* @param {Awaited<ReturnType<typeof promptServerDeployment>>} deployAnswers
|
|
241
|
-
* @param {'frontend'|'backend'|'both'} projectType
|
|
242
|
-
* @param {string} projectName
|
|
243
|
-
* @param {Record<string, unknown>|null} backendConfig
|
|
244
|
-
* @param {Record<string, unknown>|null} singleConfig
|
|
245
|
-
*/
|
|
246
|
-
function buildServerEnvEntry(
|
|
247
|
-
deployAnswers,
|
|
248
|
-
projectType,
|
|
249
|
-
projectName,
|
|
250
|
-
backendConfig,
|
|
251
|
-
singleConfig
|
|
252
|
-
) {
|
|
253
|
-
/** @type {Record<string, unknown>} */
|
|
254
|
-
const envEntry = {
|
|
255
|
-
deploymentType: 'server',
|
|
256
|
-
type: deployAnswers.deployType,
|
|
257
|
-
host: deployAnswers.host || '',
|
|
258
|
-
user: deployAnswers.user || '',
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
if (projectType === 'both') {
|
|
262
|
-
envEntry.frontendDeployPath =
|
|
263
|
-
deployAnswers.frontendDeployPath || `/var/www/${projectName}/public`;
|
|
264
|
-
envEntry.backendDeployPath =
|
|
265
|
-
deployAnswers.backendDeployPath || `/var/www/${projectName}/api`;
|
|
266
|
-
envEntry.appName = deployAnswers.appName || `${projectName}-api`;
|
|
267
|
-
envEntry.framework = backendConfig?.framework || 'express';
|
|
268
|
-
envEntry.path = envEntry.backendDeployPath;
|
|
269
|
-
envEntry.backendDeploymentType = 'server';
|
|
270
|
-
} else if (projectType === 'backend') {
|
|
271
|
-
envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
|
|
272
|
-
envEntry.path = envEntry.deployPath;
|
|
273
|
-
envEntry.appName = deployAnswers.appName || projectName;
|
|
274
|
-
envEntry.framework = singleConfig?.framework || 'express';
|
|
275
|
-
envEntry.port = singleConfig?.port || 3000;
|
|
276
|
-
} else {
|
|
277
|
-
envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
|
|
278
|
-
envEntry.path = envEntry.deployPath;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return envEntry;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
162
|
/**
|
|
285
163
|
* @param {Record<string, unknown>} config
|
|
286
164
|
* @param {Record<string, Record<string, unknown>>} environments
|
|
@@ -290,7 +168,8 @@ async function generateProjectScaffold(config, environments, cwd) {
|
|
|
290
168
|
const envList = Object.values(environments);
|
|
291
169
|
|
|
292
170
|
const usesFrontendSsh = envList.some(
|
|
293
|
-
(env) =>
|
|
171
|
+
(env) =>
|
|
172
|
+
SSH_BASED.includes(env.type) || env.deploymentType === 'server'
|
|
294
173
|
);
|
|
295
174
|
|
|
296
175
|
const isFrontendProject =
|
|
@@ -343,7 +222,7 @@ export function registerInitCommand(program) {
|
|
|
343
222
|
const defaultName = path.basename(cwd) || 'my-app';
|
|
344
223
|
const cliSource = DEFAULT_NPM_CLI_SOURCE;
|
|
345
224
|
|
|
346
|
-
|
|
225
|
+
let { projectName } = await inquirer.prompt([
|
|
347
226
|
{
|
|
348
227
|
type: 'input',
|
|
349
228
|
name: 'projectName',
|
|
@@ -352,6 +231,8 @@ export function registerInitCommand(program) {
|
|
|
352
231
|
},
|
|
353
232
|
]);
|
|
354
233
|
|
|
234
|
+
projectName = await confirmValueIfContainsSpaces(projectName, 'project name');
|
|
235
|
+
|
|
355
236
|
const { projectType } = await inquirer.prompt([
|
|
356
237
|
{
|
|
357
238
|
type: 'list',
|
|
@@ -431,15 +312,19 @@ export function registerInitCommand(program) {
|
|
|
431
312
|
/** @type {string[]} */
|
|
432
313
|
const deploy = [];
|
|
433
314
|
let healthUrl = '';
|
|
315
|
+
/** @type {string|undefined} */
|
|
316
|
+
let primaryDeployType;
|
|
317
|
+
/** @type {Record<string, string>|null} */
|
|
318
|
+
let dockerEnvToAppend = null;
|
|
434
319
|
|
|
435
320
|
if (answers.configureDeploy) {
|
|
436
321
|
if (projectType === 'backend') {
|
|
437
322
|
const deployAnswers = await promptServerDeployment(
|
|
438
323
|
projectName,
|
|
439
324
|
projectType,
|
|
440
|
-
backendConfig
|
|
441
|
-
singleConfig
|
|
325
|
+
backendConfig
|
|
442
326
|
);
|
|
327
|
+
primaryDeployType = deployAnswers.deployType;
|
|
443
328
|
deploy.push(deployAnswers.envName);
|
|
444
329
|
environments[deployAnswers.envName] = buildServerEnvEntry(
|
|
445
330
|
deployAnswers,
|
|
@@ -453,13 +338,14 @@ export function registerInitCommand(program) {
|
|
|
453
338
|
} else if (singleConfig?.port) {
|
|
454
339
|
healthUrl = `http://localhost:${singleConfig.port}/health`;
|
|
455
340
|
}
|
|
341
|
+
dockerEnvToAppend = getDockerEnvSecrets(deployAnswers);
|
|
456
342
|
} else if (projectType === 'frontend') {
|
|
457
343
|
const deployAnswers = await promptServerDeployment(
|
|
458
344
|
projectName,
|
|
459
345
|
projectType,
|
|
460
|
-
backendConfig
|
|
461
|
-
singleConfig
|
|
346
|
+
backendConfig
|
|
462
347
|
);
|
|
348
|
+
primaryDeployType = deployAnswers.deployType;
|
|
463
349
|
deploy.push(deployAnswers.envName);
|
|
464
350
|
environments[deployAnswers.envName] = buildServerEnvEntry(
|
|
465
351
|
deployAnswers,
|
|
@@ -471,16 +357,15 @@ export function registerInitCommand(program) {
|
|
|
471
357
|
if (deployAnswers.healthUrl) {
|
|
472
358
|
healthUrl = deployAnswers.healthUrl;
|
|
473
359
|
}
|
|
360
|
+
dockerEnvToAppend = getDockerEnvSecrets(deployAnswers);
|
|
474
361
|
} else {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
]);
|
|
483
|
-
deploy.push(envName);
|
|
362
|
+
const backendDeployAnswers = await promptServerDeployment(
|
|
363
|
+
projectName,
|
|
364
|
+
'both',
|
|
365
|
+
backendConfig
|
|
366
|
+
);
|
|
367
|
+
primaryDeployType = backendDeployAnswers.deployType;
|
|
368
|
+
deploy.push(backendDeployAnswers.envName);
|
|
484
369
|
|
|
485
370
|
/** @type {Record<string, unknown>} */
|
|
486
371
|
const envEntry = {
|
|
@@ -489,76 +374,25 @@ export function registerInitCommand(program) {
|
|
|
489
374
|
deploymentType: 'server',
|
|
490
375
|
};
|
|
491
376
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
type: 'input',
|
|
509
|
-
name: 'user',
|
|
510
|
-
message: 'SSH user:',
|
|
511
|
-
when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
type: 'input',
|
|
515
|
-
name: 'backendDeployPath',
|
|
516
|
-
message: 'Backend deploy path:',
|
|
517
|
-
default: `/var/www/${projectName}/api`,
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
type: 'input',
|
|
521
|
-
name: 'appName',
|
|
522
|
-
message: 'App name (for PM2):',
|
|
523
|
-
default: `${projectName}-api`,
|
|
524
|
-
},
|
|
525
|
-
]);
|
|
526
|
-
|
|
527
|
-
Object.assign(envEntry, {
|
|
528
|
-
type: backendDeployAnswers.deployType,
|
|
529
|
-
host: backendDeployAnswers.host || '',
|
|
530
|
-
user: backendDeployAnswers.user || '',
|
|
531
|
-
backendDeployPath:
|
|
532
|
-
backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
|
|
533
|
-
appName: backendDeployAnswers.appName || `${projectName}-api`,
|
|
534
|
-
framework: backendConfig?.framework || 'express',
|
|
535
|
-
path: backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
const { frontendDeployPath } = await inquirer.prompt([
|
|
539
|
-
{
|
|
540
|
-
type: 'input',
|
|
541
|
-
name: 'frontendDeployPath',
|
|
542
|
-
message: 'Frontend deploy path:',
|
|
543
|
-
default: `/var/www/${projectName}/public`,
|
|
544
|
-
},
|
|
545
|
-
]);
|
|
546
|
-
envEntry.frontendDeployPath = frontendDeployPath;
|
|
547
|
-
|
|
548
|
-
const { healthUrlInput } = await inquirer.prompt([
|
|
549
|
-
{
|
|
550
|
-
type: 'input',
|
|
551
|
-
name: 'healthUrlInput',
|
|
552
|
-
message: 'Health check URL (optional):',
|
|
553
|
-
default: healthUrl || '',
|
|
554
|
-
},
|
|
555
|
-
]);
|
|
556
|
-
if (healthUrlInput) healthUrl = healthUrlInput;
|
|
557
|
-
else if (!healthUrl && backendConfig?.port) {
|
|
377
|
+
Object.assign(
|
|
378
|
+
envEntry,
|
|
379
|
+
buildServerEnvEntry(
|
|
380
|
+
backendDeployAnswers,
|
|
381
|
+
'both',
|
|
382
|
+
projectName,
|
|
383
|
+
backendConfig,
|
|
384
|
+
singleConfig
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (backendDeployAnswers.healthUrl) {
|
|
389
|
+
healthUrl = backendDeployAnswers.healthUrl;
|
|
390
|
+
} else if (backendConfig?.port) {
|
|
558
391
|
healthUrl = `http://localhost:${backendConfig.port}/health`;
|
|
559
392
|
}
|
|
560
393
|
|
|
561
|
-
environments[envName] = envEntry;
|
|
394
|
+
environments[backendDeployAnswers.envName] = envEntry;
|
|
395
|
+
dockerEnvToAppend = getDockerEnvSecrets(backendDeployAnswers);
|
|
562
396
|
}
|
|
563
397
|
}
|
|
564
398
|
|
|
@@ -611,6 +445,9 @@ export function registerInitCommand(program) {
|
|
|
611
445
|
}
|
|
612
446
|
|
|
613
447
|
await saveConfig(config, cwd);
|
|
448
|
+
if (dockerEnvToAppend) {
|
|
449
|
+
await appendEnv(dockerEnvToAppend, cwd);
|
|
450
|
+
}
|
|
614
451
|
await addDeployhubToPackageJson(cliSource, cwd);
|
|
615
452
|
await writeWorkflowFile(
|
|
616
453
|
config.storage,
|
|
@@ -635,22 +472,28 @@ export function registerInitCommand(program) {
|
|
|
635
472
|
const secrets = getRequiredSecrets(config.storage, deploy, environments, config);
|
|
636
473
|
|
|
637
474
|
console.log('');
|
|
638
|
-
|
|
475
|
+
if (primaryDeployType) {
|
|
476
|
+
console.log(chalk.green.bold(`✔ Config generated for ${primaryDeployType} deployment.`));
|
|
477
|
+
printDeploymentNextSteps(primaryDeployType, secrets);
|
|
478
|
+
} else {
|
|
479
|
+
console.log(chalk.green.bold('✓ DeployHub initialized successfully!'));
|
|
480
|
+
console.log('');
|
|
481
|
+
console.log(chalk.bold('Next steps:'));
|
|
482
|
+
console.log(' 1. Copy .env.example to .env and fill in credentials');
|
|
483
|
+
if (secrets.length > 0) {
|
|
484
|
+
console.log(' 2. Add these secrets to GitHub (Settings → Secrets):');
|
|
485
|
+
secrets.forEach((s) => console.log(` • ${s}`));
|
|
486
|
+
}
|
|
487
|
+
console.log(` ${secrets.length > 0 ? '3' : '2'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
|
|
488
|
+
console.log(` ${secrets.length > 0 ? '4' : '3'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
|
|
489
|
+
}
|
|
490
|
+
|
|
639
491
|
console.log('');
|
|
640
492
|
console.log(chalk.bold('Generated files:'));
|
|
641
493
|
console.log(' • deployhub.config.json');
|
|
642
494
|
console.log(' • .github/workflows/deployhub.yml');
|
|
643
495
|
console.log(' • .env.example');
|
|
644
496
|
console.log('');
|
|
645
|
-
console.log(chalk.bold('Next steps:'));
|
|
646
|
-
console.log(' 1. Copy .env.example to .env and fill in credentials');
|
|
647
|
-
if (secrets.length > 0) {
|
|
648
|
-
console.log(' 2. Add these secrets to GitHub (Settings → Secrets):');
|
|
649
|
-
secrets.forEach((s) => console.log(` • ${s}`));
|
|
650
|
-
}
|
|
651
|
-
console.log(` ${secrets.length > 0 ? '3' : '2'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
|
|
652
|
-
console.log(` ${secrets.length > 0 ? '4' : '3'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
|
|
653
|
-
console.log('');
|
|
654
497
|
printAuthorFooter();
|
|
655
498
|
});
|
|
656
499
|
}
|
package/src/commands/rollback.js
CHANGED
package/src/core/config.js
CHANGED
|
@@ -22,6 +22,21 @@ const EnvironmentSchema = z.object({
|
|
|
22
22
|
path: z.string().optional(),
|
|
23
23
|
deployPath: z.string().optional(),
|
|
24
24
|
keyPath: z.string().optional(),
|
|
25
|
+
sshPort: z.number().optional(),
|
|
26
|
+
ec2InstanceId: z.string().optional(),
|
|
27
|
+
awsRegion: z.string().optional(),
|
|
28
|
+
azureSubscriptionId: z.string().optional(),
|
|
29
|
+
azureResourceGroup: z.string().optional(),
|
|
30
|
+
azureVmName: z.string().optional(),
|
|
31
|
+
gcpProjectId: z.string().optional(),
|
|
32
|
+
gcpZone: z.string().optional(),
|
|
33
|
+
gcpInstanceName: z.string().optional(),
|
|
34
|
+
kubeconfig: z.string().optional(),
|
|
35
|
+
kubeContext: z.string().optional(),
|
|
36
|
+
kubeNamespace: z.string().optional(),
|
|
37
|
+
dockerImageName: z.string().optional(),
|
|
38
|
+
dockerRegistryUrl: z.string().optional(),
|
|
39
|
+
dockerHost: z.string().optional(),
|
|
25
40
|
appName: z.string().optional(),
|
|
26
41
|
framework: z.string().optional(),
|
|
27
42
|
port: z.number().optional(),
|