@akash-chowdhury-24/deployhub 2.0.4 → 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/README.md +9 -4
- 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/artifact/engine.js +13 -0
- package/src/commands/init.js +31 -2
- package/src/core/stages.js +18 -1
- package/src/deployment/deployment-env.js +24 -5
- package/src/deployment/providers/docker.js +283 -36
- package/src/deployment/providers/kubernetes.js +6 -4
- package/src/utils/docker-image.js +181 -0
- package/src/utils/dockerfile.js +584 -0
- package/src/utils/github-actions.js +56 -1
- package/src/utils/kubernetes-manifests.js +168 -0
- package/src/utils/scaffold.js +231 -0
package/README.md
CHANGED
|
@@ -185,6 +185,8 @@ The wizard asks the same core questions for every setup:
|
|
|
185
185
|
- `.github/workflows/deployhub.yml` — CI pipeline
|
|
186
186
|
- `.env.example` — list of env vars you may need
|
|
187
187
|
- `nginx.conf` — auto-generated if frontend deploys to SSH
|
|
188
|
+
- `Dockerfile` — auto-generated if missing and you chose Docker or Kubernetes deploy (your existing `Dockerfile` is never overwritten)
|
|
189
|
+
- `k8s/deployment.yaml` and `k8s/service.yaml` — auto-generated if missing and you chose Kubernetes deploy (existing manifests are never overwritten)
|
|
188
190
|
|
|
189
191
|
---
|
|
190
192
|
|
|
@@ -596,7 +598,7 @@ DeployHub supports six deployment targets. Pick based on what infrastructure you
|
|
|
596
598
|
| **ec2** | AWS users with an existing EC2 instance | Running EC2 instance, security group, key pair |
|
|
597
599
|
| **azure-vm** | Azure users with an existing virtual machine | Running Azure VM, NSG allowing SSH |
|
|
598
600
|
| **gcp-vm** | GCP users with an existing Compute Engine VM | Running VM, firewall rule for SSH, metadata SSH key |
|
|
599
|
-
| **kubernetes** | Teams with an existing K8s cluster | Cluster, kubectl access
|
|
601
|
+
| **kubernetes** | Teams with an existing K8s cluster | Cluster, kubectl access; manifests auto-generated if missing |
|
|
600
602
|
|
|
601
603
|
---
|
|
602
604
|
|
|
@@ -675,10 +677,11 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
675
677
|
|
|
676
678
|
**Prerequisites:**
|
|
677
679
|
- [ ] Docker installed (`docker --version` works)
|
|
678
|
-
- [ ] `Dockerfile` or `docker-compose.yml` in project
|
|
679
680
|
- [ ] Registry account if pushing private images
|
|
681
|
+
- [ ] `docker-compose.yml` in project if you use multi-service Compose (not auto-generated)
|
|
680
682
|
|
|
681
683
|
**What DeployHub automates:**
|
|
684
|
+
- Starter `Dockerfile` at project root when none exists (framework-aware; skipped if you already have one)
|
|
682
685
|
- `.env.example` for image name, registry, remote `DOCKER_HOST`
|
|
683
686
|
- Docker daemon connectivity test during `init`
|
|
684
687
|
- `docker compose up` or build/push/run during deploy
|
|
@@ -812,11 +815,12 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
812
815
|
|
|
813
816
|
**Prerequisites:**
|
|
814
817
|
- [ ] Existing Kubernetes cluster (DeployHub does not provision clusters)
|
|
815
|
-
- [ ] `kubectl` installed and configured
|
|
816
|
-
- [ ] Kubernetes manifests (`.yaml` or `k8s/` directory) in your repo
|
|
818
|
+
- [ ] `kubectl` installed and configured on your **local machine** (for `deployhub doctor` / manual `deployhub deploy`)
|
|
817
819
|
- [ ] Cluster reachable from CI (kubeconfig secret or cloud auth)
|
|
818
820
|
|
|
819
821
|
**What DeployHub automates:**
|
|
822
|
+
- Starter `k8s/deployment.yaml` and `k8s/service.yaml` when no manifests exist (skipped if you already have a `k8s/` directory or root-level Kubernetes YAML files)
|
|
823
|
+
- GitHub Actions installs `kubectl` on the CI runner and writes kubeconfig from secrets (no local `kubectl` required for the automated push-to-main deploy path)
|
|
820
824
|
- Lists `kubectl` contexts during `init` for easy selection
|
|
821
825
|
- Auto-detects `~/.kube/config`
|
|
822
826
|
- Complete `.env.example` for kubeconfig, context, namespace
|
|
@@ -835,6 +839,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
835
839
|
| `KUBE_CONTEXT` | Context name | `my-cluster` | `kubectl config get-contexts` |
|
|
836
840
|
| `KUBE_NAMESPACE` | Target namespace | `my-app` | `kubectl get namespaces` |
|
|
837
841
|
| `DOCKER_IMAGE_NAME` | Container image | `ghcr.io/org/app` | Your registry |
|
|
842
|
+
| `DOCKER_IMAGE_TAG` | Image tag | `1.0.0` or `latest` | Project version or your choice |
|
|
838
843
|
| `KUBE_IMAGE_PULL_SECRET` | Pull secret name | `regcred` | `kubectl create secret docker-registry` |
|
|
839
844
|
|
|
840
845
|
---
|
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/artifact/engine.js
CHANGED
|
@@ -6,6 +6,11 @@ import { createLogger } from '../logger/index.js';
|
|
|
6
6
|
import { generateChecksums, formatChecksums } from '../utils/checksums.js';
|
|
7
7
|
import { getProjectVersion } from '../utils/version.js';
|
|
8
8
|
import { generateNginxConfig } from '../utils/nginx.js';
|
|
9
|
+
import {
|
|
10
|
+
ensureDeployScaffold,
|
|
11
|
+
copyKubernetesManifestsIfPresent,
|
|
12
|
+
copyDeployAssetsToArtifactDir,
|
|
13
|
+
} from '../utils/scaffold.js';
|
|
9
14
|
import { getGeneratedByMetadata, getArtifactReadmeFooter } from '../utils/author.js';
|
|
10
15
|
|
|
11
16
|
/**
|
|
@@ -143,6 +148,8 @@ async function stageFrontendArtifact(cwd, stagingDir, config) {
|
|
|
143
148
|
await copyIfExists(cwd, stagingDir, file);
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
await copyKubernetesManifestsIfPresent(cwd, stagingDir);
|
|
152
|
+
|
|
146
153
|
const hasSshDeploy = (config.deploy || []).some(
|
|
147
154
|
(envName) => config.environments[envName]?.type === 'ssh'
|
|
148
155
|
);
|
|
@@ -172,6 +179,8 @@ async function stageBackendArtifact(cwd, stagingDir, config) {
|
|
|
172
179
|
await copyIfExists(cwd, stagingDir, file);
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
await copyKubernetesManifestsIfPresent(cwd, stagingDir);
|
|
183
|
+
|
|
175
184
|
await copyDirectoryIfExists(cwd, stagingDir, 'config');
|
|
176
185
|
await copyDirectoryIfExists(cwd, stagingDir, 'migrations');
|
|
177
186
|
|
|
@@ -259,6 +268,8 @@ export async function createArtifact(config, deployedTargets = [], cwd = process
|
|
|
259
268
|
|
|
260
269
|
log.info(`Staging ${projectType} artifact...`);
|
|
261
270
|
|
|
271
|
+
await ensureDeployScaffold(cwd, config, config.environments || {}, { silent: false });
|
|
272
|
+
|
|
262
273
|
if (projectType === 'both') {
|
|
263
274
|
await stageFrontendArtifact(cwd, stagingDir, config);
|
|
264
275
|
const backendStaging = path.join(stagingDir, 'backend');
|
|
@@ -337,6 +348,8 @@ ${getArtifactReadmeFooter()}`;
|
|
|
337
348
|
const zipPath = path.join(artifactDir, 'artifact.zip');
|
|
338
349
|
await createZip(stagingDir, zipPath);
|
|
339
350
|
|
|
351
|
+
await copyDeployAssetsToArtifactDir(stagingDir, artifactDir);
|
|
352
|
+
|
|
340
353
|
const checksums = await generateChecksums(stagingDir);
|
|
341
354
|
const checksumContent = formatChecksums(checksums);
|
|
342
355
|
await fs.writeFile(path.join(artifactDir, 'checksums.txt'), checksumContent);
|
package/src/commands/init.js
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
} from '../utils/github-actions.js';
|
|
21
21
|
import { printAuthorFooter } from '../utils/author.js';
|
|
22
22
|
import { generateNginxConfig } from '../utils/nginx.js';
|
|
23
|
+
import {
|
|
24
|
+
ensureDockerfile,
|
|
25
|
+
ensureKubernetesManifests,
|
|
26
|
+
} from '../utils/scaffold.js';
|
|
23
27
|
import {
|
|
24
28
|
promptServerDeployment,
|
|
25
29
|
buildServerEnvEntry,
|
|
@@ -190,6 +194,24 @@ async function generateProjectScaffold(config, environments, cwd) {
|
|
|
190
194
|
await fs.writeFile(path.join(cwd, 'nginx.conf'), nginxConf);
|
|
191
195
|
console.log(chalk.gray(' • nginx.conf (auto-generated)'));
|
|
192
196
|
}
|
|
197
|
+
|
|
198
|
+
const dockerResult = await ensureDockerfile(cwd, config);
|
|
199
|
+
if (dockerResult.generated) {
|
|
200
|
+
console.log(chalk.gray(' • Dockerfile (auto-generated)'));
|
|
201
|
+
}
|
|
202
|
+
if (dockerResult.dockerignoreGenerated) {
|
|
203
|
+
console.log(chalk.gray(' • .dockerignore (auto-generated)'));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const k8sResult = await ensureKubernetesManifests(cwd, config, environments);
|
|
207
|
+
if (k8sResult.generated) {
|
|
208
|
+
console.log(chalk.gray(' • k8s/deployment.yaml, k8s/service.yaml (auto-generated)'));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
dockerfileGenerated: dockerResult.generated,
|
|
213
|
+
kubernetesGenerated: k8sResult.generated,
|
|
214
|
+
};
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
/**
|
|
@@ -397,7 +419,7 @@ export function registerInitCommand(program) {
|
|
|
397
419
|
}
|
|
398
420
|
|
|
399
421
|
const version = await getProjectVersion(cwd);
|
|
400
|
-
|
|
422
|
+
let hasDocker =
|
|
401
423
|
(detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
|
|
402
424
|
|
|
403
425
|
/** @type {Record<string, unknown>} */
|
|
@@ -458,7 +480,14 @@ export function registerInitCommand(program) {
|
|
|
458
480
|
config
|
|
459
481
|
);
|
|
460
482
|
|
|
461
|
-
await generateProjectScaffold(config, environments, cwd);
|
|
483
|
+
const scaffoldResult = await generateProjectScaffold(config, environments, cwd);
|
|
484
|
+
|
|
485
|
+
if (scaffoldResult?.dockerfileGenerated) {
|
|
486
|
+
hasDocker = true;
|
|
487
|
+
config.docker = true;
|
|
488
|
+
config.pipeline.docker = true;
|
|
489
|
+
await saveConfig(config, cwd);
|
|
490
|
+
}
|
|
462
491
|
|
|
463
492
|
const envExampleDest = path.join(cwd, '.env.example');
|
|
464
493
|
const envExampleContent = generateEnvExampleContent(
|
package/src/core/stages.js
CHANGED
|
@@ -6,6 +6,7 @@ import { deployToAll } from '../deployment/index.js';
|
|
|
6
6
|
import { sendNotifications } from '../notifications/index.js';
|
|
7
7
|
import axios from 'axios';
|
|
8
8
|
import { getProjectVersion } from '../utils/version.js';
|
|
9
|
+
import { ensureDeployScaffold } from '../utils/scaffold.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
@@ -40,6 +41,20 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
40
41
|
ctx.config.port = detected.port;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
// Resolve version before docker so pipeline build and deploy share the same tag
|
|
45
|
+
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
46
|
+
const scaffold = await ensureDeployScaffold(
|
|
47
|
+
ctx.cwd,
|
|
48
|
+
ctx.config,
|
|
49
|
+
ctx.config.environments || {},
|
|
50
|
+
{ silent: false }
|
|
51
|
+
);
|
|
52
|
+
if (scaffold.dockerfile) {
|
|
53
|
+
ctx.config.docker = true;
|
|
54
|
+
if (ctx.config.pipeline) {
|
|
55
|
+
ctx.config.pipeline.docker = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
43
58
|
ctx.state.framework = ctx.config.framework;
|
|
44
59
|
ctx.state.projectType = ctx.config.projectType || 'frontend';
|
|
45
60
|
},
|
|
@@ -105,7 +120,9 @@ export function buildPipelineStages(config, cwd, state) {
|
|
|
105
120
|
name: 'artifact',
|
|
106
121
|
enabled: (ctx) => ctx.config.artifact !== false,
|
|
107
122
|
async run(ctx) {
|
|
108
|
-
ctx.config.version
|
|
123
|
+
if (!ctx.config.version) {
|
|
124
|
+
ctx.config.version = await getProjectVersion(ctx.cwd);
|
|
125
|
+
}
|
|
109
126
|
const result = await createArtifact(
|
|
110
127
|
ctx.config,
|
|
111
128
|
/** @type {string[]} */ (ctx.state.deployedTargets || []),
|
|
@@ -323,6 +323,15 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
323
323
|
example: 'ghcr.io/myorg/myapp',
|
|
324
324
|
when: 'optional',
|
|
325
325
|
},
|
|
326
|
+
{
|
|
327
|
+
key: 'DOCKER_IMAGE_TAG',
|
|
328
|
+
optionalReason: 'defaults to your project version, then "latest" if unset',
|
|
329
|
+
comment: [
|
|
330
|
+
'Image tag written into generated manifests and used at deploy time.',
|
|
331
|
+
],
|
|
332
|
+
example: 'latest',
|
|
333
|
+
when: 'optional',
|
|
334
|
+
},
|
|
326
335
|
{
|
|
327
336
|
key: 'KUBE_IMAGE_PULL_SECRET',
|
|
328
337
|
optionalReason: 'only required when pulling from a private container registry',
|
|
@@ -379,7 +388,14 @@ export function getDeploymentSecretKeys(deployType, config = null) {
|
|
|
379
388
|
|
|
380
389
|
for (const d of defs) {
|
|
381
390
|
if (d.when === 'backend' && !isBackend) continue;
|
|
382
|
-
|
|
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
|
+
}
|
|
383
399
|
if (d.key === 'SSH_KEY_PATH') {
|
|
384
400
|
keys.push('SSH_KEY');
|
|
385
401
|
continue;
|
|
@@ -503,13 +519,16 @@ export const DEPLOYMENT_GUIDE = {
|
|
|
503
519
|
],
|
|
504
520
|
automates: [
|
|
505
521
|
'Generates config, workflow, and .env.example for registry and image settings.',
|
|
522
|
+
'Generates a starter Dockerfile and .dockerignore when missing.',
|
|
506
523
|
'Tests Docker daemon connectivity during init.',
|
|
507
|
-
'Builds
|
|
524
|
+
'Builds the image once during the pipeline docker stage, then reuses it on deploy.',
|
|
508
525
|
],
|
|
509
526
|
after: [
|
|
510
|
-
'Copy .env.example to .env and set DOCKER_IMAGE_NAME (
|
|
511
|
-
'
|
|
512
|
-
'
|
|
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.',
|
|
513
532
|
'Run deployhub doctor to verify Docker is reachable.',
|
|
514
533
|
'git push origin main to trigger your first deployment.',
|
|
515
534
|
],
|