@akash-chowdhury-24/deployhub 2.0.15 → 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 +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 +97 -64
- package/src/commands/rollback.js +84 -24
- 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/github-actions.js +344 -31
- 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,76 @@ 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
|
+
};
|
|
435
540
|
}
|
|
436
541
|
|
|
437
542
|
/**
|
|
@@ -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
|
);
|
|
@@ -3,10 +3,12 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { createLogger } from '../../logger/index.js';
|
|
6
|
+
import { getEnvSettings } from '../../core/config.js';
|
|
6
7
|
import {
|
|
7
8
|
getNginxSitesAvailablePath,
|
|
8
9
|
getNginxSitesEnabledPath,
|
|
9
10
|
getNginxConfDPath,
|
|
11
|
+
resolveNginxSiteName,
|
|
10
12
|
} from '../../utils/nginx.js';
|
|
11
13
|
import { shellQuote, formatRemoteCommandFailure } from '../../utils/shell-quote.js';
|
|
12
14
|
|
|
@@ -30,23 +32,24 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
30
32
|
throw new Error(`Environment "${envName}" not found in config`);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
+
const settings = getEnvSettings(environment);
|
|
36
|
+
const host = settings.host || env.SSH_HOST;
|
|
37
|
+
const user = settings.user || env.SSH_USER;
|
|
35
38
|
const deployPath =
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
settings.deployPath ||
|
|
40
|
+
settings.path ||
|
|
38
41
|
env.SSH_DEPLOY_PATH ||
|
|
39
42
|
'/var/www/app';
|
|
40
43
|
const frontendDeployPath =
|
|
41
|
-
|
|
44
|
+
settings.frontendDeployPath || deployPath;
|
|
42
45
|
const backendDeployPath =
|
|
43
|
-
|
|
46
|
+
settings.backendDeployPath || deployPath;
|
|
44
47
|
const appName =
|
|
45
|
-
|
|
46
|
-
const port =
|
|
48
|
+
settings.appName || env.SSH_APP_NAME || config.project;
|
|
49
|
+
const port = settings.port || config.port || Number(env.SSH_PORT) || 3000;
|
|
47
50
|
const sshKey = env.SSH_KEY;
|
|
48
|
-
const keyPath =
|
|
49
|
-
const sshPort = Number(env.SSH_SSH_PORT) ||
|
|
51
|
+
const keyPath = settings.keyPath || env.SSH_KEY_PATH;
|
|
52
|
+
const sshPort = Number(env.SSH_SSH_PORT) || settings.sshPort || 22;
|
|
50
53
|
|
|
51
54
|
const log = createLogger('ssh');
|
|
52
55
|
|
|
@@ -308,14 +311,16 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
308
311
|
: 'Detected Nginx layout: RHEL/Amazon Linux (conf.d)'
|
|
309
312
|
);
|
|
310
313
|
|
|
314
|
+
const siteName = resolveNginxSiteName(config, envName);
|
|
315
|
+
|
|
311
316
|
if (layout === 'debian') {
|
|
312
|
-
const sitePath = getNginxSitesAvailablePath(
|
|
313
|
-
const enabledPath = getNginxSitesEnabledPath(
|
|
317
|
+
const sitePath = getNginxSitesAvailablePath(siteName);
|
|
318
|
+
const enabledPath = getNginxSitesEnabledPath(siteName);
|
|
314
319
|
await exec(ssh, `sudo cp ${sh(nginxConfRemote)} ${sh(sitePath)}`);
|
|
315
320
|
await exec(ssh, `sudo ln -sf ${sh(sitePath)} ${sh(enabledPath)}`);
|
|
316
321
|
log.info(`Nginx config installed: ${sitePath}`);
|
|
317
322
|
} else {
|
|
318
|
-
const confPath = getNginxConfDPath(
|
|
323
|
+
const confPath = getNginxConfDPath(siteName);
|
|
319
324
|
await exec(ssh, `sudo cp ${sh(nginxConfRemote)} ${sh(confPath)}`);
|
|
320
325
|
log.info(`Nginx config installed: ${confPath}`);
|
|
321
326
|
}
|