-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathsync-deploy.sh
More file actions
executable file
·336 lines (286 loc) · 13.3 KB
/
sync-deploy.sh
File metadata and controls
executable file
·336 lines (286 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
#!/usr/bin/env bash
# Syncs .deploy/ artifacts from local machine to VPS via rsync.
#
# Usage:
# ./scripts/sync-deploy.sh # Stack-level files only (safe for updates)
# ./scripts/sync-deploy.sh --all # Stack files + all instance configs
# ./scripts/sync-deploy.sh --instance <name> # Stack files + one instance's config
# ./scripts/sync-deploy.sh --fresh # Implies --all, prints post-sync next-steps
# ./scripts/sync-deploy.sh --force # Skip drift detection, overwrite live configs
# ./scripts/sync-deploy.sh -n | --dry-run # Preview without transferring
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/source-config.sh"
source "$SCRIPT_DIR/lib/colors.sh"
source "$SCRIPT_DIR/lib/ssh.sh"
source "$SCRIPT_DIR/lib/instances.sh"
# ── Parse args ────────────────────────────────────────────────────────────────
SYNC_INSTANCES="" # "" = none, "all" = all, or a specific name
FRESH=false
FORCE=false
DRY_RUN=false
RSYNC_DRY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--all) SYNC_INSTANCES="all"; shift ;;
--instance) SYNC_INSTANCES="$2"; shift 2 ;;
--fresh) SYNC_INSTANCES="all"; FRESH=true; shift ;;
--force) FORCE=true; shift ;;
-n|--dry-run) DRY_RUN=true; RSYNC_DRY="--dry-run"; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# ── Validate ──────────────────────────────────────────────────────────────────
DEPLOY_DIR="${REPO_ROOT}/.deploy"
if [ ! -d "$DEPLOY_DIR" ]; then
echo "Error: .deploy/ not found. Run 'npm run pre-deploy' first." >&2
exit 1
fi
# Inject --dry-run into do_rsync when requested
RSYNC_EXTRA="$RSYNC_DRY"
# ── Sync stack-level files ────────────────────────────────────────────────────
# Ensure target directories exist on VPS
info "Ensuring target directories on VPS..."
${SSH_CMD} "${VPS}" "sudo mkdir -p ${INSTALL_DIR}/{host,openclaw-stack,setup}"
# .gitignore for deploy tracking repo
GITIGNORE_SRC="${REPO_ROOT}/deploy/vps-gitignore"
if [ -f "$GITIGNORE_SRC" ]; then
info "Syncing .gitignore..."
do_rsync "$GITIGNORE_SRC" "${VPS}:${INSTALL_DIR}/.gitignore"
success ".gitignore"
fi
# Root files (compose, stack.env, stack.json) — no --delete (would nuke siblings)
info "Syncing root files (docker-compose.yml, stack.env, stack.json)..."
do_rsync \
"${DEPLOY_DIR}/docker-compose.yml" \
"${DEPLOY_DIR}/stack.env" \
"${DEPLOY_DIR}/stack.json" \
"${VPS}:${INSTALL_DIR}/"
success "Root files"
# host/ — deploy-managed, --delete to remove stale scripts
info "Syncing host/..."
do_rsync --delete \
"${DEPLOY_DIR}/host/" \
"${VPS}:${INSTALL_DIR}/host/"
success "host/"
# openclaw-stack/ — deploy-managed, --delete to remove stale files
info "Syncing openclaw-stack/..."
do_rsync --delete \
"${DEPLOY_DIR}/openclaw-stack/" \
"${VPS}:${INSTALL_DIR}/openclaw-stack/"
success "openclaw-stack/"
# setup/ — deploy-managed, --delete
info "Syncing setup/..."
do_rsync --delete \
"${DEPLOY_DIR}/setup/" \
"${VPS}:${INSTALL_DIR}/setup/"
success "setup/"
# egress-proxy/ — deploy-managed, single file
if [ -d "${DEPLOY_DIR}/egress-proxy" ]; then
info "Syncing egress-proxy/..."
${SSH_CMD} "${VPS}" "sudo mkdir -p ${INSTALL_DIR}/egress-proxy"
do_rsync --delete \
"${DEPLOY_DIR}/egress-proxy/" \
"${VPS}:${INSTALL_DIR}/egress-proxy/"
success "egress-proxy/"
fi
# sandbox-registry/ — htpasswd auth file
if [ -d "${DEPLOY_DIR}/sandbox-registry" ]; then
info "Syncing sandbox-registry/..."
${SSH_CMD} "${VPS}" "sudo mkdir -p ${INSTALL_DIR}/sandbox-registry/data"
do_rsync --delete --exclude='data/' \
"${DEPLOY_DIR}/sandbox-registry/" \
"${VPS}:${INSTALL_DIR}/sandbox-registry/"
success "sandbox-registry/"
fi
# vector/vector.yaml — single file, protect vector/data/
if [ -f "${DEPLOY_DIR}/vector/vector.yaml" ]; then
info "Syncing vector/vector.yaml..."
${SSH_CMD} "${VPS}" "sudo mkdir -p ${INSTALL_DIR}/vector"
do_rsync \
"${DEPLOY_DIR}/vector/vector.yaml" \
"${VPS}:${INSTALL_DIR}/vector/vector.yaml"
success "vector/vector.yaml"
fi
# Fix ownership on stack-level files (all owned by openclaw)
info "Fixing ownership on stack-level files..."
${SSH_CMD} "${VPS}" "sudo chown -R openclaw:openclaw \
${INSTALL_DIR}/docker-compose.yml \
${INSTALL_DIR}/stack.env \
${INSTALL_DIR}/stack.json \
${INSTALL_DIR}/host \
${INSTALL_DIR}/openclaw-stack \
${INSTALL_DIR}/setup && \
[ -f ${INSTALL_DIR}/.gitignore ] && sudo chown openclaw:openclaw ${INSTALL_DIR}/.gitignore || true"
if [ -f "${DEPLOY_DIR}/vector/vector.yaml" ]; then
${SSH_CMD} "${VPS}" "sudo chown openclaw:openclaw ${INSTALL_DIR}/vector/vector.yaml"
fi
if [ -d "${DEPLOY_DIR}/egress-proxy" ]; then
${SSH_CMD} "${VPS}" "sudo chown -R openclaw:openclaw ${INSTALL_DIR}/egress-proxy"
fi
if [ -d "${DEPLOY_DIR}/sandbox-registry" ]; then
${SSH_CMD} "${VPS}" "sudo chown -R openclaw:openclaw ${INSTALL_DIR}/sandbox-registry"
fi
success "Ownership fixed"
# ── Sync per-instance configs ─────────────────────────────────────────────────
if [ -n "$SYNC_INSTANCES" ]; then
resolve_instance_list "$SYNC_INSTANCES"
CONFIG_HASH="${DEPLOY_DIR}/openclaw-stack/config-hash.mjs"
CONFIG_DIFF="${DEPLOY_DIR}/openclaw-stack/config-diff.mjs"
TMP_DIR="${DEPLOY_DIR}/.tmp"
DRIFT_DETECTED=false
RESTART_SUMMARY="" # "instance:key1,key2\n..." accumulated across loop
RESTART_REQUIRED_FILE="${DEPLOY_DIR}/.restart-required"
rm -f "$RESTART_REQUIRED_FILE"
for name in $INSTANCE_LIST; do
# Resolve local config: openclaw/<claw>/openclaw.jsonc (source of truth)
# Always .jsonc locally, uploaded as .json remotely (both support comments).
local_dir="${REPO_ROOT}/openclaw/${name}"
local_file="${local_dir}/openclaw.jsonc"
# Normalize: rename .json → .jsonc if needed
if [ ! -f "$local_file" ] && [ -f "${local_dir}/openclaw.json" ]; then
mv "${local_dir}/openclaw.json" "$local_file"
info "Renamed openclaw/${name}/openclaw.json → openclaw.jsonc"
fi
if [ ! -f "$local_file" ]; then
warn "No openclaw.jsonc for '${name}' — copy openclaw/default/openclaw.jsonc to openclaw/${name}/openclaw.jsonc"
continue
fi
remote_dir="${INSTALL_DIR}/instances/${name}/.openclaw"
live_version="${REPO_ROOT}/openclaw/${name}/openclaw.live-version.jsonc"
# Ensure remote directory exists and fix permissions to match setup-infra.sh:
# instances/<name>/ → openclaw:openclaw 755 (host scripts can traverse)
# .openclaw/ → 1000:1000 700 (container's node user, private data)
${SSH_CMD} "${VPS}" "sudo mkdir -p ${remote_dir} && \
sudo chown openclaw:openclaw ${INSTALL_DIR}/instances/${name} && \
sudo chown 1000:1000 ${remote_dir} && \
sudo chmod 700 ${remote_dir}"
# Per-claw temp directory: .deploy/.tmp/<claw-name>/
# Contains: upload.json (resolved local), live.json (downloaded from VPS),
# live-resolved.json (live with ${VAR} refs resolved for comparison)
claw_tmp="${TMP_DIR}/${name}"
rm -rf "$claw_tmp"
mkdir -p "$claw_tmp"
# Build resolved upload file — needed for both drift detection and upload.
# Resolves ALL ${VAR} refs using the claw's docker-compose env vars so the
# uploaded file has concrete values matching the container runtime.
RESOLVE_SCRIPT="${DEPLOY_DIR}/openclaw-stack/resolve-config-vars.mjs"
upload_file="${claw_tmp}/upload.json"
node "$RESOLVE_SCRIPT" "$local_file" "$name" > "$upload_file"
# Download live config for drift detection and restart-required analysis.
has_live_config=false
do_rsync \
--include='openclaw.json' --exclude='*' \
"${VPS}:${remote_dir}/" "$claw_tmp/" 2>/dev/null || true
# rsync downloads as openclaw.json — rename to live.json for clarity
if [ -f "$claw_tmp/openclaw.json" ]; then
mv "$claw_tmp/openclaw.json" "$claw_tmp/live.json"
has_live_config=true
fi
if ! $FRESH && ! $FORCE && $has_live_config; then
# Drift detection — compare what we'd upload vs what's live on VPS.
# Resolve ${VAR} refs in the live file too (it may predate resolve-all uploads),
# then hash both with config-hash (normalized: sorted keys, no meta, compact JSON).
node "$RESOLVE_SCRIPT" "$claw_tmp/live.json" "$name" > "$claw_tmp/live-resolved.json"
upload_hash=$(node "$CONFIG_HASH" "$upload_file")
live_hash=$(node "$CONFIG_HASH" "$claw_tmp/live-resolved.json")
if [ "$upload_hash" != "$live_hash" ]; then
# Drift: download live-version with diff for user review
rm -f "$live_version"
"${SCRIPT_DIR}/sync-down-configs.sh" --instance "$name"
warn "Config drift detected for '${name}'!"
warn " Review: openclaw/${name}/openclaw.live-version.jsonc"
warn " Re-run with --force to overwrite."
DRIFT_DETECTED=true
continue
fi
fi
# No drift — clean up any stale live-version from previous drift
rm -f "$live_version"
info "Syncing instance config: ${name}..."
do_rsync "$upload_file" "${VPS}:${remote_dir}/openclaw.json"
${SSH_CMD} "${VPS}" "sudo chown 1000:1000 ${remote_dir}/openclaw.json"
# Detect restart-required changes by comparing live config to uploaded config
if $has_live_config; then
diff_json=$(node "$CONFIG_DIFF" "$claw_tmp/live.json" "$upload_file" 2>/dev/null) || diff_json=""
if [ -n "$diff_json" ]; then
restart_keys=$(echo "$diff_json" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
if (d.restartRequired) process.stdout.write(d.restartKeys.join(','));
" 2>/dev/null) || restart_keys=""
if [ -n "$restart_keys" ]; then
RESTART_SUMMARY="${RESTART_SUMMARY}${name}:${restart_keys}\n"
fi
# Log hot-reload changes for visibility
hot_keys=$(echo "$diff_json" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
if (d.hotReloadKeys.length) process.stdout.write(d.hotReloadKeys.join(','));
" 2>/dev/null) || hot_keys=""
if [ -n "$hot_keys" ]; then
info " Hot-reloaded: ${hot_keys}"
fi
fi
fi
success "openclaw/${name}/openclaw.jsonc → instances/${name}/.openclaw/openclaw.json"
done
if $DRIFT_DETECTED; then
err "Deploy aborted — config drift detected (see warnings above)."
exit 1
fi
# Write restart-required summary if any instances need it
if [ -n "$RESTART_SUMMARY" ]; then
printf "%b" "$RESTART_SUMMARY" > "$RESTART_REQUIRED_FILE"
fi
fi
# ── Deploy tracking (diff + auto-commit) ─────────────────────────────────────
if ! $DRY_RUN; then
info "Deploy diff..."
${SSH_CMD} "${VPS}" "sudo -u openclaw bash -c 'cd ${INSTALL_DIR} && \
if [ -d .git ]; then \
git add -A && \
DIFF=\$(git diff --cached --stat) && \
if [ -n \"\$DIFF\" ]; then echo \"\$DIFF\"; else echo \"(no changes)\"; fi; \
else \
echo \"(deploy tracking not initialized — run setup-infra.sh first)\"; \
fi'"
# Auto-commit if there are staged changes
${SSH_CMD} "${VPS}" "sudo -u openclaw bash -c 'cd ${INSTALL_DIR} && \
if [ -d .git ] && ! git diff --cached --quiet 2>/dev/null; then \
git commit -m \"deploy: sync \$(date -u +%Y-%m-%dT%H:%M:%SZ)\"; \
fi'"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
if $DRY_RUN; then
echo "Dry run complete — no files transferred."
else
success "Sync complete → ${VPS}:${INSTALL_DIR}/"
if $FRESH; then
echo ""
echo "Fresh deployment — next steps:"
echo " 1. Run setup-infra.sh to create directories and clone the repo:"
echo " ssh ... \"env INSTANCE_NAMES='...' bash ${INSTALL_DIR}/setup/setup-infra.sh\""
echo " 2. Start claws:"
echo " ssh ... \"bash ${INSTALL_DIR}/host/start-claws.sh\""
fi
# Restart-required summary
if [ -f "${DEPLOY_DIR}/.restart-required" ]; then
echo ""
# Collect instance names and all changed keys
restart_instances=""
all_restart_keys=""
while IFS=: read -r inst keys; do
restart_instances="${restart_instances:+${restart_instances}, }${inst}"
for k in $(echo "$keys" | tr ',' ' '); do
case ",$all_restart_keys," in
*",$k,"*) ;; # already listed
*) all_restart_keys="${all_restart_keys:+${all_restart_keys}, }${k}" ;;
esac
done
done < "${DEPLOY_DIR}/.restart-required"
warn "Restart required for: ${restart_instances}"
warn " Changed keys: ${all_restart_keys}"
warn " Run: sudo -u openclaw bash -c 'cd ${INSTALL_DIR} && docker compose up -d --force-recreate'"
fi
# Tip: use scripts/deploy.sh for full deploy (includes workspace sync + auto-restart)
fi