@akash-chowdhury-24/deployhub 2.0.16 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akash-chowdhury-24/deployhub",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Zero-configuration deployment and artifact manager",
5
5
  "type": "module",
6
6
  "main": "./src/cli/index.js",
@@ -6,6 +6,7 @@ import {
6
6
  saveConfig,
7
7
  } from '../core/config.js';
8
8
  import {
9
+ getEnabledEnvironmentNames,
9
10
  getEnvMethod,
10
11
  getEnvTrigger,
11
12
  isEnvEnabled,
@@ -25,6 +26,21 @@ import {
25
26
  envLatestArtifactRemoteKey,
26
27
  } from '../utils/build-id.js';
27
28
 
29
+ /**
30
+ * Regenerate GitHub Actions workflows after env enable/disable/add/remove.
31
+ * @param {import('../core/config.js').DeployHubConfig} config
32
+ * @param {string} cwd
33
+ */
34
+ async function regenerateWorkflows(config, cwd) {
35
+ await writeWorkflowFile(
36
+ config.storage || [],
37
+ getEnabledEnvironmentNames(config),
38
+ config.environments || {},
39
+ cwd,
40
+ config.cli?.source,
41
+ config
42
+ );
43
+ }
28
44
  /**
29
45
  * Best-effort last buildId for an env from per-env deploy history.
30
46
  * @param {import('../core/config.js').DeployHubConfig} config
@@ -172,17 +188,7 @@ export function registerEnvCommand(program) {
172
188
 
173
189
  await saveConfig(config, cwd);
174
190
 
175
- const enabledNames = Object.keys(config.environments).filter((n) =>
176
- isEnvEnabled(config.environments[n])
177
- );
178
- await writeWorkflowFile(
179
- config.storage || [],
180
- enabledNames,
181
- config.environments,
182
- cwd,
183
- config.cli?.source,
184
- config
185
- );
191
+ await regenerateWorkflows(config, cwd);
186
192
 
187
193
  log.success(`Added environment "${name}" (${deployAnswers.deployType})`);
188
194
  console.log(chalk.gray(` defaultEnvironment: ${config.defaultEnvironment}`));
@@ -234,7 +240,9 @@ export function registerEnvCommand(program) {
234
240
  }
235
241
  config.environments[name].enabled = true;
236
242
  await saveConfig(config, cwd);
243
+ await regenerateWorkflows(config, cwd);
237
244
  console.log(chalk.green(`✓ Enabled environment "${name}"`));
245
+ console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
238
246
  });
239
247
 
240
248
  env
@@ -250,7 +258,9 @@ export function registerEnvCommand(program) {
250
258
  }
251
259
  config.environments[name].enabled = false;
252
260
  await saveConfig(config, cwd);
261
+ await regenerateWorkflows(config, cwd);
253
262
  console.log(chalk.yellow(`Disabled environment "${name}"`));
263
+ console.log(chalk.gray(' Workflows regenerated — commit .github/workflows if using CI.'));
254
264
  });
255
265
 
256
266
  env
@@ -301,17 +311,7 @@ export function registerEnvCommand(program) {
301
311
  }
302
312
  await saveConfig(config, cwd);
303
313
 
304
- const enabledNames = Object.keys(config.environments).filter((n) =>
305
- isEnvEnabled(config.environments[n])
306
- );
307
- await writeWorkflowFile(
308
- config.storage || [],
309
- enabledNames,
310
- config.environments,
311
- cwd,
312
- config.cli?.source,
313
- config
314
- );
314
+ await regenerateWorkflows(config, cwd);
315
315
 
316
316
  console.log(chalk.green(`✓ Removed environment "${name}"`));
317
317
  console.log(
@@ -28,6 +28,8 @@ import {
28
28
  promptServerDeployment,
29
29
  buildServerEnvEntry,
30
30
  getDockerEnvSecrets,
31
+ applyInitTriggerDefaults,
32
+ formatMultiEnvTriggerReminder,
31
33
  SSH_BASED,
32
34
  } from '../deployment/init-prompts.js';
33
35
  import {
@@ -452,6 +454,8 @@ export function registerInitCommand(program) {
452
454
  defaultEnvironment = picked.defaultEnvironment;
453
455
  }
454
456
 
457
+ applyInitTriggerDefaults(environments, deploy, defaultEnvironment);
458
+
455
459
  const version = await getProjectVersion(cwd);
456
460
  let hasDocker =
457
461
  (detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
@@ -571,6 +575,16 @@ export function registerInitCommand(program) {
571
575
  console.log(' • .github/workflows/deployhub-rollback.yml');
572
576
  console.log(' • .env.example');
573
577
  console.log('');
578
+
579
+ if (deploy.length >= 2 && defaultEnvironment) {
580
+ console.log(
581
+ chalk.yellow(
582
+ formatMultiEnvTriggerReminder(String(defaultEnvironment), deploy)
583
+ )
584
+ );
585
+ console.log('');
586
+ }
587
+
574
588
  printAuthorFooter();
575
589
  });
576
590
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadConfig, loadEnv } from '../core/config.js';
3
+ import { getEnabledEnvironmentNames } from '../core/environments.js';
3
4
  import {
4
5
  writeWorkflowFile,
5
6
  DEPLOY_WORKFLOW_FILENAME,
@@ -22,8 +23,10 @@ export function registerSyncWorkflowsCommand(program) {
22
23
  const config = await loadConfig(cwd);
23
24
 
24
25
  const storage = config.storage || [];
25
- const deploy = config.deploy || [];
26
26
  const environments = config.environments || {};
27
+ // Prefer enabled environments over legacy deploy[] so every reachable env
28
+ // gets its secrets into the regenerated workflow env blocks.
29
+ const deploy = getEnabledEnvironmentNames(config);
27
30
  const cliSource = config.cli?.source;
28
31
 
29
32
  await writeWorkflowFile(storage, deploy, environments, cwd, cliSource, config);
@@ -290,7 +290,7 @@ export function migrateConfigToEnvironments(raw) {
290
290
  newEnvironments.default = {
291
291
  enabled: true,
292
292
  method,
293
- trigger: 'manual',
293
+ trigger: 'push',
294
294
  config: flatConfig,
295
295
  };
296
296
  for (const key of FLAT_DEPLOY_KEYS) {
@@ -323,7 +323,7 @@ export function migrateConfigToEnvironments(raw) {
323
323
  newEnvironments[defaultEnvironment] = {
324
324
  enabled: true,
325
325
  method: 'ssh',
326
- trigger: 'manual',
326
+ trigger: 'push',
327
327
  config: {},
328
328
  };
329
329
  }
@@ -539,6 +539,57 @@ export function buildServerEnvEntry(
539
539
  };
540
540
  }
541
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');
591
+ }
592
+
542
593
  /**
543
594
  * @param {Awaited<ReturnType<typeof promptServerDeployment>>} deployAnswers
544
595
  * @returns {Record<string, string>|null}
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import yaml from 'js-yaml';
4
+ import { fileURLToPath } from 'url';
4
5
  import { getWorkflowHeaderComment } from './author.js';
5
6
  import {
6
7
  generateDeploymentEnvSection,
@@ -135,6 +136,34 @@ export function getCliInstallSpec(cliSource) {
135
136
  return normalized;
136
137
  }
137
138
 
139
+ /**
140
+ * Version/range suitable as a package.json dependency VALUE for this CLI
141
+ * (key is already NPM_PACKAGE — do not embed the package name again).
142
+ *
143
+ * @param {string} [cliSource]
144
+ * @returns {string}
145
+ */
146
+ export function getCliPackageJsonDependencyVersion(cliSource) {
147
+ const normalized = normalizeCliSource(cliSource);
148
+ if (normalized === DEFAULT_NPM_CLI_SOURCE) {
149
+ try {
150
+ const pkgPath = path.join(
151
+ path.dirname(fileURLToPath(import.meta.url)),
152
+ '../../package.json'
153
+ );
154
+ const pkg = fs.readJsonSync(pkgPath);
155
+ if (typeof pkg.version === 'string' && pkg.version.trim()) {
156
+ return `^${pkg.version.trim()}`;
157
+ }
158
+ } catch {
159
+ // fall through
160
+ }
161
+ return 'latest';
162
+ }
163
+ // github: / file: specs are valid package.json dependency values as-is
164
+ return getCliInstallSpec(cliSource);
165
+ }
166
+
138
167
  /**
139
168
  * Shell command to run deployhub build from the installed scoped package.
140
169
  * Uses node directly so it works reliably when installed from a private GitHub repo.
@@ -287,31 +316,51 @@ function upsertWorkflowEnvLine(envVars, lhs, valueExpr) {
287
316
  envVars.add(`${linePrefix}${valueExpr}`);
288
317
  }
289
318
 
319
+ /**
320
+ * Enabled environment names from the live `environments` map (preferred over
321
+ * a possibly-stale `deploy[]` argument passed into workflow generators).
322
+ * @param {Record<string, { type?: string, method?: string }>} environments
323
+ * @returns {string[]}
324
+ */
325
+ function listEnabledEnvironmentNames(environments) {
326
+ return Object.keys(environments || {}).filter((n) => isEnvEnabled(environments[n]));
327
+ }
328
+
329
+ /**
330
+ * Push-triggered enabled envs — same set `pipelineDeployTargets` deploys in GHA.
331
+ * @param {Record<string, { type?: string, method?: string }>} environments
332
+ * @returns {string[]}
333
+ */
334
+ function listPushEnvironmentNames(environments) {
335
+ return listEnabledEnvironmentNames(environments).filter(
336
+ (n) => getEnvTrigger(environments[n]) === 'push'
337
+ );
338
+ }
339
+
290
340
  /**
291
341
  * Shared env entries for deploy and rollback workflows (same secret resolution).
292
- * Multi-env: secrets are prefixed (PRODUCTION_SSH_HOST) but mapped to unprefixed
293
- * process env names the CLI already understands (SSH_HOST).
294
- *
295
- * Prefixed secret names are also exported as their own env vars so a single job can
296
- * carry credentials for every environment; deploy/rollback overlays them per target.
342
+ * Multi-env: each environment contributes its own CI secret names (grandfathered
343
+ * unprefixed SSH_HOST, or PRODUCTION_SSH_HOST, etc.). Prefixed envs are NOT
344
+ * last-wins-mapped onto unprefixed keys — that would overwrite the grandfathered
345
+ * binding and leave production looking fine in YAML while development silently
346
+ * inherits the wrong credentials. `applyEnvSecretOverlay` remaps PREFIX_*
347
+ * unprefixed names at deploy time for non-grandfathered targets.
297
348
  *
298
349
  * @param {string[]} storageProviders
299
- * @param {string[]} deployEnvironments
350
+ * @param {string[]} deployEnvironments — environments whose secrets to inject
300
351
  * @param {Record<string, { type?: string, method?: string }>} environments
301
352
  * @param {import('../core/config.js').DeployHubConfig} [config]
302
- * @param {{ mapForEnv?: string|null }} [options] — when set, only map that env's secrets to unprefixed keys
303
353
  * @returns {Set<string>}
304
354
  */
305
355
  export function buildWorkflowEnvEntries(
306
356
  storageProviders,
307
357
  deployEnvironments,
308
358
  environments,
309
- config = null,
310
- options = {}
359
+ config = null
311
360
  ) {
312
361
  /** @type {Set<string>} */
313
362
  const envVars = new Set([
314
- `DEPLOYHUB_ENV: ${options.mapForEnv || deployEnvironments[0] || 'production'}`,
363
+ `DEPLOYHUB_ENV: ${deployEnvironments[0] || 'production'}`,
315
364
  ]);
316
365
 
317
366
  for (const provider of storageProviders) {
@@ -321,13 +370,9 @@ export function buildWorkflowEnvEntries(
321
370
  }
322
371
  }
323
372
 
324
- const targets = options.mapForEnv
325
- ? [options.mapForEnv]
326
- : deployEnvironments;
327
-
328
373
  const cfg = { ...(config || {}), environments };
329
374
 
330
- for (const envName of targets) {
375
+ for (const envName of deployEnvironments) {
331
376
  const env = environments[envName];
332
377
  if (!env) continue;
333
378
 
@@ -338,26 +383,26 @@ export function buildWorkflowEnvEntries(
338
383
  const secretName = envUsesPrefixedSecrets(envName, cfg)
339
384
  ? prefixSecretKey(envName, key)
340
385
  : key;
341
- // Keep every env's prefixed secret available under its full CI name.
342
- if (secretName !== key) {
343
- upsertWorkflowEnvLine(envVars, secretName, `\${{ secrets.${secretName} }}`);
344
- }
345
- // Unprefixed mapping for CLI defaults (last target wins in static YAML;
346
- // applyEnvSecretOverlay restores the correct env's values at deploy time).
347
- upsertWorkflowEnvLine(envVars, key, `\${{ secrets.${secretName} }}`);
386
+ // Bind process.env[secretName] for this environment (prefixed or grandfathered).
387
+ upsertWorkflowEnvLine(envVars, secretName, `\${{ secrets.${secretName} }}`);
348
388
  }
349
389
  }
350
390
 
351
- applyKubernetesWorkflowEnv(targets, environments, envVars);
391
+ applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars);
352
392
  return envVars;
353
393
  }
354
394
 
355
395
  /**
396
+ * Choice options for workflow_dispatch — enabled environments only, plus `all`.
397
+ * Disabled envs are omitted so selecting them cannot waste a CI run.
398
+ *
356
399
  * @param {Record<string, unknown>} environments
357
400
  * @returns {string}
358
401
  */
359
402
  function formatEnvironmentChoiceOptions(environments) {
360
- const names = Object.keys(environments || {});
403
+ const names = Object.keys(environments || {}).filter((n) =>
404
+ isEnvEnabled(environments[n])
405
+ );
361
406
  const options = [...names, 'all'];
362
407
  return options.map((n) => ` - ${n}`).join('\n');
363
408
  }
@@ -412,25 +457,40 @@ export function generateWorkflowYaml(
412
457
  config = null
413
458
  ) {
414
459
  const envNames = Object.keys(environments || {});
460
+ // Secret injection must follow live environments (same as pipelineDeployTargets),
461
+ // not a possibly-stale deploy[] argument — otherwise push-deployed envs missing
462
+ // from deploy[] get no secrets in the job env block.
463
+ const enabledEnvs = listEnabledEnvironmentNames(environments);
464
+ const pushEnvs = listPushEnvironmentNames(environments);
415
465
  const allDeployNames =
416
- deployEnvironments.length > 0 ? deployEnvironments : envNames.filter((n) => isEnvEnabled(environments[n]));
466
+ enabledEnvs.length > 0
467
+ ? enabledEnvs
468
+ : deployEnvironments.length > 0
469
+ ? deployEnvironments
470
+ : envNames;
417
471
 
418
- // Push-triggered auto-deploy targets (build pipeline filters these in CI).
419
- const pushEnvs = allDeployNames.filter(
420
- (n) => isEnvEnabled(environments[n]) && getEnvTrigger(environments[n]) === 'push'
421
- );
422
-
423
- // Secrets for build job: storage + all push envs (or default if none are push).
472
+ // Build step: union of secrets for every push-triggered env (build deploys all of them).
424
473
  const buildSecretEnvs =
425
474
  pushEnvs.length > 0 ? pushEnvs : allDeployNames.slice(0, 1);
426
475
 
427
- const envVars = buildWorkflowEnvEntries(
476
+ // workflow_dispatch step: union for ALL enabled envs (dropdown can pick any / all).
477
+ const dispatchSecretEnvs = allDeployNames;
478
+
479
+ const buildEnvVars = buildWorkflowEnvEntries(
428
480
  storageProviders,
429
481
  buildSecretEnvs,
430
482
  environments,
431
483
  config
432
484
  );
433
- const envBlock = formatWorkflowEnvBlock(envVars);
485
+ const buildEnvBlock = formatWorkflowEnvBlock(buildEnvVars);
486
+
487
+ const dispatchEnvVars = buildWorkflowEnvEntries(
488
+ storageProviders,
489
+ dispatchSecretEnvs,
490
+ environments,
491
+ config
492
+ );
493
+ const dispatchEnvBlock = formatWorkflowEnvBlock(dispatchEnvVars);
434
494
 
435
495
  const installSpec = getCliInstallSpec(cliSource);
436
496
  const backendSteps = getBackendSetupSteps(config);
@@ -467,7 +527,7 @@ ${envChoiceOptions}
467
527
  ? ` - name: Deploy (workflow_dispatch)
468
528
  if: github.event_name == 'workflow_dispatch'
469
529
  env:
470
- ${envBlock}
530
+ ${dispatchEnvBlock}
471
531
  run: |
472
532
  ENV_INPUT="\${{ inputs.environment }}"
473
533
  if [ -z "$ENV_INPUT" ] || [ "$ENV_INPUT" = "all" ]; then
@@ -497,7 +557,7 @@ ${kubernetesSteps}${githubGitConfigStep} - name: Install project dependenci
497
557
  - name: Build (and auto-deploy push-triggered envs)
498
558
  run: ${getCliBuildCommand()}
499
559
  env:
500
- ${envBlock}
560
+ ${buildEnvBlock}
501
561
  ${manualDeployStep}`;
502
562
 
503
563
  return workflow;
@@ -529,10 +589,13 @@ export function generateRollbackWorkflowYaml(
529
589
  config = null
530
590
  ) {
531
591
  const envNames = Object.keys(environments || {});
592
+ const enabledEnvs = listEnabledEnvironmentNames(environments);
532
593
  const allDeployNames =
533
- deployEnvironments.length > 0
534
- ? deployEnvironments
535
- : envNames.filter((n) => isEnvEnabled(environments[n]));
594
+ enabledEnvs.length > 0
595
+ ? enabledEnvs
596
+ : deployEnvironments.length > 0
597
+ ? deployEnvironments
598
+ : envNames.filter((n) => isEnvEnabled(environments[n]));
536
599
 
537
600
  const envVars = buildWorkflowEnvEntries(
538
601
  storageProviders,
@@ -727,11 +790,14 @@ export function expectedWorkflowSecretKeysFromConfig(config, kind = 'rollback')
727
790
  /** @type {string[]} */
728
791
  let targets;
729
792
  if (kind === 'deploy') {
730
- const enabled = allNames.filter((n) => isEnvEnabled(environments[n]));
731
- const pushEnvs = enabled.filter((n) => getEnvTrigger(environments[n]) === 'push');
793
+ const pushEnvs = listPushEnvironmentNames(environments);
794
+ const enabled = listEnabledEnvironmentNames(environments);
795
+ // Deploy workflow Build step uses push envs; doctor also flags dispatch-needed
796
+ // secrets by using the broader enabled set via kind === 'rollback' / checklist.
732
797
  targets = pushEnvs.length > 0 ? pushEnvs : enabled.slice(0, 1);
733
798
  } else {
734
- targets = allNames;
799
+ targets = allNames.length > 0 ? listEnabledEnvironmentNames(environments) : allNames;
800
+ if (targets.length === 0) targets = allNames;
735
801
  }
736
802
 
737
803
  const entries = buildWorkflowEnvEntries(
@@ -785,7 +851,7 @@ export function detectWorkflowConfigDrift(yamlText, config, filename = DEPLOY_WO
785
851
  return { drifted: false, missingEnvs, missingSecrets, summary: '' };
786
852
  }
787
853
 
788
- const envNames = Object.keys(config.environments || {});
854
+ const envNames = listEnabledEnvironmentNames(config.environments || {});
789
855
  const root = /** @type {Record<string, any>} */ (parsed);
790
856
  const options = root?.on?.workflow_dispatch?.inputs?.environment?.options;
791
857
 
@@ -883,7 +949,9 @@ export async function addDeployhubToPackageJson(cliSource, cwd = process.cwd())
883
949
 
884
950
  const pkg = await fs.readJson(pkgPath);
885
951
  pkg.devDependencies = pkg.devDependencies || {};
886
- pkg.devDependencies[NPM_PACKAGE] = getCliInstallSpec(cliSource);
952
+ // package.json value must be a semver range / "latest" / git URL — never "name@version"
953
+ // (that form is only for `npm install <spec>` via getCliInstallSpec).
954
+ pkg.devDependencies[NPM_PACKAGE] = getCliPackageJsonDependencyVersion(cliSource);
887
955
  delete pkg.devDependencies.deployhub;
888
956
  pkg.scripts = pkg.scripts || {};
889
957
  pkg.scripts['deployhub:build'] = 'deployhub build';