@akash-chowdhury-24/deployhub 2.0.27 → 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.
package/README.md CHANGED
@@ -651,6 +651,7 @@ All JS frontends share the same install/build flow: `npm ci` → `npm run build`
651
651
 
652
652
  - **Detect:** `composer.json` with `laravel/framework` or `symfony/framework-bundle`.
653
653
  - **Install:** Composer (on CI and server).
654
+ - **CI runtime:** Generated workflows install PHP via `shivammathur/setup-php@v2` with Composer. Default version is **8.4** (meets current Laravel platform requirements). Override with `"phpVersion": "8.3"` at the config root or under `"backend"` in `deployhub.config.json`, then run `deployhub sync-workflows`.
654
655
  - **Deploy:** SSH with PHP-FPM or `php artisan` for Laravel.
655
656
 
656
657
  > ⚠️ **PHP-FPM deployments restart the FPM service for the ENTIRE host on every deploy.** If you run multiple DeployHub-managed environments on the same server, deploying ANY of them will briefly interrupt in-flight requests for ALL of them. For production use with multiple environments, either use separate hosts per environment, or set up per-environment PHP-FPM pools manually (not yet automated by DeployHub).
@@ -695,7 +696,9 @@ git commit -m "Add DeployHub CI"
695
696
  2. Add every secret listed at the end of `deployhub init` (storage + deployment).
696
697
  3. Push to `main` or `master` — the deploy workflow triggers on push.
697
698
 
698
- The deploy workflow (`deployhub.yml`) installs the correct language runtime (Node, Python, Java, Go, .NET, Ruby) based on your `deployhub.config.json`, installs DeployHub, runs `deployhub build`, and uses your secrets.
699
+ The deploy workflow (`deployhub.yml`) installs the correct language runtime (Node, Python, PHP, Java, Go, .NET, Ruby) based on your `deployhub.config.json`, installs DeployHub, runs `deployhub build`, and uses your secrets. For PHP projects, CI uses `shivammathur/setup-php` (default **8.4**; override with `phpVersion` / `backend.phpVersion`).
700
+
701
+ When an environment uses Kubernetes, the workflow installs `kubectl` and writes kubeconfig from secrets — but only when that run actually needs cluster access (push with a push-triggered k8s env, or workflow_dispatch / rollback targeting a k8s env, `all`, or blank). Plain pushes that only auto-deploy non-k8s environments (e.g. EC2 development) skip those steps.
699
702
 
700
703
  To run a deploy manually: **Actions → DeployHub → Run workflow**.
701
704
 
@@ -1002,7 +1005,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
1002
1005
 
1003
1006
  **What DeployHub automates:**
1004
1007
  - Starter `k8s/deployment.yaml` and `k8s/service.yaml` when no manifests exist (skipped if you already have a `k8s/` directory or root-level Kubernetes YAML files)
1005
- - GitHub Actions installs `kubectl` on the CI runner and writes kubeconfig from secrets (no local `kubectl` required for the automated push-to-main deploy path)
1008
+ - GitHub Actions installs `kubectl` on the CI runner and writes kubeconfig from secrets when the run targets a Kubernetes environment (skipped on push when k8s is manual-only)
1006
1009
  - Lists `kubectl` contexts during `init` for easy selection
1007
1010
  - Auto-detects `~/.kube/config`
1008
1011
  - Complete `.env.example` for kubeconfig, context, namespace, and registry settings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akash-chowdhury-24/deployhub",
3
- "version": "2.0.27",
3
+ "version": "2.0.28",
4
4
  "description": "Zero-configuration deployment and artifact manager",
5
5
  "type": "module",
6
6
  "main": "./src/cli/index.js",
@@ -20,6 +20,8 @@ import {
20
20
  const SideConfigSchema = z.object({
21
21
  framework: z.string(),
22
22
  language: z.string().optional(),
23
+ /** PHP runtime for CI (`shivammathur/setup-php`); e.g. `"8.4"`. */
24
+ phpVersion: z.string().optional(),
23
25
  buildCommand: z.string().nullable().optional(),
24
26
  startCommand: z.string().nullable().optional(),
25
27
  buildOutput: z.string().optional(),
@@ -81,6 +83,11 @@ const ConfigSchema = z.object({
81
83
  projectType: z.enum(['frontend', 'backend', 'both']).default('frontend'),
82
84
  framework: z.string().optional(),
83
85
  language: z.string().optional(),
86
+ /**
87
+ * PHP runtime for generated GitHub Actions (`shivammathur/setup-php`).
88
+ * Prefer `backend.phpVersion` for backend/both projects. Default when unset: `8.4`.
89
+ */
90
+ phpVersion: z.string().optional(),
84
91
  buildCommand: z.string().nullable().optional(),
85
92
  startCommand: z.string().nullable().optional(),
86
93
  buildOutput: z.string().optional(),
@@ -18,10 +18,14 @@ 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
 
27
31
  /** Storage-only providers — never treat deployment method ids as storage. */
@@ -267,6 +271,40 @@ function getGithubGitConfigStep() {
267
271
  fi`;
268
272
  }
269
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
+
270
308
  /**
271
309
  * @param {import('../core/config.js').DeployHubConfig} [config]
272
310
  * @returns {string[]}
@@ -292,6 +330,14 @@ function getBackendSetupSteps(config) {
292
330
  with:
293
331
  python-version: '3.11'`);
294
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
+ }
295
341
  if (['spring', 'java'].includes(framework)) {
296
342
  steps.push(` - uses: actions/setup-java@v4
297
343
  with:
@@ -362,18 +408,69 @@ function resolveKubeconfigWorkflowSecretName(deployEnvironments, environments, c
362
408
  : 'KUBECONFIG';
363
409
  }
364
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
+
365
460
  /**
366
461
  * @param {string} [kubeconfigSecretName]
462
+ * @param {string|null} [ifExpression]
367
463
  * @returns {string}
368
464
  */
369
- function getKubernetesSetupSteps(kubeconfigSecretName = 'KUBECONFIG') {
465
+ function getKubernetesSetupSteps(kubeconfigSecretName = 'KUBECONFIG', ifExpression = null) {
370
466
  // One kubeconfig file per job — see resolveKubeconfigWorkflowSecretName LIMITATION.
371
- return ` - name: Setup kubectl
467
+ const ifLine = ifExpression ? `\n if: ${ifExpression}` : '';
468
+ return ` - name: Setup kubectl${ifLine}
372
469
  uses: azure/setup-kubectl@v4
373
470
  with:
374
471
  version: '${KUBECTL_VERSION}'
375
472
 
376
- - name: Configure kubeconfig
473
+ - name: Configure kubeconfig${ifLine}
377
474
  env:
378
475
  KUBECONFIG_SECRET: \${{ secrets.${kubeconfigSecretName} }}
379
476
  run: |
@@ -580,8 +677,13 @@ export function generateWorkflowYaml(
580
677
  environments,
581
678
  config
582
679
  );
680
+ const k8sIf = getKubernetesSetupIfExpression(
681
+ allDeployNames,
682
+ environments,
683
+ 'deploy'
684
+ );
583
685
  const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
584
- ? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
686
+ ? `${getKubernetesSetupSteps(kubeconfigSecret, k8sIf)}\n`
585
687
  : '';
586
688
 
587
689
  const projectType = config?.projectType || 'frontend';
@@ -688,6 +790,13 @@ export function generateRollbackWorkflowYaml(
688
790
  const envBlock = formatWorkflowEnvBlock(envVars);
689
791
 
690
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
+ : '';
691
800
  const githubGitConfigStep = isGithubCliSource(cliSource)
692
801
  ? `${getGithubGitConfigStep()}\n`
693
802
  : '';
@@ -696,14 +805,25 @@ export function generateRollbackWorkflowYaml(
696
805
  environments,
697
806
  config
698
807
  );
808
+ const k8sIf = getKubernetesSetupIfExpression(
809
+ allDeployNames,
810
+ environments,
811
+ 'rollback'
812
+ );
699
813
  const kubernetesSteps = hasKubernetesDeploy(allDeployNames, environments)
700
- ? `${getKubernetesSetupSteps(kubeconfigSecret)}\n`
814
+ ? `${getKubernetesSetupSteps(kubeconfigSecret, k8sIf)}\n`
701
815
  : '';
702
816
 
703
817
  const rollbackCmd = getCliRollbackCommand();
704
818
  const envChoiceOptions = formatEnvironmentChoiceOptions(environments);
705
819
  const hasEnvs = envNames.length > 0;
706
820
 
821
+ const phpInstallStep = isPhpProject(config)
822
+ ? ` - name: Install project dependencies
823
+ run: ${getInstallDepsCommand(config)}
824
+ `
825
+ : '';
826
+
707
827
  const environmentInput = hasEnvs
708
828
  ? ` environment:
709
829
  description: 'Environment to roll back (or "all")'
@@ -747,10 +867,10 @@ ${environmentInput}jobs:
747
867
  runs-on: ubuntu-latest
748
868
  steps:
749
869
  - uses: actions/checkout@v4
750
- - uses: actions/setup-node@v4
870
+ ${phpSetupSteps ? `${phpSetupSteps}\n` : ''} - uses: actions/setup-node@v4
751
871
  with:
752
872
  node-version: '20'
753
- ${kubernetesSteps}${githubGitConfigStep} - name: Install DeployHub CLI
873
+ ${kubernetesSteps}${githubGitConfigStep}${phpInstallStep} - name: Install DeployHub CLI
754
874
  run: npm install ${installSpec} --no-save
755
875
  - name: Rollback
756
876
  env:
@@ -774,7 +894,7 @@ function getInstallDepsCommand(config) {
774
894
  if (language === 'python' || ['fastapi', 'django', 'flask', 'python'].includes(framework)) {
775
895
  return 'pip install -r requirements.txt';
776
896
  }
777
- if (['laravel', 'symfony', 'php'].includes(framework)) {
897
+ if (language === 'php' || ['laravel', 'symfony', 'php'].includes(framework)) {
778
898
  return 'composer install --no-interaction';
779
899
  }
780
900
  if (framework === 'spring' || framework === 'java') {