zl-code 0.0.30 → 0.0.32
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/dist/bin.mjs +6271 -5269
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/{DiffPanel-DPeVDy87.js → DiffPanel-zCbsoszm.js} +1 -1
- package/dist/client/assets/{FilePreviewPanel-CoSSgo5p.js → FilePreviewPanel-GEuC3j76.js} +1 -1
- package/dist/client/assets/{PreviewPanel-y0Pign2e.js → PreviewPanel-C9dhIGyh.js} +1 -1
- package/dist/client/assets/{angular-html-C7-uPMFl.js → angular-html-BMjNownc.js} +1 -1
- package/dist/client/assets/{angular-ts-xNhvCrgW.js → angular-ts-B5LPf24i.js} +1 -1
- package/dist/client/assets/{apl-Cbzu1Nmg.js → apl-Iiv22-rq.js} +1 -1
- package/dist/client/assets/{astro-BcXGqCfF.js → astro-C7X5I9rp.js} +1 -1
- package/dist/client/assets/{blade-DXoD1tq3.js → blade-ClfIyMfn.js} +1 -1
- package/dist/client/assets/{c-ww9DT4H9.js → c-BQ8eShnN.js} +1 -1
- package/dist/client/assets/{cobol-Uf0pxp3c.js → cobol-DqNqjz_S.js} +1 -1
- package/dist/client/assets/{coffee-CNtx8SYp.js → coffee-Dfy3eYCK.js} +1 -1
- package/dist/client/assets/{cpp-CNtAvTB0.js → cpp-C5wPYC_c.js} +1 -1
- package/dist/client/assets/{crystal-B_GNKS-f.js → crystal-C_a9rG7e.js} +1 -1
- package/dist/client/assets/{css-EjGGwsxM.js → css-D4w5bo68.js} +1 -1
- package/dist/client/assets/{edge-FdFiwuuE.js → edge-DvZwYTRV.js} +1 -1
- package/dist/client/assets/{elixir-DDsnq8Pt.js → elixir-D-VlWr-X.js} +1 -1
- package/dist/client/assets/{elm-ZpE4zkBa.js → elm-DWj16Mpb.js} +1 -1
- package/dist/client/assets/{erb-B-Z8312X.js → erb-DlxOzz2-.js} +1 -1
- package/dist/client/assets/{fileCommentAnnotations-BBQH2v2a.js → fileCommentAnnotations-ClcpOWQa.js} +1 -1
- package/dist/client/assets/{git-rebase-CGEmx0LI.js → git-rebase-BBxaCC9q.js} +1 -1
- package/dist/client/assets/{glimmer-js-COOoKt3i.js → glimmer-js-DkqomO9t.js} +1 -1
- package/dist/client/assets/{glimmer-ts-D1F8OsDX.js → glimmer-ts-DqP3lMYF.js} +1 -1
- package/dist/client/assets/{glsl-DJH58HGE.js → glsl-CTIHfVFl.js} +1 -1
- package/dist/client/assets/{graphql-DiNe2tpe.js → graphql-D12g55j9.js} +1 -1
- package/dist/client/assets/{hack-Dekeh7e5.js → hack-DiEhbY4L.js} +1 -1
- package/dist/client/assets/{haml-BVSoeu2x.js → haml-k10ejFQT.js} +1 -1
- package/dist/client/assets/{handlebars-k1nk8eCR.js → handlebars-Bp3Y28q9.js} +1 -1
- package/dist/client/assets/{html-DL9dLLUO.js → html-CUWUgIeB.js} +1 -1
- package/dist/client/assets/{html-derivative-CUKIwoBz.js → html-derivative-DfyM2SPX.js} +1 -1
- package/dist/client/assets/{http-imAz0kdM.js → http-DPJUcqBH.js} +1 -1
- package/dist/client/assets/{hurl-DmtXySiF.js → hurl-DEgmYbvA.js} +1 -1
- package/dist/client/assets/index-CvcF6-DK.js +577 -0
- package/dist/client/assets/{java-C_iicb65.js → java-C8F05rcf.js} +1 -1
- package/dist/client/assets/{javascript-BB2uj7le.js → javascript-DouaYy_g.js} +1 -1
- package/dist/client/assets/{jinja-CN975xAC.js → jinja-Be9nMfz4.js} +1 -1
- package/dist/client/assets/{jison-DgcPoUOY.js → jison-DudveEHD.js} +1 -1
- package/dist/client/assets/{json-Ddi2d9VD.js → json-Cs3qd19t.js} +1 -1
- package/dist/client/assets/{jsx-DYU0gI8H.js → jsx-5HPw0X3q.js} +1 -1
- package/dist/client/assets/{julia-DhqSTpmP.js → julia-BoQ-G9_W.js} +1 -1
- package/dist/client/assets/{just-h3se1pga.js → just-Cbl3z7sB.js} +1 -1
- package/dist/client/assets/{latex-BS-rXA6Q.js → latex-CTaz-xa9.js} +1 -1
- package/dist/client/assets/{liquid-DHdObD9D.js → liquid-nTT4KJIV.js} +1 -1
- package/dist/client/assets/{lua-BGB4Nt27.js → lua-Dvbu0Xf8.js} +1 -1
- package/dist/client/assets/{marko-Bx2nm-_y.js → marko-HSTFVa6O.js} +1 -1
- package/dist/client/assets/{mdc-1G1Pxvit.js → mdc-hyhTtO2H.js} +1 -1
- package/dist/client/assets/{nginx-BmU1aBWa.js → nginx-xeSRWU27.js} +1 -1
- package/dist/client/assets/{nim-45VoVczs.js → nim-D8r6vzf8.js} +1 -1
- package/dist/client/assets/{perl-DGeP60Eh.js → perl-CJlDsa7P.js} +1 -1
- package/dist/client/assets/{php-BOvK9gQB.js → php-akslWydZ.js} +1 -1
- package/dist/client/assets/{pug-C3v1lq7N.js → pug-Cc2NlxNM.js} +1 -1
- package/dist/client/assets/{qml-D4WEnAWk.js → qml-CSw3No2z.js} +1 -1
- package/dist/client/assets/{r-chkD_kWf.js → r-pY2PXIBb.js} +1 -1
- package/dist/client/assets/{razor-D5zg9w6m.js → razor-0_ry7Rob.js} +1 -1
- package/dist/client/assets/{regexp-Br_6RjDB.js → regexp-2vomjM_W.js} +1 -1
- package/dist/client/assets/{rst-BozCtIg9.js → rst-C-cZW4l2.js} +1 -1
- package/dist/client/assets/{ruby-CGjgsblc.js → ruby-BxaEq5Zq.js} +1 -1
- package/dist/client/assets/{sas-CVxWda8Q.js → sas-96FB6PFP.js} +1 -1
- package/dist/client/assets/{scss-DeFuqIK_.js → scss-Ct-gnIxF.js} +1 -1
- package/dist/client/assets/{shellscript-C6Q7qkUD.js → shellscript-ypyzeT3L.js} +1 -1
- package/dist/client/assets/{shellsession-WLo6A5mg.js → shellsession-DgirLgWe.js} +1 -1
- package/dist/client/assets/{soy-BcVC4qYp.js → soy-BVOIU10T.js} +1 -1
- package/dist/client/assets/{sql-ZjNem-IK.js → sql-GXzok57n.js} +1 -1
- package/dist/client/assets/{stata-BZ73C6JZ.js → stata-H-DFe69L.js} +1 -1
- package/dist/client/assets/{surrealql-CR43JrBJ.js → surrealql-3Y5zorm-.js} +1 -1
- package/dist/client/assets/{svelte-BpXqMiBr.js → svelte-CuLFybtz.js} +1 -1
- package/dist/client/assets/{templ-DSw5UMTq.js → templ-C-ht3RD5.js} +1 -1
- package/dist/client/assets/{tex-DaHKjnWo.js → tex-B3tZ6fPP.js} +1 -1
- package/dist/client/assets/textarea-CVmVd8xn.js +44 -0
- package/dist/client/assets/{ts-tags-SZ8IZlNT.js → ts-tags-Dn_kS980.js} +1 -1
- package/dist/client/assets/{tsx-CwwA7B6A.js → tsx-CYfxZJqp.js} +1 -1
- package/dist/client/assets/{twig-B_ebiDih.js → twig-CE2h-NqH.js} +1 -1
- package/dist/client/assets/{typescript-DO3j7XZx.js → typescript-Bufm6oeP.js} +1 -1
- package/dist/client/assets/{vue-B8Sj9crw.js → vue-BpYFSTwh.js} +1 -1
- package/dist/client/assets/{vue-html-DOid4MmJ.js → vue-html-C4Ma9CEV.js} +1 -1
- package/dist/client/assets/{vue-vine-CZ-OLWJd.js → vue-vine-C5Uds-em.js} +1 -1
- package/dist/client/assets/{xml-CQ7Xk9af.js → xml-D2xJwq9a.js} +1 -1
- package/dist/client/assets/{xsl-CNh6rw4f.js → xsl-DrXThzkt.js} +1 -1
- package/dist/client/assets/{yaml-BkM14TZe.js → yaml-Dnr4uuaT.js} +1 -1
- package/dist/client/favicon-16x16.png +0 -0
- package/dist/client/favicon-32x32.png +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/index.html +5 -5
- package/dist/client/zettai-labs-z-mark.svg +1 -0
- package/dist/npm-service-runtime.sh +808 -0
- package/dist/zl-code-server-npm.service +29 -0
- package/dist/zl-code-update-supervisor.service +20 -0
- package/package.json +1 -1
- package/dist/client/assets/index-CxcYRXaB.js +0 -577
- package/dist/client/assets/textarea-nMqSOy-S.js +0 -44
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
5
|
+
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
6
|
+
REPO_DIR="$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)"
|
|
7
|
+
|
|
8
|
+
RUNTIME_ROOT="${ZL_CODE_NPM_SERVICE_ROOT:-${HOME}/.local/share/zl-code}"
|
|
9
|
+
PRODUCTION_HOME="${ZL_CODE_NPM_SERVICE_HOME:-${HOME}/.zettai-labs-code}"
|
|
10
|
+
OPERATOR_STATE="${ZL_CODE_NPM_SERVICE_STATE_DIR:-${HOME}/.local/state/zl-code/npm-service}"
|
|
11
|
+
UNIT_PATH="${ZL_CODE_NPM_SERVICE_UNIT_PATH:-${HOME}/.config/systemd/user/zl-code-server.service}"
|
|
12
|
+
DEFAULT_UNIT_TEMPLATE="${REPO_DIR}/infra/systemd/zl-code-server-npm.service"
|
|
13
|
+
[[ -f "${SCRIPT_DIR}/zl-code-server-npm.service" ]] && DEFAULT_UNIT_TEMPLATE="${SCRIPT_DIR}/zl-code-server-npm.service"
|
|
14
|
+
UNIT_TEMPLATE="${ZL_CODE_NPM_SERVICE_UNIT_TEMPLATE:-${DEFAULT_UNIT_TEMPLATE}}"
|
|
15
|
+
SUPERVISOR_UNIT_PATH="${ZL_CODE_UPDATE_SUPERVISOR_UNIT_PATH:-${HOME}/.config/systemd/user/zl-code-update-supervisor.service}"
|
|
16
|
+
DEFAULT_SUPERVISOR_UNIT_TEMPLATE="${REPO_DIR}/infra/systemd/zl-code-update-supervisor.service"
|
|
17
|
+
[[ -f "${SCRIPT_DIR}/zl-code-update-supervisor.service" ]] && DEFAULT_SUPERVISOR_UNIT_TEMPLATE="${SCRIPT_DIR}/zl-code-update-supervisor.service"
|
|
18
|
+
SUPERVISOR_UNIT_TEMPLATE="${ZL_CODE_UPDATE_SUPERVISOR_UNIT_TEMPLATE:-${DEFAULT_SUPERVISOR_UNIT_TEMPLATE}}"
|
|
19
|
+
SUPERVISOR_SERVICE_NAME="${ZL_CODE_UPDATE_SUPERVISOR_SERVICE_NAME:-zl-code-update-supervisor.service}"
|
|
20
|
+
NODE_BIN="${ZL_CODE_NPM_SERVICE_NODE_BIN:-${HOME}/.local/share/mise/installs/node/24.19.0/bin/node}"
|
|
21
|
+
NPM_BIN="${ZL_CODE_NPM_SERVICE_NPM_BIN:-${HOME}/.local/share/mise/installs/node/24.19.0/bin/npm}"
|
|
22
|
+
SERVICE_NAME="${ZL_CODE_NPM_SERVICE_NAME:-zl-code-server.service}"
|
|
23
|
+
LOCAL_URL="${ZL_CODE_NPM_SERVICE_LOCAL_URL:-http://127.0.0.1:14773/}"
|
|
24
|
+
PUBLIC_URL="${ZL_CODE_NPM_SERVICE_PUBLIC_URL:-https://code.zettailabs.com/}"
|
|
25
|
+
RESTART_STATE_FILE="${ZL_CODE_NPM_SERVICE_RESTART_STATE_FILE:-${PRODUCTION_HOME}/userdata/server-restart.json}"
|
|
26
|
+
DRAIN_SIGNAL="${ZL_CODE_NPM_SERVICE_DRAIN_SIGNAL:-SIGUSR1}"
|
|
27
|
+
DRAIN_ACK_TIMEOUT_SECONDS="${ZL_CODE_NPM_SERVICE_DRAIN_ACK_TIMEOUT_SECONDS:-10}"
|
|
28
|
+
DRAIN_QUIESCE_TIMEOUT_SECONDS="${ZL_CODE_NPM_SERVICE_DRAIN_QUIESCE_TIMEOUT_SECONDS:-10}"
|
|
29
|
+
LOCK_FILE="${OPERATOR_STATE}/operation.lock"
|
|
30
|
+
ROLLBACK_DIR="${OPERATOR_STATE}/rollback"
|
|
31
|
+
TRANSACTION_DIR="${OPERATOR_STATE}/transaction"
|
|
32
|
+
LOCK_HELD=false
|
|
33
|
+
SUPERVISED_MODE=false
|
|
34
|
+
ACTIVATION_BOUNDARY_CAPTURED=false
|
|
35
|
+
ACTIVATION_CUTOVER_STARTED=false
|
|
36
|
+
ACTIVATION_ROLLBACK_COMPLETED=false
|
|
37
|
+
SUPERVISED_ROLLBACK_VERSION=""
|
|
38
|
+
|
|
39
|
+
usage() {
|
|
40
|
+
cat <<'EOF'
|
|
41
|
+
Manage the versioned npm runtime for the local Zettai Labs Code service.
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
scripts/npm-service-runtime.sh status
|
|
45
|
+
scripts/npm-service-runtime.sh prepare <version> --no-cutover
|
|
46
|
+
scripts/npm-service-runtime.sh backup
|
|
47
|
+
scripts/npm-service-runtime.sh activate <version> --backup-dir <path> --confirm-cutover
|
|
48
|
+
scripts/npm-service-runtime.sh verify <version>
|
|
49
|
+
scripts/npm-service-runtime.sh rollback --to previous|git --confirm-cutover
|
|
50
|
+
scripts/npm-service-runtime.sh supervised-update <version> --confirm-supervised
|
|
51
|
+
scripts/npm-service-runtime.sh install-supervisor --confirm-install
|
|
52
|
+
|
|
53
|
+
`prepare` and `backup` never reload or restart systemd. `activate` and
|
|
54
|
+
`rollback` refuse to run without the literal --confirm-cutover flag.
|
|
55
|
+
EOF
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fail() {
|
|
59
|
+
echo "error: $*" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log() {
|
|
64
|
+
printf '\n==> %s\n' "$*"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
require_command() {
|
|
68
|
+
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
require_file() {
|
|
72
|
+
[[ -f "$1" ]] || fail "required file not found: $1"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
require_executable() {
|
|
76
|
+
[[ -x "$1" ]] || fail "required executable not found: $1"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
acquire_lock() {
|
|
80
|
+
[[ "$LOCK_HELD" == true ]] && return
|
|
81
|
+
mkdir -p "$OPERATOR_STATE"
|
|
82
|
+
chmod 700 "$OPERATOR_STATE"
|
|
83
|
+
exec 9>"$LOCK_FILE"
|
|
84
|
+
flock -n 9 || fail "another npm service operation is running"
|
|
85
|
+
LOCK_HELD=true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
emit_supervisor_phase() {
|
|
89
|
+
[[ "$SUPERVISED_MODE" == true ]] || return 0
|
|
90
|
+
echo "supervisor_phase=$1"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
validate_version() {
|
|
94
|
+
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]] ||
|
|
95
|
+
fail "invalid exact npm version: $1"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
validate_operation_id() {
|
|
99
|
+
[[ "$1" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]] ||
|
|
100
|
+
fail "invalid supervised update operation id"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
release_dir() {
|
|
104
|
+
printf '%s/releases/%s\n' "$RUNTIME_ROOT" "$1"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
package_entry() {
|
|
108
|
+
printf '%s/node_modules/zl-code/dist/bin.mjs\n' "$(release_dir "$1")"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
atomic_symlink() {
|
|
112
|
+
local target=$1
|
|
113
|
+
local link_path=$2
|
|
114
|
+
local temporary="${link_path}.tmp.$$"
|
|
115
|
+
mkdir -p "$(dirname "$link_path")"
|
|
116
|
+
rm -f "$temporary"
|
|
117
|
+
ln -s "$target" "$temporary"
|
|
118
|
+
mv -Tf "$temporary" "$link_path"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
read_current_release_identity() {
|
|
122
|
+
local package_path="${RUNTIME_ROOT}/current/node_modules/zl-code/package.json"
|
|
123
|
+
if [[ ! -f "$package_path" ]]; then
|
|
124
|
+
printf 'release:absent\n'
|
|
125
|
+
return
|
|
126
|
+
fi
|
|
127
|
+
printf 'release:%s\n' "$($NODE_BIN -e 'const p=require(process.argv[1]); process.stdout.write(String(p.version ?? ""))' "$package_path")"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
write_restart_state() {
|
|
131
|
+
local restart_id=$1
|
|
132
|
+
local initiator=$2
|
|
133
|
+
local current_sha=$3
|
|
134
|
+
local target_sha=$4
|
|
135
|
+
local requested_at=$5
|
|
136
|
+
mkdir -p "$(dirname "$RESTART_STATE_FILE")"
|
|
137
|
+
"$NODE_BIN" - "$RESTART_STATE_FILE" "$restart_id" "$initiator" "$current_sha" "$target_sha" "$requested_at" <<'NODE'
|
|
138
|
+
const fs = require("node:fs");
|
|
139
|
+
const [file, restartId, initiator, currentSha, targetSha, requestedAt] = process.argv.slice(2);
|
|
140
|
+
const state = {
|
|
141
|
+
version: 1,
|
|
142
|
+
reason: "update",
|
|
143
|
+
restartId,
|
|
144
|
+
initiator,
|
|
145
|
+
currentSha,
|
|
146
|
+
targetSha,
|
|
147
|
+
requestedAt,
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(file, `${JSON.stringify(state)}\n`);
|
|
150
|
+
NODE
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
patch_restart_state() {
|
|
154
|
+
local patch=$1
|
|
155
|
+
"$NODE_BIN" - "$RESTART_STATE_FILE" "$patch" <<'NODE'
|
|
156
|
+
const fs = require("node:fs");
|
|
157
|
+
const [file, rawPatch] = process.argv.slice(2);
|
|
158
|
+
const patch = JSON.parse(rawPatch);
|
|
159
|
+
const current = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
160
|
+
fs.writeFileSync(file, `${JSON.stringify({ ...current, ...patch })}\n`);
|
|
161
|
+
NODE
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
read_restart_state_field() {
|
|
165
|
+
local field=$1
|
|
166
|
+
"$NODE_BIN" - "$RESTART_STATE_FILE" "$field" <<'NODE'
|
|
167
|
+
const fs = require("node:fs");
|
|
168
|
+
const [file, field] = process.argv.slice(2);
|
|
169
|
+
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
170
|
+
const value = state[field];
|
|
171
|
+
if (value === undefined || value === null) {
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
process.stdout.write(String(value));
|
|
175
|
+
NODE
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await_restart_state_field() {
|
|
179
|
+
local field=$1
|
|
180
|
+
local timeout_seconds=$2
|
|
181
|
+
local attempt=1
|
|
182
|
+
while [[ "$attempt" -le "$timeout_seconds" ]]; do
|
|
183
|
+
if read_restart_state_field "$field" >/dev/null; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
sleep 1
|
|
187
|
+
attempt=$((attempt + 1))
|
|
188
|
+
done
|
|
189
|
+
return 1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
verify_release() {
|
|
193
|
+
local version=$1
|
|
194
|
+
local directory
|
|
195
|
+
local entry
|
|
196
|
+
local reported
|
|
197
|
+
directory="$(release_dir "$version")"
|
|
198
|
+
entry="$(package_entry "$version")"
|
|
199
|
+
require_file "$directory/node_modules/zl-code/package.json"
|
|
200
|
+
require_file "$entry"
|
|
201
|
+
reported="$($NODE_BIN -e 'const p=require(process.argv[1]); process.stdout.write(p.name+"@"+p.version)' "$directory/node_modules/zl-code/package.json")"
|
|
202
|
+
[[ "$reported" == "zl-code@${version}" ]] || fail "release identity mismatch: $reported"
|
|
203
|
+
reported="$($NODE_BIN "$entry" --version)"
|
|
204
|
+
[[ "$reported" == "zl-code v${version}" ]] || fail "CLI identity mismatch: $reported"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
prepare_release() {
|
|
208
|
+
local version=$1
|
|
209
|
+
local no_cutover=$2
|
|
210
|
+
local directory
|
|
211
|
+
local temporary
|
|
212
|
+
[[ "$no_cutover" == true ]] || fail "prepare requires --no-cutover"
|
|
213
|
+
validate_version "$version"
|
|
214
|
+
acquire_lock
|
|
215
|
+
require_executable "$NODE_BIN"
|
|
216
|
+
require_executable "$NPM_BIN"
|
|
217
|
+
require_command mv
|
|
218
|
+
export PATH="$(dirname "$NODE_BIN"):${PATH}"
|
|
219
|
+
|
|
220
|
+
directory="$(release_dir "$version")"
|
|
221
|
+
mkdir -p "$RUNTIME_ROOT/releases"
|
|
222
|
+
chmod 700 "$RUNTIME_ROOT" "$RUNTIME_ROOT/releases"
|
|
223
|
+
|
|
224
|
+
if [[ ! -d "$directory" ]]; then
|
|
225
|
+
temporary="${RUNTIME_ROOT}/releases/.${version}.prepare.$$"
|
|
226
|
+
[[ ! -e "$temporary" ]] || fail "temporary release path already exists: $temporary"
|
|
227
|
+
mkdir -p "$temporary"
|
|
228
|
+
chmod 700 "$temporary"
|
|
229
|
+
log "Installing exact zl-code@${version} without changing production"
|
|
230
|
+
"$NPM_BIN" install --prefix "$temporary" --omit=dev --no-audit --no-fund \
|
|
231
|
+
--package-lock=false "zl-code@${version}"
|
|
232
|
+
mv "$temporary" "$directory"
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
verify_release "$version"
|
|
236
|
+
if [[ "${ZL_CODE_NPM_SERVICE_SKIP_SIGNATURE_AUDIT:-false}" != true ]]; then
|
|
237
|
+
log "Verifying npm registry signatures and attestations"
|
|
238
|
+
"$NPM_BIN" audit signatures --prefix "$directory"
|
|
239
|
+
fi
|
|
240
|
+
if [[ "${ZL_CODE_NPM_SERVICE_SKIP_PTY_SMOKE:-false}" != true ]]; then
|
|
241
|
+
log "Verifying the installed native PTY"
|
|
242
|
+
"$NODE_BIN" --input-type=module - "$directory" <<'NODE'
|
|
243
|
+
import { createRequire } from "node:module";
|
|
244
|
+
const require = createRequire(import.meta.url);
|
|
245
|
+
const root = process.argv[2];
|
|
246
|
+
const pty = require(`${root}/node_modules/node-pty`);
|
|
247
|
+
await new Promise((resolve, reject) => {
|
|
248
|
+
const child = pty.spawn("/bin/sh", ["-lc", "printf zl-code-pty-ok"], {
|
|
249
|
+
cols: 80,
|
|
250
|
+
rows: 24,
|
|
251
|
+
cwd: root,
|
|
252
|
+
env: process.env,
|
|
253
|
+
});
|
|
254
|
+
let output = "";
|
|
255
|
+
child.onData((chunk) => {
|
|
256
|
+
output += chunk;
|
|
257
|
+
});
|
|
258
|
+
child.onExit(({ exitCode }) => {
|
|
259
|
+
if (exitCode === 0 && output.includes("zl-code-pty-ok")) resolve();
|
|
260
|
+
else reject(new Error(`PTY smoke failed with exit ${exitCode}: ${output}`));
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
NODE
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
atomic_symlink "$directory" "$RUNTIME_ROOT/candidate"
|
|
267
|
+
echo "candidate=$(readlink -f "$RUNTIME_ROOT/candidate")"
|
|
268
|
+
if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
|
|
269
|
+
echo "current=$(readlink -f "$RUNTIME_ROOT/current")"
|
|
270
|
+
else
|
|
271
|
+
echo "current=absent"
|
|
272
|
+
fi
|
|
273
|
+
echo "cutover=not-performed"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
sqlite_quick_check() {
|
|
277
|
+
local database_path=$1
|
|
278
|
+
"$NODE_BIN" - "$database_path" <<'NODE'
|
|
279
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
280
|
+
const database = new DatabaseSync(process.argv[2], { readOnly: true });
|
|
281
|
+
const rows = database.prepare("PRAGMA quick_check").all();
|
|
282
|
+
database.close();
|
|
283
|
+
if (rows.length !== 1 || rows[0].quick_check !== "ok") {
|
|
284
|
+
console.error(JSON.stringify(rows));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
console.log("quick_check=ok");
|
|
288
|
+
NODE
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
create_backup() {
|
|
292
|
+
local source_database="${PRODUCTION_HOME}/userdata/state.sqlite"
|
|
293
|
+
local timestamp
|
|
294
|
+
local destination
|
|
295
|
+
local boundary_archive
|
|
296
|
+
acquire_lock
|
|
297
|
+
require_executable "$NODE_BIN"
|
|
298
|
+
require_file "$source_database"
|
|
299
|
+
require_file "$PRODUCTION_HOME/userdata/environment-id"
|
|
300
|
+
require_file "$PRODUCTION_HOME/userdata/server-runtime.json"
|
|
301
|
+
require_command sha256sum
|
|
302
|
+
require_command tar
|
|
303
|
+
|
|
304
|
+
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
305
|
+
destination="${OPERATOR_STATE}/backups/${timestamp}"
|
|
306
|
+
boundary_archive="${destination}/runtime-boundary.tar"
|
|
307
|
+
mkdir -p "$destination"
|
|
308
|
+
chmod 700 "$destination"
|
|
309
|
+
|
|
310
|
+
log "Creating an online SQLite backup"
|
|
311
|
+
"$NODE_BIN" --input-type=module - "$source_database" "$destination/state.sqlite" <<'NODE'
|
|
312
|
+
import { DatabaseSync, backup } from "node:sqlite";
|
|
313
|
+
const source = new DatabaseSync(process.argv[2], { readOnly: true });
|
|
314
|
+
try {
|
|
315
|
+
await backup(source, process.argv[3]);
|
|
316
|
+
} finally {
|
|
317
|
+
source.close();
|
|
318
|
+
}
|
|
319
|
+
NODE
|
|
320
|
+
sqlite_quick_check "$destination/state.sqlite"
|
|
321
|
+
|
|
322
|
+
log "Archiving the bounded configuration and identity boundary"
|
|
323
|
+
tar --create --file "$boundary_archive" --directory "$PRODUCTION_HOME" \
|
|
324
|
+
userdata/config userdata/secrets userdata/environment-id userdata/server-runtime.json
|
|
325
|
+
chmod 600 "$destination/state.sqlite" "$boundary_archive"
|
|
326
|
+
if [[ -e "$UNIT_PATH" || -L "$UNIT_PATH" ]]; then
|
|
327
|
+
cp -L "$UNIT_PATH" "$destination/zl-code-server.service.before-cutover"
|
|
328
|
+
chmod 600 "$destination/zl-code-server.service.before-cutover"
|
|
329
|
+
fi
|
|
330
|
+
sha256sum "$destination/state.sqlite" "$boundary_archive" >"$destination/SHA256SUMS"
|
|
331
|
+
chmod 600 "$destination/SHA256SUMS"
|
|
332
|
+
{
|
|
333
|
+
echo "created_at=$timestamp"
|
|
334
|
+
echo "source_home=$PRODUCTION_HOME"
|
|
335
|
+
echo "sqlite_backup=state.sqlite"
|
|
336
|
+
echo "boundary_archive=runtime-boundary.tar"
|
|
337
|
+
echo "quick_check=ok"
|
|
338
|
+
} >"$destination/manifest"
|
|
339
|
+
chmod 600 "$destination/manifest"
|
|
340
|
+
echo "backup_dir=$destination"
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
save_git_rollback_unit() {
|
|
344
|
+
mkdir -p "$ROLLBACK_DIR/git"
|
|
345
|
+
chmod 700 "$ROLLBACK_DIR" "$ROLLBACK_DIR/git"
|
|
346
|
+
if [[ ! -f "$ROLLBACK_DIR/git/zl-code-server.service" ]]; then
|
|
347
|
+
[[ -e "$UNIT_PATH" || -L "$UNIT_PATH" ]] || fail "current service unit is missing: $UNIT_PATH"
|
|
348
|
+
cp -L "$UNIT_PATH" "$ROLLBACK_DIR/git/zl-code-server.service"
|
|
349
|
+
chmod 600 "$ROLLBACK_DIR/git/zl-code-server.service"
|
|
350
|
+
if [[ -L "$UNIT_PATH" ]]; then
|
|
351
|
+
readlink "$UNIT_PATH" >"$ROLLBACK_DIR/git/symlink-target"
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
capture_current_pointer() {
|
|
357
|
+
rm -f "$ROLLBACK_DIR/pre-activation-current-target" "$ROLLBACK_DIR/pre-activation-current-absent"
|
|
358
|
+
if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
|
|
359
|
+
readlink -f "$RUNTIME_ROOT/current" >"$ROLLBACK_DIR/pre-activation-current-target"
|
|
360
|
+
else
|
|
361
|
+
: >"$ROLLBACK_DIR/pre-activation-current-absent"
|
|
362
|
+
fi
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
capture_activation_boundary() {
|
|
366
|
+
rm -rf "$TRANSACTION_DIR"
|
|
367
|
+
mkdir -p "$TRANSACTION_DIR"
|
|
368
|
+
chmod 700 "$TRANSACTION_DIR"
|
|
369
|
+
[[ -e "$UNIT_PATH" || -L "$UNIT_PATH" ]] || fail "current service unit is missing: $UNIT_PATH"
|
|
370
|
+
cp -L "$UNIT_PATH" "$TRANSACTION_DIR/zl-code-server.service"
|
|
371
|
+
chmod 600 "$TRANSACTION_DIR/zl-code-server.service"
|
|
372
|
+
if [[ -L "$UNIT_PATH" ]]; then
|
|
373
|
+
readlink "$UNIT_PATH" >"$TRANSACTION_DIR/unit-symlink-target"
|
|
374
|
+
fi
|
|
375
|
+
if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
|
|
376
|
+
readlink -f "$RUNTIME_ROOT/current" >"$TRANSACTION_DIR/current-target"
|
|
377
|
+
else
|
|
378
|
+
: >"$TRANSACTION_DIR/current-absent"
|
|
379
|
+
fi
|
|
380
|
+
ACTIVATION_BOUNDARY_CAPTURED=true
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
install_npm_unit() {
|
|
384
|
+
local temporary="${UNIT_PATH}.tmp.$$"
|
|
385
|
+
require_file "$UNIT_TEMPLATE"
|
|
386
|
+
mkdir -p "$(dirname "$UNIT_PATH")"
|
|
387
|
+
install -m 600 "$UNIT_TEMPLATE" "$temporary"
|
|
388
|
+
mv -Tf "$temporary" "$UNIT_PATH"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
restore_git_unit_and_pointer() {
|
|
392
|
+
local temporary="${UNIT_PATH}.rollback.$$"
|
|
393
|
+
require_file "$ROLLBACK_DIR/git/zl-code-server.service"
|
|
394
|
+
if [[ -f "$ROLLBACK_DIR/git/symlink-target" ]]; then
|
|
395
|
+
rm -f "$temporary"
|
|
396
|
+
ln -s "$(cat "$ROLLBACK_DIR/git/symlink-target")" "$temporary"
|
|
397
|
+
else
|
|
398
|
+
install -m 600 "$ROLLBACK_DIR/git/zl-code-server.service" "$temporary"
|
|
399
|
+
fi
|
|
400
|
+
mv -Tf "$temporary" "$UNIT_PATH"
|
|
401
|
+
|
|
402
|
+
if [[ -f "$ROLLBACK_DIR/pre-activation-current-target" ]]; then
|
|
403
|
+
atomic_symlink "$(cat "$ROLLBACK_DIR/pre-activation-current-target")" "$RUNTIME_ROOT/current"
|
|
404
|
+
elif [[ -f "$ROLLBACK_DIR/pre-activation-current-absent" ]]; then
|
|
405
|
+
rm -f "$RUNTIME_ROOT/current"
|
|
406
|
+
fi
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
restore_activation_boundary() {
|
|
410
|
+
local temporary="${UNIT_PATH}.rollback.$$"
|
|
411
|
+
require_file "$TRANSACTION_DIR/zl-code-server.service"
|
|
412
|
+
if [[ -f "$TRANSACTION_DIR/unit-symlink-target" ]]; then
|
|
413
|
+
rm -f "$temporary"
|
|
414
|
+
ln -s "$(cat "$TRANSACTION_DIR/unit-symlink-target")" "$temporary"
|
|
415
|
+
else
|
|
416
|
+
install -m 600 "$TRANSACTION_DIR/zl-code-server.service" "$temporary"
|
|
417
|
+
fi
|
|
418
|
+
mv -Tf "$temporary" "$UNIT_PATH"
|
|
419
|
+
|
|
420
|
+
if [[ -f "$TRANSACTION_DIR/current-target" ]]; then
|
|
421
|
+
atomic_symlink "$(cat "$TRANSACTION_DIR/current-target")" "$RUNTIME_ROOT/current"
|
|
422
|
+
elif [[ -f "$TRANSACTION_DIR/current-absent" ]]; then
|
|
423
|
+
rm -f "$RUNTIME_ROOT/current"
|
|
424
|
+
fi
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
supervised_exit_cleanup() {
|
|
428
|
+
local status=$?
|
|
429
|
+
trap - EXIT TERM INT
|
|
430
|
+
if [[ "$status" -ne 0 && "$ACTIVATION_CUTOVER_STARTED" == true && "$ACTIVATION_ROLLBACK_COMPLETED" != true ]]; then
|
|
431
|
+
emit_supervisor_phase "rolling_back"
|
|
432
|
+
if [[ "$ACTIVATION_BOUNDARY_CAPTURED" == true ]]; then
|
|
433
|
+
if restore_activation_boundary &&
|
|
434
|
+
systemctl --user daemon-reload &&
|
|
435
|
+
systemctl --user restart "$SERVICE_NAME" &&
|
|
436
|
+
systemctl --user is-active --quiet "$SERVICE_NAME" &&
|
|
437
|
+
wait_for_url "$LOCAL_URL"; then
|
|
438
|
+
ACTIVATION_ROLLBACK_COMPLETED=true
|
|
439
|
+
if [[ -n "$SUPERVISED_ROLLBACK_VERSION" ]]; then
|
|
440
|
+
echo "rollback_version=$SUPERVISED_ROLLBACK_VERSION"
|
|
441
|
+
fi
|
|
442
|
+
else
|
|
443
|
+
echo "error: cleanup could not verify the preceding service runtime" >&2
|
|
444
|
+
fi
|
|
445
|
+
fi
|
|
446
|
+
fi
|
|
447
|
+
exit "$status"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
wait_for_url() {
|
|
451
|
+
local url=$1
|
|
452
|
+
local attempts=${ZL_CODE_NPM_SERVICE_HEALTH_ATTEMPTS:-30}
|
|
453
|
+
local attempt
|
|
454
|
+
for ((attempt = 1; attempt <= attempts; attempt += 1)); do
|
|
455
|
+
if curl --fail --silent --show-error --max-time 3 --output /dev/null "$url"; then
|
|
456
|
+
echo "health=ok attempt=$attempt url=$url"
|
|
457
|
+
return 0
|
|
458
|
+
fi
|
|
459
|
+
sleep 1
|
|
460
|
+
done
|
|
461
|
+
return 1
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
verify_runtime() {
|
|
465
|
+
local version=$1
|
|
466
|
+
local current_target
|
|
467
|
+
local main_pid
|
|
468
|
+
local command_line
|
|
469
|
+
if ! (validate_version "$version"); then
|
|
470
|
+
return 1
|
|
471
|
+
fi
|
|
472
|
+
if ! (verify_release "$version"); then
|
|
473
|
+
return 1
|
|
474
|
+
fi
|
|
475
|
+
current_target="$(readlink -f "$RUNTIME_ROOT/current")"
|
|
476
|
+
[[ "$current_target" == "$(release_dir "$version")" ]] ||
|
|
477
|
+
{
|
|
478
|
+
echo "error: current pointer does not select $(release_dir "$version"): $current_target" >&2
|
|
479
|
+
return 1
|
|
480
|
+
}
|
|
481
|
+
systemctl --user is-active --quiet "$SERVICE_NAME" || {
|
|
482
|
+
echo "error: $SERVICE_NAME is not active" >&2
|
|
483
|
+
return 1
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if [[ "${ZL_CODE_NPM_SERVICE_SKIP_PROCESS_CHECK:-false}" != true ]]; then
|
|
487
|
+
main_pid="$(systemctl --user show "$SERVICE_NAME" -p MainPID --value)"
|
|
488
|
+
[[ "$main_pid" =~ ^[1-9][0-9]*$ ]] || {
|
|
489
|
+
echo "error: service has no live MainPID" >&2
|
|
490
|
+
return 1
|
|
491
|
+
}
|
|
492
|
+
command_line="$(tr '\0' ' ' <"/proc/${main_pid}/cmdline")"
|
|
493
|
+
[[ "$command_line" == *"$NODE_BIN"* ]] || {
|
|
494
|
+
echo "error: service is not using pinned Node: $command_line" >&2
|
|
495
|
+
return 1
|
|
496
|
+
}
|
|
497
|
+
[[ "$command_line" == *"${RUNTIME_ROOT}/current/node_modules/zl-code/dist/bin.mjs"* ]] ||
|
|
498
|
+
{
|
|
499
|
+
echo "error: service is not using the current npm runtime: $command_line" >&2
|
|
500
|
+
return 1
|
|
501
|
+
}
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
wait_for_url "$LOCAL_URL" || {
|
|
505
|
+
echo "error: local endpoint did not become healthy: $LOCAL_URL" >&2
|
|
506
|
+
return 1
|
|
507
|
+
}
|
|
508
|
+
wait_for_url "$PUBLIC_URL" || {
|
|
509
|
+
echo "error: public endpoint did not become healthy: $PUBLIC_URL" >&2
|
|
510
|
+
return 1
|
|
511
|
+
}
|
|
512
|
+
echo "version=zl-code v${version}"
|
|
513
|
+
echo "current=$current_target"
|
|
514
|
+
echo "service=active"
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
activate_release() {
|
|
518
|
+
local version=$1
|
|
519
|
+
local backup_dir=$2
|
|
520
|
+
local confirmed=$3
|
|
521
|
+
local directory
|
|
522
|
+
local restart_id
|
|
523
|
+
local restart_requested_at
|
|
524
|
+
local current_release_identity
|
|
525
|
+
local current_version
|
|
526
|
+
local target_release_identity
|
|
527
|
+
local activation_status
|
|
528
|
+
[[ "$confirmed" == true ]] || fail "activation requires --confirm-cutover"
|
|
529
|
+
validate_version "$version"
|
|
530
|
+
[[ -n "$backup_dir" ]] || fail "activation requires --backup-dir"
|
|
531
|
+
require_file "$backup_dir/manifest"
|
|
532
|
+
require_file "$backup_dir/state.sqlite"
|
|
533
|
+
require_file "$backup_dir/runtime-boundary.tar"
|
|
534
|
+
grep -Fxq "quick_check=ok" "$backup_dir/manifest" || fail "backup was not quick_check verified"
|
|
535
|
+
acquire_lock
|
|
536
|
+
require_command curl
|
|
537
|
+
require_command install
|
|
538
|
+
require_command systemctl
|
|
539
|
+
require_executable "$NODE_BIN"
|
|
540
|
+
verify_release "$version"
|
|
541
|
+
sqlite_quick_check "$backup_dir/state.sqlite"
|
|
542
|
+
directory="$(release_dir "$version")"
|
|
543
|
+
[[ "$(readlink -f "$RUNTIME_ROOT/candidate")" == "$directory" ]] ||
|
|
544
|
+
fail "candidate pointer does not select $directory"
|
|
545
|
+
current_release_identity="$(read_current_release_identity)"
|
|
546
|
+
current_version="${current_release_identity#release:}"
|
|
547
|
+
SUPERVISED_ROLLBACK_VERSION="$current_version"
|
|
548
|
+
target_release_identity="release:${version}"
|
|
549
|
+
|
|
550
|
+
save_git_rollback_unit
|
|
551
|
+
capture_current_pointer
|
|
552
|
+
capture_activation_boundary
|
|
553
|
+
if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
|
|
554
|
+
atomic_symlink "$(readlink -f "$RUNTIME_ROOT/current")" "$RUNTIME_ROOT/previous"
|
|
555
|
+
fi
|
|
556
|
+
|
|
557
|
+
restart_requested_at="$(date --iso-8601=seconds)"
|
|
558
|
+
if [[ -n "${ZL_CODE_UPDATE_OPERATION_ID:-}" ]]; then
|
|
559
|
+
validate_operation_id "$ZL_CODE_UPDATE_OPERATION_ID"
|
|
560
|
+
restart_id="$ZL_CODE_UPDATE_OPERATION_ID"
|
|
561
|
+
else
|
|
562
|
+
restart_id="$(printf '%s-%s' "$(date -u +%Y%m%dT%H%M%SZ)" "$version")"
|
|
563
|
+
fi
|
|
564
|
+
write_restart_state \
|
|
565
|
+
"$restart_id" \
|
|
566
|
+
"${SCRIPT_PATH} activate" \
|
|
567
|
+
"$current_release_identity" \
|
|
568
|
+
"$target_release_identity" \
|
|
569
|
+
"$restart_requested_at"
|
|
570
|
+
echo "restart_id=$restart_id"
|
|
571
|
+
echo "restart_state_file=$RESTART_STATE_FILE"
|
|
572
|
+
echo "current_version=$current_version"
|
|
573
|
+
echo "target_version=$version"
|
|
574
|
+
echo "validated_at=$restart_requested_at"
|
|
575
|
+
|
|
576
|
+
emit_supervisor_phase "draining"
|
|
577
|
+
log "Requesting graceful update drain"
|
|
578
|
+
ACTIVATION_CUTOVER_STARTED=true
|
|
579
|
+
systemctl --user kill --signal="$DRAIN_SIGNAL" "$SERVICE_NAME"
|
|
580
|
+
if ! await_restart_state_field "drainStartedAt" "$DRAIN_ACK_TIMEOUT_SECONDS"; then
|
|
581
|
+
restore_activation_boundary
|
|
582
|
+
systemctl --user daemon-reload
|
|
583
|
+
systemctl --user restart "$SERVICE_NAME"
|
|
584
|
+
systemctl --user is-active --quiet "$SERVICE_NAME"
|
|
585
|
+
wait_for_url "$LOCAL_URL" || fail "service did not recover after the drain acknowledgement timeout"
|
|
586
|
+
ACTIVATION_ROLLBACK_COMPLETED=true
|
|
587
|
+
echo "rollback_version=$current_version"
|
|
588
|
+
fail "service did not acknowledge update drain within ${DRAIN_ACK_TIMEOUT_SECONDS}s"
|
|
589
|
+
fi
|
|
590
|
+
echo "drain=acknowledged"
|
|
591
|
+
if await_restart_state_field "quiesceCompletedAt" "$DRAIN_QUIESCE_TIMEOUT_SECONDS"; then
|
|
592
|
+
echo "drain_quiesce=completed"
|
|
593
|
+
else
|
|
594
|
+
echo "drain_quiesce=timeout"
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
set +e
|
|
598
|
+
(
|
|
599
|
+
set -Eeuo pipefail
|
|
600
|
+
emit_supervisor_phase "activating"
|
|
601
|
+
log "Activating zl-code@${version}"
|
|
602
|
+
atomic_symlink "$directory" "$RUNTIME_ROOT/current"
|
|
603
|
+
install_npm_unit
|
|
604
|
+
systemctl --user daemon-reload
|
|
605
|
+
systemctl --user restart "$SERVICE_NAME"
|
|
606
|
+
emit_supervisor_phase "verifying"
|
|
607
|
+
verify_runtime "$version"
|
|
608
|
+
patch_restart_state "{\"healthyAt\":\"$(date --iso-8601=seconds)\"}"
|
|
609
|
+
)
|
|
610
|
+
activation_status=$?
|
|
611
|
+
set -e
|
|
612
|
+
|
|
613
|
+
if [[ "$activation_status" -ne 0 ]]; then
|
|
614
|
+
emit_supervisor_phase "rolling_back"
|
|
615
|
+
echo "error: npm activation failed; restoring the immediately preceding service runtime" >&2
|
|
616
|
+
restore_activation_boundary
|
|
617
|
+
systemctl --user daemon-reload
|
|
618
|
+
systemctl --user restart "$SERVICE_NAME"
|
|
619
|
+
systemctl --user is-active --quiet "$SERVICE_NAME"
|
|
620
|
+
wait_for_url "$LOCAL_URL" || fail "activation failed and the preceding service runtime did not recover"
|
|
621
|
+
ACTIVATION_ROLLBACK_COMPLETED=true
|
|
622
|
+
echo "rollback_version=$current_version"
|
|
623
|
+
fail "activation failed and the preceding service runtime was restored"
|
|
624
|
+
fi
|
|
625
|
+
echo "backup_dir=$backup_dir"
|
|
626
|
+
echo "rollback=git-unit-retained"
|
|
627
|
+
if [[ -f "$RESTART_STATE_FILE" ]]; then
|
|
628
|
+
echo "restart_state=$(cat "$RESTART_STATE_FILE")"
|
|
629
|
+
fi
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
supervised_update() {
|
|
633
|
+
local version=$1
|
|
634
|
+
local confirmed=$2
|
|
635
|
+
local backup_dir
|
|
636
|
+
local backup_output
|
|
637
|
+
[[ "$confirmed" == true ]] || fail "supervised update requires --confirm-supervised"
|
|
638
|
+
SUPERVISED_MODE=true
|
|
639
|
+
trap supervised_exit_cleanup EXIT
|
|
640
|
+
trap 'exit 124' TERM INT
|
|
641
|
+
acquire_lock
|
|
642
|
+
|
|
643
|
+
emit_supervisor_phase "preparing"
|
|
644
|
+
prepare_release "$version" true
|
|
645
|
+
emit_supervisor_phase "backing_up"
|
|
646
|
+
backup_output="$(create_backup)"
|
|
647
|
+
printf '%s\n' "$backup_output" >&2
|
|
648
|
+
backup_dir="$(printf '%s\n' "$backup_output" | sed -n 's/^backup_dir=//p' | tail -n 1)"
|
|
649
|
+
[[ -n "$backup_dir" ]] || fail "supervised update did not produce a backup path"
|
|
650
|
+
emit_supervisor_phase "draining"
|
|
651
|
+
activate_release "$version" "$backup_dir" true
|
|
652
|
+
emit_supervisor_phase "succeeded"
|
|
653
|
+
trap - EXIT TERM INT
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
install_update_supervisor() {
|
|
657
|
+
local confirmed=$1
|
|
658
|
+
local temporary="${SUPERVISOR_UNIT_PATH}.tmp.$$"
|
|
659
|
+
[[ "$confirmed" == true ]] || fail "supervisor installation requires --confirm-install"
|
|
660
|
+
acquire_lock
|
|
661
|
+
require_command install
|
|
662
|
+
require_command systemctl
|
|
663
|
+
require_file "$SUPERVISOR_UNIT_TEMPLATE"
|
|
664
|
+
require_file "$RUNTIME_ROOT/current/node_modules/zl-code/dist/bin.mjs"
|
|
665
|
+
|
|
666
|
+
mkdir -p "$(dirname "$SUPERVISOR_UNIT_PATH")"
|
|
667
|
+
install -m 600 "$SUPERVISOR_UNIT_TEMPLATE" "$temporary"
|
|
668
|
+
mv -Tf "$temporary" "$SUPERVISOR_UNIT_PATH"
|
|
669
|
+
systemctl --user daemon-reload
|
|
670
|
+
systemctl --user enable --now "$SUPERVISOR_SERVICE_NAME"
|
|
671
|
+
systemctl --user is-active --quiet "$SUPERVISOR_SERVICE_NAME"
|
|
672
|
+
echo "supervisor_unit=$SUPERVISOR_UNIT_PATH"
|
|
673
|
+
echo "supervisor_service=active"
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
rollback_runtime() {
|
|
677
|
+
local target=$1
|
|
678
|
+
local confirmed=$2
|
|
679
|
+
local previous_target
|
|
680
|
+
[[ "$confirmed" == true ]] || fail "rollback requires --confirm-cutover"
|
|
681
|
+
[[ "$target" == previous || "$target" == git ]] || fail "rollback target must be previous or git"
|
|
682
|
+
acquire_lock
|
|
683
|
+
require_command curl
|
|
684
|
+
require_command systemctl
|
|
685
|
+
|
|
686
|
+
if [[ "$target" == git ]]; then
|
|
687
|
+
log "Restoring the retained git-backed service unit"
|
|
688
|
+
restore_git_unit_and_pointer
|
|
689
|
+
systemctl --user daemon-reload
|
|
690
|
+
systemctl --user restart "$SERVICE_NAME"
|
|
691
|
+
systemctl --user is-active --quiet "$SERVICE_NAME"
|
|
692
|
+
wait_for_url "$LOCAL_URL" || fail "git-backed service did not become healthy"
|
|
693
|
+
echo "rollback=git"
|
|
694
|
+
return
|
|
695
|
+
fi
|
|
696
|
+
|
|
697
|
+
[[ -e "$RUNTIME_ROOT/previous" || -L "$RUNTIME_ROOT/previous" ]] ||
|
|
698
|
+
fail "previous npm pointer is absent"
|
|
699
|
+
previous_target="$(readlink -f "$RUNTIME_ROOT/previous")"
|
|
700
|
+
[[ -d "$previous_target" ]] || fail "previous npm release is missing: $previous_target"
|
|
701
|
+
atomic_symlink "$previous_target" "$RUNTIME_ROOT/current"
|
|
702
|
+
systemctl --user restart "$SERVICE_NAME"
|
|
703
|
+
systemctl --user is-active --quiet "$SERVICE_NAME"
|
|
704
|
+
wait_for_url "$LOCAL_URL" || fail "previous npm release did not become healthy"
|
|
705
|
+
echo "rollback=previous"
|
|
706
|
+
echo "current=$previous_target"
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
show_status() {
|
|
710
|
+
echo "service=$SERVICE_NAME"
|
|
711
|
+
systemctl --user show "$SERVICE_NAME" -p ActiveState -p SubState -p MainPID -p FragmentPath -p WorkingDirectory --no-pager
|
|
712
|
+
for pointer in candidate current previous; do
|
|
713
|
+
if [[ -e "$RUNTIME_ROOT/$pointer" || -L "$RUNTIME_ROOT/$pointer" ]]; then
|
|
714
|
+
echo "$pointer=$(readlink -f "$RUNTIME_ROOT/$pointer")"
|
|
715
|
+
else
|
|
716
|
+
echo "$pointer=absent"
|
|
717
|
+
fi
|
|
718
|
+
done
|
|
719
|
+
echo "unit=$(readlink -f "$UNIT_PATH")"
|
|
720
|
+
echo "node=$($NODE_BIN --version)"
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
command_name=${1:-}
|
|
724
|
+
shift || true
|
|
725
|
+
case "$command_name" in
|
|
726
|
+
status)
|
|
727
|
+
[[ $# -eq 0 ]] || fail "status accepts no arguments"
|
|
728
|
+
show_status
|
|
729
|
+
;;
|
|
730
|
+
prepare)
|
|
731
|
+
version=${1:-}
|
|
732
|
+
shift || true
|
|
733
|
+
no_cutover=false
|
|
734
|
+
[[ ${1:-} == --no-cutover ]] && no_cutover=true && shift
|
|
735
|
+
[[ $# -eq 0 ]] || fail "unexpected prepare arguments: $*"
|
|
736
|
+
prepare_release "$version" "$no_cutover"
|
|
737
|
+
;;
|
|
738
|
+
backup)
|
|
739
|
+
[[ $# -eq 0 ]] || fail "backup accepts no arguments"
|
|
740
|
+
create_backup
|
|
741
|
+
;;
|
|
742
|
+
activate)
|
|
743
|
+
version=${1:-}
|
|
744
|
+
shift || true
|
|
745
|
+
backup_dir=""
|
|
746
|
+
confirmed=false
|
|
747
|
+
while [[ $# -gt 0 ]]; do
|
|
748
|
+
case "$1" in
|
|
749
|
+
--backup-dir)
|
|
750
|
+
[[ $# -ge 2 ]] || fail "--backup-dir requires a path"
|
|
751
|
+
backup_dir=$2
|
|
752
|
+
shift 2
|
|
753
|
+
;;
|
|
754
|
+
--confirm-cutover)
|
|
755
|
+
confirmed=true
|
|
756
|
+
shift
|
|
757
|
+
;;
|
|
758
|
+
*) fail "unexpected activate argument: $1" ;;
|
|
759
|
+
esac
|
|
760
|
+
done
|
|
761
|
+
activate_release "$version" "$backup_dir" "$confirmed"
|
|
762
|
+
;;
|
|
763
|
+
verify)
|
|
764
|
+
version=${1:-}
|
|
765
|
+
shift || true
|
|
766
|
+
[[ $# -eq 0 ]] || fail "verify accepts exactly one version"
|
|
767
|
+
verify_runtime "$version"
|
|
768
|
+
;;
|
|
769
|
+
rollback)
|
|
770
|
+
target=""
|
|
771
|
+
confirmed=false
|
|
772
|
+
while [[ $# -gt 0 ]]; do
|
|
773
|
+
case "$1" in
|
|
774
|
+
--to)
|
|
775
|
+
[[ $# -ge 2 ]] || fail "--to requires previous or git"
|
|
776
|
+
target=$2
|
|
777
|
+
shift 2
|
|
778
|
+
;;
|
|
779
|
+
--confirm-cutover)
|
|
780
|
+
confirmed=true
|
|
781
|
+
shift
|
|
782
|
+
;;
|
|
783
|
+
*) fail "unexpected rollback argument: $1" ;;
|
|
784
|
+
esac
|
|
785
|
+
done
|
|
786
|
+
rollback_runtime "$target" "$confirmed"
|
|
787
|
+
;;
|
|
788
|
+
supervised-update)
|
|
789
|
+
version=${1:-}
|
|
790
|
+
confirmation=${2:-}
|
|
791
|
+
[[ $# -eq 2 ]] || fail "supervised-update requires <version> --confirm-supervised"
|
|
792
|
+
[[ "$confirmation" == "--confirm-supervised" ]] || fail "supervised update requires --confirm-supervised"
|
|
793
|
+
supervised_update "$version" true
|
|
794
|
+
;;
|
|
795
|
+
install-supervisor)
|
|
796
|
+
confirmation=${1:-}
|
|
797
|
+
[[ $# -eq 1 ]] || fail "install-supervisor requires --confirm-install"
|
|
798
|
+
[[ "$confirmation" == "--confirm-install" ]] || fail "supervisor installation requires --confirm-install"
|
|
799
|
+
install_update_supervisor true
|
|
800
|
+
;;
|
|
801
|
+
help | --help | -h)
|
|
802
|
+
usage
|
|
803
|
+
;;
|
|
804
|
+
*)
|
|
805
|
+
usage >&2
|
|
806
|
+
exit 2
|
|
807
|
+
;;
|
|
808
|
+
esac
|