@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
@@ -4,14 +4,17 @@
4
4
  * `cms-provision upgrade` for one gcp / gcp-firebase install: converge, then
5
5
  * app-roll (epic #696). The order, and why:
6
6
  *
7
- * 1. plan (read-only): release digests, both services, the env contract
7
+ * 1. plan: release digests, the services, the env contract — read-only
8
+ * but for the services an older install lacks (#1328, additive and
9
+ * serving nothing yet: mcp-auth-proxy-setup.ts)
8
10
  * 2. verify the images' signatures
9
11
  * 3. pre-upgrade Cloud SQL backup — the first change; if it fails, the
10
12
  * install is exactly as it was
11
13
  * 4. converge, the part before the roll (keyless provisioner, IAM, secrets,
12
14
  * MCP access)
13
15
  * 5. app-roll: health baseline, migrations, the converge steps that must
14
- * follow migrations, admin + gateway rolls, deep health check, rollback
16
+ * follow migrations, admin, gateway and MCP-Auth-Proxy rolls, deep
17
+ * health check, rollback
15
18
  * on failure, recorded digests
16
19
  * 6. converge, the part after the roll; a complete converge records the
17
20
  * install's infrastructure contract version
@@ -29,6 +32,7 @@
29
32
 
30
33
  import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
31
34
  import type { ImageDigests } from "../install-state.js";
35
+ import { ensureReleaseServices } from "../mcp-auth-proxy-setup.js";
32
36
  import type { HttpFetch } from "../runtime-secrets.js";
33
37
  import {
34
38
  type AppRollPlan,
@@ -75,9 +79,17 @@ export interface GcpUpgradeTarget {
75
79
  */
76
80
  export interface UpgradeCheckpoint {
77
81
  readonly backup?: SqlBackup;
78
- readonly priorRevisions?: Readonly<Record<RollSlug, string>>;
82
+ readonly priorRevisions?: PriorRevisions;
79
83
  }
80
84
 
85
+ /**
86
+ * The rollback target per service. The MCP-Auth-Proxy's is absent in a
87
+ * checkpoint a release before #1328 wrote; its planned revision is kept then.
88
+ */
89
+ export type PriorRevisions = Readonly<Record<"admin" | "gateway", string>> & {
90
+ readonly "mcp-auth-proxy"?: string;
91
+ };
92
+
81
93
  export interface GcpUpgradeDeps {
82
94
  readonly run: GcloudRunner;
83
95
  readonly reporter: UpgradeReporter;
@@ -165,6 +177,41 @@ export async function upgradeGcpInstallOutcome(
165
177
  r.error(images.error);
166
178
  return failed;
167
179
  }
180
+ // #1328 — the services the plan must find: the MCP-Auth-Proxy's account
181
+ // and, on an install whose stack predates it, the data gateway
182
+ // (gcp-firebase) or the proxy's own service (gcp). Additive, serving
183
+ // nothing yet, so the install keeps running as it is when this or
184
+ // anything after it fails. The gcp proxy runs the new release's image,
185
+ // so that image's signature is checked before it is deployed.
186
+ const proxyImage = images.images["mcp-auth-proxy"];
187
+ if (
188
+ target.provider === "gcp" &&
189
+ !target.skipVerify &&
190
+ !(await deps.verifyImages([{ slug: "mcp-auth-proxy", digest: proxyImage.digest }]))
191
+ ) {
192
+ sPre.failed("The MCP-Auth-Proxy image's signature did not verify");
193
+ return failed;
194
+ }
195
+ const ensured = await ensureReleaseServices(
196
+ {
197
+ provider: target.provider,
198
+ projectId,
199
+ region: target.region,
200
+ env: target.env,
201
+ domain: target.domain,
202
+ mcpAuthProxyImageRef: proxyImage.imageRef,
203
+ },
204
+ { run },
205
+ );
206
+ const ensureFailed = ensured.filter((o) => o.status === "failed");
207
+ if (ensureFailed.length > 0) {
208
+ sPre.failed("The services this release runs could not be set up. Nothing was changed.");
209
+ for (const o of ensureFailed) r.error(` FAILED: ${o.id}\n ${o.error ?? ""}`);
210
+ return failed;
211
+ }
212
+ for (const o of ensured.filter((o) => o.status === "applied")) {
213
+ r.info(` created: ${o.id}${o.detail ? ` (${o.detail})` : ""}`);
214
+ }
168
215
  const planned = await planAppRoll(
169
216
  {
170
217
  provider: target.provider,
@@ -269,7 +316,9 @@ export async function upgradeGcpInstallOutcome(
269
316
  },
270
317
  );
271
318
  if (!rolled.ok) return { ok: false, rolledBack: rolled.rolledBack };
272
- r.success(`Upgrade to ${target.targetTag} complete (admin + gateway revisions serving).`);
319
+ r.success(
320
+ `Upgrade to ${target.targetTag} complete (admin, gateway and MCP-Auth-Proxy revisions serving).`,
321
+ );
273
322
  r.info(
274
323
  `Pre-upgrade backup: ${backup.backup.backupId} (restore: ${restoreCommand(backup.backup)})`,
275
324
  );
@@ -314,8 +363,8 @@ function withPriorRevisions(
314
363
  return {
315
364
  ...plan,
316
365
  services: plan.services.map((s) => {
317
- const before = recorded[s.slug];
318
- if (before === s.priorRevision) return s;
366
+ const before: string | undefined = recorded[s.slug];
367
+ if (before === undefined || before === s.priorRevision) return s;
319
368
  r.info(
320
369
  `Continuing an interrupted update: ${s.slug} goes back to ${before} (what served before it started) if the new release fails its checks.`,
321
370
  );
@@ -16,6 +16,13 @@
16
16
  import { mkdtempSync } from "node:fs";
17
17
  import { tmpdir } from "node:os";
18
18
  import { join } from "node:path";
19
+ import { isIpLiteral } from "@caelo-cms/shared";
20
+ import {
21
+ ensureCloudSqlVerifyFull,
22
+ liveDatabaseUrls,
23
+ moveServicesToDnsHost,
24
+ } from "./cloudsql-tls.js";
25
+ import { findSqlInstance } from "./gateway-credentials.js";
19
26
  import { gcloud } from "./gcloud.js";
20
27
  import { type GcloudRunner, realSleep } from "./gcloud-retry.js";
21
28
  import { GCP_STACK_ENV } from "./gcp-names.js";
@@ -23,10 +30,16 @@ import type { InstallMetadata } from "./install-state.js";
23
30
  import { readAdminConfig, runMigrationsViaCloudRunJob } from "./migration-runner.js";
24
31
  import { fetchVerifiedReleaseManifest } from "./release-manifest.js";
25
32
  import type { HttpFetch } from "./runtime-secrets.js";
33
+ import { DATABASE_URL_VARS } from "./stack-contract.js";
34
+ import { liveContainerEnv } from "./stack-converge.js";
26
35
  import { opsBucketName } from "./state-backend.js";
27
36
  import { acquireBucketStateLock, GcsStateStore, realSyncGcloud } from "./state-store.js";
28
- import type { AppRollPlan } from "./upgrade/app-roll.js";
29
- import { findServiceUrl } from "./upgrade/gcp-lookup.js";
37
+ import type { AppRollPlan, RollSlug } from "./upgrade/app-roll.js";
38
+ import {
39
+ describeServiceJson,
40
+ findServiceUrl,
41
+ resolveGcpResourceName,
42
+ } from "./upgrade/gcp-lookup.js";
30
43
  import { createIapHealthProbe } from "./upgrade/health.js";
31
44
  import { createReleaseImageForms } from "./upgrade/image-forms.js";
32
45
  import { type IamTester, restIamTester } from "./upgrader-iam-preflight.js";
@@ -58,6 +71,54 @@ export async function upgraderDatabaseHost(
58
71
  return cfg.databaseHost;
59
72
  }
60
73
 
74
+ /**
75
+ * #1338 — the database host for the converge job (#819), after making sure
76
+ * a verify-full connection to it can work. The job opens its database
77
+ * queue before it converges anything, with this release's code, which
78
+ * connects with verify-full only; on an install whose services still dial
79
+ * the instance's IP it first runs the converge's database TLS step
80
+ * (cloudsql-tls.ts: shared CA, private DNS name, services moved onto it).
81
+ * The job runs as the install's `caelo-provisioner`, which holds
82
+ * `roles/cloudsql.admin` and `roles/dns.admin` (gcloud.ts PROVISIONER_ROLES).
83
+ * Idempotent: the converge's own step finds everything in shape.
84
+ */
85
+ export async function convergeDatabaseHost(
86
+ placement: Pick<UpgraderPlacement, "projectId" | "region">,
87
+ run: GcloudRunner = gcloud,
88
+ http?: HttpFetch,
89
+ ): Promise<string> {
90
+ const host = await upgraderDatabaseHost(placement, run);
91
+ if (!isIpLiteral(host)) return host;
92
+ const install = { projectId: placement.projectId, env: GCP_STACK_ENV };
93
+ const lookup = await findSqlInstance(install, { run });
94
+ if (lookup.status !== "found") {
95
+ throw new Error(`could not find the install's Cloud SQL instance (${lookup.status})`);
96
+ }
97
+ const deps = { run, ...(http ? { http } : {}) };
98
+ const tls = await ensureCloudSqlVerifyFull({ ...install, sqlInstance: lookup.name }, deps);
99
+ if (!tls.ok) throw new Error(`database TLS could not be set up: ${tls.error}`);
100
+ const services: { serviceName: string; liveUrls: Record<string, string> }[] = [];
101
+ for (const slug of ["admin", "gateway"] as const) {
102
+ const found = await resolveGcpResourceName(
103
+ run,
104
+ "run-service",
105
+ `caelo-${GCP_STACK_ENV}-${slug}`,
106
+ placement.projectId,
107
+ placement.region,
108
+ );
109
+ if (found.status !== "found") throw new Error(`could not find the ${slug} service`);
110
+ const json = await describeServiceJson(run, placement.projectId, placement.region, found.name);
111
+ if (!json) throw new Error(`could not read the ${slug} service's configuration`);
112
+ services.push({
113
+ serviceName: found.name,
114
+ liveUrls: liveDatabaseUrls(liveContainerEnv(json), DATABASE_URL_VARS[slug]),
115
+ });
116
+ }
117
+ const moved = await moveServicesToDnsHost(placement, services, tls.host, deps);
118
+ if (!moved.ok) throw new Error(moved.error);
119
+ return tls.host;
120
+ }
121
+
61
122
  /**
62
123
  * The IAP-backed deep health probe for `plan`'s install (the upgrader job;
63
124
  * the converge job, #819, too). `accessJustChanged`: a converge right
@@ -71,7 +132,7 @@ export async function healthProbeFor(
71
132
  context: { readonly accessJustChanged: boolean },
72
133
  ) {
73
134
  const { install } = plan;
74
- const urls: Partial<Record<"admin" | "gateway", string>> = {};
135
+ const urls: Partial<Record<RollSlug, string>> = {};
75
136
  for (const s of plan.services) {
76
137
  const url = await findServiceUrl(run, install.projectId, install.region, s.serviceName);
77
138
  if (!url) {
@@ -88,6 +149,7 @@ export async function healthProbeFor(
88
149
  domain: install.domain,
89
150
  adminServiceUrl: urls.admin as string,
90
151
  gatewayServiceUrl: urls.gateway as string,
152
+ mcpAuthProxyServiceUrl: urls["mcp-auth-proxy"] as string,
91
153
  },
92
154
  { run, http, sleep: realSleep, accessJustChanged: context.accessJustChanged },
93
155
  ),
@@ -73,6 +73,11 @@ function setupGrants(): Map<string, Set<string>> {
73
73
  `projects/${p}/serviceAccounts/caelo-production-gateway-sa@${p}.iam.gserviceaccount.com`,
74
74
  of("roles/iam.serviceAccountUser"),
75
75
  ],
76
+ // #1328 — the MCP-Auth-Proxy's revisions run as its own account.
77
+ [
78
+ `projects/${p}/serviceAccounts/caelo-production-mcp-proxy-sa@${p}.iam.gserviceaccount.com`,
79
+ of("roles/iam.serviceAccountUser"),
80
+ ],
76
81
  [
77
82
  `projects/${p}/serviceAccounts/caelo-production-upgrader@${p}.iam.gserviceaccount.com`,
78
83
  of("roles/iam.serviceAccountUser"),
@@ -154,6 +159,20 @@ describe("upgraderAccessChecks", () => {
154
159
  expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain("capture job");
155
160
  });
156
161
 
162
+ it("#1328: the MCP-Auth-Proxy's account is checked, since every app-roll rolls the proxy", async () => {
163
+ const proxySa =
164
+ "projects/acme/serviceAccounts/caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com";
165
+ const grants = setupGrants();
166
+ grants.delete(proxySa);
167
+ const r = await runIamPreflight(upgraderAccessChecks(TARGET), tester(grants));
168
+ expect(r.kind).toBe("missing");
169
+ if (r.kind !== "missing") return;
170
+ expect(r.missing.map((m) => [resourceName(m.check.resource), m.missing])).toEqual([
171
+ [proxySa, ["iam.serviceAccounts.actAs"]],
172
+ ]);
173
+ expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain("MCP-Auth-Proxy");
174
+ });
175
+
157
176
  it("never needs a permission only a forbidden role would give (no compute, no IAM admin)", () => {
158
177
  const all = upgraderAccessChecks(TARGET).flatMap((c) => c.permissions);
159
178
  for (const p of all) {
@@ -54,6 +54,7 @@ import {
54
54
  auditServiceAccountEmail,
55
55
  captureServiceAccountEmail,
56
56
  gatewayServiceAccountEmail,
57
+ mcpAuthProxyServiceAccountEmail,
57
58
  mcpIapServiceAccountEmail,
58
59
  runServiceAccountEmail,
59
60
  upgraderJobName,
@@ -186,6 +187,11 @@ export function upgraderAccessChecks(target: {
186
187
  resource: sa(gatewayServiceAccountEmail(projectId, env)),
187
188
  permissions: ["iam.serviceAccounts.actAs"],
188
189
  },
190
+ {
191
+ need: "run the MCP-Auth-Proxy as its service account",
192
+ resource: sa(mcpAuthProxyServiceAccountEmail(projectId, env)),
193
+ permissions: ["iam.serviceAccounts.actAs"],
194
+ },
189
195
  {
190
196
  // #875 — every app-roll moves the audit job onto the new admin image
191
197
  // (upgrade/admin-image-jobs.ts), and upgrader-setup.ts grants actAs on
@@ -57,14 +57,16 @@ const revisionJson = (deployed: string | null, resolved: string) =>
57
57
  status: { imageDigest: `${MIRROR}@${resolved}` },
58
58
  });
59
59
 
60
- /** Serving revisions deployed as `admin` / `gateway` (index digests). */
60
+ /** Serving revisions deployed as `admin` / `gateway` (index digests); the proxy serves D("3"). */
61
61
  const servingAs = (admin: string, gateway: string): [RegExp, Answer] => [
62
62
  /^run revisions describe (\S+)/,
63
63
  (l) =>
64
64
  ok(
65
65
  l.includes("-admin-")
66
66
  ? revisionJson(admin, `sha256:${"e".repeat(64)}`)
67
- : revisionJson(gateway, `sha256:${"d".repeat(64)}`),
67
+ : l.includes(PROXY)
68
+ ? revisionJson(D("3"), P("3"))
69
+ : revisionJson(gateway, `sha256:${"d".repeat(64)}`),
68
70
  ),
69
71
  ];
70
72
 
@@ -79,13 +81,16 @@ const releaseTags = (slug: "admin" | "gateway", tags: Record<string, string>): [
79
81
  ];
80
82
 
81
83
  const ADMIN = "caelo-production-admin-aaa";
82
- const GATEWAY = "caelo-production-gateway-bbb";
84
+ // #1328 — gcp-firebase: the visitor gateway is the fixed-name data gateway,
85
+ // the MCP-Auth-Proxy the service that was the gateway (E13 a).
86
+ const GATEWAY = "caelo-production-data-gateway";
87
+ const PROXY = "caelo-production-gateway-bbb";
83
88
 
84
89
  const MANIFEST: ReleaseManifest = {
85
90
  manifestVersion: 1,
86
91
  version: "0.11.0",
87
92
  channel: "stable",
88
- digests: { admin: D("a"), gateway: D("b"), ops: D("c") },
93
+ digests: { admin: D("a"), gateway: D("b"), ops: D("c"), "mcp-auth-proxy": D("d") },
89
94
  minFromVersion: "0.10.0",
90
95
  infraContractVersion: 2,
91
96
  migrations: { contract: false, lastContractVersion: null, head: "0267_platform_updates" },
@@ -121,14 +126,23 @@ const META: InstallMetadata = {
121
126
  ownerEmail: "owner@acme.com",
122
127
  region: "europe-west1",
123
128
  createdAt: "2026-01-01T00:00:00Z",
124
- imageDigests: { admin: D("1"), gateway: D("2") },
129
+ imageDigests: { admin: D("1"), gateway: D("2"), "mcp-auth-proxy": D("3") },
125
130
  releaseVersion: "0.10.45",
126
131
  infraContractVersion: 2,
127
132
  };
128
133
 
129
- const manifestOf = (version: string, admin: string, gateway: string): ReleaseManifestResult => ({
134
+ const manifestOf = (
135
+ version: string,
136
+ admin: string,
137
+ gateway: string,
138
+ proxy = D("3"),
139
+ ): ReleaseManifestResult => ({
130
140
  ok: true,
131
- manifest: { ...MANIFEST, version, digests: { admin, gateway, ops: D("c") } },
141
+ manifest: {
142
+ ...MANIFEST,
143
+ version,
144
+ digests: { admin, gateway, ops: D("c"), "mcp-auth-proxy": proxy },
145
+ },
132
146
  });
133
147
 
134
148
  const serviceJson = (image: string, env: unknown[]) =>
@@ -141,11 +155,13 @@ const ADMIN_JSON = serviceJson("registry/admin@sha256:old", [
141
155
  { name: "CAELO_FIREBASE_SITE", value: "caelo-production-site-abc123" },
142
156
  {
143
157
  name: "ADMIN_DATABASE_URL",
144
- value: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
158
+ value:
159
+ "postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
145
160
  },
146
161
  secretRef("ADMIN_DATABASE_PASSWORD", "postgres-password"),
147
162
  ]);
148
163
  const GATEWAY_JSON = serviceJson("registry/gateway@sha256:old", []);
164
+ const PROXY_JSON = serviceJson("registry/mcp-auth-proxy@sha256:old", []);
149
165
 
150
166
  type Answer = GcloudResult | ((line: string) => GcloudResult);
151
167
 
@@ -153,15 +169,23 @@ function fakeGcloud(events: string[], overrides: [RegExp, Answer][] = []) {
153
169
  const rules: [RegExp, Answer][] = [
154
170
  ...overrides,
155
171
  [/^run services list .*caelo-production-admin/, ok(`${ADMIN}\n`)],
156
- [/^run services list .*caelo-production-gateway/, ok(`${GATEWAY}\n`)],
172
+ [/^run services list .*caelo-production-data-gateway/, ok(`${GATEWAY}\n`)],
173
+ [/^run services list .*caelo-production-gateway/, ok(`${PROXY}\n`)],
157
174
  [/describe (\S+) .*revisionName/, (l) => ok(`${l.split(" ")[3]}-00001-old\n`)],
158
175
  [new RegExp(`describe ${ADMIN} .*--format=json`), ok(ADMIN_JSON)],
159
176
  [new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
177
+ [new RegExp(`describe ${PROXY} .*--format=json`), ok(PROXY_JSON)],
160
178
  [/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
161
179
  [
162
180
  /^run revisions describe (\S+)/,
163
181
  (l) =>
164
- ok(l.includes("-admin-") ? revisionJson(D("1"), P("1")) : revisionJson(D("2"), P("2"))),
182
+ ok(
183
+ l.includes("-admin-")
184
+ ? revisionJson(D("1"), P("1"))
185
+ : l.includes(PROXY)
186
+ ? revisionJson(D("3"), P("3"))
187
+ : revisionJson(D("2"), P("2")),
188
+ ),
165
189
  ],
166
190
  [/^sql instances list/, ok("caelo-production-pg-abc\n")],
167
191
  [/^sql backups list/, ok("1760000000000\n")],
@@ -307,9 +331,11 @@ function fakeImageForms(order: string[], down: boolean): ReleaseImageForms {
307
331
  return {
308
332
  ofIndex,
309
333
  of: async (m, slug) => {
334
+ const index = m.digests[slug];
335
+ if (!index) return { ok: false, transient: false, message: `no ${slug} image` };
310
336
  const signed = m.platformDigests?.[slug];
311
- if (signed) return { ok: true, digests: [m.digests[slug], signed], amd64: signed };
312
- return ofIndex(slug, m.digests[slug]);
337
+ if (signed) return { ok: true, digests: [index, signed], amd64: signed };
338
+ return ofIndex(slug, index);
313
339
  },
314
340
  };
315
341
  }
@@ -393,6 +419,21 @@ describe("runUpgraderJob", () => {
393
419
  ]);
394
420
  });
395
421
 
422
+ it("#1328: a target manifest without an MCP-Auth-Proxy image stops before anything changes", async () => {
423
+ const { "mcp-auth-proxy": _none, ...digests } = MANIFEST.digests;
424
+ const { deps, order, q, written } = setup({
425
+ manifest: { ok: true, manifest: { ...MANIFEST, digests } },
426
+ });
427
+ const r = await runUpgraderJob(deps);
428
+ expect(r.status).toBe("failed");
429
+ expect(q.end()?.codes).toEqual(["manifest"]);
430
+ expect(q.end()?.reason).toContain("names no mcp-auth-proxy image");
431
+ expect(order.some((l) => l.startsWith("gcloud sql backups create"))).toBe(false);
432
+ expect(order.some((l) => l.startsWith("migrate "))).toBe(false);
433
+ expect(order.some((l) => l.startsWith("gcloud run services update"))).toBe(false);
434
+ expect(written).toEqual([]);
435
+ });
436
+
396
437
  it("#815: decides first, locks right before the backup, holds the lock to the end", async () => {
397
438
  const ok = setup();
398
439
  await runUpgraderJob(ok.deps);
@@ -429,6 +470,7 @@ describe("runUpgraderJob", () => {
429
470
  "iam projects/acme/serviceAccounts/caelo-production-audit@acme.iam.gserviceaccount.com",
430
471
  "iam projects/acme/serviceAccounts/caelo-production-capture@acme.iam.gserviceaccount.com",
431
472
  "iam projects/acme/serviceAccounts/caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
473
+ "iam projects/acme/serviceAccounts/caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
432
474
  "iam projects/acme/serviceAccounts/caelo-production-run-sa@acme.iam.gserviceaccount.com",
433
475
  "iam projects/acme/serviceAccounts/caelo-production-upgrader@acme.iam.gserviceaccount.com",
434
476
  "iam projects/caelo-website/locations/europe-west1/repositories/caelo-cms-images",
@@ -616,12 +658,21 @@ describe("runUpgraderJob", () => {
616
658
 
617
659
  it("a revision deployed by tag matches by the digest Cloud Run resolved", async () => {
618
660
  const { deps, written } = setup({
619
- meta: { ...META, imageDigests: { admin: P("1"), gateway: D("2") } },
661
+ meta: {
662
+ ...META,
663
+ imageDigests: { admin: P("1"), gateway: D("2"), "mcp-auth-proxy": D("3") },
664
+ },
620
665
  gcloud: [
621
666
  [
622
667
  /^run revisions describe (\S+)/,
623
668
  (l) =>
624
- ok(l.includes("-admin-") ? revisionJson(null, P("1")) : revisionJson(D("2"), P("2"))),
669
+ ok(
670
+ l.includes("-admin-")
671
+ ? revisionJson(null, P("1"))
672
+ : l.includes(PROXY)
673
+ ? revisionJson(D("3"), P("3"))
674
+ : revisionJson(D("2"), P("2")),
675
+ ),
625
676
  ],
626
677
  ],
627
678
  });
@@ -664,14 +715,20 @@ describe("runUpgraderJob", () => {
664
715
 
665
716
  it("#815: drift + a stop verdict: the records are corrected under the lock, the verdict stands", async () => {
666
717
  const drift = {
667
- meta: { ...META, imageDigests: { admin: D("9"), gateway: D("2") }, infraContractVersion: 1 },
718
+ meta: {
719
+ ...META,
720
+ imageDigests: { admin: D("9"), gateway: D("2"), "mcp-auth-proxy": D("3") },
721
+ infraContractVersion: 1,
722
+ },
668
723
  manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
669
724
  };
670
725
  const free = setup(drift);
671
726
  expect((await runUpgraderJob(free.deps)).status).toBe("needs_cloud_shell");
672
727
  expect(free.q.end()?.codes).toEqual(["infra-contract"]);
673
728
  expect(free.written).toEqual([
674
- expect.objectContaining({ imageDigests: { admin: D("1"), gateway: D("2") } }),
729
+ expect.objectContaining({
730
+ imageDigests: { admin: D("1"), gateway: D("2"), "mcp-auth-proxy": D("3") },
731
+ }),
675
732
  ]);
676
733
  expect(free.order.indexOf("lock")).toBeGreaterThan(step(free.order, /^manifest 0\.11\.0/));
677
734
  expect(free.order).toContain("unlock");
@@ -814,7 +871,8 @@ describe("runUpgraderJob", () => {
814
871
  const servingPlatform = (admin: string, gateway: string): [RegExp, Answer] => [
815
872
  /^run revisions describe (\S+)/,
816
873
  (l) => {
817
- const d = l.includes("-admin-") ? admin : gateway;
874
+ // The MCP-Auth-Proxy serves its recorded index D("3") throughout.
875
+ const d = l.includes("-admin-") ? admin : l.includes(PROXY) ? D("3") : gateway;
818
876
  return ok(revisionJson(d, d));
819
877
  },
820
878
  ];
@@ -824,12 +882,17 @@ describe("runUpgraderJob", () => {
824
882
  const v064 = {
825
883
  ...MANIFEST,
826
884
  version: "0.10.64",
827
- digests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, ops: D("c") },
885
+ digests: {
886
+ admin: ADMIN_INDEX,
887
+ gateway: GATEWAY_INDEX,
888
+ ops: D("c"),
889
+ "mcp-auth-proxy": D("3"),
890
+ },
828
891
  };
829
892
  const { deps, q, written } = setup({
830
893
  meta: {
831
894
  ...META,
832
- imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX },
895
+ imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, "mcp-auth-proxy": D("3") },
833
896
  releaseVersion: "0.10.64",
834
897
  },
835
898
  manifests: { "0.10.64": { ok: true, manifest: v064 } },
@@ -839,7 +902,7 @@ describe("runUpgraderJob", () => {
839
902
  expect((await runUpgraderJob(deps)).status).toBe("succeeded");
840
903
  expect(written[0]).toMatchObject({
841
904
  releaseVersion: "0.10.64",
842
- imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX },
905
+ imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, "mcp-auth-proxy": D("3") },
843
906
  platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 },
844
907
  });
845
908
  expect(q.events.some((e) => e.message.startsWith("0.10.64 → 0.11.0"))).toBe(true);
@@ -861,6 +924,7 @@ describe("runUpgraderJob", () => {
861
924
  expect(order.filter((l) => l.startsWith("index "))).toEqual([
862
925
  `index admin@${D("a")}`,
863
926
  `index gateway@${D("b")}`,
927
+ `index mcp-auth-proxy@${D("d")}`,
864
928
  ]);
865
929
  expect(q.events.some((e) => e.message.includes("refreshed its install records"))).toBe(false);
866
930
  });
@@ -868,14 +932,14 @@ describe("runUpgraderJob", () => {
868
932
  it("the roll records the new release's platform forms, signed ones first", async () => {
869
933
  const signed = {
870
934
  ...MANIFEST,
871
- platformDigests: { admin: P("x"), gateway: P("y"), ops: P("z") },
935
+ platformDigests: { admin: P("x"), gateway: P("y"), ops: P("z"), "mcp-auth-proxy": P("w") },
872
936
  };
873
937
  const { deps, order, written } = setup({ manifest: { ok: true, manifest: signed } });
874
938
  expect((await runUpgraderJob(deps)).status).toBe("succeeded");
875
939
  expect(written.at(-1)).toMatchObject({
876
940
  releaseVersion: "0.11.0",
877
- imageDigests: { admin: D("a"), gateway: D("b") },
878
- platformDigests: { admin: P("x"), gateway: P("y") },
941
+ imageDigests: { admin: D("a"), gateway: D("b"), "mcp-auth-proxy": D("d") },
942
+ platformDigests: { admin: P("x"), gateway: P("y"), "mcp-auth-proxy": P("w") },
879
943
  });
880
944
  expect(order.some((l) => l.startsWith("index "))).toBe(false);
881
945
  });
@@ -886,7 +950,11 @@ describe("runUpgraderJob", () => {
886
950
  imageForms: "down",
887
951
  });
888
952
  expect((await runUpgraderJob(deps)).status).toBe("succeeded");
889
- expect(written.at(-1)?.imageDigests).toEqual({ admin: D("a"), gateway: D("b") });
953
+ expect(written.at(-1)?.imageDigests).toEqual({
954
+ admin: D("a"),
955
+ gateway: D("b"),
956
+ "mcp-auth-proxy": D("d"),
957
+ });
890
958
  expect(written.at(-1)?.platformDigests).toBeUndefined();
891
959
  expect(q.events.some((e) => e.level === "warn" && e.message.includes("linux/amd64"))).toBe(
892
960
  true,
@@ -995,7 +1063,9 @@ describe("runUpgraderJob", () => {
995
1063
  manifests: { "0.10.45": manifestOf("0.10.45", D("1"), D("2")) },
996
1064
  });
997
1065
  expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
998
- expect(q.end()?.reason).toContain("The admin serves v0.10.45's image, the gateway does not");
1066
+ expect(q.end()?.reason).toContain(
1067
+ "The admin and the MCP-Auth-Proxy serve v0.10.45's image, the gateway does not",
1068
+ );
999
1069
  expect(written).toEqual([]);
1000
1070
  });
1001
1071
 
@@ -1021,8 +1091,12 @@ describe("runUpgraderJob", () => {
1021
1091
  expect((await runUpgraderJob(deps)).status).toBe("needs_cloud_shell");
1022
1092
  const end = q.end();
1023
1093
  expect(end?.codes).toEqual(["install-state-drift"]);
1024
- expect(end?.reason).toContain("The gateway serves v0.10.45's image, the admin does not");
1025
- expect(end?.reason).toContain("the admin serves v0.10.44's image, the gateway does not");
1094
+ expect(end?.reason).toContain(
1095
+ "The gateway and the MCP-Auth-Proxy serve v0.10.45's image, the admin does not",
1096
+ );
1097
+ expect(end?.reason).toContain(
1098
+ "the admin and the MCP-Auth-Proxy serve v0.10.44's image, the gateway does not",
1099
+ );
1026
1100
  expect(written).toEqual([]);
1027
1101
  });
1028
1102
 
@@ -56,15 +56,21 @@ import type {
56
56
  StoredUpdatePolicy,
57
57
  } from "@caelo-cms/shared";
58
58
  import type { GcloudRunner, Sleep } from "./gcloud-retry.js";
59
- import { gcpNamePrefix, upgraderJobName } from "./gcp-names.js";
59
+ import { upgraderJobName } from "./gcp-names.js";
60
60
  import type { InstallMetadata } from "./install-state.js";
61
- import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
61
+ import {
62
+ manifestServiceImages,
63
+ type ReleaseManifest,
64
+ type ReleaseManifestResult,
65
+ } from "./release-manifest.js";
66
+ import { cloudRunServicePrefix } from "./stack-contract.js";
62
67
  import { StateLockHeldError } from "./state-store.js";
63
68
  import {
64
69
  type AppRollPlan,
65
70
  executeAppRoll,
66
71
  planAppRoll,
67
72
  type ReleaseImage,
73
+ ROLL_ORDER,
68
74
  type RollSlug,
69
75
  } from "./upgrade/app-roll.js";
70
76
  import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
@@ -389,7 +395,7 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
389
395
  // install.json must describe what actually serves: if a previous run
390
396
  // rolled but could not record it, the recorded version is stale and
391
397
  // the downgrade / minFromVersion checks would judge against it.
392
- const serving = await servingDigests(deps);
398
+ const serving = await servingDigests(deps, meta.provider);
393
399
  if (!serving.ok) {
394
400
  return stopWith("failed", ["lookup"], `Nothing was changed: ${serving.error}.`);
395
401
  }
@@ -424,6 +430,9 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
424
430
  imageDigests: {
425
431
  admin: found.manifest.digests.admin,
426
432
  gateway: found.manifest.digests.gateway,
433
+ ...(found.manifest.digests["mcp-auth-proxy"]
434
+ ? { "mcp-auth-proxy": found.manifest.digests["mcp-auth-proxy"] }
435
+ : {}),
427
436
  },
428
437
  ...(found.platformDigests ? { platformDigests: found.platformDigests } : {}),
429
438
  releaseVersion: found.manifest.version,
@@ -500,13 +509,10 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
500
509
  r.info("The upgrader has every permission this update needs.");
501
510
 
502
511
  const m = decision.manifest;
503
- const images: Record<RollSlug, ReleaseImage> = {
504
- admin: { digest: m.digests.admin, imageRef: releaseImageRef("admin", m.digests.admin) },
505
- gateway: {
506
- digest: m.digests.gateway,
507
- imageRef: releaseImageRef("gateway", m.digests.gateway),
508
- },
509
- };
512
+ const resolved = manifestServiceImages(m);
513
+ if (!resolved.ok)
514
+ return stopWith("failed", ["manifest"], `Nothing was changed: ${resolved.error}.`);
515
+ const images: Record<RollSlug, ReleaseImage> = resolved.images;
510
516
  const planned = await planAppRoll(
511
517
  {
512
518
  provider: meta.provider,
@@ -694,7 +700,10 @@ export async function recordUpdateEnd(
694
700
  }
695
701
 
696
702
  /** Every digest the revisions serving traffic are known by, per service. */
697
- async function servingDigests(deps: UpgraderJobDeps): Promise<
703
+ async function servingDigests(
704
+ deps: UpgraderJobDeps,
705
+ provider: InstallMetadata["provider"],
706
+ ): Promise<
698
707
  | {
699
708
  ok: true;
700
709
  digests: ServingDigests;
@@ -705,8 +714,12 @@ async function servingDigests(deps: UpgraderJobDeps): Promise<
705
714
  > {
706
715
  const digests: Partial<Record<RollSlug, readonly string[]>> = {};
707
716
  const services: Partial<Record<RollSlug, string>> = {};
708
- for (const slug of ["admin", "gateway"] as const) {
709
- const prefix = `${gcpNamePrefix(deps.env)}-${slug}`;
717
+ if (provider !== "gcp" && provider !== "gcp-firebase") {
718
+ return { ok: false, error: `the upgrader runs on Google Cloud installs only, not ${provider}` };
719
+ }
720
+ for (const slug of ROLL_ORDER) {
721
+ // #1328 — which service plays which role differs per provider.
722
+ const prefix = cloudRunServicePrefix(provider, deps.env, slug);
710
723
  const service = await resolveGcpResourceName(
711
724
  deps.run,
712
725
  "run-service",
@@ -739,7 +752,7 @@ async function templateDigests(
739
752
  services: Readonly<Record<RollSlug, string>>,
740
753
  ): Promise<ServingDigests> {
741
754
  const out: Partial<Record<RollSlug, readonly string[]>> = {};
742
- for (const slug of ["admin", "gateway"] as const) {
755
+ for (const slug of ROLL_ORDER) {
743
756
  const d = await findTemplateImageDigest(deps.run, deps.projectId, deps.region, services[slug]);
744
757
  out[slug] = d ? [d] : [];
745
758
  }