@caelo-cms/provisioning 0.11.9 → 0.11.10

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 (261) hide show
  1. package/dist/admin-domain.d.ts +10 -1
  2. package/dist/admin-domain.d.ts.map +1 -1
  3. package/dist/admin-domain.js +13 -3
  4. package/dist/admin-domain.js.map +1 -1
  5. package/dist/adopt-existing.d.ts +6 -1
  6. package/dist/adopt-existing.d.ts.map +1 -1
  7. package/dist/adopt-existing.js +86 -0
  8. package/dist/adopt-existing.js.map +1 -1
  9. package/dist/agent/command.d.ts +20 -0
  10. package/dist/agent/command.d.ts.map +1 -1
  11. package/dist/agent/command.js +32 -9
  12. package/dist/agent/command.js.map +1 -1
  13. package/dist/agent/run.d.ts +2 -0
  14. package/dist/agent/run.d.ts.map +1 -1
  15. package/dist/agent/run.js +5 -1
  16. package/dist/agent/run.js.map +1 -1
  17. package/dist/agent-db.js +44 -2
  18. package/dist/aws-task-roles.d.ts +4 -2
  19. package/dist/aws-task-roles.d.ts.map +1 -1
  20. package/dist/aws-task-roles.js +4 -1
  21. package/dist/aws-task-roles.js.map +1 -1
  22. package/dist/caddy.d.ts +21 -4
  23. package/dist/caddy.d.ts.map +1 -1
  24. package/dist/caddy.js +27 -3
  25. package/dist/caddy.js.map +1 -1
  26. package/dist/cli.js +12 -3
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloudsql-tls.d.ts +63 -0
  29. package/dist/cloudsql-tls.d.ts.map +1 -0
  30. package/dist/cloudsql-tls.js +289 -0
  31. package/dist/cloudsql-tls.js.map +1 -0
  32. package/dist/compose.d.ts +29 -6
  33. package/dist/compose.d.ts.map +1 -1
  34. package/dist/compose.js +95 -23
  35. package/dist/compose.js.map +1 -1
  36. package/dist/converge-checkpoint.d.ts +1 -0
  37. package/dist/converge-checkpoint.d.ts.map +1 -1
  38. package/dist/converge-checkpoint.js +8 -1
  39. package/dist/converge-checkpoint.js.map +1 -1
  40. package/dist/converge-job-gcp.d.ts.map +1 -1
  41. package/dist/converge-job-gcp.js +2 -11
  42. package/dist/converge-job-gcp.js.map +1 -1
  43. package/dist/deployed-release.d.ts +4 -1
  44. package/dist/deployed-release.d.ts.map +1 -1
  45. package/dist/deployed-release.js +18 -8
  46. package/dist/deployed-release.js.map +1 -1
  47. package/dist/gcp-names.d.ts +21 -0
  48. package/dist/gcp-names.d.ts.map +1 -1
  49. package/dist/gcp-names.js +24 -0
  50. package/dist/gcp-names.js.map +1 -1
  51. package/dist/gcp-url-map.d.ts.map +1 -1
  52. package/dist/gcp-url-map.js +6 -2
  53. package/dist/gcp-url-map.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/infra-contract.d.ts +1 -1
  59. package/dist/infra-contract.d.ts.map +1 -1
  60. package/dist/infra-contract.js +24 -1
  61. package/dist/infra-contract.js.map +1 -1
  62. package/dist/install-state.d.ts +6 -0
  63. package/dist/install-state.d.ts.map +1 -1
  64. package/dist/install-state.js +17 -4
  65. package/dist/install-state.js.map +1 -1
  66. package/dist/lifecycle.d.ts.map +1 -1
  67. package/dist/lifecycle.js +17 -8
  68. package/dist/lifecycle.js.map +1 -1
  69. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  70. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  71. package/dist/mcp-auth-proxy-setup.js +280 -0
  72. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  73. package/dist/release-manifest.d.ts +25 -1
  74. package/dist/release-manifest.d.ts.map +1 -1
  75. package/dist/release-manifest.js +36 -3
  76. package/dist/release-manifest.js.map +1 -1
  77. package/dist/remote-mcp.d.ts +55 -18
  78. package/dist/remote-mcp.d.ts.map +1 -1
  79. package/dist/remote-mcp.js +307 -77
  80. package/dist/remote-mcp.js.map +1 -1
  81. package/dist/runtime-secrets.d.ts +9 -0
  82. package/dist/runtime-secrets.d.ts.map +1 -1
  83. package/dist/runtime-secrets.js +21 -10
  84. package/dist/runtime-secrets.js.map +1 -1
  85. package/dist/seal-key-rotation.d.ts +7 -7
  86. package/dist/seal-key-rotation.d.ts.map +1 -1
  87. package/dist/seal-key-rotation.js +22 -22
  88. package/dist/seal-key-rotation.js.map +1 -1
  89. package/dist/self-hosted-db-tls.d.ts +37 -0
  90. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  91. package/dist/self-hosted-db-tls.js +220 -0
  92. package/dist/self-hosted-db-tls.js.map +1 -0
  93. package/dist/self-hosted-release.d.ts +3 -1
  94. package/dist/self-hosted-release.d.ts.map +1 -1
  95. package/dist/self-hosted-release.js +33 -9
  96. package/dist/self-hosted-release.js.map +1 -1
  97. package/dist/self-hosted-roles.d.ts +34 -0
  98. package/dist/self-hosted-roles.d.ts.map +1 -1
  99. package/dist/self-hosted-roles.js +52 -0
  100. package/dist/self-hosted-roles.js.map +1 -1
  101. package/dist/self-hosted-start.d.ts +0 -21
  102. package/dist/self-hosted-start.d.ts.map +1 -1
  103. package/dist/self-hosted-start.js +39 -1
  104. package/dist/self-hosted-start.js.map +1 -1
  105. package/dist/self-hosted-upgrade.d.ts +15 -6
  106. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  107. package/dist/self-hosted-upgrade.js +72 -23
  108. package/dist/self-hosted-upgrade.js.map +1 -1
  109. package/dist/stack-contract.d.ts +170 -25
  110. package/dist/stack-contract.d.ts.map +1 -1
  111. package/dist/stack-contract.js +314 -37
  112. package/dist/stack-contract.js.map +1 -1
  113. package/dist/stack-converge.d.ts +31 -0
  114. package/dist/stack-converge.d.ts.map +1 -1
  115. package/dist/stack-converge.js +71 -30
  116. package/dist/stack-converge.js.map +1 -1
  117. package/dist/upgrade/app-roll.d.ts +25 -4
  118. package/dist/upgrade/app-roll.d.ts.map +1 -1
  119. package/dist/upgrade/app-roll.js +63 -17
  120. package/dist/upgrade/app-roll.js.map +1 -1
  121. package/dist/upgrade/converge.d.ts.map +1 -1
  122. package/dist/upgrade/converge.js +99 -12
  123. package/dist/upgrade/converge.js.map +1 -1
  124. package/dist/upgrade/health.d.ts +31 -7
  125. package/dist/upgrade/health.d.ts.map +1 -1
  126. package/dist/upgrade/health.js +41 -17
  127. package/dist/upgrade/health.js.map +1 -1
  128. package/dist/upgrade/image-forms.d.ts +1 -1
  129. package/dist/upgrade/image-forms.d.ts.map +1 -1
  130. package/dist/upgrade/image-forms.js +24 -7
  131. package/dist/upgrade/image-forms.js.map +1 -1
  132. package/dist/upgrade/release-images.d.ts +12 -3
  133. package/dist/upgrade/release-images.d.ts.map +1 -1
  134. package/dist/upgrade/release-images.js +13 -4
  135. package/dist/upgrade/release-images.js.map +1 -1
  136. package/dist/upgrade/upgrade.d.ts +13 -3
  137. package/dist/upgrade/upgrade.d.ts.map +1 -1
  138. package/dist/upgrade/upgrade.js +34 -2
  139. package/dist/upgrade/upgrade.js.map +1 -1
  140. package/dist/upgrader-gcp.d.ts +12 -0
  141. package/dist/upgrader-gcp.d.ts.map +1 -1
  142. package/dist/upgrader-gcp.js +49 -1
  143. package/dist/upgrader-gcp.js.map +1 -1
  144. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  145. package/dist/upgrader-iam-preflight.js +6 -1
  146. package/dist/upgrader-iam-preflight.js.map +1 -1
  147. package/dist/upgrader-job.d.ts +1 -1
  148. package/dist/upgrader-job.d.ts.map +1 -1
  149. package/dist/upgrader-job.js +20 -14
  150. package/dist/upgrader-job.js.map +1 -1
  151. package/dist/upgrader-records.d.ts +1 -1
  152. package/dist/upgrader-records.d.ts.map +1 -1
  153. package/dist/upgrader-records.js +26 -7
  154. package/dist/upgrader-records.js.map +1 -1
  155. package/dist/upgrader-setup.d.ts.map +1 -1
  156. package/dist/upgrader-setup.js +10 -5
  157. package/dist/upgrader-setup.js.map +1 -1
  158. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  159. package/dist/wizards/gcp-cost.js +22 -7
  160. package/dist/wizards/gcp-cost.js.map +1 -1
  161. package/dist/wizards/gcp.d.ts.map +1 -1
  162. package/dist/wizards/gcp.js +43 -17
  163. package/dist/wizards/gcp.js.map +1 -1
  164. package/package.json +2 -2
  165. package/src/admin-domain.test.ts +9 -8
  166. package/src/admin-domain.ts +13 -3
  167. package/src/admin-memory.test.ts +1 -0
  168. package/src/adopt-existing.test.ts +39 -0
  169. package/src/adopt-existing.ts +83 -1
  170. package/src/agent/command.test.ts +49 -1
  171. package/src/agent/command.ts +36 -9
  172. package/src/agent/run.test.ts +19 -0
  173. package/src/agent/run.ts +11 -2
  174. package/src/aws-task-roles.ts +5 -2
  175. package/src/caddy-redirects.docker.test.ts +2 -0
  176. package/src/caddy-redirects.test.ts +3 -0
  177. package/src/caddy-validate.test.ts +7 -1
  178. package/src/caddy.test.ts +39 -1
  179. package/src/caddy.ts +36 -4
  180. package/src/checkpoint-secrets.test.ts +14 -6
  181. package/src/cli.ts +12 -1
  182. package/src/cloudsql-tls.test.ts +225 -0
  183. package/src/cloudsql-tls.ts +352 -0
  184. package/src/compose.ts +125 -24
  185. package/src/converge-checkpoint.ts +8 -1
  186. package/src/converge-job-gcp.ts +2 -11
  187. package/src/database-tls-builders.test.ts +55 -0
  188. package/src/deployed-release.test.ts +104 -40
  189. package/src/deployed-release.ts +30 -9
  190. package/src/gateway-env-isolation.test.ts +10 -5
  191. package/src/gcp-names.ts +31 -0
  192. package/src/gcp-url-map.test.ts +6 -8
  193. package/src/gcp-url-map.ts +6 -2
  194. package/src/index.test.ts +7 -1
  195. package/src/index.ts +1 -0
  196. package/src/infra-contract.test.ts +5 -3
  197. package/src/infra-contract.ts +24 -1
  198. package/src/install-state.ts +26 -4
  199. package/src/lifecycle.ts +20 -11
  200. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  201. package/src/mcp-auth-proxy-setup.ts +374 -0
  202. package/src/mcp-iap-stacks.test.ts +1 -1
  203. package/src/media-volume.test.ts +1 -1
  204. package/src/operator-access-stacks.test.ts +1 -1
  205. package/src/operator-access.test.ts +5 -4
  206. package/src/pool-budget-stacks.test.ts +1 -1
  207. package/src/release-manifest.test.ts +25 -0
  208. package/src/release-manifest.ts +59 -4
  209. package/src/remote-mcp-stack-config.test.ts +19 -9
  210. package/src/remote-mcp.test.ts +252 -57
  211. package/src/remote-mcp.ts +336 -77
  212. package/src/runtime-secrets.test.ts +20 -7
  213. package/src/runtime-secrets.ts +50 -9
  214. package/src/seal-key-rotation.test.ts +2 -2
  215. package/src/seal-key-rotation.ts +25 -25
  216. package/src/self-hosted-compose.test.ts +108 -5
  217. package/src/self-hosted-db-tls.test.ts +111 -0
  218. package/src/self-hosted-db-tls.ts +287 -0
  219. package/src/self-hosted-release-resolve.test.ts +71 -0
  220. package/src/self-hosted-release.ts +41 -11
  221. package/src/self-hosted-roles.test.ts +54 -0
  222. package/src/self-hosted-roles.ts +58 -0
  223. package/src/self-hosted-start.test.ts +34 -2
  224. package/src/self-hosted-start.ts +20 -2
  225. package/src/self-hosted-upgrade.test.ts +103 -14
  226. package/src/self-hosted-upgrade.ts +115 -29
  227. package/src/stack-contract-parity.test.ts +79 -12
  228. package/src/stack-contract.test.ts +169 -29
  229. package/src/stack-contract.ts +362 -42
  230. package/src/stack-converge-secrets.test.ts +26 -13
  231. package/src/stack-converge.test.ts +199 -26
  232. package/src/stack-converge.ts +92 -30
  233. package/src/state-store.test.ts +2 -1
  234. package/src/upgrade/app-roll.ts +97 -19
  235. package/src/upgrade/converge.ts +120 -11
  236. package/src/upgrade/health.test.ts +63 -25
  237. package/src/upgrade/health.ts +54 -22
  238. package/src/upgrade/image-forms.test.ts +34 -0
  239. package/src/upgrade/image-forms.ts +23 -8
  240. package/src/upgrade/release-images.test.ts +5 -1
  241. package/src/upgrade/release-images.ts +22 -6
  242. package/src/upgrade/upgrade.test.ts +353 -44
  243. package/src/upgrade/upgrade.ts +55 -6
  244. package/src/upgrader-gcp.ts +65 -3
  245. package/src/upgrader-iam-preflight.test.ts +19 -0
  246. package/src/upgrader-iam-preflight.ts +6 -0
  247. package/src/upgrader-job.test.ts +101 -27
  248. package/src/upgrader-job.ts +27 -14
  249. package/src/upgrader-records.test.ts +97 -12
  250. package/src/upgrader-records.ts +28 -11
  251. package/src/upgrader-setup.test.ts +2 -0
  252. package/src/upgrader-setup.ts +10 -4
  253. package/src/wizards/gcp-cost.test.ts +50 -0
  254. package/src/wizards/gcp-cost.ts +24 -7
  255. package/src/wizards/gcp.ts +55 -14
  256. package/stacks/aws/index.ts +32 -14
  257. package/stacks/azure/index.ts +57 -18
  258. package/stacks/gcp/index.ts +83 -23
  259. package/stacks/gcp-firebase/index.ts +162 -33
  260. package/stacks/self-hosted/README.md +2 -1
  261. package/stacks/self-hosted/index.ts +11 -1
@@ -7,14 +7,18 @@ import { startSelfHosted } from "./self-hosted-start.js";
7
7
  const DIR = "/srv/install/.caelo";
8
8
 
9
9
  /** A runner that records every command (minus the `docker compose -f <file>` prefix). */
10
- function fakeRunner(fail?: (cmd: string) => boolean): HostRunner & { calls: string[] } {
10
+ function fakeRunner(
11
+ fail?: (cmd: string) => boolean,
12
+ services = "postgres\ncaddy\ncaelo-admin\ncaelo-gateway\n",
13
+ ): HostRunner & { calls: string[] } {
11
14
  const calls: string[] = [];
12
15
  return {
13
16
  calls,
14
17
  async run(argv): Promise<CommandResult> {
15
18
  const cmd = argv.join(" ").replace(`docker compose -f ${DIR}/docker-compose.yml `, "");
16
19
  calls.push(cmd);
17
- return { exitCode: fail?.(cmd) ? 1 : 0, stdout: "", stderr: fail?.(cmd) ? "boom" : "" };
20
+ const stdout = cmd === "config --services" ? services : "";
21
+ return { exitCode: fail?.(cmd) ? 1 : 0, stdout, stderr: fail?.(cmd) ? "boom" : "" };
18
22
  },
19
23
  };
20
24
  }
@@ -33,12 +37,40 @@ describe("startSelfHosted (#707 — one command ends with a serving install)", (
33
37
  `${MIGRATE} admin`,
34
38
  `${MIGRATE} public`,
35
39
  "up -d --remove-orphans",
40
+ "config --services",
36
41
  "exec -T caelo-admin bun -e <probe>",
37
42
  "exec -T caelo-gateway bun -e <probe>",
38
43
  "exec -T caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile",
39
44
  ]);
40
45
  });
41
46
 
47
+ it("#1328 — health-checks the MCP-Auth-Proxy too when the release runs one", async () => {
48
+ const runner = fakeRunner(undefined, "caelo-admin\ncaelo-gateway\ncaelo-mcp-auth-proxy\n");
49
+ const r = await startSelfHosted({ caeloDir: DIR, runner, log: () => {} });
50
+ expect(r).toEqual({ ok: true });
51
+ const probe = runner.calls.find((c) => c.startsWith("exec -T caelo-mcp-auth-proxy bun -e "));
52
+ expect(probe).toContain("/api/mcp/health");
53
+ });
54
+
55
+ it("#1328 — reports an unhealthy MCP-Auth-Proxy with where to look", async () => {
56
+ const runner = fakeRunner(
57
+ (c) => c.startsWith("exec -T caelo-mcp-auth-proxy"),
58
+ "caelo-admin\ncaelo-gateway\ncaelo-mcp-auth-proxy\n",
59
+ );
60
+ const r = await startSelfHosted({
61
+ caeloDir: DIR,
62
+ runner,
63
+ log: () => {},
64
+ healthAttempts: 1,
65
+ sleep: async () => {},
66
+ });
67
+ expect(r.ok).toBe(false);
68
+ if (!r.ok) {
69
+ expect(r.stage).toBe("health");
70
+ expect(r.error).toContain("logs caelo-mcp-auth-proxy");
71
+ }
72
+ });
73
+
42
74
  it("stops at a failed migration — nothing else is started", async () => {
43
75
  const runner = fakeRunner((c) => c.endsWith("migrate.ts admin"));
44
76
  const r = await startSelfHosted({ caeloDir: DIR, runner, log: () => {} });
@@ -14,7 +14,8 @@
14
14
  * 4. start everything (`--remove-orphans` drops services an older Compose
15
15
  * file had, e.g. the pgBackRest sidecar);
16
16
  * 5. health-check the admin (`/_caelo/health` proves the DB pool and its
17
- * role) and the gateway;
17
+ * role), the gateway and — when the release has one — the
18
+ * MCP-Auth-Proxy (#1328);
18
19
  * 6. reload Caddy — Compose does not restart a running Caddy when only
19
20
  * its bind-mounted Caddyfile changed, so a re-run `up` would otherwise
20
21
  * keep serving the old vhosts.
@@ -22,8 +23,10 @@
22
23
  * stopped.
23
24
  */
24
25
 
26
+ import { COMPOSE_MCP_AUTH_PROXY_SERVICE } from "./compose.js";
25
27
  import type { HostRunner } from "./self-hosted-release.js";
26
28
  import { compose, runMigrations, waitHealthy } from "./self-hosted-upgrade.js";
29
+ import type { ReleaseService } from "./upgrade/release-images.js";
27
30
 
28
31
  export interface StartDeps {
29
32
  readonly caeloDir: string;
@@ -71,7 +74,22 @@ export async function startSelfHosted(deps: StartDeps): Promise<StartResult> {
71
74
  });
72
75
  if (up.exitCode !== 0) return fail("start", "docker compose up failed (see above).");
73
76
 
74
- for (const service of ["admin", "gateway"] as const) {
77
+ // #1328 — the MCP-Auth-Proxy too, when the release this file runs has one
78
+ // (a release from before it has none; Compose knows which).
79
+ const listed = await runner.run(compose(caeloDir, "config", "--services"));
80
+ if (listed.exitCode !== 0) {
81
+ return fail("start", `docker compose config --services failed: ${listed.stderr.trim()}`);
82
+ }
83
+ const runsProxy = listed.stdout
84
+ .split("\n")
85
+ .map((s) => s.trim())
86
+ .includes(COMPOSE_MCP_AUTH_PROXY_SERVICE);
87
+ const services: ReleaseService[] = [
88
+ "admin",
89
+ "gateway",
90
+ ...(runsProxy ? (["mcp-auth-proxy"] as const) : []),
91
+ ];
92
+ for (const service of services) {
75
93
  log(`Waiting for the ${service} to answer its health check …`);
76
94
  const healthy = await waitHealthy(service, caeloDir, {
77
95
  runner,
@@ -27,24 +27,34 @@ import {
27
27
  upgradeSelfHosted,
28
28
  } from "./self-hosted-upgrade.js";
29
29
 
30
+ // OLD is a release from before #1328: it has no MCP-Auth-Proxy image.
30
31
  const OLD_DIGEST = { admin: `sha256:${"1".repeat(64)}`, gateway: `sha256:${"2".repeat(64)}` };
31
32
  const OLD: ReleaseImages = {
32
33
  mode: "release",
33
34
  admin: `ghcr.io/caelo-cms/admin@${OLD_DIGEST.admin}`,
34
35
  gateway: `ghcr.io/caelo-cms/gateway@${OLD_DIGEST.gateway}`,
35
36
  };
36
- const NEW_DIGEST = { admin: `sha256:${"a".repeat(64)}`, gateway: `sha256:${"b".repeat(64)}` };
37
+ const NEW_DIGEST = {
38
+ admin: `sha256:${"a".repeat(64)}`,
39
+ gateway: `sha256:${"b".repeat(64)}`,
40
+ "mcp-auth-proxy": `sha256:${"c".repeat(64)}`,
41
+ };
37
42
  const NEW: ReleaseImages = {
38
43
  mode: "release",
39
44
  admin: `ghcr.io/caelo-cms/admin@${NEW_DIGEST.admin}`,
40
45
  gateway: `ghcr.io/caelo-cms/gateway@${NEW_DIGEST.gateway}`,
46
+ "mcp-auth-proxy": `ghcr.io/caelo-cms/mcp-auth-proxy@${NEW_DIGEST["mcp-auth-proxy"]}`,
41
47
  };
42
48
 
43
49
  /** A stand-in for generateDockerCompose: just the two image lines. */
44
50
  const render = (images: ReleaseImages) => `admin: ${images.admin}\ngateway: ${images.gateway}\n`;
45
51
 
46
52
  /** GHCR fake: the anonymous token dance, then `digests` per repository. */
47
- function registryServing(digests: { admin: string; gateway: string }): typeof fetch {
53
+ function registryServing(digests: {
54
+ admin: string;
55
+ gateway: string;
56
+ "mcp-auth-proxy"?: string;
57
+ }): typeof fetch {
48
58
  return (async (input: string | URL | Request, init?: RequestInit) => {
49
59
  const url = String(input);
50
60
  if (url.startsWith("https://api.github.com/")) return Response.json({ tag_name: "v0.11.0" });
@@ -55,11 +65,14 @@ function registryServing(digests: { admin: string; gateway: string }): typeof fe
55
65
  headers: { "www-authenticate": 'Bearer realm="https://ghcr.io/token",service="ghcr.io"' },
56
66
  });
57
67
  }
58
- const service = url.includes("/caelo-cms/admin/") ? "admin" : "gateway";
59
- return new Response(null, {
60
- status: 200,
61
- headers: { "docker-content-digest": digests[service] },
62
- });
68
+ const service = url.includes("/caelo-cms/admin/")
69
+ ? "admin"
70
+ : url.includes("/caelo-cms/mcp-auth-proxy/")
71
+ ? "mcp-auth-proxy"
72
+ : "gateway";
73
+ const digest = digests[service];
74
+ if (!digest) return new Response(null, { status: 404, statusText: "Not Found" });
75
+ return new Response(null, { status: 200, headers: { "docker-content-digest": digest } });
63
76
  }) as typeof fetch;
64
77
  }
65
78
  const registry = registryServing(NEW_DIGEST);
@@ -86,17 +99,20 @@ function fakeRunner(rules: Rule[] = []): HostRunner & { calls: string[] } {
86
99
  function steps(calls: readonly string[]): string[] {
87
100
  return calls.map((c) => {
88
101
  if (c.startsWith("cosign verify"))
89
- return `verify ${c.includes("/admin@") ? "admin" : "gateway"}`;
102
+ return `verify ${c.includes("/admin@") ? "admin" : c.includes("/mcp-auth-proxy@") ? "mcp-auth-proxy" : "gateway"}`;
90
103
  if (c.startsWith("cosign")) return "cosign-probe";
91
104
  if (c.includes("pg_isready")) return "pg_isready";
92
105
  if (c.includes("pg_dumpall")) return "dump globals";
93
106
  if (c.includes("pg_dump ")) return `dump ${c.split(" ").at(-1)}`;
94
107
  if (c.includes(" pull ")) return "pull";
95
108
  if (c.includes("migrate.ts")) return `migrate ${c.split(" ").at(-1)}`;
109
+ if (c.includes(" up -d --no-deps caelo-admin caelo-gateway caelo-mcp-auth-proxy"))
110
+ return "up all";
96
111
  if (c.includes(" up -d --no-deps caelo-admin caelo-gateway")) return "up both";
97
112
  if (c.includes(" up -d --no-deps ")) return `up ${c.split(" ").at(-1)}`;
98
113
  if (c.includes("/_caelo/health")) return "health admin";
99
114
  if (c.includes("/healthz")) return "health gateway";
115
+ if (c.includes("/api/mcp/health")) return "health mcp-auth-proxy";
100
116
  return c;
101
117
  });
102
118
  }
@@ -135,6 +151,10 @@ function deps(
135
151
  runner.calls.push("converge roles");
136
152
  return roles;
137
153
  },
154
+ applyPublicRoleLimits: async () => {
155
+ runner.calls.push("public role limits");
156
+ return { ok: true } as const;
157
+ },
138
158
  refreshCaddy: async () => {
139
159
  runner.calls.push("refresh caddy");
140
160
  return caddy;
@@ -157,11 +177,14 @@ describe("upgradeSelfHosted", () => {
157
177
  "cosign-probe",
158
178
  "verify admin",
159
179
  "verify gateway",
180
+ "verify mcp-auth-proxy",
160
181
  "pg_isready",
161
182
  "dump globals",
162
183
  "dump cms_admin",
163
184
  "dump cms_public",
164
185
  "pull",
186
+ "up postgres",
187
+ "public role limits",
165
188
  "migrate admin",
166
189
  "migrate public",
167
190
  "converge roles",
@@ -169,13 +192,20 @@ describe("upgradeSelfHosted", () => {
169
192
  "health admin",
170
193
  "up caelo-gateway",
171
194
  "health gateway",
195
+ // #1328 — the upgrade from a release without the proxy creates it.
196
+ "up caelo-mcp-auth-proxy",
197
+ "health mcp-auth-proxy",
172
198
  "refresh caddy",
173
199
  ]);
174
200
  // The migrations run the NEW admin image: the Compose file already names it.
175
201
  expect(readFileSync(composePath(dir), "utf8")).toBe(render(NEW));
176
202
  const release = readRelease(dir);
177
203
  expect(release?.version).toBe("0.11.0");
178
- expect(release?.images).toEqual({ admin: NEW.admin, gateway: NEW.gateway });
204
+ expect(release?.images).toEqual({
205
+ admin: NEW.admin,
206
+ gateway: NEW.gateway,
207
+ "mcp-auth-proxy": NEW["mcp-auth-proxy"],
208
+ });
179
209
  expect(release?.history.at(-1)).toMatchObject({
180
210
  from: "0.10.0",
181
211
  to: "0.11.0",
@@ -206,9 +236,11 @@ describe("upgradeSelfHosted", () => {
206
236
  "cosign-probe",
207
237
  "verify admin",
208
238
  "verify gateway",
209
- "up both",
239
+ "verify mcp-auth-proxy",
240
+ "up all",
210
241
  "health admin",
211
242
  "health gateway",
243
+ "health mcp-auth-proxy",
212
244
  "refresh caddy",
213
245
  ]);
214
246
  });
@@ -257,7 +289,13 @@ describe("upgradeSelfHosted", () => {
257
289
  { version: "0.10.0" },
258
290
  {
259
291
  ...deps(runner),
260
- fetchImpl: registryServing({ admin: OLD_DIGEST.admin, gateway: OLD_DIGEST.gateway }),
292
+ // The target release ships a proxy image; the fake render prints
293
+ // only admin + gateway, so the file still reads as OLD.
294
+ fetchImpl: registryServing({
295
+ admin: OLD_DIGEST.admin,
296
+ gateway: OLD_DIGEST.gateway,
297
+ "mcp-auth-proxy": NEW_DIGEST["mcp-auth-proxy"],
298
+ }),
261
299
  },
262
300
  );
263
301
  expect(r).toEqual({ ok: true, status: "upgraded", version: "0.10.0" });
@@ -305,7 +343,9 @@ describe("upgradeSelfHosted", () => {
305
343
  ]);
306
344
  const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
307
345
  expect(r).toMatchObject({ ok: false, stage: "migrate", previousRunning: true });
308
- expect(steps(runner.calls).some((s) => s.startsWith("up "))).toBe(false);
346
+ // Postgres moved onto this release's TLS config before the migrations
347
+ // (it serves the previous release too); no Caelo service restarted.
348
+ expect(steps(runner.calls).some((s) => s.startsWith("up caelo"))).toBe(false);
309
349
  expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
310
350
  });
311
351
 
@@ -325,7 +365,9 @@ describe("upgradeSelfHosted", () => {
325
365
  );
326
366
  expect(r).toMatchObject({ ok: false, stage: "roles", previousRunning: true });
327
367
  if (!r.ok) expect(r.error).toContain("role is busy");
328
- expect(steps(runner.calls).some((x) => x.startsWith("up "))).toBe(false);
368
+ // Postgres moved onto this release's TLS config before the migrations
369
+ // (it serves the previous release too); no Caelo service restarted.
370
+ expect(steps(runner.calls).some((x) => x.startsWith("up caelo"))).toBe(false);
329
371
  expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
330
372
  expect(readRelease(dir)?.version).toBe("0.10.0");
331
373
  });
@@ -359,6 +401,23 @@ describe("upgradeSelfHosted", () => {
359
401
  expect(release?.history.at(-1)).toMatchObject({ to: "0.11.0", outcome: "rolled-back" });
360
402
  });
361
403
 
404
+ it("rolls back to a release without the proxy when the new MCP-Auth-Proxy is unhealthy (#1328)", async () => {
405
+ const runner = fakeRunner([
406
+ (c) => (c.includes("/api/mcp/health") ? { exitCode: 1 } : undefined),
407
+ ]);
408
+ const r = await upgradeSelfHosted({ version: "0.11.0" }, deps(runner));
409
+ expect(r).toMatchObject({ ok: false, stage: "roll-mcp-auth-proxy", previousRunning: true });
410
+ const s = steps(runner.calls);
411
+ // OLD predates the proxy: the rollback starts and checks only admin + gateway.
412
+ expect(s.slice(s.lastIndexOf("up both"))).toEqual([
413
+ "up both",
414
+ "health admin",
415
+ "health gateway",
416
+ ]);
417
+ expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
418
+ expect(readRelease(dir)?.images["mcp-auth-proxy"]).toBeUndefined();
419
+ });
420
+
362
421
  it("reports a failed rollback loudly", async () => {
363
422
  const runner = fakeRunner([
364
423
  (c) => (c.includes("/_caelo/health") ? { exitCode: 1 } : undefined),
@@ -429,10 +488,38 @@ describe("upgradeSelfHosted — driven by the host agent (#696 Phase 4c)", () =>
429
488
  );
430
489
  expect(r).toMatchObject({ ok: false, stage: "verify", previousRunning: true });
431
490
  expect(r.ok ? "" : r.error).toContain("gateway");
432
- expect(steps(runner.calls)).toEqual(["cosign-probe", "verify admin", "verify gateway"]);
491
+ expect(steps(runner.calls)).toEqual([
492
+ "cosign-probe",
493
+ "verify admin",
494
+ "verify gateway",
495
+ "verify mcp-auth-proxy",
496
+ ]);
433
497
  expect(readFileSync(composePath(dir), "utf8")).toBe(render(OLD));
434
498
  });
435
499
 
500
+ it("refuses a registry that serves another MCP-Auth-Proxy than the manifest names (#1328)", async () => {
501
+ const r = await upgradeSelfHosted(
502
+ {
503
+ version: "0.11.0",
504
+ expectedDigests: { ...NEW_DIGEST, "mcp-auth-proxy": `sha256:${"d".repeat(64)}` },
505
+ },
506
+ deps(fakeRunner()),
507
+ );
508
+ expect(r).toMatchObject({ ok: false, stage: "verify", previousRunning: true });
509
+ expect(r.ok ? "" : r.error).toContain("mcp-auth-proxy");
510
+ });
511
+
512
+ it("checks only admin + gateway for an agent that names no proxy digest", async () => {
513
+ const r = await upgradeSelfHosted(
514
+ {
515
+ version: "0.11.0",
516
+ expectedDigests: { admin: NEW_DIGEST.admin, gateway: NEW_DIGEST.gateway },
517
+ },
518
+ deps(fakeRunner()),
519
+ );
520
+ expect(r.ok).toBe(true);
521
+ });
522
+
436
523
  it("names its stages for the admin's timeline, in order", async () => {
437
524
  const phases: string[] = [];
438
525
  const r = await upgradeSelfHosted(
@@ -449,6 +536,8 @@ describe("upgradeSelfHosted — driven by the host agent (#696 Phase 4c)", () =>
449
536
  "health",
450
537
  "roll",
451
538
  "health",
539
+ "roll",
540
+ "health",
452
541
  "record",
453
542
  ]);
454
543
  });
@@ -9,6 +9,12 @@
9
9
  * 2. back up both databases (pg_dump, hard-fail) — the last moment the
10
10
  * schema is guaranteed to match the running images;
11
11
  * 3. write the new digests into the Compose file, `docker compose pull`;
12
+ * 3b. #1338 — recreate Postgres when the Compose file changed it (TLS with
13
+ * the install's own certificate, self-hosted-db-tls.ts: the new images
14
+ * connect with verify-full only; the old containers keep connecting,
15
+ * Postgres still accepts them) and apply public_role's limits as the
16
+ * superuser (the migrations' role audit refuses public_role without
17
+ * them, and the self-hosted admin_role cannot set them);
12
18
  * 4. run the migrations from the NEW admin image (`docker compose run`,
13
19
  * the same `migrate.ts` entrypoint the GCP migrate job runs) while the
14
20
  * old containers keep serving — migrations are expand-only, so the
@@ -20,7 +26,9 @@
20
26
  * refuses an install that skipped the release the old role's last user
21
27
  * was retired in (run-order.ts): past it, nothing running uses it;
22
28
  * 6. recreate the admin, health-check it (`/_caelo/health`, which proves
23
- * the DB pool and roles); then the gateway (`/healthz`);
29
+ * the DB pool and roles); then the gateway (`/healthz`); then the
30
+ * MCP-Auth-Proxy (`/api/mcp/health`, #1328) — on the upgrade that first
31
+ * brings it, this creates its container;
24
32
  * 7. re-write the Caddyfile and reload Caddy — the edge headers live there
25
33
  * (#891), and only the new release's generator knows them;
26
34
  * 8. record the release in `.caelo/release.json`.
@@ -50,7 +58,13 @@ import {
50
58
  } from "node:fs";
51
59
  import { join } from "node:path";
52
60
  import type { PlatformUpdatePhase } from "@caelo-cms/shared";
53
- import { ghcrImageDigest, MEDIA_DIR, type ReleaseImages } from "./compose.js";
61
+ import {
62
+ COMPOSE_MCP_AUTH_PROXY_PORT,
63
+ COMPOSE_MCP_AUTH_PROXY_SERVICE,
64
+ ghcrImageDigest,
65
+ MEDIA_DIR,
66
+ type ReleaseImages,
67
+ } from "./compose.js";
54
68
  import { selfCommand } from "./self-command.js";
55
69
  import {
56
70
  type HostRunner,
@@ -61,10 +75,28 @@ import {
61
75
  resolveVerifiedRelease,
62
76
  } from "./self-hosted-release.js";
63
77
  import type { ConvergeOutcome } from "./self-hosted-roles.js";
64
- import { fetchLatestReleaseTag, RELEASE_SERVICES, releaseTag } from "./upgrade/release-images.js";
78
+ import {
79
+ fetchLatestReleaseTag,
80
+ RELEASE_SERVICES,
81
+ type ReleaseService,
82
+ releaseTag,
83
+ } from "./upgrade/release-images.js";
84
+
85
+ /** The Compose service names of the Caelo services. */
86
+ const COMPOSE_SERVICE: Readonly<Record<ReleaseService, string>> = {
87
+ admin: "caelo-admin",
88
+ gateway: "caelo-gateway",
89
+ "mcp-auth-proxy": COMPOSE_MCP_AUTH_PROXY_SERVICE,
90
+ };
65
91
 
66
- /** The Compose service names of the two Caelo services. */
67
- const COMPOSE_SERVICE = { admin: "caelo-admin", gateway: "caelo-gateway" } as const;
92
+ /**
93
+ * The release services a Compose file for `images` runs, in roll order: all
94
+ * of them, except the MCP-Auth-Proxy for a release recorded before it
95
+ * shipped (#1328, compose.ts ReleaseImages).
96
+ */
97
+ function servicesOf(images: ReleaseImages): ReleaseService[] {
98
+ return RELEASE_SERVICES.filter((s) => images[s] !== undefined);
99
+ }
68
100
 
69
101
  /** Progress output; the CLI wires it to the terminal, tests to an array. */
70
102
  export interface UpgradeLog {
@@ -92,6 +124,12 @@ export interface SelfHostedUpgradeDeps {
92
124
  * (`convergeSelfHostedRoles`); drops roles a release retired (#635).
93
125
  */
94
126
  readonly convergeRoles: () => Promise<ConvergeOutcome>;
127
+ /**
128
+ * #1338 — applies public_role's limits as the Postgres superuser
129
+ * (`applyPublicRoleLimits`), before the migrations whose role audit
130
+ * checks them.
131
+ */
132
+ readonly applyPublicRoleLimits: () => Promise<ConvergeOutcome>;
95
133
  /**
96
134
  * Re-writes the Caddyfile from the install's config + domains table and
97
135
  * reloads Caddy (#891): the edge headers (the SVG CSP, nosniff) live in
@@ -112,12 +150,17 @@ export interface SelfHostedUpgradeOpts {
112
150
  readonly channel?: "stable" | "rc" | "beta";
113
151
  readonly skipVerify?: boolean;
114
152
  /**
115
- * The admin + gateway digests the release's signed manifest names (the
116
- * host agent passes them). The digests GHCR serves for the tag must match,
117
- * or nothing changes — a registry cannot swap in another signed image of
118
- * an older release under the new tag.
153
+ * The digests the release's signed manifest names (the host agent passes
154
+ * them). The digests GHCR serves for the tag must match, or nothing
155
+ * changes — a registry cannot swap in another signed image of an older
156
+ * release under the new tag. The MCP-Auth-Proxy's is compared whenever it
157
+ * is given (#1328; an agent from before it passes only admin + gateway).
119
158
  */
120
- readonly expectedDigests?: { readonly admin: string; readonly gateway: string };
159
+ readonly expectedDigests?: {
160
+ readonly admin: string;
161
+ readonly gateway: string;
162
+ readonly "mcp-auth-proxy"?: string;
163
+ };
121
164
  }
122
165
 
123
166
  export type SelfHostedUpgradeResult =
@@ -140,6 +183,7 @@ type UpgradeStage =
140
183
  | "roles"
141
184
  | "roll-admin"
142
185
  | "roll-gateway"
186
+ | "roll-mcp-auth-proxy"
143
187
  | "caddy";
144
188
 
145
189
  /** Where an install from before #696 keeps its original Compose file during the move. */
@@ -247,6 +291,10 @@ export async function upgradeSelfHosted(
247
291
  const previousCompose = current
248
292
  ? deps.renderCompose(releaseImages(current))
249
293
  : legacyComposeCheckpoint(caeloDir, live);
294
+ // The legacy (repo-mounted) file predates the MCP-Auth-Proxy.
295
+ const previousServices: readonly ReleaseService[] = current
296
+ ? servicesOf(releaseImages(current))
297
+ : ["admin", "gateway"];
250
298
 
251
299
  // 1. Target release → verified digests.
252
300
  log.phase?.("verify");
@@ -265,8 +313,9 @@ export async function upgradeSelfHosted(
265
313
  if (opts.skipVerify) log.warn("--skip-verify set — image signatures NOT verified.");
266
314
  const images = resolved.images;
267
315
  if (opts.expectedDigests) {
316
+ const expected = opts.expectedDigests;
268
317
  const mismatch = RELEASE_SERVICES.filter(
269
- (s) => ghcrImageDigest(s, images[s]) !== opts.expectedDigests?.[s],
318
+ (s) => expected[s] !== undefined && ghcrImageDigest(s, images[s] ?? "") !== expected[s],
270
319
  );
271
320
  if (mismatch.length > 0) {
272
321
  return fail(
@@ -278,8 +327,7 @@ export async function upgradeSelfHosted(
278
327
  }
279
328
  if (
280
329
  current &&
281
- current.images.admin === images.admin &&
282
- current.images.gateway === images.gateway &&
330
+ RELEASE_SERVICES.every((s) => current.images[s] === images[s]) &&
283
331
  live === previousCompose
284
332
  ) {
285
333
  // Recorded, and the Compose file agrees. `up` converges containers a
@@ -289,7 +337,7 @@ export async function upgradeSelfHosted(
289
337
  log.info(
290
338
  `Already on ${current.version}; checking the running containers match and are healthy.`,
291
339
  );
292
- const converged = await startBoth(deps);
340
+ const converged = await startAll(deps, servicesOf(images));
293
341
  if (!converged.ok) {
294
342
  return fail(
295
343
  "converge",
@@ -328,13 +376,41 @@ export async function upgradeSelfHosted(
328
376
  log.phase?.("roll");
329
377
  log.step("Pulling the new images");
330
378
  const pull = await runner.run(
331
- compose(caeloDir, "pull", COMPOSE_SERVICE.admin, COMPOSE_SERVICE.gateway),
379
+ compose(caeloDir, "pull", ...servicesOf(images).map((s) => COMPOSE_SERVICE[s])),
332
380
  );
333
381
  if (pull.exitCode !== 0) {
334
382
  restoreCompose(file, previousCompose);
335
383
  return fail("pull", `docker compose pull failed: ${tail(pull.stderr)}`, true);
336
384
  }
337
385
 
386
+ // 3b. Postgres on this release's config (TLS, #1338). `up -d` recreates
387
+ // it only when its definition changed; `--wait` returns once it is
388
+ // healthy. The data volume stays; the old admin and gateway reconnect.
389
+ log.step("Bringing Postgres onto this release's configuration (TLS)");
390
+ const pg = await runner.run(compose(caeloDir, "up", "-d", "--no-deps", "--wait", "postgres"));
391
+ if (pg.exitCode !== 0) {
392
+ restoreCompose(file, previousCompose);
393
+ const back = await runner.run(compose(caeloDir, "up", "-d", "--no-deps", "--wait", "postgres"));
394
+ return fail(
395
+ "pull",
396
+ `Postgres did not come up on the new configuration: ${tail(pg.stderr)}\n${
397
+ back.exitCode === 0
398
+ ? "Postgres runs on the previous configuration again; nothing else changed."
399
+ : `Postgres did not come back on the previous configuration either: ${tail(back.stderr)}. Check \`docker compose ps\` / \`docker compose logs postgres\`.`
400
+ }`,
401
+ back.exitCode === 0,
402
+ );
403
+ }
404
+ const limits = await deps.applyPublicRoleLimits();
405
+ if (!limits.ok) {
406
+ restoreCompose(file, previousCompose);
407
+ return fail(
408
+ "roles",
409
+ `Could not apply public_role's connection and statement limits: ${limits.error}\nThe previous release keeps running; no migration ran. Database backup: ${backup.dir}`,
410
+ true,
411
+ );
412
+ }
413
+
338
414
  // 4. Migrations from the new admin image; the old containers keep serving.
339
415
  log.phase?.("migrations");
340
416
  log.step("Running database migrations with the new admin image");
@@ -360,11 +436,11 @@ export async function upgradeSelfHosted(
360
436
  );
361
437
  }
362
438
 
363
- // 6. Roll admin, then gateway, each health-checked.
364
- for (const service of RELEASE_SERVICES) {
365
- const stage = service === "admin" ? "roll-admin" : "roll-gateway";
439
+ // 6. Roll admin, then gateway, then the MCP-Auth-Proxy, each health-checked.
440
+ for (const service of servicesOf(images)) {
441
+ const stage: UpgradeStage = `roll-${service}`;
366
442
  log.phase?.("roll");
367
- log.step(`Restarting ${service} on ${shortDigest(images[service])}`);
443
+ log.step(`Restarting ${service} on ${shortDigest(images[service] ?? "")}`);
368
444
  const up = await runner.run(
369
445
  compose(caeloDir, "up", "-d", "--no-deps", COMPOSE_SERVICE[service]),
370
446
  );
@@ -375,7 +451,7 @@ export async function upgradeSelfHosted(
375
451
  : { ok: false as const, error: `docker compose up failed: ${tail(up.stderr)}` };
376
452
  if (!healthy.ok) {
377
453
  log.error(`${service} is not healthy on ${target.version}: ${healthy.error}`);
378
- const restored = await rollback(file, previousCompose, deps);
454
+ const restored = await rollback(file, previousCompose, previousServices, deps);
379
455
  if (current) {
380
456
  recordRollback(caeloDir, current, target.version, `${stage}: ${healthy.error}`, now());
381
457
  }
@@ -559,13 +635,16 @@ export async function runMigrations(
559
635
  * In-container probe scripts (`bun -e`): the containers publish no host
560
636
  * ports, so the probe runs where the service listens. The admin's
561
637
  * `/_caelo/health` checks the DB pool + roles; the gateway's `/healthz` is
562
- * its liveness endpoint.
638
+ * its liveness endpoint; the MCP-Auth-Proxy's `/api/mcp/health` answers
639
+ * even while remote MCP is unavailable (#1328). Its image has no shell, so
640
+ * the probe is `bun -e`, never `sh -c`.
563
641
  */
564
- const HEALTH_PROBE: Record<"admin" | "gateway", string> = {
642
+ const HEALTH_PROBE: Record<ReleaseService, string> = {
565
643
  admin:
566
644
  'const r=await fetch("http://127.0.0.1:"+(process.env.PORT||"3000")+"/_caelo/health").catch(()=>null);process.exit(r&&r.ok?0:1)',
567
645
  gateway:
568
646
  'const r=await fetch("http://127.0.0.1:"+(process.env.PORT||process.env.GATEWAY_PORT||"8090")+"/healthz").catch(()=>null);process.exit(r&&r.ok?0:1)',
647
+ "mcp-auth-proxy": `const r=await fetch("http://127.0.0.1:"+(process.env.MCP_AUTH_PROXY_PORT||"${COMPOSE_MCP_AUTH_PROXY_PORT}")+"/api/mcp/health").catch(()=>null);process.exit(r&&r.ok?0:1)`,
569
648
  };
570
649
 
571
650
  /**
@@ -573,7 +652,7 @@ const HEALTH_PROBE: Record<"admin" | "gateway", string> = {
573
652
  * after `healthAttempts` tries (default 45, 2 s apart).
574
653
  */
575
654
  export async function waitHealthy(
576
- service: "admin" | "gateway",
655
+ service: ReleaseService,
577
656
  caeloDir: string,
578
657
  deps: Pick<SelfHostedUpgradeDeps, "runner" | "sleep" | "healthAttempts" | "healthIntervalMs">,
579
658
  ): Promise<{ ok: true } | { ok: false; error: string }> {
@@ -595,28 +674,35 @@ export async function waitHealthy(
595
674
  };
596
675
  }
597
676
 
598
- /** Put the previous Compose file back, restart both services on it, health-check them. */
677
+ /**
678
+ * Put the previous Compose file back, restart its services on it,
679
+ * health-check them. `previousServices` are the services that file runs (a
680
+ * release from before #1328 has no MCP-Auth-Proxy; a proxy container the
681
+ * failed run created then keeps running unused until the next upgrade).
682
+ */
599
683
  async function rollback(
600
684
  file: string,
601
685
  previousCompose: string,
686
+ previousServices: readonly ReleaseService[],
602
687
  deps: SelfHostedUpgradeDeps,
603
688
  ): Promise<boolean> {
604
689
  deps.log.phase?.("rollback");
605
690
  deps.log.warn("Rolling back to the previous images");
606
691
  restoreCompose(file, previousCompose);
607
- return (await startBoth(deps)).ok;
692
+ return (await startAll(deps, previousServices)).ok;
608
693
  }
609
694
 
610
- /** `up -d` both services on the current Compose file, then health-check each. */
611
- async function startBoth(
695
+ /** `up -d` `services` on the current Compose file, then health-check each. */
696
+ async function startAll(
612
697
  deps: SelfHostedUpgradeDeps,
698
+ services: readonly ReleaseService[],
613
699
  ): Promise<{ ok: true } | { ok: false; error: string }> {
614
700
  const up = await deps.runner.run(
615
- compose(deps.caeloDir, "up", "-d", "--no-deps", COMPOSE_SERVICE.admin, COMPOSE_SERVICE.gateway),
701
+ compose(deps.caeloDir, "up", "-d", "--no-deps", ...services.map((s) => COMPOSE_SERVICE[s])),
616
702
  );
617
703
  if (up.exitCode !== 0)
618
704
  return { ok: false, error: `docker compose up failed: ${tail(up.stderr)}` };
619
- for (const service of RELEASE_SERVICES) {
705
+ for (const service of services) {
620
706
  const healthy = await waitHealthy(service, deps.caeloDir, deps);
621
707
  if (!healthy.ok) return { ok: false, error: `${service}: ${healthy.error}` };
622
708
  }