@mriekki/lean 0.1.0 → 0.2.0

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.
Files changed (41) hide show
  1. package/README.md +71 -67
  2. package/assets/components.json +33 -33
  3. package/assets/core/distribution.json +52 -51
  4. package/assets/deployment/distribution.json +34 -34
  5. package/assets/generated/ASSEMBLY.json +38 -33
  6. package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
  7. package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
  8. package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
  9. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
  10. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
  11. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
  12. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
  13. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
  14. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
  15. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
  16. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
  17. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
  18. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
  19. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
  20. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
  21. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
  22. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
  23. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
  24. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
  25. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
  26. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
  27. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
  28. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
  29. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
  30. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
  31. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
  32. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
  33. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
  34. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
  35. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
  36. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
  37. package/bin/lean.mjs +9 -9
  38. package/package.json +33 -33
  39. package/src/cli.mjs +131 -131
  40. package/src/engine.mjs +243 -243
  41. package/src/package-data.mjs +18 -18
@@ -1,416 +1,416 @@
1
- """Remote Bash template for generic application runtime deployment."""
2
-
3
- REMOTE_SCRIPT = r'''#!/usr/bin/env bash
4
- set -Eeuo pipefail
5
-
6
- ACTION="$1"
7
- EXPECTED_SPEC_SHA="${2:-}"
8
- DEPLOY_USER="${3:-}"
9
- APP_ROOT="__APP_ROOT__"
10
- APP_SCHEMA_VERSION="__APP_SCHEMA_VERSION__"
11
- NETWORK_NAME="__NETWORK_NAME__"
12
- CADDY_CONTAINER="__CADDY_CONTAINER__"
13
- DESIRED_PRESENT="__DESIRED_PRESENT__"
14
-
15
- D_OWNER_B64="__D_OWNER_B64__"
16
- D_ENV_B64="__D_ENV_B64__"
17
- D_APP_B64="__D_APP_B64__"
18
- D_CONTAINER_B64="__D_CONTAINER_B64__"
19
- D_IMAGE_B64="__D_IMAGE_B64__"
20
- D_PORT="__D_PORT__"
21
- D_HEALTH_B64="__D_HEALTH_B64__"
22
- D_RUN_AS_B64="__D_RUN_AS_B64__"
23
- D_PERSISTENCE="__D_PERSISTENCE__"
24
- D_DATA_TARGET_B64="__D_DATA_TARGET_B64__"
25
- D_SPEC_SHA="__D_SPEC_SHA__"
26
- D_ENV_KEYS=(__D_ENV_KEYS__)
27
- D_ENV_VALUES=(__D_ENV_VALUES__)
28
-
29
- b64d() { printf '%s' "$1" | base64 --decode; }
30
- meta_value() {
31
- local path="$1" key="$2"
32
- [[ -f "$path" ]] || return 1
33
- sed -n "s/^${key}=//p" "$path" | head -n1
34
- }
35
- valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
36
- valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
37
- valid_container() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; }
38
- valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
39
- valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( 1 <= 10#$1 && 10#$1 <= 65535 )); }
40
- valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
41
- valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
42
-
43
- platform_ready() {
44
- [[ -f /srv/lean-poc-platform/platform.env ]] &&
45
- docker network inspect "$NETWORK_NAME" >/dev/null 2>&1 &&
46
- [[ "$(docker inspect --format '{{.State.Status}}' "$CADDY_CONTAINER" 2>/dev/null || true)" == "running" ]]
47
- }
48
-
49
- network_has_container() {
50
- local container="$1"
51
- docker network inspect "$NETWORK_NAME" --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' 2>/dev/null |
52
- grep -Fxq "$container"
53
- }
54
-
55
- probe_http() {
56
- local container="$1" port="$2" path="$3"
57
- docker exec "$CADDY_CONTAINER" \
58
- wget -q -T 5 -O /dev/null "http://${container}:${port}${path}" >/dev/null 2>&1
59
- }
60
-
61
- wait_ready() {
62
- local container="$1" port="$2" path="$3"
63
- local deadline=$((SECONDS + 120)) state=""
64
- while (( SECONDS < deadline )); do
65
- state="$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)"
66
- if [[ "$state" == "running" ]] && network_has_container "$container" && probe_http "$container" "$port" "$path"; then
67
- return 0
68
- fi
69
- if [[ "$state" == "exited" || "$state" == "dead" ]]; then
70
- return 1
71
- fi
72
- sleep 2
73
- done
74
- return 1
75
- }
76
-
77
- inventory() {
78
- local count=0 state current previous release_file
79
- local owner env app container image spec_sha port health persistence
80
- local running connected healthy
81
- if [[ -d "$APP_ROOT" ]]; then
82
- while IFS= read -r state; do
83
- [[ -n "$state" ]] || continue
84
- owner="$(meta_value "$state" OWNER_REPO || true)"
85
- env="$(meta_value "$state" ENVIRONMENT || true)"
86
- app="$(meta_value "$state" APP_SLUG || true)"
87
- current="$(meta_value "$state" CURRENT_RELEASE || true)"
88
- previous="$(meta_value "$state" PREVIOUS_RELEASE || true)"
89
- spec_sha="$(meta_value "$state" SPEC_SHA256 || true)"
90
- [[ -n "$owner" && -n "$env" && -n "$app" && -n "$current" && -n "$spec_sha" ]] || {
91
- echo "Managed application state is malformed: $state" >&2
92
- exit 31
93
- }
94
- release_file="$(dirname "$state")/releases/$current/release.env"
95
- [[ -f "$release_file" && ! -L "$release_file" ]] || {
96
- echo "Current application release metadata is missing: $release_file" >&2
97
- exit 31
98
- }
99
- container="$(meta_value "$release_file" CONTAINER_NAME || true)"
100
- image="$(meta_value "$release_file" IMAGE || true)"
101
- port="$(meta_value "$release_file" RUNTIME_PORT || true)"
102
- health="$(b64d "$(meta_value "$release_file" HEALTH_PATH_B64 || true)")"
103
- persistence="$(meta_value "$release_file" PERSISTENCE_MODE || true)"
104
- [[ "$(meta_value "$release_file" SPEC_SHA256 || true)" == "$spec_sha" ]] || {
105
- echo "Application state/release fingerprint mismatch: $state" >&2
106
- exit 31
107
- }
108
- running=false
109
- connected=false
110
- healthy=false
111
- [[ "$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)" == "running" ]] && running=true || true
112
- network_has_container "$container" && connected=true || true
113
- if [[ "$running" == true && "$connected" == true ]]; then
114
- probe_http "$container" "$port" "$health" && healthy=true || true
115
- fi
116
- printf 'APP\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
117
- "$owner" "$env" "$app" "$container" "$image" "$spec_sha" "$port" "$health" "$persistence" \
118
- "$current" "$previous" "$running" "$connected" "$healthy"
119
- count=$((count + 1))
120
- done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name deployment.env -print 2>/dev/null | sort)
121
- fi
122
- printf 'SUMMARY\tappCount\t%s\n' "$count"
123
- }
124
-
125
- configure_registry_auth() {
126
- [[ -n "$DEPLOY_USER" ]] || { echo "Deployment account is required." >&2; exit 32; }
127
- [[ "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
128
- echo "Deployment account contains unsupported characters." >&2
129
- exit 32
130
- }
131
- local home config dir_mode file_mode deploy_uid
132
- home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
133
- [[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 32; }
134
- config="$home/.docker"
135
- if [[ ! -e "$config/config.json" ]]; then
136
- return 0
137
- fi
138
- [[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
139
- echo "Docker credential path is not a regular protected directory/file." >&2
140
- exit 32
141
- }
142
- dir_mode="$(stat -c '%a' "$config")"
143
- file_mode="$(stat -c '%a' "$config/config.json")"
144
- deploy_uid="$(id -u "$DEPLOY_USER")"
145
- if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
146
- [[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
147
- echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
148
- exit 32
149
- fi
150
- export DOCKER_CONFIG="$config"
151
- }
152
-
153
- decode_desired() {
154
- [[ "$DESIRED_PRESENT" == true ]] || { echo "Deploy action omitted desired runtime spec." >&2; exit 33; }
155
- D_OWNER="$(b64d "$D_OWNER_B64")"
156
- D_ENV="$(b64d "$D_ENV_B64")"
157
- D_APP="$(b64d "$D_APP_B64")"
158
- D_CONTAINER="$(b64d "$D_CONTAINER_B64")"
159
- D_IMAGE="$(b64d "$D_IMAGE_B64")"
160
- D_HEALTH="$(b64d "$D_HEALTH_B64")"
161
- D_RUN_AS="$(b64d "$D_RUN_AS_B64")"
162
- D_DATA_TARGET="$(b64d "$D_DATA_TARGET_B64")"
163
-
164
- valid_repo "$D_OWNER" || { echo "Invalid owner repository." >&2; exit 33; }
165
- valid_slug "$D_ENV" || { echo "Invalid environment slug." >&2; exit 33; }
166
- valid_slug "$D_APP" || { echo "Invalid application slug." >&2; exit 33; }
167
- valid_container "$D_CONTAINER" || { echo "Invalid container name." >&2; exit 33; }
168
- valid_image "$D_IMAGE" || { echo "Image is not an immutable sha256 digest reference." >&2; exit 33; }
169
- valid_port "$D_PORT" || { echo "Invalid runtime port." >&2; exit 33; }
170
- [[ "$D_HEALTH" == /* && "$D_HEALTH" != *$'\n'* && "$D_HEALTH" != *$'\r'* ]] || {
171
- echo "Invalid health path." >&2
172
- exit 33
173
- }
174
- valid_run_as "$D_RUN_AS" || { echo "Invalid numeric runAs value." >&2; exit 33; }
175
- [[ "$D_PERSISTENCE" == "none" || "$D_PERSISTENCE" == "directory" ]] || {
176
- echo "Invalid persistence mode." >&2
177
- exit 33
178
- }
179
- if [[ "$D_PERSISTENCE" == "directory" ]]; then
180
- [[ "$D_DATA_TARGET" == /* && "$D_DATA_TARGET" != *"/../"* && "$D_DATA_TARGET" != *$'\n'* ]] || {
181
- echo "Invalid persistent data container path." >&2
182
- exit 33
183
- }
184
- elif [[ -n "$D_DATA_TARGET" ]]; then
185
- echo "Non-persistent runtime unexpectedly supplied a data path." >&2
186
- exit 33
187
- fi
188
- [[ "$D_SPEC_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid runtime spec fingerprint." >&2; exit 33; }
189
- [[ "${#D_ENV_KEYS[@]}" -eq "${#D_ENV_VALUES[@]}" ]] || { echo "Environment payload mismatch." >&2; exit 33; }
190
-
191
- local index key
192
- for index in "${!D_ENV_KEYS[@]}"; do
193
- key="$(b64d "${D_ENV_KEYS[$index]}")"
194
- valid_env_key "$key" || { echo "Invalid environment variable name." >&2; exit 33; }
195
- done
196
- }
197
-
198
- release_value() {
199
- meta_value "$1" "$2"
200
- }
201
-
202
- write_release_metadata() {
203
- local release_file="$1"
204
- local tmp="$release_file.tmp.$$" index key_b64 value_b64
205
- {
206
- printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
207
- printf 'OWNER_REPO=%s\n' "$D_OWNER"
208
- printf 'ENVIRONMENT=%s\n' "$D_ENV"
209
- printf 'APP_SLUG=%s\n' "$D_APP"
210
- printf 'CONTAINER_NAME=%s\n' "$D_CONTAINER"
211
- printf 'IMAGE=%s\n' "$D_IMAGE"
212
- printf 'RUNTIME_PORT=%s\n' "$D_PORT"
213
- printf 'HEALTH_PATH_B64=%s\n' "$D_HEALTH_B64"
214
- printf 'RUN_AS=%s\n' "$D_RUN_AS"
215
- printf 'PERSISTENCE_MODE=%s\n' "$D_PERSISTENCE"
216
- printf 'DATA_TARGET_B64=%s\n' "$D_DATA_TARGET_B64"
217
- printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
218
- printf 'ENV_COUNT=%s\n' "${#D_ENV_KEYS[@]}"
219
- for index in "${!D_ENV_KEYS[@]}"; do
220
- key_b64="${D_ENV_KEYS[$index]}"
221
- value_b64="${D_ENV_VALUES[$index]}"
222
- printf 'ENV_KEY_B64_%s=%s\n' "$index" "$key_b64"
223
- printf 'ENV_VALUE_B64_%s=%s\n' "$index" "$value_b64"
224
- done
225
- } > "$tmp"
226
- chmod 0600 "$tmp"
227
- mv -f "$tmp" "$release_file"
228
- }
229
-
230
- start_desired_container() {
231
- local root data
232
- local uid gid index key value
233
- root="$1"
234
- data="$root/data"
235
- uid="${D_RUN_AS%%:*}"
236
- if [[ "$D_RUN_AS" == *:* ]]; then gid="${D_RUN_AS#*:}"; else gid="$uid"; fi
237
-
238
- if [[ "$D_PERSISTENCE" == "directory" ]]; then
239
- if [[ -L "$data" ]]; then
240
- echo "Persistent data directory must not be a symbolic link." >&2
241
- return 1
242
- fi
243
- if [[ ! -e "$data" ]]; then
244
- install -d -m 0750 -o "$uid" -g "$gid" "$data"
245
- elif [[ ! -d "$data" ]]; then
246
- echo "Persistent data path is not a directory." >&2
247
- return 1
248
- elif [[ "$(stat -c '%u' "$data")" != "$uid" || "$(stat -c '%g' "$data")" != "$gid" ]]; then
249
- echo "Persistent data directory ownership does not match runAs; use the persistence adapter/import path." >&2
250
- return 1
251
- fi
252
- fi
253
-
254
- local -a docker_args
255
- docker_args=(
256
- docker run -d
257
- --name "$D_CONTAINER"
258
- --restart unless-stopped
259
- --network "$NETWORK_NAME"
260
- --user "$D_RUN_AS"
261
- --log-driver json-file
262
- --log-opt max-size=10m
263
- --log-opt max-file=3
264
- )
265
- if [[ "$D_PERSISTENCE" == "directory" ]]; then
266
- docker_args+=(--mount "type=bind,src=$data,dst=$D_DATA_TARGET")
267
- fi
268
- for index in "${!D_ENV_KEYS[@]}"; do
269
- key="$(b64d "${D_ENV_KEYS[$index]}")"
270
- value="$(b64d "${D_ENV_VALUES[$index]}")"
271
- docker_args+=(--env "$key=$value")
272
- done
273
- docker_args+=("$D_IMAGE")
274
- "${docker_args[@]}" >/dev/null
275
- }
276
-
277
- record_evidence() {
278
- local root="$1" outcome="$2" release_id="$3" previous="$4"
279
- local timestamp
280
- timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
281
- printf '{"timestamp":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","container":"%s","image":"%s","specSha256":"%s","release":"%s","previousRelease":"%s","persistence":"%s","outcome":"%s"}\n' \
282
- "$timestamp" "$D_OWNER" "$D_ENV" "$D_APP" "$D_CONTAINER" "$D_IMAGE" "$D_SPEC_SHA" \
283
- "$release_id" "$previous" "$D_PERSISTENCE" "$outcome" >> "$root/deployment-evidence.jsonl"
284
- chmod 0600 "$root/deployment-evidence.jsonl"
285
- }
286
-
287
- deploy() {
288
- platform_ready || { echo "Kit shared platform is not ready." >&2; exit 34; }
289
- decode_desired
290
- configure_registry_auth
291
-
292
- local root releases state current_release="" previous_release="" current_spec="" current_container="" next_previous=""
293
- local digest release_id release_dir release_file lock_file tmp
294
- root="$APP_ROOT/$D_APP/$D_ENV"
295
- releases="$root/releases"
296
- state="$root/deployment.env"
297
- lock_file="/run/lock/lean-poc-app-$D_APP--$D_ENV.lock"
298
- digest="${D_IMAGE##*@sha256:}"
299
- release_id="sha256-${digest}-${D_SPEC_SHA:0:16}"
300
- release_dir="$releases/$release_id"
301
- release_file="$release_dir/release.env"
302
-
303
- exec 9>>"$lock_file"
304
- if ! flock -n 9; then
305
- echo "Another application deployment holds the lock for $D_APP--$D_ENV." >&2
306
- exit 75
307
- fi
308
-
309
- for path in "$root" "$releases" "$state" "$root/data" "$root/deployment-evidence.jsonl"; do
310
- [[ ! -L "$path" ]] || { echo "Managed application path must not be a symbolic link: $path" >&2; exit 35; }
311
- done
312
-
313
- if [[ -f "$state" ]]; then
314
- [[ "$(meta_value "$state" OWNER_REPO || true)" == "$D_OWNER" &&
315
- "$(meta_value "$state" ENVIRONMENT || true)" == "$D_ENV" &&
316
- "$(meta_value "$state" APP_SLUG || true)" == "$D_APP" ]] || {
317
- echo "Existing application state has different ownership." >&2
318
- exit 35
319
- }
320
- current_release="$(meta_value "$state" CURRENT_RELEASE || true)"
321
- previous_release="$(meta_value "$state" PREVIOUS_RELEASE || true)"
322
- current_spec="$(meta_value "$state" SPEC_SHA256 || true)"
323
- [[ -n "$current_release" && "$current_spec" =~ ^[0-9a-f]{64}$ ]] || {
324
- echo "Existing application state is malformed." >&2
325
- exit 35
326
- }
327
- [[ "$EXPECTED_SPEC_SHA" != "NONE" && "$current_spec" == "$EXPECTED_SPEC_SHA" ]] || {
328
- echo "Application state changed after planning; create a fresh plan." >&2
329
- exit 36
330
- }
331
- local current_file="$releases/$current_release/release.env"
332
- [[ -f "$current_file" && ! -L "$current_file" ]] || {
333
- echo "Current release metadata is missing." >&2
334
- exit 35
335
- }
336
- current_container="$(release_value "$current_file" CONTAINER_NAME || true)"
337
- valid_container "$current_container" || { echo "Current release container metadata is malformed." >&2; exit 35; }
338
- if [[ "$current_spec" == "$D_SPEC_SHA" ]]; then
339
- next_previous="$previous_release"
340
- else
341
- next_previous="$current_release"
342
- fi
343
- else
344
- [[ "$EXPECTED_SPEC_SHA" == "NONE" ]] || {
345
- echo "Application state disappeared after planning; create a fresh plan." >&2
346
- exit 36
347
- }
348
- fi
349
-
350
- if docker inspect "$D_CONTAINER" >/dev/null 2>&1 && [[ "$D_CONTAINER" != "$current_container" ]]; then
351
- echo "Desired container name is already occupied outside this managed runtime." >&2
352
- exit 37
353
- fi
354
-
355
- install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$D_APP" "$root" "$releases"
356
- if [[ -e "$release_dir" && ! -d "$release_dir" ]]; then
357
- echo "Release path exists and is not a directory." >&2
358
- exit 35
359
- fi
360
- install -d -m 0750 "$release_dir"
361
- if [[ -f "$release_file" ]]; then
362
- [[ "$(release_value "$release_file" SPEC_SHA256 || true)" == "$D_SPEC_SHA" ]] || {
363
- echo "Release directory collision with different runtime metadata." >&2
364
- exit 35
365
- }
366
- else
367
- write_release_metadata "$release_file"
368
- fi
369
-
370
- docker pull "$D_IMAGE" >/dev/null
371
-
372
- if [[ -n "$current_container" ]]; then
373
- docker rm -f "$current_container" >/dev/null
374
- fi
375
-
376
- if ! start_desired_container "$root"; then
377
- docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
378
- record_evidence "$root" "failed-start" "$release_id" "$current_release"
379
- echo "Application container failed to start; previous release metadata was retained but runtime rollback was not attempted." >&2
380
- exit 38
381
- fi
382
- if ! wait_ready "$D_CONTAINER" "$D_PORT" "$D_HEALTH"; then
383
- docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
384
- record_evidence "$root" "failed-readiness" "$release_id" "$current_release"
385
- echo "Application failed readiness; previous release metadata was retained but runtime rollback was not attempted." >&2
386
- exit 39
387
- fi
388
-
389
- tmp="$state.tmp.$$"
390
- {
391
- printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
392
- printf 'OWNER_REPO=%s\n' "$D_OWNER"
393
- printf 'ENVIRONMENT=%s\n' "$D_ENV"
394
- printf 'APP_SLUG=%s\n' "$D_APP"
395
- printf 'CURRENT_RELEASE=%s\n' "$release_id"
396
- printf 'PREVIOUS_RELEASE=%s\n' "$next_previous"
397
- printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
398
- } > "$tmp"
399
- chmod 0600 "$tmp"
400
- mv -f "$tmp" "$state"
401
- record_evidence "$root" "deployed" "$release_id" "$next_previous"
402
- }
403
-
404
- case "$ACTION" in
405
- inventory)
406
- inventory
407
- ;;
408
- deploy)
409
- deploy
410
- ;;
411
- *)
412
- echo "Unsupported application-runtime action: $ACTION" >&2
413
- exit 2
414
- ;;
415
- esac
1
+ """Remote Bash template for generic application runtime deployment."""
2
+
3
+ REMOTE_SCRIPT = r'''#!/usr/bin/env bash
4
+ set -Eeuo pipefail
5
+
6
+ ACTION="$1"
7
+ EXPECTED_SPEC_SHA="${2:-}"
8
+ DEPLOY_USER="${3:-}"
9
+ APP_ROOT="__APP_ROOT__"
10
+ APP_SCHEMA_VERSION="__APP_SCHEMA_VERSION__"
11
+ NETWORK_NAME="__NETWORK_NAME__"
12
+ CADDY_CONTAINER="__CADDY_CONTAINER__"
13
+ DESIRED_PRESENT="__DESIRED_PRESENT__"
14
+
15
+ D_OWNER_B64="__D_OWNER_B64__"
16
+ D_ENV_B64="__D_ENV_B64__"
17
+ D_APP_B64="__D_APP_B64__"
18
+ D_CONTAINER_B64="__D_CONTAINER_B64__"
19
+ D_IMAGE_B64="__D_IMAGE_B64__"
20
+ D_PORT="__D_PORT__"
21
+ D_HEALTH_B64="__D_HEALTH_B64__"
22
+ D_RUN_AS_B64="__D_RUN_AS_B64__"
23
+ D_PERSISTENCE="__D_PERSISTENCE__"
24
+ D_DATA_TARGET_B64="__D_DATA_TARGET_B64__"
25
+ D_SPEC_SHA="__D_SPEC_SHA__"
26
+ D_ENV_KEYS=(__D_ENV_KEYS__)
27
+ D_ENV_VALUES=(__D_ENV_VALUES__)
28
+
29
+ b64d() { printf '%s' "$1" | base64 --decode; }
30
+ meta_value() {
31
+ local path="$1" key="$2"
32
+ [[ -f "$path" ]] || return 1
33
+ sed -n "s/^${key}=//p" "$path" | head -n1
34
+ }
35
+ valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
36
+ valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
37
+ valid_container() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; }
38
+ valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
39
+ valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( 1 <= 10#$1 && 10#$1 <= 65535 )); }
40
+ valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
41
+ valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
42
+
43
+ platform_ready() {
44
+ [[ -f /srv/lean-poc-platform/platform.env ]] &&
45
+ docker network inspect "$NETWORK_NAME" >/dev/null 2>&1 &&
46
+ [[ "$(docker inspect --format '{{.State.Status}}' "$CADDY_CONTAINER" 2>/dev/null || true)" == "running" ]]
47
+ }
48
+
49
+ network_has_container() {
50
+ local container="$1"
51
+ docker network inspect "$NETWORK_NAME" --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' 2>/dev/null |
52
+ grep -Fxq "$container"
53
+ }
54
+
55
+ probe_http() {
56
+ local container="$1" port="$2" path="$3"
57
+ docker exec "$CADDY_CONTAINER" \
58
+ wget -q -T 5 -O /dev/null "http://${container}:${port}${path}" >/dev/null 2>&1
59
+ }
60
+
61
+ wait_ready() {
62
+ local container="$1" port="$2" path="$3"
63
+ local deadline=$((SECONDS + 120)) state=""
64
+ while (( SECONDS < deadline )); do
65
+ state="$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)"
66
+ if [[ "$state" == "running" ]] && network_has_container "$container" && probe_http "$container" "$port" "$path"; then
67
+ return 0
68
+ fi
69
+ if [[ "$state" == "exited" || "$state" == "dead" ]]; then
70
+ return 1
71
+ fi
72
+ sleep 2
73
+ done
74
+ return 1
75
+ }
76
+
77
+ inventory() {
78
+ local count=0 state current previous release_file
79
+ local owner env app container image spec_sha port health persistence
80
+ local running connected healthy
81
+ if [[ -d "$APP_ROOT" ]]; then
82
+ while IFS= read -r state; do
83
+ [[ -n "$state" ]] || continue
84
+ owner="$(meta_value "$state" OWNER_REPO || true)"
85
+ env="$(meta_value "$state" ENVIRONMENT || true)"
86
+ app="$(meta_value "$state" APP_SLUG || true)"
87
+ current="$(meta_value "$state" CURRENT_RELEASE || true)"
88
+ previous="$(meta_value "$state" PREVIOUS_RELEASE || true)"
89
+ spec_sha="$(meta_value "$state" SPEC_SHA256 || true)"
90
+ [[ -n "$owner" && -n "$env" && -n "$app" && -n "$current" && -n "$spec_sha" ]] || {
91
+ echo "Managed application state is malformed: $state" >&2
92
+ exit 31
93
+ }
94
+ release_file="$(dirname "$state")/releases/$current/release.env"
95
+ [[ -f "$release_file" && ! -L "$release_file" ]] || {
96
+ echo "Current application release metadata is missing: $release_file" >&2
97
+ exit 31
98
+ }
99
+ container="$(meta_value "$release_file" CONTAINER_NAME || true)"
100
+ image="$(meta_value "$release_file" IMAGE || true)"
101
+ port="$(meta_value "$release_file" RUNTIME_PORT || true)"
102
+ health="$(b64d "$(meta_value "$release_file" HEALTH_PATH_B64 || true)")"
103
+ persistence="$(meta_value "$release_file" PERSISTENCE_MODE || true)"
104
+ [[ "$(meta_value "$release_file" SPEC_SHA256 || true)" == "$spec_sha" ]] || {
105
+ echo "Application state/release fingerprint mismatch: $state" >&2
106
+ exit 31
107
+ }
108
+ running=false
109
+ connected=false
110
+ healthy=false
111
+ [[ "$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || true)" == "running" ]] && running=true || true
112
+ network_has_container "$container" && connected=true || true
113
+ if [[ "$running" == true && "$connected" == true ]]; then
114
+ probe_http "$container" "$port" "$health" && healthy=true || true
115
+ fi
116
+ printf 'APP\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
117
+ "$owner" "$env" "$app" "$container" "$image" "$spec_sha" "$port" "$health" "$persistence" \
118
+ "$current" "$previous" "$running" "$connected" "$healthy"
119
+ count=$((count + 1))
120
+ done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name deployment.env -print 2>/dev/null | sort)
121
+ fi
122
+ printf 'SUMMARY\tappCount\t%s\n' "$count"
123
+ }
124
+
125
+ configure_registry_auth() {
126
+ [[ -n "$DEPLOY_USER" ]] || { echo "Deployment account is required." >&2; exit 32; }
127
+ [[ "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
128
+ echo "Deployment account contains unsupported characters." >&2
129
+ exit 32
130
+ }
131
+ local home config dir_mode file_mode deploy_uid
132
+ home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
133
+ [[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 32; }
134
+ config="$home/.docker"
135
+ if [[ ! -e "$config/config.json" ]]; then
136
+ return 0
137
+ fi
138
+ [[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
139
+ echo "Docker credential path is not a regular protected directory/file." >&2
140
+ exit 32
141
+ }
142
+ dir_mode="$(stat -c '%a' "$config")"
143
+ file_mode="$(stat -c '%a' "$config/config.json")"
144
+ deploy_uid="$(id -u "$DEPLOY_USER")"
145
+ if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
146
+ [[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
147
+ echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
148
+ exit 32
149
+ fi
150
+ export DOCKER_CONFIG="$config"
151
+ }
152
+
153
+ decode_desired() {
154
+ [[ "$DESIRED_PRESENT" == true ]] || { echo "Deploy action omitted desired runtime spec." >&2; exit 33; }
155
+ D_OWNER="$(b64d "$D_OWNER_B64")"
156
+ D_ENV="$(b64d "$D_ENV_B64")"
157
+ D_APP="$(b64d "$D_APP_B64")"
158
+ D_CONTAINER="$(b64d "$D_CONTAINER_B64")"
159
+ D_IMAGE="$(b64d "$D_IMAGE_B64")"
160
+ D_HEALTH="$(b64d "$D_HEALTH_B64")"
161
+ D_RUN_AS="$(b64d "$D_RUN_AS_B64")"
162
+ D_DATA_TARGET="$(b64d "$D_DATA_TARGET_B64")"
163
+
164
+ valid_repo "$D_OWNER" || { echo "Invalid owner repository." >&2; exit 33; }
165
+ valid_slug "$D_ENV" || { echo "Invalid environment slug." >&2; exit 33; }
166
+ valid_slug "$D_APP" || { echo "Invalid application slug." >&2; exit 33; }
167
+ valid_container "$D_CONTAINER" || { echo "Invalid container name." >&2; exit 33; }
168
+ valid_image "$D_IMAGE" || { echo "Image is not an immutable sha256 digest reference." >&2; exit 33; }
169
+ valid_port "$D_PORT" || { echo "Invalid runtime port." >&2; exit 33; }
170
+ [[ "$D_HEALTH" == /* && "$D_HEALTH" != *$'\n'* && "$D_HEALTH" != *$'\r'* ]] || {
171
+ echo "Invalid health path." >&2
172
+ exit 33
173
+ }
174
+ valid_run_as "$D_RUN_AS" || { echo "Invalid numeric runAs value." >&2; exit 33; }
175
+ [[ "$D_PERSISTENCE" == "none" || "$D_PERSISTENCE" == "directory" ]] || {
176
+ echo "Invalid persistence mode." >&2
177
+ exit 33
178
+ }
179
+ if [[ "$D_PERSISTENCE" == "directory" ]]; then
180
+ [[ "$D_DATA_TARGET" == /* && "$D_DATA_TARGET" != *"/../"* && "$D_DATA_TARGET" != *$'\n'* ]] || {
181
+ echo "Invalid persistent data container path." >&2
182
+ exit 33
183
+ }
184
+ elif [[ -n "$D_DATA_TARGET" ]]; then
185
+ echo "Non-persistent runtime unexpectedly supplied a data path." >&2
186
+ exit 33
187
+ fi
188
+ [[ "$D_SPEC_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid runtime spec fingerprint." >&2; exit 33; }
189
+ [[ "${#D_ENV_KEYS[@]}" -eq "${#D_ENV_VALUES[@]}" ]] || { echo "Environment payload mismatch." >&2; exit 33; }
190
+
191
+ local index key
192
+ for index in "${!D_ENV_KEYS[@]}"; do
193
+ key="$(b64d "${D_ENV_KEYS[$index]}")"
194
+ valid_env_key "$key" || { echo "Invalid environment variable name." >&2; exit 33; }
195
+ done
196
+ }
197
+
198
+ release_value() {
199
+ meta_value "$1" "$2"
200
+ }
201
+
202
+ write_release_metadata() {
203
+ local release_file="$1"
204
+ local tmp="$release_file.tmp.$$" index key_b64 value_b64
205
+ {
206
+ printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
207
+ printf 'OWNER_REPO=%s\n' "$D_OWNER"
208
+ printf 'ENVIRONMENT=%s\n' "$D_ENV"
209
+ printf 'APP_SLUG=%s\n' "$D_APP"
210
+ printf 'CONTAINER_NAME=%s\n' "$D_CONTAINER"
211
+ printf 'IMAGE=%s\n' "$D_IMAGE"
212
+ printf 'RUNTIME_PORT=%s\n' "$D_PORT"
213
+ printf 'HEALTH_PATH_B64=%s\n' "$D_HEALTH_B64"
214
+ printf 'RUN_AS=%s\n' "$D_RUN_AS"
215
+ printf 'PERSISTENCE_MODE=%s\n' "$D_PERSISTENCE"
216
+ printf 'DATA_TARGET_B64=%s\n' "$D_DATA_TARGET_B64"
217
+ printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
218
+ printf 'ENV_COUNT=%s\n' "${#D_ENV_KEYS[@]}"
219
+ for index in "${!D_ENV_KEYS[@]}"; do
220
+ key_b64="${D_ENV_KEYS[$index]}"
221
+ value_b64="${D_ENV_VALUES[$index]}"
222
+ printf 'ENV_KEY_B64_%s=%s\n' "$index" "$key_b64"
223
+ printf 'ENV_VALUE_B64_%s=%s\n' "$index" "$value_b64"
224
+ done
225
+ } > "$tmp"
226
+ chmod 0600 "$tmp"
227
+ mv -f "$tmp" "$release_file"
228
+ }
229
+
230
+ start_desired_container() {
231
+ local root data
232
+ local uid gid index key value
233
+ root="$1"
234
+ data="$root/data"
235
+ uid="${D_RUN_AS%%:*}"
236
+ if [[ "$D_RUN_AS" == *:* ]]; then gid="${D_RUN_AS#*:}"; else gid="$uid"; fi
237
+
238
+ if [[ "$D_PERSISTENCE" == "directory" ]]; then
239
+ if [[ -L "$data" ]]; then
240
+ echo "Persistent data directory must not be a symbolic link." >&2
241
+ return 1
242
+ fi
243
+ if [[ ! -e "$data" ]]; then
244
+ install -d -m 0750 -o "$uid" -g "$gid" "$data"
245
+ elif [[ ! -d "$data" ]]; then
246
+ echo "Persistent data path is not a directory." >&2
247
+ return 1
248
+ elif [[ "$(stat -c '%u' "$data")" != "$uid" || "$(stat -c '%g' "$data")" != "$gid" ]]; then
249
+ echo "Persistent data directory ownership does not match runAs; use the persistence adapter/import path." >&2
250
+ return 1
251
+ fi
252
+ fi
253
+
254
+ local -a docker_args
255
+ docker_args=(
256
+ docker run -d
257
+ --name "$D_CONTAINER"
258
+ --restart unless-stopped
259
+ --network "$NETWORK_NAME"
260
+ --user "$D_RUN_AS"
261
+ --log-driver json-file
262
+ --log-opt max-size=10m
263
+ --log-opt max-file=3
264
+ )
265
+ if [[ "$D_PERSISTENCE" == "directory" ]]; then
266
+ docker_args+=(--mount "type=bind,src=$data,dst=$D_DATA_TARGET")
267
+ fi
268
+ for index in "${!D_ENV_KEYS[@]}"; do
269
+ key="$(b64d "${D_ENV_KEYS[$index]}")"
270
+ value="$(b64d "${D_ENV_VALUES[$index]}")"
271
+ docker_args+=(--env "$key=$value")
272
+ done
273
+ docker_args+=("$D_IMAGE")
274
+ "${docker_args[@]}" >/dev/null
275
+ }
276
+
277
+ record_evidence() {
278
+ local root="$1" outcome="$2" release_id="$3" previous="$4"
279
+ local timestamp
280
+ timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
281
+ printf '{"timestamp":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","container":"%s","image":"%s","specSha256":"%s","release":"%s","previousRelease":"%s","persistence":"%s","outcome":"%s"}\n' \
282
+ "$timestamp" "$D_OWNER" "$D_ENV" "$D_APP" "$D_CONTAINER" "$D_IMAGE" "$D_SPEC_SHA" \
283
+ "$release_id" "$previous" "$D_PERSISTENCE" "$outcome" >> "$root/deployment-evidence.jsonl"
284
+ chmod 0600 "$root/deployment-evidence.jsonl"
285
+ }
286
+
287
+ deploy() {
288
+ platform_ready || { echo "Kit shared platform is not ready." >&2; exit 34; }
289
+ decode_desired
290
+ configure_registry_auth
291
+
292
+ local root releases state current_release="" previous_release="" current_spec="" current_container="" next_previous=""
293
+ local digest release_id release_dir release_file lock_file tmp
294
+ root="$APP_ROOT/$D_APP/$D_ENV"
295
+ releases="$root/releases"
296
+ state="$root/deployment.env"
297
+ lock_file="/run/lock/lean-poc-app-$D_APP--$D_ENV.lock"
298
+ digest="${D_IMAGE##*@sha256:}"
299
+ release_id="sha256-${digest}-${D_SPEC_SHA:0:16}"
300
+ release_dir="$releases/$release_id"
301
+ release_file="$release_dir/release.env"
302
+
303
+ exec 9>>"$lock_file"
304
+ if ! flock -n 9; then
305
+ echo "Another application deployment holds the lock for $D_APP--$D_ENV." >&2
306
+ exit 75
307
+ fi
308
+
309
+ for path in "$root" "$releases" "$state" "$root/data" "$root/deployment-evidence.jsonl"; do
310
+ [[ ! -L "$path" ]] || { echo "Managed application path must not be a symbolic link: $path" >&2; exit 35; }
311
+ done
312
+
313
+ if [[ -f "$state" ]]; then
314
+ [[ "$(meta_value "$state" OWNER_REPO || true)" == "$D_OWNER" &&
315
+ "$(meta_value "$state" ENVIRONMENT || true)" == "$D_ENV" &&
316
+ "$(meta_value "$state" APP_SLUG || true)" == "$D_APP" ]] || {
317
+ echo "Existing application state has different ownership." >&2
318
+ exit 35
319
+ }
320
+ current_release="$(meta_value "$state" CURRENT_RELEASE || true)"
321
+ previous_release="$(meta_value "$state" PREVIOUS_RELEASE || true)"
322
+ current_spec="$(meta_value "$state" SPEC_SHA256 || true)"
323
+ [[ -n "$current_release" && "$current_spec" =~ ^[0-9a-f]{64}$ ]] || {
324
+ echo "Existing application state is malformed." >&2
325
+ exit 35
326
+ }
327
+ [[ "$EXPECTED_SPEC_SHA" != "NONE" && "$current_spec" == "$EXPECTED_SPEC_SHA" ]] || {
328
+ echo "Application state changed after planning; create a fresh plan." >&2
329
+ exit 36
330
+ }
331
+ local current_file="$releases/$current_release/release.env"
332
+ [[ -f "$current_file" && ! -L "$current_file" ]] || {
333
+ echo "Current release metadata is missing." >&2
334
+ exit 35
335
+ }
336
+ current_container="$(release_value "$current_file" CONTAINER_NAME || true)"
337
+ valid_container "$current_container" || { echo "Current release container metadata is malformed." >&2; exit 35; }
338
+ if [[ "$current_spec" == "$D_SPEC_SHA" ]]; then
339
+ next_previous="$previous_release"
340
+ else
341
+ next_previous="$current_release"
342
+ fi
343
+ else
344
+ [[ "$EXPECTED_SPEC_SHA" == "NONE" ]] || {
345
+ echo "Application state disappeared after planning; create a fresh plan." >&2
346
+ exit 36
347
+ }
348
+ fi
349
+
350
+ if docker inspect "$D_CONTAINER" >/dev/null 2>&1 && [[ "$D_CONTAINER" != "$current_container" ]]; then
351
+ echo "Desired container name is already occupied outside this managed runtime." >&2
352
+ exit 37
353
+ fi
354
+
355
+ install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$D_APP" "$root" "$releases"
356
+ if [[ -e "$release_dir" && ! -d "$release_dir" ]]; then
357
+ echo "Release path exists and is not a directory." >&2
358
+ exit 35
359
+ fi
360
+ install -d -m 0750 "$release_dir"
361
+ if [[ -f "$release_file" ]]; then
362
+ [[ "$(release_value "$release_file" SPEC_SHA256 || true)" == "$D_SPEC_SHA" ]] || {
363
+ echo "Release directory collision with different runtime metadata." >&2
364
+ exit 35
365
+ }
366
+ else
367
+ write_release_metadata "$release_file"
368
+ fi
369
+
370
+ docker pull "$D_IMAGE" >/dev/null
371
+
372
+ if [[ -n "$current_container" ]]; then
373
+ docker rm -f "$current_container" >/dev/null
374
+ fi
375
+
376
+ if ! start_desired_container "$root"; then
377
+ docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
378
+ record_evidence "$root" "failed-start" "$release_id" "$current_release"
379
+ echo "Application container failed to start; previous release metadata was retained but runtime rollback was not attempted." >&2
380
+ exit 38
381
+ fi
382
+ if ! wait_ready "$D_CONTAINER" "$D_PORT" "$D_HEALTH"; then
383
+ docker rm -f "$D_CONTAINER" >/dev/null 2>&1 || true
384
+ record_evidence "$root" "failed-readiness" "$release_id" "$current_release"
385
+ echo "Application failed readiness; previous release metadata was retained but runtime rollback was not attempted." >&2
386
+ exit 39
387
+ fi
388
+
389
+ tmp="$state.tmp.$$"
390
+ {
391
+ printf 'SCHEMA_VERSION=%s\n' "$APP_SCHEMA_VERSION"
392
+ printf 'OWNER_REPO=%s\n' "$D_OWNER"
393
+ printf 'ENVIRONMENT=%s\n' "$D_ENV"
394
+ printf 'APP_SLUG=%s\n' "$D_APP"
395
+ printf 'CURRENT_RELEASE=%s\n' "$release_id"
396
+ printf 'PREVIOUS_RELEASE=%s\n' "$next_previous"
397
+ printf 'SPEC_SHA256=%s\n' "$D_SPEC_SHA"
398
+ } > "$tmp"
399
+ chmod 0600 "$tmp"
400
+ mv -f "$tmp" "$state"
401
+ record_evidence "$root" "deployed" "$release_id" "$next_previous"
402
+ }
403
+
404
+ case "$ACTION" in
405
+ inventory)
406
+ inventory
407
+ ;;
408
+ deploy)
409
+ deploy
410
+ ;;
411
+ *)
412
+ echo "Unsupported application-runtime action: $ACTION" >&2
413
+ exit 2
414
+ ;;
415
+ esac
416
416
  '''