@akash-chowdhury-24/deployhub 2.0.14 → 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 +2 -0
- package/src/commands/sync-k8s-ports.js +71 -0
- package/src/utils/dockerfile-expose.js +117 -0
- package/src/utils/kubernetes-manifests.js +102 -9
- package/src/utils/scaffold.js +6 -2
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ This interactive wizard will:
|
|
|
86
86
|
- Set up storage providers (AWS, Google Drive, Azure, GCP, Dropbox, Local)
|
|
87
87
|
- Optionally configure deployment targets (SSH, Docker, EC2, Azure VM, GCP VM, Kubernetes)
|
|
88
88
|
- Generate `deployhub.config.json`
|
|
89
|
-
- Generate `.github/workflows/deployhub.yml`
|
|
89
|
+
- Generate `.github/workflows/deployhub.yml` and `.github/workflows/deployhub-rollback.yml`
|
|
90
90
|
- Generate `.env.example`
|
|
91
91
|
|
|
92
92
|
### 2. Configure credentials
|
|
@@ -182,7 +182,8 @@ The wizard asks the same core questions for every setup:
|
|
|
182
182
|
**Generated files:**
|
|
183
183
|
|
|
184
184
|
- `deployhub.config.json` — project settings (no secrets)
|
|
185
|
-
- `.github/workflows/deployhub.yml` — CI pipeline
|
|
185
|
+
- `.github/workflows/deployhub.yml` — CI deploy pipeline (push to main)
|
|
186
|
+
- `.github/workflows/deployhub-rollback.yml` — manual CI rollback (`workflow_dispatch`)
|
|
186
187
|
- `.env.example` — list of env vars you may need
|
|
187
188
|
- `nginx.conf` — auto-generated if frontend deploys to SSH
|
|
188
189
|
- `Dockerfile` — auto-generated if missing and you chose Docker or Kubernetes deploy (your existing `Dockerfile` is never overwritten)
|
|
@@ -231,9 +232,25 @@ deployhub artifact restore <buildId> # download a past build
|
|
|
231
232
|
deployhub deploy # deploy latest artifact without rebuilding
|
|
232
233
|
deployhub rollback # previous build from history
|
|
233
234
|
deployhub rollback <buildId> # exact build (required if semver is ambiguous)
|
|
235
|
+
deployhub sync-workflows # regenerate deploy + rollback GitHub Actions YAML
|
|
236
|
+
deployhub sync-k8s-ports # fix containerPort/targetPort in existing k8s manifests
|
|
234
237
|
deployhub logs # last deployment logs
|
|
235
238
|
```
|
|
236
239
|
|
|
240
|
+
### Rollback behavior
|
|
241
|
+
|
|
242
|
+
`deployhub rollback` restores a previous artifact from storage `history.json` and redeploys it:
|
|
243
|
+
|
|
244
|
+
| Argument | Behavior |
|
|
245
|
+
|----------|----------|
|
|
246
|
+
| *(none)* | Rolls back to the **previous** build (second entry in newest-first history) |
|
|
247
|
+
| Exact `buildId` | Rolls back to that specific build |
|
|
248
|
+
| Semver / version string matching **multiple** builds | Does **not** guess — prints the matching `buildId`s and exits; re-run with an exact one |
|
|
249
|
+
|
|
250
|
+
`buildId` looks like `{semver}-{stamp}` where the stamp is a short git SHA when available, otherwise a CI run id, otherwise a high-resolution timestamp. When `DOCKER_IMAGE_TAG` is left unset, Docker and Kubernetes use that same `buildId` as the image tag — so you can correlate an artifact in storage with the image that was pushed.
|
|
251
|
+
|
|
252
|
+
For CI-triggered rollback, see [CI rollback](#ci-rollback-deployhub-rollbackyml).
|
|
253
|
+
|
|
237
254
|
---
|
|
238
255
|
|
|
239
256
|
## Walkthrough: Storage only
|
|
@@ -555,17 +572,27 @@ All JS frontends share the same install/build flow: `npm ci` → `npm run build`
|
|
|
555
572
|
After `init`, commit these files:
|
|
556
573
|
|
|
557
574
|
```bash
|
|
558
|
-
git add deployhub.config.json .github/workflows/deployhub.yml .env.example
|
|
575
|
+
git add deployhub.config.json .github/workflows/deployhub.yml .github/workflows/deployhub-rollback.yml .env.example
|
|
559
576
|
git commit -m "Add DeployHub CI"
|
|
560
577
|
```
|
|
561
578
|
|
|
562
579
|
1. Open **Settings → Secrets and variables → Actions** in your GitHub repo.
|
|
563
580
|
2. Add every secret listed at the end of `deployhub init` (storage + deployment).
|
|
564
|
-
3. Push to `main` or `master` — the workflow triggers on push.
|
|
581
|
+
3. Push to `main` or `master` — the deploy workflow triggers on push.
|
|
582
|
+
|
|
583
|
+
The deploy workflow (`deployhub.yml`) installs the correct language runtime (Node, Python, Java, Go, .NET, Ruby) based on your `deployhub.config.json`, installs DeployHub, runs `deployhub build`, and uses your secrets.
|
|
584
|
+
|
|
585
|
+
To run a deploy manually: **Actions → DeployHub → Run workflow**.
|
|
586
|
+
|
|
587
|
+
### CI rollback (`deployhub-rollback.yml`)
|
|
565
588
|
|
|
566
|
-
|
|
589
|
+
`init` also generates a separate **DeployHub Rollback** workflow (`deployhub-rollback.yml`). It is triggered only via GitHub Actions' **Run workflow** button (not on push):
|
|
567
590
|
|
|
568
|
-
|
|
591
|
+
1. Open **Actions → DeployHub Rollback → Run workflow**.
|
|
592
|
+
2. Optionally enter an exact `buildId` (leave blank to roll back to the previous build).
|
|
593
|
+
3. Run the workflow.
|
|
594
|
+
|
|
595
|
+
Already-initialized projects that only have `deployhub.yml` will not get the rollback workflow from a re-run of unrelated commands — run **`deployhub sync-workflows`** once to regenerate both workflow files from your current `deployhub.config.json`, then commit and push.
|
|
569
596
|
|
|
570
597
|
---
|
|
571
598
|
|
|
@@ -648,6 +675,8 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
648
675
|
|
|
649
676
|
### SSH
|
|
650
677
|
|
|
678
|
+
**Verification:** Real-world verified DEPLOY and ROLLBACK.
|
|
679
|
+
|
|
651
680
|
**Prerequisites (before `deployhub init`):**
|
|
652
681
|
- [ ] Complete **[one-time server setup](#one-time-server-setup-before-your-first-deploy)** (deploy path ownership + Nginx/sudo for frontends)
|
|
653
682
|
- [ ] A Linux server with SSH enabled
|
|
@@ -687,7 +716,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
687
716
|
|
|
688
717
|
### Docker
|
|
689
718
|
|
|
690
|
-
**Verification:** Real-world verified (local and CI
|
|
719
|
+
**Verification:** Real-world verified DEPLOY and ROLLBACK (local and CI).
|
|
691
720
|
|
|
692
721
|
**Prerequisites:**
|
|
693
722
|
- [ ] Docker installed (`docker --version` works)
|
|
@@ -722,6 +751,8 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
722
751
|
|
|
723
752
|
### AWS EC2
|
|
724
753
|
|
|
754
|
+
**Verification:** Real-world verified DEPLOY and ROLLBACK.
|
|
755
|
+
|
|
725
756
|
**Prerequisites:**
|
|
726
757
|
- [ ] Complete **[one-time server setup](#one-time-server-setup-before-your-first-deploy)** (`ec2-user` on Amazon Linux)
|
|
727
758
|
- [ ] EC2 instance launched in AWS Console (DeployHub does not create it)
|
|
@@ -758,6 +789,8 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
758
789
|
|
|
759
790
|
### Azure VM
|
|
760
791
|
|
|
792
|
+
**Verification:** Real-world verified DEPLOY. Rollback logic confirmed via shared-SSH-path audit; not yet live-tested independently.
|
|
793
|
+
|
|
761
794
|
**Prerequisites:**
|
|
762
795
|
- [ ] Complete **[one-time server setup](#one-time-server-setup-before-your-first-deploy)** (`azureuser` or your VM login user)
|
|
763
796
|
- [ ] Azure VM created in Portal (DeployHub does not provision it)
|
|
@@ -792,6 +825,8 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
792
825
|
|
|
793
826
|
### GCP VM
|
|
794
827
|
|
|
828
|
+
**Verification:** Real-world verified DEPLOY. Rollback logic confirmed via shared-SSH-path audit; not yet live-tested independently.
|
|
829
|
+
|
|
795
830
|
**Prerequisites:**
|
|
796
831
|
- [ ] Complete **[one-time server setup](#one-time-server-setup-before-your-first-deploy)** (your GCP SSH username)
|
|
797
832
|
- [ ] Compute Engine VM created (DeployHub does not create it)
|
|
@@ -832,7 +867,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
832
867
|
|
|
833
868
|
### Kubernetes
|
|
834
869
|
|
|
835
|
-
**Verification:** Real-world verified (including k3s and CI
|
|
870
|
+
**Verification:** Real-world verified DEPLOY and ROLLBACK (including k3s and CI).
|
|
836
871
|
|
|
837
872
|
**Prerequisites:**
|
|
838
873
|
- [ ] Existing Kubernetes cluster (DeployHub does not provision clusters)
|
|
@@ -868,7 +903,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
868
903
|
6. Add GitHub Secrets for CI (see table — **`KUBECONFIG` must be the file contents, not a path**)
|
|
869
904
|
7. Run `deployhub doctor`, then `git push origin main`
|
|
870
905
|
|
|
871
|
-
> **
|
|
906
|
+
> **Ports in generated Kubernetes manifests:** `containerPort` and `targetPort` are derived from your project's Dockerfile `EXPOSE` line (frontend / nginx images correctly get **80**; backends get their real exposed port). If no Dockerfile or usable `EXPOSE` exists yet, DeployHub falls back to a per-project-type default that matches its Dockerfile templates. Fresh `init` does **not** require a manual port edit. The Service's external `port:` stays **80** for all project types by design (Ingress-friendly); only the internal `targetPort` / `containerPort` track the container. Already-initialized projects with stale `3000` values can run `deployhub sync-k8s-ports` to patch just those fields.
|
|
872
907
|
|
|
873
908
|
| Variable | Description | Example | Where to get it |
|
|
874
909
|
|----------|-------------|---------|-----------------|
|
|
@@ -961,8 +996,10 @@ Run `deployhub doctor` after any config change.
|
|
|
961
996
|
| `deployhub storage list` | List storage providers and connection status |
|
|
962
997
|
| `deployhub deploy` | Deploy latest artifact |
|
|
963
998
|
| `deployhub rollback [buildId\|semver]` | Previous build, or exact buildId (ambiguous semver lists matches and exits) |
|
|
999
|
+
| `deployhub sync-workflows` | Regenerate `.github/workflows/deployhub.yml` and `deployhub-rollback.yml` from config |
|
|
1000
|
+
| `deployhub sync-k8s-ports` | Update only `containerPort` / `targetPort` in `k8s/deployment.yaml` and `k8s/service.yaml` from Dockerfile `EXPOSE` (or config/fallback). Does **not** change replicas, resources, env, or probes. Heavily customized / multi-container manifests may need a manual review |
|
|
964
1001
|
| `deployhub logs` | Show logs from last deployment |
|
|
965
|
-
| `deployhub doctor` | Pre-flight checks |
|
|
1002
|
+
| `deployhub doctor` | Pre-flight checks (including an informational warning if the rollback workflow file is missing) |
|
|
966
1003
|
| `deployhub verify` | Health check on configured endpoint |
|
|
967
1004
|
| `deployhub clean` | Remove old local artifacts |
|
|
968
1005
|
| `deployhub update` | Check for CLI updates |
|
|
@@ -1009,6 +1046,8 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
|
|
|
1009
1046
|
| `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_USERNAME`, `DOCKER_REGISTRY_TOKEN`, `DOCKER_REGISTRY_URL`, `DOCKER_HOST` | Docker deployment (`DOCKER_IMAGE_TAG` optional) |
|
|
1010
1047
|
| `KUBECONFIG`, `KUBE_CONTEXT`, `KUBE_NAMESPACE`, `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_USERNAME`, `DOCKER_REGISTRY_TOKEN`, `DOCKER_REGISTRY_URL`, `DOCKER_IMAGE_TAG`, `KUBE_IMAGE_PULL_SECRET` | Kubernetes — **`KUBECONFIG` in GitHub Secrets must be the kubeconfig file contents (or base64), not a filesystem path**. `DOCKER_IMAGE_TAG`, `KUBE_NAMESPACE`, `DOCKER_REGISTRY_URL`, and `KUBE_IMAGE_PULL_SECRET` are optional |
|
|
1011
1048
|
|
|
1049
|
+
**Kubernetes rollback vs deploy — registry credentials:** Kubernetes **rollback** specifically **requires** `DOCKER_REGISTRY_USERNAME` and `DOCKER_REGISTRY_TOKEN`. Rollback rebuilds and must push a fresh image tagged with the restored `buildId`; without registry credentials it fails loudly and early (a rollback that cannot push can never succeed against a real cluster). This is stricter than a normal Kubernetes **deploy**, which may still allow local-only / no-push flows in some setups. If deploy worked without those secrets but rollback fails asking for them, that asymmetry is intentional.
|
|
1050
|
+
|
|
1012
1051
|
See [Deployment method guides](#deployment-method-guides) for full per-method variable tables with examples.
|
|
1013
1052
|
|
|
1014
1053
|
## `deployhub doctor` Output
|
|
@@ -1025,11 +1064,14 @@ The doctor command runs independent checks and always completes without crashing
|
|
|
1025
1064
|
Checking Health endpoint... ✓ URL reachable (HTTP 200)
|
|
1026
1065
|
Checking Secrets... ✓ All required env vars present
|
|
1027
1066
|
Checking GitHub Actions... ✓ Workflow file exists at .github/workflows/deployhub.yml
|
|
1067
|
+
Checking Rollback workflow... ✓ Missing .github/workflows/deployhub-rollback.yml — run deployhub sync-workflows to add CI rollback (workflow_dispatch)
|
|
1028
1068
|
Checking Storage write... ✓ Test upload succeeded
|
|
1029
1069
|
|
|
1030
|
-
✓ Ready to deploy (
|
|
1070
|
+
✓ Ready to deploy (11/11 checks passed)
|
|
1031
1071
|
```
|
|
1032
1072
|
|
|
1073
|
+
The **Rollback workflow** check is informational and non-blocking (`✓` even when the file is missing). It suggests `deployhub sync-workflows` so already-initialized projects can pick up `deployhub-rollback.yml` without a full re-init. When the file exists, the message confirms the path instead.
|
|
1074
|
+
|
|
1033
1075
|
If checks fail:
|
|
1034
1076
|
|
|
1035
1077
|
```
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerVerifyCommand } from '../commands/verify.js';
|
|
|
14
14
|
import { registerCleanCommand } from '../commands/clean.js';
|
|
15
15
|
import { registerUpdateCommand } from '../commands/update.js';
|
|
16
16
|
import { registerSyncWorkflowsCommand } from '../commands/sync-workflows.js';
|
|
17
|
+
import { registerSyncK8sPortsCommand } from '../commands/sync-k8s-ports.js';
|
|
17
18
|
import { formatVersionOutput, printBanner, shouldShowBanner } from '../utils/author.js';
|
|
18
19
|
|
|
19
20
|
loadEnv();
|
|
@@ -43,5 +44,6 @@ registerVerifyCommand(program);
|
|
|
43
44
|
registerCleanCommand(program);
|
|
44
45
|
registerUpdateCommand(program);
|
|
45
46
|
registerSyncWorkflowsCommand(program);
|
|
47
|
+
registerSyncK8sPortsCommand(program);
|
|
46
48
|
|
|
47
49
|
program.parse();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
+
import { resolveContainerPort } from '../utils/dockerfile-expose.js';
|
|
4
|
+
import {
|
|
5
|
+
getDefaultKubernetesManifestPaths,
|
|
6
|
+
syncKubernetesManifestPorts,
|
|
7
|
+
} from '../utils/kubernetes-manifests.js';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Surgically fix containerPort/targetPort in existing k8s manifests from Dockerfile EXPOSE.
|
|
12
|
+
* @param {import('commander').Command} program
|
|
13
|
+
*/
|
|
14
|
+
export function registerSyncK8sPortsCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('sync-k8s-ports')
|
|
17
|
+
.description(
|
|
18
|
+
'Update only containerPort/targetPort in k8s/deployment.yaml and k8s/service.yaml ' +
|
|
19
|
+
'from the Dockerfile EXPOSE port (or config/fallback). Does not change replicas, ' +
|
|
20
|
+
'resources, env, probes, or Service port. Heavily customized / multi-container ' +
|
|
21
|
+
'manifests may need a manual review.'
|
|
22
|
+
)
|
|
23
|
+
.action(async () => {
|
|
24
|
+
loadEnv();
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const config = await loadConfig(cwd);
|
|
27
|
+
|
|
28
|
+
const { deploymentPath, servicePath } = getDefaultKubernetesManifestPaths(cwd);
|
|
29
|
+
const hasDeployment = await fs.pathExists(deploymentPath);
|
|
30
|
+
const hasService = await fs.pathExists(servicePath);
|
|
31
|
+
|
|
32
|
+
if (!hasDeployment && !hasService) {
|
|
33
|
+
console.log(
|
|
34
|
+
chalk.yellow(
|
|
35
|
+
'No k8s/deployment.yaml or k8s/service.yaml found — nothing to patch. ' +
|
|
36
|
+
'Generate starter manifests via deployhub init, or add manifests under ./k8s/.'
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { port, source } = await resolveContainerPort(cwd, config);
|
|
43
|
+
const result = await syncKubernetesManifestPorts(cwd, port);
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
chalk.green(
|
|
47
|
+
`✓ Resolved container port ${port} (source: ${source})`
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (result.patched.length === 0) {
|
|
52
|
+
console.log(chalk.gray(' Port fields already match — no files changed.'));
|
|
53
|
+
} else {
|
|
54
|
+
for (const file of result.patched) {
|
|
55
|
+
console.log(chalk.gray(` Updated ${file}`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.yellow(
|
|
62
|
+
'Note: Only containerPort and targetPort are updated. Replicas, resources, env, ' +
|
|
63
|
+
'probes, and Service port: are left unchanged. If your manifests are heavily ' +
|
|
64
|
+
'customized (multiple containers, non-standard field layout), review the diff ' +
|
|
65
|
+
'manually before deploying.'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { registerSyncK8sPortsCommand };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/** Static SPA frameworks that use nginx:alpine + EXPOSE 80 in generated Dockerfiles. */
|
|
5
|
+
const STATIC_FRONTEND_FRAMEWORKS = new Set([
|
|
6
|
+
'react',
|
|
7
|
+
'vue',
|
|
8
|
+
'angular',
|
|
9
|
+
'svelte',
|
|
10
|
+
'astro',
|
|
11
|
+
'vanilla',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse the listening port from Dockerfile EXPOSE instructions.
|
|
16
|
+
* Uses the last EXPOSE line (final stage in multi-stage builds).
|
|
17
|
+
* Supports `EXPOSE 80`, `EXPOSE 80/tcp`, and multi-port lines (first numeric wins on that line).
|
|
18
|
+
*
|
|
19
|
+
* @param {string} content
|
|
20
|
+
* @returns {number|null}
|
|
21
|
+
*/
|
|
22
|
+
export function parseDockerfileExposePort(content) {
|
|
23
|
+
if (!content || typeof content !== 'string') return null;
|
|
24
|
+
|
|
25
|
+
/** @type {number|null} */
|
|
26
|
+
let lastPort = null;
|
|
27
|
+
|
|
28
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
29
|
+
const line = rawLine.trim();
|
|
30
|
+
if (!line || line.startsWith('#')) continue;
|
|
31
|
+
|
|
32
|
+
const match = line.match(/^EXPOSE\s+(.+)$/i);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
|
|
35
|
+
const tokens = match[1].trim().split(/\s+/);
|
|
36
|
+
for (const token of tokens) {
|
|
37
|
+
const portToken = token.split('/')[0];
|
|
38
|
+
if (!/^\d+$/.test(portToken)) continue;
|
|
39
|
+
const port = Number(portToken);
|
|
40
|
+
if (port >= 1 && port <= 65535) {
|
|
41
|
+
lastPort = port;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lastPort;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-type fallback matching generated Dockerfile templates when EXPOSE is unavailable.
|
|
52
|
+
*
|
|
53
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
export function resolveFallbackContainerPort(config) {
|
|
57
|
+
const projectType = config.projectType || 'frontend';
|
|
58
|
+
const framework =
|
|
59
|
+
(projectType === 'both'
|
|
60
|
+
? config.backend?.framework || config.framework
|
|
61
|
+
: config.framework) ||
|
|
62
|
+
(projectType === 'frontend' ? 'react' : 'express');
|
|
63
|
+
|
|
64
|
+
if (projectType === 'frontend' && STATIC_FRONTEND_FRAMEWORKS.has(framework)) {
|
|
65
|
+
return 80;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (['laravel', 'symfony', 'php'].includes(framework)) return 80;
|
|
69
|
+
if (['fastapi', 'django', 'flask', 'python'].includes(framework)) return 8000;
|
|
70
|
+
if (['spring', 'java'].includes(framework)) return 8080;
|
|
71
|
+
if (framework === 'go') return 8080;
|
|
72
|
+
if (framework === 'dotnet') return 5000;
|
|
73
|
+
if (framework === 'rails') return 3000;
|
|
74
|
+
|
|
75
|
+
// nextjs, nestjs, express, and other Node backends
|
|
76
|
+
return 3000;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Precedence: Dockerfile EXPOSE → config.port / backend.port → per-type fallback.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} cwd
|
|
83
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
84
|
+
* @returns {Promise<{ port: number, source: 'expose'|'config'|'fallback' }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveContainerPort(cwd, config) {
|
|
87
|
+
const dockerfilePath = path.join(cwd, 'Dockerfile');
|
|
88
|
+
if (await fs.pathExists(dockerfilePath)) {
|
|
89
|
+
try {
|
|
90
|
+
const content = await fs.readFile(dockerfilePath, 'utf8');
|
|
91
|
+
const exposed = parseDockerfileExposePort(content);
|
|
92
|
+
if (exposed != null) {
|
|
93
|
+
return { port: exposed, source: 'expose' };
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// treat as unparseable / unreadable → continue
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (config.projectType === 'both' && config.backend?.port) {
|
|
101
|
+
return { port: Number(config.backend.port), source: 'config' };
|
|
102
|
+
}
|
|
103
|
+
if (config.port) {
|
|
104
|
+
return { port: Number(config.port), source: 'config' };
|
|
105
|
+
}
|
|
106
|
+
if (config.backend?.port) {
|
|
107
|
+
return { port: Number(config.backend.port), source: 'config' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { port: resolveFallbackContainerPort(config), source: 'fallback' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default {
|
|
114
|
+
parseDockerfileExposePort,
|
|
115
|
+
resolveFallbackContainerPort,
|
|
116
|
+
resolveContainerPort,
|
|
117
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { resolveContainerPort } from './dockerfile-expose.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @param {string} name
|
|
@@ -65,6 +66,11 @@ export function generateKubernetesManifests({
|
|
|
65
66
|
? ` imagePullSecrets:\n - name: ${imagePullSecret}\n`
|
|
66
67
|
: '';
|
|
67
68
|
|
|
69
|
+
// Service port stays 80 (Ingress-friendly cluster-facing port).
|
|
70
|
+
// containerPort / targetPort must match the container's listening port (EXPOSE).
|
|
71
|
+
const servicePort = 80;
|
|
72
|
+
const targetPort = port;
|
|
73
|
+
|
|
68
74
|
const deploymentYaml = `apiVersion: apps/v1
|
|
69
75
|
kind: Deployment
|
|
70
76
|
metadata:
|
|
@@ -96,9 +102,6 @@ ${pullSecretBlock} containers:
|
|
|
96
102
|
cpu: "500m"
|
|
97
103
|
`;
|
|
98
104
|
|
|
99
|
-
const servicePort = port === 80 ? 80 : 80;
|
|
100
|
-
const targetPort = port;
|
|
101
|
-
|
|
102
105
|
const serviceYaml = `apiVersion: v1
|
|
103
106
|
kind: Service
|
|
104
107
|
metadata:
|
|
@@ -116,12 +119,29 @@ spec:
|
|
|
116
119
|
return { deploymentYaml, serviceYaml };
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Surgically update only containerPort / targetPort in manifest YAML.
|
|
124
|
+
* Does not change Service `port:`, replicas, resources, env, probes, etc.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} yaml
|
|
127
|
+
* @param {number} port
|
|
128
|
+
* @returns {{ content: string, changed: boolean }}
|
|
129
|
+
*/
|
|
130
|
+
export function patchKubernetesManifestPorts(yaml, port) {
|
|
131
|
+
const next = String(yaml)
|
|
132
|
+
.replace(/^([ \t]*-?[ \t]*containerPort:[ \t]*)\d+[ \t]*$/gm, `$1${port}`)
|
|
133
|
+
.replace(/^([ \t]*targetPort:[ \t]*)\d+[ \t]*$/gm, `$1${port}`);
|
|
134
|
+
|
|
135
|
+
return { content: next, changed: next !== yaml };
|
|
136
|
+
}
|
|
137
|
+
|
|
119
138
|
/**
|
|
120
139
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
121
140
|
* @param {Record<string, Record<string, unknown>>} [environments]
|
|
141
|
+
* @param {{ port?: number }} [options]
|
|
122
142
|
* @returns {{ appName: string, imageName: string, imageTag: string, port: number, namespace: string, imagePullSecret: string }}
|
|
123
143
|
*/
|
|
124
|
-
export function resolveKubernetesManifestOptions(config, environments = {}) {
|
|
144
|
+
export function resolveKubernetesManifestOptions(config, environments = {}, options = {}) {
|
|
125
145
|
const envList = Object.values(environments);
|
|
126
146
|
const k8sEnv = envList.find((env) => env.type === 'kubernetes') || {};
|
|
127
147
|
|
|
@@ -142,13 +162,18 @@ export function resolveKubernetesManifestOptions(config, environments = {}) {
|
|
|
142
162
|
process.env.KUBE_IMAGE_PULL_SECRET ||
|
|
143
163
|
'';
|
|
144
164
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
165
|
+
/** @type {number} */
|
|
166
|
+
let port;
|
|
167
|
+
if (typeof options.port === 'number' && Number.isFinite(options.port)) {
|
|
168
|
+
port = options.port;
|
|
169
|
+
} else if (config.projectType === 'both' && config.backend?.port) {
|
|
170
|
+
port = Number(config.backend.port);
|
|
148
171
|
} else if (config.port) {
|
|
149
|
-
port = config.port;
|
|
172
|
+
port = Number(config.port);
|
|
150
173
|
} else if (config.backend?.port) {
|
|
151
|
-
port = config.backend.port;
|
|
174
|
+
port = Number(config.backend.port);
|
|
175
|
+
} else {
|
|
176
|
+
port = 3000;
|
|
152
177
|
}
|
|
153
178
|
|
|
154
179
|
return {
|
|
@@ -161,9 +186,77 @@ export function resolveKubernetesManifestOptions(config, environments = {}) {
|
|
|
161
186
|
};
|
|
162
187
|
}
|
|
163
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Resolve manifest options with Dockerfile EXPOSE → config → fallback port.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} cwd
|
|
193
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
194
|
+
* @param {Record<string, Record<string, unknown>>} [environments]
|
|
195
|
+
*/
|
|
196
|
+
export async function resolveKubernetesManifestOptionsFromCwd(
|
|
197
|
+
cwd,
|
|
198
|
+
config,
|
|
199
|
+
environments = {}
|
|
200
|
+
) {
|
|
201
|
+
const { port, source } = await resolveContainerPort(cwd, config);
|
|
202
|
+
return {
|
|
203
|
+
...resolveKubernetesManifestOptions(config, environments, { port }),
|
|
204
|
+
portSource: source,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Paths DeployHub normally writes for Kubernetes starter manifests.
|
|
210
|
+
* @param {string} cwd
|
|
211
|
+
* @returns {{ deploymentPath: string, servicePath: string }}
|
|
212
|
+
*/
|
|
213
|
+
export function getDefaultKubernetesManifestPaths(cwd) {
|
|
214
|
+
const k8sDir = path.join(cwd, 'k8s');
|
|
215
|
+
return {
|
|
216
|
+
deploymentPath: path.join(k8sDir, 'deployment.yaml'),
|
|
217
|
+
servicePath: path.join(k8sDir, 'service.yaml'),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Patch containerPort/targetPort in existing k8s/deployment.yaml + service.yaml only.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} cwd
|
|
225
|
+
* @param {number} port
|
|
226
|
+
* @returns {Promise<{ patched: string[], skipped: string[], port: number }>}
|
|
227
|
+
*/
|
|
228
|
+
export async function syncKubernetesManifestPorts(cwd, port) {
|
|
229
|
+
const { deploymentPath, servicePath } = getDefaultKubernetesManifestPaths(cwd);
|
|
230
|
+
/** @type {string[]} */
|
|
231
|
+
const patched = [];
|
|
232
|
+
/** @type {string[]} */
|
|
233
|
+
const skipped = [];
|
|
234
|
+
|
|
235
|
+
for (const filePath of [deploymentPath, servicePath]) {
|
|
236
|
+
if (!(await fs.pathExists(filePath))) {
|
|
237
|
+
skipped.push(path.relative(cwd, filePath));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const original = await fs.readFile(filePath, 'utf8');
|
|
241
|
+
const { content, changed } = patchKubernetesManifestPorts(original, port);
|
|
242
|
+
if (changed) {
|
|
243
|
+
await fs.writeFile(filePath, content);
|
|
244
|
+
patched.push(path.relative(cwd, filePath));
|
|
245
|
+
} else {
|
|
246
|
+
skipped.push(path.relative(cwd, filePath));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { patched, skipped, port };
|
|
251
|
+
}
|
|
252
|
+
|
|
164
253
|
export default {
|
|
165
254
|
sanitizeK8sName,
|
|
166
255
|
hasKubernetesManifests,
|
|
167
256
|
generateKubernetesManifests,
|
|
257
|
+
patchKubernetesManifestPorts,
|
|
168
258
|
resolveKubernetesManifestOptions,
|
|
259
|
+
resolveKubernetesManifestOptionsFromCwd,
|
|
260
|
+
getDefaultKubernetesManifestPaths,
|
|
261
|
+
syncKubernetesManifestPorts,
|
|
169
262
|
};
|
package/src/utils/scaffold.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
generateKubernetesManifests,
|
|
12
12
|
hasKubernetesManifests,
|
|
13
|
-
|
|
13
|
+
resolveKubernetesManifestOptionsFromCwd,
|
|
14
14
|
} from './kubernetes-manifests.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -134,7 +134,11 @@ export async function ensureKubernetesManifests(
|
|
|
134
134
|
return { generated: false };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const manifestOptions =
|
|
137
|
+
const manifestOptions = await resolveKubernetesManifestOptionsFromCwd(
|
|
138
|
+
cwd,
|
|
139
|
+
config,
|
|
140
|
+
environments
|
|
141
|
+
);
|
|
138
142
|
const { deploymentYaml, serviceYaml } = generateKubernetesManifests(manifestOptions);
|
|
139
143
|
|
|
140
144
|
const k8sDir = path.join(cwd, 'k8s');
|