@akash-chowdhury-24/deployhub 2.0.41 → 2.0.44
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 +133 -16
- package/package.json +1 -1
- package/src/commands/doctor.js +10 -0
- package/src/core/config.js +10 -0
- package/src/deployment/deployment-env.js +46 -11
- package/src/deployment/env-file.js +344 -0
- package/src/deployment/hooks.js +16 -6
- package/src/deployment/index.js +3 -0
- package/src/deployment/init-prompts.js +99 -6
- package/src/deployment/providers/docker.js +48 -2
- package/src/deployment/providers/ssh.js +75 -11
- package/src/deployment/ssh-connection.js +6 -2
- package/src/utils/docker-image-deploy.js +29 -8
- package/src/utils/docker-remote.js +7 -2
package/README.md
CHANGED
|
@@ -120,6 +120,9 @@ This interactive wizard will:
|
|
|
120
120
|
- Configure build commands and output directory
|
|
121
121
|
- Set up storage providers (AWS, Google Drive, Azure, GCP, Dropbox, Local)
|
|
122
122
|
- Optionally configure deployment targets (SSH, Docker, EC2, Azure VM, GCP VM, Kubernetes)
|
|
123
|
+
- For Docker: **Where should the container run?** (`local` / `ssh` / `raw`)
|
|
124
|
+
- For push-triggered environments: **Which branch triggers this environment?**
|
|
125
|
+
- Optional deploy hooks and optional DeployHub-managed `.env`
|
|
123
126
|
- Generate `deployhub.config.json`
|
|
124
127
|
- Generate `.github/workflows/deployhub.yml` and `.github/workflows/deployhub-rollback.yml`
|
|
125
128
|
- Generate `.env.example`
|
|
@@ -353,9 +356,25 @@ deployhub sync-workflows # regenerate deployhub.yml + dep
|
|
|
353
356
|
| First / grandfathered env in multi-env `init` | `"push"` |
|
|
354
357
|
| Additional environments | `"manual"` — deploy only via Actions → Run workflow or `deployhub deploy --env` |
|
|
355
358
|
|
|
356
|
-
Multi-env `init` prints a reminder naming which environments are push vs manual and how to edit `deployhub.config.json` (`environments.<name>.trigger`) if you want a different mix. After changing triggers or envs, run `deployhub sync-workflows` and commit the regenerated YAML.
|
|
359
|
+
Multi-env `init` prints a reminder naming which environments are push vs manual and how to edit `deployhub.config.json` (`environments.<name>.trigger`) if you want a different mix. After changing triggers, branches, or envs, run `deployhub sync-workflows` and commit the regenerated YAML.
|
|
357
360
|
|
|
358
|
-
On a GitHub Actions **push**, `deployhub build` only auto-deploys environments with `trigger: "push"
|
|
361
|
+
On a GitHub Actions **push**, `deployhub build` only auto-deploys environments with `trigger: "push"` whose `branch` matches the push ref. Environments with `trigger: "manual"` are never deployed on push — even though their secrets are present in the job for dispatch/rollback.
|
|
362
|
+
|
|
363
|
+
### Branch-to-environment mapping
|
|
364
|
+
|
|
365
|
+
Each environment can name the git branch that triggers it (`environments.<env>.branch`). `init` / `env add` ask when the trigger is push:
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
? Which branch triggers this environment?
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The generated workflow's `on.push.branches` lists **only** those mapped branches. A push to an unmapped branch (for example a personal working branch that feeds `dev` via PR) **never invokes the workflow at all** — not a skipped deploy, no pipeline run. `workflow_dispatch` (Actions → Run workflow) is unaffected: you still pick an environment by name.
|
|
372
|
+
|
|
373
|
+
Because storage history is per-environment and only written when a real pipeline run for that environment completes, an environment's rollback history can only ever contain builds that came from its own mapped branch.
|
|
374
|
+
|
|
375
|
+
Configs with **no** `branch` field anywhere keep today's behavior (push trigger is `main` only). If you rename or delete a branch on GitHub, edit `environments.<env>.branch` and run `deployhub sync-workflows` — doctor cannot see remote branch changes.
|
|
376
|
+
|
|
377
|
+
How branch mapping was proven (generated YAML, history isolation): `CONTEXT.md`.
|
|
359
378
|
|
|
360
379
|
### Secret naming
|
|
361
380
|
|
|
@@ -699,13 +718,13 @@ git commit -m "Add DeployHub CI"
|
|
|
699
718
|
|
|
700
719
|
1. Open **Settings → Secrets and variables → Actions** in your GitHub repo.
|
|
701
720
|
2. Add every secret listed at the end of `deployhub init` (storage + deployment).
|
|
702
|
-
3. Push to `main`
|
|
721
|
+
3. Push to a mapped branch (default `main` / `master`) — the deploy workflow triggers on push.
|
|
703
722
|
|
|
704
723
|
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`).
|
|
705
724
|
|
|
706
725
|
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.
|
|
707
726
|
|
|
708
|
-
|
|
727
|
+
Push triggers follow [branch-to-environment mapping](#branch-to-environment-mapping) (`environments.<env>.branch`). Example: `main` → production, `dev` → staging; a push to any other branch never starts this workflow. `workflow_dispatch` still lets you pick an environment by name.
|
|
709
728
|
|
|
710
729
|
To run a deploy manually: **Actions → DeployHub → Run workflow**.
|
|
711
730
|
|
|
@@ -755,17 +774,26 @@ You can enable **multiple providers** — DeployHub uploads to all of them in pa
|
|
|
755
774
|
|
|
756
775
|
## Custom deploy hooks
|
|
757
776
|
|
|
758
|
-
SSH-based deploys (`ssh`, `ec2`, `azure-vm`, `gcp-vm`, and `docker` with `remote.mode: "ssh"`) can run your own commands on the remote host as part of deploy and rollback. Use them for migrations, cache clearing, or a notification — anything you currently SSH in to do by hand.
|
|
777
|
+
SSH-based deploys (`ssh`, `ec2`, `azure-vm`, `gcp-vm`, and `docker` with `remote.mode: "ssh"`) can run your own commands on the remote host as part of deploy and rollback. Use them for host bootstrap, migrations, codegen, cache clearing, or a notification — anything you currently SSH in to do by hand.
|
|
778
|
+
|
|
779
|
+
Config shape: `environments.<env>.config.hooks` with four stage arrays. Each entry is `{ command, continueOnError, reconnect, timeoutMs }` (`command` is required; the rest are optional).
|
|
759
780
|
|
|
760
781
|
| Hook | When it runs | Typical use | Failure default |
|
|
761
782
|
|------|----------------|-------------|-----------------|
|
|
762
|
-
| `preDeploy` | After the artifact/image is ready and the host is reachable, **before**
|
|
763
|
-
| `
|
|
783
|
+
| `preDeploy` | After the artifact/image is ready and the host is reachable, **before** extract / `docker stop` | Host bootstrap (`dnf install -y docker`, `usermod`). **Not** for codegen that needs the new files | Abort the deploy (`continueOnError: false`) |
|
|
784
|
+
| `postInstall` | After the framework's dependency install (`pip install` / `npm install` / `composer install` / `bundle install`) and **before** the app process starts. Plain `ssh` / `ec2` / `azure-vm` / `gcp-vm` only | `prisma generate`, Django `collectstatic`, any codegen that needs installed deps **and** the extracted artifact | Abort (`continueOnError: false`) |
|
|
785
|
+
| `postDeploy` | After the app is up (SSH start / docker-ssh port publish). **Skipped on rollback** | Cache warm, Slack ping, non-critical cleanup | Continue (`continueOnError: true` when added via `init` / `env add`) |
|
|
764
786
|
| `rollback` | During `deployhub rollback`, in the same slot as `preDeploy` — before the restored version takes over | Your own down-migration. DeployHub does not reverse migrations for you | Abort the rollback (`continueOnError: false`) |
|
|
765
787
|
|
|
766
|
-
|
|
788
|
+
On rollback, `postInstall` **reuses the same stage name** (unlike `preDeploy`, which is replaced by the `rollback` stage). Codegen must run against the **restored** files, not leftover generated output from the version you are rolling back from.
|
|
789
|
+
|
|
790
|
+
`postInstall` is **not** asked or run for Docker `remote.mode: "ssh"`. Docker installs dependencies at **image build** time, not at deploy time, so there is no remote `pip install` / `npm install` slot between extract and `docker run`. Put `prisma generate` (and similar) in the Dockerfile. Kubernetes and Docker `local` / `raw` do not support hooks (no persistent remote shell session); configuring them there fails loudly at deploy/rollback.
|
|
791
|
+
|
|
792
|
+
> **Hooks do not `cd` for you.** Commands run in the SSH user's home directory unless you `cd` explicitly (`cd /var/www/app && …`). Loading a `.env` in the deploy path requires `set -a && . ./.env && set +a` **in the same command** — `cd` alone does not export those variables.
|
|
767
793
|
|
|
768
|
-
|
|
794
|
+
Commands run over the **existing** SSH session (not a second connection), unless a successful hook sets `"reconnect": true`. Optional `timeoutMs` overrides the session default (`DEPLOYHUB_SSH_EXEC_TIMEOUT_MS`, 120s) so a hung command fails instead of hanging CI.
|
|
795
|
+
|
|
796
|
+
On docker-ssh, `preDeploy` / `rollback` run **before** remote registry login and `docker stop`/`run`, so a hook can install Docker on a bare host. `postDeploy` runs **after** the port-publish inspect check (`docker inspect` confirms `0.0.0.0:<port>->`). A hook that curls the app's published port therefore sees a container DeployHub already treated as published. If that inspect fails, `postDeploy` does not run. `postInstall` is skipped entirely on this method.
|
|
769
797
|
|
|
770
798
|
Hook `command` strings are raw remote shell — there is **no** `{{buildId}}` / `{{containerName}}` / `{{port}}` / `{{environment}}` substitution. Hardcode values per environment (or read them from the remote environment). Extra Docker environments use an env-scoped container name (`{project}-{env}`; the first/grandfathered env stays `{project}`), so a hook that `docker exec myapp …` on staging will miss `myapp-staging`.
|
|
771
799
|
|
|
@@ -773,7 +801,9 @@ Hook commands that look like they embed a secret (`--password`, `-p secret`, `TO
|
|
|
773
801
|
|
|
774
802
|
Set `"reconnect": true` on a hook when the command only takes effect on a **new SSH login** — the usual case is `sudo usermod -aG docker $USER`. After that command succeeds, DeployHub closes the current session and opens a new one before the next hook or deploy step. Failed commands never reconnect. Omitted / `false` (the default) never reconnects.
|
|
775
803
|
|
|
776
|
-
`init` and `env add` ask optionally — default is skip. After each command they ask **Add another … command?** so one stage can collect several entries (no cap), and whether that command needs an SSH reconnect (`[y/N]`, default N). `--yes` / non-interactive env add still writes no hooks.
|
|
804
|
+
`init` and `env add` ask optionally — default is skip. After each command they ask **Add another … command?** so one stage can collect several entries (no cap), and whether that command needs an SSH reconnect (`[y/N]`, default N). `--yes` / non-interactive env add still writes no hooks.
|
|
805
|
+
|
|
806
|
+
Example — Amazon Linux bare host bootstrap (the docker-ssh `preDeploy` case) plus FastAPI + Prisma on SSH (the case that required `postInstall`):
|
|
777
807
|
|
|
778
808
|
```json
|
|
779
809
|
"environments": {
|
|
@@ -781,8 +811,11 @@ Set `"reconnect": true` on a hook when the command only takes effect on a **new
|
|
|
781
811
|
"config": {
|
|
782
812
|
"hooks": {
|
|
783
813
|
"preDeploy": [
|
|
784
|
-
{ "command": "sudo
|
|
785
|
-
{ "command": "
|
|
814
|
+
{ "command": "sudo dnf install -y docker && sudo systemctl enable --now docker", "timeoutMs": 180000 },
|
|
815
|
+
{ "command": "sudo usermod -aG docker ec2-user", "reconnect": true }
|
|
816
|
+
],
|
|
817
|
+
"postInstall": [
|
|
818
|
+
{ "command": "cd /var/www/app && python3.11 -m prisma generate", "continueOnError": false }
|
|
786
819
|
],
|
|
787
820
|
"postDeploy": [
|
|
788
821
|
{ "command": "curl -s https://hooks.slack.com/services/T000/B000/xxx -d deployed", "continueOnError": true }
|
|
@@ -796,6 +829,75 @@ Set `"reconnect": true` on a hook when the command only takes effect on a **new
|
|
|
796
829
|
}
|
|
797
830
|
```
|
|
798
831
|
|
|
832
|
+
When that `postInstall` also needs values from a DeployHub-managed `.env` (already on disk by then — see [DeployHub-managed `.env`](#deployhub-managed-env)):
|
|
833
|
+
|
|
834
|
+
```json
|
|
835
|
+
{ "command": "cd /var/www/app && set -a && . ./.env && set +a && prisma generate", "continueOnError": false }
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
`preDeploy` cannot run `prisma generate` on a fresh host: the artifact has not been unzipped yet, so `schema.prisma` is missing. `postDeploy` is too late: uvicorn already imported `from prisma import Prisma` and crashed with `Client hasn't been generated yet.` `postInstall` sits after `pip install` / `npm install` and before process start. The same `postInstall` command re-runs during rollback against the restored schema.
|
|
839
|
+
|
|
840
|
+
How hooks were proven on real hosts (including bare-host Docker install and Prisma codegen): `CONTEXT.md`.
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## DeployHub-managed `.env`
|
|
845
|
+
|
|
846
|
+
> **Two ways to get a `.env` onto your server — don't mix them:**
|
|
847
|
+
> - **Managed** (`envFileSecretName` configured): DeployHub writes it for you from a GitHub Secret, and **OVERWRITES** it on every deploy to pick up secret updates.
|
|
848
|
+
> - **Hand-placed** (nothing configured): you put a `.env` on the server yourself once; DeployHub never touches it, on any future deploy or rollback.
|
|
849
|
+
|
|
850
|
+
| Mode | How it gets onto the server | Redeploy / rollback |
|
|
851
|
+
|------|-----------------------------|---------------------|
|
|
852
|
+
| **DeployHub-managed** (`environments.<env>.config.envFileSecretName` is set) | You paste the **full contents** of `.env` as a GitHub Secret. DeployHub SFTP-uploads it on every deploy. | **Overwritten every deploy** (and every rollback) with the **current** secret value. Secret updates take effect on the next deploy. `.env` is not versioned per `buildId`. |
|
|
853
|
+
| **Hand-placed** (no `envFileSecretName`) | You SSH in and create `.env` yourself under the deploy path. | **Left untouched.** `unzip -o` / `rsync -a` (no `--delete`) do not wipe extra files. This is the older behavior. |
|
|
854
|
+
|
|
855
|
+
> Rolling back **code** does not revert a secret change. Managed `.env` is not versioned per `buildId` — rollback re-delivers the **current** GitHub Secret. If you are rolling back because an env-var change broke the app, revert the secret as well (otherwise rollback still writes the broken value).
|
|
856
|
+
|
|
857
|
+
### Setup (GitHub Secret)
|
|
858
|
+
|
|
859
|
+
`init` / `env add` ask (SSH, EC2, Azure VM, GCP VM, and Docker `remote.mode: "ssh"` only; skipped for Kubernetes and Docker local/raw):
|
|
860
|
+
|
|
861
|
+
```
|
|
862
|
+
? Does this project have a .env file that should be deployed to the server? [y/N]
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
If yes, paste the **full contents** of the file as a GitHub Secret named:
|
|
866
|
+
|
|
867
|
+
- First / grandfathered environment: `ENV_FILE`
|
|
868
|
+
- Additional environments: `STAGING_ENV_FILE`, `PRODUCTION_ENV_FILE`, … (same `{ENV}_` prefix as `SSH_HOST`)
|
|
869
|
+
|
|
870
|
+
Then confirm. DeployHub **cannot** verify the secret exists ahead of time (GitHub Secrets are write-only). `deployhub doctor` reports that the **name** is referenced in the workflow, and says plainly that the **value** cannot be checked.
|
|
871
|
+
|
|
872
|
+
If `envFileSecretName` is set but the GitHub Secret was never created, is empty, or the name is misspelled, deploy **fails immediately** with:
|
|
873
|
+
|
|
874
|
+
```text
|
|
875
|
+
Managed .env is configured (GitHub Secret "ENV_FILE") but the value is missing or empty.
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
GitHub Actions injects an empty string for a missing secret, which hits this check. No empty `.env` is written to the target.
|
|
879
|
+
|
|
880
|
+
Config stores only the secret **name**:
|
|
881
|
+
|
|
882
|
+
```json
|
|
883
|
+
"config": {
|
|
884
|
+
"envFileSecretName": "ENV_FILE"
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Never put the `.env` contents in `deployhub.config.json`. After adding the secret, run `deployhub sync-workflows` so `ENV_FILE` (or `STAGING_ENV_FILE`) is injected into the Actions job.
|
|
889
|
+
|
|
890
|
+
Transfer is binary SFTP (`ssh.putFile` — the same mechanism as the artifact zip), not `echo "$CONTENT" > .env`. Special characters (`$`, quotes, backticks, multi-line values) stay intact, and the secret never appears in `[ssh] $ …` / `[docker] $ …` command logs. The remote file is `chmod 600` and owned by the deploy user.
|
|
891
|
+
|
|
892
|
+
### Where the file lands
|
|
893
|
+
|
|
894
|
+
- **SSH / EC2 / Azure VM / GCP VM:** `{deployPath}/.env` (fullstack: both frontend and backend deploy paths). Apps that already load `.env` from the working directory (python-dotenv, etc.) pick it up with no extra flags. The file is written **after** unzip/rsync and **before** `postInstall` / process start, so a hook like `cd /var/www/app && set -a && . ./.env && set +a && prisma generate` sees the managed values. `preDeploy` runs before extract, so `.env` is not there yet on a fresh host.
|
|
895
|
+
- **Docker `remote.mode: "ssh"`:** `/opt/deployhub/envs/<project>-<env>/.env` on the **host**, then `docker run --env-file <that path> …` is wired in automatically. Project **and** environment are in the path so two apps (or staging + production) on one host never share a file. If `envFileSecretName` is unset, `docker run` is unchanged (no `--env-file`). `/opt/deployhub` is created automatically on first deploy. The one-time `sudo mkdir -p /opt/deployhub/envs && sudo chown $USER:$USER /opt/deployhub` step is a **rare fallback** — only if the host has neither a writable `/opt` nor passwordless sudo (see [one-time server setup](#one-time-server-setup-before-your-first-deploy)). Docker-ssh has no `postInstall`; bake codegen into the image.
|
|
896
|
+
|
|
897
|
+
Kubernetes is out of scope for this feature.
|
|
898
|
+
|
|
899
|
+
How managed `.env` was proven (byte-identical SFTP, empty-secret fail, postInstall sourcing): `CONTEXT.md`.
|
|
900
|
+
|
|
799
901
|
---
|
|
800
902
|
|
|
801
903
|
## Choosing a deployment method
|
|
@@ -830,7 +932,7 @@ sudo chown your-ssh-user:your-ssh-user /var/www/your-app-name
|
|
|
830
932
|
|
|
831
933
|
Replace `/var/www/your-app-name` with your actual deploy path and `your-ssh-user` with your configured `SSH_USER` (e.g. `ec2-user` on Amazon Linux, `ubuntu` on Ubuntu). Without this, DeployHub cannot write your build output — `deployhub doctor` will catch it and show the exact fix.
|
|
832
934
|
|
|
833
|
-
**Manually placed files (`.env`, etc.):** SSH / EC2 / Azure VM / GCP VM redeploy and rollback do **not** wipe the deploy directory. Extraction uses `unzip -o` (fullstack: `rsync -a` with no `--delete`) so only files present in the artifact are added or overwritten; a `.env` you place on the server yourself is left untouched. Keep secrets out of the zip. Docker-SSH is container-based and does not use this directory extract.
|
|
935
|
+
**Manually placed files (`.env`, etc.):** SSH / EC2 / Azure VM / GCP VM redeploy and rollback do **not** wipe the deploy directory. Extraction uses `unzip -o` (fullstack: `rsync -a` with no `--delete`) so only files present in the artifact are added or overwritten; a `.env` you place on the server yourself is left untouched **when this environment does not use DeployHub-managed `.env`**. If `envFileSecretName` is set, DeployHub **overwrites** `{deployPath}/.env` on every deploy — see [DeployHub-managed `.env`](#deployhub-managed-env). Do not mix the two modes. Keep secrets out of the zip. Docker-SSH is container-based and does not use this directory extract (managed `.env` for Docker-SSH uses `/opt/deployhub/envs/<project>-<env>/.env` + `--env-file`).
|
|
834
936
|
|
|
835
937
|
**Frontend deploys** that auto-activate `nginx.conf` also need **passwordless sudo** for Nginx test/reload (and `cp` into `/etc/nginx/`). After installing Nginx, run `sudo visudo` and add a line like:
|
|
836
938
|
|
|
@@ -855,6 +957,17 @@ sudo usermod -aG docker your-ssh-user
|
|
|
855
957
|
|
|
856
958
|
Then **reconnect** (group membership applies on the next login). `deployhub doctor` reports this if missing (it prints the exact `usermod` line). See [Docker](#docker) below.
|
|
857
959
|
|
|
960
|
+
**Managed `.env` directory:** DeployHub creates `/opt/deployhub/envs/<project>-<env>/` automatically on the first managed docker-ssh deploy — `mkdir -p`, then passwordless `sudo mkdir`/`chown` if the SSH user cannot write `/opt` (the same class of auto-create as SSH deploy-path `ensureWritableDeployDir`). Typical Ubuntu / Amazon Linux cloud images already have passwordless sudo for the default user, so there is **no extra one-time mkdir**.
|
|
961
|
+
|
|
962
|
+
If the SSH user cannot write `/opt` **and** has no passwordless sudo, deploy fails with the exact commands (same class of message as the docker-group `usermod` line above):
|
|
963
|
+
|
|
964
|
+
```bash
|
|
965
|
+
sudo mkdir -p /opt/deployhub/envs
|
|
966
|
+
sudo chown $USER:$USER /opt/deployhub
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
Then retry the deploy. `deployhub doctor` cannot create that path (it does not write files over SSH).
|
|
970
|
+
|
|
858
971
|
You can also put that bootstrap in **preDeploy hooks** on a bare host (install Docker, `usermod`, `"reconnect": true`). Registry login runs **after** preDeploy, so those hooks get a chance to install `docker` before `docker login`. Failed login used to abort first (`docker: command not found`) and skip the hooks.
|
|
859
972
|
|
|
860
973
|
### SSH
|
|
@@ -906,7 +1019,7 @@ You can also put that bootstrap in **preDeploy hooks** on a bare host (install D
|
|
|
906
1019
|
- [ ] Docker installed (`docker --version` works) — on this machine / CI for **local** and **raw**; on the remote Linux host for **ssh**
|
|
907
1020
|
- [ ] Registry account if pushing private images
|
|
908
1021
|
- [ ] `docker-compose.yml` in project if you use multi-service Compose (not auto-generated)
|
|
909
|
-
- [ ] **SSH mode only:** [one-time server setup](#one-time-server-setup-before-your-first-deploy) — Docker on the host and the SSH user in the `docker` group (`sudo usermod -aG docker <user>`, then reconnect). A `preDeploy` hook can install Docker and run that `usermod` with `"reconnect": true`; registry login happens after preDeploy.
|
|
1022
|
+
- [ ] **SSH mode only:** [one-time server setup](#one-time-server-setup-before-your-first-deploy) — Docker on the host and the SSH user in the `docker` group (`sudo usermod -aG docker <user>`, then reconnect). Managed `.env` directories under `/opt/deployhub/envs` are created automatically when the SSH user can write `/opt` or has passwordless sudo. A `preDeploy` hook can install Docker and run that `usermod` with `"reconnect": true`; registry login happens after preDeploy.
|
|
910
1023
|
|
|
911
1024
|
`init` and `env add` ask **Where should the container run?**
|
|
912
1025
|
|
|
@@ -922,7 +1035,7 @@ You can also put that bootstrap in **preDeploy hooks** on a bare host (install D
|
|
|
922
1035
|
- Starter `Dockerfile` at project root when none exists (framework-aware; skipped if you already have one)
|
|
923
1036
|
- `.dockerignore` when missing (never overwrites an existing one)
|
|
924
1037
|
- `.env.example` for image name, registry, remote `DOCKER_HOST`, and SSH vars when `remote.mode` is `ssh`
|
|
925
|
-
- Docker daemon connectivity test during `init` (local / raw); SSH key, host reachability, remote daemon, and `docker` group
|
|
1038
|
+
- Docker daemon connectivity test during `init` (local / raw); SSH key validity, SSH host reachability, remote Docker daemon reachable, and remote Docker permission (`docker` group) when mode is `ssh`
|
|
926
1039
|
- Reuses the image built in the pipeline `docker` stage when present; otherwise builds from the artifact
|
|
927
1040
|
- Registry login + push when `DOCKER_REGISTRY_USERNAME` / `DOCKER_REGISTRY_TOKEN` are set
|
|
928
1041
|
- Auto-generates a unique image tag per build when `DOCKER_IMAGE_TAG` is unset (git SHA → CI run id → timestamp)
|
|
@@ -1177,6 +1290,9 @@ Prefer `deployhub init` over hand-writing config — it sets adapters, workflow,
|
|
|
1177
1290
|
| `Deploy requires storage upload` | Add at least one storage provider in config |
|
|
1178
1291
|
| AWS / GDrive check fails in `doctor` | Run `deployhub storage add <provider>` and match GitHub Secrets |
|
|
1179
1292
|
| 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 |
|
|
1293
|
+
| SSH-mode Docker: permission denied talking to dockerd | SSH user is not in the `docker` group. On the server: `sudo usermod -aG docker <user>`, then reconnect. `deployhub doctor` prints this line. |
|
|
1294
|
+
| Docker-ssh container running but app unreachable | Missing published port. Set `environments.<env>.config.port` (asked as **Default port**). Doctor / `deployhub verify` check `0.0.0.0:<port>->`. |
|
|
1295
|
+
| `Managed .env is configured … missing or empty` | Add the GitHub Secret named in `envFileSecretName` (full `.env` file contents). Doctor cannot check the value — GitHub Secrets are write-only. |
|
|
1180
1296
|
| Wrong output uploaded | Fix `buildOutput` in config (`dist` vs `build` vs `.next`) |
|
|
1181
1297
|
| Tests fail in CI | Set `"pipeline": { "test": false }` temporarily, or fix tests |
|
|
1182
1298
|
| Monorepo subfolders | Edit `buildCommand` paths in `deployhub.config.json` after init |
|
|
@@ -1225,7 +1341,7 @@ Run `deployhub doctor` after any config change.
|
|
|
1225
1341
|
| `deployhub clean` | Remove old local artifacts |
|
|
1226
1342
|
| `deployhub update` | Check for CLI updates |
|
|
1227
1343
|
|
|
1228
|
-
**Tests:** `npm test` — currently **
|
|
1344
|
+
**Tests:** `npm test` — currently **466 passing** across the Jest suites (1 skipped).
|
|
1229
1345
|
|
|
1230
1346
|
## Storage vs deployment lookup credentials
|
|
1231
1347
|
|
|
@@ -1280,6 +1396,7 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
|
|
|
1280
1396
|
| `AZURE_VM_LOOKUP_SUBSCRIPTION_ID`, `AZURE_VM_LOOKUP_RESOURCE_GROUP`, `AZURE_VM_LOOKUP_VM_NAME` | Optional Azure VM IP lookup |
|
|
1281
1397
|
| `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) |
|
|
1282
1398
|
| `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_USERNAME`, `DOCKER_REGISTRY_TOKEN`, `DOCKER_REGISTRY_URL`, `DOCKER_HOST` | Docker deployment (`DOCKER_IMAGE_TAG` optional) |
|
|
1399
|
+
| `ENV_FILE` / `{ENV}_ENV_FILE` | DeployHub-managed `.env` — **full file contents**, only when `envFileSecretName` is set. Grandfathered env uses `ENV_FILE`; additional envs use `STAGING_ENV_FILE`, `PRODUCTION_ENV_FILE`, … (same prefix as `SSH_HOST`) |
|
|
1283
1400
|
| `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 |
|
|
1284
1401
|
|
|
1285
1402
|
**Kubernetes rollback vs deploy — registry credentials:** Kubernetes **rollback** specifically **requires** `DOCKER_REGISTRY_USERNAME` and `DOCKER_REGISTRY_TOKEN`. Rollback pulls the restored `buildId` tag when it is not local, then must still push that image for the cluster; without registry credentials it fails loudly and early (a rollback that cannot push can never succeed against a real cluster). Rebuild-from-artifact is only the last fallback (interpreted backends refuse). 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.
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
preferredPhpFpmUnitName,
|
|
55
55
|
} from '../utils/php-fpm.js';
|
|
56
56
|
import { getHooksDoctorChecks } from '../deployment/hooks.js';
|
|
57
|
+
import { getEnvFileDoctorChecks } from '../deployment/env-file.js';
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* @typedef {{ name: string, pass: boolean, message: string }} CheckResult
|
|
@@ -1414,6 +1415,15 @@ export function registerDoctorCommand(program) {
|
|
|
1414
1415
|
informationalCheckNames.add(hookCheck.name);
|
|
1415
1416
|
results.push(await runCheck(hookCheck.name, async () => hookCheck));
|
|
1416
1417
|
}
|
|
1418
|
+
let workflowText = '';
|
|
1419
|
+
const deployWorkflowPath = path.join(cwd, '.github', 'workflows', 'deployhub.yml');
|
|
1420
|
+
if (await fs.pathExists(deployWorkflowPath)) {
|
|
1421
|
+
workflowText = await fs.readFile(deployWorkflowPath, 'utf8');
|
|
1422
|
+
}
|
|
1423
|
+
for (const envFileCheck of getEnvFileDoctorChecks(config, { workflowText })) {
|
|
1424
|
+
informationalCheckNames.add(envFileCheck.name);
|
|
1425
|
+
results.push(await runCheck(envFileCheck.name, async () => envFileCheck));
|
|
1426
|
+
}
|
|
1417
1427
|
const driftChecks = await getWorkflowDriftDoctorChecks(cwd, config);
|
|
1418
1428
|
for (const check of driftChecks) {
|
|
1419
1429
|
results.push(await runCheck(check.name, async () => check));
|
package/src/core/config.js
CHANGED
|
@@ -76,14 +76,24 @@ const MethodConfigSchema = z
|
|
|
76
76
|
/**
|
|
77
77
|
* Remote shell hooks for SSH-based methods (ssh / ec2 / azure-vm / gcp-vm /
|
|
78
78
|
* docker remote.mode ssh). Rejected on kubernetes and docker local/raw.
|
|
79
|
+
* `postInstall` is ssh/ec2/azure-vm/gcp-vm only (after remote dep install,
|
|
80
|
+
* before process start). Docker-ssh skips it: deps install at image build.
|
|
79
81
|
*/
|
|
80
82
|
hooks: z
|
|
81
83
|
.object({
|
|
82
84
|
preDeploy: z.array(HookCommandSchema).optional(),
|
|
85
|
+
postInstall: z.array(HookCommandSchema).optional(),
|
|
83
86
|
postDeploy: z.array(HookCommandSchema).optional(),
|
|
84
87
|
rollback: z.array(HookCommandSchema).optional(),
|
|
85
88
|
})
|
|
86
89
|
.optional(),
|
|
90
|
+
/**
|
|
91
|
+
* GitHub Secret *name* whose value is the full .env file. Never store the
|
|
92
|
+
* secret value here. SSH methods overwrite deployPath/.env every deploy;
|
|
93
|
+
* docker remote.mode ssh writes /opt/deployhub/envs/<project>-<env>/.env
|
|
94
|
+
* and passes --env-file. Unset = hand-placed .env (if any) is left alone.
|
|
95
|
+
*/
|
|
96
|
+
envFileSecretName: z.string().min(1).optional(),
|
|
87
97
|
appName: z.string().optional(),
|
|
88
98
|
framework: z.string().optional(),
|
|
89
99
|
port: z.number().optional(),
|
|
@@ -424,27 +424,62 @@ function dockerHasExplicitRemoteMode(settings) {
|
|
|
424
424
|
return mode === 'ssh' || mode === 'local' || mode === 'raw';
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
/** Added only when environments.<env>.config.envFileSecretName is set. */
|
|
428
|
+
const MANAGED_ENV_FILE_DEF = {
|
|
429
|
+
key: 'ENV_FILE',
|
|
430
|
+
optionalReason:
|
|
431
|
+
'only when this environment uses a DeployHub-managed .env (envFileSecretName in config)',
|
|
432
|
+
comment: [
|
|
433
|
+
'FULL CONTENTS of the project .env file (paste as a GitHub Secret).',
|
|
434
|
+
'Uploaded to the server on every deploy (including rollback) via SFTP — never committed.',
|
|
435
|
+
'Overwrites the remote .env each deploy. Do not also hand-place a .env for this environment.',
|
|
436
|
+
],
|
|
437
|
+
when: 'optional',
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {string} deployType
|
|
442
|
+
* @param {Record<string, unknown>|null|undefined} settings
|
|
443
|
+
* @returns {boolean}
|
|
444
|
+
*/
|
|
445
|
+
function methodSupportsManagedEnvFile(deployType, settings) {
|
|
446
|
+
if (deployType === 'ssh' || deployType === 'ec2' || deployType === 'azure-vm' || deployType === 'gcp-vm') {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (deployType === 'docker') {
|
|
450
|
+
return resolveDockerRemoteMode(settings || {}, {}) === 'ssh';
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
427
455
|
/**
|
|
428
456
|
* Per-env docker defs: ssh mode adds SSH_* and drops raw DOCKER_HOST;
|
|
429
457
|
* explicit local drops DOCKER_HOST; configs with no remote.mode keep legacy defs.
|
|
458
|
+
* ENV_FILE is appended only when envFileSecretName is set on a supported method.
|
|
430
459
|
*
|
|
431
460
|
* @param {string} deployType
|
|
432
461
|
* @param {Record<string, unknown>|null} [settings]
|
|
433
462
|
* @returns {EnvVarDef[]}
|
|
434
463
|
*/
|
|
435
464
|
export function getMethodEnvDefs(deployType, settings = null) {
|
|
436
|
-
|
|
437
|
-
if (deployType
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
465
|
+
let defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
|
|
466
|
+
if (deployType === 'docker') {
|
|
467
|
+
const s = settings || {};
|
|
468
|
+
if (dockerHasExplicitRemoteMode(s)) {
|
|
469
|
+
const mode = resolveDockerRemoteMode(s, {});
|
|
470
|
+
if (mode === 'ssh') {
|
|
471
|
+
defs = [...defs.filter((d) => !DOCKER_HOST_KEYS.has(d.key)), ...DOCKER_SSH_ENV_VARS];
|
|
472
|
+
} else if (mode === 'local') {
|
|
473
|
+
defs = defs.filter((d) => !DOCKER_HOST_KEYS.has(d.key));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
445
476
|
}
|
|
446
|
-
|
|
447
|
-
|
|
477
|
+
const secretName =
|
|
478
|
+
settings && typeof settings.envFileSecretName === 'string'
|
|
479
|
+
? settings.envFileSecretName.trim()
|
|
480
|
+
: '';
|
|
481
|
+
if (secretName && methodSupportsManagedEnvFile(deployType, settings)) {
|
|
482
|
+
defs = [...defs, MANAGED_ENV_FILE_DEF];
|
|
448
483
|
}
|
|
449
484
|
return defs;
|
|
450
485
|
}
|