@akash-chowdhury-24/deployhub 2.0.14 → 2.0.16
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 +98 -25
- package/package.json +1 -1
- package/src/artifact/engine.js +11 -4
- package/src/cli/index.js +4 -0
- package/src/commands/deploy.js +61 -16
- package/src/commands/doctor.js +212 -26
- package/src/commands/env.js +326 -0
- package/src/commands/init.js +97 -64
- package/src/commands/rollback.js +84 -24
- package/src/commands/sync-k8s-ports.js +71 -0
- package/src/commands/verify.js +97 -33
- package/src/core/config.js +371 -33
- package/src/core/environments.js +300 -0
- package/src/core/stages.js +55 -19
- package/src/deployment/deployment-env.js +210 -0
- package/src/deployment/index.js +58 -7
- package/src/deployment/init-prompts.js +151 -46
- package/src/deployment/providers/azure-vm.js +8 -10
- package/src/deployment/providers/docker.js +10 -2
- package/src/deployment/providers/ec2.js +7 -9
- package/src/deployment/providers/gcp-vm.js +8 -10
- package/src/deployment/providers/kubernetes.js +15 -6
- package/src/deployment/providers/ssh.js +18 -13
- package/src/storage/index.js +230 -3
- package/src/utils/build-id.js +23 -0
- package/src/utils/dockerfile-expose.js +117 -0
- package/src/utils/github-actions.js +344 -31
- package/src/utils/health-check.js +158 -0
- package/src/utils/kubernetes-manifests.js +105 -10
- package/src/utils/nginx.js +66 -15
- package/src/utils/rollback/engine.js +147 -40
- package/src/utils/scaffold.js +8 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { resolveContainerPort } from './dockerfile-expose.js';
|
|
4
|
+
import { getEnvMethod, getEnvSettings } from '../core/environments.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* @param {string} name
|
|
@@ -65,6 +67,11 @@ export function generateKubernetesManifests({
|
|
|
65
67
|
? ` imagePullSecrets:\n - name: ${imagePullSecret}\n`
|
|
66
68
|
: '';
|
|
67
69
|
|
|
70
|
+
// Service port stays 80 (Ingress-friendly cluster-facing port).
|
|
71
|
+
// containerPort / targetPort must match the container's listening port (EXPOSE).
|
|
72
|
+
const servicePort = 80;
|
|
73
|
+
const targetPort = port;
|
|
74
|
+
|
|
68
75
|
const deploymentYaml = `apiVersion: apps/v1
|
|
69
76
|
kind: Deployment
|
|
70
77
|
metadata:
|
|
@@ -96,9 +103,6 @@ ${pullSecretBlock} containers:
|
|
|
96
103
|
cpu: "500m"
|
|
97
104
|
`;
|
|
98
105
|
|
|
99
|
-
const servicePort = port === 80 ? 80 : 80;
|
|
100
|
-
const targetPort = port;
|
|
101
|
-
|
|
102
106
|
const serviceYaml = `apiVersion: v1
|
|
103
107
|
kind: Service
|
|
104
108
|
metadata:
|
|
@@ -116,14 +120,32 @@ spec:
|
|
|
116
120
|
return { deploymentYaml, serviceYaml };
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Surgically update only containerPort / targetPort in manifest YAML.
|
|
125
|
+
* Does not change Service `port:`, replicas, resources, env, probes, etc.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} yaml
|
|
128
|
+
* @param {number} port
|
|
129
|
+
* @returns {{ content: string, changed: boolean }}
|
|
130
|
+
*/
|
|
131
|
+
export function patchKubernetesManifestPorts(yaml, port) {
|
|
132
|
+
const next = String(yaml)
|
|
133
|
+
.replace(/^([ \t]*-?[ \t]*containerPort:[ \t]*)\d+[ \t]*$/gm, `$1${port}`)
|
|
134
|
+
.replace(/^([ \t]*targetPort:[ \t]*)\d+[ \t]*$/gm, `$1${port}`);
|
|
135
|
+
|
|
136
|
+
return { content: next, changed: next !== yaml };
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
/**
|
|
120
140
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
121
141
|
* @param {Record<string, Record<string, unknown>>} [environments]
|
|
142
|
+
* @param {{ port?: number }} [options]
|
|
122
143
|
* @returns {{ appName: string, imageName: string, imageTag: string, port: number, namespace: string, imagePullSecret: string }}
|
|
123
144
|
*/
|
|
124
|
-
export function resolveKubernetesManifestOptions(config, environments = {}) {
|
|
145
|
+
export function resolveKubernetesManifestOptions(config, environments = {}, options = {}) {
|
|
125
146
|
const envList = Object.values(environments);
|
|
126
|
-
const
|
|
147
|
+
const k8sRaw = envList.find((env) => getEnvMethod(env) === 'kubernetes') || {};
|
|
148
|
+
const k8sEnv = getEnvSettings(k8sRaw);
|
|
127
149
|
|
|
128
150
|
const appName = config.project || 'app';
|
|
129
151
|
const imageName =
|
|
@@ -142,13 +164,18 @@ export function resolveKubernetesManifestOptions(config, environments = {}) {
|
|
|
142
164
|
process.env.KUBE_IMAGE_PULL_SECRET ||
|
|
143
165
|
'';
|
|
144
166
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
/** @type {number} */
|
|
168
|
+
let port;
|
|
169
|
+
if (typeof options.port === 'number' && Number.isFinite(options.port)) {
|
|
170
|
+
port = options.port;
|
|
171
|
+
} else if (config.projectType === 'both' && config.backend?.port) {
|
|
172
|
+
port = Number(config.backend.port);
|
|
148
173
|
} else if (config.port) {
|
|
149
|
-
port = config.port;
|
|
174
|
+
port = Number(config.port);
|
|
150
175
|
} else if (config.backend?.port) {
|
|
151
|
-
port = config.backend.port;
|
|
176
|
+
port = Number(config.backend.port);
|
|
177
|
+
} else {
|
|
178
|
+
port = 3000;
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
return {
|
|
@@ -161,9 +188,77 @@ export function resolveKubernetesManifestOptions(config, environments = {}) {
|
|
|
161
188
|
};
|
|
162
189
|
}
|
|
163
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Resolve manifest options with Dockerfile EXPOSE → config → fallback port.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} cwd
|
|
195
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
196
|
+
* @param {Record<string, Record<string, unknown>>} [environments]
|
|
197
|
+
*/
|
|
198
|
+
export async function resolveKubernetesManifestOptionsFromCwd(
|
|
199
|
+
cwd,
|
|
200
|
+
config,
|
|
201
|
+
environments = {}
|
|
202
|
+
) {
|
|
203
|
+
const { port, source } = await resolveContainerPort(cwd, config);
|
|
204
|
+
return {
|
|
205
|
+
...resolveKubernetesManifestOptions(config, environments, { port }),
|
|
206
|
+
portSource: source,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Paths DeployHub normally writes for Kubernetes starter manifests.
|
|
212
|
+
* @param {string} cwd
|
|
213
|
+
* @returns {{ deploymentPath: string, servicePath: string }}
|
|
214
|
+
*/
|
|
215
|
+
export function getDefaultKubernetesManifestPaths(cwd) {
|
|
216
|
+
const k8sDir = path.join(cwd, 'k8s');
|
|
217
|
+
return {
|
|
218
|
+
deploymentPath: path.join(k8sDir, 'deployment.yaml'),
|
|
219
|
+
servicePath: path.join(k8sDir, 'service.yaml'),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Patch containerPort/targetPort in existing k8s/deployment.yaml + service.yaml only.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} cwd
|
|
227
|
+
* @param {number} port
|
|
228
|
+
* @returns {Promise<{ patched: string[], skipped: string[], port: number }>}
|
|
229
|
+
*/
|
|
230
|
+
export async function syncKubernetesManifestPorts(cwd, port) {
|
|
231
|
+
const { deploymentPath, servicePath } = getDefaultKubernetesManifestPaths(cwd);
|
|
232
|
+
/** @type {string[]} */
|
|
233
|
+
const patched = [];
|
|
234
|
+
/** @type {string[]} */
|
|
235
|
+
const skipped = [];
|
|
236
|
+
|
|
237
|
+
for (const filePath of [deploymentPath, servicePath]) {
|
|
238
|
+
if (!(await fs.pathExists(filePath))) {
|
|
239
|
+
skipped.push(path.relative(cwd, filePath));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const original = await fs.readFile(filePath, 'utf8');
|
|
243
|
+
const { content, changed } = patchKubernetesManifestPorts(original, port);
|
|
244
|
+
if (changed) {
|
|
245
|
+
await fs.writeFile(filePath, content);
|
|
246
|
+
patched.push(path.relative(cwd, filePath));
|
|
247
|
+
} else {
|
|
248
|
+
skipped.push(path.relative(cwd, filePath));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { patched, skipped, port };
|
|
253
|
+
}
|
|
254
|
+
|
|
164
255
|
export default {
|
|
165
256
|
sanitizeK8sName,
|
|
166
257
|
hasKubernetesManifests,
|
|
167
258
|
generateKubernetesManifests,
|
|
259
|
+
patchKubernetesManifestPorts,
|
|
168
260
|
resolveKubernetesManifestOptions,
|
|
261
|
+
resolveKubernetesManifestOptionsFromCwd,
|
|
262
|
+
getDefaultKubernetesManifestPaths,
|
|
263
|
+
syncKubernetesManifestPorts,
|
|
169
264
|
};
|
package/src/utils/nginx.js
CHANGED
|
@@ -7,6 +7,54 @@ export function sanitizeNginxProjectName(projectName) {
|
|
|
7
7
|
return projectName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Site filename for Nginx activation. Grandfathered envs keep legacy `{project}` only;
|
|
12
|
+
* additional environments use `{project}-{env}` so same-host multi-env deploys don't clobber.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} projectName
|
|
15
|
+
* @param {string} envName
|
|
16
|
+
* @param {boolean} isGrandfathered
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeNginxSiteName(projectName, envName, isGrandfathered) {
|
|
20
|
+
const base = sanitizeNginxProjectName(projectName);
|
|
21
|
+
if (isGrandfathered) {
|
|
22
|
+
// Avoid breaking existing single-env servers that already have sites-available/{project}.
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
return `${base}-${sanitizeNginxProjectName(envName)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
30
|
+
* @param {string} envName
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isGrandfatheredNginxEnv(config, envName) {
|
|
34
|
+
const envs = config.environments || {};
|
|
35
|
+
if (Object.keys(envs).length <= 1) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const grandfather =
|
|
39
|
+
config.unprefixedSecretEnvironment || config.defaultEnvironment || null;
|
|
40
|
+
return Boolean(grandfather && envName === grandfather);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the Nginx site filename for an environment deploy.
|
|
45
|
+
*
|
|
46
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
47
|
+
* @param {string} envName
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function resolveNginxSiteName(config, envName) {
|
|
51
|
+
return sanitizeNginxSiteName(
|
|
52
|
+
config.project || 'app',
|
|
53
|
+
envName,
|
|
54
|
+
isGrandfatheredNginxEnv(config, envName)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
10
58
|
/**
|
|
11
59
|
* Generate nginx server block config for SPA frontend deployments.
|
|
12
60
|
*
|
|
@@ -32,38 +80,38 @@ export function generateNginxConfig(projectName, deployPath, buildOutput = 'dist
|
|
|
32
80
|
}
|
|
33
81
|
|
|
34
82
|
/**
|
|
35
|
-
* Debian/Ubuntu: sites-available path for this
|
|
36
|
-
* @param {string}
|
|
83
|
+
* Debian/Ubuntu: sites-available path for this site name.
|
|
84
|
+
* @param {string} siteName — output of resolveNginxSiteName / sanitizeNginxSiteName
|
|
37
85
|
* @returns {string}
|
|
38
86
|
*/
|
|
39
|
-
export function getNginxSitesAvailablePath(
|
|
40
|
-
return `/etc/nginx/sites-available/${
|
|
87
|
+
export function getNginxSitesAvailablePath(siteName) {
|
|
88
|
+
return `/etc/nginx/sites-available/${siteName}`;
|
|
41
89
|
}
|
|
42
90
|
|
|
43
91
|
/**
|
|
44
|
-
* Debian/Ubuntu: sites-enabled symlink path for this
|
|
45
|
-
* @param {string}
|
|
92
|
+
* Debian/Ubuntu: sites-enabled symlink path for this site name.
|
|
93
|
+
* @param {string} siteName
|
|
46
94
|
* @returns {string}
|
|
47
95
|
*/
|
|
48
|
-
export function getNginxSitesEnabledPath(
|
|
49
|
-
return `/etc/nginx/sites-enabled/${
|
|
96
|
+
export function getNginxSitesEnabledPath(siteName) {
|
|
97
|
+
return `/etc/nginx/sites-enabled/${siteName}`;
|
|
50
98
|
}
|
|
51
99
|
|
|
52
100
|
/**
|
|
53
|
-
* RHEL/Amazon Linux: conf.d drop-in path for this
|
|
54
|
-
* @param {string}
|
|
101
|
+
* RHEL/Amazon Linux: conf.d drop-in path for this site name.
|
|
102
|
+
* @param {string} siteName
|
|
55
103
|
* @returns {string}
|
|
56
104
|
*/
|
|
57
|
-
export function getNginxConfDPath(
|
|
58
|
-
return `/etc/nginx/conf.d/${
|
|
105
|
+
export function getNginxConfDPath(siteName) {
|
|
106
|
+
return `/etc/nginx/conf.d/${siteName}.conf`;
|
|
59
107
|
}
|
|
60
108
|
|
|
61
109
|
/**
|
|
62
|
-
* @param {string}
|
|
110
|
+
* @param {string} siteName
|
|
63
111
|
* @returns {string}
|
|
64
112
|
*/
|
|
65
|
-
export function getNginxSitePath(
|
|
66
|
-
return getNginxSitesAvailablePath(
|
|
113
|
+
export function getNginxSitePath(siteName) {
|
|
114
|
+
return getNginxSitesAvailablePath(siteName);
|
|
67
115
|
}
|
|
68
116
|
|
|
69
117
|
/**
|
|
@@ -83,6 +131,9 @@ export function formatPasswordlessSudoGuidance(sshUser) {
|
|
|
83
131
|
export default {
|
|
84
132
|
generateNginxConfig,
|
|
85
133
|
sanitizeNginxProjectName,
|
|
134
|
+
sanitizeNginxSiteName,
|
|
135
|
+
isGrandfatheredNginxEnv,
|
|
136
|
+
resolveNginxSiteName,
|
|
86
137
|
getNginxSitesAvailablePath,
|
|
87
138
|
getNginxSitesEnabledPath,
|
|
88
139
|
getNginxConfDPath,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { downloadArtifactEntry,
|
|
1
|
+
import { downloadArtifactEntry, ensureLegacyHistoryCopiedToDefaultEnv, loadEnvArtifactHistory } 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';
|
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
formatAmbiguousRollbackMatches,
|
|
7
7
|
resolveRollbackTarget,
|
|
8
8
|
} from '../artifact-history.js';
|
|
9
|
+
import {
|
|
10
|
+
getEnabledEnvironmentNames,
|
|
11
|
+
getEnvMethod,
|
|
12
|
+
resolveDefaultEnvironmentName,
|
|
13
|
+
} from '../../core/environments.js';
|
|
9
14
|
import fs from 'fs-extra';
|
|
10
15
|
import path from 'path';
|
|
11
16
|
|
|
@@ -21,72 +26,174 @@ async function rollbackTarget(config, artifactDir, envName, meta) {
|
|
|
21
26
|
throw new Error(`Environment "${envName}" not found in config`);
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
const method = getEnvMethod(envConfig) || 'server';
|
|
24
30
|
const log = createLogger('rollback');
|
|
25
|
-
log.info(`Rolling back ${envName} (${
|
|
31
|
+
log.info(`Rolling back ${envName} (${method})...`);
|
|
26
32
|
|
|
27
|
-
const provider = getDeploymentProvider(
|
|
33
|
+
const provider = getDeploymentProvider(method, config, envName);
|
|
28
34
|
await provider.rollback(artifactDir, meta);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/**
|
|
38
|
+
* Map resolveRollbackTarget failures to env-scoped messages.
|
|
39
|
+
* @param {string} envName
|
|
40
|
+
* @param {{ reason: string, message: string, matches?: unknown[] }} resolved
|
|
41
|
+
*/
|
|
42
|
+
function formatEnvRollbackError(envName, resolved) {
|
|
43
|
+
if (resolved.reason === 'no-previous') {
|
|
44
|
+
return `No previous build found for environment "${envName}" (only one deploy in that environment's history). Deploy again before rolling back.`;
|
|
45
|
+
}
|
|
46
|
+
if (resolved.reason === 'empty') {
|
|
47
|
+
return `No previous build found for environment "${envName}" — that environment has no deploy history yet. Deploy at least once first.`;
|
|
48
|
+
}
|
|
49
|
+
if (resolved.reason === 'ambiguous' && resolved.matches) {
|
|
50
|
+
return `[${envName}] ${resolved.message}\n${formatAmbiguousRollbackMatches(
|
|
51
|
+
/** @type {import('../artifact-history.js').ArtifactHistoryEntry[]} */ (resolved.matches)
|
|
52
|
+
)}`;
|
|
53
|
+
}
|
|
54
|
+
return `[${envName}] ${resolved.message}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rollback one or more environments. Each target resolves against THAT
|
|
59
|
+
* environment's `envs/{env}/history.json` ONLY — never the project-wide
|
|
60
|
+
* build catalog (`{project}/history.json`) and never another env's history.
|
|
61
|
+
*
|
|
32
62
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
33
63
|
* @param {string} [versionOrBuildId]
|
|
34
64
|
* @param {string} [cwd]
|
|
65
|
+
* @param {{ envNames?: string[], continueOnError?: boolean }} [options]
|
|
66
|
+
* @returns {Promise<{
|
|
67
|
+
* artifactDir: string|null,
|
|
68
|
+
* entry: import('../artifact-history.js').ArtifactHistoryEntry|null,
|
|
69
|
+
* results: { envName: string, entry: import('../artifact-history.js').ArtifactHistoryEntry, artifactDir: string }[],
|
|
70
|
+
* failures: { envName: string, error: string }[],
|
|
71
|
+
* }>}
|
|
35
72
|
*/
|
|
36
|
-
export async function rollbackToVersion(
|
|
73
|
+
export async function rollbackToVersion(
|
|
74
|
+
config,
|
|
75
|
+
versionOrBuildId,
|
|
76
|
+
cwd = process.cwd(),
|
|
77
|
+
options = {}
|
|
78
|
+
) {
|
|
37
79
|
const log = createLogger('rollback');
|
|
38
80
|
const providers = config.storage || [];
|
|
39
81
|
if (providers.length === 0) {
|
|
40
82
|
throw new Error('No storage providers configured — cannot download artifact for rollback');
|
|
41
83
|
}
|
|
42
84
|
|
|
43
|
-
const
|
|
44
|
-
|
|
85
|
+
const targets = options.envNames || getEnabledEnvironmentNames(config);
|
|
86
|
+
if (targets.length === 0) {
|
|
87
|
+
log.warn('No deployment targets configured');
|
|
88
|
+
return { artifactDir: null, entry: null, results: [], failures: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const continueOnError = options.continueOnError === true;
|
|
45
92
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
93
|
+
/** @type {{ envName: string, entry: import('../artifact-history.js').ArtifactHistoryEntry, artifactDir: string }[]} */
|
|
94
|
+
const results = [];
|
|
95
|
+
/** @type {{ envName: string, error: string }[]} */
|
|
96
|
+
const failures = [];
|
|
97
|
+
|
|
98
|
+
for (const envName of targets) {
|
|
99
|
+
try {
|
|
100
|
+
const defaultEnv = resolveDefaultEnvironmentName(config);
|
|
101
|
+
if (defaultEnv && envName === defaultEnv) {
|
|
102
|
+
await ensureLegacyHistoryCopiedToDefaultEnv(
|
|
103
|
+
providers,
|
|
104
|
+
config.project,
|
|
105
|
+
defaultEnv,
|
|
106
|
+
{ legacyHistoryMigrated: config.legacyHistoryMigrated === true }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// CRITICAL: allowLegacyFallback: false — never read project-wide history.json
|
|
111
|
+
// (build catalog) during rollback. Isolation is absolute per environment.
|
|
112
|
+
const { entries: history } = await loadEnvArtifactHistory(
|
|
113
|
+
providers,
|
|
114
|
+
config.project,
|
|
115
|
+
envName,
|
|
116
|
+
{ allowLegacyFallback: false }
|
|
50
117
|
);
|
|
51
|
-
}
|
|
52
|
-
throw new Error(resolved.message);
|
|
53
|
-
}
|
|
54
118
|
|
|
55
|
-
|
|
56
|
-
const restoreDir = path.join(cwd, '.deployhub-restore', `v${entry.buildId}`);
|
|
57
|
-
const artifactDir = path.join(restoreDir, 'artifact');
|
|
119
|
+
const resolved = resolveRollbackTarget(history, versionOrBuildId);
|
|
58
120
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
if (!resolved.ok) {
|
|
122
|
+
throw new Error(formatEnvRollbackError(envName, resolved));
|
|
123
|
+
}
|
|
62
124
|
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
const entry = resolved.entry;
|
|
126
|
+
const restoreDir = path.join(cwd, '.deployhub-restore', envName, `v${entry.buildId}`);
|
|
127
|
+
const artifactDir = path.join(restoreDir, 'artifact');
|
|
65
128
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
129
|
+
log.info(
|
|
130
|
+
`Downloading artifact buildId=${entry.buildId} for env "${envName}" (semver=${entry.semver})...`
|
|
131
|
+
);
|
|
132
|
+
await fs.emptyDir(restoreDir);
|
|
133
|
+
await fs.ensureDir(artifactDir);
|
|
70
134
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
135
|
+
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
136
|
+
await downloadArtifactEntry(providers, config, entry, zipPath);
|
|
137
|
+
|
|
138
|
+
log.info(`Extracting artifact for rollback (${envName})...`);
|
|
139
|
+
await extractArtifact(artifactDir, artifactDir);
|
|
140
|
+
|
|
141
|
+
const meta = {
|
|
142
|
+
buildId: entry.buildId,
|
|
143
|
+
semver: entry.semver,
|
|
144
|
+
remoteKey: entry.remoteKey,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await rollbackTarget(config, artifactDir, envName, meta);
|
|
148
|
+
results.push({ envName, entry, artifactDir });
|
|
149
|
+
log.success(`Rollback of ${envName} to buildId=${entry.buildId} complete`);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
152
|
+
if (!continueOnError) {
|
|
153
|
+
throw err instanceof Error ? err : new Error(error);
|
|
154
|
+
}
|
|
155
|
+
failures.push({ envName, error });
|
|
156
|
+
log.error(`Rollback of ${envName} failed: ${error}`);
|
|
157
|
+
}
|
|
75
158
|
}
|
|
76
159
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
160
|
+
const last = results[results.length - 1];
|
|
161
|
+
return {
|
|
162
|
+
artifactDir: last?.artifactDir || null,
|
|
163
|
+
entry: last?.entry || null,
|
|
164
|
+
results,
|
|
165
|
+
failures,
|
|
81
166
|
};
|
|
167
|
+
}
|
|
82
168
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Format the continue-on-error summary for `--env all`.
|
|
171
|
+
* @param {{ envName: string, entry: { buildId: string } }[]} results
|
|
172
|
+
* @param {{ envName: string, error: string }[]} failures
|
|
173
|
+
* @param {string[]} [skippedDisabled]
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
export function formatRollbackAllSummary(results, failures, skippedDisabled = []) {
|
|
177
|
+
const lines = ['Rollback summary (--env all):'];
|
|
178
|
+
for (const r of results) {
|
|
179
|
+
lines.push(` ✓ ${r.envName.padEnd(14)} → rolled back to ${r.entry.buildId}`);
|
|
86
180
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
181
|
+
for (const f of failures) {
|
|
182
|
+
lines.push(` ✗ ${f.envName.padEnd(14)} → FAILED: ${f.error}`);
|
|
183
|
+
}
|
|
184
|
+
for (const name of skippedDisabled) {
|
|
185
|
+
lines.push(` – ${name.padEnd(14)} → skipped (disabled)`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
if (failures.length === 0) {
|
|
189
|
+
lines.push(`All ${results.length} environment(s) rolled back successfully.`);
|
|
190
|
+
} else {
|
|
191
|
+
const total = results.length + failures.length;
|
|
192
|
+
lines.push(
|
|
193
|
+
`${failures.length} of ${total} environments failed to roll back. See above for details.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return lines.join('\n');
|
|
90
197
|
}
|
|
91
198
|
|
|
92
|
-
export default { rollbackToVersion };
|
|
199
|
+
export default { rollbackToVersion, formatRollbackAllSummary };
|
package/src/utils/scaffold.js
CHANGED
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
generateKubernetesManifests,
|
|
12
12
|
hasKubernetesManifests,
|
|
13
|
-
|
|
13
|
+
resolveKubernetesManifestOptionsFromCwd,
|
|
14
14
|
} from './kubernetes-manifests.js';
|
|
15
|
+
import { getEnvMethod } from '../core/environments.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @param {Record<string, Record<string, unknown>>} environments
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
export function getDeployTypes(environments) {
|
|
21
22
|
return new Set(
|
|
22
23
|
Object.values(environments)
|
|
23
|
-
.map((env) =>
|
|
24
|
+
.map((env) => getEnvMethod(env))
|
|
24
25
|
.filter(Boolean)
|
|
25
26
|
);
|
|
26
27
|
}
|
|
@@ -134,7 +135,11 @@ export async function ensureKubernetesManifests(
|
|
|
134
135
|
return { generated: false };
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
const manifestOptions =
|
|
138
|
+
const manifestOptions = await resolveKubernetesManifestOptionsFromCwd(
|
|
139
|
+
cwd,
|
|
140
|
+
config,
|
|
141
|
+
environments
|
|
142
|
+
);
|
|
138
143
|
const { deploymentYaml, serviceYaml } = generateKubernetesManifests(manifestOptions);
|
|
139
144
|
|
|
140
145
|
const k8sDir = path.join(cwd, 'k8s');
|