@akash-chowdhury-24/deployhub 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/package.json +1 -1
- package/src/cli/index.js +4 -0
- package/src/commands/artifact.js +17 -9
- package/src/commands/doctor.js +19 -2
- package/src/commands/init.js +1 -0
- package/src/commands/sync-k8s-ports.js +71 -0
- package/src/commands/sync-workflows.js +43 -0
- package/src/deployment/index.js +3 -2
- package/src/deployment/providers/azure-vm.js +39 -18
- package/src/deployment/providers/docker.js +29 -7
- package/src/deployment/providers/ec2.js +39 -18
- package/src/deployment/providers/gcp-vm.js +39 -18
- package/src/deployment/providers/kubernetes.js +67 -12
- package/src/deployment/providers/ssh.js +1 -1
- package/src/storage/index.js +34 -10
- package/src/storage/providers/aws.js +6 -2
- package/src/storage/providers/dropbox.js +8 -2
- package/src/storage/providers/ftp.js +8 -2
- package/src/storage/storage-errors.js +106 -0
- package/src/utils/docker-image-deploy.js +44 -21
- package/src/utils/docker-image.js +48 -9
- package/src/utils/dockerfile-expose.js +117 -0
- package/src/utils/github-actions.js +175 -8
- package/src/utils/kubernetes-manifests.js +102 -9
- package/src/utils/rollback/engine.js +14 -12
- package/src/utils/scaffold.js +6 -2
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
|
@@ -13,6 +13,8 @@ import { registerDoctorCommand } from '../commands/doctor.js';
|
|
|
13
13
|
import { registerVerifyCommand } from '../commands/verify.js';
|
|
14
14
|
import { registerCleanCommand } from '../commands/clean.js';
|
|
15
15
|
import { registerUpdateCommand } from '../commands/update.js';
|
|
16
|
+
import { registerSyncWorkflowsCommand } from '../commands/sync-workflows.js';
|
|
17
|
+
import { registerSyncK8sPortsCommand } from '../commands/sync-k8s-ports.js';
|
|
16
18
|
import { formatVersionOutput, printBanner, shouldShowBanner } from '../utils/author.js';
|
|
17
19
|
|
|
18
20
|
loadEnv();
|
|
@@ -41,5 +43,7 @@ registerDoctorCommand(program);
|
|
|
41
43
|
registerVerifyCommand(program);
|
|
42
44
|
registerCleanCommand(program);
|
|
43
45
|
registerUpdateCommand(program);
|
|
46
|
+
registerSyncWorkflowsCommand(program);
|
|
47
|
+
registerSyncK8sPortsCommand(program);
|
|
44
48
|
|
|
45
49
|
program.parse();
|
package/src/commands/artifact.js
CHANGED
|
@@ -55,10 +55,21 @@ export function registerArtifactCommand(program) {
|
|
|
55
55
|
if (opts.remote) {
|
|
56
56
|
console.log(chalk.bold('\nRemote history (storage):\n'));
|
|
57
57
|
try {
|
|
58
|
-
const history = await loadArtifactHistory(
|
|
58
|
+
const { entries: history, source } = await loadArtifactHistory(
|
|
59
|
+
config.storage || [],
|
|
60
|
+
config.project
|
|
61
|
+
);
|
|
59
62
|
if (history.length === 0) {
|
|
60
|
-
console.log(
|
|
63
|
+
console.log(
|
|
64
|
+
chalk.yellow(
|
|
65
|
+
' No artifact history found for this project — you may not have deployed any builds yet.'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
61
68
|
} else {
|
|
69
|
+
if (source) {
|
|
70
|
+
console.log(chalk.gray(` Source: ${source}`));
|
|
71
|
+
console.log('');
|
|
72
|
+
}
|
|
62
73
|
for (const e of history) {
|
|
63
74
|
console.log(
|
|
64
75
|
` ${chalk.cyan(e.buildId)} semver=${e.semver} ${e.uploadedAt || ''}`
|
|
@@ -67,11 +78,8 @@ export function registerArtifactCommand(program) {
|
|
|
67
78
|
}
|
|
68
79
|
}
|
|
69
80
|
} catch (err) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
` Could not load remote history: ${err instanceof Error ? err.message : String(err)}`
|
|
73
|
-
)
|
|
74
|
-
);
|
|
81
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
82
|
+
console.log(chalk.red(` ${detail}`));
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -101,8 +109,8 @@ export function registerArtifactCommand(program) {
|
|
|
101
109
|
|
|
102
110
|
const history = await loadArtifactHistory(config.storage || [], config.project);
|
|
103
111
|
const histMatch =
|
|
104
|
-
history.find((e) => e.buildId === needle || e.buildId === versionOrBuildId) ||
|
|
105
|
-
history.find((e) => e.semver === needle);
|
|
112
|
+
history.entries.find((e) => e.buildId === needle || e.buildId === versionOrBuildId) ||
|
|
113
|
+
history.entries.find((e) => e.semver === needle);
|
|
106
114
|
|
|
107
115
|
const restoreDir = path.join(cwd, '.deployhub-restore', `v${needle}`);
|
|
108
116
|
await fs.ensureDir(restoreDir);
|
package/src/commands/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ import axios from 'axios';
|
|
|
7
7
|
import { loadConfig, loadEnv } from '../core/config.js';
|
|
8
8
|
import { testProvider } from '../storage/index.js';
|
|
9
9
|
import { getDeploymentProvider } from '../deployment/index.js';
|
|
10
|
-
import { PROVIDER_ENV_MAP } from '../utils/github-actions.js';
|
|
10
|
+
import { PROVIDER_ENV_MAP, getRollbackWorkflowDoctorCheck } from '../utils/github-actions.js';
|
|
11
11
|
import { printDoctorFooter } from '../utils/author.js';
|
|
12
12
|
import { createLocalProvider } from '../storage/providers/local.js';
|
|
13
13
|
import {
|
|
@@ -888,11 +888,28 @@ export function registerDoctorCommand(program) {
|
|
|
888
888
|
return {
|
|
889
889
|
name: 'GitHub Actions',
|
|
890
890
|
pass: false,
|
|
891
|
-
message: 'Workflow file missing — run deployhub init',
|
|
891
|
+
message: 'Workflow file missing — run deployhub init or deployhub sync-workflows',
|
|
892
892
|
};
|
|
893
893
|
})
|
|
894
894
|
);
|
|
895
895
|
|
|
896
|
+
const hasStorage = (config.storage || []).length > 0;
|
|
897
|
+
const hasDeploy = (config.deploy || []).length > 0;
|
|
898
|
+
if (hasStorage && hasDeploy) {
|
|
899
|
+
results.push(
|
|
900
|
+
await runCheck('Rollback workflow', async () => {
|
|
901
|
+
const check = await getRollbackWorkflowDoctorCheck(cwd, config);
|
|
902
|
+
return (
|
|
903
|
+
check || {
|
|
904
|
+
name: 'Rollback workflow',
|
|
905
|
+
pass: true,
|
|
906
|
+
message: 'Skipped',
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
})
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
896
913
|
results.push(
|
|
897
914
|
await runCheck('Storage write', async () => {
|
|
898
915
|
const provider = createLocalProvider();
|
package/src/commands/init.js
CHANGED
|
@@ -535,6 +535,7 @@ export function registerInitCommand(program) {
|
|
|
535
535
|
console.log(chalk.bold('Generated files:'));
|
|
536
536
|
console.log(' • deployhub.config.json');
|
|
537
537
|
console.log(' • .github/workflows/deployhub.yml');
|
|
538
|
+
console.log(' • .github/workflows/deployhub-rollback.yml');
|
|
538
539
|
console.log(' • .env.example');
|
|
539
540
|
console.log('');
|
|
540
541
|
printAuthorFooter();
|
|
@@ -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,43 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, loadEnv } from '../core/config.js';
|
|
3
|
+
import {
|
|
4
|
+
writeWorkflowFile,
|
|
5
|
+
DEPLOY_WORKFLOW_FILENAME,
|
|
6
|
+
ROLLBACK_WORKFLOW_FILENAME,
|
|
7
|
+
} from '../utils/github-actions.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regenerate GitHub Actions workflows from deployhub.config.json (no interactive init).
|
|
11
|
+
* @param {import('commander').Command} program
|
|
12
|
+
*/
|
|
13
|
+
export function registerSyncWorkflowsCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('sync-workflows')
|
|
16
|
+
.description(
|
|
17
|
+
'Regenerate .github/workflows/deployhub.yml and deployhub-rollback.yml from deployhub.config.json'
|
|
18
|
+
)
|
|
19
|
+
.action(async () => {
|
|
20
|
+
loadEnv();
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const config = await loadConfig(cwd);
|
|
23
|
+
|
|
24
|
+
const storage = config.storage || [];
|
|
25
|
+
const deploy = config.deploy || [];
|
|
26
|
+
const environments = config.environments || {};
|
|
27
|
+
const cliSource = config.cli?.source;
|
|
28
|
+
|
|
29
|
+
await writeWorkflowFile(storage, deploy, environments, cwd, cliSource, config);
|
|
30
|
+
|
|
31
|
+
console.log(chalk.green('✓ Regenerated GitHub Actions workflows:'));
|
|
32
|
+
console.log(` • .github/workflows/${DEPLOY_WORKFLOW_FILENAME}`);
|
|
33
|
+
console.log(` • .github/workflows/${ROLLBACK_WORKFLOW_FILENAME}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.gray(
|
|
37
|
+
'Commit and push these files, then use Actions → DeployHub Rollback (workflow_dispatch) to roll back.'
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default { registerSyncWorkflowsCommand };
|
package/src/deployment/index.js
CHANGED
|
@@ -71,13 +71,14 @@ export async function deployToAll(config, artifactDir, envNames) {
|
|
|
71
71
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
72
72
|
* @param {string} artifactDir
|
|
73
73
|
* @param {string[]} [envNames]
|
|
74
|
+
* @param {{ buildId?: string, semver?: string, remoteKey?: string }} [meta]
|
|
74
75
|
*/
|
|
75
|
-
export async function rollbackAll(config, artifactDir, envNames) {
|
|
76
|
+
export async function rollbackAll(config, artifactDir, envNames, meta) {
|
|
76
77
|
const targets = envNames || config.deploy || [];
|
|
77
78
|
for (const envName of targets) {
|
|
78
79
|
const envConfig = config.environments[envName];
|
|
79
80
|
const provider = getDeploymentProvider(envConfig.type, config, envName);
|
|
80
|
-
await provider.rollback(artifactDir);
|
|
81
|
+
await provider.rollback(artifactDir, meta);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -21,7 +21,8 @@ export function createAzureVmProvider(config, envName, env = process.env) {
|
|
|
21
21
|
|
|
22
22
|
if (!subscriptionId || !resourceGroup || !vmName) {
|
|
23
23
|
throw new Error(
|
|
24
|
-
'
|
|
24
|
+
'Could not resolve host via Azure VM lookup, and no SSH_HOST was set — ' +
|
|
25
|
+
'provide SSH_HOST (VM public IP/DNS) or set AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP, and AZURE_VM_NAME for auto lookup.'
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -55,34 +56,54 @@ export function createAzureVmProvider(config, envName, env = process.env) {
|
|
|
55
56
|
} catch (err) {
|
|
56
57
|
const msg = err instanceof Error ? err.message : String(err);
|
|
57
58
|
throw new Error(
|
|
58
|
-
`Could not resolve
|
|
59
|
+
`Could not resolve host via Azure VM lookup (${vmName}): ${msg}. ` +
|
|
60
|
+
'Set SSH_HOST to the VM public IP/DNS, or run az login and verify resource group/VM name.'
|
|
59
61
|
);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Resolve host (skipping cloud lookup when SSH_HOST/environment.host is set),
|
|
67
|
+
* then create an SSH provider that closes over the resolved host.
|
|
68
|
+
*/
|
|
69
|
+
async function getSshProvider() {
|
|
66
70
|
const host = await resolveHost();
|
|
71
|
+
if (!host) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Could not resolve host via Azure VM lookup, and no SSH_HOST was set — provide one or the other.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
67
76
|
const environment = config.environments[envName];
|
|
68
|
-
if (environment
|
|
77
|
+
if (environment) {
|
|
69
78
|
environment.host = host;
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
env.SSH_HOST = host;
|
|
73
|
-
}
|
|
74
|
-
return sshProvider.connect();
|
|
80
|
+
return createSshProvider(config, envName, { ...env, SSH_HOST: host });
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
async connect() {
|
|
85
|
+
const ssh = await getSshProvider();
|
|
86
|
+
return ssh.connect();
|
|
87
|
+
},
|
|
88
|
+
async deploy(artifactDir, options) {
|
|
89
|
+
const ssh = await getSshProvider();
|
|
90
|
+
return ssh.deploy(artifactDir, options);
|
|
91
|
+
},
|
|
92
|
+
async rollback(artifactDir, meta) {
|
|
93
|
+
const ssh = await getSshProvider();
|
|
94
|
+
return ssh.rollback(artifactDir, meta);
|
|
95
|
+
},
|
|
96
|
+
async healthCheck() {
|
|
97
|
+
const ssh = await getSshProvider();
|
|
98
|
+
return ssh.healthCheck();
|
|
99
|
+
},
|
|
100
|
+
async testConnection() {
|
|
101
|
+
const ssh = await getSshProvider();
|
|
102
|
+
return ssh.testConnection();
|
|
103
|
+
},
|
|
104
|
+
async runRemoteCheck(command) {
|
|
105
|
+
const ssh = await getSshProvider();
|
|
106
|
+
return ssh.runRemoteCheck(command);
|
|
86
107
|
},
|
|
87
108
|
};
|
|
88
109
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { createLogger } from '../../logger/index.js';
|
|
3
3
|
import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
|
|
4
|
+
import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -14,12 +15,17 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @param {string} artifactDir
|
|
18
|
+
* @param {{ fullImage?: string, skipImageReuse?: boolean }} [options]
|
|
17
19
|
*/
|
|
18
|
-
async function deploy(artifactDir) {
|
|
19
|
-
|
|
20
|
+
async function deploy(artifactDir, options = {}) {
|
|
21
|
+
const imageRef = options.fullImage || fullImage;
|
|
22
|
+
log.info(`Deploying via Docker (image: ${imageRef})...`);
|
|
20
23
|
const dockerEnv = getDockerEnv();
|
|
21
24
|
|
|
22
|
-
const result = await ensureImageReadyForDeploy(artifactDir
|
|
25
|
+
const result = await ensureImageReadyForDeploy(artifactDir, {
|
|
26
|
+
fullImage: options.fullImage,
|
|
27
|
+
skipImageReuse: options.skipImageReuse,
|
|
28
|
+
});
|
|
23
29
|
if (result.ranCompose) {
|
|
24
30
|
log.success('Docker deployment complete');
|
|
25
31
|
return;
|
|
@@ -31,7 +37,7 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
31
37
|
{ stdio: 'pipe', env: dockerEnv }
|
|
32
38
|
).catch(() => {});
|
|
33
39
|
|
|
34
|
-
await execa('docker', ['run', '-d', '--rm', '--name', config.project,
|
|
40
|
+
await execa('docker', ['run', '-d', '--rm', '--name', config.project, imageRef], {
|
|
35
41
|
stdio: 'inherit',
|
|
36
42
|
env: dockerEnv,
|
|
37
43
|
});
|
|
@@ -39,9 +45,25 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
39
45
|
log.success('Docker deployment complete');
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} artifactDir
|
|
50
|
+
* @param {{ buildId?: string, semver?: string, remoteKey?: string }} [meta]
|
|
51
|
+
*/
|
|
52
|
+
async function rollback(artifactDir, meta = {}) {
|
|
53
|
+
if (!meta.buildId) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Docker rollback requires buildId from the restored artifact history entry'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rollbackImage = resolveDockerImageRefForTag(config, env, meta.buildId).fullImage;
|
|
60
|
+
log.info(
|
|
61
|
+
`Rolling back Docker to buildId=${meta.buildId} (image: ${rollbackImage})...`
|
|
62
|
+
);
|
|
63
|
+
await deploy(artifactDir, {
|
|
64
|
+
fullImage: rollbackImage,
|
|
65
|
+
skipImageReuse: true,
|
|
66
|
+
});
|
|
45
67
|
}
|
|
46
68
|
|
|
47
69
|
async function healthCheck() {
|