vela 0.10.2 → 0.10.3

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.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Prepare a stock Ubuntu/Debian server to host vela apps.
4
+ #
5
+ # Idempotent: every step checks before it acts, so re-running after a CLI
6
+ # upgrade only fills in what is missing. It never touches an app's data, and it
7
+ # never rewrites an existing Caddyfile beyond appending one import line.
8
+ set -euo pipefail
9
+
10
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
11
+ # shellcheck source=lib.sh
12
+ . "$SCRIPT_DIR/lib.sh"
13
+
14
+ PB_VERSION=""
15
+ NODE_MAJOR=22
16
+ CLI_VERSION="unknown"
17
+ LAYOUT_VERSION=1
18
+
19
+ while [ $# -gt 0 ]; do
20
+ case "$1" in
21
+ --pb-version) PB_VERSION=$2; shift 2 ;;
22
+ --node-major) NODE_MAJOR=$2; shift 2 ;;
23
+ --cli-version) CLI_VERSION=$2; shift 2 ;;
24
+ *) die "unknown argument: $1" ;;
25
+ esac
26
+ done
27
+
28
+ [ "$(id -u)" -eq 0 ] || die "provision must run as root"
29
+
30
+ . /etc/os-release
31
+ case "${ID:-}" in
32
+ ubuntu|debian) ;;
33
+ *) die "unsupported OS: ${PRETTY_NAME:-$ID}. vela provision supports Ubuntu and Debian." ;;
34
+ esac
35
+
36
+ export DEBIAN_FRONTEND=noninteractive
37
+ APT_UPDATED=0
38
+ apt_update_once() {
39
+ [ "$APT_UPDATED" -eq 1 ] && return 0
40
+ apt-get update -qq
41
+ APT_UPDATED=1
42
+ }
43
+
44
+ ensure_packages() {
45
+ local missing=()
46
+ for pkg in "$@"; do
47
+ dpkg -s "$pkg" >/dev/null 2>&1 || missing+=("$pkg")
48
+ done
49
+ [ ${#missing[@]} -eq 0 ] && return 0
50
+ log "installing ${missing[*]}"
51
+ apt_update_once
52
+ apt-get install -y -qq --no-install-recommends "${missing[@]}" >/dev/null
53
+ }
54
+
55
+ log "checking base packages"
56
+ ensure_packages ca-certificates curl gnupg rsync unzip tar sqlite3 jq git
57
+
58
+ # Native npm modules (argon2, better-sqlite3) are compiled on the server when a
59
+ # prebuild is unavailable, so the toolchain has to be here.
60
+ ensure_packages build-essential python3
61
+
62
+ install_node() {
63
+ local current=0
64
+ if command -v node >/dev/null 2>&1; then
65
+ current=$(node -p 'process.versions.node.split(".")[0]')
66
+ fi
67
+ if [ "$current" -ge 20 ] 2>/dev/null; then
68
+ log "node $(node -v) already installed"
69
+ return 0
70
+ fi
71
+ log "installing Node.js $NODE_MAJOR"
72
+ curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o /tmp/nodesource_setup.sh
73
+ bash /tmp/nodesource_setup.sh >/dev/null
74
+ rm -f /tmp/nodesource_setup.sh
75
+ apt-get install -y -qq nodejs >/dev/null
76
+ }
77
+ install_node
78
+
79
+ install_caddy() {
80
+ if command -v caddy >/dev/null 2>&1; then
81
+ log "caddy $(caddy version | head -1) already installed"
82
+ return 0
83
+ fi
84
+ log "installing caddy"
85
+ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
86
+ | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
87
+ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
88
+ > /etc/apt/sources.list.d/caddy-stable.list
89
+ apt-get update -qq
90
+ apt-get install -y -qq caddy >/dev/null
91
+ APT_UPDATED=1
92
+ }
93
+ install_caddy
94
+
95
+ log "creating $VELA_USER user"
96
+ if ! id -u "$VELA_USER" >/dev/null 2>&1; then
97
+ useradd --system --home-dir "$VELA_ROOT" --shell /usr/sbin/nologin "$VELA_USER"
98
+ fi
99
+
100
+ log "creating directory layout"
101
+ mkdir -p "$VELA_ROOT"/{apps,runtime,state,scripts,by-name,cache}
102
+ mkdir -p "$VELA_ETC"/{apps,caddy}
103
+ chown -R "$VELA_USER:$VELA_USER" "$VELA_ROOT/apps" "$VELA_ROOT/state" "$VELA_ROOT/by-name" "$VELA_ROOT/cache"
104
+ chmod 0755 "$VELA_ROOT" "$VELA_ROOT/apps"
105
+ chown root:root "$VELA_ETC" "$VELA_ETC/apps"
106
+ # Env files hold production secrets; only root reads them, systemd included.
107
+ chmod 0755 "$VELA_ETC"
108
+ chmod 0700 "$VELA_ETC/apps"
109
+
110
+ log "installing PocketBase $PB_VERSION"
111
+ "$SCRIPT_DIR/pocketbase.sh" "$PB_VERSION"
112
+
113
+ log "installing systemd unit templates"
114
+ install -m 0644 "$SCRIPT_DIR/systemd/vela-web@.service" /etc/systemd/system/vela-web@.service
115
+ install -m 0644 "$SCRIPT_DIR/systemd/vela-pb@.service" /etc/systemd/system/vela-pb@.service
116
+ systemctl daemon-reload
117
+
118
+ log "wiring caddy"
119
+ # An import glob that matches nothing is a hard error in Caddy, so keep a
120
+ # comment-only file in place before the first app is deployed.
121
+ if [ ! -f "$VELA_ETC/caddy/00-vela.caddy" ]; then
122
+ printf '# Managed by vela. App snippets are written alongside this file.\n' \
123
+ > "$VELA_ETC/caddy/00-vela.caddy"
124
+ fi
125
+ CADDYFILE=/etc/caddy/Caddyfile
126
+ mkdir -p /etc/caddy
127
+ [ -f "$CADDYFILE" ] || printf '' > "$CADDYFILE"
128
+ if ! grep -q 'import /etc/vela/caddy/\*.caddy' "$CADDYFILE"; then
129
+ {
130
+ printf '\n# Added by vela provision - app routes are generated into /etc/vela/caddy.\n'
131
+ printf 'import /etc/vela/caddy/*.caddy\n'
132
+ } >> "$CADDYFILE"
133
+ fi
134
+ caddy validate --config "$CADDYFILE" --adapter caddyfile >/dev/null 2>&1 \
135
+ || die "existing /etc/caddy/Caddyfile is not valid after adding the vela import"
136
+ systemctl enable --now caddy >/dev/null 2>&1 || true
137
+ systemctl reload caddy 2>/dev/null || systemctl restart caddy
138
+
139
+ log "recording provisioning marker"
140
+ jq -c -n \
141
+ --arg cli "$CLI_VERSION" \
142
+ --arg pb "$PB_VERSION" \
143
+ --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
144
+ --argjson layout "$LAYOUT_VERSION" \
145
+ '{cliVersion: $cli, pocketbaseVersion: $pb, provisionedAt: $at, layoutVersion: $layout}' \
146
+ > "$VELA_ETC/provisioned"
147
+ chmod 0644 "$VELA_ETC/provisioned"
148
+
149
+ emit_result \
150
+ --arg node "$(node -v)" \
151
+ --arg caddy "$(caddy version | head -1 | awk '{print $1}')" \
152
+ --arg pocketbase "$PB_VERSION" \
153
+ '{node: $node, caddy: $caddy, pocketbase: $pocketbase}'
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Put the previous release back for one instance.
4
+ #
5
+ # usage: rollback.sh <instance> [--to <release>]
6
+ set -Eeuo pipefail
7
+
8
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
9
+ # shellcheck source=lib.sh
10
+ . "$SCRIPT_DIR/lib.sh"
11
+
12
+ require_provisioned
13
+ [ "$(id -u)" -eq 0 ] || die "rollback must run as root"
14
+
15
+ INSTANCE=${1:-}; shift || true
16
+ [ -n "$INSTANCE" ] || die "usage: rollback.sh <instance> [--to <release>]"
17
+
18
+ TARGET=""
19
+ while [ $# -gt 0 ]; do
20
+ case "$1" in
21
+ --to) TARGET=$2; shift 2 ;;
22
+ *) die "unknown argument: $1" ;;
23
+ esac
24
+ done
25
+
26
+ # Commands that drop to the app user inherit this working directory, and the
27
+ # directory the CLI happened to invoke from is usually one it cannot stat.
28
+ cd "$VELA_ROOT"
29
+
30
+ APP=$(app_dir "$INSTANCE")
31
+ [ -f "$(state_file "$INSTANCE")" ] || die "no instance $INSTANCE on this server"
32
+
33
+ CURRENT=$(state_get "$INSTANCE" activeRelease || echo "")
34
+ [ -n "$TARGET" ] || TARGET=$(state_get "$INSTANCE" previousRelease || echo "")
35
+ [ -n "$TARGET" ] || die "no previous release recorded for $INSTANCE"
36
+ [ -d "$APP/releases/$TARGET" ] || die "release $TARGET is no longer on disk"
37
+ [ "$TARGET" != "$CURRENT" ] || die "$TARGET is already the active release"
38
+
39
+ BACKEND=$(state_get "$INSTANCE" backend 2>/dev/null || echo "true")
40
+ HEALTH_PATH=$(state_get "$INSTANCE" healthCheckPath 2>/dev/null || echo "/")
41
+ WEB_PORT=$(state_get "$INSTANCE" webPort)
42
+ PB_PORT=$(state_get "$INSTANCE" pbPort)
43
+ WEB_UNIT=$(unit_web "$INSTANCE")
44
+ PB_UNIT=$(unit_pb "$INSTANCE")
45
+
46
+ log "rolling back to $TARGET"
47
+ systemctl stop "$WEB_UNIT" >/dev/null 2>&1 || true
48
+ if [ "$BACKEND" = "true" ]; then
49
+ systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true
50
+ # Down migrations belong to the release being left behind, so they run from
51
+ # the current release's migration set before the symlink moves.
52
+ if [ -n "$CURRENT" ] && [ -d "$APP/releases/$CURRENT/migrations" ]; then
53
+ AHEAD=$(migrations_ahead "$APP/releases/$CURRENT/migrations" "$APP/releases/$TARGET/migrations")
54
+ if [ "$AHEAD" -gt 0 ]; then
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
62
+ fi
63
+ fi
64
+
65
+ ln -sfn "$APP/releases/$TARGET" "$APP/.current.tmp"
66
+ mv -Tf "$APP/.current.tmp" "$APP/current"
67
+
68
+ # The running release is part of the instance's environment, so it has to move
69
+ # with the symlink.
70
+ ETC=$(etc_dir "$INSTANCE")
71
+ if [ -f "$ETC/runtime.env" ]; then
72
+ sed -i "s|^VELA_RELEASE=.*|VELA_RELEASE=$TARGET|" "$ETC/runtime.env"
73
+ fi
74
+
75
+ if [ "$BACKEND" = "true" ]; then
76
+ systemctl restart "$PB_UNIT"
77
+ wait_for_http "http://127.0.0.1:$PB_PORT/api/health" 60 0.5 \
78
+ || die "PocketBase did not become healthy after rollback"
79
+ fi
80
+ systemctl restart "$WEB_UNIT"
81
+ wait_for_http "http://127.0.0.1:$WEB_PORT$HEALTH_PATH" 60 0.5 \
82
+ || die "app did not become healthy after rollback"
83
+
84
+ state_merge "$INSTANCE" "$(jq -c -n --arg t "$TARGET" --arg c "$CURRENT" \
85
+ --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
86
+ '{activeRelease: $t, previousRelease: $c, rolledBackAt: $at}')"
87
+
88
+ emit_result --arg release "$TARGET" --arg from "$CURRENT" '{release: $release, from: $from}'
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Print the state of one instance, or every instance on the server, as JSON.
4
+ #
5
+ # usage: status.sh [instance]
6
+ set -Eeuo pipefail
7
+
8
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
9
+ # shellcheck source=lib.sh
10
+ . "$SCRIPT_DIR/lib.sh"
11
+
12
+ require_provisioned
13
+
14
+ describe() {
15
+ local instance=$1
16
+ local app; app=$(app_dir "$instance")
17
+ local state="$app/state.json"
18
+ [ -f "$state" ] || return 0
19
+
20
+ local web_state pb_state releases current
21
+ web_state=$(systemctl is-active "$(unit_web "$instance")" 2>/dev/null || true)
22
+ pb_state=$(systemctl is-active "$(unit_pb "$instance")" 2>/dev/null || true)
23
+ current=$([ -L "$app/current" ] && basename "$(readlink -f "$app/current")" || echo "")
24
+ releases=$(ls -1 "$app/releases" 2>/dev/null | sort -r | jq -R . | jq -sc .)
25
+
26
+ jq -c \
27
+ --arg web "$web_state" --arg pb "$pb_state" --arg current "$current" \
28
+ --argjson releases "$releases" \
29
+ '. + {services: {web: $web, pocketbase: $pb}, current: $current, releases: $releases}' \
30
+ "$state"
31
+ }
32
+
33
+ collect() {
34
+ if [ $# -ge 1 ] && [ -n "$1" ]; then
35
+ describe "$1"
36
+ else
37
+ for dir in "$VELA_ROOT"/apps/*/; do
38
+ [ -d "$dir" ] || continue
39
+ describe "$(basename "$dir")"
40
+ done
41
+ fi
42
+ }
43
+
44
+ printf 'VELA_RESULT %s\n' "$(collect "$@" | jq -sc .)"
@@ -0,0 +1,41 @@
1
+ [Unit]
2
+ Description=vela PocketBase %i
3
+ Documentation=https://docs.velastack.dev
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=vela
10
+ Group=vela
11
+ WorkingDirectory=/var/lib/vela/apps/%i
12
+
13
+ EnvironmentFile=-/etc/vela/apps/%i/env
14
+ EnvironmentFile=/etc/vela/apps/%i/runtime.env
15
+
16
+ ExecStart=/var/lib/vela/apps/%i/bin/pocketbase \
17
+ --dir /var/lib/vela/apps/%i/shared/pb_data \
18
+ --migrationsDir /var/lib/vela/apps/%i/current/migrations \
19
+ --hooksDir /var/lib/vela/apps/%i/current/hooks \
20
+ serve --http=127.0.0.1:${PB_PORT}
21
+
22
+ Restart=on-failure
23
+ RestartSec=2
24
+ TimeoutStartSec=60
25
+ TimeoutStopSec=30
26
+ KillSignal=SIGINT
27
+
28
+ StandardOutput=journal
29
+ StandardError=journal
30
+ SyslogIdentifier=vela-pb-%i
31
+
32
+ LimitNOFILE=65535
33
+
34
+ NoNewPrivileges=true
35
+ PrivateTmp=true
36
+ ProtectSystem=strict
37
+ ProtectHome=true
38
+ ReadWritePaths=/var/lib/vela/apps/%i
39
+
40
+ [Install]
41
+ WantedBy=multi-user.target
@@ -0,0 +1,39 @@
1
+ [Unit]
2
+ Description=vela app %i
3
+ Documentation=https://docs.velastack.dev
4
+ After=network-online.target vela-pb@%i.service
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=vela
10
+ Group=vela
11
+ WorkingDirectory=/var/lib/vela/apps/%i/current
12
+
13
+ # Production secrets first, vela-generated runtime settings second, so PORT and
14
+ # POCKETBASE_URL always match what the server actually assigned.
15
+ EnvironmentFile=-/etc/vela/apps/%i/env
16
+ EnvironmentFile=/etc/vela/apps/%i/runtime.env
17
+
18
+ ExecStart=/usr/bin/node /var/lib/vela/apps/%i/current/build/index.js
19
+
20
+ Restart=on-failure
21
+ RestartSec=2
22
+ TimeoutStartSec=60
23
+ TimeoutStopSec=30
24
+ KillSignal=SIGINT
25
+
26
+ StandardOutput=journal
27
+ StandardError=journal
28
+ SyslogIdentifier=vela-web-%i
29
+
30
+ LimitNOFILE=65535
31
+
32
+ NoNewPrivileges=true
33
+ PrivateTmp=true
34
+ ProtectSystem=strict
35
+ ProtectHome=true
36
+ ReadWritePaths=/var/lib/vela/apps/%i
37
+
38
+ [Install]
39
+ WantedBy=multi-user.target