@akash-chowdhury-24/deployhub 2.0.5 → 2.0.7

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.5",
3
+ "version": "2.0.7",
4
4
  "description": "Zero-configuration deployment and artifact manager",
5
5
  "type": "module",
6
6
  "main": "./src/cli/index.js",
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  function create(config, cwd) {
7
8
  const log = createLogger('dotnet');
@@ -28,11 +29,17 @@ function create(config, cwd) {
28
29
  },
29
30
 
30
31
  async docker() {
31
- if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
32
- await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
33
- cwd,
34
- stdio: 'inherit',
35
- });
32
+ if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
33
+ return;
34
+ }
35
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
36
+ log.info(`Building Docker image (${fullImage})...`);
37
+ await execa('docker', ['build', '-t', fullImage, '.'], {
38
+ cwd,
39
+ stdio: 'inherit',
40
+ });
41
+ if (fullImage !== latestImage) {
42
+ await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
36
43
  }
37
44
  },
38
45
  };
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  function create(config, cwd) {
7
8
  const log = createLogger('go');
@@ -27,11 +28,17 @@ function create(config, cwd) {
27
28
  },
28
29
 
29
30
  async docker() {
30
- if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
31
- await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
32
- cwd,
33
- stdio: 'inherit',
34
- });
31
+ if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
32
+ return;
33
+ }
34
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
35
+ log.info(`Building Docker image (${fullImage})...`);
36
+ await execa('docker', ['build', '-t', fullImage, '.'], {
37
+ cwd,
38
+ stdio: 'inherit',
39
+ });
40
+ if (fullImage !== latestImage) {
41
+ await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
35
42
  }
36
43
  },
37
44
  };
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  function create(config, cwd) {
7
8
  const log = createLogger('java');
@@ -33,11 +34,17 @@ function create(config, cwd) {
33
34
  },
34
35
 
35
36
  async docker() {
36
- if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
37
- await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
38
- cwd,
39
- stdio: 'inherit',
40
- });
37
+ if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
38
+ return;
39
+ }
40
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
41
+ log.info(`Building Docker image (${fullImage})...`);
42
+ await execa('docker', ['build', '-t', fullImage, '.'], {
43
+ cwd,
44
+ stdio: 'inherit',
45
+ });
46
+ if (fullImage !== latestImage) {
47
+ await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
41
48
  }
42
49
  },
43
50
  };
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  /**
7
8
  * @typedef {Object} LanguageAdapter
@@ -64,12 +65,18 @@ function create(config, cwd) {
64
65
  log.warn('No Dockerfile found, skipping docker build');
65
66
  return;
66
67
  }
67
- log.info('Building Docker image...');
68
- const imageName = `${config.project}:latest`;
69
- await execa('docker', ['build', '-t', imageName, '.'], {
68
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
69
+ log.info(`Building Docker image (${fullImage})...`);
70
+ await execa('docker', ['build', '-t', fullImage, '.'], {
70
71
  cwd,
71
72
  stdio: 'inherit',
72
73
  });
74
+ if (fullImage !== latestImage) {
75
+ await execa('docker', ['tag', fullImage, latestImage], {
76
+ cwd,
77
+ stdio: 'pipe',
78
+ });
79
+ }
73
80
  },
74
81
  };
75
82
  }
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  function create(config, cwd) {
7
8
  const log = createLogger('php');
@@ -30,11 +31,17 @@ function create(config, cwd) {
30
31
  },
31
32
 
32
33
  async docker() {
33
- if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
34
- await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
35
- cwd,
36
- stdio: 'inherit',
37
- });
34
+ if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
35
+ return;
36
+ }
37
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
38
+ log.info(`Building Docker image (${fullImage})...`);
39
+ await execa('docker', ['build', '-t', fullImage, '.'], {
40
+ cwd,
41
+ stdio: 'inherit',
42
+ });
43
+ if (fullImage !== latestImage) {
44
+ await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
38
45
  }
39
46
  },
40
47
  };
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  function create(config, cwd) {
7
8
  const log = createLogger('python');
@@ -43,10 +44,15 @@ function create(config, cwd) {
43
44
  log.warn('No Dockerfile found, skipping');
44
45
  return;
45
46
  }
46
- await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
47
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
48
+ log.info(`Building Docker image (${fullImage})...`);
49
+ await execa('docker', ['build', '-t', fullImage, '.'], {
47
50
  cwd,
48
51
  stdio: 'inherit',
49
52
  });
53
+ if (fullImage !== latestImage) {
54
+ await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
55
+ }
50
56
  },
51
57
  };
52
58
  }
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { createLogger } from '../logger/index.js';
5
+ import { resolveDockerImageRef } from '../utils/docker-image.js';
5
6
 
6
7
  /**
7
8
  * @param {import('../core/config.js').DeployHubConfig} config
@@ -54,11 +55,15 @@ function create(config, cwd) {
54
55
  log.warn('No Dockerfile found, skipping docker build');
55
56
  return;
56
57
  }
57
- log.info('Building Docker image...');
58
- await execa('docker', ['build', '-t', `${config.project}:latest`, '.'], {
58
+ const { fullImage, latestImage } = resolveDockerImageRef(config);
59
+ log.info(`Building Docker image (${fullImage})...`);
60
+ await execa('docker', ['build', '-t', fullImage, '.'], {
59
61
  cwd,
60
62
  stdio: 'inherit',
61
63
  });
64
+ if (fullImage !== latestImage) {
65
+ await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
66
+ }
62
67
  },
63
68
  };
64
69
  }
@@ -13,7 +13,7 @@ import {
13
13
  import { getProjectVersion } from '../utils/version.js';
14
14
  import {
15
15
  writeWorkflowFile,
16
- getRequiredSecrets,
16
+ getGithubSecretsChecklist,
17
17
  generateEnvExampleContent,
18
18
  addDeployhubToPackageJson,
19
19
  DEFAULT_NPM_CLI_SOURCE,
@@ -30,7 +30,10 @@ import {
30
30
  getDockerEnvSecrets,
31
31
  SSH_BASED,
32
32
  } from '../deployment/init-prompts.js';
33
- import { printDeploymentNextSteps } from '../deployment/deployment-env.js';
33
+ import {
34
+ printDeploymentNextSteps,
35
+ formatSecretChecklistLine,
36
+ } from '../deployment/deployment-env.js';
34
37
  import { confirmValueIfContainsSpaces } from '../deployment/init-helpers.js';
35
38
 
36
39
  const FRONTEND_CHOICES = [
@@ -199,6 +202,9 @@ async function generateProjectScaffold(config, environments, cwd) {
199
202
  if (dockerResult.generated) {
200
203
  console.log(chalk.gray(' • Dockerfile (auto-generated)'));
201
204
  }
205
+ if (dockerResult.dockerignoreGenerated) {
206
+ console.log(chalk.gray(' • .dockerignore (auto-generated)'));
207
+ }
202
208
 
203
209
  const k8sResult = await ensureKubernetesManifests(cwd, config, environments);
204
210
  if (k8sResult.generated) {
@@ -495,23 +501,34 @@ export function registerInitCommand(program) {
495
501
  );
496
502
  await fs.writeFile(envExampleDest, envExampleContent);
497
503
 
498
- const secrets = getRequiredSecrets(config.storage, deploy, environments, config);
504
+ const secretsChecklist = getGithubSecretsChecklist(
505
+ config.storage,
506
+ deploy,
507
+ environments,
508
+ config
509
+ );
499
510
 
500
511
  console.log('');
501
512
  if (primaryDeployType) {
502
513
  console.log(chalk.green.bold(`✔ Config generated for ${primaryDeployType} deployment.`));
503
- printDeploymentNextSteps(primaryDeployType, secrets);
514
+ printDeploymentNextSteps(primaryDeployType, secretsChecklist);
504
515
  } else {
505
516
  console.log(chalk.green.bold('✓ DeployHub initialized successfully!'));
506
517
  console.log('');
507
518
  console.log(chalk.bold('Next steps:'));
508
519
  console.log(' 1. Copy .env.example to .env and fill in credentials');
509
- if (secrets.length > 0) {
520
+ if (secretsChecklist.length > 0) {
510
521
  console.log(' 2. Add these secrets to GitHub (Settings → Secrets):');
511
- secrets.forEach((s) => console.log(` • ${s}`));
522
+ secretsChecklist.forEach((item) =>
523
+ console.log(` ${formatSecretChecklistLine(item)}`)
524
+ );
512
525
  }
513
- console.log(` ${secrets.length > 0 ? '3' : '2'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
514
- console.log(` ${secrets.length > 0 ? '4' : '3'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
526
+ console.log(
527
+ ` ${secretsChecklist.length > 0 ? '3' : '2'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`
528
+ );
529
+ console.log(
530
+ ` ${secretsChecklist.length > 0 ? '4' : '3'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`
531
+ );
515
532
  }
516
533
 
517
534
  console.log('');
@@ -41,6 +41,8 @@ export function buildPipelineStages(config, cwd, state) {
41
41
  ctx.config.port = detected.port;
42
42
  }
43
43
  }
44
+ // Resolve version before docker so pipeline build and deploy share the same tag
45
+ ctx.config.version = await getProjectVersion(ctx.cwd);
44
46
  const scaffold = await ensureDeployScaffold(
45
47
  ctx.cwd,
46
48
  ctx.config,
@@ -118,7 +120,9 @@ export function buildPipelineStages(config, cwd, state) {
118
120
  name: 'artifact',
119
121
  enabled: (ctx) => ctx.config.artifact !== false,
120
122
  async run(ctx) {
121
- ctx.config.version = await getProjectVersion(ctx.cwd);
123
+ if (!ctx.config.version) {
124
+ ctx.config.version = await getProjectVersion(ctx.cwd);
125
+ }
122
126
  const result = await createArtifact(
123
127
  ctx.config,
124
128
  /** @type {string[]} */ (ctx.state.deployedTargets || []),
@@ -353,6 +353,8 @@ export const DEPLOYMENT_ENV_KEYS = Object.fromEntries(
353
353
  );
354
354
 
355
355
  /**
356
+ * Locally required env keys for doctor method-specific checks.
357
+ * Excludes optional and CI-only vars.
356
358
  * @param {string} deployType
357
359
  * @param {import('../core/config.js').DeployHubConfig} [config]
358
360
  * @returns {string[]}
@@ -373,7 +375,16 @@ export function getDeploymentEnvKeys(deployType, config = null) {
373
375
  }
374
376
 
375
377
  /**
376
- * Required secrets for GitHub Actions (includes CI-only vars like SSH_KEY).
378
+ * Map a def key to the GitHub Actions secret name (SSH_KEY_PATH SSH_KEY).
379
+ * @param {string} key
380
+ */
381
+ function toGithubSecretKey(key) {
382
+ return key === 'SSH_KEY_PATH' ? 'SSH_KEY' : key;
383
+ }
384
+
385
+ /**
386
+ * Genuinely required secrets for doctor "Secrets" check and required checklist items.
387
+ * Includes CI-only vars (e.g. SSH_KEY) but NOT optional vars.
377
388
  * @param {string} deployType
378
389
  * @param {import('../core/config.js').DeployHubConfig} [config]
379
390
  * @returns {string[]}
@@ -389,14 +400,92 @@ export function getDeploymentSecretKeys(deployType, config = null) {
389
400
  for (const d of defs) {
390
401
  if (d.when === 'backend' && !isBackend) continue;
391
402
  if (d.when === 'optional') continue;
392
- if (d.key === 'SSH_KEY_PATH') {
393
- keys.push('SSH_KEY');
403
+ keys.push(toGithubSecretKey(d.key));
404
+ }
405
+
406
+ return [...new Set(keys)];
407
+ }
408
+
409
+ /**
410
+ * Broad CI-wiring list for the GitHub Actions workflow generator.
411
+ * Includes required, CI-only, and optional keys so `${{ secrets.X }}` is
412
+ * available when the user sets optional secrets — empty secrets are fine.
413
+ * @param {string} deployType
414
+ * @param {import('../core/config.js').DeployHubConfig} [config]
415
+ * @returns {string[]}
416
+ */
417
+ export function getDeploymentWorkflowSecretKeys(deployType, config = null) {
418
+ const defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
419
+ const projectType = config?.projectType || 'frontend';
420
+ const isBackend = projectType === 'backend' || projectType === 'both';
421
+
422
+ /** @type {string[]} */
423
+ const keys = [];
424
+
425
+ for (const d of defs) {
426
+ if (d.when === 'backend' && !isBackend) continue;
427
+ keys.push(toGithubSecretKey(d.key));
428
+ }
429
+
430
+ return [...new Set(keys)];
431
+ }
432
+
433
+ /**
434
+ * @typedef {{ key: string, required: boolean, note?: string }} SecretChecklistItem
435
+ */
436
+
437
+ /**
438
+ * Labeled GitHub Secrets checklist entries for a single deploy method.
439
+ * @param {string} deployType
440
+ * @param {import('../core/config.js').DeployHubConfig} [config]
441
+ * @returns {SecretChecklistItem[]}
442
+ */
443
+ export function getDeploymentSecretChecklistItems(deployType, config = null) {
444
+ const defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
445
+ const projectType = config?.projectType || 'frontend';
446
+ const isBackend = projectType === 'backend' || projectType === 'both';
447
+
448
+ /** @type {Map<string, SecretChecklistItem>} */
449
+ const byKey = new Map();
450
+
451
+ for (const d of defs) {
452
+ if (d.when === 'backend' && !isBackend) continue;
453
+
454
+ const key = toGithubSecretKey(d.key);
455
+ const required = d.when !== 'optional';
456
+ const note =
457
+ d.when === 'optional'
458
+ ? d.optionalReason
459
+ : d.when === 'ci'
460
+ ? d.optionalReason || 'required for GitHub Actions CI (paste private key contents)'
461
+ : undefined;
462
+
463
+ const existing = byKey.get(key);
464
+ if (existing) {
465
+ // Prefer required if any def for this key is required
466
+ if (required && !existing.required) {
467
+ byKey.set(key, { key, required: true, note });
468
+ }
394
469
  continue;
395
470
  }
396
- keys.push(d.key);
471
+
472
+ byKey.set(key, { key, required, note });
397
473
  }
398
474
 
399
- return keys;
475
+ return Array.from(byKey.values());
476
+ }
477
+
478
+ /**
479
+ * Format a checklist item for console output.
480
+ * @param {SecretChecklistItem} item
481
+ */
482
+ export function formatSecretChecklistLine(item) {
483
+ if (item.required) {
484
+ const note = item.note ? ` — ${item.note}` : '';
485
+ return `• ${item.key} (required${note})`;
486
+ }
487
+ const note = item.note ? ` — ${item.note}` : '';
488
+ return `• ${item.key} (optional${note})`;
400
489
  }
401
490
 
402
491
  /**
@@ -442,6 +531,7 @@ export function generateDeploymentEnvSection(
442
531
  return lines.join('\n').trimEnd();
443
532
  }
444
533
 
534
+
445
535
  /**
446
536
  * @param {string} key
447
537
  * @param {import('../core/config.js').DeployHubConfig} [config]
@@ -512,13 +602,15 @@ export const DEPLOYMENT_GUIDE = {
512
602
  ],
513
603
  automates: [
514
604
  'Generates config, workflow, and .env.example for registry and image settings.',
605
+ 'Generates a starter Dockerfile and .dockerignore when missing.',
515
606
  'Tests Docker daemon connectivity during init.',
516
- 'Builds and runs containers via docker compose during deploy.',
607
+ 'Builds the image once during the pipeline docker stage, then reuses it on deploy.',
517
608
  ],
518
609
  after: [
519
- 'Copy .env.example to .env and set DOCKER_IMAGE_NAME (and registry creds if private).',
520
- 'If using a remote Docker host, set DOCKER_HOST and TLS cert paths in .env.',
521
- 'Add the same values as GitHub Secrets for CI.',
610
+ 'Copy .env.example to .env and set DOCKER_IMAGE_NAME (required e.g. myuser/myapp).',
611
+ 'Optional in .env: DOCKER_IMAGE_TAG, DOCKER_REGISTRY_URL, DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH.',
612
+ 'If using a private registry: also set DOCKER_REGISTRY_USERNAME and DOCKER_REGISTRY_TOKEN.',
613
+ 'Add the GitHub Secrets listed below (Settings → Secrets and variables → Actions). Local .env is NOT used by GitHub Actions — doctor only checks your machine.',
522
614
  'Run deployhub doctor to verify Docker is reachable.',
523
615
  'git push origin main to trigger your first deployment.',
524
616
  ],
@@ -612,9 +704,9 @@ export const DEPLOYMENT_GUIDE = {
612
704
 
613
705
  /**
614
706
  * @param {string} deployType
615
- * @param {string[]} [extraSecrets]
707
+ * @param {SecretChecklistItem[]} [checklist]
616
708
  */
617
- export function printDeploymentNextSteps(deployType, extraSecrets = []) {
709
+ export function printDeploymentNextSteps(deployType, checklist = []) {
618
710
  const guide = DEPLOYMENT_GUIDE[deployType];
619
711
  if (!guide) return;
620
712
 
@@ -625,10 +717,10 @@ export function printDeploymentNextSteps(deployType, extraSecrets = []) {
625
717
  console.log(` ${i + 1}. ${step}`);
626
718
  });
627
719
 
628
- if (extraSecrets.length > 0) {
720
+ if (checklist.length > 0) {
629
721
  console.log('\n GitHub Secrets to add (Settings → Secrets and variables → Actions):');
630
- for (const s of extraSecrets) {
631
- console.log(` ${s}`);
722
+ for (const item of checklist) {
723
+ console.log(` ${formatSecretChecklistLine(item)}`);
632
724
  }
633
725
  }
634
726
  }