@akash-chowdhury-24/deployhub 2.0.5 → 2.0.6
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 +1 -1
- package/src/adapters/dotnet.adapter.js +12 -5
- package/src/adapters/go.adapter.js +12 -5
- package/src/adapters/java.adapter.js +12 -5
- package/src/adapters/node.adapter.js +10 -3
- package/src/adapters/php.adapter.js +12 -5
- package/src/adapters/python.adapter.js +7 -1
- package/src/adapters/rails.adapter.js +7 -2
- package/src/commands/init.js +3 -0
- package/src/core/stages.js +5 -1
- package/src/deployment/deployment-env.js +15 -5
- package/src/deployment/providers/docker.js +283 -36
- package/src/utils/docker-image.js +181 -0
- package/src/utils/dockerfile.js +98 -0
- package/src/utils/scaffold.js +49 -10
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
function create(config, cwd) {
|
|
7
8
|
const log = createLogger('dotnet');
|
|
@@ -28,11 +29,17 @@ function create(config, cwd) {
|
|
|
28
29
|
},
|
|
29
30
|
|
|
30
31
|
async docker() {
|
|
31
|
-
if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
36
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
37
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
38
|
+
cwd,
|
|
39
|
+
stdio: 'inherit',
|
|
40
|
+
});
|
|
41
|
+
if (fullImage !== latestImage) {
|
|
42
|
+
await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
|
|
36
43
|
}
|
|
37
44
|
},
|
|
38
45
|
};
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
function create(config, cwd) {
|
|
7
8
|
const log = createLogger('go');
|
|
@@ -27,11 +28,17 @@ function create(config, cwd) {
|
|
|
27
28
|
},
|
|
28
29
|
|
|
29
30
|
async docker() {
|
|
30
|
-
if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
35
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
36
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
});
|
|
40
|
+
if (fullImage !== latestImage) {
|
|
41
|
+
await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
|
|
35
42
|
}
|
|
36
43
|
},
|
|
37
44
|
};
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
function create(config, cwd) {
|
|
7
8
|
const log = createLogger('java');
|
|
@@ -33,11 +34,17 @@ function create(config, cwd) {
|
|
|
33
34
|
},
|
|
34
35
|
|
|
35
36
|
async docker() {
|
|
36
|
-
if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
41
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
42
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
43
|
+
cwd,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
if (fullImage !== latestImage) {
|
|
47
|
+
await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
|
|
41
48
|
}
|
|
42
49
|
},
|
|
43
50
|
};
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {Object} LanguageAdapter
|
|
@@ -64,12 +65,18 @@ function create(config, cwd) {
|
|
|
64
65
|
log.warn('No Dockerfile found, skipping docker build');
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await execa('docker', ['build', '-t',
|
|
68
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
69
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
70
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
70
71
|
cwd,
|
|
71
72
|
stdio: 'inherit',
|
|
72
73
|
});
|
|
74
|
+
if (fullImage !== latestImage) {
|
|
75
|
+
await execa('docker', ['tag', fullImage, latestImage], {
|
|
76
|
+
cwd,
|
|
77
|
+
stdio: 'pipe',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
73
80
|
},
|
|
74
81
|
};
|
|
75
82
|
}
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
function create(config, cwd) {
|
|
7
8
|
const log = createLogger('php');
|
|
@@ -30,11 +31,17 @@ function create(config, cwd) {
|
|
|
30
31
|
},
|
|
31
32
|
|
|
32
33
|
async docker() {
|
|
33
|
-
if (await fs.pathExists(path.join(cwd, 'Dockerfile'))) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (!(await fs.pathExists(path.join(cwd, 'Dockerfile')))) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
38
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
39
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
40
|
+
cwd,
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
});
|
|
43
|
+
if (fullImage !== latestImage) {
|
|
44
|
+
await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
|
|
38
45
|
}
|
|
39
46
|
},
|
|
40
47
|
};
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
function create(config, cwd) {
|
|
7
8
|
const log = createLogger('python');
|
|
@@ -43,10 +44,15 @@ function create(config, cwd) {
|
|
|
43
44
|
log.warn('No Dockerfile found, skipping');
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
48
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
49
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
47
50
|
cwd,
|
|
48
51
|
stdio: 'inherit',
|
|
49
52
|
});
|
|
53
|
+
if (fullImage !== latestImage) {
|
|
54
|
+
await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
|
|
55
|
+
}
|
|
50
56
|
},
|
|
51
57
|
};
|
|
52
58
|
}
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../logger/index.js';
|
|
5
|
+
import { resolveDockerImageRef } from '../utils/docker-image.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
@@ -54,11 +55,15 @@ function create(config, cwd) {
|
|
|
54
55
|
log.warn('No Dockerfile found, skipping docker build');
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const { fullImage, latestImage } = resolveDockerImageRef(config);
|
|
59
|
+
log.info(`Building Docker image (${fullImage})...`);
|
|
60
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
59
61
|
cwd,
|
|
60
62
|
stdio: 'inherit',
|
|
61
63
|
});
|
|
64
|
+
if (fullImage !== latestImage) {
|
|
65
|
+
await execa('docker', ['tag', fullImage, latestImage], { stdio: 'pipe' });
|
|
66
|
+
}
|
|
62
67
|
},
|
|
63
68
|
};
|
|
64
69
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -199,6 +199,9 @@ async function generateProjectScaffold(config, environments, cwd) {
|
|
|
199
199
|
if (dockerResult.generated) {
|
|
200
200
|
console.log(chalk.gray(' • Dockerfile (auto-generated)'));
|
|
201
201
|
}
|
|
202
|
+
if (dockerResult.dockerignoreGenerated) {
|
|
203
|
+
console.log(chalk.gray(' • .dockerignore (auto-generated)'));
|
|
204
|
+
}
|
|
202
205
|
|
|
203
206
|
const k8sResult = await ensureKubernetesManifests(cwd, config, environments);
|
|
204
207
|
if (k8sResult.generated) {
|
package/src/core/stages.js
CHANGED
|
@@ -41,6 +41,8 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
41
41
|
ctx.config.port = detected.port;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
// Resolve version before docker so pipeline build and deploy share the same tag
|
|
45
|
+
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
44
46
|
const scaffold = await ensureDeployScaffold(
|
|
45
47
|
ctx.cwd,
|
|
46
48
|
ctx.config,
|
|
@@ -118,7 +120,9 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
118
120
|
name: 'artifact',
|
|
119
121
|
enabled: (ctx) => ctx.config.artifact !== false,
|
|
120
122
|
async run(ctx) {
|
|
121
|
-
ctx.config.version
|
|
123
|
+
if (!ctx.config.version) {
|
|
124
|
+
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
125
|
+
}
|
|
122
126
|
const result = await createArtifact(
|
|
123
127
|
ctx.config,
|
|
124
128
|
/** @type {string[]} */ (ctx.state.deployedTargets || []),
|
|
@@ -388,7 +388,14 @@ export function getDeploymentSecretKeys(deployType, config = null) {
|
|
|
388
388
|
|
|
389
389
|
for (const d of defs) {
|
|
390
390
|
if (d.when === 'backend' && !isBackend) continue;
|
|
391
|
-
|
|
391
|
+
// Docker optional vars must still appear in CI workflow/secrets checklist —
|
|
392
|
+
// empty secrets are fine when unused; missing DOCKER_IMAGE_NAME is not.
|
|
393
|
+
if (d.when === 'optional') {
|
|
394
|
+
if (deployType === 'docker' && d.key.startsWith('DOCKER_')) {
|
|
395
|
+
keys.push(d.key);
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
392
399
|
if (d.key === 'SSH_KEY_PATH') {
|
|
393
400
|
keys.push('SSH_KEY');
|
|
394
401
|
continue;
|
|
@@ -512,13 +519,16 @@ export const DEPLOYMENT_GUIDE = {
|
|
|
512
519
|
],
|
|
513
520
|
automates: [
|
|
514
521
|
'Generates config, workflow, and .env.example for registry and image settings.',
|
|
522
|
+
'Generates a starter Dockerfile and .dockerignore when missing.',
|
|
515
523
|
'Tests Docker daemon connectivity during init.',
|
|
516
|
-
'Builds
|
|
524
|
+
'Builds the image once during the pipeline docker stage, then reuses it on deploy.',
|
|
517
525
|
],
|
|
518
526
|
after: [
|
|
519
|
-
'Copy .env.example to .env and set DOCKER_IMAGE_NAME (
|
|
520
|
-
'
|
|
521
|
-
'
|
|
527
|
+
'Copy .env.example to .env and set DOCKER_IMAGE_NAME (required — e.g. myuser/myapp).',
|
|
528
|
+
'Optional in .env: DOCKER_IMAGE_TAG, DOCKER_REGISTRY_URL, DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH.',
|
|
529
|
+
'If using a private registry: also set DOCKER_REGISTRY_USERNAME and DOCKER_REGISTRY_TOKEN.',
|
|
530
|
+
'Add GitHub Secrets (Settings → Secrets and variables → Actions): DOCKER_IMAGE_NAME (required). Local .env is NOT used by GitHub Actions — doctor only checks your machine.',
|
|
531
|
+
'Also add as GitHub Secrets if set locally: DOCKER_IMAGE_TAG, DOCKER_REGISTRY_URL, DOCKER_REGISTRY_USERNAME, DOCKER_REGISTRY_TOKEN, DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH.',
|
|
522
532
|
'Run deployhub doctor to verify Docker is reachable.',
|
|
523
533
|
'git push origin main to trigger your first deployment.',
|
|
524
534
|
],
|
|
@@ -2,6 +2,17 @@ import { execa } from 'execa';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createLogger } from '../../logger/index.js';
|
|
5
|
+
import { extractArtifact } from '../../artifact/engine.js';
|
|
6
|
+
import {
|
|
7
|
+
describeInterpretedBackendGap,
|
|
8
|
+
generateDotnetRuntimeDockerfile,
|
|
9
|
+
generateFrontendRuntimeDockerfile,
|
|
10
|
+
generateGoRuntimeDockerfile,
|
|
11
|
+
generateSpringRuntimeDockerfile,
|
|
12
|
+
isFrontendStaticFramework,
|
|
13
|
+
isInterpretedBackendFramework,
|
|
14
|
+
resolveDockerImageRef,
|
|
15
|
+
} from '../../utils/docker-image.js';
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
18
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -11,8 +22,8 @@ import { createLogger } from '../../logger/index.js';
|
|
|
11
22
|
export function createDockerProvider(config, envName, env = process.env) {
|
|
12
23
|
const log = createLogger('docker');
|
|
13
24
|
|
|
14
|
-
const
|
|
15
|
-
|
|
25
|
+
const { fullImage, latestImage, legacyLatestImage, imageTag } =
|
|
26
|
+
resolveDockerImageRef(config, env);
|
|
16
27
|
const registryUrl = env.DOCKER_REGISTRY_URL || '';
|
|
17
28
|
const registryUser = env.DOCKER_REGISTRY_USERNAME || '';
|
|
18
29
|
const registryToken = env.DOCKER_REGISTRY_TOKEN || '';
|
|
@@ -27,12 +38,12 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
27
38
|
return dockerEnv;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
function hasRegistryCredentials() {
|
|
42
|
+
return Boolean(registryUser && registryToken);
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
async function dockerLogin() {
|
|
35
|
-
if (!
|
|
46
|
+
if (!hasRegistryCredentials()) return;
|
|
36
47
|
const registry = registryUrl || 'https://index.docker.io/v1/';
|
|
37
48
|
log.info('Logging in to container registry...');
|
|
38
49
|
await execa(
|
|
@@ -47,48 +58,284 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/**
|
|
50
|
-
*
|
|
61
|
+
* Push only when registry credentials are configured. Avoids a noisy failed
|
|
62
|
+
* push to Docker Hub for local-only image names.
|
|
51
63
|
*/
|
|
52
|
-
async function
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
async function maybePushImage() {
|
|
65
|
+
if (!hasRegistryCredentials()) {
|
|
66
|
+
log.info(
|
|
67
|
+
'docker push skipped (DOCKER_REGISTRY_USERNAME/TOKEN not set — local image only)'
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
log.info(`Pushing ${fullImage} to registry...`);
|
|
73
|
+
await execa('docker', ['push', fullImage], {
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
env: getDockerEnv(),
|
|
76
|
+
});
|
|
77
|
+
log.success(`Pushed ${fullImage}`);
|
|
78
|
+
}
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} ref
|
|
82
|
+
*/
|
|
83
|
+
async function imageExists(ref) {
|
|
84
|
+
try {
|
|
85
|
+
await execa('docker', ['image', 'inspect', ref], {
|
|
86
|
+
stdio: 'pipe',
|
|
87
|
+
env: getDockerEnv(),
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Prefer the image already built during the pipeline `docker` stage.
|
|
97
|
+
* Retag when the pipeline used `:latest` and deploy needs a version tag.
|
|
98
|
+
*/
|
|
99
|
+
async function ensureImageFromPipeline() {
|
|
100
|
+
if (await imageExists(fullImage)) {
|
|
101
|
+
log.info(`Reusing existing image ${fullImage}`);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
await execa('docker', ['
|
|
72
|
-
cwd: artifactDir,
|
|
105
|
+
const candidates = [...new Set([latestImage, legacyLatestImage])].filter(
|
|
106
|
+
(ref) => ref !== fullImage
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
if (!(await imageExists(candidate))) continue;
|
|
111
|
+
log.info(`Re-tagging pipeline image ${candidate} → ${fullImage}`);
|
|
112
|
+
await execa('docker', ['tag', candidate, fullImage], {
|
|
73
113
|
stdio: 'inherit',
|
|
74
|
-
env:
|
|
114
|
+
env: getDockerEnv(),
|
|
75
115
|
});
|
|
76
|
-
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Backend artifacts omit lockfiles/node_modules. Prefer a pre-built binary
|
|
124
|
+
* runtime image when present; otherwise fail with an actionable error.
|
|
125
|
+
* @param {string} buildContext
|
|
126
|
+
* @param {Record<string, unknown>} metadata
|
|
127
|
+
* @param {string} framework
|
|
128
|
+
* @param {number} port
|
|
129
|
+
*/
|
|
130
|
+
async function prepareBackendBuildContext(buildContext, metadata, framework, port) {
|
|
131
|
+
if (framework === 'spring') {
|
|
132
|
+
const targetDir = path.join(buildContext, 'target');
|
|
133
|
+
if (await fs.pathExists(targetDir)) {
|
|
134
|
+
const jars = (await fs.readdir(targetDir)).filter((f) => f.endsWith('.jar'));
|
|
135
|
+
if (jars.length > 0) {
|
|
136
|
+
const jarRel = `target/${jars[0]}`;
|
|
137
|
+
log.info(`Building runtime image from pre-built JAR (${jarRel})...`);
|
|
138
|
+
await fs.writeFile(
|
|
139
|
+
path.join(buildContext, 'Dockerfile'),
|
|
140
|
+
generateSpringRuntimeDockerfile(jarRel, port)
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (framework === 'go') {
|
|
148
|
+
const binDir = path.join(buildContext, 'bin');
|
|
149
|
+
if (await fs.pathExists(binDir)) {
|
|
150
|
+
const bins = await fs.readdir(binDir);
|
|
151
|
+
if (bins.length > 0) {
|
|
152
|
+
const binRel = `bin/${bins[0]}`;
|
|
153
|
+
log.info(`Building runtime image from pre-built Go binary (${binRel})...`);
|
|
154
|
+
await fs.writeFile(
|
|
155
|
+
path.join(buildContext, 'Dockerfile'),
|
|
156
|
+
generateGoRuntimeDockerfile(binRel, port)
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (framework === 'dotnet') {
|
|
164
|
+
const publishDir =
|
|
165
|
+
/** @type {string} */ (metadata.buildOutput) ||
|
|
166
|
+
config.buildOutput ||
|
|
167
|
+
'publish';
|
|
168
|
+
const publishPath = path.join(buildContext, publishDir);
|
|
169
|
+
if (await fs.pathExists(publishPath)) {
|
|
170
|
+
log.info(`Building runtime image from pre-built .NET output (${publishDir}/)...`);
|
|
171
|
+
await fs.writeFile(
|
|
172
|
+
path.join(buildContext, 'Dockerfile'),
|
|
173
|
+
generateDotnetRuntimeDockerfile(publishDir, port)
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dockerfilePath = path.join(buildContext, 'Dockerfile');
|
|
180
|
+
if (!(await fs.pathExists(dockerfilePath))) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'No Dockerfile found in artifact and no pipeline image to reuse. ' +
|
|
183
|
+
'Add a Dockerfile and enable pipeline.docker so the image is built from project source.'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Interpreted backends (Node/Python/PHP/Ruby): never rebuild from artifact.
|
|
188
|
+
// Artifacts ship source + manifest files, not installed dependency trees.
|
|
189
|
+
if (isInterpretedBackendFramework(framework)) {
|
|
190
|
+
const gap = describeInterpretedBackendGap(framework);
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Cannot rebuild ${gap.ecosystem} backend image "${fullImage}" from the packaged artifact.\n` +
|
|
193
|
+
`Backend artifacts include source/manifests but not ${gap.missing}, ` +
|
|
194
|
+
`so Dockerfiles that run \`${gap.installCmd}\` cannot reliably succeed from the artifact alone.\n\n` +
|
|
195
|
+
'What to do instead:\n' +
|
|
196
|
+
' 1. Enable pipeline.docker in deployhub.config.json (default when Docker deploy is selected).\n' +
|
|
197
|
+
' 2. Run a full `deployhub build` so the image is built from the project root (with full deps).\n' +
|
|
198
|
+
' 3. Deploy will reuse that local image (retag/push/run) — it will not rebuild from the artifact.\n\n' +
|
|
199
|
+
`Standalone \`deployhub deploy\` without a pre-built local image is not supported for ${gap.ecosystem} backends.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Remaining backends (unknown frameworks): try the packaged Dockerfile, but warn.
|
|
204
|
+
log.warn(
|
|
205
|
+
'Building backend image from extracted artifact. Prefer pipeline.docker so the image ' +
|
|
206
|
+
'is built once from full project source, then reused on deploy.'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Standalone deploy fallback: extract the packaged artifact and build a
|
|
212
|
+
* runtime image from pre-built output (frontend) or compiled backend artifacts.
|
|
213
|
+
* Never builds from artifactDir root — that only has zip/metadata + a source Dockerfile.
|
|
214
|
+
* @param {string} artifactDir
|
|
215
|
+
*/
|
|
216
|
+
async function buildFromArtifactContents(artifactDir) {
|
|
217
|
+
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
218
|
+
if (!(await fs.pathExists(zipPath))) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`No local image found for ${fullImage} and no artifact.zip to build from. ` +
|
|
221
|
+
'Enable pipeline.docker so the image is built from project source, or run deployhub build first.'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const buildContext = path.join(artifactDir, '_docker_build');
|
|
226
|
+
await fs.remove(buildContext);
|
|
227
|
+
await extractArtifact(artifactDir, buildContext);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const metadataPath = path.join(buildContext, 'metadata.json');
|
|
231
|
+
const metadata = (await fs.pathExists(metadataPath))
|
|
232
|
+
? await fs.readJson(metadataPath)
|
|
233
|
+
: {};
|
|
234
|
+
|
|
235
|
+
const projectType = metadata.projectType || config.projectType || 'frontend';
|
|
236
|
+
const framework =
|
|
237
|
+
metadata.framework ||
|
|
238
|
+
config.framework ||
|
|
239
|
+
config.frontend?.framework ||
|
|
240
|
+
'';
|
|
241
|
+
const buildOutput =
|
|
242
|
+
metadata.buildOutput ||
|
|
243
|
+
config.buildOutput ||
|
|
244
|
+
config.frontend?.buildOutput ||
|
|
245
|
+
'dist';
|
|
246
|
+
const port =
|
|
247
|
+
Number(metadata.port) ||
|
|
248
|
+
config.port ||
|
|
249
|
+
config.backend?.port ||
|
|
250
|
+
3000;
|
|
251
|
+
|
|
252
|
+
const composePath = path.join(buildContext, 'docker-compose.yml');
|
|
253
|
+
if (await fs.pathExists(composePath)) {
|
|
254
|
+
log.info('Building via docker compose from extracted artifact...');
|
|
255
|
+
await execa('docker', ['compose', 'up', '-d', '--build'], {
|
|
256
|
+
cwd: buildContext,
|
|
257
|
+
stdio: 'inherit',
|
|
258
|
+
env: getDockerEnv(),
|
|
259
|
+
});
|
|
260
|
+
return { ranCompose: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const isStaticFrontend =
|
|
264
|
+
projectType === 'frontend' || isFrontendStaticFramework(framework);
|
|
265
|
+
|
|
266
|
+
if (isStaticFrontend) {
|
|
267
|
+
const outputDir = path.join(buildContext, buildOutput);
|
|
268
|
+
if (!(await fs.pathExists(outputDir))) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Frontend artifact is missing build output "${buildOutput}". ` +
|
|
271
|
+
'Cannot build a runtime image from this artifact.'
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
log.info(
|
|
275
|
+
`Building runtime image from pre-built ${buildOutput}/ (no source rebuild)...`
|
|
276
|
+
);
|
|
277
|
+
await fs.writeFile(
|
|
278
|
+
path.join(buildContext, 'Dockerfile'),
|
|
279
|
+
generateFrontendRuntimeDockerfile(buildOutput)
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
await prepareBackendBuildContext(buildContext, metadata, framework, port);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await execa('docker', ['build', '-t', fullImage, '.'], {
|
|
286
|
+
cwd: buildContext,
|
|
77
287
|
stdio: 'inherit',
|
|
78
|
-
env:
|
|
79
|
-
}).catch(() => {
|
|
80
|
-
log.warn('docker push skipped (registry may be local or push not configured)');
|
|
288
|
+
env: getDockerEnv(),
|
|
81
289
|
});
|
|
82
|
-
|
|
83
|
-
|
|
290
|
+
return { ranCompose: false };
|
|
291
|
+
} finally {
|
|
292
|
+
await fs.remove(buildContext).catch(() => {});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {string} artifactDir
|
|
298
|
+
*/
|
|
299
|
+
async function deploy(artifactDir) {
|
|
300
|
+
log.info(`Deploying via Docker (image: ${fullImage})...`);
|
|
301
|
+
const dockerEnv = getDockerEnv();
|
|
302
|
+
|
|
303
|
+
await dockerLogin();
|
|
304
|
+
|
|
305
|
+
const reused = await ensureImageFromPipeline();
|
|
306
|
+
let ranCompose = false;
|
|
307
|
+
|
|
308
|
+
if (!reused) {
|
|
309
|
+
const result = await buildFromArtifactContents(artifactDir);
|
|
310
|
+
ranCompose = Boolean(result?.ranCompose);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (ranCompose) {
|
|
314
|
+
log.success('Docker deployment complete');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await maybePushImage();
|
|
319
|
+
|
|
320
|
+
// Keep :latest in sync when deploy uses a version tag
|
|
321
|
+
if (imageTag !== 'latest' && fullImage !== latestImage) {
|
|
322
|
+
await execa('docker', ['tag', fullImage, latestImage], {
|
|
323
|
+
stdio: 'pipe',
|
|
84
324
|
env: dockerEnv,
|
|
85
|
-
});
|
|
86
|
-
} else {
|
|
87
|
-
throw new Error(
|
|
88
|
-
'No Dockerfile or docker-compose.yml found in artifact. Add one to your project or enable Docker in pipeline config.'
|
|
89
|
-
);
|
|
325
|
+
}).catch(() => {});
|
|
90
326
|
}
|
|
91
327
|
|
|
328
|
+
await execa(
|
|
329
|
+
'docker',
|
|
330
|
+
['rm', '-f', config.project],
|
|
331
|
+
{ stdio: 'pipe', env: dockerEnv }
|
|
332
|
+
).catch(() => {});
|
|
333
|
+
|
|
334
|
+
await execa('docker', ['run', '-d', '--rm', '--name', config.project, fullImage], {
|
|
335
|
+
stdio: 'inherit',
|
|
336
|
+
env: dockerEnv,
|
|
337
|
+
});
|
|
338
|
+
|
|
92
339
|
log.success('Docker deployment complete');
|
|
93
340
|
}
|
|
94
341
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Docker image naming for pipeline builds and deploy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
7
|
+
* @param {Record<string, string|undefined>} [env]
|
|
8
|
+
* @returns {{
|
|
9
|
+
* imageName: string,
|
|
10
|
+
* imageTag: string,
|
|
11
|
+
* fullImage: string,
|
|
12
|
+
* latestImage: string,
|
|
13
|
+
* legacyLatestImage: string,
|
|
14
|
+
* }}
|
|
15
|
+
*/
|
|
16
|
+
export function resolveDockerImageRef(config, env = process.env) {
|
|
17
|
+
const imageName = env.DOCKER_IMAGE_NAME || config.project;
|
|
18
|
+
const imageTag = env.DOCKER_IMAGE_TAG || config.version || 'latest';
|
|
19
|
+
const registryUrl = env.DOCKER_REGISTRY_URL || '';
|
|
20
|
+
|
|
21
|
+
const repository =
|
|
22
|
+
registryUrl && !imageName.includes('/')
|
|
23
|
+
? `${registryUrl.replace(/\/$/, '')}/${imageName}`
|
|
24
|
+
: imageName;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
imageName,
|
|
28
|
+
imageTag,
|
|
29
|
+
fullImage: `${repository}:${imageTag}`,
|
|
30
|
+
latestImage: `${repository}:latest`,
|
|
31
|
+
legacyLatestImage: `${config.project}:latest`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runtime-only Dockerfile for frontend artifacts that already contain built static output.
|
|
37
|
+
* Used when deploy must build from an artifact (no source / no package-lock).
|
|
38
|
+
* @param {string} [buildOutput]
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function generateFrontendRuntimeDockerfile(buildOutput = 'dist') {
|
|
42
|
+
const output = buildOutput.replace(/^\/+|\/+$/g, '') || 'dist';
|
|
43
|
+
return `# Generated by DeployHub for artifact deploy (pre-built static output)
|
|
44
|
+
FROM nginx:alpine
|
|
45
|
+
COPY ${output}/ /usr/share/nginx/html/
|
|
46
|
+
EXPOSE 80
|
|
47
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} jarRelativePath e.g. target/app.jar
|
|
53
|
+
* @param {number} [port]
|
|
54
|
+
*/
|
|
55
|
+
export function generateSpringRuntimeDockerfile(jarRelativePath, port = 8080) {
|
|
56
|
+
return `# Generated by DeployHub for artifact deploy (pre-built JAR)
|
|
57
|
+
FROM eclipse-temurin:17-jre-alpine
|
|
58
|
+
WORKDIR /app
|
|
59
|
+
COPY ${jarRelativePath} app.jar
|
|
60
|
+
EXPOSE ${port}
|
|
61
|
+
CMD ["java", "-jar", "app.jar"]
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} binaryRelativePath e.g. bin/app
|
|
67
|
+
* @param {number} [port]
|
|
68
|
+
*/
|
|
69
|
+
export function generateGoRuntimeDockerfile(binaryRelativePath, port = 8080) {
|
|
70
|
+
return `# Generated by DeployHub for artifact deploy (pre-built Go binary)
|
|
71
|
+
FROM alpine:3.19
|
|
72
|
+
WORKDIR /app
|
|
73
|
+
RUN apk add --no-cache ca-certificates
|
|
74
|
+
COPY ${binaryRelativePath} ./app
|
|
75
|
+
RUN chmod +x ./app
|
|
76
|
+
EXPOSE ${port}
|
|
77
|
+
CMD ["./app"]
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} publishDir
|
|
83
|
+
* @param {number} [port]
|
|
84
|
+
*/
|
|
85
|
+
export function generateDotnetRuntimeDockerfile(publishDir = 'publish', port = 5000) {
|
|
86
|
+
const dir = publishDir.replace(/^\/+|\/+$/g, '') || 'publish';
|
|
87
|
+
return `# Generated by DeployHub for artifact deploy (pre-built .NET publish output)
|
|
88
|
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
|
89
|
+
WORKDIR /app
|
|
90
|
+
COPY ${dir}/ .
|
|
91
|
+
EXPOSE ${port}
|
|
92
|
+
ENV ASPNETCORE_URLS=http://+:${port}
|
|
93
|
+
CMD ["dotnet", "App.dll"]
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} framework
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
export function isFrontendStaticFramework(framework) {
|
|
102
|
+
return ['react', 'vue', 'angular', 'svelte', 'astro', 'vanilla'].includes(framework || '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Node-style backends whose generated Dockerfiles run npm ci and need a lockfile.
|
|
107
|
+
* @param {string} framework
|
|
108
|
+
*/
|
|
109
|
+
export function isNodeBackendFramework(framework) {
|
|
110
|
+
return ['express', 'nestjs', 'fastify', 'koa', 'nextjs'].includes(framework || '');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Interpreted backends that install deps at image-build time (pip/composer/bundle/npm).
|
|
115
|
+
* Their artifacts lack vendor/venv/node_modules, so standalone artifact rebuild is unsupported.
|
|
116
|
+
* @param {string} framework
|
|
117
|
+
*/
|
|
118
|
+
export function isInterpretedBackendFramework(framework) {
|
|
119
|
+
return [
|
|
120
|
+
...['express', 'nestjs', 'fastify', 'koa', 'nextjs'],
|
|
121
|
+
'fastapi',
|
|
122
|
+
'django',
|
|
123
|
+
'flask',
|
|
124
|
+
'laravel',
|
|
125
|
+
'symfony',
|
|
126
|
+
'rails',
|
|
127
|
+
].includes(framework || '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Human-readable dependency gap for interpreted backend artifact rebuilds.
|
|
132
|
+
* @param {string} framework
|
|
133
|
+
* @returns {{ ecosystem: string, missing: string, installCmd: string }}
|
|
134
|
+
*/
|
|
135
|
+
export function describeInterpretedBackendGap(framework) {
|
|
136
|
+
if (isNodeBackendFramework(framework)) {
|
|
137
|
+
return {
|
|
138
|
+
ecosystem: 'Node',
|
|
139
|
+
missing: 'package-lock.json / node_modules',
|
|
140
|
+
installCmd: 'npm ci',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (['fastapi', 'django', 'flask'].includes(framework)) {
|
|
144
|
+
return {
|
|
145
|
+
ecosystem: 'Python',
|
|
146
|
+
missing: 'a pre-installed virtualenv / site-packages (artifacts only ship requirements.txt + source)',
|
|
147
|
+
installCmd: 'pip install',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (['laravel', 'symfony'].includes(framework)) {
|
|
151
|
+
return {
|
|
152
|
+
ecosystem: 'PHP',
|
|
153
|
+
missing: 'vendor/ (Composer dependencies are not packaged into the artifact)',
|
|
154
|
+
installCmd: 'composer install',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (framework === 'rails') {
|
|
158
|
+
return {
|
|
159
|
+
ecosystem: 'Ruby',
|
|
160
|
+
missing: 'vendor/bundle / installed gems (artifacts only ship Gemfile + lock)',
|
|
161
|
+
installCmd: 'bundle install',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ecosystem: 'backend',
|
|
166
|
+
missing: 'installed dependencies (not packaged into the artifact)',
|
|
167
|
+
installCmd: 'dependency install',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default {
|
|
172
|
+
resolveDockerImageRef,
|
|
173
|
+
generateFrontendRuntimeDockerfile,
|
|
174
|
+
generateSpringRuntimeDockerfile,
|
|
175
|
+
generateGoRuntimeDockerfile,
|
|
176
|
+
generateDotnetRuntimeDockerfile,
|
|
177
|
+
isFrontendStaticFramework,
|
|
178
|
+
isNodeBackendFramework,
|
|
179
|
+
isInterpretedBackendFramework,
|
|
180
|
+
describeInterpretedBackendGap,
|
|
181
|
+
};
|
package/src/utils/dockerfile.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
const GENERATED_HEADER =
|
|
6
6
|
'# Generated by DeployHub — review exposed port and start command before deploying.\n';
|
|
7
7
|
|
|
8
|
+
const DOCKERIGNORE_HEADER =
|
|
9
|
+
'# Generated by DeployHub — safe to edit. Existing .dockerignore is never overwritten.\n';
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* @param {string} projectName
|
|
10
13
|
* @returns {string}
|
|
@@ -41,6 +44,100 @@ export function resolveDockerSettings(config) {
|
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Generate a .dockerignore matching the Dockerfile language/framework.
|
|
49
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function generateDockerignore(config) {
|
|
53
|
+
const settings = resolveDockerSettings(config);
|
|
54
|
+
const { framework, buildOutput, projectType } = settings;
|
|
55
|
+
|
|
56
|
+
/** @type {string[]} */
|
|
57
|
+
const lines = [DOCKERIGNORE_HEADER.trimEnd(), ''];
|
|
58
|
+
|
|
59
|
+
const common = [
|
|
60
|
+
'.git',
|
|
61
|
+
'.gitignore',
|
|
62
|
+
'.env',
|
|
63
|
+
'.env.*',
|
|
64
|
+
'!.env.example',
|
|
65
|
+
'artifact',
|
|
66
|
+
'.deployhub',
|
|
67
|
+
'coverage',
|
|
68
|
+
'*.md',
|
|
69
|
+
'!README.md',
|
|
70
|
+
'.github',
|
|
71
|
+
'.vscode',
|
|
72
|
+
'.idea',
|
|
73
|
+
'deployhub.config.json',
|
|
74
|
+
];
|
|
75
|
+
lines.push(...common, '');
|
|
76
|
+
|
|
77
|
+
const frontendStatic = ['react', 'vue', 'angular', 'svelte', 'astro', 'vanilla'];
|
|
78
|
+
const nodeFrameworks = [
|
|
79
|
+
...frontendStatic,
|
|
80
|
+
'nextjs',
|
|
81
|
+
'nestjs',
|
|
82
|
+
'express',
|
|
83
|
+
'fastify',
|
|
84
|
+
'koa',
|
|
85
|
+
];
|
|
86
|
+
const pythonFrameworks = ['fastapi', 'django', 'flask'];
|
|
87
|
+
const phpFrameworks = ['laravel', 'symfony'];
|
|
88
|
+
|
|
89
|
+
if (nodeFrameworks.includes(framework) || !framework) {
|
|
90
|
+
lines.push('node_modules', 'npm-debug.log*', 'yarn-error.log*', '.npm', '');
|
|
91
|
+
// Source Dockerfile rebuilds inside the image — exclude local build output from context
|
|
92
|
+
if (
|
|
93
|
+
frontendStatic.includes(framework) ||
|
|
94
|
+
framework === 'nextjs' ||
|
|
95
|
+
framework === 'nestjs' ||
|
|
96
|
+
(projectType === 'frontend' && buildOutput && buildOutput !== '.')
|
|
97
|
+
) {
|
|
98
|
+
const out = buildOutput || 'dist';
|
|
99
|
+
if (out !== '.') lines.push(out);
|
|
100
|
+
if (framework === 'nextjs') lines.push('.next');
|
|
101
|
+
if (out !== 'build') lines.push('build');
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (pythonFrameworks.includes(framework)) {
|
|
107
|
+
lines.push(
|
|
108
|
+
'__pycache__',
|
|
109
|
+
'*.py[cod]',
|
|
110
|
+
'.venv',
|
|
111
|
+
'venv',
|
|
112
|
+
'.pytest_cache',
|
|
113
|
+
'*.egg-info',
|
|
114
|
+
''
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (phpFrameworks.includes(framework)) {
|
|
119
|
+
lines.push('vendor', '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (framework === 'spring') {
|
|
123
|
+
lines.push('target', '.gradle', 'build', '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (framework === 'go') {
|
|
127
|
+
lines.push('bin', '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (framework === 'dotnet') {
|
|
131
|
+
lines.push('bin', 'obj', '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (framework === 'rails') {
|
|
135
|
+
lines.push('tmp', 'log', 'vendor/bundle', '');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
44
141
|
/**
|
|
45
142
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
46
143
|
* @returns {string}
|
|
@@ -480,6 +577,7 @@ export function getDockerfileFrameworkLabel(framework) {
|
|
|
480
577
|
|
|
481
578
|
export default {
|
|
482
579
|
generateDockerfile,
|
|
580
|
+
generateDockerignore,
|
|
483
581
|
resolveDockerSettings,
|
|
484
582
|
sanitizeDockerProjectName,
|
|
485
583
|
getDockerfileFrameworkLabel,
|
package/src/utils/scaffold.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import {
|
|
5
5
|
generateDockerfile,
|
|
6
|
+
generateDockerignore,
|
|
6
7
|
getDockerfileFrameworkLabel,
|
|
7
8
|
resolveDockerSettings,
|
|
8
9
|
} from './dockerfile.js';
|
|
@@ -48,11 +49,11 @@ export function needsKubernetesManifests(
|
|
|
48
49
|
* @param {string} cwd
|
|
49
50
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
50
51
|
* @param {{ silent?: boolean }} [options]
|
|
51
|
-
* @returns {Promise<{ generated: boolean
|
|
52
|
+
* @returns {Promise<{ generated: boolean }>}
|
|
52
53
|
*/
|
|
53
|
-
export async function
|
|
54
|
-
const
|
|
55
|
-
if (await fs.pathExists(
|
|
54
|
+
export async function ensureDockerignore(cwd, config, options = {}) {
|
|
55
|
+
const dockerignorePath = path.join(cwd, '.dockerignore');
|
|
56
|
+
if (await fs.pathExists(dockerignorePath)) {
|
|
56
57
|
return { generated: false };
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -60,20 +61,56 @@ export async function ensureDockerfile(cwd, config, options = {}) {
|
|
|
60
61
|
return { generated: false };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
const content = generateDockerfile(config);
|
|
65
|
-
await fs.writeFile(dockerfilePath, content);
|
|
64
|
+
await fs.writeFile(dockerignorePath, generateDockerignore(config));
|
|
66
65
|
|
|
67
66
|
if (!options.silent) {
|
|
68
|
-
const label = getDockerfileFrameworkLabel(settings.framework);
|
|
69
67
|
console.log(
|
|
70
68
|
chalk.yellow(
|
|
71
|
-
|
|
69
|
+
'No .dockerignore found — generated one at ./.dockerignore to keep node_modules and build caches out of the Docker build context.'
|
|
72
70
|
)
|
|
73
71
|
);
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
return { generated: true
|
|
74
|
+
return { generated: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} cwd
|
|
79
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
80
|
+
* @param {{ silent?: boolean }} [options]
|
|
81
|
+
* @returns {Promise<{ generated: boolean, framework?: string, dockerignoreGenerated?: boolean }>}
|
|
82
|
+
*/
|
|
83
|
+
export async function ensureDockerfile(cwd, config, options = {}) {
|
|
84
|
+
const dockerfilePath = path.join(cwd, 'Dockerfile');
|
|
85
|
+
let generated = false;
|
|
86
|
+
/** @type {string|undefined} */
|
|
87
|
+
let framework;
|
|
88
|
+
|
|
89
|
+
if (!(await fs.pathExists(dockerfilePath)) && needsDockerfile(config)) {
|
|
90
|
+
const settings = resolveDockerSettings(config);
|
|
91
|
+
const content = generateDockerfile(config);
|
|
92
|
+
await fs.writeFile(dockerfilePath, content);
|
|
93
|
+
generated = true;
|
|
94
|
+
framework = settings.framework;
|
|
95
|
+
|
|
96
|
+
if (!options.silent) {
|
|
97
|
+
const label = getDockerfileFrameworkLabel(settings.framework);
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.yellow(
|
|
100
|
+
`No Dockerfile found — generated a starter Dockerfile at ./Dockerfile based on your detected ${label}. Review it before deploying, especially the exposed port and start command.`
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Always pair Dockerfile generation path with .dockerignore (never overwrite existing)
|
|
107
|
+
const dockerignoreResult = await ensureDockerignore(cwd, config, options);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
generated,
|
|
111
|
+
framework,
|
|
112
|
+
dockerignoreGenerated: dockerignoreResult.generated,
|
|
113
|
+
};
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
/**
|
|
@@ -133,6 +170,7 @@ export async function ensureDeployScaffold(
|
|
|
133
170
|
const k8sResult = await ensureKubernetesManifests(cwd, config, environments, options);
|
|
134
171
|
return {
|
|
135
172
|
dockerfile: dockerResult.generated,
|
|
173
|
+
dockerignore: Boolean(dockerResult.dockerignoreGenerated),
|
|
136
174
|
kubernetes: k8sResult.generated,
|
|
137
175
|
};
|
|
138
176
|
}
|
|
@@ -183,6 +221,7 @@ export async function copyDeployAssetsToArtifactDir(stagingDir, artifactDir) {
|
|
|
183
221
|
|
|
184
222
|
export default {
|
|
185
223
|
ensureDockerfile,
|
|
224
|
+
ensureDockerignore,
|
|
186
225
|
ensureKubernetesManifests,
|
|
187
226
|
ensureDeployScaffold,
|
|
188
227
|
needsDockerfile,
|