@akash-chowdhury-24/deployhub 2.0.33 → 2.0.34

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.33",
3
+ "version": "2.0.34",
4
4
  "description": "Zero-configuration deployment and artifact manager",
5
5
  "type": "module",
6
6
  "main": "./src/cli/index.js",
@@ -15,6 +15,57 @@ import {
15
15
  EXPLICIT_IMAGE_TAG_WARNING,
16
16
  } from './docker-image.js';
17
17
 
18
+ /**
19
+ * Classify `docker pull` failure for rollback logs / interpreted-backend errors.
20
+ * @param {unknown} err
21
+ * @returns {'not found'|'auth failed'|'network error'|string}
22
+ */
23
+ export function classifyDockerPullFailure(err) {
24
+ const execErr = /** @type {{ message?: string, stderr?: string, stdout?: string }} */ (err);
25
+ const combined = `${execErr?.message || ''} ${execErr?.stderr || ''} ${execErr?.stdout || ''}`.toLowerCase();
26
+ // Docker Hub reports missing repos as "denied" — check existence first.
27
+ if (
28
+ combined.includes('manifest unknown') ||
29
+ combined.includes('not found') ||
30
+ combined.includes('repository does not exist') ||
31
+ combined.includes('no such image')
32
+ ) {
33
+ return 'not found';
34
+ }
35
+ if (
36
+ combined.includes('unauthorized') ||
37
+ combined.includes('authentication required') ||
38
+ combined.includes('access denied') ||
39
+ combined.includes('denied: requested access')
40
+ ) {
41
+ return 'auth failed';
42
+ }
43
+ if (
44
+ combined.includes('network') ||
45
+ combined.includes('timeout') ||
46
+ combined.includes('timed out') ||
47
+ combined.includes('econnrefused') ||
48
+ combined.includes('connection refused') ||
49
+ combined.includes('no such host') ||
50
+ combined.includes('dial tcp')
51
+ ) {
52
+ return 'network error';
53
+ }
54
+ const stderr = String(execErr?.stderr || '').trim().split('\n').filter(Boolean).pop();
55
+ return stderr || (err instanceof Error ? err.message : 'pull failed');
56
+ }
57
+
58
+ /**
59
+ * @param {string} imageRef
60
+ * @param {string} reason
61
+ */
62
+ export function formatImageNotLocalAndPullFailed(imageRef, reason) {
63
+ return (
64
+ `Target image ${imageRef} not found locally and could not be pulled ` +
65
+ `from the registry (${reason}).`
66
+ );
67
+ }
68
+
18
69
  /**
19
70
  * Shared Docker image build, reuse, push, and pullability logic used by
20
71
  * docker and kubernetes deploy providers.
@@ -105,6 +156,41 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
105
156
  }
106
157
  }
107
158
 
159
+ /**
160
+ * Rollback (and any skipImageReuse path): after a local-cache miss, pull the
161
+ * exact tag from the registry before falling through to artifact rebuild.
162
+ * @param {string} ref
163
+ * @returns {Promise<{ ok: true, output: string }|{ ok: false, reason: string, output: string }>}
164
+ */
165
+ async function tryPullImage(ref) {
166
+ log.info(`Pulling ${ref} from registry...`);
167
+ try {
168
+ const pulled = await execa('docker', ['pull', ref], {
169
+ stdio: 'pipe',
170
+ env: getDockerEnv(),
171
+ });
172
+ const output = [pulled.stdout, pulled.stderr].filter(Boolean).join('\n').trim();
173
+ if (output) log.info(output);
174
+ if (await imageExistsLocally(ref)) {
175
+ log.success(`Pulled ${ref}`);
176
+ return { ok: true, output };
177
+ }
178
+ return {
179
+ ok: false,
180
+ reason: 'pull reported success but image is still missing locally',
181
+ output,
182
+ };
183
+ } catch (err) {
184
+ const execErr = /** @type {{ stdout?: string, stderr?: string }} */ (err);
185
+ const output = [execErr.stdout, execErr.stderr]
186
+ .filter(Boolean)
187
+ .join('\n')
188
+ .trim();
189
+ if (output) log.info(output);
190
+ return { ok: false, reason: classifyDockerPullFailure(err), output };
191
+ }
192
+ }
193
+
108
194
  /**
109
195
  * Prefer the image already built during the pipeline `docker` stage.
110
196
  * Retag when the pipeline used `:latest` and deploy needs a version tag.
@@ -138,8 +224,15 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
138
224
  * @param {Record<string, unknown>} metadata
139
225
  * @param {string} framework
140
226
  * @param {number} port
227
+ * @param {string} [imageRef]
141
228
  */
142
- async function prepareBackendBuildContext(buildContext, metadata, framework, port) {
229
+ async function prepareBackendBuildContext(
230
+ buildContext,
231
+ metadata,
232
+ framework,
233
+ port,
234
+ imageRef = fullImage
235
+ ) {
143
236
  if (framework === 'spring') {
144
237
  const targetDir = path.join(buildContext, 'target');
145
238
  if (await fs.pathExists(targetDir)) {
@@ -199,7 +292,7 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
199
292
  if (isInterpretedBackendFramework(framework)) {
200
293
  const gap = describeInterpretedBackendGap(framework);
201
294
  throw new Error(
202
- `Cannot rebuild ${gap.ecosystem} backend image "${fullImage}" from the packaged artifact.\n` +
295
+ `Cannot rebuild ${gap.ecosystem} backend image "${imageRef}" from the packaged artifact.\n` +
203
296
  `Backend artifacts include source/manifests but not ${gap.missing}, ` +
204
297
  `so Dockerfiles that run \`${gap.installCmd}\` cannot reliably succeed from the artifact alone.\n\n` +
205
298
  'What to do instead:\n' +
@@ -286,7 +379,13 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
286
379
  generateFrontendRuntimeDockerfile(buildOutput)
287
380
  );
288
381
  } else {
289
- await prepareBackendBuildContext(buildContext, metadata, framework, port);
382
+ await prepareBackendBuildContext(
383
+ buildContext,
384
+ metadata,
385
+ framework,
386
+ port,
387
+ imageRef
388
+ );
290
389
  }
291
390
 
292
391
  await execa('docker', ['build', '-t', imageRef, '.'], {
@@ -322,6 +421,8 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
322
421
  await dockerLogin();
323
422
 
324
423
  let reused = false;
424
+ /** @type {string|null} */
425
+ let registryPullFailure = null;
325
426
  if (!options.skipImageReuse) {
326
427
  // Normal deploy: prefer pipeline image (exact tag, then :latest retag).
327
428
  reused = await ensureImageFromPipeline(imageRef);
@@ -331,16 +432,32 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
331
432
  log.info(`Using restored image ${imageRef} (skipImageReuse — no :latest retag)`);
332
433
  reused = true;
333
434
  } else {
334
- log.info(
335
- `Target image ${imageRef} not found locally — attempting rebuild from artifact`
336
- );
435
+ const pulled = await tryPullImage(imageRef);
436
+ if (pulled.ok) {
437
+ reused = true;
438
+ } else {
439
+ registryPullFailure = pulled.reason;
440
+ log.info(
441
+ `${formatImageNotLocalAndPullFailed(imageRef, pulled.reason)} — attempting rebuild from artifact`
442
+ );
443
+ }
337
444
  }
338
445
 
339
446
  let ranCompose = false;
340
447
 
341
448
  if (!reused) {
342
- const result = await buildFromArtifactContents(artifactDir, imageRef);
343
- ranCompose = Boolean(result?.ranCompose);
449
+ try {
450
+ const result = await buildFromArtifactContents(artifactDir, imageRef);
451
+ ranCompose = Boolean(result?.ranCompose);
452
+ } catch (err) {
453
+ if (registryPullFailure) {
454
+ const detail = err instanceof Error ? err.message : String(err);
455
+ throw new Error(
456
+ `${formatImageNotLocalAndPullFailed(imageRef, registryPullFailure)}\n${detail}`
457
+ );
458
+ }
459
+ throw err;
460
+ }
344
461
  }
345
462
 
346
463
  if (ranCompose) {
@@ -372,6 +489,7 @@ export function createDockerImageDeployContext(config, env = process.env, log) {
372
489
  dockerLogin,
373
490
  maybePushImage,
374
491
  ensureImageFromPipeline,
492
+ tryPullImage,
375
493
  buildFromArtifactContents,
376
494
  ensureImageReadyForDeploy,
377
495
  };
@@ -11,6 +11,9 @@ import {
11
11
  getEnvMethod,
12
12
  resolveDefaultEnvironmentName,
13
13
  } from '../../core/environments.js';
14
+ import {
15
+ runDockerPortPublishChecksForEnvs,
16
+ } from '../docker-port-publish.js';
14
17
  import fs from 'fs-extra';
15
18
  import path from 'path';
16
19
 
@@ -32,6 +35,19 @@ async function rollbackTarget(config, artifactDir, envName, meta) {
32
35
 
33
36
  const provider = getDeploymentProvider(method, config, envName);
34
37
  await provider.rollback(artifactDir, meta);
38
+
39
+ // Same post-deploy port-publish check as the deploy pipeline verify stage
40
+ // (`runDockerPortPublishChecksForEnvs`). SSH/EC2/VM skip; docker envs fail
41
+ // if the restored container is up without 0.0.0.0:<port>->.
42
+ const portOutcome = await runDockerPortPublishChecksForEnvs(config, [envName], {
43
+ requireRunning: true,
44
+ });
45
+ if (portOutcome.failures.length > 0) {
46
+ throw new Error(portOutcome.failures[0].error);
47
+ }
48
+ for (const r of portOutcome.results) {
49
+ log.success(r.message);
50
+ }
35
51
  }
36
52
 
37
53
  /**