@akash-chowdhury-24/deployhub 2.0.26 → 2.0.28
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 +38 -20
- package/package.json +1 -1
- package/src/adapters/index.js +1 -0
- package/src/artifact/engine.js +2 -2
- package/src/commands/doctor.js +8 -4
- package/src/commands/init.js +5 -0
- package/src/core/config.js +7 -0
- package/src/deployment/deployment-env.js +115 -41
- package/src/deployment/init-prompts.js +52 -7
- package/src/deployment/providers/azure-vm.js +6 -4
- package/src/deployment/providers/ec2.js +8 -3
- package/src/deployment/providers/gcp-vm.js +4 -4
- package/src/deployment/providers/ssh.js +2 -2
- package/src/detectors/backend.detector.js +107 -2
- package/src/detectors/index.js +3 -2
- package/src/utils/credential-inventory.js +292 -0
- package/src/utils/docker-image.js +6 -3
- package/src/utils/dockerfile-expose.js +1 -0
- package/src/utils/dockerfile.js +181 -28
- package/src/utils/github-actions.js +194 -17
package/README.md
CHANGED
|
@@ -651,6 +651,7 @@ All JS frontends share the same install/build flow: `npm ci` → `npm run build`
|
|
|
651
651
|
|
|
652
652
|
- **Detect:** `composer.json` with `laravel/framework` or `symfony/framework-bundle`.
|
|
653
653
|
- **Install:** Composer (on CI and server).
|
|
654
|
+
- **CI runtime:** Generated workflows install PHP via `shivammathur/setup-php@v2` with Composer. Default version is **8.4** (meets current Laravel platform requirements). Override with `"phpVersion": "8.3"` at the config root or under `"backend"` in `deployhub.config.json`, then run `deployhub sync-workflows`.
|
|
654
655
|
- **Deploy:** SSH with PHP-FPM or `php artisan` for Laravel.
|
|
655
656
|
|
|
656
657
|
> ⚠️ **PHP-FPM deployments restart the FPM service for the ENTIRE host on every deploy.** If you run multiple DeployHub-managed environments on the same server, deploying ANY of them will briefly interrupt in-flight requests for ALL of them. For production use with multiple environments, either use separate hosts per environment, or set up per-environment PHP-FPM pools manually (not yet automated by DeployHub).
|
|
@@ -695,7 +696,9 @@ git commit -m "Add DeployHub CI"
|
|
|
695
696
|
2. Add every secret listed at the end of `deployhub init` (storage + deployment).
|
|
696
697
|
3. Push to `main` or `master` — the deploy workflow triggers on push.
|
|
697
698
|
|
|
698
|
-
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.
|
|
699
|
+
The deploy workflow (`deployhub.yml`) installs the correct language runtime (Node, Python, PHP, Java, Go, .NET, Ruby) based on your `deployhub.config.json`, installs DeployHub, runs `deployhub build`, and uses your secrets. For PHP projects, CI uses `shivammathur/setup-php` (default **8.4**; override with `phpVersion` / `backend.phpVersion`).
|
|
700
|
+
|
|
701
|
+
When an environment uses Kubernetes, the workflow installs `kubectl` and writes kubeconfig from secrets — but only when that run actually needs cluster access (push with a push-triggered k8s env, or workflow_dispatch / rollback targeting a k8s env, `all`, or blank). Plain pushes that only auto-deploy non-k8s environments (e.g. EC2 development) skip those steps.
|
|
699
702
|
|
|
700
703
|
To run a deploy manually: **Actions → DeployHub → Run workflow**.
|
|
701
704
|
|
|
@@ -883,13 +886,13 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
883
886
|
- Nginx layout detection and config test before reload (frontend)
|
|
884
887
|
- Passwordless sudo and Nginx checks during `deployhub doctor` (frontend)
|
|
885
888
|
- OS user suggestion from AMI hint (ubuntu, ec2-user)
|
|
886
|
-
- Optional public IP lookup via `EC2_INSTANCE_ID` + AWS CLI
|
|
889
|
+
- Optional public IP lookup via `EC2_INSTANCE_ID` + `EC2_LOOKUP_AWS_*` (AWS CLI)
|
|
887
890
|
|
|
888
891
|
**After `init`:**
|
|
889
892
|
1. AWS Console → EC2 → Security Groups → Inbound rules → SSH port 22 from My IP
|
|
890
893
|
2. Ensure your deploy directory exists and is owned by your SSH user (see prerequisite above if `deployhub doctor` reports permission denied)
|
|
891
|
-
3. Copy `.env.example` → `.env`; set `SSH_KEY_PATH`, `SSH_HOST` (or `EC2_INSTANCE_ID` +
|
|
892
|
-
4. GitHub Secrets: `SSH_HOST`, `SSH_USER`, `SSH_KEY`, plus `
|
|
894
|
+
3. Copy `.env.example` → `.env`; set `SSH_KEY_PATH`, `SSH_HOST` (or `EC2_INSTANCE_ID` + `EC2_LOOKUP_AWS_*`)
|
|
895
|
+
4. GitHub Secrets: `SSH_HOST`, `SSH_USER`, `SSH_KEY`, plus `EC2_LOOKUP_AWS_*` if using instance ID lookup (these are **not** the same secrets as AWS S3 storage)
|
|
893
896
|
5. Run `deployhub doctor`, then `git push origin main`
|
|
894
897
|
|
|
895
898
|
| Variable | Description | Example | Where to get it |
|
|
@@ -898,9 +901,9 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
898
901
|
| `SSH_USER` | SSH user for AMI | `ec2-user` | AMI documentation |
|
|
899
902
|
| `SSH_KEY_PATH` | Path to .pem key | `~/.ssh/ec2-key.pem` | Downloaded at instance launch |
|
|
900
903
|
| `EC2_INSTANCE_ID` | Instance ID (optional) | `i-0abc123...` | EC2 Console |
|
|
901
|
-
| `
|
|
902
|
-
| `
|
|
903
|
-
| `
|
|
904
|
+
| `EC2_LOOKUP_AWS_ACCESS_KEY_ID` | AWS key for EC2 IP lookup (distinct from S3) | `AKIA...` | IAM → Users → Security credentials |
|
|
905
|
+
| `EC2_LOOKUP_AWS_SECRET_ACCESS_KEY` | AWS secret for EC2 IP lookup | *(secret)* | Same as above |
|
|
906
|
+
| `EC2_LOOKUP_AWS_REGION` | Instance region for DescribeInstances | `us-east-1` | EC2 Console top bar |
|
|
904
907
|
|
|
905
908
|
### Azure VM
|
|
906
909
|
|
|
@@ -926,7 +929,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
926
929
|
1. Azure Portal → VM → Networking → allow SSH (22) from your IP
|
|
927
930
|
2. Ensure your deploy directory exists and is owned by your SSH user (see prerequisite above if `deployhub doctor` reports permission denied)
|
|
928
931
|
3. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
|
|
929
|
-
4. For CI: add `
|
|
932
|
+
4. For CI: add `AZURE_VM_LOOKUP_TENANT_ID`, `AZURE_VM_LOOKUP_CLIENT_ID`, `AZURE_VM_LOOKUP_CLIENT_SECRET` as GitHub Secrets
|
|
930
933
|
5. Run `deployhub doctor`, then `git push origin main`
|
|
931
934
|
|
|
932
935
|
| Variable | Description | Example | Where to get it |
|
|
@@ -934,9 +937,9 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
934
937
|
| `SSH_HOST` | VM public IP | `20.1.2.3` | Azure Portal → VM overview |
|
|
935
938
|
| `SSH_USER` | SSH username | `azureuser` | Chosen at VM creation |
|
|
936
939
|
| `SSH_KEY_PATH` | Private key path | `~/.ssh/azure.pem` | Your key file |
|
|
937
|
-
| `
|
|
938
|
-
| `
|
|
939
|
-
| `
|
|
940
|
+
| `AZURE_VM_LOOKUP_SUBSCRIPTION_ID` | Subscription (optional IP lookup) | `uuid` | `az account show` |
|
|
941
|
+
| `AZURE_VM_LOOKUP_RESOURCE_GROUP` | Resource group | `my-app-rg` | Portal → Resource groups |
|
|
942
|
+
| `AZURE_VM_LOOKUP_VM_NAME` | VM name | `my-vm` | Portal → Virtual machines |
|
|
940
943
|
|
|
941
944
|
### GCP VM
|
|
942
945
|
|
|
@@ -975,10 +978,10 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
975
978
|
| `SSH_APP_NAME` | PM2 process name (backend) | `my-api` | Your choice |
|
|
976
979
|
| `SSH_PORT` | App listen port (backend) | `3000` | Your app config |
|
|
977
980
|
| `SSH_KEY` | Private key contents (CI only) | `-----BEGIN...` | Same key as `SSH_KEY_PATH` |
|
|
978
|
-
| `
|
|
981
|
+
| `GCP_VM_LOOKUP_PROJECT_ID` | Project ID for VM IP lookup (distinct from GCP Storage) | `my-project-123` | `gcloud config get-value project` |
|
|
979
982
|
| `GCP_ZONE` | VM zone (optional) | `us-central1-a` | VM instance details |
|
|
980
983
|
| `GCP_INSTANCE_NAME` | Instance name (optional) | `my-vm` | Compute Engine list |
|
|
981
|
-
| `
|
|
984
|
+
| `GCP_VM_LOOKUP_KEY_FILE` | Service account JSON for VM lookup (distinct from GCP Storage) | `/path/to/key.json` | IAM → Service Accounts → Keys |
|
|
982
985
|
|
|
983
986
|
### Kubernetes
|
|
984
987
|
|
|
@@ -993,7 +996,7 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
993
996
|
**Init prompts (what `deployhub init` asks for Kubernetes):**
|
|
994
997
|
1. Path to kubeconfig file (e.g. `~/.kube/config`)
|
|
995
998
|
2. Kubernetes context
|
|
996
|
-
3. Namespace (defaults to project name)
|
|
999
|
+
3. Namespace (defaults to project name for the first env; additional envs suggest `{project}-{envName}` to avoid collisions)
|
|
997
1000
|
4. Container image name
|
|
998
1001
|
5. Registry URL (leave empty for Docker Hub)
|
|
999
1002
|
6. Registry username (required to push)
|
|
@@ -1002,13 +1005,15 @@ DeployHub detects whether the server uses Debian-style `sites-available` or RHEL
|
|
|
1002
1005
|
|
|
1003
1006
|
**What DeployHub automates:**
|
|
1004
1007
|
- Starter `k8s/deployment.yaml` and `k8s/service.yaml` when no manifests exist (skipped if you already have a `k8s/` directory or root-level Kubernetes YAML files)
|
|
1005
|
-
- GitHub Actions installs `kubectl` on the CI runner and writes kubeconfig from secrets
|
|
1008
|
+
- GitHub Actions installs `kubectl` on the CI runner and writes kubeconfig from secrets when the run targets a Kubernetes environment (skipped on push when k8s is manual-only)
|
|
1006
1009
|
- Lists `kubectl` contexts during `init` for easy selection
|
|
1007
1010
|
- Auto-detects `~/.kube/config`
|
|
1008
1011
|
- Complete `.env.example` for kubeconfig, context, namespace, and registry settings
|
|
1009
1012
|
- Cluster connectivity test during `init`
|
|
1010
1013
|
- On deploy: registry login → reuse or build image → push (unique tag unless `DOCKER_IMAGE_TAG` is set) → ensure namespace exists (prompt locally / auto-create in CI) → `kubectl apply` → `kubectl set image` with the full resolved image ref → `kubectl rollout restart` when that ref is unchanged so pods pick up a new digest
|
|
1011
1014
|
|
|
1015
|
+
> **Limitation — interpreted backends (Node / Python / PHP / Rails) + rollback:** Kubernetes rollback forces a rebuild from the restored artifact when the exact `buildId` image is not already local. Those artifacts ship source/`composer.json` (etc.) but **not** installed deps (`vendor/`, `node_modules`, …), so DeployHub **refuses** with a clear error instead of a confusing Docker build failure. Successful rollback needs the restored image tag already present locally (or a prior `pipeline.docker` build that produced it). Same rule as standalone Docker deploy fallback.
|
|
1016
|
+
|
|
1012
1017
|
> **Limitation — multiple Kubernetes clusters:** A generated workflow writes **one** kubeconfig file per job. Multiple Kubernetes environments that target **different clusters** in the same workflow run are not yet fully supported (follow-up). Same-cluster multi-namespace / multi-env is fine.
|
|
1013
1018
|
|
|
1014
1019
|
**After `init`:**
|
|
@@ -1140,7 +1145,20 @@ Run `deployhub doctor` after any config change.
|
|
|
1140
1145
|
| `deployhub clean` | Remove old local artifacts |
|
|
1141
1146
|
| `deployhub update` | Check for CLI updates |
|
|
1142
1147
|
|
|
1143
|
-
**Tests:** `npm test` — currently **
|
|
1148
|
+
**Tests:** `npm test` — currently **442 passing** across the Jest suites (1 skipped).
|
|
1149
|
+
|
|
1150
|
+
## Storage vs deployment lookup credentials
|
|
1151
|
+
|
|
1152
|
+
Storage credentials (`AWS_*` for S3, `GCP_*` for GCP Storage, …) are **project-wide and unprefixed**. Optional cloud-API credentials used only for **dynamic VM IP lookup** on EC2 / Azure VM / GCP VM use a distinct `{METHOD}_LOOKUP_…` namespace so they never collide with storage — even on a single-environment project.
|
|
1153
|
+
|
|
1154
|
+
| Concern | Examples |
|
|
1155
|
+
|---------|----------|
|
|
1156
|
+
| Storage | `AWS_ACCESS_KEY_ID`, `GCP_PROJECT_ID`, `GCP_KEY_FILE` |
|
|
1157
|
+
| EC2 instance-IP lookup | `EC2_LOOKUP_AWS_ACCESS_KEY_ID`, `EC2_LOOKUP_AWS_SECRET_ACCESS_KEY`, `EC2_LOOKUP_AWS_REGION` |
|
|
1158
|
+
| GCP VM IP lookup | `GCP_VM_LOOKUP_PROJECT_ID`, `GCP_VM_LOOKUP_KEY_FILE` |
|
|
1159
|
+
| Azure VM IP lookup | `AZURE_VM_LOOKUP_SUBSCRIPTION_ID`, `AZURE_VM_LOOKUP_RESOURCE_GROUP`, `AZURE_VM_LOOKUP_VM_NAME`, … |
|
|
1160
|
+
|
|
1161
|
+
Multi-env secret prefixing still applies on top (e.g. `PRODUCTION_EC2_LOOKUP_AWS_ACCESS_KEY_ID`).
|
|
1144
1162
|
|
|
1145
1163
|
## GitHub Secrets
|
|
1146
1164
|
|
|
@@ -1164,7 +1182,7 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
|
|
|
1164
1182
|
| `GCP_KEY_FILE` | GCP Storage |
|
|
1165
1183
|
| `GCP_BUCKET` | GCP Storage |
|
|
1166
1184
|
| `DROPBOX_ACCESS_TOKEN` | Dropbox |
|
|
1167
|
-
| `FTP_HOST`, `FTP_USER`, `FTP_PASSWORD` | FTP storage |
|
|
1185
|
+
| `FTP_HOST`, `FTP_USER`, `FTP_PASSWORD`, `FTP_PORT`, `FTP_PATH` | FTP storage (`FTP_PORT` default 21, `FTP_PATH` default `/uploads` — connection tuning, not secrets) |
|
|
1168
1186
|
|
|
1169
1187
|
### Server deployment (SSH, EC2, VMs, Docker, Kubernetes)
|
|
1170
1188
|
|
|
@@ -1178,9 +1196,9 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
|
|
|
1178
1196
|
| `SSH_DEPLOY_PATH` | Remote directory (optional if set in config) |
|
|
1179
1197
|
| `SSH_APP_NAME` | PM2 process name for backends |
|
|
1180
1198
|
| `SSH_PORT` | App port on server (backend) |
|
|
1181
|
-
| `EC2_INSTANCE_ID`, `
|
|
1182
|
-
| `
|
|
1183
|
-
| `
|
|
1199
|
+
| `EC2_INSTANCE_ID`, `EC2_LOOKUP_AWS_*` | Optional EC2 dynamic IP lookup (distinct from S3 `AWS_*`) |
|
|
1200
|
+
| `AZURE_VM_LOOKUP_SUBSCRIPTION_ID`, `AZURE_VM_LOOKUP_RESOURCE_GROUP`, `AZURE_VM_LOOKUP_VM_NAME` | Optional Azure VM IP lookup |
|
|
1201
|
+
| `GCP_VM_LOOKUP_PROJECT_ID`, `GCP_ZONE`, `GCP_INSTANCE_NAME`, `GCP_VM_LOOKUP_KEY_FILE` | Optional GCP VM IP lookup (project/key distinct from GCP Storage) |
|
|
1184
1202
|
| `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_USERNAME`, `DOCKER_REGISTRY_TOKEN`, `DOCKER_REGISTRY_URL`, `DOCKER_HOST` | Docker deployment (`DOCKER_IMAGE_TAG` optional) |
|
|
1185
1203
|
| `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 |
|
|
1186
1204
|
|
package/package.json
CHANGED
package/src/adapters/index.js
CHANGED
package/src/artifact/engine.js
CHANGED
|
@@ -241,7 +241,7 @@ async function stageBackendArtifact(cwd, stagingDir, config) {
|
|
|
241
241
|
await copyDirectoryIfExists(cwd, stagingDir, 'resources');
|
|
242
242
|
await copyDirectoryIfExists(cwd, stagingDir, 'storage');
|
|
243
243
|
await copyDirectoryIfExists(cwd, stagingDir, 'bin');
|
|
244
|
-
} else if (framework === 'spring') {
|
|
244
|
+
} else if (framework === 'spring' || framework === 'java') {
|
|
245
245
|
await copyIfExists(cwd, stagingDir, 'pom.xml');
|
|
246
246
|
const targetDir = path.join(cwd, 'target');
|
|
247
247
|
if (await fs.pathExists(targetDir)) {
|
|
@@ -261,7 +261,7 @@ async function stageBackendArtifact(cwd, stagingDir, config) {
|
|
|
261
261
|
await copyIfExists(cwd, stagingDir, f);
|
|
262
262
|
}
|
|
263
263
|
await copyDirectoryIfExists(cwd, stagingDir, settings.buildOutput || 'publish');
|
|
264
|
-
} else if (framework === 'rails') {
|
|
264
|
+
} else if (framework === 'rails' || framework === 'ruby') {
|
|
265
265
|
await copyIfExists(cwd, stagingDir, 'Gemfile');
|
|
266
266
|
await copyIfExists(cwd, stagingDir, 'Gemfile.lock');
|
|
267
267
|
await copyIfExists(cwd, stagingDir, 'config.ru');
|
package/src/commands/doctor.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { loadEnvArtifactHistory } from '../storage/index.js';
|
|
14
14
|
import { testProvider } from '../storage/index.js';
|
|
15
15
|
import { getDeploymentProvider } from '../deployment/index.js';
|
|
16
|
-
import { PROVIDER_ENV_MAP, getRollbackWorkflowDoctorCheck, getWorkflowDriftDoctorChecks } from '../utils/github-actions.js';
|
|
16
|
+
import { PROVIDER_ENV_MAP, STORAGE_PROVIDER_IDS, getRollbackWorkflowDoctorCheck, getWorkflowDriftDoctorChecks } from '../utils/github-actions.js';
|
|
17
17
|
import { printDoctorFooter } from '../utils/author.js';
|
|
18
18
|
import { createLocalProvider } from '../storage/providers/local.js';
|
|
19
19
|
import {
|
|
@@ -561,9 +561,12 @@ export async function runDeploymentChecks(config, envName, envConfig) {
|
|
|
561
561
|
if (deployType === 'ec2' && process.env.EC2_INSTANCE_ID) {
|
|
562
562
|
checks.push(
|
|
563
563
|
await runCheck('EC2 API', async () => {
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
564
|
+
const required = [
|
|
565
|
+
'EC2_LOOKUP_AWS_ACCESS_KEY_ID',
|
|
566
|
+
'EC2_LOOKUP_AWS_SECRET_ACCESS_KEY',
|
|
567
|
+
'EC2_LOOKUP_AWS_REGION',
|
|
568
|
+
];
|
|
569
|
+
const missing = required.filter((k) => !process.env[k]);
|
|
567
570
|
if (missing.length > 0) {
|
|
568
571
|
return {
|
|
569
572
|
name: 'EC2 API',
|
|
@@ -969,6 +972,7 @@ export function registerDoctorCommand(program) {
|
|
|
969
972
|
/** @type {string[]} */
|
|
970
973
|
const required = [];
|
|
971
974
|
for (const provider of config.storage || []) {
|
|
975
|
+
if (!STORAGE_PROVIDER_IDS.has(provider)) continue;
|
|
972
976
|
const keys = PROVIDER_ENV_MAP[provider] || [];
|
|
973
977
|
required.push(...keys);
|
|
974
978
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -54,15 +54,20 @@ const BACKEND_CHOICES = [
|
|
|
54
54
|
{ name: 'Node.js — NestJS', value: 'nestjs' },
|
|
55
55
|
{ name: 'Node.js — Fastify', value: 'fastify' },
|
|
56
56
|
{ name: 'Node.js — Koa', value: 'koa' },
|
|
57
|
+
{ name: 'Node.js — plain', value: 'node' },
|
|
57
58
|
{ name: 'Python — FastAPI', value: 'fastapi' },
|
|
58
59
|
{ name: 'Python — Django', value: 'django' },
|
|
59
60
|
{ name: 'Python — Flask', value: 'flask' },
|
|
61
|
+
{ name: 'Python — plain', value: 'python' },
|
|
60
62
|
{ name: 'PHP — Laravel', value: 'laravel' },
|
|
61
63
|
{ name: 'PHP — Symfony', value: 'symfony' },
|
|
64
|
+
{ name: 'PHP — plain (Composer / .php)', value: 'php' },
|
|
62
65
|
{ name: 'Java — Spring Boot', value: 'spring' },
|
|
66
|
+
{ name: 'Java — plain (Maven/Gradle)', value: 'java' },
|
|
63
67
|
{ name: 'Go', value: 'go' },
|
|
64
68
|
{ name: '.NET (ASP.NET Core)', value: 'dotnet' },
|
|
65
69
|
{ name: 'Ruby on Rails', value: 'rails' },
|
|
70
|
+
{ name: 'Ruby — plain (Rack/Puma)', value: 'ruby' },
|
|
66
71
|
{ name: "Other (I'll configure manually)", value: 'other' },
|
|
67
72
|
];
|
|
68
73
|
|
package/src/core/config.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
const SideConfigSchema = z.object({
|
|
21
21
|
framework: z.string(),
|
|
22
22
|
language: z.string().optional(),
|
|
23
|
+
/** PHP runtime for CI (`shivammathur/setup-php`); e.g. `"8.4"`. */
|
|
24
|
+
phpVersion: z.string().optional(),
|
|
23
25
|
buildCommand: z.string().nullable().optional(),
|
|
24
26
|
startCommand: z.string().nullable().optional(),
|
|
25
27
|
buildOutput: z.string().optional(),
|
|
@@ -81,6 +83,11 @@ const ConfigSchema = z.object({
|
|
|
81
83
|
projectType: z.enum(['frontend', 'backend', 'both']).default('frontend'),
|
|
82
84
|
framework: z.string().optional(),
|
|
83
85
|
language: z.string().optional(),
|
|
86
|
+
/**
|
|
87
|
+
* PHP runtime for generated GitHub Actions (`shivammathur/setup-php`).
|
|
88
|
+
* Prefer `backend.phpVersion` for backend/both projects. Default when unset: `8.4`.
|
|
89
|
+
*/
|
|
90
|
+
phpVersion: z.string().optional(),
|
|
84
91
|
buildCommand: z.string().nullable().optional(),
|
|
85
92
|
startCommand: z.string().nullable().optional(),
|
|
86
93
|
buildOutput: z.string().optional(),
|
|
@@ -169,27 +169,32 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
169
169
|
when: 'optional',
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
|
-
key: '
|
|
172
|
+
key: 'EC2_LOOKUP_AWS_ACCESS_KEY_ID',
|
|
173
173
|
optionalReason:
|
|
174
174
|
'only required if using EC2_INSTANCE_ID for dynamic IP lookup; otherwise leave blank',
|
|
175
175
|
comment: [
|
|
176
|
+
'EC2 instance-IP lookup credential (NOT the same as AWS S3 storage credentials).',
|
|
176
177
|
'AWS access key with ec2:DescribeInstances permission.',
|
|
177
178
|
'Create in AWS Console → IAM → Users → Security credentials.',
|
|
178
179
|
],
|
|
179
180
|
when: 'optional',
|
|
180
181
|
},
|
|
181
182
|
{
|
|
182
|
-
key: '
|
|
183
|
+
key: 'EC2_LOOKUP_AWS_SECRET_ACCESS_KEY',
|
|
183
184
|
optionalReason:
|
|
184
185
|
'only required if using EC2_INSTANCE_ID for dynamic IP lookup; otherwise leave blank',
|
|
185
|
-
comment: [
|
|
186
|
+
comment: [
|
|
187
|
+
'Secret for EC2_LOOKUP_AWS_ACCESS_KEY_ID (distinct from AWS_SECRET_ACCESS_KEY used by S3).',
|
|
188
|
+
],
|
|
186
189
|
when: 'optional',
|
|
187
190
|
},
|
|
188
191
|
{
|
|
189
|
-
key: '
|
|
192
|
+
key: 'EC2_LOOKUP_AWS_REGION',
|
|
190
193
|
optionalReason:
|
|
191
194
|
'only required if using EC2_INSTANCE_ID for dynamic IP lookup; otherwise leave blank',
|
|
192
|
-
comment: [
|
|
195
|
+
comment: [
|
|
196
|
+
'AWS region for EC2 DescribeInstances lookup (distinct from AWS_REGION used by S3).',
|
|
197
|
+
],
|
|
193
198
|
example: 'us-east-1',
|
|
194
199
|
when: 'optional',
|
|
195
200
|
},
|
|
@@ -199,52 +204,60 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
199
204
|
...SSH_BACKEND_ENV_VARS,
|
|
200
205
|
...SSH_CI_ENV_VARS,
|
|
201
206
|
{
|
|
202
|
-
key: '
|
|
207
|
+
key: 'AZURE_VM_LOOKUP_SUBSCRIPTION_ID',
|
|
203
208
|
optionalReason:
|
|
204
209
|
'only required for dynamic VM IP lookup via Azure API; otherwise set SSH_HOST directly',
|
|
205
210
|
comment: [
|
|
206
|
-
'Azure subscription ID
|
|
211
|
+
'Azure VM IP-lookup subscription ID (deployment-side; not Azure Blob storage).',
|
|
207
212
|
'Find in Azure Portal → Subscriptions, or run: az account show --query id -o tsv',
|
|
208
213
|
],
|
|
209
214
|
when: 'optional',
|
|
210
215
|
},
|
|
211
216
|
{
|
|
212
|
-
key: '
|
|
217
|
+
key: 'AZURE_VM_LOOKUP_RESOURCE_GROUP',
|
|
213
218
|
optionalReason:
|
|
214
219
|
'only required for dynamic VM IP lookup via Azure API; otherwise leave blank',
|
|
215
|
-
comment: [
|
|
220
|
+
comment: [
|
|
221
|
+
'Resource group containing your VM (deployment lookup; not Azure Blob storage).',
|
|
222
|
+
],
|
|
216
223
|
example: 'my-app-rg',
|
|
217
224
|
when: 'optional',
|
|
218
225
|
},
|
|
219
226
|
{
|
|
220
|
-
key: '
|
|
227
|
+
key: 'AZURE_VM_LOOKUP_VM_NAME',
|
|
221
228
|
optionalReason:
|
|
222
229
|
'only required for dynamic VM IP lookup via Azure API; otherwise set SSH_HOST directly',
|
|
223
230
|
comment: [
|
|
224
|
-
'Name of the Azure virtual machine.',
|
|
231
|
+
'Name of the Azure virtual machine for IP lookup.',
|
|
225
232
|
'If unset, SSH_HOST must be set to the VM public IP or DNS.',
|
|
226
233
|
],
|
|
227
234
|
when: 'optional',
|
|
228
235
|
},
|
|
229
236
|
{
|
|
230
|
-
key: '
|
|
237
|
+
key: 'AZURE_VM_LOOKUP_TENANT_ID',
|
|
231
238
|
optionalReason:
|
|
232
239
|
'only required for non-interactive CI deploys (GitHub Actions) using a service principal',
|
|
233
|
-
comment: [
|
|
240
|
+
comment: [
|
|
241
|
+
'Azure AD tenant ID for service principal auth in CI (VM deploy lookup).',
|
|
242
|
+
],
|
|
234
243
|
when: 'optional',
|
|
235
244
|
},
|
|
236
245
|
{
|
|
237
|
-
key: '
|
|
246
|
+
key: 'AZURE_VM_LOOKUP_CLIENT_ID',
|
|
238
247
|
optionalReason:
|
|
239
248
|
'only required for non-interactive CI deploys (GitHub Actions) using a service principal',
|
|
240
|
-
comment: [
|
|
249
|
+
comment: [
|
|
250
|
+
'Service principal application (client) ID for CI auth (VM deploy lookup).',
|
|
251
|
+
],
|
|
241
252
|
when: 'optional',
|
|
242
253
|
},
|
|
243
254
|
{
|
|
244
|
-
key: '
|
|
255
|
+
key: 'AZURE_VM_LOOKUP_CLIENT_SECRET',
|
|
245
256
|
optionalReason:
|
|
246
257
|
'only required for non-interactive CI deploys (GitHub Actions) using a service principal',
|
|
247
|
-
comment: [
|
|
258
|
+
comment: [
|
|
259
|
+
'Service principal client secret for CI auth (VM deploy lookup).',
|
|
260
|
+
],
|
|
248
261
|
when: 'optional',
|
|
249
262
|
},
|
|
250
263
|
],
|
|
@@ -253,11 +266,11 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
253
266
|
...SSH_BACKEND_ENV_VARS,
|
|
254
267
|
...SSH_CI_ENV_VARS,
|
|
255
268
|
{
|
|
256
|
-
key: '
|
|
269
|
+
key: 'GCP_VM_LOOKUP_PROJECT_ID',
|
|
257
270
|
optionalReason:
|
|
258
271
|
'only required for dynamic VM IP lookup via GCP API; otherwise set SSH_HOST directly',
|
|
259
272
|
comment: [
|
|
260
|
-
'GCP project ID
|
|
273
|
+
'GCP project ID for Compute Engine IP lookup (NOT the same as GCP Storage GCP_PROJECT_ID).',
|
|
261
274
|
'Find in GCP Console → Dashboard, or run: gcloud config get-value project',
|
|
262
275
|
],
|
|
263
276
|
when: 'optional',
|
|
@@ -282,11 +295,11 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
282
295
|
when: 'optional',
|
|
283
296
|
},
|
|
284
297
|
{
|
|
285
|
-
key: '
|
|
298
|
+
key: 'GCP_VM_LOOKUP_KEY_FILE',
|
|
286
299
|
optionalReason:
|
|
287
300
|
'only required for dynamic VM IP lookup via GCP API or non-interactive CI auth; otherwise leave blank',
|
|
288
301
|
comment: [
|
|
289
|
-
'Path to a GCP service account JSON key
|
|
302
|
+
'Path to a GCP service account JSON key for VM IP lookup (distinct from GCP Storage GCP_KEY_FILE).',
|
|
290
303
|
'Create in GCP Console → IAM → Service Accounts → Keys.',
|
|
291
304
|
],
|
|
292
305
|
when: 'optional',
|
|
@@ -381,6 +394,27 @@ export const DEPLOYMENT_ENV_KEYS = Object.fromEntries(
|
|
|
381
394
|
])
|
|
382
395
|
);
|
|
383
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Deployment-side cloud-API lookup credentials — distinct from storage-provider
|
|
399
|
+
* env vars (storage stays project-wide / unprefixed).
|
|
400
|
+
*
|
|
401
|
+
* Convention: `{METHOD}_LOOKUP_…` — method-scoped, purpose-clear, layers under
|
|
402
|
+
* the existing multi-env prefixing system (e.g. PRODUCTION_EC2_LOOKUP_AWS_ACCESS_KEY_ID).
|
|
403
|
+
*/
|
|
404
|
+
export const DEPLOYMENT_LOOKUP_ENV_KEYS = new Set([
|
|
405
|
+
'EC2_LOOKUP_AWS_ACCESS_KEY_ID',
|
|
406
|
+
'EC2_LOOKUP_AWS_SECRET_ACCESS_KEY',
|
|
407
|
+
'EC2_LOOKUP_AWS_REGION',
|
|
408
|
+
'AZURE_VM_LOOKUP_SUBSCRIPTION_ID',
|
|
409
|
+
'AZURE_VM_LOOKUP_RESOURCE_GROUP',
|
|
410
|
+
'AZURE_VM_LOOKUP_VM_NAME',
|
|
411
|
+
'AZURE_VM_LOOKUP_TENANT_ID',
|
|
412
|
+
'AZURE_VM_LOOKUP_CLIENT_ID',
|
|
413
|
+
'AZURE_VM_LOOKUP_CLIENT_SECRET',
|
|
414
|
+
'GCP_VM_LOOKUP_PROJECT_ID',
|
|
415
|
+
'GCP_VM_LOOKUP_KEY_FILE',
|
|
416
|
+
]);
|
|
417
|
+
|
|
384
418
|
/**
|
|
385
419
|
* Locally required env keys for doctor method-specific checks.
|
|
386
420
|
* Excludes optional and CI-only vars.
|
|
@@ -482,13 +516,25 @@ export function getDeploymentSecretChecklistItems(deployType, config = null) {
|
|
|
482
516
|
|
|
483
517
|
const key = toGithubSecretKey(d.key);
|
|
484
518
|
const required = d.when !== 'optional';
|
|
485
|
-
|
|
519
|
+
let note =
|
|
486
520
|
d.when === 'optional'
|
|
487
521
|
? d.optionalReason
|
|
488
522
|
: d.when === 'ci'
|
|
489
523
|
? d.optionalReason || 'required for GitHub Actions CI (paste private key contents)'
|
|
490
524
|
: undefined;
|
|
491
525
|
|
|
526
|
+
if (DEPLOYMENT_LOOKUP_ENV_KEYS.has(d.key)) {
|
|
527
|
+
const purpose =
|
|
528
|
+
deployType === 'ec2'
|
|
529
|
+
? 'EC2 instance-IP lookup credential (distinct from AWS S3 storage)'
|
|
530
|
+
: deployType === 'gcp-vm'
|
|
531
|
+
? 'GCP VM instance-IP lookup credential (distinct from GCP Storage)'
|
|
532
|
+
: deployType === 'azure-vm'
|
|
533
|
+
? 'Azure VM IP-lookup credential (distinct from Azure Blob storage)'
|
|
534
|
+
: 'deployment cloud-API lookup credential';
|
|
535
|
+
note = note ? `${purpose}; ${note}` : purpose;
|
|
536
|
+
}
|
|
537
|
+
|
|
492
538
|
const existing = byKey.get(key);
|
|
493
539
|
if (existing) {
|
|
494
540
|
// Prefer required if any def for this key is required
|
|
@@ -731,21 +777,48 @@ export function formatSecretChecklistLine(item) {
|
|
|
731
777
|
* @param {string} deployType
|
|
732
778
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
733
779
|
* @param {Record<string, Record<string, unknown>>} [environments]
|
|
780
|
+
* @param {{ envName?: string }} [options]
|
|
734
781
|
* @returns {string}
|
|
735
782
|
*/
|
|
736
783
|
export function generateDeploymentEnvSection(
|
|
737
784
|
deployType,
|
|
738
785
|
config = null,
|
|
739
|
-
environments = {}
|
|
786
|
+
environments = {},
|
|
787
|
+
options = {}
|
|
740
788
|
) {
|
|
741
789
|
const defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
|
|
742
790
|
const projectType = config?.projectType || 'frontend';
|
|
743
791
|
const isBackend = projectType === 'backend' || projectType === 'both';
|
|
792
|
+
const envName = options.envName;
|
|
793
|
+
const cfg = {
|
|
794
|
+
...(config || {}),
|
|
795
|
+
environments: environments || config?.environments || {},
|
|
796
|
+
};
|
|
797
|
+
const shouldPrefix =
|
|
798
|
+
!!envName && envUsesPrefixedSecrets(envName, /** @type {any} */ (cfg));
|
|
744
799
|
|
|
745
|
-
const
|
|
800
|
+
const baseTitle = DEPLOYMENT_SECTION_TITLES[deployType] || deployType;
|
|
801
|
+
const title =
|
|
802
|
+
envName && Object.keys(environments || {}).length > 1
|
|
803
|
+
? `${baseTitle} (${envName})`
|
|
804
|
+
: baseTitle;
|
|
746
805
|
/** @type {string[]} */
|
|
747
806
|
const lines = [`# ${title}`];
|
|
748
807
|
|
|
808
|
+
// Prefer defaults from this environment's config when generating per-env sections.
|
|
809
|
+
const envEntry = envName
|
|
810
|
+
? /** @type {Record<string, unknown>} */ (
|
|
811
|
+
(environments || {})[envName] || {}
|
|
812
|
+
)
|
|
813
|
+
: /** @type {Record<string, unknown>} */ (
|
|
814
|
+
Object.values(environments || {})[0] || {}
|
|
815
|
+
);
|
|
816
|
+
// Support both new { config: {...} } shape and flat legacy env entries.
|
|
817
|
+
const settings =
|
|
818
|
+
envEntry && typeof envEntry.config === 'object' && envEntry.config
|
|
819
|
+
? /** @type {Record<string, unknown>} */ (envEntry.config)
|
|
820
|
+
: envEntry;
|
|
821
|
+
|
|
749
822
|
for (const d of defs) {
|
|
750
823
|
if (d.when === 'backend' && !isBackend) continue;
|
|
751
824
|
|
|
@@ -762,8 +835,10 @@ export function generateDeploymentEnvSection(
|
|
|
762
835
|
lines.push(`# Example: ${d.example}`);
|
|
763
836
|
}
|
|
764
837
|
|
|
765
|
-
const
|
|
766
|
-
|
|
838
|
+
const key = shouldPrefix ? prefixSecretKey(envName, d.key) : d.key;
|
|
839
|
+
const defaultVal =
|
|
840
|
+
d.default || getDefaultFromConfig(d.key, config, settings);
|
|
841
|
+
lines.push(defaultVal ? `${key}=${defaultVal}` : `${key}=`);
|
|
767
842
|
lines.push('');
|
|
768
843
|
}
|
|
769
844
|
|
|
@@ -774,23 +849,21 @@ export function generateDeploymentEnvSection(
|
|
|
774
849
|
/**
|
|
775
850
|
* @param {string} key
|
|
776
851
|
* @param {import('../core/config.js').DeployHubConfig} [config]
|
|
777
|
-
* @param {Record<string,
|
|
852
|
+
* @param {Record<string, unknown>} [settings]
|
|
778
853
|
*/
|
|
779
|
-
function getDefaultFromConfig(key, config,
|
|
780
|
-
const envEntry = Object.values(environments || {})[0] || {};
|
|
781
|
-
|
|
854
|
+
function getDefaultFromConfig(key, config, settings = {}) {
|
|
782
855
|
const map = {
|
|
783
|
-
SSH_HOST:
|
|
784
|
-
SSH_USER:
|
|
785
|
-
SSH_DEPLOY_PATH:
|
|
786
|
-
SSH_APP_NAME:
|
|
856
|
+
SSH_HOST: settings.host,
|
|
857
|
+
SSH_USER: settings.user,
|
|
858
|
+
SSH_DEPLOY_PATH: settings.deployPath || settings.path,
|
|
859
|
+
SSH_APP_NAME: settings.appName,
|
|
787
860
|
SSH_PORT: config?.port || config?.backend?.port,
|
|
788
861
|
SSH_SSH_PORT: '22',
|
|
789
|
-
KUBE_NAMESPACE: config?.project || 'default',
|
|
790
|
-
DOCKER_IMAGE_NAME: config?.project,
|
|
862
|
+
KUBE_NAMESPACE: settings.kubeNamespace || config?.project || 'default',
|
|
863
|
+
DOCKER_IMAGE_NAME: settings.dockerImageName || config?.project,
|
|
791
864
|
DOCKER_IMAGE_TAG: '',
|
|
792
|
-
DOCKER_REGISTRY_URL:
|
|
793
|
-
|
|
865
|
+
DOCKER_REGISTRY_URL: settings.dockerRegistryUrl,
|
|
866
|
+
EC2_LOOKUP_AWS_REGION: settings.awsRegion || 'us-east-1',
|
|
794
867
|
};
|
|
795
868
|
|
|
796
869
|
const val = map[key];
|
|
@@ -871,8 +944,8 @@ export const DEPLOYMENT_GUIDE = {
|
|
|
871
944
|
after: [
|
|
872
945
|
'AWS Console → EC2 → Security Groups → your instance group → Inbound rules →',
|
|
873
946
|
' Add rule: SSH, port 22, source: My IP',
|
|
874
|
-
'Copy .env.example to .env — set SSH_KEY_PATH, SSH_HOST (or EC2_INSTANCE_ID +
|
|
875
|
-
'Add GitHub Secrets: SSH_HOST, SSH_USER, SSH_KEY, plus
|
|
947
|
+
'Copy .env.example to .env — set SSH_KEY_PATH, SSH_HOST (or EC2_INSTANCE_ID + EC2_LOOKUP_AWS_*).',
|
|
948
|
+
'Add GitHub Secrets: SSH_HOST, SSH_USER, SSH_KEY, plus EC2_LOOKUP_AWS_* if using instance ID lookup (distinct from S3 AWS_*).',
|
|
876
949
|
'Run deployhub doctor to verify SSH and optional AWS API access.',
|
|
877
950
|
'git push origin main to trigger your first deployment.',
|
|
878
951
|
],
|
|
@@ -893,7 +966,7 @@ export const DEPLOYMENT_GUIDE = {
|
|
|
893
966
|
'Azure Portal → VM → Networking → Inbound port rules → allow SSH (22) from your IP.',
|
|
894
967
|
'Or: az network nsg rule create --name AllowSSH --priority 1000 --source-address-prefix YOUR_IP ...',
|
|
895
968
|
'Copy .env.example to .env and fill in SSH_HOST, SSH_USER, SSH_KEY_PATH.',
|
|
896
|
-
'For CI: add
|
|
969
|
+
'For CI: add AZURE_VM_LOOKUP_TENANT_ID, AZURE_VM_LOOKUP_CLIENT_ID, AZURE_VM_LOOKUP_CLIENT_SECRET as GitHub Secrets.',
|
|
897
970
|
'Run deployhub doctor, then git push origin main.',
|
|
898
971
|
],
|
|
899
972
|
},
|
|
@@ -940,6 +1013,7 @@ export const DEPLOYMENT_GUIDE = {
|
|
|
940
1013
|
'Copy .env.example to .env and set DOCKER_IMAGE_NAME, DOCKER_REGISTRY_USERNAME, and DOCKER_REGISTRY_TOKEN.',
|
|
941
1014
|
'Skipping registry credentials will very likely cause ImagePullBackOff — the cluster cannot see local Docker images.',
|
|
942
1015
|
'For private registries: also create kubectl create secret docker-registry ... and set KUBE_IMAGE_PULL_SECRET.',
|
|
1016
|
+
'Interpreted backends (Node/Python/PHP/Rails): rollback refuses to rebuild from the artifact when the buildId image is not local — keep pipeline.docker builds so the restored tag exists.',
|
|
943
1017
|
'Add the GitHub Secrets listed below (Settings → Secrets and variables → Actions).',
|
|
944
1018
|
'Run deployhub doctor to verify cluster access and that your image is pullable.',
|
|
945
1019
|
'git push origin main to trigger your first deployment.',
|