vela 0.11.2 → 0.11.4
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 +5 -1
- package/dist/bin.js +472 -285
- package/dist/bin.js.map +4 -4
- package/package.json +8 -5
- package/templates/minimal/vitest.config.ts +3 -0
- package/templates/server/apply.sh +55 -6
- package/templates/server/destroy.sh +49 -6
- package/templates/server/lib.sh +98 -4
- package/templates/server/origin.sh +2 -0
- package/templates/server/restore.sh +7 -4
- package/templates/server/rollback.sh +10 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vela",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A CLI for creating and updating SvelteKit projects",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,17 +24,19 @@
|
|
|
24
24
|
"build": "node --experimental-strip-types scripts/build.ts",
|
|
25
25
|
"dev": "node --experimental-strip-types scripts/build.ts --watch",
|
|
26
26
|
"clean": "rm -rf dist",
|
|
27
|
-
"lint": "npm run lint:ts && npm run lint:format",
|
|
27
|
+
"lint": "npm run lint:ts && npm run lint:format && npm run lint:sh",
|
|
28
28
|
"lint:ts": "tsc --noEmit",
|
|
29
29
|
"lint:format": "prettier --check .",
|
|
30
30
|
"format": "prettier --write .",
|
|
31
31
|
"test": "vitest run",
|
|
32
|
-
"prepare": "npm run build"
|
|
32
|
+
"prepare": "npm run build",
|
|
33
|
+
"lint:sh": "shellcheck -x -P SCRIPTDIR -S warning templates/server/*.sh",
|
|
34
|
+
"test:scripts": "bash scripts/test-server-scripts.sh"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"@clack/prompts": "^1.7.0",
|
|
36
38
|
"@faker-js/faker": "^10.6.0",
|
|
37
|
-
"@velastack/patterns": "^0.2.
|
|
39
|
+
"@velastack/patterns": "^0.2.3",
|
|
38
40
|
"@velastack/pocketbase-codegen": "^0.1.0",
|
|
39
41
|
"annotate-json-schema": "^0.1.0",
|
|
40
42
|
"commander": "^13.1.0",
|
|
@@ -62,7 +64,8 @@
|
|
|
62
64
|
"prettier": "^3.9.6",
|
|
63
65
|
"typescript": "^5.6.0",
|
|
64
66
|
"vite": "^8.2.2",
|
|
65
|
-
"vitest": "^4.1.11"
|
|
67
|
+
"vitest": "^4.1.11",
|
|
68
|
+
"shellcheck": "^4.1.0"
|
|
66
69
|
},
|
|
67
70
|
"peerDependencies": {
|
|
68
71
|
"@sveltejs/kit": "^2.57.1",
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
# is put back and the services are restarted before exiting non-zero.
|
|
8
8
|
#
|
|
9
9
|
# usage: apply.sh <instance> <release> [options]
|
|
10
|
+
#
|
|
11
|
+
# Only one of apply, destroy, rollback and restore runs for an instance at a
|
|
12
|
+
# time; a second waits for the first (--lock-wait seconds) and then gives up. A
|
|
13
|
+
# release that arrives after a newer one has gone live is refused rather than
|
|
14
|
+
# put live over it.
|
|
10
15
|
set -Eeuo pipefail
|
|
11
16
|
|
|
12
17
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
@@ -19,6 +24,8 @@ require_provisioned
|
|
|
19
24
|
INSTANCE=${1:-}; shift || true
|
|
20
25
|
RELEASE=${1:-}; shift || true
|
|
21
26
|
[ -n "$INSTANCE" ] && [ -n "$RELEASE" ] || die "usage: apply.sh <instance> <release> [options]"
|
|
27
|
+
require_instance_id "$INSTANCE"
|
|
28
|
+
require_release_id "$RELEASE"
|
|
22
29
|
|
|
23
30
|
APP_NAME=$INSTANCE
|
|
24
31
|
APP_ID=$INSTANCE
|
|
@@ -30,10 +37,13 @@ KEEP=5
|
|
|
30
37
|
PB_VERSION=""
|
|
31
38
|
BACKEND=1
|
|
32
39
|
GIT_SHA=""
|
|
40
|
+
LOCK_WAIT=300
|
|
33
41
|
SU_CREATED=0
|
|
42
|
+
MIGRATED=0
|
|
34
43
|
|
|
35
44
|
while [ $# -gt 0 ]; do
|
|
36
45
|
case "$1" in
|
|
46
|
+
--lock-wait) LOCK_WAIT=$2; shift 2 ;;
|
|
37
47
|
--name) APP_NAME=$2; shift 2 ;;
|
|
38
48
|
--app-id) APP_ID=$2; shift 2 ;;
|
|
39
49
|
--env) ENV_TAG=$2; shift 2 ;;
|
|
@@ -58,10 +68,22 @@ RELEASE_DIR="$APP/releases/$RELEASE"
|
|
|
58
68
|
WEB_UNIT=$(unit_web "$INSTANCE")
|
|
59
69
|
PB_UNIT=$(unit_pb "$INSTANCE")
|
|
60
70
|
|
|
71
|
+
lock_instance "$INSTANCE" "$LOCK_WAIT"
|
|
72
|
+
|
|
61
73
|
[ -d "$RELEASE_DIR" ] || die "release $RELEASE was not uploaded to $RELEASE_DIR"
|
|
62
74
|
[ -f "$RELEASE_DIR/build/index.js" ] || die \
|
|
63
75
|
"release $RELEASE has no build/index.js - the app must build with @sveltejs/adapter-node"
|
|
64
76
|
|
|
77
|
+
# A release that sorts at or below the active one arrived late: a newer deploy
|
|
78
|
+
# went live while this one was building or waiting for the lock. Putting it
|
|
79
|
+
# live would take the site backwards, so it is dropped - its own directory
|
|
80
|
+
# only; what is live is not touched.
|
|
81
|
+
ACTIVE=$(state_get "$INSTANCE" activeRelease 2>/dev/null || true)
|
|
82
|
+
if [ -n "$ACTIVE" ] && [ -L "$APP/current" ] && ! [[ "$RELEASE" > "$ACTIVE" ]]; then
|
|
83
|
+
rm -rf "${RELEASE_DIR:?}"
|
|
84
|
+
die "release $RELEASE is not newer than the active release $ACTIVE on $INSTANCE - a newer deploy went live first, so this one was dropped"
|
|
85
|
+
fi
|
|
86
|
+
|
|
65
87
|
mkdir -p "$APP"/{releases,shared,bin,deps} "$APP/shared/pb_data"
|
|
66
88
|
mkdir -p "$ETC"
|
|
67
89
|
chmod 0700 "$ETC"
|
|
@@ -188,9 +210,11 @@ install_deps() {
|
|
|
188
210
|
# so the app user can never swap out the scripts root runs.
|
|
189
211
|
mkdir -p "$VELA_ROOT/cache/npm"
|
|
190
212
|
chown -R "$VELA_USER:$VELA_USER" "$VELA_ROOT/cache"
|
|
213
|
+
# 8>&-: npm must not inherit the instance lock, or a child it leaves
|
|
214
|
+
# behind would hold it after this script has exited.
|
|
191
215
|
( cd "$deps" && runuser -u "$VELA_USER" -- env \
|
|
192
216
|
HOME="$deps" npm_config_cache="$VELA_ROOT/cache/npm" \
|
|
193
|
-
"${install_cmd[@]}" >&2 ) || die "dependency install failed"
|
|
217
|
+
"${install_cmd[@]}" >&2 8>&- ) || die "dependency install failed"
|
|
194
218
|
else
|
|
195
219
|
log "dependencies already installed ($key)"
|
|
196
220
|
fi
|
|
@@ -214,9 +238,21 @@ ACTIVATED=0
|
|
|
214
238
|
|
|
215
239
|
restore() {
|
|
216
240
|
log "deploy failed - restoring ${PREVIOUS:-nothing}"
|
|
241
|
+
# The schema moved with this release; the previous one must not run
|
|
242
|
+
# against it. Same steps `vela rollback` takes, best-effort here because
|
|
243
|
+
# the app is being put back either way and the alternative is a dead site.
|
|
244
|
+
if [ "$BACKEND" = "1" ] && [ "$MIGRATED" = "1" ] && [ -n "$PREVIOUS" ] \
|
|
245
|
+
&& [ -d "$APP/releases/$PREVIOUS" ]; then
|
|
246
|
+
systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true
|
|
247
|
+
revert_migrations "$APP" "$RELEASE_DIR/migrations" "$APP/releases/$PREVIOUS/migrations" \
|
|
248
|
+
|| log "down migrations failed - the database schema is ahead of $PREVIOUS; run 'vela rollback' once it is fixed"
|
|
249
|
+
fi
|
|
217
250
|
if [ -n "$PREVIOUS" ] && [ -d "$APP/releases/$PREVIOUS" ]; then
|
|
218
251
|
ln -sfn "$APP/releases/$PREVIOUS" "$APP/.current.tmp"
|
|
219
252
|
mv -Tf "$APP/.current.tmp" "$APP/current"
|
|
253
|
+
# runtime.env was already written for the release that failed; the
|
|
254
|
+
# services about to restart must read the one they will actually run.
|
|
255
|
+
set_runtime_release "$ETC" "$PREVIOUS"
|
|
220
256
|
if [ "$BACKEND" = "1" ]; then systemctl restart "$PB_UNIT" >/dev/null 2>&1 || true; fi
|
|
221
257
|
systemctl restart "$WEB_UNIT" >/dev/null 2>&1 || true
|
|
222
258
|
else
|
|
@@ -245,6 +281,7 @@ if [ "$BACKEND" = "1" ]; then
|
|
|
245
281
|
--dir "$APP/shared/pb_data" \
|
|
246
282
|
--migrationsDir "$RELEASE_DIR/migrations" \
|
|
247
283
|
migrate up >&2
|
|
284
|
+
MIGRATED=1
|
|
248
285
|
fi
|
|
249
286
|
|
|
250
287
|
# ---------------------------------------------------- superuser bootstrap
|
|
@@ -284,8 +321,11 @@ mv -Tf "$APP/.current.tmp" "$APP/current"
|
|
|
284
321
|
if [ "$ENV_TAG" = "prod" ]; then LINK_LABEL=$APP_NAME; else LINK_LABEL="$APP_NAME-$ENV_TAG"; fi
|
|
285
322
|
LINK_NAME=$(printf '%s' "$LINK_LABEL" | tr -c 'A-Za-z0-9._-' '-')
|
|
286
323
|
if [ -n "$LINK_NAME" ]; then
|
|
287
|
-
|
|
288
|
-
|
|
324
|
+
# A staging name of its own: a fixed one would be shared with every other
|
|
325
|
+
# deploy on the server, and two of them would swap each other's links.
|
|
326
|
+
link_tmp=$(mktemp -u "$VELA_ROOT/by-name/.link.XXXXXX")
|
|
327
|
+
{ ln -sfn "$APP" "$link_tmp" && mv -Tf "$link_tmp" "$VELA_ROOT/by-name/$LINK_NAME"; } \
|
|
328
|
+
|| rm -f "$link_tmp" || true
|
|
289
329
|
fi
|
|
290
330
|
|
|
291
331
|
systemctl daemon-reload
|
|
@@ -352,6 +392,11 @@ CADDY_SNIPPET="$VELA_ETC/caddy/$INSTANCE.caddy"
|
|
|
352
392
|
ROUTE_SNIPPET="$VELA_ETC/caddy/routes/$INSTANCE.route"
|
|
353
393
|
ROUTING_CHANGED=0
|
|
354
394
|
|
|
395
|
+
# From here the release is live and stays live: a routing failure below
|
|
396
|
+
# exits non-zero with the site running the new release and its routes as
|
|
397
|
+
# they were, which is what the messages say.
|
|
398
|
+
caddy_lock
|
|
399
|
+
|
|
355
400
|
if [ -n "$DOMAIN" ]; then
|
|
356
401
|
hosts=$(printf '%s' "$DOMAIN" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' | paste -sd, - | sed 's/,/, /g')
|
|
357
402
|
tmp=$(mktemp "$VELA_ETC/caddy/.snippet.XXXXXX")
|
|
@@ -359,7 +404,8 @@ if [ -n "$DOMAIN" ]; then
|
|
|
359
404
|
printf '# Managed by vela - app %s (%s)\n' "$APP_NAME" "$INSTANCE"
|
|
360
405
|
printf '%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "$hosts" "$WEB_PORT"
|
|
361
406
|
} > "$tmp"
|
|
362
|
-
caddy_install "$tmp" "$CADDY_SNIPPET"
|
|
407
|
+
caddy_install "$tmp" "$CADDY_SNIPPET" \
|
|
408
|
+
|| die "release $RELEASE is live, but the generated Caddy config for $DOMAIN is invalid and was not installed - routing is unchanged"
|
|
363
409
|
ROUTING_CHANGED=1
|
|
364
410
|
elif [ -f "$CADDY_SNIPPET" ]; then
|
|
365
411
|
rm -f "$CADDY_SNIPPET"
|
|
@@ -386,7 +432,8 @@ if [ -n "$MANAGED" ]; then
|
|
|
386
432
|
printf '\t\theader_up X-Forwarded-For {http.request.header.X-Velastack-Client-IP}\n'
|
|
387
433
|
printf '\t}\n}\n'
|
|
388
434
|
} > "$tmp"
|
|
389
|
-
caddy_install "$tmp" "$ROUTE_SNIPPET"
|
|
435
|
+
caddy_install "$tmp" "$ROUTE_SNIPPET" \
|
|
436
|
+
|| die "release $RELEASE is live, but the generated Caddy route for $MANAGED is invalid and was not installed - routing is unchanged"
|
|
390
437
|
ROUTING_CHANGED=1
|
|
391
438
|
elif [ -f "$ROUTE_SNIPPET" ]; then
|
|
392
439
|
rm -f "$ROUTE_SNIPPET"
|
|
@@ -394,7 +441,7 @@ elif [ -f "$ROUTE_SNIPPET" ]; then
|
|
|
394
441
|
fi
|
|
395
442
|
|
|
396
443
|
[ "$ROUTING_CHANGED" = 0 ] || caddy_reload
|
|
397
|
-
|
|
444
|
+
caddy_unlock
|
|
398
445
|
|
|
399
446
|
# ------------------------------------------------------------------- pruning
|
|
400
447
|
|
|
@@ -404,6 +451,8 @@ if [ "$KEEP" -gt 0 ] 2>/dev/null; then
|
|
|
404
451
|
[ -n "$rel" ] || continue
|
|
405
452
|
[ "$rel" = "$RELEASE" ] && continue
|
|
406
453
|
[ "$rel" = "$PREVIOUS" ] && continue
|
|
454
|
+
# Uploaded by a deploy that is waiting on the lock: not ours to prune.
|
|
455
|
+
if [[ "$rel" > "$RELEASE" ]]; then continue; fi
|
|
407
456
|
log "pruning release $rel"
|
|
408
457
|
rm -rf "${APP:?}/releases/$rel"
|
|
409
458
|
done
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Releases and configuration always go; the database and uploads only go with
|
|
6
6
|
# --purge, so a mistyped instance name cannot silently delete production data.
|
|
7
|
+
# A purge snapshots them into $VELA_ROOT/trash first, kept for two weeks.
|
|
7
8
|
#
|
|
8
|
-
# usage: destroy.sh <instance> [--purge]
|
|
9
|
+
# usage: destroy.sh <instance> [--purge] [--lock-wait <seconds>]
|
|
9
10
|
set -Eeuo pipefail
|
|
10
11
|
|
|
11
12
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
@@ -16,33 +17,75 @@ require_provisioned
|
|
|
16
17
|
[ "$(id -u)" -eq 0 ] || die "destroy must run as root"
|
|
17
18
|
|
|
18
19
|
INSTANCE=${1:-}; shift || true
|
|
19
|
-
[ -n "$INSTANCE" ] || die "usage: destroy.sh <instance> [--purge]"
|
|
20
|
+
[ -n "$INSTANCE" ] || die "usage: destroy.sh <instance> [--purge] [--lock-wait <seconds>]"
|
|
20
21
|
PURGE=0
|
|
22
|
+
LOCK_WAIT=300
|
|
21
23
|
while [ $# -gt 0 ]; do
|
|
22
24
|
case "$1" in
|
|
23
25
|
--purge) PURGE=1; shift ;;
|
|
26
|
+
--lock-wait) LOCK_WAIT=$2; shift 2 ;;
|
|
24
27
|
*) die "unknown argument: $1" ;;
|
|
25
28
|
esac
|
|
26
29
|
done
|
|
27
30
|
|
|
31
|
+
require_instance_id "$INSTANCE"
|
|
32
|
+
|
|
28
33
|
APP=$(app_dir "$INSTANCE")
|
|
29
34
|
ETC=$(etc_dir "$INSTANCE")
|
|
30
35
|
|
|
36
|
+
lock_instance "$INSTANCE" "$LOCK_WAIT"
|
|
37
|
+
|
|
38
|
+
# Nothing to remove is a result, not an error: a preview that never deployed
|
|
39
|
+
# still gets a cleanup run when its pull request closes. Nothing is touched,
|
|
40
|
+
# so a mistyped name cannot do harm either way.
|
|
41
|
+
EXISTS=0
|
|
42
|
+
for path in "$APP/state.json" "$APP/releases" "$APP/current" "$ETC"; do
|
|
43
|
+
if [ -e "$path" ]; then EXISTS=1; fi
|
|
44
|
+
done
|
|
45
|
+
if [ "$PURGE" = 1 ] && [ -d "$APP" ]; then EXISTS=1; fi
|
|
46
|
+
if [ "$EXISTS" = 0 ]; then
|
|
47
|
+
log "nothing named $INSTANCE on this server"
|
|
48
|
+
emit_result --arg instance "$INSTANCE" \
|
|
49
|
+
'{instance: $instance, purged: false, existed: false}'
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
31
53
|
log "stopping services"
|
|
32
54
|
for unit in "$(unit_web "$INSTANCE")" "$(unit_pb "$INSTANCE")"; do
|
|
33
55
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
|
34
56
|
done
|
|
35
57
|
|
|
36
58
|
log "removing routing"
|
|
59
|
+
caddy_lock
|
|
37
60
|
rm -f "$VELA_ETC/caddy/$INSTANCE.caddy" "$VELA_ETC/caddy/routes/$INSTANCE.route"
|
|
38
61
|
caddy_reload || true
|
|
62
|
+
caddy_unlock
|
|
39
63
|
|
|
40
64
|
log "removing releases"
|
|
41
|
-
rm -rf "$APP/releases" "$APP/deps" "$APP/current" "$APP/bin"
|
|
65
|
+
rm -rf "${APP:?}/releases" "${APP:?}/deps" "${APP:?}/current" "${APP:?}/bin"
|
|
42
66
|
|
|
67
|
+
SNAPSHOT=""
|
|
43
68
|
if [ "$PURGE" = "1" ]; then
|
|
69
|
+
# The services are stopped, so the database is quiet: this is the one copy
|
|
70
|
+
# of it that will exist once the purge runs. Root-only, pruned after two
|
|
71
|
+
# weeks, and taken before anything is removed - a snapshot that fails
|
|
72
|
+
# leaves the instance's data where it was.
|
|
73
|
+
TRASH="$VELA_ROOT/trash"
|
|
74
|
+
mkdir -p "$TRASH"
|
|
75
|
+
chmod 0700 "$TRASH"
|
|
76
|
+
find "$TRASH" -maxdepth 1 -name '*.tar.gz' -mtime +14 -delete 2>/dev/null || true
|
|
77
|
+
members=()
|
|
78
|
+
[ -d "$APP/shared" ] && members+=("${APP#/}/shared")
|
|
79
|
+
[ -d "$ETC" ] && members+=("${ETC#/}")
|
|
80
|
+
if [ "${#members[@]}" -gt 0 ]; then
|
|
81
|
+
SNAPSHOT="$TRASH/$INSTANCE-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
|
|
82
|
+
log "snapshotting data and configuration to $SNAPSHOT"
|
|
83
|
+
tar -C / -czf "$SNAPSHOT" "${members[@]}" \
|
|
84
|
+
|| die "could not snapshot $INSTANCE before purging - its data was left in place"
|
|
85
|
+
chmod 0600 "$SNAPSHOT"
|
|
86
|
+
fi
|
|
44
87
|
log "purging data and configuration"
|
|
45
|
-
rm -rf "$APP" "$ETC"
|
|
88
|
+
rm -rf "${APP:?}" "${ETC:?}"
|
|
46
89
|
release_ports "$INSTANCE"
|
|
47
90
|
else
|
|
48
91
|
log "keeping $APP/shared (pass --purge to remove the database)"
|
|
@@ -51,5 +94,5 @@ fi
|
|
|
51
94
|
|
|
52
95
|
find "$VELA_ROOT/by-name" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true
|
|
53
96
|
|
|
54
|
-
emit_result --arg instance "$INSTANCE" --argjson purged "$PURGE" \
|
|
55
|
-
'{instance: $instance, purged: ($purged == 1)}'
|
|
97
|
+
emit_result --arg instance "$INSTANCE" --argjson purged "$PURGE" --arg trash "$SNAPSHOT" \
|
|
98
|
+
'{instance: $instance, purged: ($purged == 1), existed: true, trash: $trash}'
|
package/templates/server/lib.sh
CHANGED
|
@@ -15,6 +15,45 @@ state_file() { printf '%s/apps/%s/state.json' "$VELA_ROOT" "$1"; }
|
|
|
15
15
|
log() { printf ' %s\n' "$*" >&2; }
|
|
16
16
|
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
|
17
17
|
|
|
18
|
+
# Release ids are compared as strings, in `sort` and in `[[ < ]]` alike, and
|
|
19
|
+
# the two have to agree whatever locale the server booted with.
|
|
20
|
+
export LC_ALL=C
|
|
21
|
+
|
|
22
|
+
# An instance id is `<appId>` or `<appId>--<envTag>`: lowercase letters and
|
|
23
|
+
# digits joined by single or double dashes, exactly what the CLI's instanceId()
|
|
24
|
+
# produces. It arrives from the CLI, but it ends up in paths that root removes,
|
|
25
|
+
# so its shape is checked here too before anything is touched.
|
|
26
|
+
require_instance_id() {
|
|
27
|
+
[[ $1 =~ ^[a-z0-9]+(-{1,2}[a-z0-9]+)*$ ]] || die "not an instance id: $1"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# A release id is a UTC stamp with an optional short suffix: 20260910T141203Z
|
|
31
|
+
# or 20260910T141203Z-a3f9.
|
|
32
|
+
require_release_id() {
|
|
33
|
+
[[ $1 =~ ^[0-9]{8}T[0-9]{6}Z(-[a-z0-9]{1,8})?$ ]] || die "not a release id: $1"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Hold the instance's lock for the rest of this process.
|
|
37
|
+
#
|
|
38
|
+
# Every script that mutates an instance - deploy, destroy, rollback, restore -
|
|
39
|
+
# takes this first, so two of them can never interleave: the second waits for
|
|
40
|
+
# the first, up to `wait` seconds, then gives up. The lock file lives under
|
|
41
|
+
# state/ rather than in the instance's own directory, so a purge cannot unlink
|
|
42
|
+
# a lock somebody else is holding. Descriptor 8; 9 belongs to the port table.
|
|
43
|
+
#
|
|
44
|
+
# usage: lock_instance <instance> [wait_seconds]
|
|
45
|
+
lock_instance() {
|
|
46
|
+
local instance=$1 wait=${2:-300} dir="$VELA_ROOT/state/locks"
|
|
47
|
+
mkdir -p "$dir"
|
|
48
|
+
exec 8>"$dir/$instance.lock"
|
|
49
|
+
if flock -n 8; then return 0; fi
|
|
50
|
+
[ "$wait" -gt 0 ] 2>/dev/null \
|
|
51
|
+
|| die "another deploy, destroy, rollback or restore is running for $instance - aborting"
|
|
52
|
+
log "waiting for another operation on $instance to finish (up to ${wait}s)"
|
|
53
|
+
flock -w "$wait" 8 \
|
|
54
|
+
|| die "another deploy, destroy, rollback or restore is still running for $instance after ${wait}s - aborting"
|
|
55
|
+
}
|
|
56
|
+
|
|
18
57
|
require_provisioned() {
|
|
19
58
|
[ -f "$VELA_ETC/provisioned" ] || die "server is not provisioned - run 'vela provision' first"
|
|
20
59
|
}
|
|
@@ -115,14 +154,19 @@ unit_pb() { printf 'vela-pb@%s.service' "$1"; }
|
|
|
115
154
|
|
|
116
155
|
unit_active() { systemctl is-active --quiet "$1"; }
|
|
117
156
|
|
|
118
|
-
# Poll an HTTP endpoint until it answers
|
|
157
|
+
# Poll an HTTP endpoint until it answers like a running app.
|
|
158
|
+
#
|
|
159
|
+
# Success, a redirect, or a refusal that proves something is home (401, 403 -
|
|
160
|
+
# a health path behind auth). A 404 is not that: it is what a wrong
|
|
161
|
+
# --health-path or a route that never mounted looks like, and it used to pass.
|
|
162
|
+
# No `-f`: curl has to report the status of an error response, not fail on it.
|
|
119
163
|
wait_for_http() {
|
|
120
164
|
local url=$1 attempts=${2:-60} delay=${3:-0.5} code
|
|
121
165
|
local i=0
|
|
122
166
|
while [ "$i" -lt "$attempts" ]; do
|
|
123
|
-
code=$(curl -
|
|
167
|
+
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "$url" 2>/dev/null || echo 000)
|
|
124
168
|
case "$code" in
|
|
125
|
-
2*|3*|
|
|
169
|
+
2*|3*|401|403) return 0 ;;
|
|
126
170
|
esac
|
|
127
171
|
i=$((i + 1))
|
|
128
172
|
sleep "$delay"
|
|
@@ -142,6 +186,39 @@ migrations_ahead() {
|
|
|
142
186
|
printf '%s' "$count"
|
|
143
187
|
}
|
|
144
188
|
|
|
189
|
+
# Revert the migrations `from_dir` has that `to_dir` lacks. They run from
|
|
190
|
+
# `from_dir` - the release that introduced them owns their down steps - and
|
|
191
|
+
# PocketBase reverts by count, which `migrations_ahead` supplies. The
|
|
192
|
+
# instance's PocketBase must be stopped: `migrate down` opens the database
|
|
193
|
+
# directly. Returns non-zero if PocketBase refuses; the caller decides how bad
|
|
194
|
+
# that is.
|
|
195
|
+
#
|
|
196
|
+
# usage: revert_migrations <app_dir> <from_dir> <to_dir>
|
|
197
|
+
revert_migrations() {
|
|
198
|
+
local app=$1 from=$2 to=$3 ahead
|
|
199
|
+
ahead=$(migrations_ahead "$from" "$to")
|
|
200
|
+
[ "$ahead" -gt 0 ] || return 0
|
|
201
|
+
log "reverting $ahead migration(s) the previous release does not have"
|
|
202
|
+
runuser -u "$VELA_USER" -- "$app/bin/pocketbase" \
|
|
203
|
+
--dir "$app/shared/pb_data" \
|
|
204
|
+
--migrationsDir "$from" \
|
|
205
|
+
migrate down "$ahead" >&2
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Point an instance's runtime.env at a release. Rewritten whole and renamed
|
|
209
|
+
# into place, the way apply.sh writes it, so systemd never reads half a line.
|
|
210
|
+
#
|
|
211
|
+
# usage: set_runtime_release <etc_dir> <release>
|
|
212
|
+
set_runtime_release() {
|
|
213
|
+
local etc=$1 release=$2 tmp
|
|
214
|
+
[ -f "$etc/runtime.env" ] || return 0
|
|
215
|
+
tmp=$(mktemp "$etc/.runtime.XXXXXX")
|
|
216
|
+
sed "s|^VELA_RELEASE=.*|VELA_RELEASE=$release|" "$etc/runtime.env" > "$tmp"
|
|
217
|
+
chmod 0600 "$tmp"
|
|
218
|
+
chown root:root "$tmp" 2>/dev/null || true
|
|
219
|
+
mv -f "$tmp" "$etc/runtime.env"
|
|
220
|
+
}
|
|
221
|
+
|
|
145
222
|
emit_result() { printf 'VELA_RESULT %s\n' "$(jq -c -n "$@")"; }
|
|
146
223
|
|
|
147
224
|
# Read one value out of a vela-managed env file. `vela env` writes values with
|
|
@@ -216,7 +293,7 @@ reconcile_superuser() {
|
|
|
216
293
|
|
|
217
294
|
# Prove the app's own credentials actually sign in to its database.
|
|
218
295
|
#
|
|
219
|
-
# `wait_for_http` accepts
|
|
296
|
+
# `wait_for_http` accepts a 401 as healthy, so a PocketBase whose database no
|
|
220
297
|
# longer matches `$ETC/env` sails through the health gate and then answers every
|
|
221
298
|
# render with a 401. Only an actual login catches that. The password travels
|
|
222
299
|
# through the environment rather than argv, so it never appears in `ps`.
|
|
@@ -266,3 +343,20 @@ caddy_install() {
|
|
|
266
343
|
caddy_reload() {
|
|
267
344
|
systemctl reload caddy >/dev/null 2>&1 || systemctl restart caddy
|
|
268
345
|
}
|
|
346
|
+
|
|
347
|
+
# Serialize every change to the Caddy config across instances.
|
|
348
|
+
#
|
|
349
|
+
# `caddy_valid` checks the whole Caddyfile, so two scripts installing snippets
|
|
350
|
+
# at once would each judge the other's: a bad snippet from one deploy would
|
|
351
|
+
# have the other roll back a good route of its own. Held from the first
|
|
352
|
+
# snippet write through the reload. Descriptor 7; blocking, since a Caddy
|
|
353
|
+
# change takes well under a second.
|
|
354
|
+
caddy_lock() {
|
|
355
|
+
mkdir -p "$VELA_ROOT/state"
|
|
356
|
+
exec 7>"$VELA_ROOT/state/.caddy.lock"
|
|
357
|
+
flock 7
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
caddy_unlock() {
|
|
361
|
+
exec 7>&-
|
|
362
|
+
}
|
|
@@ -51,6 +51,7 @@ if [ -f "$SNIPPET" ] && [ "$(cat "$SNIPPET")" = "$desired" ]; then
|
|
|
51
51
|
exit 0
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
+
caddy_lock
|
|
54
55
|
tmp=$(mktemp "$VELA_ETC/caddy/.origin.XXXXXX")
|
|
55
56
|
printf '%s\n' "$desired" > "$tmp"
|
|
56
57
|
# Readable by the caddy user (which is what `caddy reload` runs as) and no one
|
|
@@ -58,5 +59,6 @@ printf '%s\n' "$desired" > "$tmp"
|
|
|
58
59
|
caddy_install "$tmp" "$SNIPPET" 0640 root:caddy \
|
|
59
60
|
|| die "generated origin config for $HOST is invalid"
|
|
60
61
|
caddy_reload
|
|
62
|
+
caddy_unlock
|
|
61
63
|
|
|
62
64
|
emit_result --arg host "$HOST" '{originHost: $host, changed: true}'
|
|
@@ -23,16 +23,19 @@ require_provisioned
|
|
|
23
23
|
|
|
24
24
|
INSTANCE=${1:-}; shift || true
|
|
25
25
|
[ -n "$INSTANCE" ] || die "usage: restore.sh <instance> --archive <path> [options]"
|
|
26
|
+
require_instance_id "$INSTANCE"
|
|
26
27
|
|
|
27
28
|
ARCHIVE=""
|
|
28
29
|
MIGRATE=1
|
|
29
30
|
KEEP_PREVIOUS=1
|
|
30
31
|
CLEANUP_ARCHIVE=0
|
|
31
32
|
HEALTH_PATH=""
|
|
33
|
+
LOCK_WAIT=300
|
|
32
34
|
|
|
33
35
|
while [ $# -gt 0 ]; do
|
|
34
36
|
case "$1" in
|
|
35
37
|
--archive) ARCHIVE=$2; shift 2 ;;
|
|
38
|
+
--lock-wait) LOCK_WAIT=$2; shift 2 ;;
|
|
36
39
|
--no-migrate) MIGRATE=0; shift ;;
|
|
37
40
|
--keep-previous) KEEP_PREVIOUS=$2; shift 2 ;;
|
|
38
41
|
--cleanup-archive) CLEANUP_ARCHIVE=1; shift ;;
|
|
@@ -47,6 +50,10 @@ done
|
|
|
47
50
|
# directory the CLI happened to invoke from is usually one it cannot stat.
|
|
48
51
|
cd "$VELA_ROOT"
|
|
49
52
|
|
|
53
|
+
# Taken before anything is read: a deploy or another restore in flight would
|
|
54
|
+
# otherwise have this one moving a pb_data it is about to replace.
|
|
55
|
+
lock_instance "$INSTANCE" "$LOCK_WAIT"
|
|
56
|
+
|
|
50
57
|
require_backend "$INSTANCE"
|
|
51
58
|
|
|
52
59
|
APP=$(app_dir "$INSTANCE")
|
|
@@ -66,10 +73,6 @@ PORTS=$(allocate_ports "$INSTANCE")
|
|
|
66
73
|
WEB_PORT=$(printf '%s' "$PORTS" | jq -r .web)
|
|
67
74
|
PB_PORT=$(printf '%s' "$PORTS" | jq -r .pb)
|
|
68
75
|
|
|
69
|
-
# Two restores at once would each move the other's pb_data aside.
|
|
70
|
-
exec 9>"$APP/.restore.lock"
|
|
71
|
-
flock -n 9 || die "another restore is already running for $INSTANCE"
|
|
72
|
-
|
|
73
76
|
# ---------------------------------------------------------------- preflight
|
|
74
77
|
#
|
|
75
78
|
# Everything that can be checked before a service stops is checked before a
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Put the previous release back for one instance.
|
|
4
4
|
#
|
|
5
|
-
# usage: rollback.sh <instance> [--to <release>]
|
|
5
|
+
# usage: rollback.sh <instance> [--to <release>] [--lock-wait <seconds>]
|
|
6
6
|
set -Eeuo pipefail
|
|
7
7
|
|
|
8
8
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
@@ -13,21 +13,26 @@ require_provisioned
|
|
|
13
13
|
[ "$(id -u)" -eq 0 ] || die "rollback must run as root"
|
|
14
14
|
|
|
15
15
|
INSTANCE=${1:-}; shift || true
|
|
16
|
-
[ -n "$INSTANCE" ] || die "usage: rollback.sh <instance> [--to <release>]"
|
|
16
|
+
[ -n "$INSTANCE" ] || die "usage: rollback.sh <instance> [--to <release>] [--lock-wait <seconds>]"
|
|
17
|
+
require_instance_id "$INSTANCE"
|
|
17
18
|
|
|
18
19
|
TARGET=""
|
|
20
|
+
LOCK_WAIT=300
|
|
19
21
|
while [ $# -gt 0 ]; do
|
|
20
22
|
case "$1" in
|
|
21
23
|
--to) TARGET=$2; shift 2 ;;
|
|
24
|
+
--lock-wait) LOCK_WAIT=$2; shift 2 ;;
|
|
22
25
|
*) die "unknown argument: $1" ;;
|
|
23
26
|
esac
|
|
24
27
|
done
|
|
28
|
+
[ -z "$TARGET" ] || require_release_id "$TARGET"
|
|
25
29
|
|
|
26
30
|
# Commands that drop to the app user inherit this working directory, and the
|
|
27
31
|
# directory the CLI happened to invoke from is usually one it cannot stat.
|
|
28
32
|
cd "$VELA_ROOT"
|
|
29
33
|
|
|
30
34
|
APP=$(app_dir "$INSTANCE")
|
|
35
|
+
lock_instance "$INSTANCE" "$LOCK_WAIT"
|
|
31
36
|
[ -f "$(state_file "$INSTANCE")" ] || die "no instance $INSTANCE on this server"
|
|
32
37
|
|
|
33
38
|
CURRENT=$(state_get "$INSTANCE" activeRelease || echo "")
|
|
@@ -50,15 +55,8 @@ if [ "$BACKEND" = "true" ]; then
|
|
|
50
55
|
# Down migrations belong to the release being left behind, so they run from
|
|
51
56
|
# the current release's migration set before the symlink moves.
|
|
52
57
|
if [ -n "$CURRENT" ] && [ -d "$APP/releases/$CURRENT/migrations" ]; then
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
log "reverting $AHEAD migration(s) introduced after $TARGET"
|
|
56
|
-
runuser -u "$VELA_USER" -- "$APP/bin/pocketbase" \
|
|
57
|
-
--dir "$APP/shared/pb_data" \
|
|
58
|
-
--migrationsDir "$APP/releases/$CURRENT/migrations" \
|
|
59
|
-
migrate down "$AHEAD" >&2 \
|
|
60
|
-
|| die "down migrations failed - the app is still stopped"
|
|
61
|
-
fi
|
|
58
|
+
revert_migrations "$APP" "$APP/releases/$CURRENT/migrations" "$APP/releases/$TARGET/migrations" \
|
|
59
|
+
|| die "down migrations failed - the app is still stopped"
|
|
62
60
|
fi
|
|
63
61
|
fi
|
|
64
62
|
|
|
@@ -68,9 +66,7 @@ mv -Tf "$APP/.current.tmp" "$APP/current"
|
|
|
68
66
|
# The running release is part of the instance's environment, so it has to move
|
|
69
67
|
# with the symlink.
|
|
70
68
|
ETC=$(etc_dir "$INSTANCE")
|
|
71
|
-
|
|
72
|
-
sed -i "s|^VELA_RELEASE=.*|VELA_RELEASE=$TARGET|" "$ETC/runtime.env"
|
|
73
|
-
fi
|
|
69
|
+
set_runtime_release "$ETC" "$TARGET"
|
|
74
70
|
|
|
75
71
|
if [ "$BACKEND" = "true" ]; then
|
|
76
72
|
systemctl restart "$PB_UNIT"
|