@akash-chowdhury-24/deployhub 2.0.26 → 2.0.28

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.
@@ -18,12 +18,27 @@ import {
18
18
  } from '../deployment/deployment-env.js';
19
19
  import {
20
20
  getEnvMethod,
21
+ getEnvTrigger,
21
22
  getEnabledEnvironmentNames,
22
23
  isEnvEnabled,
23
24
  } from '../core/environments.js';
24
25
 
26
+ /** Default PHP for CI when config does not set `phpVersion` / `backend.phpVersion`. */
27
+ export const DEFAULT_PHP_VERSION = '8.4';
28
+
25
29
  /** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
26
30
 
31
+ /** Storage-only providers — never treat deployment method ids as storage. */
32
+ const STORAGE_PROVIDER_IDS = new Set([
33
+ 'aws',
34
+ 'azure',
35
+ 'gcp',
36
+ 'gdrive',
37
+ 'dropbox',
38
+ 'local',
39
+ 'ftp',
40
+ ]);
41
+
27
42
  const PROVIDER_ENV_MAP = {
28
43
  aws: [
29
44
  'AWS_ACCESS_KEY_ID',
@@ -41,7 +56,7 @@ const PROVIDER_ENV_MAP = {
41
56
  ],
42
57
  dropbox: ['DROPBOX_ACCESS_TOKEN'],
43
58
  local: [],
44
- ftp: ['FTP_HOST', 'FTP_USER', 'FTP_PASSWORD'],
59
+ ftp: ['FTP_HOST', 'FTP_USER', 'FTP_PASSWORD', 'FTP_PORT', 'FTP_PATH'],
45
60
  ssh: DEPLOYMENT_ENV_KEYS.ssh,
46
61
  docker: DEPLOYMENT_ENV_KEYS.docker,
47
62
  ec2: DEPLOYMENT_ENV_KEYS.ec2,
@@ -50,6 +65,27 @@ const PROVIDER_ENV_MAP = {
50
65
  kubernetes: DEPLOYMENT_ENV_KEYS.kubernetes,
51
66
  };
52
67
 
68
+ /** @type {Record<string, string>} */
69
+ const STORAGE_SECRET_NOTES = {
70
+ AWS_ACCESS_KEY_ID: 'AWS S3 storage credential (not EC2 instance lookup)',
71
+ AWS_SECRET_ACCESS_KEY: 'AWS S3 storage credential (not EC2 instance lookup)',
72
+ AWS_BUCKET: 'AWS S3 storage bucket',
73
+ AWS_REGION: 'AWS S3 storage region (not EC2_LOOKUP_AWS_REGION)',
74
+ AZURE_CONNECTION_STRING: 'Azure Blob storage credential',
75
+ AZURE_CONTAINER: 'Azure Blob storage container',
76
+ GCP_PROJECT_ID: 'GCP Storage credential (not GCP VM instance lookup)',
77
+ GCP_KEY_FILE: 'GCP Storage credential (not GCP_VM_LOOKUP_KEY_FILE)',
78
+ GCP_BUCKET: 'GCP Storage bucket',
79
+ GDRIVE_CLIENT_ID: 'Google Drive storage credential',
80
+ GDRIVE_CLIENT_SECRET: 'Google Drive storage credential',
81
+ GDRIVE_REFRESH_TOKEN: 'Google Drive storage credential',
82
+ GDRIVE_FOLDER_ID: 'Google Drive folder',
83
+ DROPBOX_ACCESS_TOKEN: 'Dropbox storage credential',
84
+ FTP_HOST: 'FTP storage host',
85
+ FTP_USER: 'FTP storage user',
86
+ FTP_PASSWORD: 'FTP storage password',
87
+ };
88
+
53
89
  const PROVIDER_LABELS = {
54
90
  aws: 'AWS S3',
55
91
  azure: 'Azure Blob',
@@ -67,6 +103,7 @@ const PROVIDER_LABELS = {
67
103
 
68
104
  const ENV_VAR_DEFAULTS = {
69
105
  AWS_REGION: 'us-east-1',
106
+ EC2_LOOKUP_AWS_REGION: 'us-east-1',
70
107
  FTP_PORT: '21',
71
108
  FTP_PATH: '/uploads',
72
109
  SSH_DEPLOY_PATH: '/var/www/app',
@@ -234,6 +271,40 @@ function getGithubGitConfigStep() {
234
271
  fi`;
235
272
  }
236
273
 
274
+ /**
275
+ * Resolve PHP version for CI setup-php.
276
+ * Order: `backend.phpVersion` → top-level `phpVersion` → {@link DEFAULT_PHP_VERSION} (`8.4`).
277
+ * Set either config key in deployhub.config.json to pin a different runtime
278
+ * (e.g. `"phpVersion": "8.3"` or `"backend": { "phpVersion": "8.3" }`).
279
+ *
280
+ * @param {import('../core/config.js').DeployHubConfig} [config]
281
+ * @returns {string}
282
+ */
283
+ export function resolvePhpVersion(config) {
284
+ const fromBackend = config?.backend?.phpVersion;
285
+ if (typeof fromBackend === 'string' && fromBackend.trim()) {
286
+ return fromBackend.trim();
287
+ }
288
+ const fromRoot = config?.phpVersion;
289
+ if (typeof fromRoot === 'string' && fromRoot.trim()) {
290
+ return fromRoot.trim();
291
+ }
292
+ return DEFAULT_PHP_VERSION;
293
+ }
294
+
295
+ /**
296
+ * @param {import('../core/config.js').DeployHubConfig} [config]
297
+ * @returns {boolean}
298
+ */
299
+ function isPhpProject(config) {
300
+ if (!config) return false;
301
+ const projectType = config.projectType || 'frontend';
302
+ if (projectType !== 'backend' && projectType !== 'both') return false;
303
+ const framework = config.backend?.framework || config.framework || '';
304
+ const language = config.backend?.language || config.language || '';
305
+ return language === 'php' || ['laravel', 'symfony', 'php'].includes(framework);
306
+ }
307
+
237
308
  /**
238
309
  * @param {import('../core/config.js').DeployHubConfig} [config]
239
310
  * @returns {string[]}
@@ -249,16 +320,24 @@ function getBackendSetupSteps(config) {
249
320
 
250
321
  if (projectType === 'backend' || projectType === 'both') {
251
322
  const framework = config.backend?.framework || config.framework || 'express';
252
- if (['express', 'nestjs', 'fastify', 'koa', 'nextjs'].includes(framework)) {
323
+ if (['express', 'nestjs', 'fastify', 'koa', 'nextjs', 'node'].includes(framework)) {
253
324
  steps.push(` - uses: actions/setup-node@v4
254
325
  with:
255
326
  node-version: '20'`);
256
327
  }
257
- if (['fastapi', 'django', 'flask'].includes(framework)) {
328
+ if (['fastapi', 'django', 'flask', 'python'].includes(framework)) {
258
329
  steps.push(` - uses: actions/setup-python@v5
259
330
  with:
260
331
  python-version: '3.11'`);
261
332
  }
333
+ if (isPhpProject(config)) {
334
+ const phpVersion = resolvePhpVersion(config);
335
+ steps.push(` - name: Setup PHP
336
+ uses: shivammathur/setup-php@v2
337
+ with:
338
+ php-version: '${phpVersion}'
339
+ tools: composer`);
340
+ }
262
341
  if (['spring', 'java'].includes(framework)) {
263
342
  steps.push(` - uses: actions/setup-java@v4
264
343
  with:
@@ -275,7 +354,7 @@ function getBackendSetupSteps(config) {
275
354
  with:
276
355
  dotnet-version: '8.0.x'`);
277
356
  }
278
- if (framework === 'rails') {
357
+ if (framework === 'rails' || framework === 'ruby') {
279
358
  steps.push(` - uses: ruby/setup-ruby@v1
280
359
  with:
281
360
  ruby-version: '3.2'
@@ -329,18 +408,69 @@ function resolveKubeconfigWorkflowSecretName(deployEnvironments, environments, c
329
408
  : 'KUBECONFIG';
330
409
  }
331
410
 
411
+ /**
412
+ * GitHub Actions `if:` so kubectl/kubeconfig only run when this job needs k8s.
413
+ * - Push: only when at least one push-triggered env uses kubernetes (skip when push
414
+ * only deploys EC2/SSH/etc. and k8s is manual-only).
415
+ * - workflow_dispatch / rollback: when selected env is k8s, `all`, or blank
416
+ * (blank = deploy/rollback all enabled envs).
417
+ * Returns null when every enabled env is kubernetes (steps always needed).
418
+ *
419
+ * @param {string[]} deployEnvironments
420
+ * @param {Record<string, { type?: string, method?: string, trigger?: string }>} environments
421
+ * @param {'deploy'|'rollback'} [kind]
422
+ * @returns {string|null}
423
+ */
424
+ function getKubernetesSetupIfExpression(
425
+ deployEnvironments,
426
+ environments,
427
+ kind = 'deploy'
428
+ ) {
429
+ const k8sNames = deployEnvironments.filter(
430
+ (n) => getEnvMethod(environments[n]) === 'kubernetes'
431
+ );
432
+ if (k8sNames.length === 0) return null;
433
+
434
+ const nonK8s = deployEnvironments.filter(
435
+ (n) => getEnvMethod(environments[n]) !== 'kubernetes'
436
+ );
437
+ // All enabled envs are kubernetes — setup is always required.
438
+ if (nonK8s.length === 0) return null;
439
+
440
+ const dispatchNeedK8s = [
441
+ ...k8sNames.map((n) => `inputs.environment == '${n}'`),
442
+ `inputs.environment == 'all'`,
443
+ `inputs.environment == ''`,
444
+ ].join(' || ');
445
+
446
+ if (kind === 'rollback') {
447
+ return `github.event_name == 'workflow_dispatch' && (${dispatchNeedK8s})`;
448
+ }
449
+
450
+ const pushK8s = k8sNames.filter(
451
+ (n) => getEnvTrigger(environments[n]) === 'push'
452
+ );
453
+ if (pushK8s.length > 0) {
454
+ return `github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && (${dispatchNeedK8s}))`;
455
+ }
456
+ // Manual-only kubernetes: never configure kubeconfig on plain push.
457
+ return `github.event_name == 'workflow_dispatch' && (${dispatchNeedK8s})`;
458
+ }
459
+
332
460
  /**
333
461
  * @param {string} [kubeconfigSecretName]
462
+ * @param {string|null} [ifExpression]
334
463
  * @returns {string}
335
464
  */
336
- function getKubernetesSetupSteps(kubeconfigSecretName = 'KUBECONFIG') {
465
+ function getKubernetesSetupSteps(kubeconfigSecretName = 'KUBECONFIG', ifExpression = null) {
337
466
  // One kubeconfig file per job — see resolveKubeconfigWorkflowSecretName LIMITATION.
338
- return ` - name: Setup kubectl
467
+ const ifLine = ifExpression ? `\n if: ${ifExpression}` : '';
468
+ return ` - name: Setup kubectl${ifLine}
339
469
  uses: azure/setup-kubectl@v4
340
470
  with:
341
471
  version: '${KUBECTL_VERSION}'
342
472
 
343
- - name: Configure kubeconfig
473
+ - name: Configure kubeconfig${ifLine}
344
474
  env:
345
475
  KUBECONFIG_SECRET: \${{ secrets.${kubeconfigSecretName} }}
346
476
  run: |
@@ -416,6 +546,7 @@ export function buildWorkflowEnvEntries(
416
546
  ]);
417
547
 
418
548
  for (const provider of storageProviders) {
549
+ if (!STORAGE_PROVIDER_IDS.has(provider)) continue;
419
550
  const keys = PROVIDER_ENV_MAP[provider] || [];
420
551
  for (const key of keys) {
421
552
  upsertWorkflowEnvLine(envVars, key, `\${{ secrets.${key} }}`);
@@ -546,8 +677,13 @@ export function generateWorkflowYaml(
546
677
  environments,
547
678
  config
548
679
  );
680
+ const k8sIf = getKubernetesSetupIfExpression(
681
+ allDeployNames,
682
+ environments,
683
+ 'deploy'
684
+ );
549
685
  const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
550
- ? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
686
+ ? `${getKubernetesSetupSteps(kubeconfigSecret, k8sIf)}\n`
551
687
  : '';
552
688
 
553
689
  const projectType = config?.projectType || 'frontend';
@@ -654,6 +790,13 @@ export function generateRollbackWorkflowYaml(
654
790
  const envBlock = formatWorkflowEnvBlock(envVars);
655
791
 
656
792
  const installSpec = getCliInstallSpec(cliSource);
793
+ // Rollback does not generally install project deps; PHP is the exception
794
+ // (composer install) so we only inject setup-php here — not other backends.
795
+ const phpSetupSteps = isPhpProject(config)
796
+ ? [...new Set(getBackendSetupSteps(config))]
797
+ .filter((s) => s.includes('setup-php'))
798
+ .join('\n')
799
+ : '';
657
800
  const githubGitConfigStep = isGithubCliSource(cliSource)
658
801
  ? `${getGithubGitConfigStep()}\n`
659
802
  : '';
@@ -662,14 +805,25 @@ export function generateRollbackWorkflowYaml(
662
805
  environments,
663
806
  config
664
807
  );
808
+ const k8sIf = getKubernetesSetupIfExpression(
809
+ allDeployNames,
810
+ environments,
811
+ 'rollback'
812
+ );
665
813
  const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
666
- ? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
814
+ ? `${getKubernetesSetupSteps(kubeconfigSecret, k8sIf)}\n`
667
815
  : '';
668
816
 
669
817
  const rollbackCmd = getCliRollbackCommand();
670
818
  const envChoiceOptions = formatEnvironmentChoiceOptions(environments);
671
819
  const hasEnvs = envNames.length > 0;
672
820
 
821
+ const phpInstallStep = isPhpProject(config)
822
+ ? ` - name: Install project dependencies
823
+ run: ${getInstallDepsCommand(config)}
824
+ `
825
+ : '';
826
+
673
827
  const environmentInput = hasEnvs
674
828
  ? ` environment:
675
829
  description: 'Environment to roll back (or "all")'
@@ -713,10 +867,10 @@ ${environmentInput}jobs:
713
867
  runs-on: ubuntu-latest
714
868
  steps:
715
869
  - uses: actions/checkout@v4
716
- - uses: actions/setup-node@v4
870
+ ${phpSetupSteps ? `${phpSetupSteps}\n` : ''} - uses: actions/setup-node@v4
717
871
  with:
718
872
  node-version: '20'
719
- ${kubernetesSteps}${githubGitConfigStep} - name: Install DeployHub CLI
873
+ ${kubernetesSteps}${githubGitConfigStep}${phpInstallStep} - name: Install DeployHub CLI
720
874
  run: npm install ${installSpec} --no-save
721
875
  - name: Rollback
722
876
  env:
@@ -737,10 +891,10 @@ function getInstallDepsCommand(config) {
737
891
  config.backend?.framework || config.framework || 'express';
738
892
  const language = config.backend?.language || config.language;
739
893
 
740
- if (language === 'python' || ['fastapi', 'django', 'flask'].includes(framework)) {
894
+ if (language === 'python' || ['fastapi', 'django', 'flask', 'python'].includes(framework)) {
741
895
  return 'pip install -r requirements.txt';
742
896
  }
743
- if (['laravel', 'symfony'].includes(framework)) {
897
+ if (language === 'php' || ['laravel', 'symfony', 'php'].includes(framework)) {
744
898
  return 'composer install --no-interaction';
745
899
  }
746
900
  if (framework === 'spring' || framework === 'java') {
@@ -752,7 +906,7 @@ function getInstallDepsCommand(config) {
752
906
  if (framework === 'dotnet') {
753
907
  return 'dotnet restore';
754
908
  }
755
- if (framework === 'rails') {
909
+ if (framework === 'rails' || framework === 'ruby') {
756
910
  return 'bundle install';
757
911
  }
758
912
  return 'npm install';
@@ -1202,9 +1356,14 @@ export function getGithubSecretsChecklist(
1202
1356
  }
1203
1357
 
1204
1358
  for (const provider of storageProviders) {
1359
+ if (!STORAGE_PROVIDER_IDS.has(provider)) continue;
1205
1360
  const keys = PROVIDER_ENV_MAP[provider] || [];
1206
1361
  for (const key of keys) {
1207
- byKey.set(key, { key, required: true });
1362
+ byKey.set(key, {
1363
+ key,
1364
+ required: true,
1365
+ note: STORAGE_SECRET_NOTES[key] || 'storage credential',
1366
+ });
1208
1367
  }
1209
1368
  }
1210
1369
 
@@ -1280,18 +1439,36 @@ export function generateEnvExampleContent(
1280
1439
  };
1281
1440
 
1282
1441
  for (const provider of storageProviders) {
1442
+ // Defensive: ignore accidental deploy-method ids in storageProviders
1443
+ // (PROVIDER_ENV_MAP also maps ec2/ssh/… for workflow wiring).
1444
+ if (!STORAGE_PROVIDER_IDS.has(provider)) continue;
1283
1445
  const keys = PROVIDER_ENV_MAP[provider] || [];
1284
1446
  if (keys.length > 0) {
1285
1447
  addSection(PROVIDER_LABELS[provider] || provider, keys);
1286
1448
  }
1287
1449
  }
1288
1450
 
1451
+ /** @type {Set<string>} */
1452
+ const seenDeployEnvs = new Set();
1453
+ const cfg = {
1454
+ ...(config || {}),
1455
+ environments: environments || config?.environments || {},
1456
+ };
1457
+
1289
1458
  for (const envName of deployEnvironments) {
1459
+ if (seenDeployEnvs.has(envName)) continue;
1460
+ seenDeployEnvs.add(envName);
1461
+
1290
1462
  const env = environments[envName];
1291
1463
  const method = getEnvMethod(env);
1292
1464
  if (!method) continue;
1293
1465
 
1294
- const deploySection = generateDeploymentEnvSection(method, config, environments);
1466
+ const deploySection = generateDeploymentEnvSection(
1467
+ method,
1468
+ cfg,
1469
+ environments,
1470
+ { envName }
1471
+ );
1295
1472
  if (deploySection) {
1296
1473
  sections.push(`${deploySection}\n`);
1297
1474
  }
@@ -1323,4 +1500,4 @@ export function generateEnvExampleContent(
1323
1500
  return `${sections.join('\n')}\n`;
1324
1501
  }
1325
1502
 
1326
- export { PROVIDER_ENV_MAP };
1503
+ export { PROVIDER_ENV_MAP, STORAGE_PROVIDER_IDS };