@keepur/hive 0.1.10 → 0.2.1
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 +13 -0
- package/install/bootstrap.sh +49 -0
- package/install/migrate-0.2.fixtures/loose-files.txt +24 -0
- package/install/migrate-0.2.sh +822 -0
- package/install/migrate-0.2.test.sh +122 -0
- package/package.json +4 -2
- package/pkg/cli.min.js +138 -136
- package/pkg/server.min.js +157 -156
- package/service/deploy-check.sh +88 -0
- package/service/deploy.sh +476 -0
- package/service/deploy.test.sh +208 -0
- package/service/install.sh +64 -0
- package/service/instances.conf +11 -0
- package/service/rotate-logs.sh +43 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Deploy check — per-instance version compare against npm
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Reads each instance's pinned tag from instances.conf. For each instance whose
|
|
9
|
+
# installed version (from .hive/package.json) differs from the tag it's pinned to,
|
|
10
|
+
# invokes deploy.sh --instance=<id> --tag=<tag>.
|
|
11
|
+
#
|
|
12
|
+
# Pinned "latest" compares against the npm `latest` dist-tag — so unpinned
|
|
13
|
+
# instances autoupgrade whenever a new @keepur/hive is published.
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
BUILD_DIR="${BUILD_DIR:-$HOME/build/hive}"
|
|
18
|
+
DEPLOY_DIR="${DEPLOY_DIR:-$HOME/services/hive}"
|
|
19
|
+
INSTANCES_CONF="$SCRIPT_DIR/instances.conf"
|
|
20
|
+
|
|
21
|
+
cd "$BUILD_DIR"
|
|
22
|
+
[[ "$(git branch --show-current)" == "deploy" ]] || { echo "ERROR: Build dir not on deploy branch"; exit 1; }
|
|
23
|
+
|
|
24
|
+
echo "Checking for updates on deploy branch..."
|
|
25
|
+
git fetch origin deploy --quiet
|
|
26
|
+
|
|
27
|
+
# --- Load instances ---
|
|
28
|
+
declare -a INSTANCES=()
|
|
29
|
+
while IFS='|' read -r id config _agents _label logs_dir ports engine_tag; do
|
|
30
|
+
[[ "$id" =~ ^[[:space:]]*# ]] && continue
|
|
31
|
+
[[ -z "$id" ]] && continue
|
|
32
|
+
id=$(echo "$id" | xargs)
|
|
33
|
+
engine_tag=$(echo "${engine_tag:-}" | xargs)
|
|
34
|
+
INSTANCES+=("$id|${engine_tag:-latest}")
|
|
35
|
+
done < "$INSTANCES_CONF"
|
|
36
|
+
|
|
37
|
+
UPDATES_NEEDED=()
|
|
38
|
+
for inst in "${INSTANCES[@]}"; do
|
|
39
|
+
IFS='|' read -r id tag <<< "$inst"
|
|
40
|
+
version="${tag#v}"
|
|
41
|
+
# Per-instance root: $DEPLOY_DIR/<id> if it exists (post-Phase-5 layout),
|
|
42
|
+
# else $DEPLOY_DIR (today's shared-dir primary layout).
|
|
43
|
+
if [[ -d "$DEPLOY_DIR/$id" ]]; then
|
|
44
|
+
instance_root="$DEPLOY_DIR/$id"
|
|
45
|
+
else
|
|
46
|
+
instance_root="$DEPLOY_DIR"
|
|
47
|
+
fi
|
|
48
|
+
installed=$(jq -r .version < "$instance_root/.hive/package.json" 2>/dev/null || echo "unknown")
|
|
49
|
+
if [[ "$version" == "latest" ]]; then
|
|
50
|
+
target=$(npm view @keepur/hive version 2>/dev/null || echo "unknown")
|
|
51
|
+
else
|
|
52
|
+
target=$(npm view "@keepur/hive@$version" version 2>/dev/null || echo "unknown")
|
|
53
|
+
fi
|
|
54
|
+
if [[ "$target" == "unknown" ]]; then
|
|
55
|
+
echo " [$id] could not resolve target version (pinned: $tag). Skipping."
|
|
56
|
+
continue
|
|
57
|
+
fi
|
|
58
|
+
if [[ "$installed" == "$target" ]]; then
|
|
59
|
+
echo " [$id] up to date ($installed)."
|
|
60
|
+
else
|
|
61
|
+
echo " [$id] $installed → $target (pinned: $tag)"
|
|
62
|
+
UPDATES_NEEDED+=("$id|$target")
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
if [[ ${#UPDATES_NEEDED[@]} -eq 0 ]]; then
|
|
67
|
+
echo "All instances up to date. Nothing to deploy."
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo ""
|
|
72
|
+
echo "Updates needed:"
|
|
73
|
+
for u in "${UPDATES_NEEDED[@]}"; do
|
|
74
|
+
IFS='|' read -r id target <<< "$u"
|
|
75
|
+
echo " - $id → $target"
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
# Deploy each one. deploy.sh handles build once per invocation, so we loop.
|
|
79
|
+
# Rationale for one-at-a-time: build-phase is shared, but the build is cheap
|
|
80
|
+
# to re-run and keeping it inside deploy.sh keeps one script responsible for
|
|
81
|
+
# the full flow. If this becomes a performance issue, split build into a
|
|
82
|
+
# separate phase invoked once up front.
|
|
83
|
+
for u in "${UPDATES_NEEDED[@]}"; do
|
|
84
|
+
IFS='|' read -r id target <<< "$u"
|
|
85
|
+
echo ""
|
|
86
|
+
echo "=== Deploying $id → $target ==="
|
|
87
|
+
"$SCRIPT_DIR/deploy.sh" --instance="$id" --tag="$target"
|
|
88
|
+
done
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Hive Deploy — build once, deploy to all instances
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
8
|
+
# --- Configuration ---
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
BUILD_DIR="${BUILD_DIR:-$HOME/build/hive}"
|
|
11
|
+
DEPLOY_DIR="${DEPLOY_DIR:-$HOME/services/hive}"
|
|
12
|
+
INSTANCES_CONF="$SCRIPT_DIR/instances.conf"
|
|
13
|
+
|
|
14
|
+
# --- Flags ---
|
|
15
|
+
DRY_RUN=false
|
|
16
|
+
ROLLBACK=false
|
|
17
|
+
FILTER_INSTANCE=""
|
|
18
|
+
OVERRIDE_TAG=""
|
|
19
|
+
for arg in "$@"; do
|
|
20
|
+
case "$arg" in
|
|
21
|
+
--dry-run) DRY_RUN=true ;;
|
|
22
|
+
--rollback) ROLLBACK=true ;;
|
|
23
|
+
--instance=*) FILTER_INSTANCE="${arg#--instance=}" ;;
|
|
24
|
+
--tag=*) OVERRIDE_TAG="${arg#--tag=}" ;;
|
|
25
|
+
*)
|
|
26
|
+
echo "ERROR: unknown arg: $arg" >&2
|
|
27
|
+
echo "Usage: deploy.sh [--dry-run] [--rollback] [--instance=<id>] [--tag=<tag>]" >&2
|
|
28
|
+
exit 2
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
# --- Notification config (from dodi's .env — the primary instance) ---
|
|
34
|
+
# shellcheck source=/dev/null
|
|
35
|
+
source "$DEPLOY_DIR/.env"
|
|
36
|
+
: "${SLACK_BOT_TOKEN:?SLACK_BOT_TOKEN not set in .env}"
|
|
37
|
+
: "${DEVOPS_CHANNEL_ID:?DEVOPS_CHANNEL_ID not set in .env}"
|
|
38
|
+
|
|
39
|
+
# --- Load instances ---
|
|
40
|
+
declare -a INSTANCES=()
|
|
41
|
+
while IFS='|' read -r id config _agents_path label logs_dir ports engine_tag; do
|
|
42
|
+
[[ "$id" =~ ^[[:space:]]*# ]] && continue # skip comments
|
|
43
|
+
[[ -z "$id" ]] && continue # skip blank lines
|
|
44
|
+
# Trim whitespace
|
|
45
|
+
id=$(echo "$id" | xargs)
|
|
46
|
+
config=$(echo "$config" | xargs)
|
|
47
|
+
label=$(echo "$label" | xargs)
|
|
48
|
+
logs_dir=$(echo "$logs_dir" | xargs)
|
|
49
|
+
ports=$(echo "$ports" | xargs)
|
|
50
|
+
engine_tag=$(echo "${engine_tag:-}" | xargs)
|
|
51
|
+
INSTANCES+=("$id|$config|$label|$logs_dir|$ports|$engine_tag")
|
|
52
|
+
done < "$INSTANCES_CONF"
|
|
53
|
+
|
|
54
|
+
if [[ ${#INSTANCES[@]} -eq 0 ]]; then
|
|
55
|
+
echo "ERROR: No instances found in $INSTANCES_CONF"
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo "=== Hive Deploy (${#INSTANCES[@]} instances) ==="
|
|
60
|
+
for inst in "${INSTANCES[@]}"; do
|
|
61
|
+
echo " - $(echo "$inst" | cut -d'|' -f1)"
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
# --- Helpers ---
|
|
65
|
+
run_cmd() {
|
|
66
|
+
if $DRY_RUN; then
|
|
67
|
+
echo "[DRY RUN] $*"
|
|
68
|
+
else
|
|
69
|
+
"$@"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
notify() {
|
|
74
|
+
local message="$1"
|
|
75
|
+
if $DRY_RUN; then
|
|
76
|
+
echo "[DRY RUN] notify: $message"
|
|
77
|
+
return
|
|
78
|
+
fi
|
|
79
|
+
local payload
|
|
80
|
+
payload=$(jq -n --arg channel "$DEVOPS_CHANNEL_ID" --arg text "$message" \
|
|
81
|
+
'{channel: $channel, text: $text}')
|
|
82
|
+
curl -s -X POST https://slack.com/api/chat.postMessage \
|
|
83
|
+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
|
84
|
+
-H "Content-Type: application/json" \
|
|
85
|
+
-d "$payload" \
|
|
86
|
+
> /dev/null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
health_check() {
|
|
90
|
+
local log_file="$1"
|
|
91
|
+
if $DRY_RUN; then
|
|
92
|
+
echo "[DRY RUN] health_check: would check $log_file"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
for _ in $(seq 1 30); do
|
|
96
|
+
sleep 1
|
|
97
|
+
if tail -5 "$log_file" 2>/dev/null | grep -q '"Hive is running"'; then
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
done
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
kill_ports() {
|
|
105
|
+
local ports_str="$1"
|
|
106
|
+
if $DRY_RUN; then
|
|
107
|
+
echo "[DRY RUN] kill_ports: would scan $ports_str"
|
|
108
|
+
return
|
|
109
|
+
fi
|
|
110
|
+
for port in $ports_str; do
|
|
111
|
+
local pids
|
|
112
|
+
pids=$(lsof -i :"$port" -t 2>/dev/null || true)
|
|
113
|
+
if [[ -n "$pids" ]]; then
|
|
114
|
+
echo " Killing stale process(es) on port $port: $pids"
|
|
115
|
+
echo "$pids" | xargs kill -9 2>/dev/null || true
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
sleep 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# --- Engine fetch/swap/rollback (KPR-53 / Phase 3) ---
|
|
122
|
+
|
|
123
|
+
# Resolve a tag to an npm-facing bare-semver version string.
|
|
124
|
+
# "v0.2.0" → "0.2.0"; "latest" → "latest"; "0.2.0" → "0.2.0"
|
|
125
|
+
_normalize_tag() {
|
|
126
|
+
local tag="${1:-latest}"
|
|
127
|
+
if [[ "$tag" == "latest" ]]; then
|
|
128
|
+
echo "latest"
|
|
129
|
+
else
|
|
130
|
+
echo "${tag#v}"
|
|
131
|
+
fi
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# _instance_root <id>
|
|
135
|
+
# Returns the instance root dir: $DEPLOY_DIR/<id> if that dir exists (post-Phase-5
|
|
136
|
+
# per-instance layout), else $DEPLOY_DIR (today's primary-shared-dir layout).
|
|
137
|
+
# Lets deploy.sh be per-instance-dir aware now so Phase 5's migration is a no-op
|
|
138
|
+
# at the script level.
|
|
139
|
+
_instance_root() {
|
|
140
|
+
local id="$1"
|
|
141
|
+
if [[ -d "$DEPLOY_DIR/$id" ]]; then
|
|
142
|
+
echo "$DEPLOY_DIR/$id"
|
|
143
|
+
else
|
|
144
|
+
echo "$DEPLOY_DIR"
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# fetch_engine <instance_dir> <tag>
|
|
149
|
+
# Populates <instance_dir>/.hive.next/ with the tag's contents.
|
|
150
|
+
# Primary path: npm pack @keepur/hive@<version> + tar -xzf --strip-components=1.
|
|
151
|
+
# Fallback: rsync from $BUILD_DIR (developer-ergonomics path), only when npm pack
|
|
152
|
+
# actually fails (not merely when the tag isn't in the registry — transient
|
|
153
|
+
# registry errors should surface, not silently swap to rsync).
|
|
154
|
+
fetch_engine() {
|
|
155
|
+
local instance_dir="$1"
|
|
156
|
+
local tag="$2"
|
|
157
|
+
local version
|
|
158
|
+
version=$(_normalize_tag "$tag")
|
|
159
|
+
|
|
160
|
+
if $DRY_RUN; then
|
|
161
|
+
echo "[DRY RUN] fetch_engine: would populate $instance_dir/.hive.next/ from @keepur/hive@$version"
|
|
162
|
+
return 0
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
rm -rf "$instance_dir/.hive.next"
|
|
166
|
+
mkdir -p "$instance_dir/.hive.next"
|
|
167
|
+
|
|
168
|
+
local packdir
|
|
169
|
+
packdir=$(mktemp -d)
|
|
170
|
+
local tarball
|
|
171
|
+
# npm pack prints the tarball filename on the last line of stdout.
|
|
172
|
+
# Run from a temp dir so the tarball doesn't litter $PWD.
|
|
173
|
+
echo " fetch_engine: npm pack @keepur/hive@$version"
|
|
174
|
+
tarball=$(cd "$packdir" && npm pack "@keepur/hive@$version" 2>/dev/null | tail -n1)
|
|
175
|
+
|
|
176
|
+
if [[ -n "$tarball" && -f "$packdir/$tarball" ]]; then
|
|
177
|
+
tar -xzf "$packdir/$tarball" --strip-components=1 -C "$instance_dir/.hive.next/"
|
|
178
|
+
rm -rf "$packdir"
|
|
179
|
+
else
|
|
180
|
+
rm -rf "$packdir"
|
|
181
|
+
echo " fetch_engine: npm pack failed; falling back to rsync from $BUILD_DIR" >&2
|
|
182
|
+
local src="$BUILD_DIR"
|
|
183
|
+
if [[ ! -f "$src/pkg/server.min.js" ]]; then
|
|
184
|
+
rm -rf "$instance_dir/.hive.next"
|
|
185
|
+
echo "ERROR: npm pack @keepur/hive@$version failed and fallback needs $src/pkg/server.min.js — run 'npm run bundle' in $src" >&2
|
|
186
|
+
return 1
|
|
187
|
+
fi
|
|
188
|
+
rsync -a --delete "$src/pkg/" "$instance_dir/.hive.next/pkg/"
|
|
189
|
+
rsync -a --delete "$src/seeds/" "$instance_dir/.hive.next/seeds/"
|
|
190
|
+
rsync -a --delete "$src/templates/" "$instance_dir/.hive.next/templates/"
|
|
191
|
+
rsync -a --delete "$src/install/" "$instance_dir/.hive.next/install/"
|
|
192
|
+
rsync -a --delete "$src/service/" "$instance_dir/.hive.next/service/"
|
|
193
|
+
# scripts/honeypot is a single binary, not the whole scripts/ dir
|
|
194
|
+
mkdir -p "$instance_dir/.hive.next/scripts"
|
|
195
|
+
[[ -f "$src/scripts/honeypot" ]] && cp "$src/scripts/honeypot" "$instance_dir/.hive.next/scripts/honeypot"
|
|
196
|
+
cp "$src/package.json" "$instance_dir/.hive.next/"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# Sanity check — if the tarball/rsync was broken, catch it before the swap.
|
|
200
|
+
if [[ ! -f "$instance_dir/.hive.next/pkg/server.min.js" ]]; then
|
|
201
|
+
rm -rf "$instance_dir/.hive.next"
|
|
202
|
+
echo "ERROR: .hive.next/pkg/server.min.js missing after fetch_engine" >&2
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# install_engine_deps <instance_dir>
|
|
208
|
+
# Runs `npm install --omit=dev` inside .hive.next/ so the bundle's runtime
|
|
209
|
+
# externals (native modules, large SDKs, asset loaders — 14 of them) resolve
|
|
210
|
+
# from .hive/pkg/. Node walks up to .hive/node_modules/ to find them.
|
|
211
|
+
# Mirrors src/setup/populate-engine.ts so `hive init` and `hive update`
|
|
212
|
+
# produce byte-identical .hive/ layouts.
|
|
213
|
+
install_engine_deps() {
|
|
214
|
+
local instance_dir="$1"
|
|
215
|
+
if $DRY_RUN; then
|
|
216
|
+
echo "[DRY RUN] install_engine_deps: would npm install --omit=dev in $instance_dir/.hive.next/"
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
if [[ ! -f "$instance_dir/.hive.next/package.json" ]]; then
|
|
220
|
+
echo "ERROR: install_engine_deps needs $instance_dir/.hive.next/package.json" >&2
|
|
221
|
+
return 1
|
|
222
|
+
fi
|
|
223
|
+
echo " install_engine_deps: npm install --omit=dev in .hive.next/"
|
|
224
|
+
(cd "$instance_dir/.hive.next" && npm install --omit=dev --no-audit --no-fund --no-progress >&2)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# swap_engine <instance_dir>
|
|
228
|
+
# Rotates: old .hive.prev → dropped; live .hive → .hive.prev; .hive.next → .hive.
|
|
229
|
+
# Assumes the service is already stopped. The ~50ms window where .hive/ doesn't
|
|
230
|
+
# exist is covered by the service being down.
|
|
231
|
+
swap_engine() {
|
|
232
|
+
local instance_dir="$1"
|
|
233
|
+
if $DRY_RUN; then
|
|
234
|
+
echo "[DRY RUN] swap_engine: would rotate $instance_dir/.hive{,.prev,.next}"
|
|
235
|
+
return 0
|
|
236
|
+
fi
|
|
237
|
+
if [[ ! -d "$instance_dir/.hive.next" ]]; then
|
|
238
|
+
echo "ERROR: swap_engine called but $instance_dir/.hive.next does not exist" >&2
|
|
239
|
+
return 1
|
|
240
|
+
fi
|
|
241
|
+
if [[ -d "$instance_dir/.hive" ]]; then
|
|
242
|
+
rm -rf "$instance_dir/.hive.prev" # drop older backup
|
|
243
|
+
mv "$instance_dir/.hive" "$instance_dir/.hive.prev"
|
|
244
|
+
fi
|
|
245
|
+
mv "$instance_dir/.hive.next" "$instance_dir/.hive"
|
|
246
|
+
# Clear any .hive.broken/ from a previous failed rollback — this is a successful deploy.
|
|
247
|
+
rm -rf "$instance_dir/.hive.broken"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# rollback_engine <instance_dir>
|
|
251
|
+
# Manual rollback OR auto-rollback after health-check failure.
|
|
252
|
+
# Moves the failed engine to .hive.broken/ (preserved for inspection) and
|
|
253
|
+
# restores .hive.prev → .hive.
|
|
254
|
+
rollback_engine() {
|
|
255
|
+
local instance_dir="$1"
|
|
256
|
+
if $DRY_RUN; then
|
|
257
|
+
if [[ ! -d "$instance_dir/.hive.prev" ]]; then
|
|
258
|
+
echo "[DRY RUN] rollback_engine: would fail — no $instance_dir/.hive.prev"
|
|
259
|
+
return 1
|
|
260
|
+
fi
|
|
261
|
+
echo "[DRY RUN] rollback_engine: would swap $instance_dir/.hive ↔ .hive.prev (failed engine → .hive.broken)"
|
|
262
|
+
return 0
|
|
263
|
+
fi
|
|
264
|
+
if [[ ! -d "$instance_dir/.hive.prev" ]]; then
|
|
265
|
+
echo "ERROR: no previous engine at $instance_dir/.hive.prev — rollback unavailable" >&2
|
|
266
|
+
return 1
|
|
267
|
+
fi
|
|
268
|
+
rm -rf "$instance_dir/.hive.broken"
|
|
269
|
+
if [[ -d "$instance_dir/.hive" ]]; then
|
|
270
|
+
mv "$instance_dir/.hive" "$instance_dir/.hive.broken"
|
|
271
|
+
fi
|
|
272
|
+
mv "$instance_dir/.hive.prev" "$instance_dir/.hive"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# --- Short-circuit: --rollback mode ---
|
|
276
|
+
# Rollback is per-instance; requires --instance=<id> so we know which to roll.
|
|
277
|
+
# No build phase, no notify until after the swap.
|
|
278
|
+
if $ROLLBACK; then
|
|
279
|
+
if [[ -z "$FILTER_INSTANCE" ]]; then
|
|
280
|
+
echo "ERROR: --rollback requires --instance=<id>" >&2
|
|
281
|
+
exit 2
|
|
282
|
+
fi
|
|
283
|
+
# Find the matching instance row so we know its logs dir + label.
|
|
284
|
+
ROLLBACK_ROW=""
|
|
285
|
+
for inst in "${INSTANCES[@]}"; do
|
|
286
|
+
IFS='|' read -r id _config _label _logs _ports _tag <<< "$inst"
|
|
287
|
+
if [[ "$id" == "$FILTER_INSTANCE" ]]; then
|
|
288
|
+
ROLLBACK_ROW="$inst"
|
|
289
|
+
break
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
if [[ -z "$ROLLBACK_ROW" ]]; then
|
|
293
|
+
echo "ERROR: no instance '$FILTER_INSTANCE' in $INSTANCES_CONF" >&2
|
|
294
|
+
exit 2
|
|
295
|
+
fi
|
|
296
|
+
IFS='|' read -r id _config label logs_dir ports _tag <<< "$ROLLBACK_ROW"
|
|
297
|
+
instance_root=$(_instance_root "$id")
|
|
298
|
+
echo "--- Rolling back $id (root: $instance_root) ---"
|
|
299
|
+
# Stop LaunchAgent BEFORE rotating .hive/. Without -kp, launchd auto-respawns
|
|
300
|
+
# mid-rollback and the restart picks up a partial state. Mirrors the deploy
|
|
301
|
+
# loop's stop sequence at the main branch.
|
|
302
|
+
echo " Stopping $label..."
|
|
303
|
+
run_cmd launchctl kickstart -kp "gui/$(id -u)/$label" 2>/dev/null || true
|
|
304
|
+
kill_ports "$ports"
|
|
305
|
+
if ! rollback_engine "$instance_root"; then
|
|
306
|
+
notify "Rollback FAILED for \`$id\`: no previous engine (.hive.prev missing)."
|
|
307
|
+
exit 1
|
|
308
|
+
fi
|
|
309
|
+
run_cmd launchctl kickstart -k "gui/$(id -u)/$label"
|
|
310
|
+
if health_check "$instance_root/$logs_dir/hive.log"; then
|
|
311
|
+
rollback_version=$(jq -r .version < "$instance_root/.hive/package.json" 2>/dev/null || echo "unknown")
|
|
312
|
+
notify "Rollback succeeded for \`$id\` → \`$rollback_version\`."
|
|
313
|
+
echo "Rollback complete."
|
|
314
|
+
exit 0
|
|
315
|
+
else
|
|
316
|
+
notify "Rollback succeeded but health check failed for \`$id\`. Check logs."
|
|
317
|
+
exit 1
|
|
318
|
+
fi
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# --- Guard: shared instance_root with diverging pins ---
|
|
322
|
+
# When multiple instances resolve to the same instance_root (today: dodi+personal
|
|
323
|
+
# both share $DEPLOY_DIR until Phase 5 migrates them into per-instance dirs),
|
|
324
|
+
# they share a single .hive/ — their ENGINE_TAG pins MUST agree. Otherwise
|
|
325
|
+
# per-instance deploys silently clobber each other and deploy-check.sh
|
|
326
|
+
# oscillates between versions on every poll. Compares the configured pins
|
|
327
|
+
# (not the effective tag for this run) so even an explicit --tag override
|
|
328
|
+
# can't sneak past a misconfigured pinning state. Fail fast before any work.
|
|
329
|
+
declare -a _seen_roots=()
|
|
330
|
+
declare -a _seen_pins=()
|
|
331
|
+
for inst in "${INSTANCES[@]}"; do
|
|
332
|
+
IFS='|' read -r _id _config _label _logs _ports _engine_tag <<< "$inst"
|
|
333
|
+
_root=$(_instance_root "$_id")
|
|
334
|
+
_pin="${_engine_tag:-latest}"
|
|
335
|
+
for i in "${!_seen_roots[@]}"; do
|
|
336
|
+
if [[ "${_seen_roots[$i]}" == "$_root" && "${_seen_pins[$i]}" != "$_pin" ]]; then
|
|
337
|
+
echo "ERROR: instances share root '$_root' but pin different ENGINE_TAGs ('${_seen_pins[$i]}' vs '$_pin')." >&2
|
|
338
|
+
echo " Set the same ENGINE_TAG for all instances under one root, or migrate them to per-instance dirs (Phase 5)." >&2
|
|
339
|
+
exit 2
|
|
340
|
+
fi
|
|
341
|
+
done
|
|
342
|
+
_seen_roots+=("$_root")
|
|
343
|
+
_seen_pins+=("$_pin")
|
|
344
|
+
done
|
|
345
|
+
unset _seen_roots _seen_pins _id _config _label _logs _ports _engine_tag _root _pin i
|
|
346
|
+
|
|
347
|
+
# =============================================================================
|
|
348
|
+
# Phase 1: Build (once, in $BUILD_DIR)
|
|
349
|
+
# =============================================================================
|
|
350
|
+
|
|
351
|
+
echo ""
|
|
352
|
+
echo "--- Phase 1: Build ---"
|
|
353
|
+
# Per-instance current versions are reported in Phase 2 after each health check.
|
|
354
|
+
|
|
355
|
+
echo "Pulling latest in build dir..."
|
|
356
|
+
cd "$BUILD_DIR"
|
|
357
|
+
[[ "$(git branch --show-current)" == "deploy" ]] || { echo "ERROR: Build dir not on deploy branch"; exit 1; }
|
|
358
|
+
run_cmd git pull --ff-only
|
|
359
|
+
|
|
360
|
+
DEPLOY_SHA=$(git rev-parse --short HEAD)
|
|
361
|
+
DEPLOY_MSG=$(git log -1 --pretty=%s)
|
|
362
|
+
|
|
363
|
+
echo "Installing dependencies..."
|
|
364
|
+
if ! run_cmd npm install; then
|
|
365
|
+
notify "Deploy aborted. \`npm install\` failed. Commit: \`$DEPLOY_SHA\`."
|
|
366
|
+
exit 1
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
echo "Running checks..."
|
|
370
|
+
if ! run_cmd npm run check; then
|
|
371
|
+
notify "Deploy aborted. \`npm run check\` failed. Commit: \`$DEPLOY_SHA\`."
|
|
372
|
+
exit 1
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
echo "Building..."
|
|
376
|
+
if ! run_cmd npm run build; then
|
|
377
|
+
notify "Deploy aborted. Build failed. Commit: \`$DEPLOY_SHA\`."
|
|
378
|
+
exit 1
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
echo "Bundling..."
|
|
382
|
+
if ! run_cmd npm run bundle; then
|
|
383
|
+
notify "Deploy aborted. Bundle failed. Commit: \`$DEPLOY_SHA\`."
|
|
384
|
+
exit 1
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
# =============================================================================
|
|
388
|
+
# Phase 2: Deploy each instance
|
|
389
|
+
# =============================================================================
|
|
390
|
+
|
|
391
|
+
FAILED_INSTANCES=()
|
|
392
|
+
|
|
393
|
+
for inst in "${INSTANCES[@]}"; do
|
|
394
|
+
IFS='|' read -r id config label logs_dir ports engine_tag <<< "$inst"
|
|
395
|
+
|
|
396
|
+
# --instance=<id> filter
|
|
397
|
+
if [[ -n "$FILTER_INSTANCE" && "$FILTER_INSTANCE" != "$id" ]]; then
|
|
398
|
+
echo ""
|
|
399
|
+
echo "--- Skipping '$id' (filtered: --instance=$FILTER_INSTANCE) ---"
|
|
400
|
+
continue
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# --tag override > per-instance engine_tag > "latest"
|
|
404
|
+
tag="${OVERRIDE_TAG:-${engine_tag:-latest}}"
|
|
405
|
+
instance_root=$(_instance_root "$id")
|
|
406
|
+
|
|
407
|
+
echo ""
|
|
408
|
+
echo "--- Phase 2: Deploy instance '$id' @ $tag (root: $instance_root) ---"
|
|
409
|
+
echo " config=$config label=$label logs=$logs_dir ports=$ports"
|
|
410
|
+
|
|
411
|
+
mkdir -p "$instance_root/$logs_dir"
|
|
412
|
+
|
|
413
|
+
echo " Stopping $label..."
|
|
414
|
+
run_cmd launchctl kickstart -kp "gui/$(id -u)/$label" 2>/dev/null || true
|
|
415
|
+
kill_ports "$ports"
|
|
416
|
+
|
|
417
|
+
echo " Fetching engine..."
|
|
418
|
+
if ! fetch_engine "$instance_root" "$tag"; then
|
|
419
|
+
notify "Deploy FAILED for \`$id\`: fetch_engine errored at tag \`$tag\`."
|
|
420
|
+
FAILED_INSTANCES+=("$id")
|
|
421
|
+
run_cmd launchctl kickstart -k "gui/$(id -u)/$label" || true # bring old engine back up
|
|
422
|
+
continue
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
echo " Installing engine deps..."
|
|
426
|
+
if ! install_engine_deps "$instance_root"; then
|
|
427
|
+
notify "Deploy FAILED for \`$id\`: install_engine_deps errored at tag \`$tag\`."
|
|
428
|
+
FAILED_INSTANCES+=("$id")
|
|
429
|
+
rm -rf "$instance_root/.hive.next"
|
|
430
|
+
run_cmd launchctl kickstart -k "gui/$(id -u)/$label" || true # bring old engine back up
|
|
431
|
+
continue
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
echo " Swapping engine..."
|
|
435
|
+
swap_engine "$instance_root"
|
|
436
|
+
|
|
437
|
+
echo " Restarting $label..."
|
|
438
|
+
run_cmd launchctl kickstart -k "gui/$(id -u)/$label"
|
|
439
|
+
|
|
440
|
+
echo " Checking health..."
|
|
441
|
+
if ! health_check "$instance_root/$logs_dir/hive.log"; then
|
|
442
|
+
echo " Health check FAILED for $id — rolling back"
|
|
443
|
+
if rollback_engine "$instance_root"; then
|
|
444
|
+
run_cmd launchctl kickstart -k "gui/$(id -u)/$label"
|
|
445
|
+
notify "Deploy rolled back for \`$id\`: \`$tag\` failed health check, restored previous version."
|
|
446
|
+
else
|
|
447
|
+
notify "Deploy FAILED for \`$id\` and auto-rollback unavailable (.hive.prev missing). Manual intervention required."
|
|
448
|
+
fi
|
|
449
|
+
FAILED_INSTANCES+=("$id")
|
|
450
|
+
continue
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
new_version=$(jq -r .version < "$instance_root/.hive/package.json" 2>/dev/null || echo "unknown")
|
|
454
|
+
echo " Instance '$id' is healthy at version $new_version."
|
|
455
|
+
done
|
|
456
|
+
|
|
457
|
+
# =============================================================================
|
|
458
|
+
# Phase 3: Report
|
|
459
|
+
# =============================================================================
|
|
460
|
+
|
|
461
|
+
echo ""
|
|
462
|
+
echo "--- Phase 3: Report ---"
|
|
463
|
+
|
|
464
|
+
if [[ ${#FAILED_INSTANCES[@]} -gt 0 ]]; then
|
|
465
|
+
failed_list=$(printf ", %s" "${FAILED_INSTANCES[@]}")
|
|
466
|
+
failed_list=${failed_list:2}
|
|
467
|
+
notify "Deploy partial. Build commit \`$DEPLOY_SHA\`: $DEPLOY_MSG. Failed instances: $failed_list."
|
|
468
|
+
echo "Deploy completed with failures: $failed_list"
|
|
469
|
+
exit 1
|
|
470
|
+
else
|
|
471
|
+
# Count actual deploy targets (respecting --instance filter)
|
|
472
|
+
deployed=${#INSTANCES[@]}
|
|
473
|
+
[[ -n "$FILTER_INSTANCE" ]] && deployed=1
|
|
474
|
+
notify "Deploy succeeded ($deployed instance(s)). Build commit \`$DEPLOY_SHA\`: $DEPLOY_MSG."
|
|
475
|
+
echo "Deploy complete. $deployed instance(s) running."
|
|
476
|
+
fi
|