@akash-chowdhury-24/deployhub 2.0.14 → 2.0.16
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 +98 -25
- package/package.json +1 -1
- package/src/artifact/engine.js +11 -4
- package/src/cli/index.js +4 -0
- package/src/commands/deploy.js +61 -16
- package/src/commands/doctor.js +212 -26
- package/src/commands/env.js +326 -0
- package/src/commands/init.js +97 -64
- package/src/commands/rollback.js +84 -24
- package/src/commands/sync-k8s-ports.js +71 -0
- package/src/commands/verify.js +97 -33
- package/src/core/config.js +371 -33
- package/src/core/environments.js +300 -0
- package/src/core/stages.js +55 -19
- package/src/deployment/deployment-env.js +210 -0
- package/src/deployment/index.js +58 -7
- package/src/deployment/init-prompts.js +151 -46
- package/src/deployment/providers/azure-vm.js +8 -10
- package/src/deployment/providers/docker.js +10 -2
- package/src/deployment/providers/ec2.js +7 -9
- package/src/deployment/providers/gcp-vm.js +8 -10
- package/src/deployment/providers/kubernetes.js +15 -6
- package/src/deployment/providers/ssh.js +18 -13
- package/src/storage/index.js +230 -3
- package/src/utils/build-id.js +23 -0
- package/src/utils/dockerfile-expose.js +117 -0
- package/src/utils/github-actions.js +344 -31
- package/src/utils/health-check.js +158 -0
- package/src/utils/kubernetes-manifests.js +105 -10
- package/src/utils/nginx.js +66 -15
- package/src/utils/rollback/engine.js +147 -40
- package/src/utils/scaffold.js +8 -3
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, appendEnv } from '../core/config.js';
|
|
5
|
+
import { saveConfig, appendEnv, getEnvMethod, getEnvSettings } from '../core/config.js';
|
|
6
6
|
import { detectFrontend, detectBackend } from '../detectors/index.js';
|
|
7
7
|
import {
|
|
8
8
|
getFrontendInfo,
|
|
@@ -174,10 +174,11 @@ async function promptBuildSettings(defaults, side) {
|
|
|
174
174
|
async function generateProjectScaffold(config, environments, cwd) {
|
|
175
175
|
const envList = Object.values(environments);
|
|
176
176
|
|
|
177
|
-
const usesFrontendSsh = envList.some(
|
|
178
|
-
(env)
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
const usesFrontendSsh = envList.some((env) => {
|
|
178
|
+
const method = getEnvMethod(env);
|
|
179
|
+
const settings = getEnvSettings(env);
|
|
180
|
+
return SSH_BASED.includes(method) || settings.deploymentType === 'server';
|
|
181
|
+
});
|
|
181
182
|
|
|
182
183
|
const isFrontendProject =
|
|
183
184
|
config.projectType === 'frontend' || config.projectType === 'both';
|
|
@@ -343,84 +344,114 @@ export function registerInitCommand(program) {
|
|
|
343
344
|
let dockerEnvToAppend = null;
|
|
344
345
|
|
|
345
346
|
if (answers.configureDeploy) {
|
|
346
|
-
|
|
347
|
+
const { envMode } = await inquirer.prompt([
|
|
348
|
+
{
|
|
349
|
+
type: 'list',
|
|
350
|
+
name: 'envMode',
|
|
351
|
+
message:
|
|
352
|
+
'How many environments do you want to set up? (1 is fine — you can add more later)',
|
|
353
|
+
choices: [
|
|
354
|
+
{ name: 'Just one environment', value: 'one' },
|
|
355
|
+
{
|
|
356
|
+
name: 'Multiple environments (e.g. development, testing, production)',
|
|
357
|
+
value: 'multi',
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
default: 'one',
|
|
361
|
+
},
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Shared single-round prompt used by init (loop) and env add.
|
|
366
|
+
* @param {{ envName?: string }} [opts]
|
|
367
|
+
*/
|
|
368
|
+
async function promptAndStoreEnvironment(opts = {}) {
|
|
369
|
+
const deployTypeForPrompt =
|
|
370
|
+
projectType === 'both' ? 'both' : projectType;
|
|
347
371
|
const deployAnswers = await promptServerDeployment(
|
|
348
372
|
projectName,
|
|
349
|
-
|
|
350
|
-
backendConfig
|
|
351
|
-
);
|
|
352
|
-
primaryDeployType = deployAnswers.deployType;
|
|
353
|
-
deploy.push(deployAnswers.envName);
|
|
354
|
-
environments[deployAnswers.envName] = buildServerEnvEntry(
|
|
355
|
-
deployAnswers,
|
|
356
|
-
projectType,
|
|
357
|
-
projectName,
|
|
373
|
+
deployTypeForPrompt,
|
|
358
374
|
backendConfig,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
} else if (singleConfig?.port) {
|
|
364
|
-
healthUrl = `http://localhost:${singleConfig.port}/health`;
|
|
365
|
-
}
|
|
366
|
-
dockerEnvToAppend = getDockerEnvSecrets(deployAnswers);
|
|
367
|
-
} else if (projectType === 'frontend') {
|
|
368
|
-
const deployAnswers = await promptServerDeployment(
|
|
369
|
-
projectName,
|
|
370
|
-
projectType,
|
|
371
|
-
backendConfig
|
|
375
|
+
{
|
|
376
|
+
...(opts.envName ? { envName: opts.envName } : {}),
|
|
377
|
+
existingEnvNames: Object.keys(environments),
|
|
378
|
+
}
|
|
372
379
|
);
|
|
373
380
|
primaryDeployType = deployAnswers.deployType;
|
|
374
|
-
|
|
375
|
-
|
|
381
|
+
const name = deployAnswers.envName;
|
|
382
|
+
deploy.push(name);
|
|
383
|
+
|
|
384
|
+
let entry = buildServerEnvEntry(
|
|
376
385
|
deployAnswers,
|
|
377
386
|
projectType,
|
|
378
387
|
projectName,
|
|
379
388
|
backendConfig,
|
|
380
389
|
singleConfig
|
|
381
390
|
);
|
|
391
|
+
|
|
392
|
+
if (projectType === 'both' && entry.config) {
|
|
393
|
+
entry = {
|
|
394
|
+
...entry,
|
|
395
|
+
config: {
|
|
396
|
+
...entry.config,
|
|
397
|
+
backendDeploymentType: 'server',
|
|
398
|
+
frontendDeploymentType: 'server',
|
|
399
|
+
deploymentType: 'server',
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
environments[name] = entry;
|
|
405
|
+
|
|
382
406
|
if (deployAnswers.healthUrl) {
|
|
383
407
|
healthUrl = deployAnswers.healthUrl;
|
|
408
|
+
} else if (!healthUrl && (singleConfig?.port || backendConfig?.port)) {
|
|
409
|
+
healthUrl = `http://localhost:${singleConfig?.port || backendConfig?.port}/health`;
|
|
384
410
|
}
|
|
385
|
-
dockerEnvToAppend = getDockerEnvSecrets(deployAnswers);
|
|
386
|
-
} else {
|
|
387
|
-
const backendDeployAnswers = await promptServerDeployment(
|
|
388
|
-
projectName,
|
|
389
|
-
'both',
|
|
390
|
-
backendConfig
|
|
391
|
-
);
|
|
392
|
-
primaryDeployType = backendDeployAnswers.deployType;
|
|
393
|
-
deploy.push(backendDeployAnswers.envName);
|
|
394
|
-
|
|
395
|
-
/** @type {Record<string, unknown>} */
|
|
396
|
-
const envEntry = {
|
|
397
|
-
backendDeploymentType: 'server',
|
|
398
|
-
frontendDeploymentType: 'server',
|
|
399
|
-
deploymentType: 'server',
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
Object.assign(
|
|
403
|
-
envEntry,
|
|
404
|
-
buildServerEnvEntry(
|
|
405
|
-
backendDeployAnswers,
|
|
406
|
-
'both',
|
|
407
|
-
projectName,
|
|
408
|
-
backendConfig,
|
|
409
|
-
singleConfig
|
|
410
|
-
)
|
|
411
|
-
);
|
|
412
411
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
healthUrl = `http://localhost:${backendConfig.port}/health`;
|
|
412
|
+
const secrets = getDockerEnvSecrets(deployAnswers);
|
|
413
|
+
if (secrets) {
|
|
414
|
+
dockerEnvToAppend = { ...(dockerEnvToAppend || {}), ...secrets };
|
|
417
415
|
}
|
|
416
|
+
return name;
|
|
417
|
+
}
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
if (envMode === 'one') {
|
|
420
|
+
// Transparent single env named "default" (visible later via env list).
|
|
421
|
+
await promptAndStoreEnvironment({ envName: 'default' });
|
|
422
|
+
} else {
|
|
423
|
+
let addMore = true;
|
|
424
|
+
while (addMore) {
|
|
425
|
+
await promptAndStoreEnvironment();
|
|
426
|
+
if (deploy.length >= 1) {
|
|
427
|
+
const again = await inquirer.prompt([
|
|
428
|
+
{
|
|
429
|
+
type: 'confirm',
|
|
430
|
+
name: 'more',
|
|
431
|
+
message: 'Add another environment?',
|
|
432
|
+
default: false,
|
|
433
|
+
},
|
|
434
|
+
]);
|
|
435
|
+
addMore = again.more;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
421
438
|
}
|
|
422
439
|
}
|
|
423
440
|
|
|
441
|
+
let defaultEnvironment = deploy[0];
|
|
442
|
+
if (deploy.length >= 2) {
|
|
443
|
+
const picked = await inquirer.prompt([
|
|
444
|
+
{
|
|
445
|
+
type: 'list',
|
|
446
|
+
name: 'defaultEnvironment',
|
|
447
|
+
message: 'Which environment is the default for `deployhub deploy`?',
|
|
448
|
+
choices: deploy,
|
|
449
|
+
default: deploy[0],
|
|
450
|
+
},
|
|
451
|
+
]);
|
|
452
|
+
defaultEnvironment = picked.defaultEnvironment;
|
|
453
|
+
}
|
|
454
|
+
|
|
424
455
|
const version = await getProjectVersion(cwd);
|
|
425
456
|
let hasDocker =
|
|
426
457
|
(detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
|
|
@@ -432,7 +463,9 @@ export function registerInitCommand(program) {
|
|
|
432
463
|
projectType,
|
|
433
464
|
artifact: true,
|
|
434
465
|
storage: answers.storage.length > 0 ? answers.storage : ['local'],
|
|
435
|
-
|
|
466
|
+
defaultEnvironment: defaultEnvironment || undefined,
|
|
467
|
+
unprefixedSecretEnvironment: defaultEnvironment || undefined,
|
|
468
|
+
legacyHistoryMigrated: true,
|
|
436
469
|
environments,
|
|
437
470
|
healthCheck: {
|
|
438
471
|
url: healthUrl,
|
package/src/commands/rollback.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveEnvTargets } from '../core/environments.js';
|
|
4
|
+
import {
|
|
5
|
+
rollbackToVersion,
|
|
6
|
+
formatRollbackAllSummary,
|
|
7
|
+
} from '../utils/rollback/engine.js';
|
|
8
|
+
import { createLogger } from '../logger/index.js';
|
|
4
9
|
import axios from 'axios';
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -12,37 +17,92 @@ export function registerRollbackCommand(program) {
|
|
|
12
17
|
.description(
|
|
13
18
|
'Rollback to a previous artifact build (omit arg = previous build; use exact buildId if semver is ambiguous)'
|
|
14
19
|
)
|
|
15
|
-
.
|
|
20
|
+
.option(
|
|
21
|
+
'--env <name>',
|
|
22
|
+
'Environment to roll back (name, or "all" for every enabled environment independently)'
|
|
23
|
+
)
|
|
24
|
+
.action(async (versionOrBuildId, opts) => {
|
|
16
25
|
loadEnv();
|
|
17
26
|
const config = await loadConfig();
|
|
27
|
+
const log = createLogger('rollback');
|
|
18
28
|
|
|
29
|
+
let targets;
|
|
30
|
+
let skippedDisabled;
|
|
19
31
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
({ targets, skippedDisabled } = resolveEnvTargets(config, opts.env));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const name of skippedDisabled) {
|
|
39
|
+
log.info(`Skipping disabled environment "${name}"`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (targets.length === 0) {
|
|
43
|
+
console.error(chalk.red('No enabled environments to roll back.'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
24
46
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const isAll = opts.env === 'all';
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const { results, failures } = await rollbackToVersion(
|
|
51
|
+
config,
|
|
52
|
+
versionOrBuildId,
|
|
53
|
+
process.cwd(),
|
|
54
|
+
{
|
|
55
|
+
envNames: targets,
|
|
56
|
+
// --env all: attempt every enabled env independently; summarize at end.
|
|
57
|
+
continueOnError: isAll,
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (isAll) {
|
|
62
|
+
const summary = formatRollbackAllSummary(
|
|
63
|
+
results || [],
|
|
64
|
+
failures || [],
|
|
65
|
+
skippedDisabled
|
|
66
|
+
);
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(summary);
|
|
69
|
+
if ((failures || []).length > 0) {
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
for (const r of results || []) {
|
|
74
|
+
if (!versionOrBuildId) {
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.gray(`Rolled back ${r.envName} to previous build: ${r.entry.buildId}`)
|
|
77
|
+
);
|
|
35
78
|
}
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.log(
|
|
38
|
-
chalk.yellow(
|
|
39
|
-
`Health check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
40
|
-
)
|
|
41
|
-
);
|
|
42
79
|
}
|
|
43
|
-
}
|
|
44
80
|
|
|
45
|
-
|
|
81
|
+
if (config.healthCheck?.url) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await axios.get(config.healthCheck.url, {
|
|
84
|
+
timeout: (config.healthCheck.timeout || 30) * 1000,
|
|
85
|
+
validateStatus: () => true,
|
|
86
|
+
});
|
|
87
|
+
if (response.status >= 200 && response.status < 400) {
|
|
88
|
+
console.log(chalk.green(`Health check passed: HTTP ${response.status}`));
|
|
89
|
+
} else {
|
|
90
|
+
console.log(chalk.yellow(`Health check returned HTTP ${response.status}`));
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.yellow(
|
|
95
|
+
`Health check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const summary = (results || [])
|
|
102
|
+
.map((r) => `${r.envName}→${r.entry.buildId}`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
console.log(chalk.green(`✓ Rolled back: ${summary}`));
|
|
105
|
+
}
|
|
46
106
|
} catch (err) {
|
|
47
107
|
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
48
108
|
process.exit(1);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
+
import { resolveContainerPort } from '../utils/dockerfile-expose.js';
|
|
4
|
+
import {
|
|
5
|
+
getDefaultKubernetesManifestPaths,
|
|
6
|
+
syncKubernetesManifestPorts,
|
|
7
|
+
} from '../utils/kubernetes-manifests.js';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Surgically fix containerPort/targetPort in existing k8s manifests from Dockerfile EXPOSE.
|
|
12
|
+
* @param {import('commander').Command} program
|
|
13
|
+
*/
|
|
14
|
+
export function registerSyncK8sPortsCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('sync-k8s-ports')
|
|
17
|
+
.description(
|
|
18
|
+
'Update only containerPort/targetPort in k8s/deployment.yaml and k8s/service.yaml ' +
|
|
19
|
+
'from the Dockerfile EXPOSE port (or config/fallback). Does not change replicas, ' +
|
|
20
|
+
'resources, env, probes, or Service port. Heavily customized / multi-container ' +
|
|
21
|
+
'manifests may need a manual review.'
|
|
22
|
+
)
|
|
23
|
+
.action(async () => {
|
|
24
|
+
loadEnv();
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const config = await loadConfig(cwd);
|
|
27
|
+
|
|
28
|
+
const { deploymentPath, servicePath } = getDefaultKubernetesManifestPaths(cwd);
|
|
29
|
+
const hasDeployment = await fs.pathExists(deploymentPath);
|
|
30
|
+
const hasService = await fs.pathExists(servicePath);
|
|
31
|
+
|
|
32
|
+
if (!hasDeployment && !hasService) {
|
|
33
|
+
console.log(
|
|
34
|
+
chalk.yellow(
|
|
35
|
+
'No k8s/deployment.yaml or k8s/service.yaml found — nothing to patch. ' +
|
|
36
|
+
'Generate starter manifests via deployhub init, or add manifests under ./k8s/.'
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { port, source } = await resolveContainerPort(cwd, config);
|
|
43
|
+
const result = await syncKubernetesManifestPorts(cwd, port);
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
chalk.green(
|
|
47
|
+
`✓ Resolved container port ${port} (source: ${source})`
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (result.patched.length === 0) {
|
|
52
|
+
console.log(chalk.gray(' Port fields already match — no files changed.'));
|
|
53
|
+
} else {
|
|
54
|
+
for (const file of result.patched) {
|
|
55
|
+
console.log(chalk.gray(` Updated ${file}`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.yellow(
|
|
62
|
+
'Note: Only containerPort and targetPort are updated. Replicas, resources, env, ' +
|
|
63
|
+
'probes, and Service port: are left unchanged. If your manifests are heavily ' +
|
|
64
|
+
'customized (multiple containers, non-standard field layout), review the diff ' +
|
|
65
|
+
'manually before deploying.'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { registerSyncK8sPortsCommand };
|
package/src/commands/verify.js
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import axios from 'axios';
|
|
3
2
|
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
+
import { resolveEnvTargets } from '../core/environments.js';
|
|
4
|
+
import {
|
|
5
|
+
anyEnvHasResolvableHealthCheckUrl,
|
|
6
|
+
runHealthChecksForEnvs,
|
|
7
|
+
formatHealthCheckAllSummary,
|
|
8
|
+
} from '../utils/health-check.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Standalone verify — same per-env URL resolution and summary as the deploy pipeline stage.
|
|
12
|
+
*
|
|
13
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
14
|
+
* @param {string|undefined} envFlag — omitted = defaultEnvironment; `"all"` = every enabled
|
|
15
|
+
* @param {{ httpGet?: (url: string, timeoutMs: number) => Promise<{ status: number }> }} [options]
|
|
16
|
+
* @returns {Promise<{ ok: boolean, targets: string[], results: object[], failures: object[], summary: string, message: string }>}
|
|
17
|
+
*/
|
|
18
|
+
export async function runVerify(config, envFlag, options = {}) {
|
|
19
|
+
const { targets, skippedDisabled } = resolveEnvTargets(config, envFlag);
|
|
20
|
+
|
|
21
|
+
if (targets.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
targets,
|
|
25
|
+
results: [],
|
|
26
|
+
failures: [],
|
|
27
|
+
summary: '',
|
|
28
|
+
message: 'No enabled environments to verify.',
|
|
29
|
+
skippedDisabled,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!anyEnvHasResolvableHealthCheckUrl(config, targets)) {
|
|
34
|
+
const label =
|
|
35
|
+
targets.length === 1
|
|
36
|
+
? `environment "${targets[0]}"`
|
|
37
|
+
: 'the selected environment(s)';
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
targets,
|
|
41
|
+
results: [],
|
|
42
|
+
failures: [],
|
|
43
|
+
summary: '',
|
|
44
|
+
message: `No health check URL configured for ${label}. Set healthCheck.url or environments.<name>.config.healthCheckUrl.`,
|
|
45
|
+
skippedDisabled,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { results, failures } = await runHealthChecksForEnvs(config, targets, options);
|
|
50
|
+
const multi = targets.length > 1 || failures.length > 0;
|
|
51
|
+
const summary = multi ? formatHealthCheckAllSummary(results, failures) : '';
|
|
52
|
+
|
|
53
|
+
let message = '';
|
|
54
|
+
if (failures.length === 0 && results.length === 1 && !multi) {
|
|
55
|
+
const r = results[0];
|
|
56
|
+
message = `Health check passed (${r.envName}): HTTP ${r.status} (${r.elapsed}ms)`;
|
|
57
|
+
} else if (failures.length === 0 && results.length === 0) {
|
|
58
|
+
message = `No health check URL configured for the selected environment(s).`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: failures.length === 0 && results.length > 0,
|
|
63
|
+
targets,
|
|
64
|
+
results,
|
|
65
|
+
failures,
|
|
66
|
+
summary,
|
|
67
|
+
message:
|
|
68
|
+
message ||
|
|
69
|
+
(failures.length > 0
|
|
70
|
+
? failures[0].error
|
|
71
|
+
: `All ${results.length} environment(s) passed health checks.`),
|
|
72
|
+
skippedDisabled,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
4
75
|
|
|
5
76
|
/**
|
|
6
77
|
* @param {import('commander').Command} program
|
|
@@ -8,48 +79,41 @@ import { loadConfig, loadEnv } from '../core/config.js';
|
|
|
8
79
|
export function registerVerifyCommand(program) {
|
|
9
80
|
program
|
|
10
81
|
.command('verify')
|
|
11
|
-
.description('Run health check on configured endpoint')
|
|
12
|
-
.
|
|
82
|
+
.description('Run health check on configured endpoint (default environment, or --env)')
|
|
83
|
+
.option(
|
|
84
|
+
'--env <name>',
|
|
85
|
+
'Environment to verify (name, or "all" for every enabled environment)'
|
|
86
|
+
)
|
|
87
|
+
.action(async (opts) => {
|
|
13
88
|
loadEnv();
|
|
14
89
|
const config = await loadConfig();
|
|
15
|
-
const url = config.healthCheck?.url;
|
|
16
90
|
|
|
17
|
-
|
|
18
|
-
|
|
91
|
+
let outcome;
|
|
92
|
+
try {
|
|
93
|
+
outcome = await runVerify(config, opts.env);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
19
96
|
process.exit(1);
|
|
20
97
|
}
|
|
21
98
|
|
|
22
|
-
const
|
|
23
|
-
|
|
99
|
+
for (const name of outcome.skippedDisabled || []) {
|
|
100
|
+
console.log(chalk.gray(`Skipping disabled environment "${name}"`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (outcome.summary) {
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(outcome.summary);
|
|
106
|
+
} else if (outcome.ok && outcome.message) {
|
|
107
|
+
console.log(chalk.green(`✓ ${outcome.message}`));
|
|
108
|
+
}
|
|
24
109
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
validateStatus: () => true,
|
|
29
|
-
});
|
|
30
|
-
const elapsed = Date.now() - start;
|
|
31
|
-
const ok = response.status >= 200 && response.status < 400;
|
|
32
|
-
|
|
33
|
-
if (ok) {
|
|
34
|
-
console.log(
|
|
35
|
-
chalk.green(`✓ Health check passed: HTTP ${response.status} (${elapsed}ms)`)
|
|
36
|
-
);
|
|
37
|
-
} else {
|
|
38
|
-
console.log(
|
|
39
|
-
chalk.red(`✗ Health check failed: HTTP ${response.status} (${elapsed}ms)`)
|
|
40
|
-
);
|
|
41
|
-
process.exit(1);
|
|
110
|
+
if (!outcome.ok) {
|
|
111
|
+
if (!outcome.summary && outcome.message) {
|
|
112
|
+
console.error(chalk.red(`✗ ${outcome.message}`));
|
|
42
113
|
}
|
|
43
|
-
} catch (err) {
|
|
44
|
-
const elapsed = Date.now() - start;
|
|
45
|
-
console.error(
|
|
46
|
-
chalk.red(
|
|
47
|
-
`✗ Health check failed: ${err instanceof Error ? err.message : String(err)} (${elapsed}ms)`
|
|
48
|
-
)
|
|
49
|
-
);
|
|
50
114
|
process.exit(1);
|
|
51
115
|
}
|
|
52
116
|
});
|
|
53
117
|
}
|
|
54
118
|
|
|
55
|
-
export default { registerVerifyCommand };
|
|
119
|
+
export default { registerVerifyCommand, runVerify };
|