@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
package/src/deployment/index.js
CHANGED
|
@@ -5,6 +5,15 @@ import { createAzureVmProvider } from './providers/azure-vm.js';
|
|
|
5
5
|
import { createGcpVmProvider } from './providers/gcp-vm.js';
|
|
6
6
|
import { createKubernetesProvider } from './providers/kubernetes.js';
|
|
7
7
|
import { createLogger } from '../logger/index.js';
|
|
8
|
+
import {
|
|
9
|
+
getEnabledEnvironmentNames,
|
|
10
|
+
getEnvMethod,
|
|
11
|
+
} from '../core/environments.js';
|
|
12
|
+
import { applyEnvSecretOverlay } from './deployment-env.js';
|
|
13
|
+
import { recordEnvDeployment } from '../storage/index.js';
|
|
14
|
+
import { buildArtifactRemoteKey } from '../utils/build-id.js';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs-extra';
|
|
8
17
|
|
|
9
18
|
/** @type {Record<string, Function>} */
|
|
10
19
|
const PROVIDER_FACTORIES = {
|
|
@@ -28,12 +37,13 @@ export function getDeploymentProvider(type, config, envName, env = process.env)
|
|
|
28
37
|
throw new Error(`Environment "${envName}" not found in config`);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
const providerType = type || environment
|
|
40
|
+
const providerType = type || getEnvMethod(environment);
|
|
32
41
|
const factory = PROVIDER_FACTORIES[providerType];
|
|
33
42
|
if (!factory) {
|
|
34
43
|
throw new Error(`Unknown deployment provider: ${providerType}`);
|
|
35
44
|
}
|
|
36
|
-
|
|
45
|
+
const resolvedEnv = applyEnvSecretOverlay(envName, config, env);
|
|
46
|
+
return factory(config, envName, resolvedEnv);
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/**
|
|
@@ -43,13 +53,26 @@ export function getDeploymentProvider(type, config, envName, env = process.env)
|
|
|
43
53
|
*/
|
|
44
54
|
export async function deployToAll(config, artifactDir, envNames) {
|
|
45
55
|
const log = createLogger('deploy');
|
|
46
|
-
const targets = envNames || config
|
|
56
|
+
const targets = envNames || getEnabledEnvironmentNames(config);
|
|
47
57
|
|
|
48
58
|
if (targets.length === 0) {
|
|
49
59
|
log.warn('No deployment targets configured, skipping');
|
|
50
60
|
return [];
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
const buildId =
|
|
64
|
+
config.buildId ||
|
|
65
|
+
(await fs.readJson(path.join(artifactDir, 'metadata.json')).catch(() => ({}))).buildId;
|
|
66
|
+
if (buildId) {
|
|
67
|
+
config.buildId = String(buildId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const zipCandidates = [
|
|
71
|
+
path.join(artifactDir, 'artifact.zip'),
|
|
72
|
+
path.join(artifactDir, `${config.project || 'artifact'}.zip`),
|
|
73
|
+
];
|
|
74
|
+
let zipPath = zipCandidates.find((p) => fs.existsSync(p)) || null;
|
|
75
|
+
|
|
53
76
|
const deployed = [];
|
|
54
77
|
for (const envName of targets) {
|
|
55
78
|
const envConfig = config.environments[envName];
|
|
@@ -57,11 +80,39 @@ export async function deployToAll(config, artifactDir, envNames) {
|
|
|
57
80
|
throw new Error(`Environment "${envName}" not found in config`);
|
|
58
81
|
}
|
|
59
82
|
|
|
60
|
-
const
|
|
61
|
-
|
|
83
|
+
const method = getEnvMethod(envConfig);
|
|
84
|
+
const provider = getDeploymentProvider(method, config, envName);
|
|
85
|
+
log.info(`Deploying to ${envName} (${method})...`);
|
|
62
86
|
await provider.deploy(artifactDir);
|
|
63
87
|
deployed.push(envName);
|
|
64
88
|
log.success(`Deployed to ${envName}`);
|
|
89
|
+
|
|
90
|
+
// Per-env deploy history (isolated). Shared builds/{buildId} is unchanged.
|
|
91
|
+
if (config.buildId && (config.storage || []).length > 0) {
|
|
92
|
+
try {
|
|
93
|
+
if (!zipPath || !(await fs.pathExists(zipPath))) {
|
|
94
|
+
const files = await fs.readdir(artifactDir).catch(() => []);
|
|
95
|
+
const found = files.find((f) => f.endsWith('.zip'));
|
|
96
|
+
zipPath = found ? path.join(artifactDir, found) : zipPath;
|
|
97
|
+
}
|
|
98
|
+
await recordEnvDeployment(
|
|
99
|
+
config.storage,
|
|
100
|
+
zipPath || '',
|
|
101
|
+
config,
|
|
102
|
+
envName,
|
|
103
|
+
{
|
|
104
|
+
buildId: String(config.buildId),
|
|
105
|
+
semver: config.version,
|
|
106
|
+
remoteKey: buildArtifactRemoteKey(config.project, String(config.buildId)),
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Deploy to ${envName} succeeded but recording env history failed: ${msg}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
65
116
|
}
|
|
66
117
|
|
|
67
118
|
return deployed;
|
|
@@ -74,10 +125,10 @@ export async function deployToAll(config, artifactDir, envNames) {
|
|
|
74
125
|
* @param {{ buildId?: string, semver?: string, remoteKey?: string }} [meta]
|
|
75
126
|
*/
|
|
76
127
|
export async function rollbackAll(config, artifactDir, envNames, meta) {
|
|
77
|
-
const targets = envNames || config
|
|
128
|
+
const targets = envNames || getEnabledEnvironmentNames(config);
|
|
78
129
|
for (const envName of targets) {
|
|
79
130
|
const envConfig = config.environments[envName];
|
|
80
|
-
const provider = getDeploymentProvider(envConfig
|
|
131
|
+
const provider = getDeploymentProvider(getEnvMethod(envConfig), config, envName);
|
|
81
132
|
await provider.rollback(artifactDir, meta);
|
|
82
133
|
}
|
|
83
134
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { createEnvNamePromptValidate } from '../core/environments.js';
|
|
3
4
|
import {
|
|
4
5
|
suggestSshUser,
|
|
5
6
|
listKubeContexts,
|
|
@@ -24,27 +25,73 @@ export const SERVER_DEPLOY_TYPES = [
|
|
|
24
25
|
const SSH_BASED = ['ssh', 'ec2', 'azure-vm', 'gcp-vm'];
|
|
25
26
|
|
|
26
27
|
/**
|
|
28
|
+
* Prompt for one environment's deployment method + method-specific config.
|
|
29
|
+
* Shared by `deployhub init` and `deployhub env add`.
|
|
30
|
+
*
|
|
27
31
|
* @param {string} projectName
|
|
28
32
|
* @param {'frontend'|'backend'|'both'} projectType
|
|
29
33
|
* @param {Record<string, unknown>|null} backendConfig
|
|
34
|
+
* @param {{
|
|
35
|
+
* envName?: string,
|
|
36
|
+
* existingEnvNames?: string[],
|
|
37
|
+
* deployType?: string,
|
|
38
|
+
* nonInteractive?: boolean,
|
|
39
|
+
* }} [options]
|
|
40
|
+
* — when envName is set, skip the name prompt; existingEnvNames blocks in-session / config duplicates
|
|
41
|
+
* — deployType skips the method list; nonInteractive uses defaults (requires deployType)
|
|
30
42
|
*/
|
|
31
|
-
export async function promptServerDeployment(
|
|
32
|
-
|
|
43
|
+
export async function promptServerDeployment(
|
|
44
|
+
projectName,
|
|
45
|
+
projectType,
|
|
46
|
+
backendConfig,
|
|
47
|
+
options = {}
|
|
48
|
+
) {
|
|
49
|
+
const existingEnvNames = options.existingEnvNames || [];
|
|
50
|
+
const allowedMethods = new Set(SERVER_DEPLOY_TYPES.map((c) => c.value));
|
|
51
|
+
|
|
52
|
+
if (options.nonInteractive) {
|
|
53
|
+
if (!options.deployType || !allowedMethods.has(options.deployType)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Non-interactive env add requires --method <ssh|docker|ec2|azure-vm|gcp-vm|kubernetes>.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return buildNonInteractiveDeployAnswers(options.deployType, projectName, options.envName);
|
|
59
|
+
}
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
/** @type {import('inquirer').QuestionCollection} */
|
|
62
|
+
const questions = [];
|
|
63
|
+
|
|
64
|
+
if (!options.deployType) {
|
|
65
|
+
questions.push({
|
|
36
66
|
type: 'list',
|
|
37
67
|
name: 'deployType',
|
|
38
68
|
message: 'Deployment type:',
|
|
39
69
|
choices: SERVER_DEPLOY_TYPES,
|
|
40
|
-
}
|
|
41
|
-
|
|
70
|
+
});
|
|
71
|
+
} else if (!allowedMethods.has(options.deployType)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unknown deployment method "${options.deployType}". Use: ${[...allowedMethods].join(', ')}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!options.envName) {
|
|
78
|
+
questions.push({
|
|
42
79
|
type: 'input',
|
|
43
80
|
name: 'envName',
|
|
44
81
|
message: 'Environment name (e.g. production, staging):',
|
|
45
82
|
default: 'production',
|
|
46
|
-
|
|
47
|
-
|
|
83
|
+
validate: createEnvNamePromptValidate(existingEnvNames),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const base =
|
|
88
|
+
questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
89
|
+
if (options.envName) {
|
|
90
|
+
base.envName = options.envName;
|
|
91
|
+
}
|
|
92
|
+
if (options.deployType) {
|
|
93
|
+
base.deployType = options.deployType;
|
|
94
|
+
}
|
|
48
95
|
|
|
49
96
|
const deployType = base.deployType;
|
|
50
97
|
|
|
@@ -59,6 +106,50 @@ export async function promptServerDeployment(projectName, projectType, backendCo
|
|
|
59
106
|
return promptSshBasedDeployment(base, projectName, projectType, backendConfig, deployType);
|
|
60
107
|
}
|
|
61
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Defaults for `deployhub env add <name> --method <type> --yes` (no prompts).
|
|
111
|
+
* @param {string} deployType
|
|
112
|
+
* @param {string} projectName
|
|
113
|
+
* @param {string} [envName]
|
|
114
|
+
*/
|
|
115
|
+
function buildNonInteractiveDeployAnswers(deployType, projectName, envName) {
|
|
116
|
+
const base = { deployType, envName: envName || 'default' };
|
|
117
|
+
if (deployType === 'docker') {
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
dockerImageName: projectName,
|
|
121
|
+
dockerRegistryUrl: '',
|
|
122
|
+
dockerRegistryUsername: '',
|
|
123
|
+
dockerRegistryToken: '',
|
|
124
|
+
dockerHost: '',
|
|
125
|
+
healthUrl: '',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (deployType === 'kubernetes') {
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
kubeconfig: '~/.kube/config',
|
|
132
|
+
kubeContext: '',
|
|
133
|
+
kubeNamespace: projectName,
|
|
134
|
+
dockerImageName: projectName,
|
|
135
|
+
dockerRegistryUrl: '',
|
|
136
|
+
dockerRegistryUsername: '',
|
|
137
|
+
dockerRegistryToken: '',
|
|
138
|
+
healthUrl: '',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// ssh / ec2 / azure-vm / gcp-vm — host/user left empty (filled via secrets / env)
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
host: '',
|
|
145
|
+
user: 'deploy',
|
|
146
|
+
keyPath: '',
|
|
147
|
+
sshPort: '22',
|
|
148
|
+
deployPath: `/var/www/${projectName}`,
|
|
149
|
+
osHint: '',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
62
153
|
/**
|
|
63
154
|
* @param {Record<string, string>} base
|
|
64
155
|
* @param {string} projectName
|
|
@@ -376,62 +467,127 @@ export function buildServerEnvEntry(
|
|
|
376
467
|
singleConfig
|
|
377
468
|
) {
|
|
378
469
|
/** @type {Record<string, unknown>} */
|
|
379
|
-
const
|
|
470
|
+
const settings = {
|
|
380
471
|
deploymentType: 'server',
|
|
381
|
-
type: deployAnswers.deployType,
|
|
382
472
|
};
|
|
383
473
|
|
|
384
474
|
if (deployAnswers.deployType === 'kubernetes') {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return
|
|
475
|
+
settings.kubeconfig = deployAnswers.kubeconfig;
|
|
476
|
+
settings.kubeContext = deployAnswers.kubeContext;
|
|
477
|
+
settings.kubeNamespace = deployAnswers.kubeNamespace || projectName;
|
|
478
|
+
settings.dockerImageName = deployAnswers.dockerImageName || projectName;
|
|
479
|
+
settings.dockerRegistryUrl = deployAnswers.dockerRegistryUrl || '';
|
|
480
|
+
return {
|
|
481
|
+
enabled: true,
|
|
482
|
+
method: 'kubernetes',
|
|
483
|
+
trigger: 'manual',
|
|
484
|
+
config: settings,
|
|
485
|
+
};
|
|
391
486
|
}
|
|
392
487
|
|
|
393
488
|
if (deployAnswers.deployType === 'docker') {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return
|
|
489
|
+
settings.dockerImageName = deployAnswers.dockerImageName || projectName;
|
|
490
|
+
settings.dockerRegistryUrl = deployAnswers.dockerRegistryUrl || '';
|
|
491
|
+
settings.dockerHost = deployAnswers.dockerHost || '';
|
|
492
|
+
return {
|
|
493
|
+
enabled: true,
|
|
494
|
+
method: 'docker',
|
|
495
|
+
trigger: 'manual',
|
|
496
|
+
config: settings,
|
|
497
|
+
};
|
|
398
498
|
}
|
|
399
499
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
500
|
+
settings.host = deployAnswers.host || '';
|
|
501
|
+
settings.user = deployAnswers.user || '';
|
|
502
|
+
settings.keyPath = deployAnswers.keyPath || '';
|
|
503
|
+
settings.sshPort = Number(deployAnswers.sshPort) || 22;
|
|
404
504
|
|
|
405
|
-
if (deployAnswers.ec2InstanceId)
|
|
406
|
-
if (deployAnswers.awsRegion)
|
|
407
|
-
if (deployAnswers.azureSubscriptionId)
|
|
408
|
-
if (deployAnswers.azureResourceGroup)
|
|
409
|
-
if (deployAnswers.azureVmName)
|
|
410
|
-
if (deployAnswers.gcpProjectId)
|
|
411
|
-
if (deployAnswers.gcpZone)
|
|
412
|
-
if (deployAnswers.gcpInstanceName)
|
|
505
|
+
if (deployAnswers.ec2InstanceId) settings.ec2InstanceId = deployAnswers.ec2InstanceId;
|
|
506
|
+
if (deployAnswers.awsRegion) settings.awsRegion = deployAnswers.awsRegion;
|
|
507
|
+
if (deployAnswers.azureSubscriptionId) settings.azureSubscriptionId = deployAnswers.azureSubscriptionId;
|
|
508
|
+
if (deployAnswers.azureResourceGroup) settings.azureResourceGroup = deployAnswers.azureResourceGroup;
|
|
509
|
+
if (deployAnswers.azureVmName) settings.azureVmName = deployAnswers.azureVmName;
|
|
510
|
+
if (deployAnswers.gcpProjectId) settings.gcpProjectId = deployAnswers.gcpProjectId;
|
|
511
|
+
if (deployAnswers.gcpZone) settings.gcpZone = deployAnswers.gcpZone;
|
|
512
|
+
if (deployAnswers.gcpInstanceName) settings.gcpInstanceName = deployAnswers.gcpInstanceName;
|
|
413
513
|
|
|
414
514
|
if (projectType === 'both') {
|
|
415
|
-
|
|
515
|
+
settings.frontendDeployPath =
|
|
416
516
|
deployAnswers.frontendDeployPath || `/var/www/${projectName}/public`;
|
|
417
|
-
|
|
517
|
+
settings.backendDeployPath =
|
|
418
518
|
deployAnswers.backendDeployPath || `/var/www/${projectName}/api`;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
519
|
+
settings.appName = deployAnswers.appName || `${projectName}-api`;
|
|
520
|
+
settings.framework = backendConfig?.framework || 'express';
|
|
521
|
+
settings.path = settings.backendDeployPath;
|
|
522
|
+
settings.backendDeploymentType = 'server';
|
|
423
523
|
} else if (projectType === 'backend') {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
524
|
+
settings.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
|
|
525
|
+
settings.path = settings.deployPath;
|
|
526
|
+
settings.appName = deployAnswers.appName || projectName;
|
|
527
|
+
settings.framework = singleConfig?.framework || 'express';
|
|
528
|
+
settings.port = singleConfig?.port || 3000;
|
|
429
529
|
} else {
|
|
430
|
-
|
|
431
|
-
|
|
530
|
+
settings.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
|
|
531
|
+
settings.path = settings.deployPath;
|
|
432
532
|
}
|
|
433
533
|
|
|
434
|
-
return
|
|
534
|
+
return {
|
|
535
|
+
enabled: true,
|
|
536
|
+
method: deployAnswers.deployType,
|
|
537
|
+
trigger: 'manual',
|
|
538
|
+
config: settings,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Apply init-time trigger defaults after environments are collected.
|
|
544
|
+
* - Exactly one env → always `push` (git push = auto-deploy).
|
|
545
|
+
* - Two or more → grandfathered/default gets `push`; all others stay `manual`.
|
|
546
|
+
*
|
|
547
|
+
* @param {Record<string, { trigger?: string }>} environments
|
|
548
|
+
* @param {string[]} deployNames
|
|
549
|
+
* @param {string|undefined|null} defaultEnvironment
|
|
550
|
+
*/
|
|
551
|
+
export function applyInitTriggerDefaults(environments, deployNames, defaultEnvironment) {
|
|
552
|
+
if (deployNames.length === 1 && environments[deployNames[0]]) {
|
|
553
|
+
environments[deployNames[0]].trigger = 'push';
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (deployNames.length >= 2 && defaultEnvironment) {
|
|
557
|
+
for (const name of deployNames) {
|
|
558
|
+
if (!environments[name]) continue;
|
|
559
|
+
environments[name].trigger =
|
|
560
|
+
name === defaultEnvironment ? 'push' : 'manual';
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* End-of-init reminder for multi-env setups (grandfathered = push, others = manual).
|
|
567
|
+
* @param {string} grandfathered
|
|
568
|
+
* @param {string[]} allEnvNames
|
|
569
|
+
* @returns {string}
|
|
570
|
+
*/
|
|
571
|
+
export function formatMultiEnvTriggerReminder(grandfathered, allEnvNames) {
|
|
572
|
+
const others = allEnvNames.filter((n) => n !== grandfathered);
|
|
573
|
+
const otherList =
|
|
574
|
+
others.length > 0 ? others.map((n) => `"${n}"`).join(', ') : '(none)';
|
|
575
|
+
const exampleEnv = others[0] || '<env-name>';
|
|
576
|
+
return [
|
|
577
|
+
'─────────────────────────────────────────────',
|
|
578
|
+
`By default, only your first environment ("${grandfathered}")`,
|
|
579
|
+
`auto-deploys on push. Your other environment(s) — ${otherList} — are set to manual and will only deploy via:`,
|
|
580
|
+
' deployhub deploy --env <name>',
|
|
581
|
+
'or GitHub Actions → Run workflow.',
|
|
582
|
+
'',
|
|
583
|
+
'To make an environment auto-deploy on push instead, open',
|
|
584
|
+
'deployhub.config.json and change its "trigger" to "push":',
|
|
585
|
+
' "environments": {',
|
|
586
|
+
` "${exampleEnv}": { "trigger": "push", ... }`,
|
|
587
|
+
' }',
|
|
588
|
+
'Then run: deployhub sync-workflows',
|
|
589
|
+
'─────────────────────────────────────────────',
|
|
590
|
+
].join('\n');
|
|
435
591
|
}
|
|
436
592
|
|
|
437
593
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { createSshProvider } from './ssh.js';
|
|
3
3
|
import { createLogger } from '../../logger/index.js';
|
|
4
|
+
import { getEnvSettings } from '../../core/config.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -9,14 +10,14 @@ import { createLogger } from '../../logger/index.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function createAzureVmProvider(config, envName, env = process.env) {
|
|
11
12
|
const log = createLogger('azure-vm');
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const settings = getEnvSettings(config.environments[envName]);
|
|
14
|
+
const subscriptionId = env.AZURE_SUBSCRIPTION_ID || settings.azureSubscriptionId;
|
|
15
|
+
const resourceGroup = env.AZURE_RESOURCE_GROUP || settings.azureResourceGroup;
|
|
16
|
+
const vmName = env.AZURE_VM_NAME || settings.azureVmName;
|
|
15
17
|
|
|
16
18
|
async function resolveHost() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return environment?.host || env.SSH_HOST;
|
|
19
|
+
if (settings.host || env.SSH_HOST) {
|
|
20
|
+
return settings.host || env.SSH_HOST;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
if (!subscriptionId || !resourceGroup || !vmName) {
|
|
@@ -73,10 +74,7 @@ export function createAzureVmProvider(config, envName, env = process.env) {
|
|
|
73
74
|
'Could not resolve host via Azure VM lookup, and no SSH_HOST was set — provide one or the other.'
|
|
74
75
|
);
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
if (environment) {
|
|
78
|
-
environment.host = host;
|
|
79
|
-
}
|
|
77
|
+
settings.host = host;
|
|
80
78
|
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
81
79
|
}
|
|
82
80
|
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import { createLogger } from '../../logger/index.js';
|
|
3
3
|
import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
|
|
4
4
|
import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
5
|
+
import { getEnvSettings, mergeMethodSettingsIntoEnv } from '../../core/environments.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -10,7 +11,9 @@ import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
|
10
11
|
*/
|
|
11
12
|
export function createDockerProvider(config, envName, env = process.env) {
|
|
12
13
|
const log = createLogger('docker');
|
|
13
|
-
const
|
|
14
|
+
const settings = getEnvSettings(config.environments?.[envName]);
|
|
15
|
+
const effectiveEnv = mergeMethodSettingsIntoEnv(env, settings);
|
|
16
|
+
const imageOps = createDockerImageDeployContext(config, effectiveEnv, log);
|
|
14
17
|
const { fullImage, getDockerEnv, ensureImageReadyForDeploy } = imageOps;
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -56,7 +59,12 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
56
59
|
);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
// Tag stays buildId-based; env only scopes which history informed this buildId.
|
|
63
|
+
const rollbackImage = resolveDockerImageRefForTag(
|
|
64
|
+
config,
|
|
65
|
+
effectiveEnv,
|
|
66
|
+
meta.buildId
|
|
67
|
+
).fullImage;
|
|
60
68
|
log.info(
|
|
61
69
|
`Rolling back Docker to buildId=${meta.buildId} (image: ${rollbackImage})...`
|
|
62
70
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { createSshProvider } from './ssh.js';
|
|
3
3
|
import { createLogger } from '../../logger/index.js';
|
|
4
|
+
import { getEnvSettings } from '../../core/config.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -9,13 +10,13 @@ import { createLogger } from '../../logger/index.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function createEc2Provider(config, envName, env = process.env) {
|
|
11
12
|
const log = createLogger('ec2');
|
|
12
|
-
const
|
|
13
|
-
const
|
|
13
|
+
const settings = getEnvSettings(config.environments[envName]);
|
|
14
|
+
const instanceId = env.EC2_INSTANCE_ID || settings.ec2InstanceId;
|
|
15
|
+
const region = env.AWS_REGION || settings.awsRegion || 'us-east-1';
|
|
14
16
|
|
|
15
17
|
async function resolveHost() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return environment?.host || env.SSH_HOST;
|
|
18
|
+
if (settings.host || env.SSH_HOST) {
|
|
19
|
+
return settings.host || env.SSH_HOST;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
if (!instanceId) {
|
|
@@ -76,10 +77,7 @@ export function createEc2Provider(config, envName, env = process.env) {
|
|
|
76
77
|
'Could not resolve host via EC2 instance lookup, and no SSH_HOST was set — provide one or the other.'
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
if (environment) {
|
|
81
|
-
environment.host = host;
|
|
82
|
-
}
|
|
80
|
+
settings.host = host;
|
|
83
81
|
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
84
82
|
}
|
|
85
83
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { createSshProvider } from './ssh.js';
|
|
3
3
|
import { createLogger } from '../../logger/index.js';
|
|
4
|
+
import { getEnvSettings } from '../../core/config.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -9,14 +10,14 @@ import { createLogger } from '../../logger/index.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function createGcpVmProvider(config, envName, env = process.env) {
|
|
11
12
|
const log = createLogger('gcp-vm');
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const settings = getEnvSettings(config.environments[envName]);
|
|
14
|
+
const projectId = env.GCP_PROJECT_ID || settings.gcpProjectId;
|
|
15
|
+
const zone = env.GCP_ZONE || settings.gcpZone;
|
|
16
|
+
const instanceName = env.GCP_INSTANCE_NAME || settings.gcpInstanceName;
|
|
15
17
|
|
|
16
18
|
async function resolveHost() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return environment?.host || env.SSH_HOST;
|
|
19
|
+
if (settings.host || env.SSH_HOST) {
|
|
20
|
+
return settings.host || env.SSH_HOST;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
if (!projectId || !zone || !instanceName) {
|
|
@@ -77,10 +78,7 @@ export function createGcpVmProvider(config, envName, env = process.env) {
|
|
|
77
78
|
'Could not resolve host via GCP instance lookup, and no SSH_HOST was set — provide one or the other.'
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
if (environment) {
|
|
82
|
-
environment.host = host;
|
|
83
|
-
}
|
|
81
|
+
settings.host = host;
|
|
84
82
|
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
85
83
|
}
|
|
86
84
|
|
|
@@ -8,6 +8,7 @@ import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.
|
|
|
8
8
|
import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
9
9
|
import { ensureKubernetesNamespace } from '../../utils/kubernetes-namespace.js';
|
|
10
10
|
import { syncKubernetesDeploymentImage } from '../../utils/kubernetes-deploy-image.js';
|
|
11
|
+
import { getEnvSettings, mergeMethodSettingsIntoEnv } from '../../core/environments.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -16,11 +17,15 @@ import { syncKubernetesDeploymentImage } from '../../utils/kubernetes-deploy-ima
|
|
|
16
17
|
*/
|
|
17
18
|
export function createKubernetesProvider(config, envName, env = process.env) {
|
|
18
19
|
const log = createLogger('kubernetes');
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
20
|
+
const settings = getEnvSettings(config.environments?.[envName]);
|
|
21
|
+
const effectiveEnv = mergeMethodSettingsIntoEnv(env, settings);
|
|
22
|
+
const imageOps = createDockerImageDeployContext(config, effectiveEnv, log);
|
|
23
|
+
|
|
24
|
+
const kubeconfig =
|
|
25
|
+
effectiveEnv.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
|
|
26
|
+
const context = effectiveEnv.KUBE_CONTEXT || '';
|
|
27
|
+
const namespace =
|
|
28
|
+
effectiveEnv.KUBE_NAMESPACE || config.project || 'default';
|
|
24
29
|
const deploymentName = sanitizeK8sName(config.project || 'app');
|
|
25
30
|
|
|
26
31
|
function getKubectlEnv() {
|
|
@@ -158,7 +163,11 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
158
163
|
);
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
const rollbackImage = resolveDockerImageRefForTag(
|
|
166
|
+
const rollbackImage = resolveDockerImageRefForTag(
|
|
167
|
+
config,
|
|
168
|
+
effectiveEnv,
|
|
169
|
+
meta.buildId
|
|
170
|
+
).fullImage;
|
|
162
171
|
log.info(
|
|
163
172
|
`Rolling back Kubernetes to buildId=${meta.buildId} (image: ${rollbackImage})...`
|
|
164
173
|
);
|