Skip to content

Commit 2a50a9f

Browse files
author
Alade Yessoufou
committed
Add deploy --dry-run command to preview deployment steps (Task #5)
1 parent 385f6ab commit 2a50a9f

4 files changed

Lines changed: 260 additions & 2 deletions

File tree

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@
3535
- Allow profile configs: `shipnode.<env>.conf` (e.g., staging, prod).
3636
- Add `shipnode init --print` to emit config without writing a file.
3737

38-
### 5) Deploy Dry Run
38+
### 5) Deploy Dry Run ✅ COMPLETED
3939
- Add `shipnode deploy --dry-run` to show:
4040
- Resolved config values (redacting secrets).
4141
- Local build commands to be executed.
4242
- Remote commands and rsync targets.
4343
- Zero-downtime flow steps if enabled.
44+
- Supports both backend and frontend apps, zero-downtime and legacy modes.
4445

4546
## Docs Updates
4647
- Update `README.md` with new command flags and examples.

lib/commands/deploy.sh

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,245 @@
1+
redact_secrets() {
2+
local value="$1"
3+
local var_name="$2"
4+
5+
# Check if variable name suggests it contains a secret (case-insensitive)
6+
local lower_var_name=$(echo "$var_name" | tr '[:upper:]' '[:lower:]')
7+
if [[ "$lower_var_name" =~ (password|secret|key|token|auth|private|credential) ]]; then
8+
echo "[REDACTED]"
9+
else
10+
echo "$value"
11+
fi
12+
}
13+
14+
cmd_deploy_dry_run() {
15+
load_config
16+
17+
local SKIP_BUILD=false
18+
if [ "$1" = "--skip-build" ]; then
19+
SKIP_BUILD=true
20+
fi
21+
22+
# Detect package manager
23+
PKG_MANAGER=$(detect_pkg_manager)
24+
PKG_INSTALL_CMD=$(get_pkg_install_cmd "$PKG_MANAGER")
25+
PKG_RUN_CMD=$(get_pkg_run_cmd "$PKG_MANAGER" "build")
26+
27+
# Detect build directory for frontend
28+
local BUILD_DIR="dist"
29+
if [ -d "build" ]; then
30+
BUILD_DIR="build"
31+
elif [ -d "public" ]; then
32+
BUILD_DIR="public"
33+
fi
34+
35+
echo ""
36+
echo "==========================================="
37+
echo " DEPLOYMENT DRY RUN MODE"
38+
echo "==========================================="
39+
echo ""
40+
41+
# Configuration Section
42+
echo "Configuration (from $SHIPNODE_CONFIG_FILE):"
43+
echo " App Type: $APP_TYPE"
44+
echo " SSH User: $SSH_USER"
45+
echo " SSH Host: $SSH_HOST"
46+
echo " SSH Port: $(redact_secrets "$SSH_PORT" "SSH_PORT")"
47+
echo " Remote Path: $REMOTE_PATH"
48+
echo " Package Manager: $PKG_MANAGER"
49+
echo " Zero Downtime: ${ZERO_DOWNTIME:-true}"
50+
51+
if [ "$APP_TYPE" = "backend" ]; then
52+
echo " PM2 App Name: $PM2_APP_NAME"
53+
echo " Backend Port: $(redact_secrets "$BACKEND_PORT" "BACKEND_PORT")"
54+
fi
55+
56+
if [ -n "$DOMAIN" ]; then
57+
echo " Domain: $DOMAIN"
58+
fi
59+
60+
# Show redacted secrets
61+
echo ""
62+
echo "Security - Secrets Redacted:"
63+
echo " - Variables containing: PASSWORD, SECRET, KEY, TOKEN, AUTH, PRIVATE, CREDENTIAL"
64+
echo " - SSH connection details shown, but authentication keys are redacted"
65+
66+
# Local Build Commands
67+
echo ""
68+
echo "Local Build Commands:"
69+
if [ "$SKIP_BUILD" = true ]; then
70+
echo " [SKIPPED via --skip-build flag]"
71+
else
72+
if [ "$APP_TYPE" = "frontend" ]; then
73+
echo " 1. $PKG_RUN_CMD"
74+
echo " (builds frontend for production)"
75+
echo " 2. Detected build output: $BUILD_DIR/"
76+
else
77+
echo " [Backend builds happen on remote server]"
78+
fi
79+
fi
80+
81+
# Remote Commands Section
82+
echo ""
83+
echo "Remote Deployment Commands:"
84+
85+
if [ "$ZERO_DOWNTIME" = "true" ]; then
86+
local timestamp=$(date +"%Y%m%d%H%M%S")
87+
local release_path="$REMOTE_PATH/releases/$timestamp"
88+
89+
echo " Mode: Zero-Downtime Deployment"
90+
echo ""
91+
echo " Deployment Flow:"
92+
echo " 1. Acquire deployment lock at $REMOTE_PATH/.shipnode/deploy.lock"
93+
echo " 2. Create new release directory: $release_path"
94+
echo " 3. Setup release structure (releases/, shared/, .shipnode/)"
95+
echo ""
96+
echo " 4. Rsync local files to release directory:"
97+
echo " Source: ./"
98+
echo " Target: $SSH_USER@$SSH_HOST:$release_path/"
99+
echo " Excludes: node_modules, .env, .git, .gitignore, shipnode.conf, *.log"
100+
echo ""
101+
echo " 5. Remote setup commands:"
102+
echo " - cd $release_path"
103+
if [ -f "$REMOTE_PATH/shared/.env" ] || [ -f ".env" ]; then
104+
echo " - ln -sf $REMOTE_PATH/shared/.env .env"
105+
fi
106+
107+
if [ "$APP_TYPE" = "backend" ]; then
108+
echo " - $PKG_INSTALL_CMD"
109+
if [ -f "package.json" ] && [ "$SKIP_BUILD" = false ]; then
110+
if jq -e '.scripts.build' package.json >/dev/null 2>&1; then
111+
echo " - $PKG_RUN_CMD (detected build script in package.json)"
112+
fi
113+
fi
114+
echo " - [Link .env into build/ if AdonisJS framework detected]"
115+
fi
116+
echo ""
117+
echo " 6. Run pre-deploy hook (if PRE_DEPLOY_HOOK is configured)"
118+
echo ""
119+
echo " 7. Atomic symlink switch:"
120+
echo " ln -sfn $release_path $REMOTE_PATH/current.tmp"
121+
echo " mv -Tf $REMOTE_PATH/current.tmp $REMOTE_PATH/current"
122+
echo ""
123+
124+
if [ "$APP_TYPE" = "backend" ]; then
125+
echo " 8. Reload application:"
126+
echo " - Generate PM2 ecosystem config at $REMOTE_PATH/shared/ecosystem.config.cjs"
127+
echo " - pm2 startOrReload ecosystem.config.cjs --update-env"
128+
echo " - pm2 save"
129+
echo ""
130+
131+
if [ "${HEALTH_CHECK_ENABLED:-true}" = "true" ]; then
132+
echo " 9. Health check (if enabled):"
133+
echo " - Endpoint: http://localhost:$BACKEND_PORT${HEALTH_CHECK_PATH:-/health}"
134+
echo " - Retries: ${HEALTH_CHECK_RETRIES:-3}"
135+
echo " - Timeout: ${HEALTH_CHECK_TIMEOUT:-30}s per attempt"
136+
echo " - [On failure: automatic rollback to previous release]"
137+
echo ""
138+
echo " 10. Record successful release in $REMOTE_PATH/.shipnode/releases.json"
139+
echo ""
140+
echo " 11. Cleanup old releases (keep ${KEEP_RELEASES:-5} most recent)"
141+
echo ""
142+
echo " 12. Run post-deploy hook (if POST_DEPLOY_HOOK is configured)"
143+
echo ""
144+
echo " 13. Release deployment lock"
145+
else
146+
echo " 9. Record successful release"
147+
echo " 10. Cleanup old releases (keep ${KEEP_RELEASES:-5} most recent)"
148+
echo " 11. Run post-deploy hook (if configured)"
149+
echo " 12. Release deployment lock"
150+
fi
151+
else
152+
echo " 8. Record successful release"
153+
echo " 9. Run post-deploy hook (if POST_DEPLOY_HOOK is configured)"
154+
echo " 10. Cleanup old releases (keep ${KEEP_RELEASES:-5} most recent)"
155+
echo " 11. Release deployment lock"
156+
fi
157+
158+
echo ""
159+
echo " Directory Structure:"
160+
echo " $REMOTE_PATH/"
161+
echo " ├── current/ -> symlink to active release"
162+
echo " ├── releases/"
163+
echo " │ └── $timestamp/ <- new release"
164+
echo " ├── shared/"
165+
echo " │ ├── .env # persistent environment"
166+
echo " │ └── ecosystem.config.cjs # PM2 config"
167+
echo " └── .shipnode/"
168+
echo " ├── deploy.lock # prevents concurrent deploys"
169+
echo " └── releases.json # release history"
170+
171+
else
172+
echo " Mode: Legacy Deployment (Non-Zero-Downtime)"
173+
echo ""
174+
echo " Deployment Steps:"
175+
echo " 1. Create remote directory: $REMOTE_PATH"
176+
echo ""
177+
echo " 2. Rsync local files:"
178+
echo " Source: ./"
179+
echo " Target: $SSH_USER@$SSH_HOST:$REMOTE_PATH/"
180+
echo " Excludes: node_modules, .env, .git, .gitignore, shipnode.conf, *.log"
181+
echo ""
182+
echo " 3. Remote build commands:"
183+
if [ "$APP_TYPE" = "backend" ]; then
184+
echo " - cd $REMOTE_PATH"
185+
echo " - $PKG_INSTALL_CMD"
186+
if [ -f "package.json" ] && [ "$SKIP_BUILD" = false ]; then
187+
if jq -e '.scripts.build' package.json >/dev/null 2>&1; then
188+
echo " - $PKG_RUN_CMD (detected build script)"
189+
fi
190+
fi
191+
echo " - [Link .env into build/ if needed]"
192+
fi
193+
echo ""
194+
echo " 4. Run pre-deploy hook (if PRE_DEPLOY_HOOK is configured)"
195+
echo ""
196+
197+
if [ "$APP_TYPE" = "backend" ]; then
198+
echo " 5. Start/Reload with PM2:"
199+
echo " - Generate ecosystem.config.cjs"
200+
echo " - pm2 startOrReload ecosystem.config.cjs --update-env"
201+
echo " - pm2 save"
202+
fi
203+
204+
echo ""
205+
echo " 6. Run post-deploy hook (if POST_DEPLOY_HOOK is configured)"
206+
fi
207+
208+
# Caddy Configuration (if domain is set)
209+
if [ -n "$DOMAIN" ]; then
210+
echo ""
211+
echo "Caddy Configuration:"
212+
if [ "$APP_TYPE" = "backend" ]; then
213+
echo " - Configure reverse proxy: $DOMAIN -> localhost:$BACKEND_PORT"
214+
echo " - Config file: /etc/caddy/conf.d/$PM2_APP_NAME.caddy"
215+
else
216+
local SERVE_PATH="$REMOTE_PATH"
217+
if [ "$ZERO_DOWNTIME" = "true" ]; then
218+
SERVE_PATH="$REMOTE_PATH/current"
219+
fi
220+
local APP_NAME=$(basename "$REMOTE_PATH")
221+
echo " - Configure static file server: $DOMAIN -> $SERVE_PATH"
222+
echo " - Config file: /etc/caddy/conf.d/$APP_NAME.caddy"
223+
echo " - SPA support enabled (try_files {path} /index.html)"
224+
fi
225+
echo " - Log file: /var/log/caddy/$(basename "$REMOTE_PATH").log"
226+
fi
227+
228+
echo ""
229+
echo "==========================================="
230+
echo " DRY RUN COMPLETE - No changes made"
231+
echo "==========================================="
232+
echo ""
233+
echo "To execute this deployment, run:"
234+
echo " shipnode deploy"
235+
if [ "$SKIP_BUILD" = true ]; then
236+
echo ""
237+
echo "Or with --skip-build flag:"
238+
echo " shipnode deploy --skip-build"
239+
fi
240+
echo ""
241+
}
242+
1243
cmd_deploy() {
2244
load_config
3245

lib/commands/help.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Commands:
1313
setup First-time server setup (Node, PM2, Caddy, jq)
1414
deploy Deploy the application
1515
deploy --skip-build Deploy without running build step
16+
deploy --dry-run Preview deployment without executing
1617
1718
Global Options:
1819
--config <path> Use custom config file (default: shipnode.conf)

lib/commands/main.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,21 @@ main() {
4747
cmd_setup
4848
;;
4949
deploy)
50-
cmd_deploy "${cmd_args[@]}"
50+
# Check for --dry-run flag
51+
local DRY_RUN=false
52+
local DEPLOY_ARGS=()
53+
for arg in "${cmd_args[@]}"; do
54+
if [ "$arg" = "--dry-run" ]; then
55+
DRY_RUN=true
56+
else
57+
DEPLOY_ARGS+=("$arg")
58+
fi
59+
done
60+
if [ "$DRY_RUN" = true ]; then
61+
cmd_deploy_dry_run "${DEPLOY_ARGS[@]}"
62+
else
63+
cmd_deploy "${DEPLOY_ARGS[@]}"
64+
fi
5165
;;
5266
doctor)
5367
cmd_doctor "${cmd_args[@]}"

0 commit comments

Comments
 (0)