@akash-chowdhury-24/deployhub 2.0.0 → 2.0.3
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 +227 -5
- package/package.json +4 -3
- package/src/commands/deploy.js +1 -1
- package/src/commands/doctor.js +244 -30
- package/src/commands/init.js +66 -223
- package/src/commands/rollback.js +1 -1
- package/src/core/config.js +15 -0
- package/src/deployment/deployment-env.js +625 -0
- package/src/deployment/init-helpers.js +471 -0
- package/src/deployment/init-prompts.js +434 -0
- package/src/deployment/providers/azure-vm.js +85 -2
- package/src/deployment/providers/docker.js +101 -8
- package/src/deployment/providers/ec2.js +88 -2
- package/src/deployment/providers/gcp-vm.js +89 -2
- package/src/deployment/providers/kubernetes.js +104 -7
- package/src/deployment/providers/ssh.js +70 -42
- package/src/utils/github-actions.js +24 -37
- package/src/{rollback → utils/rollback}/engine.js +6 -6
- package/src/utils/shell-quote.js +64 -0
package/README.md
CHANGED
|
@@ -585,6 +585,219 @@ You can enable **multiple providers** — DeployHub uploads to all of them in pa
|
|
|
585
585
|
|
|
586
586
|
---
|
|
587
587
|
|
|
588
|
+
## Choosing a deployment method
|
|
589
|
+
|
|
590
|
+
DeployHub supports six deployment targets. Pick based on what infrastructure you already have — DeployHub does not provision servers, VMs, or clusters for you.
|
|
591
|
+
|
|
592
|
+
| Method | Best for | You need already |
|
|
593
|
+
|--------|----------|------------------|
|
|
594
|
+
| **ssh** | Any Linux VPS or bare-metal server you control | Server with SSH, key pair, app runtime |
|
|
595
|
+
| **docker** | Containerized apps (Dockerfile or docker-compose.yml) | Docker locally or on a remote host |
|
|
596
|
+
| **ec2** | AWS users with an existing EC2 instance | Running EC2 instance, security group, key pair |
|
|
597
|
+
| **azure-vm** | Azure users with an existing virtual machine | Running Azure VM, NSG allowing SSH |
|
|
598
|
+
| **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, manifests in repo |
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## Deployment method guides
|
|
604
|
+
|
|
605
|
+
Each method below follows the same structure: **prerequisites** (before `deployhub init`), **what DeployHub automates**, **after init** (matches terminal output), and a **variable reference**.
|
|
606
|
+
|
|
607
|
+
### SSH
|
|
608
|
+
|
|
609
|
+
**Prerequisites (before `deployhub init`):**
|
|
610
|
+
- [ ] A Linux server with SSH enabled
|
|
611
|
+
- [ ] Private SSH key file (.pem/.key) and public key in `authorized_keys`
|
|
612
|
+
- [ ] Port 22 open in firewall for your IP
|
|
613
|
+
- [ ] Deploy directory writable by your SSH user (e.g. `sudo mkdir -p /var/www/my-app && sudo chown ubuntu:ubuntu /var/www/my-app` — `/var/www` is root-owned on most fresh Linux images)
|
|
614
|
+
- [ ] App runtime on server (Node.js, Python, etc.) for backends
|
|
615
|
+
|
|
616
|
+
**What DeployHub automates:**
|
|
617
|
+
- Complete `.env.example` with commented variables
|
|
618
|
+
- SSH key permission check (offers to `chmod 600`)
|
|
619
|
+
- SSH connectivity test during `init`
|
|
620
|
+
- Deploy path write-permission check during `deployhub doctor`
|
|
621
|
+
- Artifact upload, extract, app restart (PM2, gunicorn, etc.)
|
|
622
|
+
|
|
623
|
+
**After `init`:**
|
|
624
|
+
1. Ensure port 22 is open in your server firewall
|
|
625
|
+
2. Ensure your deploy directory exists and is owned by your SSH user (see prerequisite above if `deployhub doctor` reports permission denied)
|
|
626
|
+
3. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
|
|
627
|
+
4. Add GitHub Secrets: `SSH_HOST`, `SSH_USER`, `SSH_KEY` (paste private key for CI)
|
|
628
|
+
5. Run `deployhub doctor`
|
|
629
|
+
6. `git push origin main`
|
|
630
|
+
|
|
631
|
+
| Variable | Description | Example | Where to get it |
|
|
632
|
+
|----------|-------------|---------|-----------------|
|
|
633
|
+
| `SSH_HOST` | Server IP or hostname | `203.0.113.10` | Your hosting provider dashboard |
|
|
634
|
+
| `SSH_USER` | SSH login user | `ubuntu` | AMI/image docs (Ubuntu→ubuntu, Amazon Linux→ec2-user) |
|
|
635
|
+
| `SSH_KEY_PATH` | Path to private key file | `~/.ssh/my-key.pem` | Downloaded when server was created |
|
|
636
|
+
| `SSH_SSH_PORT` | SSH port (optional) | `22` | Server SSH config |
|
|
637
|
+
| `SSH_DEPLOY_PATH` | Remote deploy directory | `/var/www/my-app` | Your server layout |
|
|
638
|
+
| `SSH_APP_NAME` | PM2 process name (backend) | `my-api` | Your choice |
|
|
639
|
+
| `SSH_PORT` | App listen port (backend) | `3000` | Your app config |
|
|
640
|
+
| `SSH_KEY` | Private key contents (CI only) | `-----BEGIN...` | Same key as `SSH_KEY_PATH` |
|
|
641
|
+
|
|
642
|
+
### Docker
|
|
643
|
+
|
|
644
|
+
**Prerequisites:**
|
|
645
|
+
- [ ] Docker installed (`docker --version` works)
|
|
646
|
+
- [ ] `Dockerfile` or `docker-compose.yml` in project
|
|
647
|
+
- [ ] Registry account if pushing private images
|
|
648
|
+
|
|
649
|
+
**What DeployHub automates:**
|
|
650
|
+
- `.env.example` for image name, registry, remote `DOCKER_HOST`
|
|
651
|
+
- Docker daemon connectivity test during `init`
|
|
652
|
+
- `docker compose up` or build/push/run during deploy
|
|
653
|
+
|
|
654
|
+
**After `init`:**
|
|
655
|
+
1. Set `DOCKER_IMAGE_NAME` in `.env`
|
|
656
|
+
2. For private registries: set `DOCKER_REGISTRY_USERNAME` and `DOCKER_REGISTRY_TOKEN`
|
|
657
|
+
3. For remote Docker: set `DOCKER_HOST` (e.g. `ssh://ubuntu@203.0.113.10`)
|
|
658
|
+
4. Run `deployhub doctor`, then `git push origin main`
|
|
659
|
+
|
|
660
|
+
| Variable | Description | Example | Where to get it |
|
|
661
|
+
|----------|-------------|---------|-----------------|
|
|
662
|
+
| `DOCKER_IMAGE_NAME` | Image repository path | `myorg/myapp` | Your registry naming |
|
|
663
|
+
| `DOCKER_IMAGE_TAG` | Image tag | `latest` | Version or `latest` |
|
|
664
|
+
| `DOCKER_REGISTRY_URL` | Registry URL (optional) | `https://ghcr.io` | Registry docs |
|
|
665
|
+
| `DOCKER_REGISTRY_USERNAME` | Registry user | `myuser` | Registry account |
|
|
666
|
+
| `DOCKER_REGISTRY_TOKEN` | Registry password/token | *(secret)* | Docker Hub / GHCR PAT |
|
|
667
|
+
| `DOCKER_HOST` | Remote daemon (optional) | `ssh://ubuntu@host` | Remote Docker setup |
|
|
668
|
+
|
|
669
|
+
### AWS EC2
|
|
670
|
+
|
|
671
|
+
**Prerequisites:**
|
|
672
|
+
- [ ] EC2 instance launched in AWS Console (DeployHub does not create it)
|
|
673
|
+
- [ ] Key pair `.pem` downloaded at launch
|
|
674
|
+
- [ ] Security group: inbound SSH (22) from your IP
|
|
675
|
+
- [ ] Deploy directory writable by your SSH user (e.g. `sudo mkdir -p /var/www/my-app && sudo chown ec2-user:ec2-user /var/www/my-app` — `/var/www` is root-owned on Amazon Linux by default)
|
|
676
|
+
- [ ] App runtime on instance for backends
|
|
677
|
+
|
|
678
|
+
**What DeployHub automates:**
|
|
679
|
+
- EC2-specific `.env.example` (SSH + optional AWS API vars)
|
|
680
|
+
- SSH key validation and connectivity test
|
|
681
|
+
- Deploy path write-permission check during `deployhub doctor`
|
|
682
|
+
- OS user suggestion from AMI hint (ubuntu, ec2-user)
|
|
683
|
+
- Optional public IP lookup via `EC2_INSTANCE_ID` + AWS CLI
|
|
684
|
+
|
|
685
|
+
**After `init`:**
|
|
686
|
+
1. AWS Console → EC2 → Security Groups → Inbound rules → SSH port 22 from My IP
|
|
687
|
+
2. Ensure your deploy directory exists and is owned by your SSH user (see prerequisite above if `deployhub doctor` reports permission denied)
|
|
688
|
+
3. Copy `.env.example` → `.env`; set `SSH_KEY_PATH`, `SSH_HOST` (or `EC2_INSTANCE_ID` + AWS creds)
|
|
689
|
+
4. GitHub Secrets: `SSH_HOST`, `SSH_USER`, `SSH_KEY`, plus `AWS_*` if using instance ID lookup
|
|
690
|
+
5. Run `deployhub doctor`, then `git push origin main`
|
|
691
|
+
|
|
692
|
+
| Variable | Description | Example | Where to get it |
|
|
693
|
+
|----------|-------------|---------|-----------------|
|
|
694
|
+
| `SSH_HOST` | Instance public IP/DNS | `54.123.45.67` | EC2 Console → Instances |
|
|
695
|
+
| `SSH_USER` | SSH user for AMI | `ec2-user` | AMI documentation |
|
|
696
|
+
| `SSH_KEY_PATH` | Path to .pem key | `~/.ssh/ec2-key.pem` | Downloaded at instance launch |
|
|
697
|
+
| `EC2_INSTANCE_ID` | Instance ID (optional) | `i-0abc123...` | EC2 Console |
|
|
698
|
+
| `AWS_ACCESS_KEY_ID` | AWS key for API lookup | `AKIA...` | IAM → Users → Security credentials |
|
|
699
|
+
| `AWS_SECRET_ACCESS_KEY` | AWS secret | *(secret)* | Same as above |
|
|
700
|
+
| `AWS_REGION` | Instance region | `us-east-1` | EC2 Console top bar |
|
|
701
|
+
|
|
702
|
+
### Azure VM
|
|
703
|
+
|
|
704
|
+
**Prerequisites:**
|
|
705
|
+
- [ ] Azure VM created in Portal (DeployHub does not provision it)
|
|
706
|
+
- [ ] NSG rule allowing inbound SSH (port 22)
|
|
707
|
+
- [ ] SSH public key on the VM
|
|
708
|
+
- [ ] Deploy directory writable by your SSH user (e.g. `sudo mkdir -p /var/www/my-app && sudo chown azureuser:azureuser /var/www/my-app`)
|
|
709
|
+
- [ ] App runtime for backends
|
|
710
|
+
|
|
711
|
+
**What DeployHub automates:**
|
|
712
|
+
- Azure VM `.env.example` with SSH + optional Azure API vars
|
|
713
|
+
- Auto-detects subscription ID via `az` CLI if logged in
|
|
714
|
+
- SSH key validation and connectivity test
|
|
715
|
+
- Deploy path write-permission check during `deployhub doctor`
|
|
716
|
+
|
|
717
|
+
**After `init`:**
|
|
718
|
+
1. Azure Portal → VM → Networking → allow SSH (22) from your IP
|
|
719
|
+
2. Ensure your deploy directory exists and is owned by your SSH user (see prerequisite above if `deployhub doctor` reports permission denied)
|
|
720
|
+
3. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
|
|
721
|
+
4. For CI: add `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` as GitHub Secrets
|
|
722
|
+
5. Run `deployhub doctor`, then `git push origin main`
|
|
723
|
+
|
|
724
|
+
| Variable | Description | Example | Where to get it |
|
|
725
|
+
|----------|-------------|---------|-----------------|
|
|
726
|
+
| `SSH_HOST` | VM public IP | `20.1.2.3` | Azure Portal → VM overview |
|
|
727
|
+
| `SSH_USER` | SSH username | `azureuser` | Chosen at VM creation |
|
|
728
|
+
| `SSH_KEY_PATH` | Private key path | `~/.ssh/azure.pem` | Your key file |
|
|
729
|
+
| `AZURE_SUBSCRIPTION_ID` | Subscription (optional) | `uuid` | `az account show` |
|
|
730
|
+
| `AZURE_RESOURCE_GROUP` | Resource group | `my-app-rg` | Portal → Resource groups |
|
|
731
|
+
| `AZURE_VM_NAME` | VM name | `my-vm` | Portal → Virtual machines |
|
|
732
|
+
|
|
733
|
+
### GCP VM
|
|
734
|
+
|
|
735
|
+
**Prerequisites:**
|
|
736
|
+
- [ ] Compute Engine VM created (DeployHub does not create it)
|
|
737
|
+
- [ ] Firewall rule allowing `tcp:22` (default `default-allow-ssh` may exist)
|
|
738
|
+
- [ ] SSH public key in **Metadata → SSH Keys** (GCP uses metadata keys, not launch key pairs like AWS)
|
|
739
|
+
- [ ] Deploy directory writable by your SSH user (e.g. `sudo mkdir -p /var/www/my-app && sudo chown $USER:$USER /var/www/my-app`)
|
|
740
|
+
- [ ] App runtime for backends
|
|
741
|
+
|
|
742
|
+
**What DeployHub automates:**
|
|
743
|
+
- GCP VM `.env.example` with SSH + optional GCP API vars
|
|
744
|
+
- Auto-detects project ID via `gcloud` if authenticated
|
|
745
|
+
- SSH key validation and connectivity test
|
|
746
|
+
- Deploy path write-permission check during `deployhub doctor`
|
|
747
|
+
|
|
748
|
+
**After `init`:**
|
|
749
|
+
1. GCP Console → VPC → Firewall → ensure SSH (tcp:22) allowed from your IP
|
|
750
|
+
2. Add SSH public key: Console → Compute Engine → Metadata → SSH Keys
|
|
751
|
+
3. Ensure your deploy directory exists and is owned by your SSH user (see prerequisite above if `deployhub doctor` reports permission denied)
|
|
752
|
+
4. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
|
|
753
|
+
5. Run `deployhub doctor`, then `git push origin main`
|
|
754
|
+
|
|
755
|
+
| Variable | Description | Example | Where to get it |
|
|
756
|
+
|----------|-------------|---------|-----------------|
|
|
757
|
+
| `SSH_HOST` | External IP | `34.56.78.90` | Compute Engine → VM instances |
|
|
758
|
+
| `SSH_USER` | SSH username | `your_google_username` | GCP OS Login or metadata |
|
|
759
|
+
| `SSH_KEY_PATH` | Private key path | `~/.ssh/gcp-key` | Your local key pair |
|
|
760
|
+
| `SSH_SSH_PORT` | SSH connection port (optional) | `22` | Server SSH config |
|
|
761
|
+
| `SSH_DEPLOY_PATH` | Remote deploy directory (optional) | `/var/www/my-app` | Your server layout |
|
|
762
|
+
| `SSH_APP_NAME` | PM2 process name (backend) | `my-api` | Your choice |
|
|
763
|
+
| `SSH_PORT` | App listen port (backend) | `3000` | Your app config |
|
|
764
|
+
| `SSH_KEY` | Private key contents (CI only) | `-----BEGIN...` | Same key as `SSH_KEY_PATH` |
|
|
765
|
+
| `GCP_PROJECT_ID` | Project ID (optional) | `my-project-123` | `gcloud config get-value project` |
|
|
766
|
+
| `GCP_ZONE` | VM zone (optional) | `us-central1-a` | VM instance details |
|
|
767
|
+
| `GCP_INSTANCE_NAME` | Instance name (optional) | `my-vm` | Compute Engine list |
|
|
768
|
+
| `GCP_KEY_FILE` | Service account JSON (optional, CI) | `/path/to/key.json` | IAM → Service Accounts → Keys |
|
|
769
|
+
|
|
770
|
+
### Kubernetes
|
|
771
|
+
|
|
772
|
+
**Prerequisites:**
|
|
773
|
+
- [ ] Existing Kubernetes cluster (DeployHub does not provision clusters)
|
|
774
|
+
- [ ] `kubectl` installed and configured
|
|
775
|
+
- [ ] Kubernetes manifests (`.yaml` or `k8s/` directory) in your repo
|
|
776
|
+
- [ ] Cluster reachable from CI (kubeconfig secret or cloud auth)
|
|
777
|
+
|
|
778
|
+
**What DeployHub automates:**
|
|
779
|
+
- Lists `kubectl` contexts during `init` for easy selection
|
|
780
|
+
- Auto-detects `~/.kube/config`
|
|
781
|
+
- Complete `.env.example` for kubeconfig, context, namespace
|
|
782
|
+
- Cluster connectivity test during `init`
|
|
783
|
+
|
|
784
|
+
**After `init`:**
|
|
785
|
+
1. Verify context: `kubectl config get-contexts`
|
|
786
|
+
2. Create namespace if needed: `kubectl create namespace my-app`
|
|
787
|
+
3. For private registries: create `imagePullSecret` and set `KUBE_IMAGE_PULL_SECRET`
|
|
788
|
+
4. Copy `.env.example` → `.env`; add kubeconfig/auth to GitHub Secrets for CI
|
|
789
|
+
5. Run `deployhub doctor`, then `git push origin main`
|
|
790
|
+
|
|
791
|
+
| Variable | Description | Example | Where to get it |
|
|
792
|
+
|----------|-------------|---------|-----------------|
|
|
793
|
+
| `KUBECONFIG` | Path to kubeconfig | `~/.kube/config` | Default kubectl config |
|
|
794
|
+
| `KUBE_CONTEXT` | Context name | `my-cluster` | `kubectl config get-contexts` |
|
|
795
|
+
| `KUBE_NAMESPACE` | Target namespace | `my-app` | `kubectl get namespaces` |
|
|
796
|
+
| `DOCKER_IMAGE_NAME` | Container image | `ghcr.io/org/app` | Your registry |
|
|
797
|
+
| `KUBE_IMAGE_PULL_SECRET` | Pull secret name | `regcred` | `kubectl create secret docker-registry` |
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
588
801
|
## Minimal `deployhub.config.json` examples
|
|
589
802
|
|
|
590
803
|
### Storage only — React
|
|
@@ -640,7 +853,7 @@ Prefer `deployhub init` over hand-writing config — it sets adapters, workflow,
|
|
|
640
853
|
|---------|-----|
|
|
641
854
|
| `Deploy requires storage upload` | Add at least one storage provider in config |
|
|
642
855
|
| AWS / GDrive check fails in `doctor` | Run `deployhub storage add <provider>` and match GitHub Secrets |
|
|
643
|
-
| SSH deploy fails | Verify `
|
|
856
|
+
| SSH deploy fails | Verify `SSH_KEY_PATH` points to your private `.pem` file (or `SSH_KEY` in CI); user can write to deploy path; port 22 open |
|
|
644
857
|
| Wrong output uploaded | Fix `buildOutput` in config (`dist` vs `build` vs `.next`) |
|
|
645
858
|
| Tests fail in CI | Set `"pipeline": { "test": false }` temporarily, or fix tests |
|
|
646
859
|
| Monorepo subfolders | Edit `buildCommand` paths in `deployhub.config.json` after init |
|
|
@@ -692,16 +905,25 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
|
|
|
692
905
|
| `DROPBOX_ACCESS_TOKEN` | Dropbox |
|
|
693
906
|
| `FTP_HOST`, `FTP_USER`, `FTP_PASSWORD` | FTP storage |
|
|
694
907
|
|
|
695
|
-
### Server deployment (SSH, EC2, VMs)
|
|
908
|
+
### Server deployment (SSH, EC2, VMs, Docker, Kubernetes)
|
|
696
909
|
|
|
697
910
|
| Secret | Used for |
|
|
698
911
|
|--------|----------|
|
|
699
|
-
| `SSH_HOST` | Target server hostname |
|
|
912
|
+
| `SSH_HOST` | Target server hostname or IP |
|
|
700
913
|
| `SSH_USER` | SSH username |
|
|
701
|
-
| `
|
|
914
|
+
| `SSH_KEY_PATH` | Local path to private key (`.env` only) |
|
|
915
|
+
| `SSH_KEY` | Private key contents (GitHub Actions / CI) |
|
|
916
|
+
| `SSH_SSH_PORT` | SSH connection port (default 22) |
|
|
702
917
|
| `SSH_DEPLOY_PATH` | Remote directory (optional if set in config) |
|
|
703
918
|
| `SSH_APP_NAME` | PM2 process name for backends |
|
|
704
|
-
| `SSH_PORT` | App port on server (
|
|
919
|
+
| `SSH_PORT` | App port on server (backend) |
|
|
920
|
+
| `EC2_INSTANCE_ID`, `AWS_*` | Optional EC2 dynamic IP lookup |
|
|
921
|
+
| `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, `AZURE_VM_NAME` | Optional Azure VM IP lookup |
|
|
922
|
+
| `GCP_PROJECT_ID`, `GCP_ZONE`, `GCP_INSTANCE_NAME`, `GCP_KEY_FILE` | Optional GCP VM IP lookup |
|
|
923
|
+
| `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_*`, `DOCKER_HOST` | Docker deployment |
|
|
924
|
+
| `KUBECONFIG`, `KUBE_CONTEXT`, `KUBE_NAMESPACE` | Kubernetes deployment |
|
|
925
|
+
|
|
926
|
+
See [Deployment method guides](#deployment-method-guides) for full per-method variable tables with examples.
|
|
705
927
|
|
|
706
928
|
## `deployhub doctor` Output
|
|
707
929
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akash-chowdhury-24/deployhub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Zero-configuration deployment and artifact manager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli/index.js",
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"start": "node src/cli/index.js",
|
|
37
37
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
38
|
+
"verify:bundle-sources": "node scripts/verify-bundle-sources.mjs",
|
|
39
|
+
"prepublishOnly": "npm test && npm run verify:bundle-sources",
|
|
40
|
+
"build:bundle": "node scripts/verify-bundle-sources.mjs && node scripts/build-bundle.mjs",
|
|
40
41
|
"build:binaries": "npm run build:bundle && node scripts/prefetch-pkg-cache.mjs && pkg dist/deployhub.cjs --targets node20-linux-x64,node20-macos-x64,node20-macos-arm64,node20-win-x64 --output dist/deployhub --compress GZip"
|
|
41
42
|
},
|
|
42
43
|
"pkg": {
|
package/src/commands/deploy.js
CHANGED
|
@@ -71,7 +71,7 @@ export function registerDeployCommand(program) {
|
|
|
71
71
|
|
|
72
72
|
const { failure } = await runPipeline(stages, { config, cwd, state });
|
|
73
73
|
if (failure) {
|
|
74
|
-
console.error(chalk.red(
|
|
74
|
+
console.error(chalk.red(failure.message));
|
|
75
75
|
process.exit(1);
|
|
76
76
|
}
|
|
77
77
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -9,6 +9,15 @@ import { getDeploymentProvider } from '../deployment/index.js';
|
|
|
9
9
|
import { PROVIDER_ENV_MAP } from '../utils/github-actions.js';
|
|
10
10
|
import { printDoctorFooter } from '../utils/author.js';
|
|
11
11
|
import { createLocalProvider } from '../storage/providers/local.js';
|
|
12
|
+
import {
|
|
13
|
+
getDeploymentEnvKeys,
|
|
14
|
+
getDeploymentSecretKeys,
|
|
15
|
+
} from '../deployment/deployment-env.js';
|
|
16
|
+
import { testSshConnectivity, validateSshKeyForDoctor, testSshHostReachability } from '../deployment/init-helpers.js';
|
|
17
|
+
import {
|
|
18
|
+
buildDeployPathWriteTestCommand,
|
|
19
|
+
formatDeployPathWriteFailure,
|
|
20
|
+
} from '../utils/shell-quote.js';
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
23
|
* @typedef {{ name: string, pass: boolean, message: string }} CheckResult
|
|
@@ -48,14 +57,237 @@ function resolveBackendFramework(config) {
|
|
|
48
57
|
return config.backend?.framework || config.framework || 'express';
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
/** @type {Set<string>} */
|
|
61
|
+
const SSH_DEPLOY_TYPES = new Set(['ssh', 'ec2', 'azure-vm', 'gcp-vm']);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
65
|
+
* @param {Record<string, unknown>} envConfig
|
|
66
|
+
* @returns {string[]}
|
|
67
|
+
*/
|
|
68
|
+
function resolveSshDeployPaths(config, envConfig) {
|
|
69
|
+
/** @type {string[]} */
|
|
70
|
+
const paths = [];
|
|
71
|
+
|
|
72
|
+
if (config.projectType === 'both') {
|
|
73
|
+
if (envConfig.frontendDeployPath) paths.push(String(envConfig.frontendDeployPath));
|
|
74
|
+
if (envConfig.backendDeployPath) paths.push(String(envConfig.backendDeployPath));
|
|
75
|
+
else if (envConfig.path) paths.push(String(envConfig.path));
|
|
76
|
+
} else {
|
|
77
|
+
const deployPath =
|
|
78
|
+
envConfig.deployPath || envConfig.path || process.env.SSH_DEPLOY_PATH;
|
|
79
|
+
if (deployPath) paths.push(String(deployPath));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [...new Set(paths.filter(Boolean))];
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
/**
|
|
52
86
|
* @param {import('../core/config.js').DeployHubConfig} config
|
|
53
87
|
* @param {string} envName
|
|
88
|
+
* @param {Record<string, unknown>} envConfig
|
|
54
89
|
* @returns {Promise<CheckResult[]>}
|
|
55
90
|
*/
|
|
56
|
-
async function
|
|
91
|
+
async function runDeploymentChecks(config, envName, envConfig) {
|
|
92
|
+
const deployType = envConfig.type;
|
|
93
|
+
if (!deployType || typeof deployType !== 'string') return [];
|
|
94
|
+
|
|
95
|
+
/** @type {CheckResult[]} */
|
|
96
|
+
const checks = [];
|
|
97
|
+
const requiredKeys = getDeploymentEnvKeys(deployType, config);
|
|
98
|
+
|
|
99
|
+
checks.push(
|
|
100
|
+
await runCheck(`${deployType} env vars`, async () => {
|
|
101
|
+
const missing = requiredKeys.filter((k) => {
|
|
102
|
+
if (k === 'SSH_KEY_PATH') {
|
|
103
|
+
return !process.env.SSH_KEY_PATH && !process.env.SSH_KEY;
|
|
104
|
+
}
|
|
105
|
+
return !process.env[k];
|
|
106
|
+
});
|
|
107
|
+
if (missing.length > 0) {
|
|
108
|
+
return {
|
|
109
|
+
name: `${deployType} env vars`,
|
|
110
|
+
pass: false,
|
|
111
|
+
message: `Missing required variables: ${missing.join(', ')} — copy .env.example to .env and fill in values (see inline comments).`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
name: `${deployType} env vars`,
|
|
116
|
+
pass: true,
|
|
117
|
+
message: 'All required deployment variables present',
|
|
118
|
+
};
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (SSH_DEPLOY_TYPES.has(deployType)) {
|
|
123
|
+
const host = envConfig.host || process.env.SSH_HOST;
|
|
124
|
+
const user = envConfig.user || process.env.SSH_USER;
|
|
125
|
+
const keyPath = envConfig.keyPath || process.env.SSH_KEY_PATH;
|
|
126
|
+
const sshPort = Number(process.env.SSH_SSH_PORT || envConfig.sshPort) || 22;
|
|
127
|
+
|
|
128
|
+
checks.push(
|
|
129
|
+
await runCheck('SSH key', async () => {
|
|
130
|
+
const result = await validateSshKeyForDoctor(
|
|
131
|
+
keyPath ? String(keyPath) : undefined,
|
|
132
|
+
process.env.SSH_KEY
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
name: 'SSH key',
|
|
136
|
+
pass: result.ok,
|
|
137
|
+
message: result.message,
|
|
138
|
+
};
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
checks.push(
|
|
143
|
+
await runCheck('SSH host reachability', async () => {
|
|
144
|
+
if (!host) {
|
|
145
|
+
return {
|
|
146
|
+
name: 'SSH host reachability',
|
|
147
|
+
pass: false,
|
|
148
|
+
message: 'SSH_HOST is required — set it in .env to your server IP or hostname.',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const result = await testSshHostReachability(String(host), sshPort);
|
|
152
|
+
return {
|
|
153
|
+
name: 'SSH host reachability',
|
|
154
|
+
pass: result.ok,
|
|
155
|
+
message: result.message,
|
|
156
|
+
};
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const deployPaths = resolveSshDeployPaths(config, envConfig);
|
|
161
|
+
const sshUser = String(user || process.env.SSH_USER || 'your-user');
|
|
162
|
+
|
|
163
|
+
for (const deployPath of deployPaths) {
|
|
164
|
+
const checkName = `Deploy path write (${deployPath})`;
|
|
165
|
+
checks.push(
|
|
166
|
+
await runCheck(checkName, async () => {
|
|
167
|
+
if (!host || !user) {
|
|
168
|
+
return {
|
|
169
|
+
name: checkName,
|
|
170
|
+
pass: false,
|
|
171
|
+
message: 'SSH_HOST and SSH_USER are required for deploy path write test.',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!keyPath && !process.env.SSH_KEY) {
|
|
175
|
+
return {
|
|
176
|
+
name: checkName,
|
|
177
|
+
pass: false,
|
|
178
|
+
message: 'SSH_KEY_PATH or SSH_KEY is required for deploy path write test.',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const provider = getDeploymentProvider(deployType, config, envName);
|
|
183
|
+
if (!provider.runRemoteCheck) {
|
|
184
|
+
return {
|
|
185
|
+
name: checkName,
|
|
186
|
+
pass: false,
|
|
187
|
+
message: 'Deploy provider does not support remote checks.',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const command = buildDeployPathWriteTestCommand(deployPath);
|
|
192
|
+
const result = await provider.runRemoteCheck(command);
|
|
193
|
+
if (result.pass) {
|
|
194
|
+
return {
|
|
195
|
+
name: checkName,
|
|
196
|
+
pass: true,
|
|
197
|
+
message: `Write access OK for ${deployPath}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
name: checkName,
|
|
203
|
+
pass: false,
|
|
204
|
+
message: formatDeployPathWriteFailure(deployPath, sshUser, result.message),
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const isBackend = config.projectType === 'backend' || config.projectType === 'both';
|
|
211
|
+
if (isBackend) {
|
|
212
|
+
const backendChecks = await runBackendProcessChecks(config, envName, deployType);
|
|
213
|
+
checks.push(...backendChecks);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (deployType === 'docker') {
|
|
218
|
+
checks.push(
|
|
219
|
+
await runCheck('Docker daemon', async () => {
|
|
220
|
+
try {
|
|
221
|
+
const provider = getDeploymentProvider('docker', config, envName);
|
|
222
|
+
await provider.testConnection();
|
|
223
|
+
return { name: 'Docker daemon', pass: true, message: 'Docker daemon reachable' };
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
226
|
+
return {
|
|
227
|
+
name: 'Docker daemon',
|
|
228
|
+
pass: false,
|
|
229
|
+
message: `Docker not reachable — ${msg}. Install Docker or set DOCKER_HOST for a remote daemon.`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (deployType === 'kubernetes') {
|
|
237
|
+
checks.push(
|
|
238
|
+
await runCheck('Kubernetes cluster', async () => {
|
|
239
|
+
try {
|
|
240
|
+
const provider = getDeploymentProvider('kubernetes', config, envName);
|
|
241
|
+
await provider.testConnection();
|
|
242
|
+
const ctx = process.env.KUBE_CONTEXT || 'current';
|
|
243
|
+
const ns = process.env.KUBE_NAMESPACE || config.project || 'default';
|
|
244
|
+
return {
|
|
245
|
+
name: 'Kubernetes cluster',
|
|
246
|
+
pass: true,
|
|
247
|
+
message: `kubectl cluster-info OK (context: ${ctx}, namespace: ${ns})`,
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
251
|
+
return {
|
|
252
|
+
name: 'Kubernetes cluster',
|
|
253
|
+
pass: false,
|
|
254
|
+
message: `kubectl cluster-info failed — ${msg}. Check KUBECONFIG path and KUBE_CONTEXT.`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (deployType === 'ec2' && process.env.EC2_INSTANCE_ID) {
|
|
262
|
+
checks.push(
|
|
263
|
+
await runCheck('EC2 API', async () => {
|
|
264
|
+
const missing = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'].filter(
|
|
265
|
+
(k) => !process.env[k]
|
|
266
|
+
);
|
|
267
|
+
if (missing.length > 0) {
|
|
268
|
+
return {
|
|
269
|
+
name: 'EC2 API',
|
|
270
|
+
pass: false,
|
|
271
|
+
message: `EC2_INSTANCE_ID is set but missing: ${missing.join(', ')} — needed for dynamic IP lookup.`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return { name: 'EC2 API', pass: true, message: 'AWS credentials present for EC2 lookup' };
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return checks;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
284
|
+
* @param {string} envName
|
|
285
|
+
* @param {string} [deployType]
|
|
286
|
+
* @returns {Promise<CheckResult[]>}
|
|
287
|
+
*/
|
|
288
|
+
async function runBackendProcessChecks(config, envName, deployType = 'ssh') {
|
|
57
289
|
const framework = resolveBackendFramework(config);
|
|
58
|
-
const provider = getDeploymentProvider(
|
|
290
|
+
const provider = getDeploymentProvider(deployType, config, envName);
|
|
59
291
|
|
|
60
292
|
if (!provider.runRemoteCheck) {
|
|
61
293
|
return [];
|
|
@@ -338,27 +570,8 @@ export function registerDoctorCommand(program) {
|
|
|
338
570
|
const env = config.environments[envName];
|
|
339
571
|
if (!env) continue;
|
|
340
572
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
await runCheck('SSH target', async () => {
|
|
344
|
-
const provider = getDeploymentProvider(env.type, config, envName);
|
|
345
|
-
await provider.testConnection();
|
|
346
|
-
const host = env.host || process.env.SSH_HOST;
|
|
347
|
-
return {
|
|
348
|
-
name: 'SSH target',
|
|
349
|
-
pass: true,
|
|
350
|
-
message: `Can reach ${host || 'host'}`,
|
|
351
|
-
};
|
|
352
|
-
})
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
const isBackend =
|
|
356
|
-
config.projectType === 'backend' || config.projectType === 'both';
|
|
357
|
-
if (isBackend && env.type === 'ssh') {
|
|
358
|
-
const backendChecks = await runBackendProcessChecks(config, envName);
|
|
359
|
-
results.push(...backendChecks);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
573
|
+
const deployChecks = await runDeploymentChecks(config, envName, env);
|
|
574
|
+
results.push(...deployChecks);
|
|
362
575
|
}
|
|
363
576
|
|
|
364
577
|
results.push(
|
|
@@ -405,16 +618,17 @@ export function registerDoctorCommand(program) {
|
|
|
405
618
|
}
|
|
406
619
|
for (const envName of config.deploy || []) {
|
|
407
620
|
const env = config.environments[envName];
|
|
408
|
-
if (!env) continue;
|
|
409
|
-
|
|
410
|
-
if (env.type) {
|
|
411
|
-
const keys = PROVIDER_ENV_MAP[env.type] || [];
|
|
412
|
-
required.push(...keys);
|
|
413
|
-
}
|
|
621
|
+
if (!env?.type) continue;
|
|
622
|
+
required.push(...getDeploymentSecretKeys(env.type, config));
|
|
414
623
|
}
|
|
415
624
|
|
|
416
625
|
const unique = [...new Set(required)];
|
|
417
|
-
const missing = unique.filter((k) =>
|
|
626
|
+
const missing = unique.filter((k) => {
|
|
627
|
+
if (k === 'SSH_KEY') {
|
|
628
|
+
return !process.env.SSH_KEY && !process.env.SSH_KEY_PATH;
|
|
629
|
+
}
|
|
630
|
+
return !process.env[k];
|
|
631
|
+
});
|
|
418
632
|
if (missing.length > 0) {
|
|
419
633
|
return {
|
|
420
634
|
name: 'Secrets',
|