@akash-chowdhury-24/deployhub 2.0.15 → 2.0.17
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 +47 -16
- package/package.json +1 -1
- package/src/artifact/engine.js +11 -4
- package/src/cli/index.js +2 -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 +111 -64
- package/src/commands/rollback.js +84 -24
- package/src/commands/sync-workflows.js +4 -1
- 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 +202 -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/github-actions.js +414 -33
- package/src/utils/health-check.js +158 -0
- package/src/utils/kubernetes-manifests.js +3 -1
- package/src/utils/nginx.js +66 -15
- package/src/utils/rollback/engine.js +147 -40
- package/src/utils/scaffold.js +2 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import {
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadEnv,
|
|
6
|
+
saveConfig,
|
|
7
|
+
} from '../core/config.js';
|
|
8
|
+
import {
|
|
9
|
+
getEnabledEnvironmentNames,
|
|
10
|
+
getEnvMethod,
|
|
11
|
+
getEnvTrigger,
|
|
12
|
+
isEnvEnabled,
|
|
13
|
+
resolveDefaultEnvironmentName,
|
|
14
|
+
validateEnvironmentName,
|
|
15
|
+
} from '../core/environments.js';
|
|
16
|
+
import { promptServerDeployment, buildServerEnvEntry } from '../deployment/init-prompts.js';
|
|
17
|
+
import { writeWorkflowFile } from '../utils/github-actions.js';
|
|
18
|
+
import { loadEnvArtifactHistory } from '../storage/index.js';
|
|
19
|
+
import { createLogger } from '../logger/index.js';
|
|
20
|
+
import {
|
|
21
|
+
envUsesPrefixedSecrets,
|
|
22
|
+
getDeploymentWorkflowSecretKeysForEnv,
|
|
23
|
+
} from '../deployment/deployment-env.js';
|
|
24
|
+
import {
|
|
25
|
+
envHistoryRemoteKey,
|
|
26
|
+
envLatestArtifactRemoteKey,
|
|
27
|
+
} from '../utils/build-id.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Regenerate GitHub Actions workflows after env enable/disable/add/remove.
|
|
31
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
32
|
+
* @param {string} cwd
|
|
33
|
+
*/
|
|
34
|
+
async function regenerateWorkflows(config, cwd) {
|
|
35
|
+
await writeWorkflowFile(
|
|
36
|
+
config.storage || [],
|
|
37
|
+
getEnabledEnvironmentNames(config),
|
|
38
|
+
config.environments || {},
|
|
39
|
+
cwd,
|
|
40
|
+
config.cli?.source,
|
|
41
|
+
config
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Best-effort last buildId for an env from per-env deploy history.
|
|
46
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
47
|
+
* @param {string} envName
|
|
48
|
+
* @returns {Promise<string>}
|
|
49
|
+
*/
|
|
50
|
+
async function lastDeployedBuildId(config, envName) {
|
|
51
|
+
try {
|
|
52
|
+
const providers = config.storage || [];
|
|
53
|
+
if (providers.length === 0) return '—';
|
|
54
|
+
const { entries } = await loadEnvArtifactHistory(providers, config.project, envName, {
|
|
55
|
+
defaultEnvironment: resolveDefaultEnvironmentName(config),
|
|
56
|
+
});
|
|
57
|
+
if (entries[0]?.buildId) return entries[0].buildId;
|
|
58
|
+
} catch {
|
|
59
|
+
// list never crashes
|
|
60
|
+
}
|
|
61
|
+
return '—';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {import('commander').Command} program
|
|
66
|
+
*/
|
|
67
|
+
export function registerEnvCommand(program) {
|
|
68
|
+
const env = program.command('env').description('Manage deployment environments');
|
|
69
|
+
|
|
70
|
+
env
|
|
71
|
+
.command('list')
|
|
72
|
+
.description('List configured environments')
|
|
73
|
+
.action(async () => {
|
|
74
|
+
loadEnv();
|
|
75
|
+
const config = await loadConfig();
|
|
76
|
+
const names = Object.keys(config.environments || {});
|
|
77
|
+
if (names.length === 0) {
|
|
78
|
+
console.log(chalk.yellow('No environments configured. Run: deployhub env add <name>'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const def = resolveDefaultEnvironmentName(config);
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(
|
|
85
|
+
chalk.bold(
|
|
86
|
+
`${'Name'.padEnd(18)} ${'Method'.padEnd(12)} ${'Status'.padEnd(10)} ${'Trigger'.padEnd(8)} Last deploy`
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
console.log(chalk.gray('─'.repeat(72)));
|
|
90
|
+
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const entry = config.environments[name];
|
|
93
|
+
const method = getEnvMethod(entry) || '?';
|
|
94
|
+
const status = isEnvEnabled(entry) ? 'enabled' : 'disabled';
|
|
95
|
+
const trigger = getEnvTrigger(entry);
|
|
96
|
+
const last = await lastDeployedBuildId(config, name);
|
|
97
|
+
const label = name === def ? `${name}*` : name;
|
|
98
|
+
console.log(
|
|
99
|
+
`${label.padEnd(18)} ${method.padEnd(12)} ${status.padEnd(10)} ${trigger.padEnd(8)} ${last}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(chalk.gray('* defaultEnvironment'));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
env
|
|
107
|
+
.command('add <name>')
|
|
108
|
+
.description('Add a named deployment environment')
|
|
109
|
+
.option(
|
|
110
|
+
'--method <type>',
|
|
111
|
+
'Deployment method (ssh|docker|ec2|azure-vm|gcp-vm|kubernetes); skips the type prompt'
|
|
112
|
+
)
|
|
113
|
+
.option(
|
|
114
|
+
'--yes',
|
|
115
|
+
'Non-interactive: use defaults for the chosen --method (required with --yes)'
|
|
116
|
+
)
|
|
117
|
+
.action(async (rawName, opts) => {
|
|
118
|
+
loadEnv();
|
|
119
|
+
const cwd = process.cwd();
|
|
120
|
+
const config = await loadConfig(cwd);
|
|
121
|
+
const log = createLogger('env');
|
|
122
|
+
|
|
123
|
+
const nameCheck = validateEnvironmentName(rawName, Object.keys(config.environments || {}));
|
|
124
|
+
if (!nameCheck.ok) {
|
|
125
|
+
console.error(chalk.red(nameCheck.error));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const name = nameCheck.name;
|
|
129
|
+
|
|
130
|
+
if (opts.yes && !opts.method) {
|
|
131
|
+
console.error(
|
|
132
|
+
chalk.red('Non-interactive --yes requires --method <ssh|docker|ec2|azure-vm|gcp-vm|kubernetes>.')
|
|
133
|
+
);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const projectType = config.projectType || 'frontend';
|
|
138
|
+
const backendConfig = config.backend || null;
|
|
139
|
+
const singleConfig =
|
|
140
|
+
projectType === 'both'
|
|
141
|
+
? null
|
|
142
|
+
: {
|
|
143
|
+
framework: config.framework,
|
|
144
|
+
port: config.port,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const existingNames = Object.keys(config.environments || {});
|
|
148
|
+
// Permanently grandfather the original environment's unprefixed secrets.
|
|
149
|
+
if (!config.unprefixedSecretEnvironment && existingNames.length === 1) {
|
|
150
|
+
config.unprefixedSecretEnvironment = existingNames[0];
|
|
151
|
+
} else if (!config.unprefixedSecretEnvironment && existingNames.length === 0) {
|
|
152
|
+
// First env ever — this new one stays unprefixed.
|
|
153
|
+
config.unprefixedSecretEnvironment = name;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let deployAnswers;
|
|
157
|
+
try {
|
|
158
|
+
deployAnswers = await promptServerDeployment(
|
|
159
|
+
config.project,
|
|
160
|
+
projectType,
|
|
161
|
+
backendConfig,
|
|
162
|
+
{
|
|
163
|
+
envName: name,
|
|
164
|
+
existingEnvNames: Object.keys(config.environments || {}),
|
|
165
|
+
deployType: opts.method,
|
|
166
|
+
nonInteractive: Boolean(opts.yes),
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
config.environments[name] = buildServerEnvEntry(
|
|
175
|
+
deployAnswers,
|
|
176
|
+
projectType,
|
|
177
|
+
config.project,
|
|
178
|
+
backendConfig,
|
|
179
|
+
singleConfig
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!config.defaultEnvironment) {
|
|
183
|
+
config.defaultEnvironment = name;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
config.pipeline = config.pipeline || {};
|
|
187
|
+
config.pipeline.deploy = true;
|
|
188
|
+
|
|
189
|
+
await saveConfig(config, cwd);
|
|
190
|
+
|
|
191
|
+
await regenerateWorkflows(config, cwd);
|
|
192
|
+
|
|
193
|
+
log.success(`Added environment "${name}" (${deployAnswers.deployType})`);
|
|
194
|
+
console.log(chalk.gray(` defaultEnvironment: ${config.defaultEnvironment}`));
|
|
195
|
+
console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
|
|
196
|
+
|
|
197
|
+
const envCount = Object.keys(config.environments).length;
|
|
198
|
+
if (envCount >= 2 && envUsesPrefixedSecrets(name, config)) {
|
|
199
|
+
const original = config.unprefixedSecretEnvironment;
|
|
200
|
+
const method = getEnvMethod(config.environments[name]);
|
|
201
|
+
const newSecrets = method
|
|
202
|
+
? getDeploymentWorkflowSecretKeysForEnv(name, method, config, config.environments)
|
|
203
|
+
: [];
|
|
204
|
+
const requiredNew = newSecrets.filter((k) => !k.includes('OPTIONAL'));
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(chalk.yellow(`⚠ You now have ${envCount} environments configured.`));
|
|
207
|
+
if (original) {
|
|
208
|
+
console.log(
|
|
209
|
+
chalk.yellow(
|
|
210
|
+
` "${original}" keeps using its existing unprefixed secrets (e.g. SSH_HOST) — no action needed.`
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
console.log(
|
|
215
|
+
chalk.yellow(
|
|
216
|
+
` "${name}" requires new secrets in CI: ${requiredNew.slice(0, 8).join(', ')}${
|
|
217
|
+
requiredNew.length > 8 ? ', …' : ''
|
|
218
|
+
}`
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
console.log(
|
|
222
|
+
chalk.yellow(
|
|
223
|
+
' Add these to your GitHub repo secrets before running a deploy/rollback for this environment in CI.'
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
console.log(chalk.yellow(' Run `deployhub sync-workflows` after adding them (already regenerated).'));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
env
|
|
231
|
+
.command('enable <name>')
|
|
232
|
+
.description('Enable an environment for deploy / --env all')
|
|
233
|
+
.action(async (name) => {
|
|
234
|
+
loadEnv();
|
|
235
|
+
const cwd = process.cwd();
|
|
236
|
+
const config = await loadConfig(cwd);
|
|
237
|
+
if (!config.environments[name]) {
|
|
238
|
+
console.error(chalk.red(`Environment "${name}" not found.`));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
config.environments[name].enabled = true;
|
|
242
|
+
await saveConfig(config, cwd);
|
|
243
|
+
await regenerateWorkflows(config, cwd);
|
|
244
|
+
console.log(chalk.green(`✓ Enabled environment "${name}"`));
|
|
245
|
+
console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
env
|
|
249
|
+
.command('disable <name>')
|
|
250
|
+
.description('Disable an environment (deploy to it will fail until re-enabled)')
|
|
251
|
+
.action(async (name) => {
|
|
252
|
+
loadEnv();
|
|
253
|
+
const cwd = process.cwd();
|
|
254
|
+
const config = await loadConfig(cwd);
|
|
255
|
+
if (!config.environments[name]) {
|
|
256
|
+
console.error(chalk.red(`Environment "${name}" not found.`));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
config.environments[name].enabled = false;
|
|
260
|
+
await saveConfig(config, cwd);
|
|
261
|
+
await regenerateWorkflows(config, cwd);
|
|
262
|
+
console.log(chalk.yellow(`Disabled environment "${name}"`));
|
|
263
|
+
console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
env
|
|
267
|
+
.command('remove <name>')
|
|
268
|
+
.description('Remove an environment and its rollback history reference')
|
|
269
|
+
.action(async (name) => {
|
|
270
|
+
loadEnv();
|
|
271
|
+
const cwd = process.cwd();
|
|
272
|
+
const config = await loadConfig(cwd);
|
|
273
|
+
if (!config.environments[name]) {
|
|
274
|
+
console.error(chalk.red(`Environment "${name}" not found.`));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { confirm } = await inquirer.prompt([
|
|
279
|
+
{
|
|
280
|
+
type: 'confirm',
|
|
281
|
+
name: 'confirm',
|
|
282
|
+
message: `Remove environment "${name}"? Its per-environment rollback history will no longer be tracked by DeployHub.`,
|
|
283
|
+
default: false,
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
if (!confirm) {
|
|
287
|
+
console.log(chalk.gray('Cancelled.'));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (config.defaultEnvironment === name) {
|
|
292
|
+
const remaining = Object.keys(config.environments).filter((n) => n !== name);
|
|
293
|
+
if (remaining.length > 0) {
|
|
294
|
+
console.error(
|
|
295
|
+
chalk.red(
|
|
296
|
+
`Cannot remove "${name}" while it is defaultEnvironment.\n` +
|
|
297
|
+
` Set a new default first (edit defaultEnvironment in deployhub.config.json), ` +
|
|
298
|
+
`then run deployhub env remove ${name} again.`
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const historyKey = envHistoryRemoteKey(config.project, name);
|
|
306
|
+
const latestKey = envLatestArtifactRemoteKey(config.project, name);
|
|
307
|
+
|
|
308
|
+
delete config.environments[name];
|
|
309
|
+
if (config.defaultEnvironment === name) {
|
|
310
|
+
config.defaultEnvironment = undefined;
|
|
311
|
+
}
|
|
312
|
+
await saveConfig(config, cwd);
|
|
313
|
+
|
|
314
|
+
await regenerateWorkflows(config, cwd);
|
|
315
|
+
|
|
316
|
+
console.log(chalk.green(`✓ Removed environment "${name}"`));
|
|
317
|
+
console.log(
|
|
318
|
+
chalk.yellow(
|
|
319
|
+
` Storage not deleted (orphaned): ${historyKey}, ${latestKey}\n` +
|
|
320
|
+
' Remove these manually from your storage provider if you no longer need rollback data.'
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export default { registerEnvCommand };
|
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,
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
promptServerDeployment,
|
|
29
29
|
buildServerEnvEntry,
|
|
30
30
|
getDockerEnvSecrets,
|
|
31
|
+
applyInitTriggerDefaults,
|
|
32
|
+
formatMultiEnvTriggerReminder,
|
|
31
33
|
SSH_BASED,
|
|
32
34
|
} from '../deployment/init-prompts.js';
|
|
33
35
|
import {
|
|
@@ -174,10 +176,11 @@ async function promptBuildSettings(defaults, side) {
|
|
|
174
176
|
async function generateProjectScaffold(config, environments, cwd) {
|
|
175
177
|
const envList = Object.values(environments);
|
|
176
178
|
|
|
177
|
-
const usesFrontendSsh = envList.some(
|
|
178
|
-
(env)
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
const usesFrontendSsh = envList.some((env) => {
|
|
180
|
+
const method = getEnvMethod(env);
|
|
181
|
+
const settings = getEnvSettings(env);
|
|
182
|
+
return SSH_BASED.includes(method) || settings.deploymentType === 'server';
|
|
183
|
+
});
|
|
181
184
|
|
|
182
185
|
const isFrontendProject =
|
|
183
186
|
config.projectType === 'frontend' || config.projectType === 'both';
|
|
@@ -343,84 +346,116 @@ export function registerInitCommand(program) {
|
|
|
343
346
|
let dockerEnvToAppend = null;
|
|
344
347
|
|
|
345
348
|
if (answers.configureDeploy) {
|
|
346
|
-
|
|
349
|
+
const { envMode } = await inquirer.prompt([
|
|
350
|
+
{
|
|
351
|
+
type: 'list',
|
|
352
|
+
name: 'envMode',
|
|
353
|
+
message:
|
|
354
|
+
'How many environments do you want to set up? (1 is fine — you can add more later)',
|
|
355
|
+
choices: [
|
|
356
|
+
{ name: 'Just one environment', value: 'one' },
|
|
357
|
+
{
|
|
358
|
+
name: 'Multiple environments (e.g. development, testing, production)',
|
|
359
|
+
value: 'multi',
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
default: 'one',
|
|
363
|
+
},
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Shared single-round prompt used by init (loop) and env add.
|
|
368
|
+
* @param {{ envName?: string }} [opts]
|
|
369
|
+
*/
|
|
370
|
+
async function promptAndStoreEnvironment(opts = {}) {
|
|
371
|
+
const deployTypeForPrompt =
|
|
372
|
+
projectType === 'both' ? 'both' : projectType;
|
|
347
373
|
const deployAnswers = await promptServerDeployment(
|
|
348
374
|
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,
|
|
375
|
+
deployTypeForPrompt,
|
|
358
376
|
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
|
|
377
|
+
{
|
|
378
|
+
...(opts.envName ? { envName: opts.envName } : {}),
|
|
379
|
+
existingEnvNames: Object.keys(environments),
|
|
380
|
+
}
|
|
372
381
|
);
|
|
373
382
|
primaryDeployType = deployAnswers.deployType;
|
|
374
|
-
|
|
375
|
-
|
|
383
|
+
const name = deployAnswers.envName;
|
|
384
|
+
deploy.push(name);
|
|
385
|
+
|
|
386
|
+
let entry = buildServerEnvEntry(
|
|
376
387
|
deployAnswers,
|
|
377
388
|
projectType,
|
|
378
389
|
projectName,
|
|
379
390
|
backendConfig,
|
|
380
391
|
singleConfig
|
|
381
392
|
);
|
|
393
|
+
|
|
394
|
+
if (projectType === 'both' && entry.config) {
|
|
395
|
+
entry = {
|
|
396
|
+
...entry,
|
|
397
|
+
config: {
|
|
398
|
+
...entry.config,
|
|
399
|
+
backendDeploymentType: 'server',
|
|
400
|
+
frontendDeploymentType: 'server',
|
|
401
|
+
deploymentType: 'server',
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
environments[name] = entry;
|
|
407
|
+
|
|
382
408
|
if (deployAnswers.healthUrl) {
|
|
383
409
|
healthUrl = deployAnswers.healthUrl;
|
|
410
|
+
} else if (!healthUrl && (singleConfig?.port || backendConfig?.port)) {
|
|
411
|
+
healthUrl = `http://localhost:${singleConfig?.port || backendConfig?.port}/health`;
|
|
384
412
|
}
|
|
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
413
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
healthUrl = `http://localhost:${backendConfig.port}/health`;
|
|
414
|
+
const secrets = getDockerEnvSecrets(deployAnswers);
|
|
415
|
+
if (secrets) {
|
|
416
|
+
dockerEnvToAppend = { ...(dockerEnvToAppend || {}), ...secrets };
|
|
417
417
|
}
|
|
418
|
+
return name;
|
|
419
|
+
}
|
|
418
420
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
+
if (envMode === 'one') {
|
|
422
|
+
// Transparent single env named "default" (visible later via env list).
|
|
423
|
+
await promptAndStoreEnvironment({ envName: 'default' });
|
|
424
|
+
} else {
|
|
425
|
+
let addMore = true;
|
|
426
|
+
while (addMore) {
|
|
427
|
+
await promptAndStoreEnvironment();
|
|
428
|
+
if (deploy.length >= 1) {
|
|
429
|
+
const again = await inquirer.prompt([
|
|
430
|
+
{
|
|
431
|
+
type: 'confirm',
|
|
432
|
+
name: 'more',
|
|
433
|
+
message: 'Add another environment?',
|
|
434
|
+
default: false,
|
|
435
|
+
},
|
|
436
|
+
]);
|
|
437
|
+
addMore = again.more;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
421
440
|
}
|
|
422
441
|
}
|
|
423
442
|
|
|
443
|
+
let defaultEnvironment = deploy[0];
|
|
444
|
+
if (deploy.length >= 2) {
|
|
445
|
+
const picked = await inquirer.prompt([
|
|
446
|
+
{
|
|
447
|
+
type: 'list',
|
|
448
|
+
name: 'defaultEnvironment',
|
|
449
|
+
message: 'Which environment is the default for `deployhub deploy`?',
|
|
450
|
+
choices: deploy,
|
|
451
|
+
default: deploy[0],
|
|
452
|
+
},
|
|
453
|
+
]);
|
|
454
|
+
defaultEnvironment = picked.defaultEnvironment;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
applyInitTriggerDefaults(environments, deploy, defaultEnvironment);
|
|
458
|
+
|
|
424
459
|
const version = await getProjectVersion(cwd);
|
|
425
460
|
let hasDocker =
|
|
426
461
|
(detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
|
|
@@ -432,7 +467,9 @@ export function registerInitCommand(program) {
|
|
|
432
467
|
projectType,
|
|
433
468
|
artifact: true,
|
|
434
469
|
storage: answers.storage.length > 0 ? answers.storage : ['local'],
|
|
435
|
-
|
|
470
|
+
defaultEnvironment: defaultEnvironment || undefined,
|
|
471
|
+
unprefixedSecretEnvironment: defaultEnvironment || undefined,
|
|
472
|
+
legacyHistoryMigrated: true,
|
|
436
473
|
environments,
|
|
437
474
|
healthCheck: {
|
|
438
475
|
url: healthUrl,
|
|
@@ -538,6 +575,16 @@ export function registerInitCommand(program) {
|
|
|
538
575
|
console.log(' • .github/workflows/deployhub-rollback.yml');
|
|
539
576
|
console.log(' • .env.example');
|
|
540
577
|
console.log('');
|
|
578
|
+
|
|
579
|
+
if (deploy.length >= 2 && defaultEnvironment) {
|
|
580
|
+
console.log(
|
|
581
|
+
chalk.yellow(
|
|
582
|
+
formatMultiEnvTriggerReminder(String(defaultEnvironment), deploy)
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
console.log('');
|
|
586
|
+
}
|
|
587
|
+
|
|
541
588
|
printAuthorFooter();
|
|
542
589
|
});
|
|
543
590
|
}
|
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);
|