@akash-chowdhury-24/deployhub 2.0.13 → 2.0.14

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.
@@ -259,19 +259,23 @@ function applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars) {
259
259
  envVars.add('KUBECONFIG: ${{ github.workspace }}/.kube/config');
260
260
  }
261
261
 
262
+ export const DEPLOY_WORKFLOW_FILENAME = 'deployhub.yml';
263
+ export const ROLLBACK_WORKFLOW_FILENAME = 'deployhub-rollback.yml';
264
+
262
265
  /**
266
+ * Shared env entries for deploy and rollback workflows (same secret resolution).
267
+ * Uses getDeploymentWorkflowSecretKeys(env.type) — no separate rollback list.
268
+ *
263
269
  * @param {string[]} storageProviders
264
270
  * @param {string[]} deployEnvironments
265
271
  * @param {Record<string, { type: string }>} environments
266
- * @param {string} [cliSource]
267
272
  * @param {import('../core/config.js').DeployHubConfig} [config]
268
- * @returns {string}
273
+ * @returns {Set<string>}
269
274
  */
270
- export function generateWorkflowYaml(
275
+ export function buildWorkflowEnvEntries(
271
276
  storageProviders,
272
277
  deployEnvironments,
273
278
  environments,
274
- cliSource = DEFAULT_NPM_CLI_SOURCE,
275
279
  config = null
276
280
  ) {
277
281
  /** @type {Set<string>} */
@@ -295,10 +299,65 @@ export function generateWorkflowYaml(
295
299
  }
296
300
 
297
301
  applyKubernetesWorkflowEnv(deployEnvironments, environments, envVars);
302
+ return envVars;
303
+ }
298
304
 
299
- const envBlock = Array.from(envVars)
300
- .map((line) => ` ${line}`)
305
+ /**
306
+ * @param {Set<string>} envVars
307
+ * @param {string} [indent]
308
+ */
309
+ function formatWorkflowEnvBlock(envVars, indent = ' ') {
310
+ return Array.from(envVars)
311
+ .map((line) => `${indent}${line}`)
301
312
  .join('\n');
313
+ }
314
+
315
+ /**
316
+ * Shell command to run deployhub rollback from the installed scoped package.
317
+ * @returns {string}
318
+ */
319
+ export function getCliRollbackCommand() {
320
+ return `node ./node_modules/${NPM_PACKAGE}/src/cli/index.js rollback`;
321
+ }
322
+
323
+ /**
324
+ * Collect secret names referenced as ${{ secrets.NAME }} in workflow YAML.
325
+ * @param {string} yaml
326
+ * @returns {string[]}
327
+ */
328
+ export function extractWorkflowSecretKeys(yaml) {
329
+ /** @type {Set<string>} */
330
+ const keys = new Set();
331
+ const re = /\$\{\{\s*secrets\.([A-Z0-9_]+)\s*\}\}/g;
332
+ let match;
333
+ while ((match = re.exec(yaml)) !== null) {
334
+ keys.add(match[1]);
335
+ }
336
+ return [...keys].sort();
337
+ }
338
+
339
+ /**
340
+ * @param {string[]} storageProviders
341
+ * @param {string[]} deployEnvironments
342
+ * @param {Record<string, { type: string }>} environments
343
+ * @param {string} [cliSource]
344
+ * @param {import('../core/config.js').DeployHubConfig} [config]
345
+ * @returns {string}
346
+ */
347
+ export function generateWorkflowYaml(
348
+ storageProviders,
349
+ deployEnvironments,
350
+ environments,
351
+ cliSource = DEFAULT_NPM_CLI_SOURCE,
352
+ config = null
353
+ ) {
354
+ const envVars = buildWorkflowEnvEntries(
355
+ storageProviders,
356
+ deployEnvironments,
357
+ environments,
358
+ config
359
+ );
360
+ const envBlock = formatWorkflowEnvBlock(envVars);
302
361
 
303
362
  const installSpec = getCliInstallSpec(cliSource);
304
363
  const backendSteps = getBackendSetupSteps(config);
@@ -340,6 +399,71 @@ ${envBlock}
340
399
  return workflow;
341
400
  }
342
401
 
402
+ /**
403
+ * Manual rollback via Actions tab / gh workflow run (workflow_dispatch only).
404
+ *
405
+ * @param {string[]} storageProviders
406
+ * @param {string[]} deployEnvironments
407
+ * @param {Record<string, { type: string }>} environments
408
+ * @param {string} [cliSource]
409
+ * @param {import('../core/config.js').DeployHubConfig} [config]
410
+ * @returns {string}
411
+ */
412
+ export function generateRollbackWorkflowYaml(
413
+ storageProviders,
414
+ deployEnvironments,
415
+ environments,
416
+ cliSource = DEFAULT_NPM_CLI_SOURCE,
417
+ config = null
418
+ ) {
419
+ const envVars = buildWorkflowEnvEntries(
420
+ storageProviders,
421
+ deployEnvironments,
422
+ environments,
423
+ config
424
+ );
425
+ const envBlock = formatWorkflowEnvBlock(envVars);
426
+
427
+ const installSpec = getCliInstallSpec(cliSource);
428
+ const githubGitConfigStep = isGithubCliSource(cliSource)
429
+ ? `${getGithubGitConfigStep()}\n`
430
+ : '';
431
+ const kubernetesSteps = hasKubernetesDeploy(deployEnvironments, environments)
432
+ ? `${getKubernetesSetupSteps()}\n`
433
+ : '';
434
+
435
+ const rollbackCmd = getCliRollbackCommand();
436
+
437
+ return `${getWorkflowHeaderComment()}name: DeployHub Rollback
438
+ on:
439
+ workflow_dispatch:
440
+ inputs:
441
+ buildId:
442
+ description: 'Exact buildId to restore (leave blank = previous build)'
443
+ required: false
444
+ type: string
445
+ jobs:
446
+ rollback:
447
+ runs-on: ubuntu-latest
448
+ steps:
449
+ - uses: actions/checkout@v4
450
+ - uses: actions/setup-node@v4
451
+ with:
452
+ node-version: '20'
453
+ ${kubernetesSteps}${githubGitConfigStep} - name: Install DeployHub CLI
454
+ run: npm install ${installSpec} --no-save
455
+ - name: Rollback
456
+ env:
457
+ ${envBlock}
458
+ run: |
459
+ if [ -n "\${{ inputs.buildId }}" ]; then
460
+ ${rollbackCmd} "\${{ inputs.buildId }}"
461
+ else
462
+ ${rollbackCmd}
463
+ fi
464
+ `;
465
+ }
466
+
343
467
  /**
344
468
  * @param {import('../core/config.js').DeployHubConfig} [config]
345
469
  * @returns {string}
@@ -373,6 +497,8 @@ function getInstallDepsCommand(config) {
373
497
  }
374
498
 
375
499
  /**
500
+ * Write deployhub.yml and deployhub-rollback.yml from the same secret/env helpers.
501
+ *
376
502
  * @param {string[]} storageProviders
377
503
  * @param {string[]} deployEnvironments
378
504
  * @param {Record<string, { type: string }>} environments
@@ -390,14 +516,55 @@ export async function writeWorkflowFile(
390
516
  ) {
391
517
  const workflowDir = path.join(cwd, '.github', 'workflows');
392
518
  await fs.ensureDir(workflowDir);
393
- const content = generateWorkflowYaml(
519
+
520
+ const deployContent = generateWorkflowYaml(
394
521
  storageProviders,
395
522
  deployEnvironments,
396
523
  environments,
397
524
  cliSource,
398
525
  config
399
526
  );
400
- await fs.writeFile(path.join(workflowDir, 'deployhub.yml'), content);
527
+ const rollbackContent = generateRollbackWorkflowYaml(
528
+ storageProviders,
529
+ deployEnvironments,
530
+ environments,
531
+ cliSource,
532
+ config
533
+ );
534
+
535
+ await fs.writeFile(path.join(workflowDir, DEPLOY_WORKFLOW_FILENAME), deployContent);
536
+ await fs.writeFile(path.join(workflowDir, ROLLBACK_WORKFLOW_FILENAME), rollbackContent);
537
+ }
538
+
539
+ /**
540
+ * Doctor helper: informational status for the rollback workflow file.
541
+ * Returns null when the check does not apply (no storage/deploy configured).
542
+ *
543
+ * @param {string} cwd
544
+ * @param {{ storage?: string[], deploy?: string[] }} config
545
+ * @returns {Promise<null | { name: string, pass: boolean, message: string }>}
546
+ */
547
+ export async function getRollbackWorkflowDoctorCheck(cwd, config) {
548
+ const hasStorage = (config.storage || []).length > 0;
549
+ const hasDeploy = (config.deploy || []).length > 0;
550
+ if (!hasStorage || !hasDeploy) return null;
551
+
552
+ const rollbackPath = path.join(cwd, '.github', 'workflows', ROLLBACK_WORKFLOW_FILENAME);
553
+ if (await fs.pathExists(rollbackPath)) {
554
+ return {
555
+ name: 'Rollback workflow',
556
+ pass: true,
557
+ message: `Workflow file exists at .github/workflows/${ROLLBACK_WORKFLOW_FILENAME}`,
558
+ };
559
+ }
560
+
561
+ return {
562
+ name: 'Rollback workflow',
563
+ pass: true,
564
+ message:
565
+ `Missing .github/workflows/${ROLLBACK_WORKFLOW_FILENAME} — ` +
566
+ 'run deployhub sync-workflows to add CI rollback (workflow_dispatch)',
567
+ };
401
568
  }
402
569
 
403
570
  /**
@@ -13,8 +13,9 @@ import path from 'path';
13
13
  * @param {import('../../core/config.js').DeployHubConfig} config
14
14
  * @param {string} artifactDir
15
15
  * @param {string} envName
16
+ * @param {{ buildId: string, semver: string, remoteKey: string }} meta
16
17
  */
17
- async function rollbackTarget(config, artifactDir, envName) {
18
+ async function rollbackTarget(config, artifactDir, envName, meta) {
18
19
  const envConfig = config.environments[envName];
19
20
  if (!envConfig) {
20
21
  throw new Error(`Environment "${envName}" not found in config`);
@@ -24,7 +25,7 @@ async function rollbackTarget(config, artifactDir, envName) {
24
25
  log.info(`Rolling back ${envName} (${envConfig.type || 'server'})...`);
25
26
 
26
27
  const provider = getDeploymentProvider(envConfig.type, config, envName);
27
- await provider.rollback(artifactDir);
28
+ await provider.rollback(artifactDir, meta);
28
29
  }
29
30
 
30
31
  /**
@@ -39,7 +40,7 @@ export async function rollbackToVersion(config, versionOrBuildId, cwd = process.
39
40
  throw new Error('No storage providers configured — cannot download artifact for rollback');
40
41
  }
41
42
 
42
- const history = await loadArtifactHistory(providers, config.project);
43
+ const { entries: history } = await loadArtifactHistory(providers, config.project);
43
44
  const resolved = resolveRollbackTarget(history, versionOrBuildId);
44
45
 
45
46
  if (!resolved.ok) {
@@ -62,15 +63,10 @@ export async function rollbackToVersion(config, versionOrBuildId, cwd = process.
62
63
  const zipPath = path.join(artifactDir, 'artifact.zip');
63
64
  await downloadArtifactEntry(providers, config, entry, zipPath);
64
65
 
66
+ // Extract into artifactDir itself so layout matches a normal build artifact:
67
+ // top-level contents (k8s/, dist/, metadata.json, …) alongside artifact.zip.
65
68
  log.info('Extracting artifact for rollback...');
66
- const extractedDir = path.join(artifactDir, '_extracted');
67
- await fs.emptyDir(extractedDir);
68
- await extractArtifact(artifactDir, extractedDir);
69
-
70
- const extractedDeployment = path.join(extractedDir, 'deployment.json');
71
- if (await fs.pathExists(extractedDeployment)) {
72
- await fs.copy(extractedDeployment, path.join(artifactDir, 'deployment.json'));
73
- }
69
+ await extractArtifact(artifactDir, artifactDir);
74
70
 
75
71
  const targets = config.deploy || [];
76
72
  if (targets.length === 0) {
@@ -78,9 +74,15 @@ export async function rollbackToVersion(config, versionOrBuildId, cwd = process.
78
74
  return { artifactDir, entry };
79
75
  }
80
76
 
77
+ const meta = {
78
+ buildId: entry.buildId,
79
+ semver: entry.semver,
80
+ remoteKey: entry.remoteKey,
81
+ };
82
+
81
83
  log.info('Redeploying previous artifact to server targets...');
82
84
  for (const envName of targets) {
83
- await rollbackTarget(config, artifactDir, envName);
85
+ await rollbackTarget(config, artifactDir, envName, meta);
84
86
  }
85
87
 
86
88
  log.success(`Rollback to buildId=${entry.buildId} complete`);