@akash-chowdhury-24/deployhub 2.0.13 → 2.0.15
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 +53 -11
- package/package.json +1 -1
- package/src/cli/index.js +4 -0
- package/src/commands/artifact.js +17 -9
- package/src/commands/doctor.js +19 -2
- package/src/commands/init.js +1 -0
- package/src/commands/sync-k8s-ports.js +71 -0
- package/src/commands/sync-workflows.js +43 -0
- 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 +34 -10
- 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/docker-image-deploy.js +44 -21
- package/src/utils/docker-image.js +48 -9
- package/src/utils/dockerfile-expose.js +117 -0
- package/src/utils/github-actions.js +175 -8
- package/src/utils/kubernetes-manifests.js +102 -9
- package/src/utils/rollback/engine.js +14 -12
- package/src/utils/scaffold.js +6 -2
|
@@ -20,7 +20,8 @@ export function createEc2Provider(config, envName, env = process.env) {
|
|
|
20
20
|
|
|
21
21
|
if (!instanceId) {
|
|
22
22
|
throw new Error(
|
|
23
|
-
'
|
|
23
|
+
'Could not resolve host via EC2 instance lookup, and no SSH_HOST was set — ' +
|
|
24
|
+
'provide SSH_HOST (instance public IP/DNS) or set EC2_INSTANCE_ID with AWS credentials for auto lookup.'
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -58,34 +59,54 @@ export function createEc2Provider(config, envName, env = process.env) {
|
|
|
58
59
|
} catch (err) {
|
|
59
60
|
const msg = err instanceof Error ? err.message : String(err);
|
|
60
61
|
throw new Error(
|
|
61
|
-
`Could not resolve
|
|
62
|
+
`Could not resolve host via EC2 instance lookup (${instanceId}): ${msg}. ` +
|
|
63
|
+
'Set SSH_HOST to the instance public IP/DNS, or fix AWS CLI credentials / ec2:DescribeInstances access.'
|
|
62
64
|
);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Resolve host (skipping cloud lookup when SSH_HOST/environment.host is set),
|
|
70
|
+
* then create an SSH provider that closes over the resolved host.
|
|
71
|
+
*/
|
|
72
|
+
async function getSshProvider() {
|
|
69
73
|
const host = await resolveHost();
|
|
74
|
+
if (!host) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'Could not resolve host via EC2 instance lookup, and no SSH_HOST was set — provide one or the other.'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
70
79
|
const environment = config.environments[envName];
|
|
71
|
-
if (environment
|
|
80
|
+
if (environment) {
|
|
72
81
|
environment.host = host;
|
|
73
82
|
}
|
|
74
|
-
|
|
75
|
-
env.SSH_HOST = host;
|
|
76
|
-
}
|
|
77
|
-
return sshProvider.connect();
|
|
83
|
+
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
return {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
async connect() {
|
|
88
|
+
const ssh = await getSshProvider();
|
|
89
|
+
return ssh.connect();
|
|
90
|
+
},
|
|
91
|
+
async deploy(artifactDir, options) {
|
|
92
|
+
const ssh = await getSshProvider();
|
|
93
|
+
return ssh.deploy(artifactDir, options);
|
|
94
|
+
},
|
|
95
|
+
async rollback(artifactDir, meta) {
|
|
96
|
+
const ssh = await getSshProvider();
|
|
97
|
+
return ssh.rollback(artifactDir, meta);
|
|
98
|
+
},
|
|
99
|
+
async healthCheck() {
|
|
100
|
+
const ssh = await getSshProvider();
|
|
101
|
+
return ssh.healthCheck();
|
|
102
|
+
},
|
|
103
|
+
async testConnection() {
|
|
104
|
+
const ssh = await getSshProvider();
|
|
105
|
+
return ssh.testConnection();
|
|
106
|
+
},
|
|
107
|
+
async runRemoteCheck(command) {
|
|
108
|
+
const ssh = await getSshProvider();
|
|
109
|
+
return ssh.runRemoteCheck(command);
|
|
89
110
|
},
|
|
90
111
|
};
|
|
91
112
|
}
|
|
@@ -21,7 +21,8 @@ export function createGcpVmProvider(config, envName, env = process.env) {
|
|
|
21
21
|
|
|
22
22
|
if (!projectId || !zone || !instanceName) {
|
|
23
23
|
throw new Error(
|
|
24
|
-
'
|
|
24
|
+
'Could not resolve host via GCP instance lookup, and no SSH_HOST was set — ' +
|
|
25
|
+
'provide SSH_HOST (instance external IP/DNS) or set GCP_PROJECT_ID, GCP_ZONE, and GCP_INSTANCE_NAME for auto lookup.'
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -59,34 +60,54 @@ export function createGcpVmProvider(config, envName, env = process.env) {
|
|
|
59
60
|
} catch (err) {
|
|
60
61
|
const msg = err instanceof Error ? err.message : String(err);
|
|
61
62
|
throw new Error(
|
|
62
|
-
`Could not resolve
|
|
63
|
+
`Could not resolve host via GCP instance lookup (${instanceName}): ${msg}. ` +
|
|
64
|
+
'Set SSH_HOST to the instance external IP/DNS, or run gcloud auth login and verify project/zone/instance name.'
|
|
63
65
|
);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Resolve host (skipping cloud lookup when SSH_HOST/environment.host is set),
|
|
71
|
+
* then create an SSH provider that closes over the resolved host.
|
|
72
|
+
*/
|
|
73
|
+
async function getSshProvider() {
|
|
70
74
|
const host = await resolveHost();
|
|
75
|
+
if (!host) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Could not resolve host via GCP instance lookup, and no SSH_HOST was set — provide one or the other.'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
71
80
|
const environment = config.environments[envName];
|
|
72
|
-
if (environment
|
|
81
|
+
if (environment) {
|
|
73
82
|
environment.host = host;
|
|
74
83
|
}
|
|
75
|
-
|
|
76
|
-
env.SSH_HOST = host;
|
|
77
|
-
}
|
|
78
|
-
return sshProvider.connect();
|
|
84
|
+
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
async connect() {
|
|
89
|
+
const ssh = await getSshProvider();
|
|
90
|
+
return ssh.connect();
|
|
91
|
+
},
|
|
92
|
+
async deploy(artifactDir, options) {
|
|
93
|
+
const ssh = await getSshProvider();
|
|
94
|
+
return ssh.deploy(artifactDir, options);
|
|
95
|
+
},
|
|
96
|
+
async rollback(artifactDir, meta) {
|
|
97
|
+
const ssh = await getSshProvider();
|
|
98
|
+
return ssh.rollback(artifactDir, meta);
|
|
99
|
+
},
|
|
100
|
+
async healthCheck() {
|
|
101
|
+
const ssh = await getSshProvider();
|
|
102
|
+
return ssh.healthCheck();
|
|
103
|
+
},
|
|
104
|
+
async testConnection() {
|
|
105
|
+
const ssh = await getSshProvider();
|
|
106
|
+
return ssh.testConnection();
|
|
107
|
+
},
|
|
108
|
+
async runRemoteCheck(command) {
|
|
109
|
+
const ssh = await getSshProvider();
|
|
110
|
+
return ssh.runRemoteCheck(command);
|
|
90
111
|
},
|
|
91
112
|
};
|
|
92
113
|
}
|
|
@@ -5,6 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import { createLogger } from '../../logger/index.js';
|
|
6
6
|
import { sanitizeK8sName } from '../../utils/kubernetes-manifests.js';
|
|
7
7
|
import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
|
|
8
|
+
import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
8
9
|
import { ensureKubernetesNamespace } from '../../utils/kubernetes-namespace.js';
|
|
9
10
|
import { syncKubernetesDeploymentImage } from '../../utils/kubernetes-deploy-image.js';
|
|
10
11
|
|
|
@@ -50,10 +51,24 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* @param {string} artifactDir
|
|
54
|
+
* @param {{ fullImage?: string, skipImageReuse?: boolean }} [options]
|
|
53
55
|
*/
|
|
54
|
-
async function deploy(artifactDir) {
|
|
56
|
+
async function deploy(artifactDir, options = {}) {
|
|
57
|
+
const imageRef = options.fullImage || imageOps.fullImage;
|
|
58
|
+
const isRollbackRedeploy = Boolean(options.skipImageReuse);
|
|
59
|
+
|
|
55
60
|
log.info(`Deploying to Kubernetes (namespace: ${namespace}${context ? `, context: ${context}` : ''})...`);
|
|
56
61
|
|
|
62
|
+
// Rollback always rebuilds and must push — without registry creds the cluster
|
|
63
|
+
// cannot pull the new tag and would sit in ImagePullBackOff after a false success.
|
|
64
|
+
if (isRollbackRedeploy && !imageOps.hasRegistryCredentials()) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Kubernetes rollback requires DOCKER_REGISTRY_USERNAME and DOCKER_REGISTRY_TOKEN ' +
|
|
67
|
+
`so the rebuilt image (${imageRef}) can be pushed for the cluster to pull. ` +
|
|
68
|
+
'Set those credentials and retry.'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
const manifestDir = artifactDir;
|
|
58
73
|
const hasManifests =
|
|
59
74
|
(await fs.pathExists(path.join(manifestDir, 'k8s'))) ||
|
|
@@ -65,8 +80,11 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
65
80
|
);
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
log.info(`Ensuring container image ${
|
|
69
|
-
const imageResult = await imageOps.ensureImageReadyForDeploy(artifactDir
|
|
83
|
+
log.info(`Ensuring container image ${imageRef} is built and pushed before apply...`);
|
|
84
|
+
const imageResult = await imageOps.ensureImageReadyForDeploy(artifactDir, {
|
|
85
|
+
fullImage: options.fullImage,
|
|
86
|
+
skipImageReuse: options.skipImageReuse,
|
|
87
|
+
});
|
|
70
88
|
if (imageResult.ranCompose) {
|
|
71
89
|
log.warn(
|
|
72
90
|
'docker compose was used — ensure the cluster can pull the resulting image from your registry.'
|
|
@@ -93,23 +111,60 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
93
111
|
// If the live ref already equals fullImage, rollout restart so a new digest is pulled.
|
|
94
112
|
await syncKubernetesDeploymentImage({
|
|
95
113
|
deploymentName,
|
|
96
|
-
fullImage:
|
|
114
|
+
fullImage: imageRef,
|
|
97
115
|
kubectlArgs,
|
|
98
116
|
getKubectlEnv,
|
|
99
117
|
log,
|
|
100
118
|
});
|
|
101
119
|
|
|
120
|
+
// Rollback-only safety net: wait until pods are actually healthy (catches ImagePullBackOff).
|
|
121
|
+
if (isRollbackRedeploy) {
|
|
122
|
+
const timeout = '120s';
|
|
123
|
+
log.info(
|
|
124
|
+
`Waiting for deployment/${deploymentName} rollout to complete (timeout ${timeout})...`
|
|
125
|
+
);
|
|
126
|
+
try {
|
|
127
|
+
await execa(
|
|
128
|
+
'kubectl',
|
|
129
|
+
kubectlArgs([
|
|
130
|
+
'rollout',
|
|
131
|
+
'status',
|
|
132
|
+
`deployment/${deploymentName}`,
|
|
133
|
+
`--timeout=${timeout}`,
|
|
134
|
+
]),
|
|
135
|
+
{ stdio: 'inherit', env: getKubectlEnv() }
|
|
136
|
+
);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Kubernetes rollback failed: deployment/${deploymentName} did not become healthy within ${timeout}. ` +
|
|
141
|
+
`The cluster may be unable to pull ${imageRef} (ImagePullBackOff) or pods are failing. ${detail}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
log.success('Kubernetes deployment complete');
|
|
103
147
|
}
|
|
104
148
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Artifact-based rollback: restore buildId X's code and image (not cluster undo history).
|
|
151
|
+
* @param {string} artifactDir
|
|
152
|
+
* @param {{ buildId?: string, semver?: string, remoteKey?: string }} [meta]
|
|
153
|
+
*/
|
|
154
|
+
async function rollback(artifactDir, meta = {}) {
|
|
155
|
+
if (!meta.buildId) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
'Kubernetes rollback requires buildId from the restored artifact history entry'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const rollbackImage = resolveDockerImageRefForTag(config, env, meta.buildId).fullImage;
|
|
162
|
+
log.info(
|
|
163
|
+
`Rolling back Kubernetes to buildId=${meta.buildId} (image: ${rollbackImage})...`
|
|
164
|
+
);
|
|
165
|
+
await deploy(artifactDir, {
|
|
166
|
+
fullImage: rollbackImage,
|
|
167
|
+
skipImageReuse: true,
|
|
113
168
|
});
|
|
114
169
|
}
|
|
115
170
|
|
|
@@ -405,7 +405,7 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
405
405
|
return result.code === 0 && result.stdout.trim() === 'yes';
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
async function rollback(artifactDir) {
|
|
408
|
+
async function rollback(artifactDir, _meta) {
|
|
409
409
|
await deploy(artifactDir);
|
|
410
410
|
}
|
|
411
411
|
|
package/src/storage/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
parseArtifactHistory,
|
|
21
21
|
prependHistoryEntry,
|
|
22
22
|
} from '../utils/artifact-history.js';
|
|
23
|
+
import { summarizeStorageError } from './storage-errors.js';
|
|
23
24
|
|
|
24
25
|
/** @type {Record<string, (env?: Record<string, string>) => ReturnType<typeof createAwsProvider>>} */
|
|
25
26
|
const PROVIDER_FACTORIES = {
|
|
@@ -59,28 +60,51 @@ function ensureBuildIdentity(config) {
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Read history.json from the first provider that has it.
|
|
63
|
+
* Missing keys across all providers → { entries: [], source: null }.
|
|
64
|
+
* Auth / network / permission failures → thrown with a concise actionable message
|
|
65
|
+
* (not silently treated as "no history").
|
|
66
|
+
*
|
|
62
67
|
* @param {string[]} providers
|
|
63
68
|
* @param {string} project
|
|
64
|
-
* @returns {Promise<
|
|
69
|
+
* @returns {Promise<{
|
|
70
|
+
* entries: import('../utils/artifact-history.js').ArtifactHistoryEntry[],
|
|
71
|
+
* source: string|null,
|
|
72
|
+
* }>}
|
|
65
73
|
*/
|
|
66
74
|
export async function loadArtifactHistory(providers, project) {
|
|
75
|
+
if (!providers || providers.length === 0) {
|
|
76
|
+
return { entries: [], source: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
const key = historyRemoteKey(project);
|
|
68
80
|
const tmp = path.join(os.tmpdir(), `deployhub-history-${Date.now()}.json`);
|
|
81
|
+
|
|
69
82
|
try {
|
|
70
83
|
for (const name of providers) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
try {
|
|
85
|
+
const provider = getStorageProvider(name);
|
|
86
|
+
const exists = await provider.verify(key);
|
|
87
|
+
if (!exists) continue;
|
|
88
|
+
|
|
89
|
+
await provider.download(key, tmp);
|
|
90
|
+
const raw = await fs.readFile(tmp, 'utf8');
|
|
91
|
+
return {
|
|
92
|
+
entries: parseArtifactHistory(raw),
|
|
93
|
+
source: name,
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const reason = summarizeStorageError(err);
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Could not check remote history via ${name}: ${reason} — ` +
|
|
99
|
+
'verify your storage credentials and configuration are correct.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
77
102
|
}
|
|
78
|
-
} catch {
|
|
79
|
-
return [];
|
|
80
103
|
} finally {
|
|
81
104
|
await fs.remove(tmp).catch(() => {});
|
|
82
105
|
}
|
|
83
|
-
|
|
106
|
+
|
|
107
|
+
return { entries: [], source: null };
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
/**
|
|
@@ -2,6 +2,7 @@ import { S3Client, HeadBucketCommand, DeleteObjectCommand, GetObjectCommand } fr
|
|
|
2
2
|
import { Upload } from '@aws-sdk/lib-storage';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { isNotFoundStorageError } from '../storage-errors.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @param {Record<string, string>} env
|
|
@@ -65,6 +66,8 @@ export function createAwsProvider(env = process.env) {
|
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* @param {string} remoteKey
|
|
69
|
+
* @returns {Promise<boolean>} true if object exists; false if missing.
|
|
70
|
+
* Auth / network / permission errors are rethrown (not treated as missing).
|
|
68
71
|
*/
|
|
69
72
|
async function verify(remoteKey) {
|
|
70
73
|
try {
|
|
@@ -72,8 +75,9 @@ export function createAwsProvider(env = process.env) {
|
|
|
72
75
|
new GetObjectCommand({ Bucket: bucket, Key: remoteKey })
|
|
73
76
|
);
|
|
74
77
|
return true;
|
|
75
|
-
} catch {
|
|
76
|
-
return false;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (isNotFoundStorageError(err)) return false;
|
|
80
|
+
throw err;
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Dropbox } from 'dropbox';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { isNotFoundStorageError } from '../storage-errors.js';
|
|
4
5
|
|
|
5
6
|
export function createDropboxProvider(env = process.env) {
|
|
6
7
|
const token = env.DROPBOX_ACCESS_TOKEN;
|
|
@@ -24,13 +25,18 @@ export function createDropboxProvider(env = process.env) {
|
|
|
24
25
|
await fs.writeFile(localPath, fileBlob);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} remoteKey
|
|
30
|
+
* @returns {Promise<boolean>}
|
|
31
|
+
*/
|
|
27
32
|
async function verify(remoteKey) {
|
|
28
33
|
const key = remoteKey.startsWith('/') ? remoteKey : `/${remoteKey}`;
|
|
29
34
|
try {
|
|
30
35
|
await dbx.filesGetMetadata({ path: key });
|
|
31
36
|
return true;
|
|
32
|
-
} catch {
|
|
33
|
-
return false;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (isNotFoundStorageError(err)) return false;
|
|
39
|
+
throw err;
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client } from 'basic-ftp';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { isNotFoundStorageError } from '../storage-errors.js';
|
|
4
5
|
|
|
5
6
|
export function createFtpProvider(env = process.env) {
|
|
6
7
|
const host = env.FTP_HOST;
|
|
@@ -38,6 +39,10 @@ export function createFtpProvider(env = process.env) {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} remoteKey
|
|
44
|
+
* @returns {Promise<boolean>}
|
|
45
|
+
*/
|
|
41
46
|
async function verify(remoteKey) {
|
|
42
47
|
const remotePath = `${basePath}/${remoteKey}`;
|
|
43
48
|
try {
|
|
@@ -45,8 +50,9 @@ export function createFtpProvider(env = process.env) {
|
|
|
45
50
|
await client.size(remotePath);
|
|
46
51
|
});
|
|
47
52
|
return true;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isNotFoundStorageError(err)) return false;
|
|
55
|
+
throw err;
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for storage provider errors (history load, verify, etc.).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Concise, actionable one-line reason — no SDK stack dumps.
|
|
7
|
+
* @param {unknown} err
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function summarizeStorageError(err) {
|
|
11
|
+
if (!err) return 'unknown error';
|
|
12
|
+
|
|
13
|
+
if (typeof err === 'string') {
|
|
14
|
+
return truncateReason(err);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const e = /** @type {Record<string, unknown>} */ (err);
|
|
18
|
+
const name = typeof e.name === 'string' ? e.name : '';
|
|
19
|
+
const code =
|
|
20
|
+
typeof e.Code === 'string'
|
|
21
|
+
? e.Code
|
|
22
|
+
: typeof e.code === 'string'
|
|
23
|
+
? e.code
|
|
24
|
+
: '';
|
|
25
|
+
const message =
|
|
26
|
+
err instanceof Error
|
|
27
|
+
? err.message
|
|
28
|
+
: typeof e.message === 'string'
|
|
29
|
+
? e.message
|
|
30
|
+
: String(err);
|
|
31
|
+
|
|
32
|
+
const firstLine = message.split(/\r?\n/)[0].trim();
|
|
33
|
+
|
|
34
|
+
// Prefer a short "AccessDenied: ..." style when the SDK exposes a name/code.
|
|
35
|
+
const label = [name, code].find(
|
|
36
|
+
(v) => v && v !== 'Error' && !firstLine.includes(v)
|
|
37
|
+
);
|
|
38
|
+
const combined = label ? `${label}: ${firstLine}` : firstLine;
|
|
39
|
+
return truncateReason(combined || 'unknown error');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} text
|
|
44
|
+
*/
|
|
45
|
+
function truncateReason(text) {
|
|
46
|
+
const cleaned = text.replace(/\s+/g, ' ').trim();
|
|
47
|
+
if (cleaned.length <= 200) return cleaned;
|
|
48
|
+
return `${cleaned.slice(0, 197)}...`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when an error almost certainly means the object/key is missing
|
|
53
|
+
* (as opposed to auth, network, or permission failures).
|
|
54
|
+
* @param {unknown} err
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isNotFoundStorageError(err) {
|
|
58
|
+
if (!err || typeof err !== 'object') return false;
|
|
59
|
+
const e = /** @type {Record<string, unknown>} */ (err);
|
|
60
|
+
const status =
|
|
61
|
+
(e.$metadata &&
|
|
62
|
+
typeof e.$metadata === 'object' &&
|
|
63
|
+
/** @type {{ httpStatusCode?: number }} */ (e.$metadata).httpStatusCode) ||
|
|
64
|
+
(typeof e.statusCode === 'number' ? e.statusCode : undefined) ||
|
|
65
|
+
(typeof e.status === 'number' ? e.status : undefined);
|
|
66
|
+
|
|
67
|
+
const name = String(e.name || e.Code || e.code || '');
|
|
68
|
+
const msg = String(e.message || '').toLowerCase();
|
|
69
|
+
const blob = `${name} ${msg}`;
|
|
70
|
+
|
|
71
|
+
// Auth / permission / credential failures are never "not found"
|
|
72
|
+
if (
|
|
73
|
+
/\b(accessdenied|access denied|invalidaccesskey|forbidden|unauthorized|credentials|signaturedoesnotmatch|expiredtoken)\b/i.test(
|
|
74
|
+
blob
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (status === 403 || status === 401) return false;
|
|
81
|
+
|
|
82
|
+
if (status === 404) return true;
|
|
83
|
+
|
|
84
|
+
if (/^(NoSuchKey|NotFound|NotFoundError|ENOENT)$/i.test(name)) return true;
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
/\b(nosuchkey|not\s*found|no such file|path\/not_found)\b/i.test(msg) ||
|
|
88
|
+
/\bthe specified key does not exist\b/i.test(msg)
|
|
89
|
+
) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Dropbox often uses 409 with path/not_found
|
|
94
|
+
if (status === 409 && /not_found/i.test(msg + name + JSON.stringify(e.error || ''))) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// FTP / classic responses
|
|
99
|
+
if (/\b550\b/.test(msg) && /not found|no such|failed to open/i.test(msg)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default { summarizeStorageError, isNotFoundStorageError };
|