@akash-chowdhury-24/deployhub 2.0.11 → 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.
- package/README.md +33 -9
- package/package.json +1 -1
- package/src/artifact/engine.js +9 -2
- package/src/cli/index.js +2 -0
- package/src/commands/artifact.js +77 -22
- package/src/commands/doctor.js +19 -2
- package/src/commands/init.js +1 -0
- package/src/commands/rollback.js +31 -28
- package/src/commands/sync-workflows.js +43 -0
- package/src/core/config.js +1 -0
- package/src/core/stages.js +8 -1
- package/src/deployment/index.js +3 -2
- package/src/deployment/providers/azure-vm.js +39 -18
- package/src/deployment/providers/docker.js +29 -7
- package/src/deployment/providers/ec2.js +39 -18
- package/src/deployment/providers/gcp-vm.js +39 -18
- package/src/deployment/providers/kubernetes.js +67 -12
- package/src/deployment/providers/ssh.js +1 -1
- package/src/storage/index.js +165 -9
- package/src/storage/providers/aws.js +6 -2
- package/src/storage/providers/dropbox.js +8 -2
- package/src/storage/providers/ftp.js +8 -2
- package/src/storage/storage-errors.js +106 -0
- package/src/utils/artifact-history.js +155 -0
- package/src/utils/build-id.js +149 -0
- package/src/utils/docker-image-deploy.js +44 -21
- package/src/utils/docker-image.js +59 -54
- package/src/utils/github-actions.js +175 -8
- package/src/utils/rollback/engine.js +44 -21
|
@@ -2,53 +2,27 @@
|
|
|
2
2
|
* Shared Docker image naming for pipeline builds and deploy.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { resolveUniqueBuildStamp, highResBuildStamp } from './build-id.js';
|
|
6
6
|
|
|
7
|
-
/** @typedef {'explicit'|'git'|'ci'|'timestamp'} ImageTagSource */
|
|
7
|
+
/** @typedef {'explicit'|'git'|'ci'|'timestamp'|'buildId'} ImageTagSource */
|
|
8
8
|
|
|
9
9
|
export const EXPLICIT_IMAGE_TAG_WARNING =
|
|
10
10
|
'DOCKER_IMAGE_TAG is set — reusing the same tag across deploys can leave Kubernetes pods on a stale image (imagePullPolicy defaults to IfNotPresent) unless imagePullPolicy is Always or a rollout restart runs.';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* High-resolution timestamp for image tags only (artifact versioning keeps getDateVersion()).
|
|
14
|
-
* Minute prefix matches getDateVersion(); seconds+ms avoid collisions in fast rebuild loops.
|
|
15
|
-
* @param {Date} [now]
|
|
16
|
-
* @returns {string}
|
|
17
|
-
*/
|
|
12
|
+
/** @deprecated use highResBuildStamp from build-id.js — kept for existing imports/tests */
|
|
18
13
|
export function highResImageTagFallback(now = new Date()) {
|
|
19
|
-
|
|
20
|
-
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
21
|
-
const d = String(now.getDate()).padStart(2, '0');
|
|
22
|
-
const h = String(now.getHours()).padStart(2, '0');
|
|
23
|
-
const min = String(now.getMinutes()).padStart(2, '0');
|
|
24
|
-
const sec = String(now.getSeconds()).padStart(2, '0');
|
|
25
|
-
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
26
|
-
return `${y}.${m}.${d}.${h}${min}-${sec}${ms}`;
|
|
14
|
+
return highResBuildStamp(now);
|
|
27
15
|
}
|
|
28
16
|
|
|
29
17
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
function defaultGetGitShortSha() {
|
|
33
|
-
try {
|
|
34
|
-
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
35
|
-
encoding: 'utf8',
|
|
36
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
37
|
-
}).trim();
|
|
38
|
-
return sha || null;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Resolve image tag when DOCKER_IMAGE_TAG is unset: git SHA → CI id → high-res timestamp.
|
|
46
|
-
* Does not use config.version (static package versions would prevent redeploys).
|
|
18
|
+
* Resolve image tag when DOCKER_IMAGE_TAG is unset: prefer config.buildId (same
|
|
19
|
+
* pipeline identity as the artifact), else git SHA → CI id → high-res timestamp.
|
|
47
20
|
*
|
|
48
21
|
* @param {Record<string, string|undefined>} [env]
|
|
49
22
|
* @param {{
|
|
50
23
|
* getGitShortSha?: () => string|null,
|
|
51
24
|
* now?: () => Date,
|
|
25
|
+
* buildId?: string,
|
|
52
26
|
* }} [options]
|
|
53
27
|
* @returns {{ imageTag: string, tagSource: ImageTagSource }}
|
|
54
28
|
*/
|
|
@@ -58,32 +32,21 @@ export function resolveImageTag(env = process.env, options = {}) {
|
|
|
58
32
|
return { imageTag: explicit, tagSource: 'explicit' };
|
|
59
33
|
}
|
|
60
34
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (gitSha) {
|
|
64
|
-
return { imageTag: gitSha, tagSource: 'git' };
|
|
35
|
+
if (options.buildId) {
|
|
36
|
+
return { imageTag: options.buildId, tagSource: 'buildId' };
|
|
65
37
|
}
|
|
66
38
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
env.GITHUB_RUN_ID ||
|
|
70
|
-
env.CI_COMMIT_SHORT_SHA ||
|
|
71
|
-
env.CI_PIPELINE_ID;
|
|
72
|
-
if (ciTag) {
|
|
73
|
-
return { imageTag: String(ciTag), tagSource: 'ci' };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const now = options.now ? options.now() : new Date();
|
|
77
|
-
return { imageTag: highResImageTagFallback(now), tagSource: 'timestamp' };
|
|
39
|
+
const { stamp, source } = resolveUniqueBuildStamp(env, options);
|
|
40
|
+
return { imageTag: stamp, tagSource: source };
|
|
78
41
|
}
|
|
79
42
|
|
|
80
43
|
/**
|
|
44
|
+
* Same repository naming as resolveDockerImageRef, with an explicit tag.
|
|
45
|
+
* Ignores DOCKER_IMAGE_TAG / git / CI — used when restoring a known buildId.
|
|
46
|
+
*
|
|
81
47
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
82
48
|
* @param {Record<string, string|undefined>} [env]
|
|
83
|
-
* @param {
|
|
84
|
-
* getGitShortSha?: () => string|null,
|
|
85
|
-
* now?: () => Date,
|
|
86
|
-
* }} [options]
|
|
49
|
+
* @param {string} imageTag
|
|
87
50
|
* @returns {{
|
|
88
51
|
* imageName: string,
|
|
89
52
|
* imageTag: string,
|
|
@@ -93,9 +56,8 @@ export function resolveImageTag(env = process.env, options = {}) {
|
|
|
93
56
|
* tagSource: ImageTagSource,
|
|
94
57
|
* }}
|
|
95
58
|
*/
|
|
96
|
-
export function
|
|
59
|
+
export function resolveDockerImageRefForTag(config, env = process.env, imageTag) {
|
|
97
60
|
const imageName = env.DOCKER_IMAGE_NAME || config.project;
|
|
98
|
-
const { imageTag, tagSource } = resolveImageTag(env, options);
|
|
99
61
|
const registryUrl = env.DOCKER_REGISTRY_URL || '';
|
|
100
62
|
|
|
101
63
|
const repository =
|
|
@@ -109,6 +71,47 @@ export function resolveDockerImageRef(config, env = process.env, options = {}) {
|
|
|
109
71
|
fullImage: `${repository}:${imageTag}`,
|
|
110
72
|
latestImage: `${repository}:latest`,
|
|
111
73
|
legacyLatestImage: `${config.project}:latest`,
|
|
74
|
+
tagSource: 'buildId',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Replace the tag portion of a docker image ref (handles registry:port/name:tag).
|
|
80
|
+
* @param {string} imageRef
|
|
81
|
+
* @param {string} newTag
|
|
82
|
+
*/
|
|
83
|
+
export function replaceDockerImageTag(imageRef, newTag) {
|
|
84
|
+
const lastSlash = imageRef.lastIndexOf('/');
|
|
85
|
+
const lastColon = imageRef.lastIndexOf(':');
|
|
86
|
+
if (lastColon > lastSlash) {
|
|
87
|
+
return `${imageRef.slice(0, lastColon)}:${newTag}`;
|
|
88
|
+
}
|
|
89
|
+
return `${imageRef}:${newTag}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
94
|
+
* @param {Record<string, string|undefined>} [env]
|
|
95
|
+
* @param {{
|
|
96
|
+
* getGitShortSha?: () => string|null,
|
|
97
|
+
* now?: () => Date,
|
|
98
|
+
* }} [options]
|
|
99
|
+
* @returns {{
|
|
100
|
+
* imageName: string,
|
|
101
|
+
* imageTag: string,
|
|
102
|
+
* fullImage: string,
|
|
103
|
+
* latestImage: string,
|
|
104
|
+
* legacyLatestImage: string,
|
|
105
|
+
* tagSource: ImageTagSource,
|
|
106
|
+
* }}
|
|
107
|
+
*/
|
|
108
|
+
export function resolveDockerImageRef(config, env = process.env, options = {}) {
|
|
109
|
+
const { imageTag, tagSource } = resolveImageTag(env, {
|
|
110
|
+
...options,
|
|
111
|
+
buildId: /** @type {{ buildId?: string }} */ (config).buildId,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
...resolveDockerImageRefForTag(config, env, imageTag),
|
|
112
115
|
tagSource,
|
|
113
116
|
};
|
|
114
117
|
}
|
|
@@ -251,6 +254,8 @@ export function describeInterpretedBackendGap(framework) {
|
|
|
251
254
|
|
|
252
255
|
export default {
|
|
253
256
|
resolveDockerImageRef,
|
|
257
|
+
resolveDockerImageRefForTag,
|
|
258
|
+
replaceDockerImageTag,
|
|
254
259
|
resolveImageTag,
|
|
255
260
|
highResImageTagFallback,
|
|
256
261
|
EXPLICIT_IMAGE_TAG_WARNING,
|
|
@@ -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
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
519
|
+
|
|
520
|
+
const deployContent = generateWorkflowYaml(
|
|
394
521
|
storageProviders,
|
|
395
522
|
deployEnvironments,
|
|
396
523
|
environments,
|
|
397
524
|
cliSource,
|
|
398
525
|
config
|
|
399
526
|
);
|
|
400
|
-
|
|
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
|
/**
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { downloadArtifactEntry, loadArtifactHistory } from '../../storage/index.js';
|
|
2
2
|
import { getDeploymentProvider } from '../../deployment/index.js';
|
|
3
3
|
import { extractArtifact } from '../../artifact/engine.js';
|
|
4
4
|
import { createLogger } from '../../logger/index.js';
|
|
5
|
+
import {
|
|
6
|
+
formatAmbiguousRollbackMatches,
|
|
7
|
+
resolveRollbackTarget,
|
|
8
|
+
} from '../artifact-history.js';
|
|
5
9
|
import fs from 'fs-extra';
|
|
6
10
|
import path from 'path';
|
|
7
11
|
|
|
@@ -9,8 +13,9 @@ import path from 'path';
|
|
|
9
13
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
10
14
|
* @param {string} artifactDir
|
|
11
15
|
* @param {string} envName
|
|
16
|
+
* @param {{ buildId: string, semver: string, remoteKey: string }} meta
|
|
12
17
|
*/
|
|
13
|
-
async function rollbackTarget(config, artifactDir, envName) {
|
|
18
|
+
async function rollbackTarget(config, artifactDir, envName, meta) {
|
|
14
19
|
const envConfig = config.environments[envName];
|
|
15
20
|
if (!envConfig) {
|
|
16
21
|
throw new Error(`Environment "${envName}" not found in config`);
|
|
@@ -20,50 +25,68 @@ async function rollbackTarget(config, artifactDir, envName) {
|
|
|
20
25
|
log.info(`Rolling back ${envName} (${envConfig.type || 'server'})...`);
|
|
21
26
|
|
|
22
27
|
const provider = getDeploymentProvider(envConfig.type, config, envName);
|
|
23
|
-
await provider.rollback(artifactDir);
|
|
28
|
+
await provider.rollback(artifactDir, meta);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
28
|
-
* @param {string}
|
|
33
|
+
* @param {string} [versionOrBuildId]
|
|
29
34
|
* @param {string} [cwd]
|
|
30
35
|
*/
|
|
31
|
-
export async function rollbackToVersion(config,
|
|
36
|
+
export async function rollbackToVersion(config, versionOrBuildId, cwd = process.cwd()) {
|
|
32
37
|
const log = createLogger('rollback');
|
|
33
|
-
const
|
|
34
|
-
|
|
38
|
+
const providers = config.storage || [];
|
|
39
|
+
if (providers.length === 0) {
|
|
40
|
+
throw new Error('No storage providers configured — cannot download artifact for rollback');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { entries: history } = await loadArtifactHistory(providers, config.project);
|
|
44
|
+
const resolved = resolveRollbackTarget(history, versionOrBuildId);
|
|
45
|
+
|
|
46
|
+
if (!resolved.ok) {
|
|
47
|
+
if (resolved.reason === 'ambiguous' && resolved.matches) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${resolved.message}\n${formatAmbiguousRollbackMatches(resolved.matches)}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
throw new Error(resolved.message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entry = resolved.entry;
|
|
56
|
+
const restoreDir = path.join(cwd, '.deployhub-restore', `v${entry.buildId}`);
|
|
35
57
|
const artifactDir = path.join(restoreDir, 'artifact');
|
|
36
58
|
|
|
37
|
-
log.info(`Downloading artifact
|
|
59
|
+
log.info(`Downloading artifact buildId=${entry.buildId} (semver=${entry.semver})...`);
|
|
38
60
|
await fs.emptyDir(restoreDir);
|
|
39
61
|
await fs.ensureDir(artifactDir);
|
|
40
62
|
|
|
41
63
|
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
42
|
-
await
|
|
64
|
+
await downloadArtifactEntry(providers, config, entry, zipPath);
|
|
43
65
|
|
|
66
|
+
// Extract into artifactDir itself so layout matches a normal build artifact:
|
|
67
|
+
// top-level contents (k8s/, dist/, metadata.json, …) alongside artifact.zip.
|
|
44
68
|
log.info('Extracting artifact for rollback...');
|
|
45
|
-
|
|
46
|
-
await fs.emptyDir(extractedDir);
|
|
47
|
-
await extractArtifact(artifactDir, extractedDir);
|
|
48
|
-
|
|
49
|
-
const extractedDeployment = path.join(extractedDir, 'deployment.json');
|
|
50
|
-
if (await fs.pathExists(extractedDeployment)) {
|
|
51
|
-
await fs.copy(extractedDeployment, path.join(artifactDir, 'deployment.json'));
|
|
52
|
-
}
|
|
69
|
+
await extractArtifact(artifactDir, artifactDir);
|
|
53
70
|
|
|
54
71
|
const targets = config.deploy || [];
|
|
55
72
|
if (targets.length === 0) {
|
|
56
73
|
log.warn('No deployment targets configured');
|
|
57
|
-
return artifactDir;
|
|
74
|
+
return { artifactDir, entry };
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
const meta = {
|
|
78
|
+
buildId: entry.buildId,
|
|
79
|
+
semver: entry.semver,
|
|
80
|
+
remoteKey: entry.remoteKey,
|
|
81
|
+
};
|
|
82
|
+
|
|
60
83
|
log.info('Redeploying previous artifact to server targets...');
|
|
61
84
|
for (const envName of targets) {
|
|
62
|
-
await rollbackTarget(config, artifactDir, envName);
|
|
85
|
+
await rollbackTarget(config, artifactDir, envName, meta);
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
log.success(`Rollback to
|
|
66
|
-
return artifactDir;
|
|
88
|
+
log.success(`Rollback to buildId=${entry.buildId} complete`);
|
|
89
|
+
return { artifactDir, entry };
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
export default { rollbackToVersion };
|