@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.
@@ -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.type;
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
- return factory(config, envName, env);
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.deploy || [];
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 provider = getDeploymentProvider(envConfig.type, config, envName);
61
- log.info(`Deploying to ${envName} (${envConfig.type})...`);
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.deploy || [];
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.type, config, envName);
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(projectName, projectType, backendConfig) {
32
- const isBackend = projectType === 'backend' || projectType === 'both';
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
- const base = await inquirer.prompt([
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 envEntry = {
470
+ const settings = {
380
471
  deploymentType: 'server',
381
- type: deployAnswers.deployType,
382
472
  };
383
473
 
384
474
  if (deployAnswers.deployType === 'kubernetes') {
385
- envEntry.kubeconfig = deployAnswers.kubeconfig;
386
- envEntry.kubeContext = deployAnswers.kubeContext;
387
- envEntry.kubeNamespace = deployAnswers.kubeNamespace || projectName;
388
- envEntry.dockerImageName = deployAnswers.dockerImageName || projectName;
389
- envEntry.dockerRegistryUrl = deployAnswers.dockerRegistryUrl || '';
390
- return envEntry;
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
- envEntry.dockerImageName = deployAnswers.dockerImageName || projectName;
395
- envEntry.dockerRegistryUrl = deployAnswers.dockerRegistryUrl || '';
396
- envEntry.dockerHost = deployAnswers.dockerHost || '';
397
- return envEntry;
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
- envEntry.host = deployAnswers.host || '';
401
- envEntry.user = deployAnswers.user || '';
402
- envEntry.keyPath = deployAnswers.keyPath || '';
403
- envEntry.sshPort = Number(deployAnswers.sshPort) || 22;
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) envEntry.ec2InstanceId = deployAnswers.ec2InstanceId;
406
- if (deployAnswers.awsRegion) envEntry.awsRegion = deployAnswers.awsRegion;
407
- if (deployAnswers.azureSubscriptionId) envEntry.azureSubscriptionId = deployAnswers.azureSubscriptionId;
408
- if (deployAnswers.azureResourceGroup) envEntry.azureResourceGroup = deployAnswers.azureResourceGroup;
409
- if (deployAnswers.azureVmName) envEntry.azureVmName = deployAnswers.azureVmName;
410
- if (deployAnswers.gcpProjectId) envEntry.gcpProjectId = deployAnswers.gcpProjectId;
411
- if (deployAnswers.gcpZone) envEntry.gcpZone = deployAnswers.gcpZone;
412
- if (deployAnswers.gcpInstanceName) envEntry.gcpInstanceName = 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
- envEntry.frontendDeployPath =
515
+ settings.frontendDeployPath =
416
516
  deployAnswers.frontendDeployPath || `/var/www/${projectName}/public`;
417
- envEntry.backendDeployPath =
517
+ settings.backendDeployPath =
418
518
  deployAnswers.backendDeployPath || `/var/www/${projectName}/api`;
419
- envEntry.appName = deployAnswers.appName || `${projectName}-api`;
420
- envEntry.framework = backendConfig?.framework || 'express';
421
- envEntry.path = envEntry.backendDeployPath;
422
- envEntry.backendDeploymentType = 'server';
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
- envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
425
- envEntry.path = envEntry.deployPath;
426
- envEntry.appName = deployAnswers.appName || projectName;
427
- envEntry.framework = singleConfig?.framework || 'express';
428
- envEntry.port = singleConfig?.port || 3000;
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
- envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
431
- envEntry.path = envEntry.deployPath;
530
+ settings.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
531
+ settings.path = settings.deployPath;
432
532
  }
433
533
 
434
- return envEntry;
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 subscriptionId = env.AZURE_SUBSCRIPTION_ID;
13
- const resourceGroup = env.AZURE_RESOURCE_GROUP;
14
- const vmName = env.AZURE_VM_NAME;
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
- const environment = config.environments[envName];
18
- if (environment?.host || env.SSH_HOST) {
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
- const environment = config.environments[envName];
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 imageOps = createDockerImageDeployContext(config, env, log);
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
- const rollbackImage = resolveDockerImageRefForTag(config, env, meta.buildId).fullImage;
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 instanceId = env.EC2_INSTANCE_ID;
13
- const region = env.AWS_REGION || 'us-east-1';
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
- const environment = config.environments[envName];
17
- if (environment?.host || env.SSH_HOST) {
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
- const environment = config.environments[envName];
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 projectId = env.GCP_PROJECT_ID;
13
- const zone = env.GCP_ZONE;
14
- const instanceName = env.GCP_INSTANCE_NAME;
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
- const environment = config.environments[envName];
18
- if (environment?.host || env.SSH_HOST) {
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
- const environment = config.environments[envName];
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 imageOps = createDockerImageDeployContext(config, env, log);
20
-
21
- const kubeconfig = env.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
22
- const context = env.KUBE_CONTEXT || '';
23
- const namespace = env.KUBE_NAMESPACE || config.project || 'default';
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(config, env, meta.buildId).fullImage;
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 host = environment.host || env.SSH_HOST;
34
- const user = environment.user || env.SSH_USER;
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
- environment.deployPath ||
37
- environment.path ||
39
+ settings.deployPath ||
40
+ settings.path ||
38
41
  env.SSH_DEPLOY_PATH ||
39
42
  '/var/www/app';
40
43
  const frontendDeployPath =
41
- environment.frontendDeployPath || deployPath;
44
+ settings.frontendDeployPath || deployPath;
42
45
  const backendDeployPath =
43
- environment.backendDeployPath || deployPath;
46
+ settings.backendDeployPath || deployPath;
44
47
  const appName =
45
- environment.appName || env.SSH_APP_NAME || config.project;
46
- const port = environment.port || config.port || Number(env.SSH_PORT) || 3000;
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 = environment.keyPath || env.SSH_KEY_PATH;
49
- const sshPort = Number(env.SSH_SSH_PORT) || environment.sshPort || 22;
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(config.project);
313
- const enabledPath = getNginxSitesEnabledPath(config.project);
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(config.project);
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
  }