@mriekki/lean 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +66 -2
  2. package/assets/components.json +33 -0
  3. package/assets/core/distribution.json +51 -0
  4. package/assets/deployment/distribution.json +34 -0
  5. package/assets/generated/ASSEMBLY.json +382 -0
  6. package/assets/generated/core/core/agents/coach.agent.md +69 -0
  7. package/assets/generated/core/core/agents/reviewer.agent.md +85 -0
  8. package/assets/generated/core/core/instructions/lean-docs.instructions.md +14 -0
  9. package/assets/generated/core/core/instructions/lean-workflow.instructions.md +35 -0
  10. package/assets/generated/core/core/learning/CODING_DRILLS.md +35 -0
  11. package/assets/generated/core/core/learning/DIAGRAM_GUIDE.md +19 -0
  12. package/assets/generated/core/core/learning/ENGINEERING_LEARNING_WORKBOOK.md +82 -0
  13. package/assets/generated/core/core/learning/LEARNING_MAP.md +51 -0
  14. package/assets/generated/core/core/learning/LEARNING_PROGRESS.md +31 -0
  15. package/assets/generated/core/core/learning/RESOURCE_CATALOG.md +25 -0
  16. package/assets/generated/core/core/learning/SQL_DRILLS.md +27 -0
  17. package/assets/generated/core/core/learning/SYSTEM_DESIGN_DRILLS.md +21 -0
  18. package/assets/generated/core/core/process/COACHING_GUIDE.md +57 -0
  19. package/assets/generated/core/core/process/GUIDED_NEXT_STEPS.md +48 -0
  20. package/assets/generated/core/core/process/HUMAN_GATES.md +81 -0
  21. package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +62 -0
  22. package/assets/generated/core/core/process/PRODUCTION_ENABLEMENT.md +59 -0
  23. package/assets/generated/core/core/process/PR_WORKFLOW.md +53 -0
  24. package/assets/generated/core/core/process/SETUP_ACCEPTANCE_CHECKLIST.md +72 -0
  25. package/assets/generated/core/core/process/WORK_ITEM_MODEL.md +56 -0
  26. package/assets/generated/core/core/skills/cleanup-branches/SKILL.md +190 -0
  27. package/assets/generated/core/core/skills/coach/SKILL.md +136 -0
  28. package/assets/generated/core/core/skills/code-walkthrough/SKILL.md +197 -0
  29. package/assets/generated/core/core/skills/core-sync/SKILL.md +220 -0
  30. package/assets/generated/core/core/skills/define-feature/SKILL.md +144 -0
  31. package/assets/generated/core/core/skills/deliver/SKILL.md +315 -0
  32. package/assets/generated/core/core/skills/deploy/SKILL.md +193 -0
  33. package/assets/generated/core/core/skills/grill-me/SKILL.md +77 -0
  34. package/assets/generated/core/core/skills/grill-with-docs/SKILL.md +90 -0
  35. package/assets/generated/core/core/skills/learning-review/SKILL.md +90 -0
  36. package/assets/generated/core/core/skills/next-step/SKILL.md +166 -0
  37. package/assets/generated/core/core/skills/production-readiness/SKILL.md +357 -0
  38. package/assets/generated/core/core/skills/pull-latest/SKILL.md +166 -0
  39. package/assets/generated/core/core/skills/refine-requirements/SKILL.md +207 -0
  40. package/assets/generated/core/core/skills/start-work/SKILL.md +178 -0
  41. package/assets/generated/core/core/skills/tear-down/SKILL.md +138 -0
  42. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +206 -0
  43. package/assets/generated/core/core/skills/wait-what/SKILL.md +86 -0
  44. package/assets/generated/core/core/templates/FEATURE_BRIEF_TEMPLATE.md +27 -0
  45. package/assets/generated/core/core/templates/PRODUCT_BRIEF_TEMPLATE.md +27 -0
  46. package/assets/generated/core/core/templates/PULL_REQUEST_TEMPLATE.md +37 -0
  47. package/assets/generated/core/core/templates/REQUIREMENTS_TEMPLATE.md +17 -0
  48. package/assets/generated/core/profiles/greenfield/lean-workspace.json +12 -0
  49. package/assets/generated/core/tools/package/check_baseline.py +102 -0
  50. package/assets/generated/core/tools/package/check_upstream.py +171 -0
  51. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -0
  52. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -0
  53. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -0
  54. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -0
  55. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -0
  56. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +416 -0
  57. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -0
  58. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -0
  59. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -0
  60. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -0
  61. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -0
  62. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -0
  63. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -0
  64. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -0
  65. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -0
  66. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -0
  67. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -0
  68. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -0
  69. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +335 -0
  70. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +340 -0
  71. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +86 -0
  72. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -0
  73. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -0
  74. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -0
  75. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -0
  76. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -0
  77. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -0
  78. package/bin/lean.mjs +9 -0
  79. package/package.json +31 -4
  80. package/src/cli.mjs +131 -0
  81. package/src/engine.mjs +243 -0
  82. package/src/package-data.mjs +18 -0
@@ -0,0 +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
416
+ '''
@@ -0,0 +1,208 @@
1
+ """Shared Ubuntu box platform ownership and lifecycle operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import shlex
9
+ import subprocess
10
+ from typing import Any
11
+
12
+ from .box_platform_assets import (
13
+ AVAHI_SERVICE,
14
+ CADDYFILE,
15
+ CADDY_CONTAINER,
16
+ CADDY_IMAGE,
17
+ COMPOSE_YAML,
18
+ HOST_PLATFORM_WRITE_PHRASE,
19
+ NETWORK_NAME,
20
+ PLATFORM_DISTRIBUTION,
21
+ PLATFORM_ROOT,
22
+ PLATFORM_SCHEMA_VERSION,
23
+ PLATFORM_VERSION,
24
+ PUBLISH_ALIAS_SH,
25
+ )
26
+ from .box_platform_remote import REMOTE_SCRIPT
27
+ from .errors import DeploymentKitError
28
+ from .profiles import load_profile
29
+ from .ssh import ssh_args, verify_access
30
+
31
+ _BOOL_KEYS = {
32
+ "markerPresent",
33
+ "prerequisitesReady",
34
+ "networkPresent",
35
+ "caddyRunning",
36
+ "avahiRunning",
37
+ "port80InUse",
38
+ }
39
+ _EXPECTED_KEYS = {
40
+ "state",
41
+ "markerPresent",
42
+ "markerSchemaVersion",
43
+ "markerDistribution",
44
+ "markerPlatformVersion",
45
+ "expectedPlatformVersion",
46
+ "prerequisitesReady",
47
+ "networkPresent",
48
+ "caddyRunning",
49
+ "avahiRunning",
50
+ "port80InUse",
51
+ "platformRoot",
52
+ "networkName",
53
+ "caddyContainer",
54
+ }
55
+
56
+
57
+ def build_remote_script() -> str:
58
+ replacements = {
59
+ "__PLATFORM_ROOT__": PLATFORM_ROOT,
60
+ "__NETWORK_NAME__": NETWORK_NAME,
61
+ "__CADDY_CONTAINER__": CADDY_CONTAINER,
62
+ "__CADDY_IMAGE__": CADDY_IMAGE,
63
+ "__SCHEMA_VERSION__": str(PLATFORM_SCHEMA_VERSION),
64
+ "__PLATFORM_VERSION__": PLATFORM_VERSION,
65
+ "__DISTRIBUTION__": PLATFORM_DISTRIBUTION,
66
+ "__COMPOSE_B64__": base64.b64encode(COMPOSE_YAML.encode()).decode("ascii"),
67
+ "__CADDYFILE_B64__": base64.b64encode(CADDYFILE.encode()).decode("ascii"),
68
+ "__PUBLISH_ALIAS_B64__": base64.b64encode(PUBLISH_ALIAS_SH.encode()).decode("ascii"),
69
+ "__AVAHI_SERVICE_B64__": base64.b64encode(AVAHI_SERVICE.encode()).decode("ascii"),
70
+ }
71
+ script = REMOTE_SCRIPT
72
+ for placeholder, value in replacements.items():
73
+ script = script.replace(placeholder, value)
74
+ if "__" in script:
75
+ raise DeploymentKitError("Internal host-platform script contains an unresolved placeholder.")
76
+ return script
77
+
78
+
79
+ def _parse_status(output: str) -> dict[str, Any]:
80
+ result: dict[str, Any] = {}
81
+ for raw in output.splitlines():
82
+ if not raw.strip():
83
+ continue
84
+ if "=" not in raw:
85
+ raise DeploymentKitError(f"Malformed host-platform status line: {raw!r}")
86
+ key, value = raw.split("=", 1)
87
+ if key not in _EXPECTED_KEYS or key in result:
88
+ raise DeploymentKitError(f"Unexpected or duplicate host-platform status key: {key!r}")
89
+ if key in _BOOL_KEYS:
90
+ if value not in {"true", "false"}:
91
+ raise DeploymentKitError(f"Invalid boolean host-platform status value for {key}.")
92
+ result[key] = value == "true"
93
+ else:
94
+ result[key] = value
95
+ missing = _EXPECTED_KEYS - result.keys()
96
+ if missing:
97
+ raise DeploymentKitError(
98
+ "Host-platform status omitted required keys: " + ", ".join(sorted(missing))
99
+ )
100
+ return result
101
+
102
+
103
+ def _run_remote(profile: dict[str, Any], address: str, action: str, *, sudo: bool) -> str:
104
+ if action not in {"status", "install", "upgrade"}:
105
+ raise DeploymentKitError(f"Unsupported host-platform action: {action}")
106
+ remote_command = "bash -s -- " + shlex.quote(action)
107
+ if sudo:
108
+ remote_command = "sudo -n " + remote_command
109
+ command = ssh_args(profile, address, command=remote_command)
110
+ completed = subprocess.run(
111
+ command,
112
+ input=build_remote_script().encode("utf-8"),
113
+ check=False,
114
+ capture_output=True,
115
+ )
116
+ stdout = completed.stdout.decode("utf-8", errors="replace")
117
+ stderr = completed.stderr.decode("utf-8", errors="replace")
118
+ if completed.returncode != 0:
119
+ detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
120
+ raise DeploymentKitError(f"Remote host-platform {action} failed: {detail}")
121
+ return stdout
122
+
123
+
124
+ def platform_status(name: str) -> dict[str, Any]:
125
+ profile = load_profile(name)
126
+ evidence = verify_access(profile)
127
+ status = _parse_status(_run_remote(profile, evidence["address"], "status", sudo=True))
128
+ status["targetName"] = name
129
+ status["verifiedAddress"] = evidence["address"]
130
+ return status
131
+
132
+
133
+ def platform_doctor(name: str) -> dict[str, Any]:
134
+ status = platform_status(name)
135
+ next_actions = {
136
+ "installable": "platform-install",
137
+ "upgrade-required": "platform-upgrade",
138
+ "drifted": "inspect-platform-drift",
139
+ "blocked": "prepare-host-prerequisites",
140
+ "blocked-port-80": "identify-existing-port-80-owner",
141
+ "incompatible": "manual-platform-recovery",
142
+ "ready": "none",
143
+ }
144
+ status["nextAction"] = next_actions.get(status["state"], "manual-review")
145
+ status["mutated"] = False
146
+ return status
147
+
148
+
149
+ def _snapshot(status: dict[str, Any]) -> str:
150
+ selected = {key: status[key] for key in sorted(_EXPECTED_KEYS)}
151
+ return hashlib.sha256(json.dumps(selected, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
152
+
153
+
154
+ def plan_platform_change(name: str, action: str) -> dict[str, Any]:
155
+ if action not in {"install", "upgrade"}:
156
+ raise DeploymentKitError("Platform change action must be install or upgrade.")
157
+ status = platform_status(name)
158
+ expected_state = "installable" if action == "install" else "upgrade-required"
159
+ if status["state"] != expected_state:
160
+ raise DeploymentKitError(
161
+ f"Platform {action} requires state '{expected_state}', but target '{name}' is '{status['state']}'."
162
+ )
163
+ return {
164
+ "targetName": name,
165
+ "action": action,
166
+ "fromState": status["state"],
167
+ "toPlatformVersion": PLATFORM_VERSION,
168
+ "distribution": PLATFORM_DISTRIBUTION,
169
+ "platformRoot": PLATFORM_ROOT,
170
+ "preserves": ["route-conf.d", "route-registry", "caddy-data", "caddy-config", "shared-network"],
171
+ "doesNotOwn": ["application-containers", "application-data", "application-routes"],
172
+ "statusSnapshot": _snapshot(status),
173
+ }
174
+
175
+
176
+ def apply_platform_change(plan: dict[str, Any]) -> dict[str, Any]:
177
+ required = {
178
+ "targetName",
179
+ "action",
180
+ "fromState",
181
+ "toPlatformVersion",
182
+ "distribution",
183
+ "platformRoot",
184
+ "preserves",
185
+ "doesNotOwn",
186
+ "statusSnapshot",
187
+ }
188
+ if not isinstance(plan, dict) or set(plan) != required:
189
+ raise DeploymentKitError("Malformed host-platform change plan.")
190
+ name = plan["targetName"]
191
+ action = plan["action"]
192
+ if action not in {"install", "upgrade"}:
193
+ raise DeploymentKitError("Malformed host-platform change action.")
194
+ current = platform_status(name)
195
+ if _snapshot(current) != plan["statusSnapshot"]:
196
+ raise DeploymentKitError(
197
+ "Host-platform state changed after the plan was reviewed; create and authorize a fresh plan."
198
+ )
199
+ profile = load_profile(name)
200
+ evidence = verify_access(profile)
201
+ _run_remote(profile, evidence["address"], action, sudo=True)
202
+ final = platform_status(name)
203
+ if final["state"] != "ready":
204
+ raise DeploymentKitError(
205
+ f"Host-platform {action} completed but readiness is '{final['state']}', not 'ready'."
206
+ )
207
+ final["mutated"] = True
208
+ return final