@caelo-cms/provisioning 0.10.47 → 0.10.48

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 (315) hide show
  1. package/dist/agent/apply.d.ts +45 -0
  2. package/dist/agent/apply.d.ts.map +1 -0
  3. package/dist/agent/apply.js +83 -0
  4. package/dist/agent/apply.js.map +1 -0
  5. package/dist/agent/cli-provenance.d.ts +42 -0
  6. package/dist/agent/cli-provenance.d.ts.map +1 -0
  7. package/dist/agent/cli-provenance.js +138 -0
  8. package/dist/agent/cli-provenance.js.map +1 -0
  9. package/dist/agent/command.d.ts +79 -0
  10. package/dist/agent/command.d.ts.map +1 -0
  11. package/dist/agent/command.js +454 -0
  12. package/dist/agent/command.js.map +1 -0
  13. package/dist/agent/config.d.ts +30 -0
  14. package/dist/agent/config.d.ts.map +1 -0
  15. package/dist/agent/config.js +60 -0
  16. package/dist/agent/config.js.map +1 -0
  17. package/dist/agent/lock.d.ts +13 -0
  18. package/dist/agent/lock.d.ts.map +1 -0
  19. package/dist/agent/lock.js +100 -0
  20. package/dist/agent/lock.js.map +1 -0
  21. package/dist/agent/queue.d.ts +35 -0
  22. package/dist/agent/queue.d.ts.map +1 -0
  23. package/dist/agent/queue.js +50 -0
  24. package/dist/agent/queue.js.map +1 -0
  25. package/dist/agent/run.d.ts +97 -0
  26. package/dist/agent/run.d.ts.map +1 -0
  27. package/dist/agent/run.js +198 -0
  28. package/dist/agent/run.js.map +1 -0
  29. package/dist/agent/systemd.d.ts +55 -0
  30. package/dist/agent/systemd.d.ts.map +1 -0
  31. package/dist/agent/systemd.js +171 -0
  32. package/dist/agent/systemd.js.map +1 -0
  33. package/dist/agent/units.d.ts +44 -0
  34. package/dist/agent/units.d.ts.map +1 -0
  35. package/dist/agent/units.js +127 -0
  36. package/dist/agent/units.js.map +1 -0
  37. package/dist/agent-db.js +18362 -0
  38. package/dist/cli.js +119 -16
  39. package/dist/cli.js.map +1 -1
  40. package/dist/cloudsql-iam-user.d.ts +39 -0
  41. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  42. package/dist/cloudsql-iam-user.js +119 -0
  43. package/dist/cloudsql-iam-user.js.map +1 -0
  44. package/dist/compose.d.ts +16 -0
  45. package/dist/compose.d.ts.map +1 -1
  46. package/dist/compose.js +26 -1
  47. package/dist/compose.js.map +1 -1
  48. package/dist/dns/index.d.ts +2 -0
  49. package/dist/dns/index.d.ts.map +1 -1
  50. package/dist/dns/index.js +3 -2
  51. package/dist/dns/index.js.map +1 -1
  52. package/dist/dns/manual.d.ts +5 -0
  53. package/dist/dns/manual.d.ts.map +1 -1
  54. package/dist/dns/manual.js +28 -12
  55. package/dist/dns/manual.js.map +1 -1
  56. package/dist/gcloud.d.ts +2 -0
  57. package/dist/gcloud.d.ts.map +1 -1
  58. package/dist/gcloud.js +1 -1
  59. package/dist/gcloud.js.map +1 -1
  60. package/dist/gcp-names.d.ts +15 -0
  61. package/dist/gcp-names.d.ts.map +1 -1
  62. package/dist/gcp-names.js +23 -0
  63. package/dist/gcp-names.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/infra-contract.d.ts +1 -1
  69. package/dist/infra-contract.js +7 -1
  70. package/dist/infra-contract.js.map +1 -1
  71. package/dist/install-job-runner.d.ts +13 -0
  72. package/dist/install-job-runner.d.ts.map +1 -0
  73. package/dist/install-job-runner.js +155 -0
  74. package/dist/install-job-runner.js.map +1 -0
  75. package/dist/install-job.d.ts +210 -0
  76. package/dist/install-job.d.ts.map +1 -0
  77. package/dist/install-job.js +449 -0
  78. package/dist/install-job.js.map +1 -0
  79. package/dist/install-state.d.ts +10 -1
  80. package/dist/install-state.d.ts.map +1 -1
  81. package/dist/install-state.js +10 -2
  82. package/dist/install-state.js.map +1 -1
  83. package/dist/lifecycle.d.ts.map +1 -1
  84. package/dist/lifecycle.js +19 -4
  85. package/dist/lifecycle.js.map +1 -1
  86. package/dist/migration-runner.d.ts +5 -0
  87. package/dist/migration-runner.d.ts.map +1 -1
  88. package/dist/migration-runner.js +1 -1
  89. package/dist/migration-runner.js.map +1 -1
  90. package/dist/operator-access.d.ts +14 -8
  91. package/dist/operator-access.d.ts.map +1 -1
  92. package/dist/operator-access.js +16 -102
  93. package/dist/operator-access.js.map +1 -1
  94. package/dist/run-mode.d.ts +27 -0
  95. package/dist/run-mode.d.ts.map +1 -0
  96. package/dist/run-mode.js +22 -0
  97. package/dist/run-mode.js.map +1 -0
  98. package/dist/runtime-secrets.d.ts +19 -0
  99. package/dist/runtime-secrets.d.ts.map +1 -1
  100. package/dist/runtime-secrets.js +44 -0
  101. package/dist/runtime-secrets.js.map +1 -1
  102. package/dist/self-hosted-roles.d.ts +22 -1
  103. package/dist/self-hosted-roles.d.ts.map +1 -1
  104. package/dist/self-hosted-roles.js +81 -12
  105. package/dist/self-hosted-roles.js.map +1 -1
  106. package/dist/self-hosted-upgrade.d.ts +17 -0
  107. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  108. package/dist/self-hosted-upgrade.js +16 -1
  109. package/dist/self-hosted-upgrade.js.map +1 -1
  110. package/dist/stack-contract.d.ts +6 -0
  111. package/dist/stack-contract.d.ts.map +1 -1
  112. package/dist/stack-contract.js +10 -1
  113. package/dist/stack-contract.js.map +1 -1
  114. package/dist/upgrade/converge.d.ts +6 -0
  115. package/dist/upgrade/converge.d.ts.map +1 -1
  116. package/dist/upgrade/converge.js +59 -2
  117. package/dist/upgrade/converge.js.map +1 -1
  118. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  119. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  120. package/dist/upgrade/gcp-lookup.js +25 -0
  121. package/dist/upgrade/gcp-lookup.js.map +1 -1
  122. package/dist/upgrade/release-images.d.ts +12 -0
  123. package/dist/upgrade/release-images.d.ts.map +1 -1
  124. package/dist/upgrade/release-images.js +31 -0
  125. package/dist/upgrade/release-images.js.map +1 -1
  126. package/dist/upgrade/upgrade.d.ts +5 -0
  127. package/dist/upgrade/upgrade.d.ts.map +1 -1
  128. package/dist/upgrade/upgrade.js +28 -1
  129. package/dist/upgrade/upgrade.js.map +1 -1
  130. package/dist/upgrader-assess.d.ts +82 -0
  131. package/dist/upgrader-assess.d.ts.map +1 -0
  132. package/dist/upgrader-assess.js +116 -0
  133. package/dist/upgrader-assess.js.map +1 -0
  134. package/dist/upgrader-gcp.d.ts +17 -0
  135. package/dist/upgrader-gcp.d.ts.map +1 -0
  136. package/dist/upgrader-gcp.js +87 -0
  137. package/dist/upgrader-gcp.js.map +1 -0
  138. package/dist/upgrader-job.d.ts +131 -0
  139. package/dist/upgrader-job.d.ts.map +1 -0
  140. package/dist/upgrader-job.js +259 -0
  141. package/dist/upgrader-job.js.map +1 -0
  142. package/dist/upgrader-reporter.d.ts +35 -0
  143. package/dist/upgrader-reporter.d.ts.map +1 -0
  144. package/dist/upgrader-reporter.js +79 -0
  145. package/dist/upgrader-reporter.js.map +1 -0
  146. package/dist/upgrader-setup.d.ts +59 -0
  147. package/dist/upgrader-setup.d.ts.map +1 -0
  148. package/dist/upgrader-setup.js +308 -0
  149. package/dist/upgrader-setup.js.map +1 -0
  150. package/dist/wizard.d.ts +6 -0
  151. package/dist/wizard.d.ts.map +1 -1
  152. package/dist/wizard.js +32 -4
  153. package/dist/wizard.js.map +1 -1
  154. package/dist/wizards/gcp-firebase.d.ts +3 -0
  155. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  156. package/dist/wizards/gcp-firebase.js +1 -0
  157. package/dist/wizards/gcp-firebase.js.map +1 -1
  158. package/dist/wizards/gcp-install-job.d.ts +74 -0
  159. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  160. package/dist/wizards/gcp-install-job.js +398 -0
  161. package/dist/wizards/gcp-install-job.js.map +1 -0
  162. package/dist/wizards/gcp-prompts.d.ts +3 -0
  163. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  164. package/dist/wizards/gcp-prompts.js +25 -0
  165. package/dist/wizards/gcp-prompts.js.map +1 -0
  166. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  167. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  168. package/dist/wizards/gcp-pulumi.js +1 -1
  169. package/dist/wizards/gcp-pulumi.js.map +1 -1
  170. package/dist/wizards/gcp.d.ts +40 -0
  171. package/dist/wizards/gcp.d.ts.map +1 -1
  172. package/dist/wizards/gcp.js +223 -59
  173. package/dist/wizards/gcp.js.map +1 -1
  174. package/package.json +6 -4
  175. package/src/adapter.ts +97 -0
  176. package/src/adc-impersonation.ts +176 -0
  177. package/src/admin-domain.test.ts +254 -0
  178. package/src/admin-domain.ts +245 -0
  179. package/src/admin-memory.test.ts +151 -0
  180. package/src/agent/apply.test.ts +185 -0
  181. package/src/agent/apply.ts +124 -0
  182. package/src/agent/cli-provenance.test.ts +159 -0
  183. package/src/agent/cli-provenance.ts +191 -0
  184. package/src/agent/command.test.ts +77 -0
  185. package/src/agent/command.ts +564 -0
  186. package/src/agent/config.ts +86 -0
  187. package/src/agent/lock-config.test.ts +97 -0
  188. package/src/agent/lock.ts +110 -0
  189. package/src/agent/queue.test.ts +10 -0
  190. package/src/agent/queue.ts +91 -0
  191. package/src/agent/run.test.ts +382 -0
  192. package/src/agent/run.ts +295 -0
  193. package/src/agent/systemd.ts +209 -0
  194. package/src/agent/units.test.ts +223 -0
  195. package/src/agent/units.ts +163 -0
  196. package/src/azure-front-door.test.ts +119 -0
  197. package/src/azure-front-door.ts +56 -0
  198. package/src/bootstrap-token.ts +20 -0
  199. package/src/caddy.ts +112 -0
  200. package/src/cdn-copy.ts +91 -0
  201. package/src/cert-wait.test.ts +194 -0
  202. package/src/cert-wait.ts +163 -0
  203. package/src/checkpoint-secrets.test.ts +380 -0
  204. package/src/checkpoint-secrets.ts +283 -0
  205. package/src/cli-routing.test.ts +45 -0
  206. package/src/cli-routing.ts +43 -0
  207. package/src/cli.ts +1491 -0
  208. package/src/cloudsql-iam-user.ts +147 -0
  209. package/src/compose.ts +334 -0
  210. package/src/default-firewall.test.ts +411 -0
  211. package/src/default-firewall.ts +342 -0
  212. package/src/deployed-release.test.ts +231 -0
  213. package/src/deployed-release.ts +220 -0
  214. package/src/dns/cloudflare.ts +182 -0
  215. package/src/dns/groups.test.ts +76 -0
  216. package/src/dns/groups.ts +86 -0
  217. package/src/dns/index.ts +56 -0
  218. package/src/dns/manual.ts +130 -0
  219. package/src/dns/types.ts +27 -0
  220. package/src/gateway-credentials.test.ts +180 -0
  221. package/src/gateway-credentials.ts +144 -0
  222. package/src/gcloud-components.test.ts +76 -0
  223. package/src/gcloud-components.ts +85 -0
  224. package/src/gcloud-retry.ts +41 -0
  225. package/src/gcloud-runner.test.ts +61 -0
  226. package/src/gcloud.test.ts +99 -0
  227. package/src/gcloud.ts +322 -0
  228. package/src/gcp-names.ts +135 -0
  229. package/src/index.test.ts +332 -0
  230. package/src/index.ts +114 -0
  231. package/src/infra-contract.ts +32 -0
  232. package/src/install-job-runner.ts +166 -0
  233. package/src/install-job.test.ts +566 -0
  234. package/src/install-job.ts +542 -0
  235. package/src/install-state.test.ts +239 -0
  236. package/src/install-state.ts +583 -0
  237. package/src/internal-token.ts +35 -0
  238. package/src/lifecycle.ts +900 -0
  239. package/src/mcp-iap-stacks.test.ts +58 -0
  240. package/src/mcp-iap.test.ts +149 -0
  241. package/src/mcp-iap.ts +131 -0
  242. package/src/media-volume.test.ts +296 -0
  243. package/src/migration-runner.ts +365 -0
  244. package/src/operator-access-stacks.test.ts +55 -0
  245. package/src/operator-access.test.ts +332 -0
  246. package/src/operator-access.ts +554 -0
  247. package/src/ops-backend.test.ts +320 -0
  248. package/src/ops-backend.ts +414 -0
  249. package/src/provisioner-identity.test.ts +482 -0
  250. package/src/provisioner-identity.ts +407 -0
  251. package/src/redirects-emit.ts +175 -0
  252. package/src/region-data.ts +428 -0
  253. package/src/regions.test.ts +277 -0
  254. package/src/regions.ts +370 -0
  255. package/src/release-contract.ts +57 -0
  256. package/src/release-manifest.test.ts +293 -0
  257. package/src/release-manifest.ts +430 -0
  258. package/src/run-mode.ts +40 -0
  259. package/src/runtime-secrets.test.ts +627 -0
  260. package/src/runtime-secrets.ts +754 -0
  261. package/src/self-command.test.ts +14 -0
  262. package/src/self-command.ts +21 -0
  263. package/src/self-hosted-compose.test.ts +177 -0
  264. package/src/self-hosted-release.ts +240 -0
  265. package/src/self-hosted-roles.integration.test.ts +161 -0
  266. package/src/self-hosted-roles.test.ts +124 -0
  267. package/src/self-hosted-roles.ts +203 -0
  268. package/src/self-hosted-upgrade.test.ts +394 -0
  269. package/src/self-hosted-upgrade.ts +548 -0
  270. package/src/stack-contract-parity.test.ts +199 -0
  271. package/src/stack-contract.test.ts +391 -0
  272. package/src/stack-contract.ts +888 -0
  273. package/src/stack-converge-secrets.test.ts +421 -0
  274. package/src/stack-converge.test.ts +833 -0
  275. package/src/stack-converge.ts +909 -0
  276. package/src/staging-admin.test.ts +246 -0
  277. package/src/staging-admin.ts +324 -0
  278. package/src/state-attach.test.ts +211 -0
  279. package/src/state-attach.ts +132 -0
  280. package/src/state-backend.test.ts +145 -0
  281. package/src/state-backend.ts +262 -0
  282. package/src/state-command.ts +293 -0
  283. package/src/state-migrate.test.ts +479 -0
  284. package/src/state-migrate.ts +434 -0
  285. package/src/state-pulumi.ts +289 -0
  286. package/src/state-store.test.ts +212 -0
  287. package/src/state-store.ts +243 -0
  288. package/src/upgrade/app-roll.ts +486 -0
  289. package/src/upgrade/backup.ts +109 -0
  290. package/src/upgrade/converge.ts +437 -0
  291. package/src/upgrade/gcp-lookup.ts +167 -0
  292. package/src/upgrade/health.test.ts +279 -0
  293. package/src/upgrade/health.ts +305 -0
  294. package/src/upgrade/release-images.test.ts +119 -0
  295. package/src/upgrade/release-images.ts +391 -0
  296. package/src/upgrade/reporter.ts +46 -0
  297. package/src/upgrade/upgrade.test.ts +626 -0
  298. package/src/upgrade/upgrade.ts +249 -0
  299. package/src/upgrader-assess.test.ts +265 -0
  300. package/src/upgrader-assess.ts +233 -0
  301. package/src/upgrader-gcp.ts +123 -0
  302. package/src/upgrader-job.test.ts +408 -0
  303. package/src/upgrader-job.ts +499 -0
  304. package/src/upgrader-reporter.test.ts +46 -0
  305. package/src/upgrader-reporter.ts +105 -0
  306. package/src/upgrader-setup.test.ts +169 -0
  307. package/src/upgrader-setup.ts +355 -0
  308. package/src/wizard.ts +425 -0
  309. package/src/wizards/gcp-cost.ts +188 -0
  310. package/src/wizards/gcp-firebase.ts +72 -0
  311. package/src/wizards/gcp-install-job.ts +506 -0
  312. package/src/wizards/gcp-prompts.ts +26 -0
  313. package/src/wizards/gcp-pulumi.test.ts +246 -0
  314. package/src/wizards/gcp-pulumi.ts +516 -0
  315. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,394 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import type { ReleaseImages } from "./compose.js";
16
+ import {
17
+ type CommandResult,
18
+ type HostRunner,
19
+ readRelease,
20
+ recordRelease,
21
+ } from "./self-hosted-release.js";
22
+ import {
23
+ composePath,
24
+ legacyComposePath,
25
+ type UpgradeLog,
26
+ upgradeSelfHosted,
27
+ } from "./self-hosted-upgrade.js";
28
+
29
+ const OLD_DIGEST = { admin: `sha256:${"1".repeat(64)}`, gateway: `sha256:${"2".repeat(64)}` };
30
+ const OLD: ReleaseImages = {
31
+ mode: "release",
32
+ admin: `ghcr.io/caelo-cms/admin@${OLD_DIGEST.admin}`,
33
+ gateway: `ghcr.io/caelo-cms/gateway@${OLD_DIGEST.gateway}`,
34
+ };
35
+ const NEW_DIGEST = { admin: `sha256:${"a".repeat(64)}`, gateway: `sha256:${"b".repeat(64)}` };
36
+ const NEW: ReleaseImages = {
37
+ mode: "release",
38
+ admin: `ghcr.io/caelo-cms/admin@${NEW_DIGEST.admin}`,
39
+ gateway: `ghcr.io/caelo-cms/gateway@${NEW_DIGEST.gateway}`,
40
+ };
41
+
42
+ /** A stand-in for generateDockerCompose: just the two image lines. */
43
+ const render = (images: ReleaseImages) => `admin: ${images.admin}\ngateway: ${images.gateway}\n`;
44
+
45
+ /** GHCR fake: the anonymous token dance, then `digests` per repository. */
46
+ function registryServing(digests: { admin: string; gateway: string }): typeof fetch {
47
+ return (async (input: string | URL | Request, init?: RequestInit) => {
48
+ const url = String(input);
49
+ if (url.startsWith("https://api.github.com/")) return Response.json({ tag_name: "v0.11.0" });
50
+ if (url.startsWith("https://ghcr.io/token")) return Response.json({ token: "t" });
51
+ if (!new Headers(init?.headers).get("authorization")) {
52
+ return new Response(null, {
53
+ status: 401,
54
+ headers: { "www-authenticate": 'Bearer realm="https://ghcr.io/token",service="ghcr.io"' },
55
+ });
56
+ }
57
+ const service = url.includes("/caelo-cms/admin/") ? "admin" : "gateway";
58
+ return new Response(null, {
59
+ status: 200,
60
+ headers: { "docker-content-digest": digests[service] },
61
+ });
62
+ }) as typeof fetch;
63
+ }
64
+ const registry = registryServing(NEW_DIGEST);
65
+
66
+ type Rule = (argv: string) => Partial<CommandResult> | undefined;
67
+
68
+ /** Records every host command; `rules` decide the outcome (default: success). */
69
+ function fakeRunner(rules: Rule[] = []): HostRunner & { calls: string[] } {
70
+ const calls: string[] = [];
71
+ return {
72
+ calls,
73
+ async run(argv, opts) {
74
+ const line = argv.join(" ");
75
+ calls.push(line);
76
+ const hit = rules.map((r) => r(line)).find((r) => r !== undefined);
77
+ const result = { exitCode: 0, stdout: "", stderr: "", ...hit };
78
+ if (opts?.stdoutPath && result.exitCode === 0) writeFileSync(opts.stdoutPath, "DUMP");
79
+ return result;
80
+ },
81
+ };
82
+ }
83
+
84
+ /** A compact label per call, for asserting the order of the steps. */
85
+ function steps(calls: readonly string[]): string[] {
86
+ return calls.map((c) => {
87
+ if (c.startsWith("cosign verify"))
88
+ return `verify ${c.includes("/admin@") ? "admin" : "gateway"}`;
89
+ if (c.startsWith("cosign")) return "cosign-probe";
90
+ if (c.includes("pg_isready")) return "pg_isready";
91
+ if (c.includes("pg_dumpall")) return "dump globals";
92
+ if (c.includes("pg_dump ")) return `dump ${c.split(" ").at(-1)}`;
93
+ if (c.includes(" pull ")) return "pull";
94
+ if (c.includes("migrate.ts")) return `migrate ${c.split(" ").at(-1)}`;
95
+ if (c.includes(" up -d --no-deps caelo-admin caelo-gateway")) return "up both";
96
+ if (c.includes(" up -d --no-deps ")) return `up ${c.split(" ").at(-1)}`;
97
+ if (c.includes("/_caelo/health")) return "health admin";
98
+ if (c.includes("/healthz")) return "health gateway";
99
+ return c;
100
+ });
101
+ }
102
+
103
+ let dir: string;
104
+ let messages: string[];
105
+ const log: UpgradeLog = {
106
+ step: (m) => messages.push(`step ${m}`),
107
+ info: (m) => messages.push(`info ${m}`),
108
+ warn: (m) => messages.push(`warn ${m}`),
109
+ error: (m) => messages.push(`error ${m}`),
110
+ };
111
+
112
+ beforeEach(() => {
113
+ dir = mkdtempSync(join(tmpdir(), "caelo-sh-upgrade-"));
114
+ messages = [];
115
+ recordRelease(
116
+ dir,
117
+ { version: "0.10.0", images: OLD },
118
+ { from: null, to: "0.10.0", outcome: "installed" },
119
+ );
120
+ writeFileSync(composePath(dir), render(OLD));
121
+ });
122
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
123
+
124
+ function deps(runner: HostRunner) {
125
+ return {
126
+ caeloDir: dir,
127
+ runner,
128
+ renderCompose: render,
129
+ log,
130
+ fetchImpl: registry,
131
+ sleep: async () => {},
132
+ healthAttempts: 3,
133
+ healthIntervalMs: 0,
134
+ now: () => new Date("2026-10-13T12:00:00Z"),
135
+ };
136
+ }
137
+
138
+ describe("upgradeSelfHosted", () => {
139
+ it("verifies, backs up, pulls, migrates, then rolls admin before gateway", async () => {
140
+ const runner = fakeRunner();
141
+ const r = await upgradeSelfHosted({}, deps(runner));
142
+ expect(r).toEqual({ ok: true, status: "upgraded", version: "0.11.0" });
143
+ expect(steps(runner.calls)).toEqual([
144
+ "cosign-probe",
145
+ "verify admin",
146
+ "verify gateway",
147
+ "pg_isready",
148
+ "dump globals",
149
+ "dump cms_admin",
150
+ "dump cms_public",
151
+ "pull",
152
+ "migrate admin",
153
+ "migrate public",
154
+ "up caelo-admin",
155
+ "health admin",
156
+ "up caelo-gateway",
157
+ "health gateway",
158
+ ]);
159
+ // The migrations run the NEW admin image: the Compose file already names it.
160
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(NEW));
161
+ const release = readRelease(dir);
162
+ expect(release?.version).toBe("0.11.0");
163
+ expect(release?.images).toEqual({ admin: NEW.admin, gateway: NEW.gateway });
164
+ expect(release?.history.at(-1)).toMatchObject({
165
+ from: "0.10.0",
166
+ to: "0.11.0",
167
+ outcome: "upgraded",
168
+ });
169
+ });
170
+
171
+ it("writes the backup into a private dir before touching anything", async () => {
172
+ await upgradeSelfHosted({ version: "v0.11.0" }, deps(fakeRunner()));
173
+ const backup = join(dir, "backups", "pre-upgrade-2026-10-13T12-00-00-000Z");
174
+ expect(statSync(backup).mode & 0o777).toBe(0o700);
175
+ expect(readFileSync(join(backup, "cms_admin.dump"), "utf8")).toBe("DUMP");
176
+ expect(statSync(join(backup, "cms_public.dump")).mode & 0o777).toBe(0o600);
177
+ });
178
+
179
+ it("is a no-op when the install already runs the target digests", async () => {
180
+ recordRelease(
181
+ dir,
182
+ { version: "0.11.0", images: NEW },
183
+ { from: "0.10.0", to: "0.11.0", outcome: "upgraded" },
184
+ );
185
+ writeFileSync(composePath(dir), render(NEW));
186
+ const runner = fakeRunner();
187
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
188
+ expect(r).toEqual({ ok: true, status: "already-current", version: "0.11.0" });
189
+ // No backup, pull or migration — only converging + proving health.
190
+ expect(steps(runner.calls)).toEqual([
191
+ "cosign-probe",
192
+ "verify admin",
193
+ "verify gateway",
194
+ "up both",
195
+ "health admin",
196
+ "health gateway",
197
+ ]);
198
+ });
199
+
200
+ it("reports an unhealthy install that is already on the target", async () => {
201
+ recordRelease(
202
+ dir,
203
+ { version: "0.11.0", images: NEW },
204
+ { from: "0.10.0", to: "0.11.0", outcome: "upgraded" },
205
+ );
206
+ writeFileSync(composePath(dir), render(NEW));
207
+ const runner = fakeRunner([(c) => (c.includes("/healthz") ? { exitCode: 1 } : undefined)]);
208
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
209
+ expect(r).toMatchObject({ ok: false, stage: "converge", previousRunning: false });
210
+ });
211
+
212
+ it("re-runs the full flow when a stopped run left the Compose file on other digests", async () => {
213
+ // release.json says 0.10.0 (OLD); an interrupted upgrade left NEW in the file.
214
+ writeFileSync(composePath(dir), render(NEW));
215
+ const runner = fakeRunner();
216
+ const r = await upgradeSelfHosted(
217
+ { version: "0.10.0" },
218
+ {
219
+ ...deps(runner),
220
+ fetchImpl: registryServing({ admin: OLD_DIGEST.admin, gateway: OLD_DIGEST.gateway }),
221
+ },
222
+ );
223
+ expect(r).toEqual({ ok: true, status: "upgraded", version: "0.10.0" });
224
+ expect(steps(runner.calls)).toContain("migrate admin");
225
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
226
+ });
227
+
228
+ it("refuses to touch the host when a signature does not verify", async () => {
229
+ const runner = fakeRunner([
230
+ (c) =>
231
+ c.startsWith("cosign verify") && c.includes("/gateway@") ? { exitCode: 1 } : undefined,
232
+ ]);
233
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
234
+ expect(r).toMatchObject({ ok: false, stage: "verify", previousRunning: true });
235
+ expect(runner.calls.some((c) => c.startsWith("docker"))).toBe(false);
236
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
237
+ });
238
+
239
+ it("aborts before pulling when the backup fails", async () => {
240
+ const runner = fakeRunner([
241
+ (c) =>
242
+ c.includes("pg_dump ") && c.endsWith("cms_admin")
243
+ ? { exitCode: 1, stderr: "disk full" }
244
+ : undefined,
245
+ ]);
246
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
247
+ expect(r).toMatchObject({ ok: false, stage: "backup", previousRunning: true });
248
+ if (!r.ok) expect(r.error).toContain("disk full");
249
+ expect(steps(runner.calls)).not.toContain("pull");
250
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
251
+ expect(readRelease(dir)?.version).toBe("0.10.0");
252
+ });
253
+
254
+ it("does not start when Postgres is down (no backup possible)", async () => {
255
+ const runner = fakeRunner([(c) => (c.includes("pg_isready") ? { exitCode: 1 } : undefined)]);
256
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
257
+ expect(r).toMatchObject({ ok: false, stage: "backup" });
258
+ expect(steps(runner.calls)).not.toContain("dump cms_admin");
259
+ });
260
+
261
+ it("restores the Compose file and restarts nothing when a migration fails", async () => {
262
+ const runner = fakeRunner([
263
+ (c) =>
264
+ c.includes("migrate.ts public") ? { exitCode: 1, stderr: "relation exists" } : undefined,
265
+ ]);
266
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
267
+ expect(r).toMatchObject({ ok: false, stage: "migrate", previousRunning: true });
268
+ expect(steps(runner.calls).some((s) => s.startsWith("up "))).toBe(false);
269
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
270
+ });
271
+
272
+ it("rolls back to the previous images when the gateway fails its health check", async () => {
273
+ let compose = "";
274
+ const runner = fakeRunner([
275
+ (c) => {
276
+ if (c.includes(" up -d ")) compose = readFileSync(composePath(dir), "utf8");
277
+ return undefined;
278
+ },
279
+ // Unhealthy only on the new image: healthy again once rolled back.
280
+ (c) => (c.includes("/healthz") && compose === render(NEW) ? { exitCode: 1 } : undefined),
281
+ ]);
282
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
283
+ expect(r).toMatchObject({ ok: false, stage: "roll-gateway", previousRunning: true });
284
+ const s = steps(runner.calls);
285
+ expect(s.slice(s.indexOf("up caelo-gateway"))).toEqual([
286
+ "up caelo-gateway",
287
+ "health gateway",
288
+ "health gateway",
289
+ "health gateway",
290
+ "up both",
291
+ "health admin",
292
+ "health gateway",
293
+ ]);
294
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
295
+ const release = readRelease(dir);
296
+ expect(release?.version).toBe("0.10.0");
297
+ expect(release?.images.admin).toBe(OLD.admin);
298
+ expect(release?.history.at(-1)).toMatchObject({ to: "0.11.0", outcome: "rolled-back" });
299
+ });
300
+
301
+ it("reports a failed rollback loudly", async () => {
302
+ const runner = fakeRunner([
303
+ (c) => (c.includes("/_caelo/health") ? { exitCode: 1 } : undefined),
304
+ ]);
305
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
306
+ expect(r).toMatchObject({ ok: false, stage: "roll-admin", previousRunning: false });
307
+ if (!r.ok) expect(r.error).toContain("ROLLBACK FAILED");
308
+ });
309
+
310
+ it("moves an install from before #696 (no release.json) onto the release images", async () => {
311
+ rmSync(join(dir, "release.json"));
312
+ const legacy = "caelo-admin:\n image: oven/bun:1.3\n volumes: [../..:/app]\n";
313
+ writeFileSync(composePath(dir), legacy);
314
+ const failing = fakeRunner([(c) => (c.includes("/healthz") ? { exitCode: 1 } : undefined)]);
315
+ const failed = await upgradeSelfHosted({ version: "0.11.0" }, deps(failing));
316
+ expect(failed.ok).toBe(false);
317
+ // Rollback restores the legacy file byte for byte.
318
+ expect(readFileSync(composePath(dir), "utf8")).toBe(legacy);
319
+
320
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(fakeRunner()));
321
+ expect(r).toEqual({ ok: true, status: "upgraded", version: "0.11.0" });
322
+ expect(readRelease(dir)?.history.at(-1)).toMatchObject({ from: null, to: "0.11.0" });
323
+ });
324
+
325
+ it("keeps the legacy Compose file for rollback across a stopped first upgrade", async () => {
326
+ rmSync(join(dir, "release.json"));
327
+ const legacy = "legacy: true\n";
328
+ writeFileSync(composePath(dir), legacy);
329
+ // A first run that stopped after writing the release images into the file.
330
+ const stopped = fakeRunner([(c) => (c.includes(" pull ") ? { exitCode: 1 } : undefined)]);
331
+ await upgradeSelfHosted({ version: "0.11.0" }, deps(stopped));
332
+ expect(readFileSync(legacyComposePath(dir), "utf8")).toBe(legacy);
333
+ writeFileSync(composePath(dir), render(NEW));
334
+
335
+ const failing = fakeRunner([(c) => (c.includes("/healthz") ? { exitCode: 1 } : undefined)]);
336
+ await upgradeSelfHosted({ version: "0.11.0" }, deps(failing));
337
+ expect(readFileSync(composePath(dir), "utf8")).toBe(legacy);
338
+ });
339
+
340
+ it("copies a legacy install's media from the checkout into .caelo/media", async () => {
341
+ const root = mkdtempSync(join(tmpdir(), "caelo-sh-repo-"));
342
+ try {
343
+ const caeloDir = join(root, ".caelo");
344
+ mkdirSync(join(root, "apps", "admin", "data", "media", "a"), { recursive: true });
345
+ writeFileSync(join(root, "apps", "admin", "data", "media", "a", "logo.png"), "PNG");
346
+ mkdirSync(caeloDir);
347
+ writeFileSync(composePath(caeloDir), "legacy: true\n");
348
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, { ...deps(fakeRunner()), caeloDir });
349
+ expect(r.ok).toBe(true);
350
+ expect(readFileSync(join(caeloDir, "media", "a", "logo.png"), "utf8")).toBe("PNG");
351
+ // The originals stay where they were.
352
+ expect(existsSync(join(root, "apps", "admin", "data", "media", "a", "logo.png"))).toBe(true);
353
+ } finally {
354
+ rmSync(root, { recursive: true, force: true });
355
+ }
356
+ });
357
+ });
358
+
359
+ describe("upgradeSelfHosted — driven by the host agent (#696 Phase 4c)", () => {
360
+ it("changes nothing when the registry serves other digests than the signed manifest names", async () => {
361
+ const runner = fakeRunner();
362
+ const r = await upgradeSelfHosted(
363
+ {
364
+ version: "0.11.0",
365
+ expectedDigests: { admin: NEW_DIGEST.admin, gateway: OLD_DIGEST.gateway },
366
+ },
367
+ deps(runner),
368
+ );
369
+ expect(r).toMatchObject({ ok: false, stage: "verify", previousRunning: true });
370
+ expect(r.ok ? "" : r.error).toContain("gateway");
371
+ expect(steps(runner.calls)).toEqual(["cosign-probe", "verify admin", "verify gateway"]);
372
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
373
+ });
374
+
375
+ it("names its stages for the admin's timeline, in order", async () => {
376
+ const phases: string[] = [];
377
+ const r = await upgradeSelfHosted(
378
+ { version: "0.11.0", expectedDigests: NEW_DIGEST },
379
+ { ...deps(fakeRunner()), log: { ...log, phase: (p) => phases.push(p) } },
380
+ );
381
+ expect(r.ok).toBe(true);
382
+ expect(phases).toEqual([
383
+ "verify",
384
+ "backup",
385
+ "roll",
386
+ "migrations",
387
+ "roll",
388
+ "health",
389
+ "roll",
390
+ "health",
391
+ "record",
392
+ ]);
393
+ });
394
+ });