@akash-chowdhury-24/deployhub 2.0.19 → 2.0.21
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 +49 -3
- package/install.ps1 +142 -17
- package/install.sh +81 -3
- package/package.json +1 -1
- package/src/commands/init.js +8 -5
- package/src/deployment/init-helpers.js +12 -0
- package/src/deployment/providers/docker.js +6 -3
- package/src/deployment/providers/kubernetes.js +4 -2
- package/src/deployment/providers/ssh.js +78 -25
- package/src/utils/docker-container-name.js +51 -0
- package/src/utils/kube-namespace-name.js +63 -0
- package/src/utils/pm2-app-name.js +71 -0
package/README.md
CHANGED
|
@@ -31,17 +31,52 @@ Downloads the latest release from [GitHub Releases](https://github.com/Akash-Cho
|
|
|
31
31
|
curl -fsSL https://raw.githubusercontent.com/Akash-Chowdhury-24/DeployHub/main/install.sh | sh
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Supported platforms: Linux x64, macOS x64, macOS ARM64. If the binary download fails, the script falls back to `npm install -g`.
|
|
34
|
+
Supported platforms: Linux x64, macOS x64, macOS ARM64. If the binary download fails, the script falls back to `npm install -g`. After install it warns if `deployhub` on PATH still resolves to a different (e.g. older npm-global) binary — see the Windows notes below for the same dual-channel conflict.
|
|
35
|
+
|
|
36
|
+
> **macOS `install.sh` — UNTESTED on real hardware in this project’s verification so far**
|
|
37
|
+
>
|
|
38
|
+
> Release assets `deployhub-macos-x64` and `deployhub-macos-arm64` are published and downloadable, and `install.sh` has `darwin` / `x86_64` / `arm64` branches that *should* select those names. The PATH-collision warning and clean/no-collision cases were proven on **Windows** (native) and **Linux** (Docker). They have **not** been run on a real Mac (Intel or Apple Silicon). Linux Docker proof does **not** cover macOS: different `uname` platform branch, different binary assets, BSD userland (`sed` / `readlink` / etc.), and default interactive shell is **zsh** (Catalina+) — the script does not edit `.zshrc` / `.zprofile` / `.bashrc` at all; for a non-sudo install it only prints that you must ensure `~/.local/bin` is on PATH. Treat first macOS binary install as unverified until someone pastes real Terminal output for collision + clean cases on both arch flavors (or at least the arch you use).
|
|
35
39
|
|
|
36
40
|
### Standalone binary (Windows)
|
|
37
41
|
|
|
38
|
-
Run in PowerShell:
|
|
42
|
+
Run in **PowerShell** (does **not** require an elevated/Administrator session — installs under your user profile only):
|
|
39
43
|
|
|
40
44
|
```powershell
|
|
41
45
|
irm https://raw.githubusercontent.com/Akash-Chowdhury-24/DeployHub/main/install.ps1 | iex
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
**What the script actually does:**
|
|
49
|
+
- **x64:** downloads the GitHub Release asset `deployhub-win.exe` and saves it as `%LOCALAPPDATA%\Programs\DeployHub\deployhub.exe` (plain `deployhub` on PATH), then appends that folder to your **user** `PATH` (not machine-wide).
|
|
50
|
+
- **ARM64:** there is no Windows ARM64 binary yet — the script skips the download and runs `npm install -g @akash-chowdhury-24/deployhub@latest` immediately (**requires Node.js 18+**).
|
|
51
|
+
- **Failed x64 download:** the script **does** fall back to the same npm global install (also requires Node.js 18+). If you have neither a working download nor Node.js, installation fails.
|
|
52
|
+
- **No checksum/signature check in the script** — release assets include a `checksums.txt` (SHA-256), but `install.ps1` does not verify it automatically. Binaries are **not Authenticode code-signed**.
|
|
53
|
+
- **PATH collision warning:** if an older install (commonly `npm install -g`) still resolves first, the script prints a warning with `Get-Command deployhub -All` / `npm uninstall -g` guidance. `install.sh` on Linux/macOS does the same check (`type -a deployhub`).
|
|
54
|
+
|
|
55
|
+
> **Open a new PowerShell window after installing.** User `PATH` changes do not apply to the window that ran `iex`. Until you open a new session, `deployhub` may be “not recognized” even though the install succeeded.
|
|
56
|
+
|
|
57
|
+
> **Previously installed via npm?** Binary and npm installs can both put `deployhub` on PATH. After the binary installer finishes, if `deployhub --version` still shows an older version, run `Get-Command deployhub -All` (Windows) or `type -a deployhub` (Linux/macOS). Remove the stale npm copy with `npm uninstall -g @akash-chowdhury-24/deployhub`, or put `%LOCALAPPDATA%\Programs\DeployHub` (Windows) / `/usr/local/bin` or `~/.local/bin` (Unix) earlier in PATH. Then open a **new** terminal and re-check `--version`.
|
|
58
|
+
|
|
59
|
+
> **Windows `pkg` binary — what is and isn’t verified**
|
|
60
|
+
>
|
|
61
|
+
> DeployHub’s only native-adjacent dependency is **`ssh2`** (used by SSH / EC2 / Azure VM / GCP VM). It optionally uses `cpu-features` and an optional `sshcrypto.node` binding; both are try/catch’d and fall back to pure JS/`crypto` when missing. `cpu-features` is **not** listed in `pkg.assets` and is typically absent from installs — that is expected. Storage SDKs (`@aws-sdk/*`, `@azure/storage-blob`, `@google-cloud/storage`, `googleapis`, `dropbox`, `basic-ftp`) and `execa` (Docker / Kubernetes / cloud-CLI lookups) are pure JS / HTTP / process-spawn.
|
|
62
|
+
>
|
|
63
|
+
> **Exercised on the published `deployhub-win.exe` (module load / CLI spawn — not a full live deploy):**
|
|
64
|
+
> - CLI boot (`--version` / `--help`)
|
|
65
|
+
> - **SSH path:** `ssh2`/`node-ssh` load and attempt a real TCP connect (failed on missing key / `ECONNREFUSED` — i.e. past native-load risk)
|
|
66
|
+
> - **AWS S3 SDK** load (SDK warning + doctor AWS check ran)
|
|
67
|
+
> - **Azure / GCP / Google Drive / Dropbox / local** storage provider modules load (doctor checks ran; failures were credentials/API/ENOENT — not `MODULE_NOT_FOUND`)
|
|
68
|
+
> - **Docker** via `execa` → host `docker.exe` (daemon pipe missing is an environment issue; spawn worked)
|
|
69
|
+
> - **Kubernetes** via `execa` → host `kubectl` (cluster unreachable; spawn worked)
|
|
70
|
+
>
|
|
71
|
+
> **Still unproven end-to-end on Windows binary against real infrastructure:** a successful SSH session to a live host (auth + remote commands), a successful S3 (or other cloud) upload with real credentials, and a successful Docker/Kubernetes deploy. Those remain first-real-use risks for network/auth/config — not for missing native addons based on current evidence. If SSH from the `.exe` misbehaves oddly, compare with `npm install -g @akash-chowdhury-24/deployhub` (requires Node.js 18+).
|
|
72
|
+
|
|
73
|
+
Manual checksum check (optional, recommended for locked-down environments):
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
# After install, or after downloading deployhub-win.exe from the release page:
|
|
77
|
+
Get-FileHash "$env:LOCALAPPDATA\Programs\DeployHub\deployhub.exe" -Algorithm SHA256
|
|
78
|
+
# Compare to the deployhub-win.exe line in checksums.txt from the same release.
|
|
79
|
+
```
|
|
45
80
|
|
|
46
81
|
### Manual download
|
|
47
82
|
|
|
@@ -618,6 +653,8 @@ All JS frontends share the same install/build flow: `npm ci` → `npm run build`
|
|
|
618
653
|
- **Install:** Composer (on CI and server).
|
|
619
654
|
- **Deploy:** SSH with PHP-FPM or `php artisan` for Laravel.
|
|
620
655
|
|
|
656
|
+
> ⚠️ **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).
|
|
657
|
+
|
|
621
658
|
### Java
|
|
622
659
|
|
|
623
660
|
- **Detect:** `pom.xml` with Spring Boot.
|
|
@@ -1059,6 +1096,15 @@ Prefer `deployhub init` over hand-writing config — it sets adapters, workflow,
|
|
|
1059
1096
|
| Tests fail in CI | Set `"pipeline": { "test": false }` temporarily, or fix tests |
|
|
1060
1097
|
| Monorepo subfolders | Edit `buildCommand` paths in `deployhub.config.json` after init |
|
|
1061
1098
|
|
|
1099
|
+
### Windows install (`install.ps1`)
|
|
1100
|
+
|
|
1101
|
+
| Problem | Fix |
|
|
1102
|
+
|---------|-----|
|
|
1103
|
+
| `irm` / `iex` blocked, or SmartScreen / Defender flags `deployhub.exe` | The Windows binary is **not Authenticode code-signed**, so SmartScreen often warns on first run. Click **More info → Run anyway**, or download `deployhub-win.exe` + `checksums.txt` from [Releases](https://github.com/Akash-Chowdhury-24/DeployHub/releases/latest) and verify the SHA-256 hash before running. If your org blocks unsigned binaries entirely, install via npm instead: `npm install -g @akash-chowdhury-24/deployhub` (requires Node.js 18+). |
|
|
1104
|
+
| `deployhub : The term 'deployhub' is not recognized` right after install | **Open a new PowerShell window** first — user `PATH` updates do not apply to the session that ran `iex`. Confirm the file exists at `%LOCALAPPDATA%\Programs\DeployHub\deployhub.exe`. If it is missing, re-run the install script or use the npm fallback. |
|
|
1105
|
+
| `deployhub --version` shows an unexpected / old version | Compare against the [latest release tag](https://github.com/Akash-Chowdhury-24/DeployHub/releases/latest). Run `Get-Command deployhub -All` (Windows) or `type -a deployhub` (Unix) — an older **npm global** install often wins PATH order. Fix: `npm uninstall -g @akash-chowdhury-24/deployhub`, or put the binary install dir first in PATH, then open a **new** terminal. Force a clean binary reinstall: delete `%LOCALAPPDATA%\Programs\DeployHub`, re-run `install.ps1`, open a new PowerShell window. |
|
|
1106
|
+
| Binary download fails and npm fallback errors | The x64 script falls back to `npm install -g …` when the GitHub download fails. That fallback **requires Node.js 18+ and npm on PATH**. Without Node, fix network access to GitHub Releases, or install Node and retry. |
|
|
1107
|
+
|
|
1062
1108
|
Run `deployhub doctor` after any config change.
|
|
1063
1109
|
|
|
1064
1110
|
---
|
package/install.ps1
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# DeployHub install script for Windows
|
|
2
|
-
# Author: Akash Chowdhury
|
|
2
|
+
# Author: Akash Chowdhury - canonical source: src/utils/author.js
|
|
3
3
|
# Repository: https://github.com/Akash-Chowdhury-24/DeployHub
|
|
4
|
+
#
|
|
5
|
+
# IMPORTANT: PowerShell variable names are CASE-INSENSITIVE.
|
|
6
|
+
# Never use $BinaryName / $binaryName as two different values - the second write
|
|
7
|
+
# overwrites the first (that bug previously saved deployhub-win.exe instead of deployhub.exe).
|
|
4
8
|
|
|
5
9
|
$ErrorActionPreference = "Stop"
|
|
6
10
|
|
|
7
11
|
$GitHubRepo = "Akash-Chowdhury-24/DeployHub"
|
|
8
|
-
|
|
12
|
+
# Filename on disk after install (what PATH resolves as `deployhub`)
|
|
13
|
+
$InstalledExeName = "deployhub.exe"
|
|
14
|
+
# GitHub Release asset name for Windows x64
|
|
15
|
+
$ReleaseAssetName = "deployhub-win.exe"
|
|
16
|
+
$NpmPackage = "@akash-chowdhury-24/deployhub"
|
|
9
17
|
|
|
10
18
|
function Get-LatestVersion {
|
|
11
19
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$GitHubRepo/releases/latest"
|
|
@@ -20,46 +28,163 @@ function Get-InstallDir {
|
|
|
20
28
|
return $localBin
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
function Refresh-SessionPathFromPersistent {
|
|
32
|
+
# Rebuild PATH the way a NEW PowerShell window would see it after our User PATH edit.
|
|
33
|
+
# Do NOT prepend the install dir here - that would hide npm-global collisions
|
|
34
|
+
# (user PATH appends DeployHub at the end; npm often wins earlier in PATH).
|
|
35
|
+
$machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
|
36
|
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
37
|
+
if ($machinePath -and $userPath) {
|
|
38
|
+
$env:Path = "$machinePath;$userPath"
|
|
39
|
+
} elseif ($userPath) {
|
|
40
|
+
$env:Path = $userPath
|
|
41
|
+
} elseif ($machinePath) {
|
|
42
|
+
$env:Path = $machinePath
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
function Add-ToPath {
|
|
24
47
|
param([string]$Dir)
|
|
25
48
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
26
49
|
if ($userPath -notlike "*$Dir*") {
|
|
27
|
-
[
|
|
28
|
-
|
|
50
|
+
if ([string]::IsNullOrWhiteSpace($userPath)) {
|
|
51
|
+
[Environment]::SetEnvironmentVariable("Path", $Dir, "User")
|
|
52
|
+
} else {
|
|
53
|
+
[Environment]::SetEnvironmentVariable("Path", "$userPath;$Dir", "User")
|
|
54
|
+
}
|
|
29
55
|
Write-Host "Added $Dir to user PATH"
|
|
30
56
|
}
|
|
57
|
+
Refresh-SessionPathFromPersistent
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-VersionFromOutput {
|
|
61
|
+
param([string]$Output)
|
|
62
|
+
if ([string]::IsNullOrWhiteSpace($Output)) { return $null }
|
|
63
|
+
$line = ($Output -split "`r?`n" | Where-Object { $_.Trim() -ne '' } | Select-Object -First 1)
|
|
64
|
+
if (-not $line) { return $null }
|
|
65
|
+
if ($line -match '(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)') {
|
|
66
|
+
return $Matches[1]
|
|
67
|
+
}
|
|
68
|
+
return $line.Trim()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Warn-PathCollision {
|
|
72
|
+
param(
|
|
73
|
+
[string]$InstallDir,
|
|
74
|
+
[string]$InstalledExe,
|
|
75
|
+
[string]$ExpectedTag
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
$expectedVersion = $ExpectedTag.TrimStart('v')
|
|
79
|
+
$directOut = & $InstalledExe --version 2>&1 | Out-String
|
|
80
|
+
$directVersion = Get-VersionFromOutput $directOut
|
|
81
|
+
|
|
82
|
+
Write-Host ""
|
|
83
|
+
Write-Host "Installed binary reports: $(if ($directVersion) { $directVersion } else { '(unknown)' }) ($InstalledExe)"
|
|
84
|
+
|
|
85
|
+
# Re-sync PATH from Machine+User so this check matches a freshly opened shell.
|
|
86
|
+
Refresh-SessionPathFromPersistent
|
|
87
|
+
|
|
88
|
+
$resolved = $null
|
|
89
|
+
try {
|
|
90
|
+
$resolved = Get-Command deployhub -ErrorAction Stop
|
|
91
|
+
} catch {
|
|
92
|
+
Write-Host "Note: 'deployhub' is not yet visible on PATH in this session." -ForegroundColor Yellow
|
|
93
|
+
Write-Host "Open a NEW PowerShell window, then run: deployhub --version" -ForegroundColor Yellow
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
$resolvedPath = $resolved.Source
|
|
98
|
+
# Prefer .exe over .cmd/.ps1 shims when comparing versions of what PATH runs
|
|
99
|
+
$pathOut = & deployhub --version 2>&1 | Out-String
|
|
100
|
+
$pathVersion = Get-VersionFromOutput $pathOut
|
|
101
|
+
|
|
102
|
+
$installedFull = [System.IO.Path]::GetFullPath($InstalledExe)
|
|
103
|
+
$resolvedFull = [System.IO.Path]::GetFullPath($resolvedPath)
|
|
104
|
+
|
|
105
|
+
# npm global on Windows resolves to deployhub.cmd / deployhub.ps1, not our .exe
|
|
106
|
+
$sameFile = ($installedFull -ieq $resolvedFull)
|
|
107
|
+
$versionMatch = $pathVersion -and $expectedVersion -and ($pathVersion -eq $expectedVersion)
|
|
108
|
+
|
|
109
|
+
if ($sameFile -and $versionMatch) {
|
|
110
|
+
Write-Host "PATH resolves to the newly installed binary ($pathVersion). OK."
|
|
111
|
+
Write-Host "Still open a NEW PowerShell window before day-to-day use so user PATH updates apply cleanly."
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ((-not $sameFile) -or (-not $versionMatch)) {
|
|
116
|
+
Write-Host ""
|
|
117
|
+
Write-Host "WARNING: Installed DeployHub $expectedVersion to $InstallDir," -ForegroundColor Yellow
|
|
118
|
+
Write-Host " but running 'deployhub --version' currently resolves to a DIFFERENT" -ForegroundColor Yellow
|
|
119
|
+
Write-Host " installation reporting a different version (or path)." -ForegroundColor Yellow
|
|
120
|
+
Write-Host ""
|
|
121
|
+
Write-Host " Expected: $installedFull ($expectedVersion)"
|
|
122
|
+
Write-Host " Resolved: $resolvedFull ($(if ($pathVersion) { $pathVersion } else { 'unknown version' }))"
|
|
123
|
+
Write-Host ""
|
|
124
|
+
Write-Host " This usually means an older install (e.g. via 'npm install -g') is earlier in your PATH."
|
|
125
|
+
Write-Host ""
|
|
126
|
+
Write-Host " To see every 'deployhub' on your PATH, run:"
|
|
127
|
+
Write-Host " Get-Command deployhub -All"
|
|
128
|
+
Write-Host ""
|
|
129
|
+
Write-Host " To remove a conflicting npm install:"
|
|
130
|
+
Write-Host " npm uninstall -g $NpmPackage"
|
|
131
|
+
Write-Host ""
|
|
132
|
+
Write-Host " Or reorder your PATH so $InstallDir comes first."
|
|
133
|
+
Write-Host ""
|
|
134
|
+
Write-Host " This check rebuilds PATH from your saved Machine+User environment" -ForegroundColor Yellow
|
|
135
|
+
Write-Host " (same order a NEW PowerShell window would use). After fixing PATH," -ForegroundColor Yellow
|
|
136
|
+
Write-Host " open a NEW window and run: deployhub --version" -ForegroundColor Yellow
|
|
137
|
+
}
|
|
31
138
|
}
|
|
32
139
|
|
|
33
140
|
try {
|
|
34
141
|
$arch = $env:PROCESSOR_ARCHITECTURE
|
|
35
142
|
if ($arch -eq "ARM64") {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
npm install -g @akash-chowdhury-24/deployhub@latest
|
|
143
|
+
Write-Host "Windows ARM64 detected - no native binary available yet. Installing via npm instead..."
|
|
144
|
+
npm install -g "$NpmPackage@latest"
|
|
39
145
|
exit 0
|
|
40
|
-
} else {
|
|
41
|
-
$binaryName = "deployhub-win.exe"
|
|
42
146
|
}
|
|
43
147
|
|
|
44
148
|
$version = Get-LatestVersion
|
|
45
|
-
$
|
|
46
|
-
$url = "https://github.com/$GitHubRepo/releases/download/$version/$assetName"
|
|
149
|
+
$url = "https://github.com/$GitHubRepo/releases/download/$version/$ReleaseAssetName"
|
|
47
150
|
$installDir = Get-InstallDir
|
|
48
|
-
$dest = Join-Path $installDir $
|
|
151
|
+
$dest = Join-Path $installDir $InstalledExeName
|
|
49
152
|
|
|
50
|
-
Write-Host "Downloading DeployHub $version for Windows..."
|
|
51
|
-
|
|
153
|
+
Write-Host "Downloading DeployHub $version for Windows ($ReleaseAssetName -> $InstalledExeName)..."
|
|
154
|
+
# Download to a temp name first, then rename - avoids leaving a half-written deployhub.exe
|
|
155
|
+
$tmpDest = Join-Path $installDir ("deployhub-download-" + [guid]::NewGuid().ToString("n") + ".exe")
|
|
156
|
+
try {
|
|
157
|
+
Invoke-WebRequest -Uri $url -OutFile $tmpDest -UseBasicParsing
|
|
158
|
+
if (-not (Test-Path $tmpDest) -or (Get-Item $tmpDest).Length -eq 0) {
|
|
159
|
+
throw "Binary download failed"
|
|
160
|
+
}
|
|
161
|
+
# Remove any legacy misnamed asset from older install.ps1 (PowerShell case-bug).
|
|
162
|
+
$legacy = Join-Path $installDir $ReleaseAssetName
|
|
163
|
+
if (Test-Path $legacy) {
|
|
164
|
+
Remove-Item -Force $legacy -ErrorAction SilentlyContinue
|
|
165
|
+
}
|
|
166
|
+
Move-Item -Force -Path $tmpDest -Destination $dest
|
|
167
|
+
} finally {
|
|
168
|
+
if (Test-Path $tmpDest) {
|
|
169
|
+
Remove-Item -Force $tmpDest -ErrorAction SilentlyContinue
|
|
170
|
+
}
|
|
171
|
+
}
|
|
52
172
|
|
|
53
173
|
if (-not (Test-Path $dest) -or (Get-Item $dest).Length -eq 0) {
|
|
54
|
-
throw "Binary
|
|
174
|
+
throw "Binary install failed - $InstalledExeName missing after download"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if ((Split-Path -Leaf $dest) -ne $InstalledExeName) {
|
|
178
|
+
throw "Internal error: installed file is not named $InstalledExeName"
|
|
55
179
|
}
|
|
56
180
|
|
|
57
181
|
Add-ToPath $installDir
|
|
58
182
|
Write-Host "DeployHub $version installed to $dest"
|
|
59
|
-
|
|
183
|
+
Write-Host "On-disk filename: $(Split-Path -Leaf $dest)"
|
|
184
|
+
Warn-PathCollision -InstallDir $installDir -InstalledExe $dest -ExpectedTag $version
|
|
60
185
|
}
|
|
61
186
|
catch {
|
|
62
187
|
Write-Host "Binary install failed ($($_.Exception.Message)). Falling back to npm..." -ForegroundColor Yellow
|
|
63
|
-
npm install -g @
|
|
188
|
+
npm install -g "$NpmPackage@latest"
|
|
64
189
|
Write-Host "DeployHub installed via npm."
|
|
65
190
|
}
|
package/install.sh
CHANGED
|
@@ -8,6 +8,8 @@ set -e
|
|
|
8
8
|
GITHUB_REPO="Akash-Chowdhury-24/DeployHub"
|
|
9
9
|
INSTALL_DIR="/usr/local/bin"
|
|
10
10
|
BINARY_NAME="deployhub"
|
|
11
|
+
NPM_PACKAGE="@akash-chowdhury-24/deployhub"
|
|
12
|
+
INSTALLED_PATH=""
|
|
11
13
|
|
|
12
14
|
detect_platform() {
|
|
13
15
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
@@ -50,7 +52,7 @@ fetch_latest_version() {
|
|
|
50
52
|
|
|
51
53
|
if [ -z "$VERSION" ]; then
|
|
52
54
|
echo "Could not determine latest release version. Falling back to npm install." >&2
|
|
53
|
-
npm install -g @
|
|
55
|
+
npm install -g "${NPM_PACKAGE}@latest"
|
|
54
56
|
echo "DeployHub installed via npm."
|
|
55
57
|
exit 0
|
|
56
58
|
fi
|
|
@@ -71,19 +73,22 @@ download_binary() {
|
|
|
71
73
|
if [ ! -s "$TMP" ]; then
|
|
72
74
|
echo "Binary download failed. Falling back to npm install." >&2
|
|
73
75
|
rm -f "$TMP"
|
|
74
|
-
npm install -g @
|
|
76
|
+
npm install -g "${NPM_PACKAGE}@latest"
|
|
75
77
|
echo "DeployHub installed via npm."
|
|
76
78
|
exit 0
|
|
77
79
|
fi
|
|
78
80
|
|
|
79
81
|
if [ "$(id -u)" -eq 0 ]; then
|
|
80
82
|
install -m 755 "$TMP" "${INSTALL_DIR}/${BINARY_NAME}"
|
|
83
|
+
INSTALLED_PATH="${INSTALL_DIR}/${BINARY_NAME}"
|
|
81
84
|
else
|
|
82
85
|
if command -v sudo >/dev/null 2>&1; then
|
|
83
86
|
sudo install -m 755 "$TMP" "${INSTALL_DIR}/${BINARY_NAME}"
|
|
87
|
+
INSTALLED_PATH="${INSTALL_DIR}/${BINARY_NAME}"
|
|
84
88
|
else
|
|
85
89
|
mkdir -p "${HOME}/.local/bin"
|
|
86
90
|
install -m 755 "$TMP" "${HOME}/.local/bin/${BINARY_NAME}"
|
|
91
|
+
INSTALLED_PATH="${HOME}/.local/bin/${BINARY_NAME}"
|
|
87
92
|
echo "Installed to ${HOME}/.local/bin/${BINARY_NAME} — ensure it is on your PATH"
|
|
88
93
|
fi
|
|
89
94
|
fi
|
|
@@ -91,9 +96,82 @@ download_binary() {
|
|
|
91
96
|
rm -f "$TMP"
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
# Compare PATH-resolved `deployhub` to the binary we just installed.
|
|
100
|
+
warn_path_collision() {
|
|
101
|
+
EXPECTED_VERSION="${VERSION#v}"
|
|
102
|
+
if [ -z "$INSTALLED_PATH" ] || [ ! -x "$INSTALLED_PATH" ]; then
|
|
103
|
+
return 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
INSTALL_BIN_DIR="$(dirname "$INSTALLED_PATH")"
|
|
107
|
+
# Put install dir on PATH only if missing, and APPEND (do not prepend).
|
|
108
|
+
# Prepending would hide an older npm-global install that sits earlier on PATH —
|
|
109
|
+
# the same failure mode the Windows installer hit.
|
|
110
|
+
case ":${PATH}:" in
|
|
111
|
+
*":${INSTALL_BIN_DIR}:"*) ;;
|
|
112
|
+
*) PATH="${PATH}:${INSTALL_BIN_DIR}"; export PATH ;;
|
|
113
|
+
esac
|
|
114
|
+
|
|
115
|
+
DIRECT_OUT="$("$INSTALLED_PATH" --version 2>/dev/null || true)"
|
|
116
|
+
DIRECT_VER="$(printf '%s\n' "$DIRECT_OUT" | head -n 1 | tr -d '\r' | sed -n 's/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[-0-9A-Za-z.]*\).*/\1/p')"
|
|
117
|
+
|
|
118
|
+
echo ""
|
|
119
|
+
echo "Installed binary reports: ${DIRECT_VER:-unknown} ($INSTALLED_PATH)"
|
|
120
|
+
|
|
121
|
+
if ! command -v deployhub >/dev/null 2>&1; then
|
|
122
|
+
echo "Note: 'deployhub' is not yet visible on PATH in this shell."
|
|
123
|
+
echo "Open a NEW terminal, then run: deployhub --version"
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
RESOLVED_PATH="$(command -v deployhub)"
|
|
128
|
+
PATH_OUT="$(deployhub --version 2>/dev/null || true)"
|
|
129
|
+
PATH_VER="$(printf '%s\n' "$PATH_OUT" | head -n 1 | tr -d '\r' | sed -n 's/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[-0-9A-Za-z.]*\).*/\1/p')"
|
|
130
|
+
|
|
131
|
+
# Resolve symlinks where possible for a fair path comparison.
|
|
132
|
+
# Prefer `realpath` (GNU coreutils; also present on recent macOS).
|
|
133
|
+
# Do NOT use `readlink -f` — that flag is GNU-only; BSD readlink on macOS
|
|
134
|
+
# rejects `-f` (the `|| echo` fallback usually masks it, but it is not portable).
|
|
135
|
+
INST_REAL="$INSTALLED_PATH"
|
|
136
|
+
RES_REAL="$RESOLVED_PATH"
|
|
137
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
138
|
+
INST_REAL="$(realpath "$INSTALLED_PATH" 2>/dev/null || echo "$INSTALLED_PATH")"
|
|
139
|
+
RES_REAL="$(realpath "$RESOLVED_PATH" 2>/dev/null || echo "$RESOLVED_PATH")"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if [ "$INST_REAL" = "$RES_REAL" ] && [ -n "$PATH_VER" ] && [ "$PATH_VER" = "$EXPECTED_VERSION" ]; then
|
|
143
|
+
echo "PATH resolves to the newly installed binary ($PATH_VER). OK."
|
|
144
|
+
echo "Still open a NEW terminal before day-to-day use so your shell profile PATH applies cleanly."
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
echo ""
|
|
149
|
+
echo "WARNING: Installed DeployHub ${EXPECTED_VERSION} to ${INSTALLED_PATH},"
|
|
150
|
+
echo " but running 'deployhub --version' currently resolves to a DIFFERENT"
|
|
151
|
+
echo " installation reporting a different version (or path)."
|
|
152
|
+
echo ""
|
|
153
|
+
echo " Expected: ${INST_REAL} (${EXPECTED_VERSION})"
|
|
154
|
+
echo " Resolved: ${RES_REAL} (${PATH_VER:-unknown version})"
|
|
155
|
+
echo ""
|
|
156
|
+
echo " This usually means an older install (e.g. via 'npm install -g') is earlier in your PATH."
|
|
157
|
+
echo ""
|
|
158
|
+
echo " To see every 'deployhub' on your PATH, run:"
|
|
159
|
+
echo " type -a deployhub"
|
|
160
|
+
echo " # or: which -a deployhub"
|
|
161
|
+
echo ""
|
|
162
|
+
echo " To remove a conflicting npm install:"
|
|
163
|
+
echo " npm uninstall -g ${NPM_PACKAGE}"
|
|
164
|
+
echo ""
|
|
165
|
+
echo " Or reorder your PATH so the directory containing the new binary comes first."
|
|
166
|
+
echo ""
|
|
167
|
+
echo " This check uses the CURRENT shell PATH (install dir appended only if it was"
|
|
168
|
+
echo " missing — not prepended, so earlier npm installs are not hidden). After fixing"
|
|
169
|
+
echo " PATH, open a NEW terminal and run: deployhub --version"
|
|
170
|
+
}
|
|
171
|
+
|
|
94
172
|
detect_platform
|
|
95
173
|
fetch_latest_version
|
|
96
174
|
download_binary
|
|
97
175
|
|
|
98
176
|
echo "DeployHub ${VERSION} installed successfully."
|
|
99
|
-
|
|
177
|
+
warn_path_collision
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
printDeploymentNextSteps,
|
|
37
37
|
formatSecretChecklistLine,
|
|
38
38
|
} from '../deployment/deployment-env.js';
|
|
39
|
-
import { confirmValueIfContainsSpaces } from '../deployment/init-helpers.js';
|
|
39
|
+
import { confirmValueIfContainsSpaces, normalizeInitHealthCheckUrl } from '../deployment/init-helpers.js';
|
|
40
40
|
|
|
41
41
|
const FRONTEND_CHOICES = [
|
|
42
42
|
{ name: 'React', value: 'react' },
|
|
@@ -405,10 +405,13 @@ export function registerInitCommand(program) {
|
|
|
405
405
|
|
|
406
406
|
environments[name] = entry;
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
// Only persist a health check URL the user actually entered.
|
|
409
|
+
// Never synthesize http://localhost:<port>/health — that always fails
|
|
410
|
+
// from GitHub Actions (remote runner ≠ deploy target) and incorrectly
|
|
411
|
+
// enables the verify stage when the user left the optional prompt blank.
|
|
412
|
+
const answered = normalizeInitHealthCheckUrl(deployAnswers.healthUrl);
|
|
413
|
+
if (answered) {
|
|
414
|
+
healthUrl = answered;
|
|
412
415
|
}
|
|
413
416
|
|
|
414
417
|
const secrets = getDockerEnvSecrets(deployAnswers);
|
|
@@ -32,6 +32,18 @@ export function suggestSshUser(osHint) {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Normalize the optional health-check URL from `deployhub init`.
|
|
37
|
+
* Blank / whitespace → empty string. Never synthesizes localhost defaults
|
|
38
|
+
* (those always fail from GitHub Actions runners, which are not the deploy target).
|
|
39
|
+
*
|
|
40
|
+
* @param {unknown} answer
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeInitHealthCheckUrl(answer) {
|
|
44
|
+
return typeof answer === 'string' && answer.trim() ? answer.trim() : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
/**
|
|
36
48
|
* @param {string} keyPath
|
|
37
49
|
* @returns {Promise<{ ok: boolean, message: string, fixed?: boolean }>}
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import { createLogger } from '../../logger/index.js';
|
|
3
3
|
import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
|
|
4
4
|
import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
5
|
+
import { resolveDockerContainerName } from '../../utils/docker-container-name.js';
|
|
5
6
|
import { getEnvSettings, mergeMethodSettingsIntoEnv } from '../../core/environments.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -15,6 +16,8 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
15
16
|
const effectiveEnv = mergeMethodSettingsIntoEnv(env, settings);
|
|
16
17
|
const imageOps = createDockerImageDeployContext(config, effectiveEnv, log);
|
|
17
18
|
const { fullImage, getDockerEnv, ensureImageReadyForDeploy } = imageOps;
|
|
19
|
+
// Env-scoped like PM2/Nginx — same-daemon multi-env must not share one container name.
|
|
20
|
+
const containerName = resolveDockerContainerName(config, envName);
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* @param {string} artifactDir
|
|
@@ -36,11 +39,11 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
36
39
|
|
|
37
40
|
await execa(
|
|
38
41
|
'docker',
|
|
39
|
-
['rm', '-f',
|
|
42
|
+
['rm', '-f', containerName],
|
|
40
43
|
{ stdio: 'pipe', env: dockerEnv }
|
|
41
44
|
).catch(() => {});
|
|
42
45
|
|
|
43
|
-
await execa('docker', ['run', '-d', '--rm', '--name',
|
|
46
|
+
await execa('docker', ['run', '-d', '--rm', '--name', containerName, imageRef], {
|
|
44
47
|
stdio: 'inherit',
|
|
45
48
|
env: dockerEnv,
|
|
46
49
|
});
|
|
@@ -81,7 +84,7 @@ export function createDockerProvider(config, envName, env = process.env) {
|
|
|
81
84
|
try {
|
|
82
85
|
const { stdout } = await execa(
|
|
83
86
|
'docker',
|
|
84
|
-
['ps', '--filter', `name
|
|
87
|
+
['ps', '--filter', `name=^/${containerName}$`, '--format', '{{.Status}}'],
|
|
85
88
|
{ stdio: 'pipe', env: getDockerEnv() }
|
|
86
89
|
);
|
|
87
90
|
return stdout.includes('Up');
|
|
@@ -8,6 +8,7 @@ import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.
|
|
|
8
8
|
import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
|
|
9
9
|
import { ensureKubernetesNamespace } from '../../utils/kubernetes-namespace.js';
|
|
10
10
|
import { syncKubernetesDeploymentImage } from '../../utils/kubernetes-deploy-image.js';
|
|
11
|
+
import { resolveKubeNamespace } from '../../utils/kube-namespace-name.js';
|
|
11
12
|
import { getEnvSettings, mergeMethodSettingsIntoEnv } from '../../core/environments.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -24,8 +25,9 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
24
25
|
const kubeconfig =
|
|
25
26
|
effectiveEnv.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
|
|
26
27
|
const context = effectiveEnv.KUBE_CONTEXT || '';
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Env-scoped like Nginx/PM2 — same-cluster multi-env must not share one namespace
|
|
29
|
+
// when settings still default to the project name for every environment.
|
|
30
|
+
const namespace = resolveKubeNamespace(config, envName, effectiveEnv);
|
|
29
31
|
const deploymentName = sanitizeK8sName(config.project || 'app');
|
|
30
32
|
|
|
31
33
|
function getKubectlEnv() {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getNginxConfDPath,
|
|
11
11
|
resolveNginxSiteName,
|
|
12
12
|
} from '../../utils/nginx.js';
|
|
13
|
+
import { resolvePm2AppName } from '../../utils/pm2-app-name.js';
|
|
13
14
|
import { shellQuote, formatRemoteCommandFailure } from '../../utils/shell-quote.js';
|
|
14
15
|
|
|
15
16
|
/** @type {Set<string>} */
|
|
@@ -44,8 +45,8 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
44
45
|
settings.frontendDeployPath || deployPath;
|
|
45
46
|
const backendDeployPath =
|
|
46
47
|
settings.backendDeployPath || deployPath;
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Env-scoped like Nginx site names — same-host multi-env must not share one PM2 name.
|
|
49
|
+
const appName = resolvePm2AppName(config, envName, env);
|
|
49
50
|
const port = settings.port || config.port || Number(env.SSH_PORT) || 3000;
|
|
50
51
|
const sshKey = env.SSH_KEY;
|
|
51
52
|
const keyPath = settings.keyPath || env.SSH_KEY_PATH;
|
|
@@ -133,6 +134,58 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
133
134
|
);
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Stop a previously managed non-PM2 backend for THIS env only.
|
|
139
|
+
*
|
|
140
|
+
* PID-file kill is gated: we only signal a PID if /proc shows our
|
|
141
|
+
* DEPLOYHUB_APP / deployhub.app marker in cmdline or environ. A stale PID
|
|
142
|
+
* reused by an unrelated process is left alone (file still removed).
|
|
143
|
+
* Marker-based pkill remains the primary stop for live processes.
|
|
144
|
+
*
|
|
145
|
+
* @param {import('node-ssh').NodeSSH} ssh
|
|
146
|
+
* @param {string} targetPath
|
|
147
|
+
*/
|
|
148
|
+
async function stopScopedBackendProcess(ssh, targetPath) {
|
|
149
|
+
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
150
|
+
const markerEnv = `DEPLOYHUB_APP=${appName}`;
|
|
151
|
+
const markerJvm = `deployhub.app=${appName}`;
|
|
152
|
+
// Verify-then-kill: only signal a PID if /proc shows our marker in cmdline
|
|
153
|
+
// or environ. A stale PID reused by an unrelated process is left alone
|
|
154
|
+
// (the pidfile is still removed so the next start writes a fresh one).
|
|
155
|
+
await exec(
|
|
156
|
+
ssh,
|
|
157
|
+
`if [ -f ${sh(pidFile)} ]; then ` +
|
|
158
|
+
`pid="$(cat ${sh(pidFile)} 2>/dev/null | tr -cd '0-9')"; ` +
|
|
159
|
+
`if [ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ]; then ` +
|
|
160
|
+
`if tr '\\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q -F ${sh(markerEnv)} ` +
|
|
161
|
+
`|| tr '\\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q -F ${sh(markerJvm)} ` +
|
|
162
|
+
`|| { [ -r "/proc/$pid/environ" ] && tr '\\0' ' ' < "/proc/$pid/environ" 2>/dev/null | grep -q -F ${sh(markerEnv)}; }; then ` +
|
|
163
|
+
`kill "$pid" 2>/dev/null || true; ` +
|
|
164
|
+
`fi; ` +
|
|
165
|
+
`fi; ` +
|
|
166
|
+
`rm -f ${sh(pidFile)}; ` +
|
|
167
|
+
`fi`
|
|
168
|
+
);
|
|
169
|
+
// Marker match — covers lost PID files / processes without a readable pidfile.
|
|
170
|
+
await exec(ssh, `pkill -f ${sh(markerEnv)} || true`);
|
|
171
|
+
await exec(ssh, `pkill -f ${sh(markerJvm)} || true`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Start a nohup process with DEPLOYHUB_APP marker and write PID file.
|
|
176
|
+
* @param {import('node-ssh').NodeSSH} ssh
|
|
177
|
+
* @param {string} targetPath
|
|
178
|
+
* @param {string} command — command body after `nohup` (no trailing &)
|
|
179
|
+
*/
|
|
180
|
+
async function startScopedNohup(ssh, targetPath, command) {
|
|
181
|
+
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
182
|
+
const dir = sh(targetPath);
|
|
183
|
+
await exec(
|
|
184
|
+
ssh,
|
|
185
|
+
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} nohup ${command} > app.log 2>&1 & echo $! > ${sh(pidFile)}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
136
189
|
/**
|
|
137
190
|
* @param {import('node-ssh').NodeSSH} ssh
|
|
138
191
|
* @param {string} targetPath
|
|
@@ -141,6 +194,7 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
141
194
|
const framework = resolveFramework();
|
|
142
195
|
const startCommand = resolveStartCommand();
|
|
143
196
|
const dir = sh(targetPath);
|
|
197
|
+
const pidFile = `${targetPath}/.deployhub.pid`;
|
|
144
198
|
|
|
145
199
|
if (NODE_FRAMEWORKS.has(framework)) {
|
|
146
200
|
await exec(ssh, `cd ${dir} && npm install --production`);
|
|
@@ -172,25 +226,28 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
172
226
|
if (framework === 'django') {
|
|
173
227
|
await exec(ssh, `cd ${dir} && python manage.py migrate`);
|
|
174
228
|
}
|
|
229
|
+
await stopScopedBackendProcess(ssh, targetPath);
|
|
175
230
|
if (framework === 'fastapi') {
|
|
176
|
-
await
|
|
177
|
-
await exec(
|
|
231
|
+
await startScopedNohup(
|
|
178
232
|
ssh,
|
|
179
|
-
|
|
233
|
+
targetPath,
|
|
234
|
+
`uvicorn main:app --host 0.0.0.0 --port ${port}`
|
|
180
235
|
);
|
|
181
236
|
} else {
|
|
182
|
-
|
|
237
|
+
// gunicorn --daemon writes its own PID; still set DEPLOYHUB_APP for pkill fallback.
|
|
183
238
|
const appTarget =
|
|
184
239
|
framework === 'django' ? 'config.wsgi:application' : 'app:app';
|
|
185
240
|
await exec(
|
|
186
241
|
ssh,
|
|
187
|
-
`cd ${dir} &&
|
|
242
|
+
`cd ${dir} && DEPLOYHUB_APP=${sh(appName)} gunicorn ${appTarget} --name ${sh(`deployhub-${appName}`)} --bind 0.0.0.0:${port} --pid ${sh(pidFile)} --daemon`
|
|
188
243
|
);
|
|
189
244
|
}
|
|
190
245
|
return;
|
|
191
246
|
}
|
|
192
247
|
|
|
193
248
|
if (PHP_FRAMEWORKS.has(framework)) {
|
|
249
|
+
// PHP uses a host-wide `systemctl restart php*-fpm` (see README PHP warning).
|
|
250
|
+
// Per-env isolation is Nginx site name + deploy path — not automated FPM pools.
|
|
194
251
|
await exec(ssh, `cd ${dir} && composer install --no-dev`);
|
|
195
252
|
if (framework === 'laravel') {
|
|
196
253
|
await exec(ssh, `cd ${dir} && php artisan migrate --force`);
|
|
@@ -202,40 +259,36 @@ export function createSshProvider(config, envName, env = process.env) {
|
|
|
202
259
|
}
|
|
203
260
|
|
|
204
261
|
if (framework === 'spring' || framework === 'java') {
|
|
205
|
-
await
|
|
206
|
-
|
|
262
|
+
await stopScopedBackendProcess(ssh, targetPath);
|
|
263
|
+
// -Ddeployhub.app= embeds the env-scoped identity in the JVM command line
|
|
264
|
+
// so pkill -f DEPLOYHUB_APP=… and the PID file both target only this env.
|
|
265
|
+
await startScopedNohup(
|
|
207
266
|
ssh,
|
|
208
|
-
|
|
267
|
+
targetPath,
|
|
268
|
+
`java -Ddeployhub.app=${appName} -jar target/*.jar`
|
|
209
269
|
);
|
|
210
270
|
return;
|
|
211
271
|
}
|
|
212
272
|
|
|
213
273
|
if (framework === 'go') {
|
|
214
|
-
await
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
);
|
|
274
|
+
await stopScopedBackendProcess(ssh, targetPath);
|
|
275
|
+
// Binary is always ./bin/app — must NOT pkill by appName alone (that never
|
|
276
|
+
// matched the process) and must NOT pkill a bare "app" (cross-env collision).
|
|
277
|
+
await startScopedNohup(ssh, targetPath, './bin/app');
|
|
219
278
|
return;
|
|
220
279
|
}
|
|
221
280
|
|
|
222
281
|
if (framework === 'dotnet') {
|
|
223
|
-
await
|
|
282
|
+
await stopScopedBackendProcess(ssh, targetPath);
|
|
224
283
|
const dll = startCommand?.replace('dotnet ', '') || 'App.dll';
|
|
225
|
-
await
|
|
226
|
-
ssh,
|
|
227
|
-
`cd ${dir} && nohup dotnet ${dll} > app.log 2>&1 &`
|
|
228
|
-
);
|
|
284
|
+
await startScopedNohup(ssh, targetPath, `dotnet ${dll}`);
|
|
229
285
|
return;
|
|
230
286
|
}
|
|
231
287
|
|
|
232
288
|
if (framework === 'rails') {
|
|
233
289
|
await exec(ssh, `cd ${dir} && bundle install --deployment`);
|
|
234
|
-
await
|
|
235
|
-
await exec
|
|
236
|
-
ssh,
|
|
237
|
-
`cd ${dir} && nohup bundle exec puma -p ${port} > app.log 2>&1 &`
|
|
238
|
-
);
|
|
290
|
+
await stopScopedBackendProcess(ssh, targetPath);
|
|
291
|
+
await startScopedNohup(ssh, targetPath, `bundle exec puma -p ${port}`);
|
|
239
292
|
return;
|
|
240
293
|
}
|
|
241
294
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isGrandfatheredNginxEnv } from './nginx.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize a Docker container name (alphanumeric, underscore, hyphen, period).
|
|
5
|
+
* @param {string} name
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeDockerContainerName(name) {
|
|
9
|
+
return String(name || 'app')
|
|
10
|
+
.replace(/[^a-zA-Z0-9_.-]/g, '-')
|
|
11
|
+
.replace(/^[^a-zA-Z0-9]/, 'a');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether this env is the grandfathered / single-env case for container naming.
|
|
16
|
+
* Reuses the same grandfather rule as Nginx / PM2.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
19
|
+
* @param {string} envName
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isGrandfatheredDockerContainerEnv(config, envName) {
|
|
23
|
+
return isGrandfatheredNginxEnv(config, envName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the Docker container `--name` for an environment.
|
|
28
|
+
*
|
|
29
|
+
* Same risk class as PM2 process names: two docker envs targeting the same
|
|
30
|
+
* daemon with `docker run --name ${project}` will `docker rm -f` each other.
|
|
31
|
+
*
|
|
32
|
+
* - Grandfathered / single-env: `config.project` (unchanged).
|
|
33
|
+
* - Additional envs: `{project}-{env}`.
|
|
34
|
+
*
|
|
35
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
36
|
+
* @param {string} envName
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function resolveDockerContainerName(config, envName) {
|
|
40
|
+
const project = sanitizeDockerContainerName(config.project || 'app');
|
|
41
|
+
if (isGrandfatheredDockerContainerEnv(config, envName)) {
|
|
42
|
+
return project;
|
|
43
|
+
}
|
|
44
|
+
return `${project}-${sanitizeDockerContainerName(envName)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
sanitizeDockerContainerName,
|
|
49
|
+
isGrandfatheredDockerContainerEnv,
|
|
50
|
+
resolveDockerContainerName,
|
|
51
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isGrandfatheredNginxEnv } from './nginx.js';
|
|
2
|
+
import { getEnvSettings } from '../core/environments.js';
|
|
3
|
+
import { sanitizeK8sName } from './kubernetes-manifests.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether this env is the grandfathered / single-env case for namespace naming.
|
|
7
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
8
|
+
* @param {string} envName
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function isGrandfatheredKubeNamespaceEnv(config, envName) {
|
|
12
|
+
return isGrandfatheredNginxEnv(config, envName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the Kubernetes namespace for an environment.
|
|
17
|
+
*
|
|
18
|
+
* Deployment *names* stay project-scoped (safe when namespaces differ). Two
|
|
19
|
+
* environments targeting the same cluster with the same namespace WILL collide
|
|
20
|
+
* on Deployment/Service objects — same risk class as PM2/Nginx on one host.
|
|
21
|
+
*
|
|
22
|
+
* - Grandfathered / single-env: settings.kubeNamespace || KUBE_NAMESPACE || project
|
|
23
|
+
* (unchanged).
|
|
24
|
+
* - Additional envs: if configured namespace is missing or equals the project /
|
|
25
|
+
* grandfathered namespace, auto-scope to `{project}-{env}`. An explicitly
|
|
26
|
+
* distinct namespace is kept.
|
|
27
|
+
*
|
|
28
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
29
|
+
* @param {string} envName
|
|
30
|
+
* @param {Record<string, string|undefined>} [env] — already secret-overlaid
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function resolveKubeNamespace(config, envName, env = process.env) {
|
|
34
|
+
const project = sanitizeK8sName(config.project || 'app');
|
|
35
|
+
const settings = getEnvSettings(config.environments?.[envName]);
|
|
36
|
+
const configured = String(
|
|
37
|
+
settings.kubeNamespace || env.KUBE_NAMESPACE || ''
|
|
38
|
+
).trim();
|
|
39
|
+
|
|
40
|
+
if (isGrandfatheredKubeNamespaceEnv(config, envName)) {
|
|
41
|
+
return sanitizeK8sName(configured || project);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const grandfather =
|
|
45
|
+
config.unprefixedSecretEnvironment || config.defaultEnvironment || null;
|
|
46
|
+
let grandfatherNs = project;
|
|
47
|
+
if (grandfather && config.environments?.[grandfather]) {
|
|
48
|
+
const gfSettings = getEnvSettings(config.environments[grandfather]);
|
|
49
|
+
grandfatherNs = sanitizeK8sName(gfSettings.kubeNamespace || project);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaults = new Set([project, 'default', grandfatherNs]);
|
|
53
|
+
if (configured && !defaults.has(sanitizeK8sName(configured))) {
|
|
54
|
+
return sanitizeK8sName(configured);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sanitizeK8sName(`${project}-${envName}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
isGrandfatheredKubeNamespaceEnv,
|
|
62
|
+
resolveKubeNamespace,
|
|
63
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isGrandfatheredNginxEnv } from './nginx.js';
|
|
2
|
+
import { getEnvSettings } from '../core/environments.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize a name for PM2 process naming (same charset as Nginx site names).
|
|
6
|
+
* @param {string} name
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function sanitizePm2AppName(name) {
|
|
10
|
+
return String(name || 'app').replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether this env is the grandfathered / single-env case for process naming.
|
|
15
|
+
* Reuses the same grandfather rule as Nginx site filenames.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
18
|
+
* @param {string} envName
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export function isGrandfatheredPm2Env(config, envName) {
|
|
22
|
+
return isGrandfatheredNginxEnv(config, envName);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the PM2 process name for an environment.
|
|
27
|
+
*
|
|
28
|
+
* Same risk class as Nginx site filenames: two backend envs on one host with
|
|
29
|
+
* the same PM2 name will restart/replace each other's process.
|
|
30
|
+
*
|
|
31
|
+
* - Grandfathered / single-env: `settings.appName` || `SSH_APP_NAME` || `project`
|
|
32
|
+
* (unchanged — existing single-env PM2 processes keep their name).
|
|
33
|
+
* - Additional envs: auto-scope to `{project}-{env}` when the configured name
|
|
34
|
+
* is missing, equals the project default, or collides with the grandfathered
|
|
35
|
+
* env's resolved name. An explicitly distinct `appName` / `SSH_APP_NAME` is kept.
|
|
36
|
+
*
|
|
37
|
+
* @param {import('../core/config.js').DeployHubConfig} config
|
|
38
|
+
* @param {string} envName
|
|
39
|
+
* @param {Record<string, string|undefined>} [env] — already secret-overlaid for this env
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function resolvePm2AppName(config, envName, env = process.env) {
|
|
43
|
+
const project = sanitizePm2AppName(config.project || 'app');
|
|
44
|
+
const settings = getEnvSettings(config.environments?.[envName]);
|
|
45
|
+
const configured = (settings.appName || env.SSH_APP_NAME || '').trim();
|
|
46
|
+
|
|
47
|
+
if (isGrandfatheredPm2Env(config, envName)) {
|
|
48
|
+
return sanitizePm2AppName(configured || project);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const grandfather =
|
|
52
|
+
config.unprefixedSecretEnvironment || config.defaultEnvironment || null;
|
|
53
|
+
let grandfatherName = project;
|
|
54
|
+
if (grandfather && config.environments?.[grandfather]) {
|
|
55
|
+
const gfSettings = getEnvSettings(config.environments[grandfather]);
|
|
56
|
+
grandfatherName = sanitizePm2AppName(gfSettings.appName || project);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaults = new Set([project, `${project}-api`, grandfatherName]);
|
|
60
|
+
if (configured && !defaults.has(sanitizePm2AppName(configured))) {
|
|
61
|
+
return sanitizePm2AppName(configured);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `${project}-${sanitizePm2AppName(envName)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default {
|
|
68
|
+
sanitizePm2AppName,
|
|
69
|
+
isGrandfatheredPm2Env,
|
|
70
|
+
resolvePm2AppName,
|
|
71
|
+
};
|