@caelo-cms/provisioning 0.11.9 → 0.11.11

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 (352) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/admin-domain.d.ts +10 -1
  4. package/dist/admin-domain.d.ts.map +1 -1
  5. package/dist/admin-domain.js +13 -3
  6. package/dist/admin-domain.js.map +1 -1
  7. package/dist/adopt-existing.d.ts +12 -3
  8. package/dist/adopt-existing.d.ts.map +1 -1
  9. package/dist/adopt-existing.js +143 -0
  10. package/dist/adopt-existing.js.map +1 -1
  11. package/dist/agent/command.d.ts +23 -0
  12. package/dist/agent/command.d.ts.map +1 -1
  13. package/dist/agent/command.js +32 -9
  14. package/dist/agent/command.js.map +1 -1
  15. package/dist/agent/run.d.ts +2 -0
  16. package/dist/agent/run.d.ts.map +1 -1
  17. package/dist/agent/run.js +5 -1
  18. package/dist/agent/run.js.map +1 -1
  19. package/dist/agent-db.js +69 -10
  20. package/dist/aws-task-roles.d.ts +4 -2
  21. package/dist/aws-task-roles.d.ts.map +1 -1
  22. package/dist/aws-task-roles.js +4 -1
  23. package/dist/aws-task-roles.js.map +1 -1
  24. package/dist/caddy.d.ts +21 -4
  25. package/dist/caddy.d.ts.map +1 -1
  26. package/dist/caddy.js +27 -3
  27. package/dist/caddy.js.map +1 -1
  28. package/dist/cli-routing.js +1 -1
  29. package/dist/cli-routing.js.map +1 -1
  30. package/dist/cli.js +68 -7
  31. package/dist/cli.js.map +1 -1
  32. package/dist/cloudsql-tls.d.ts +93 -0
  33. package/dist/cloudsql-tls.d.ts.map +1 -0
  34. package/dist/cloudsql-tls.js +295 -0
  35. package/dist/cloudsql-tls.js.map +1 -0
  36. package/dist/compose.d.ts +43 -6
  37. package/dist/compose.d.ts.map +1 -1
  38. package/dist/compose.js +148 -23
  39. package/dist/compose.js.map +1 -1
  40. package/dist/converge-checkpoint.d.ts +1 -0
  41. package/dist/converge-checkpoint.d.ts.map +1 -1
  42. package/dist/converge-checkpoint.js +8 -1
  43. package/dist/converge-checkpoint.js.map +1 -1
  44. package/dist/converge-db-trust.d.ts +44 -0
  45. package/dist/converge-db-trust.d.ts.map +1 -0
  46. package/dist/converge-db-trust.js +158 -0
  47. package/dist/converge-db-trust.js.map +1 -0
  48. package/dist/converge-job-gcp.d.ts +5 -2
  49. package/dist/converge-job-gcp.d.ts.map +1 -1
  50. package/dist/converge-job-gcp.js +11 -13
  51. package/dist/converge-job-gcp.js.map +1 -1
  52. package/dist/converge-job-preflight.d.ts.map +1 -1
  53. package/dist/converge-job-preflight.js +8 -0
  54. package/dist/converge-job-preflight.js.map +1 -1
  55. package/dist/converge-job.d.ts +17 -0
  56. package/dist/converge-job.d.ts.map +1 -1
  57. package/dist/converge-job.js +12 -0
  58. package/dist/converge-job.js.map +1 -1
  59. package/dist/deployed-release.d.ts +4 -1
  60. package/dist/deployed-release.d.ts.map +1 -1
  61. package/dist/deployed-release.js +18 -8
  62. package/dist/deployed-release.js.map +1 -1
  63. package/dist/gcp-names.d.ts +34 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +45 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/gcp-url-map.d.ts.map +1 -1
  68. package/dist/gcp-url-map.js +6 -2
  69. package/dist/gcp-url-map.js.map +1 -1
  70. package/dist/index.d.ts +3 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/infra-contract.d.ts +1 -1
  75. package/dist/infra-contract.d.ts.map +1 -1
  76. package/dist/infra-contract.js +45 -1
  77. package/dist/infra-contract.js.map +1 -1
  78. package/dist/install-job.d.ts +4 -2
  79. package/dist/install-job.d.ts.map +1 -1
  80. package/dist/install-job.js +4 -1
  81. package/dist/install-job.js.map +1 -1
  82. package/dist/install-state.d.ts +13 -0
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +17 -4
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +20 -8
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  90. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  91. package/dist/mcp-auth-proxy-setup.js +280 -0
  92. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  93. package/dist/migration-runner.d.ts.map +1 -1
  94. package/dist/migration-runner.js +8 -3
  95. package/dist/migration-runner.js.map +1 -1
  96. package/dist/migration-targets.d.ts +11 -0
  97. package/dist/migration-targets.d.ts.map +1 -0
  98. package/dist/migration-targets.js +12 -0
  99. package/dist/migration-targets.js.map +1 -0
  100. package/dist/operator-access.d.ts +10 -0
  101. package/dist/operator-access.d.ts.map +1 -1
  102. package/dist/operator-access.js +5 -0
  103. package/dist/operator-access.js.map +1 -1
  104. package/dist/pool-budget-stack.d.ts +13 -1
  105. package/dist/pool-budget-stack.d.ts.map +1 -1
  106. package/dist/pool-budget-stack.js +11 -1
  107. package/dist/pool-budget-stack.js.map +1 -1
  108. package/dist/public-role-isolation.d.ts +7 -3
  109. package/dist/public-role-isolation.d.ts.map +1 -1
  110. package/dist/public-role-isolation.js +25 -13
  111. package/dist/public-role-isolation.js.map +1 -1
  112. package/dist/release-contract.d.ts.map +1 -1
  113. package/dist/release-contract.js +3 -0
  114. package/dist/release-contract.js.map +1 -1
  115. package/dist/release-manifest.d.ts +25 -1
  116. package/dist/release-manifest.d.ts.map +1 -1
  117. package/dist/release-manifest.js +36 -3
  118. package/dist/release-manifest.js.map +1 -1
  119. package/dist/remote-mcp.d.ts +55 -18
  120. package/dist/remote-mcp.d.ts.map +1 -1
  121. package/dist/remote-mcp.js +307 -77
  122. package/dist/remote-mcp.js.map +1 -1
  123. package/dist/runtime-secrets.d.ts +11 -0
  124. package/dist/runtime-secrets.d.ts.map +1 -1
  125. package/dist/runtime-secrets.js +40 -25
  126. package/dist/runtime-secrets.js.map +1 -1
  127. package/dist/scheduler-setup.d.ts +75 -0
  128. package/dist/scheduler-setup.d.ts.map +1 -0
  129. package/dist/scheduler-setup.js +331 -0
  130. package/dist/scheduler-setup.js.map +1 -0
  131. package/dist/seal-key-rotation.d.ts +7 -7
  132. package/dist/seal-key-rotation.d.ts.map +1 -1
  133. package/dist/seal-key-rotation.js +22 -22
  134. package/dist/seal-key-rotation.js.map +1 -1
  135. package/dist/self-hosted-db-tls.d.ts +37 -0
  136. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  137. package/dist/self-hosted-db-tls.js +220 -0
  138. package/dist/self-hosted-db-tls.js.map +1 -0
  139. package/dist/self-hosted-release.d.ts +3 -1
  140. package/dist/self-hosted-release.d.ts.map +1 -1
  141. package/dist/self-hosted-release.js +33 -9
  142. package/dist/self-hosted-release.js.map +1 -1
  143. package/dist/self-hosted-roles.d.ts +43 -2
  144. package/dist/self-hosted-roles.d.ts.map +1 -1
  145. package/dist/self-hosted-roles.js +87 -1
  146. package/dist/self-hosted-roles.js.map +1 -1
  147. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  148. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  149. package/dist/self-hosted-scheduler-interval.js +68 -0
  150. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  151. package/dist/self-hosted-start.d.ts +0 -21
  152. package/dist/self-hosted-start.d.ts.map +1 -1
  153. package/dist/self-hosted-start.js +39 -1
  154. package/dist/self-hosted-start.js.map +1 -1
  155. package/dist/self-hosted-upgrade.d.ts +19 -7
  156. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  157. package/dist/self-hosted-upgrade.js +78 -25
  158. package/dist/self-hosted-upgrade.js.map +1 -1
  159. package/dist/stack-contract.d.ts +234 -28
  160. package/dist/stack-contract.d.ts.map +1 -1
  161. package/dist/stack-contract.js +397 -39
  162. package/dist/stack-contract.js.map +1 -1
  163. package/dist/stack-converge.d.ts +31 -0
  164. package/dist/stack-converge.d.ts.map +1 -1
  165. package/dist/stack-converge.js +71 -30
  166. package/dist/stack-converge.js.map +1 -1
  167. package/dist/staging-database.d.ts +22 -0
  168. package/dist/staging-database.d.ts.map +1 -0
  169. package/dist/staging-database.js +117 -0
  170. package/dist/staging-database.js.map +1 -0
  171. package/dist/upgrade/app-roll.d.ts +25 -4
  172. package/dist/upgrade/app-roll.d.ts.map +1 -1
  173. package/dist/upgrade/app-roll.js +63 -17
  174. package/dist/upgrade/app-roll.js.map +1 -1
  175. package/dist/upgrade/converge.d.ts +5 -0
  176. package/dist/upgrade/converge.d.ts.map +1 -1
  177. package/dist/upgrade/converge.js +207 -20
  178. package/dist/upgrade/converge.js.map +1 -1
  179. package/dist/upgrade/health.d.ts +31 -7
  180. package/dist/upgrade/health.d.ts.map +1 -1
  181. package/dist/upgrade/health.js +41 -17
  182. package/dist/upgrade/health.js.map +1 -1
  183. package/dist/upgrade/image-forms.d.ts +1 -1
  184. package/dist/upgrade/image-forms.d.ts.map +1 -1
  185. package/dist/upgrade/image-forms.js +24 -7
  186. package/dist/upgrade/image-forms.js.map +1 -1
  187. package/dist/upgrade/release-images.d.ts +12 -3
  188. package/dist/upgrade/release-images.d.ts.map +1 -1
  189. package/dist/upgrade/release-images.js +13 -4
  190. package/dist/upgrade/release-images.js.map +1 -1
  191. package/dist/upgrade/upgrade.d.ts +19 -3
  192. package/dist/upgrade/upgrade.d.ts.map +1 -1
  193. package/dist/upgrade/upgrade.js +37 -2
  194. package/dist/upgrade/upgrade.js.map +1 -1
  195. package/dist/upgrader-gcp.d.ts +13 -0
  196. package/dist/upgrader-gcp.d.ts.map +1 -1
  197. package/dist/upgrader-gcp.js +50 -1
  198. package/dist/upgrader-gcp.js.map +1 -1
  199. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  200. package/dist/upgrader-iam-preflight.js +6 -1
  201. package/dist/upgrader-iam-preflight.js.map +1 -1
  202. package/dist/upgrader-job.d.ts +1 -1
  203. package/dist/upgrader-job.d.ts.map +1 -1
  204. package/dist/upgrader-job.js +20 -14
  205. package/dist/upgrader-job.js.map +1 -1
  206. package/dist/upgrader-records.d.ts +1 -1
  207. package/dist/upgrader-records.d.ts.map +1 -1
  208. package/dist/upgrader-records.js +26 -7
  209. package/dist/upgrader-records.js.map +1 -1
  210. package/dist/upgrader-setup.d.ts.map +1 -1
  211. package/dist/upgrader-setup.js +10 -5
  212. package/dist/upgrader-setup.js.map +1 -1
  213. package/dist/wizard.d.ts +7 -0
  214. package/dist/wizard.d.ts.map +1 -1
  215. package/dist/wizard.js +11 -0
  216. package/dist/wizard.js.map +1 -1
  217. package/dist/wizards/gcp-cost.d.ts +31 -0
  218. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  219. package/dist/wizards/gcp-cost.js +103 -18
  220. package/dist/wizards/gcp-cost.js.map +1 -1
  221. package/dist/wizards/gcp.d.ts.map +1 -1
  222. package/dist/wizards/gcp.js +129 -27
  223. package/dist/wizards/gcp.js.map +1 -1
  224. package/package.json +2 -2
  225. package/src/adapter.ts +6 -0
  226. package/src/admin-domain.test.ts +9 -8
  227. package/src/admin-domain.ts +13 -3
  228. package/src/admin-memory.test.ts +2 -1
  229. package/src/adopt-existing.test.ts +85 -0
  230. package/src/adopt-existing.ts +141 -3
  231. package/src/agent/command.test.ts +49 -1
  232. package/src/agent/command.ts +39 -9
  233. package/src/agent/run.test.ts +19 -0
  234. package/src/agent/run.ts +11 -2
  235. package/src/aws-task-roles.ts +5 -2
  236. package/src/caddy-redirects.docker.test.ts +2 -0
  237. package/src/caddy-redirects.test.ts +4 -1
  238. package/src/caddy-validate.test.ts +7 -1
  239. package/src/caddy.test.ts +39 -1
  240. package/src/caddy.ts +36 -4
  241. package/src/checkpoint-secrets.test.ts +14 -6
  242. package/src/cli-routing.test.ts +1 -1
  243. package/src/cli-routing.ts +1 -1
  244. package/src/cli.ts +71 -5
  245. package/src/cloudsql-tls.test.ts +255 -0
  246. package/src/cloudsql-tls.ts +359 -0
  247. package/src/compose.ts +193 -24
  248. package/src/converge-checkpoint.ts +8 -1
  249. package/src/converge-db-trust.test.ts +237 -0
  250. package/src/converge-db-trust.ts +212 -0
  251. package/src/converge-job-gcp.ts +11 -12
  252. package/src/converge-job-preflight.test.ts +5 -0
  253. package/src/converge-job-preflight.ts +8 -0
  254. package/src/converge-job.test.ts +70 -0
  255. package/src/converge-job.ts +32 -0
  256. package/src/database-tls-builders.test.ts +56 -0
  257. package/src/deployed-release.test.ts +107 -40
  258. package/src/deployed-release.ts +30 -9
  259. package/src/gateway-env-isolation.test.ts +50 -12
  260. package/src/gcp-names.ts +56 -0
  261. package/src/gcp-url-map.test.ts +6 -8
  262. package/src/gcp-url-map.ts +6 -2
  263. package/src/index.test.ts +10 -4
  264. package/src/index.ts +7 -0
  265. package/src/infra-contract.test.ts +7 -3
  266. package/src/infra-contract.ts +45 -1
  267. package/src/install-job.ts +4 -1
  268. package/src/install-state.ts +33 -4
  269. package/src/lifecycle.ts +23 -11
  270. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  271. package/src/mcp-auth-proxy-setup.ts +374 -0
  272. package/src/mcp-iap-stacks.test.ts +1 -1
  273. package/src/media-volume.test.ts +1 -1
  274. package/src/migration-runner.ts +14 -9
  275. package/src/migration-targets.ts +12 -0
  276. package/src/operator-access-stacks.test.ts +1 -1
  277. package/src/operator-access.test.ts +20 -4
  278. package/src/operator-access.ts +14 -0
  279. package/src/pool-budget-stack.ts +15 -0
  280. package/src/pool-budget-stacks.test.ts +39 -14
  281. package/src/public-role-isolation.test.ts +18 -8
  282. package/src/public-role-isolation.ts +31 -17
  283. package/src/release-contract.ts +3 -0
  284. package/src/release-manifest.test.ts +25 -0
  285. package/src/release-manifest.ts +59 -4
  286. package/src/remote-mcp-stack-config.test.ts +19 -9
  287. package/src/remote-mcp.test.ts +252 -57
  288. package/src/remote-mcp.ts +336 -77
  289. package/src/runtime-secrets.test.ts +37 -14
  290. package/src/runtime-secrets.ts +87 -27
  291. package/src/scheduler-setup.test.ts +201 -0
  292. package/src/scheduler-setup.ts +375 -0
  293. package/src/seal-key-rotation.test.ts +2 -2
  294. package/src/seal-key-rotation.ts +25 -25
  295. package/src/self-hosted-backup.test.ts +1 -0
  296. package/src/self-hosted-compose.test.ts +180 -6
  297. package/src/self-hosted-db-tls.test.ts +111 -0
  298. package/src/self-hosted-db-tls.ts +287 -0
  299. package/src/self-hosted-release-resolve.test.ts +71 -0
  300. package/src/self-hosted-release.ts +41 -11
  301. package/src/self-hosted-roles.integration.test.ts +2 -0
  302. package/src/self-hosted-roles.test.ts +87 -3
  303. package/src/self-hosted-roles.ts +101 -3
  304. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  305. package/src/self-hosted-scheduler-interval.ts +79 -0
  306. package/src/self-hosted-start.test.ts +35 -2
  307. package/src/self-hosted-start.ts +20 -2
  308. package/src/self-hosted-upgrade.test.ts +104 -14
  309. package/src/self-hosted-upgrade.ts +123 -31
  310. package/src/stack-contract-parity.test.ts +79 -12
  311. package/src/stack-contract.test.ts +199 -31
  312. package/src/stack-contract.ts +472 -45
  313. package/src/stack-converge-secrets.test.ts +28 -13
  314. package/src/stack-converge.test.ts +203 -28
  315. package/src/stack-converge.ts +92 -30
  316. package/src/staging-database.test.ts +114 -0
  317. package/src/staging-database.ts +148 -0
  318. package/src/state-store.test.ts +2 -1
  319. package/src/upgrade/app-roll.ts +97 -19
  320. package/src/upgrade/converge.ts +249 -19
  321. package/src/upgrade/health.test.ts +63 -25
  322. package/src/upgrade/health.ts +54 -22
  323. package/src/upgrade/image-forms.test.ts +34 -0
  324. package/src/upgrade/image-forms.ts +23 -8
  325. package/src/upgrade/release-images.test.ts +5 -1
  326. package/src/upgrade/release-images.ts +22 -6
  327. package/src/upgrade/upgrade.test.ts +438 -46
  328. package/src/upgrade/upgrade.ts +64 -6
  329. package/src/upgrader-gcp.ts +66 -3
  330. package/src/upgrader-iam-preflight.test.ts +19 -0
  331. package/src/upgrader-iam-preflight.ts +6 -0
  332. package/src/upgrader-job.test.ts +101 -27
  333. package/src/upgrader-job.ts +27 -14
  334. package/src/upgrader-records.test.ts +97 -12
  335. package/src/upgrader-records.ts +28 -11
  336. package/src/upgrader-setup.test.ts +2 -0
  337. package/src/upgrader-setup.ts +10 -4
  338. package/src/wizard.ts +19 -0
  339. package/src/wizards/gcp-cost.test.ts +103 -0
  340. package/src/wizards/gcp-cost.ts +125 -18
  341. package/src/wizards/gcp.ts +164 -22
  342. package/stacks/aws/Pulumi.yaml +1 -1
  343. package/stacks/aws/index.ts +72 -14
  344. package/stacks/azure/index.ts +108 -21
  345. package/stacks/gcp/Pulumi.yaml +1 -1
  346. package/stacks/gcp/README.md +1 -0
  347. package/stacks/gcp/index.ts +133 -24
  348. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  349. package/stacks/gcp-firebase/index.ts +212 -34
  350. package/stacks/self-hosted/Pulumi.yaml +3 -0
  351. package/stacks/self-hosted/README.md +6 -1
  352. package/stacks/self-hosted/index.ts +40 -5
@@ -25,15 +25,23 @@
25
25
  */
26
26
 
27
27
  import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
28
- import { gatewayServiceAccountEmail, runServiceAccountEmail } from "../gcp-names.js";
28
+ import {
29
+ gatewayServiceAccountEmail,
30
+ mcpAuthProxyServiceAccountEmail,
31
+ runServiceAccountEmail,
32
+ } from "../gcp-names.js";
29
33
  import type { ImageDigests } from "../install-state.js";
30
34
  import {
31
35
  adminMediaVolume,
36
+ CLOUD_RUN_SLUGS,
32
37
  type CloudRunEnvVar,
33
- GATEWAY_TIMEOUT_SECONDS,
38
+ type CloudRunSlug,
39
+ cloudRunServicePrefix,
40
+ MCP_AUTH_PROXY_TIMEOUT_SECONDS,
34
41
  RETIRED_SECRET_FILES,
35
42
  SERVICE_CPU_IDLE,
36
43
  SERVICE_SECRET_FILES,
44
+ SERVICE_VPC_EGRESS,
37
45
  } from "../stack-contract.js";
38
46
  import {
39
47
  type DeployedService,
@@ -44,12 +52,14 @@ import {
44
52
  liveContainerEnv,
45
53
  liveContainerMemory,
46
54
  liveCpuIdle,
55
+ liveDirectVpc,
47
56
  liveTimeoutSeconds,
48
57
  liveVolumes,
49
58
  planAdminMemory,
50
59
  planContractEnv,
51
60
  planCpuIdle,
52
61
  planMediaVolume,
62
+ planNoVpc,
53
63
  planRequestTimeout,
54
64
  planSecretFiles,
55
65
  rollService,
@@ -69,10 +79,27 @@ import {
69
79
  import type { HealthProbe } from "./health.js";
70
80
  import type { UpgradeReporter } from "./reporter.js";
71
81
 
72
- export type RollSlug = "admin" | "gateway";
82
+ export type RollSlug = CloudRunSlug;
73
83
 
74
- /** The roll order: the gateway follows the admin, never the other way round. */
75
- export const ROLL_ORDER: readonly RollSlug[] = ["admin", "gateway"];
84
+ /**
85
+ * The roll order: the gateway follows the admin, never the other way round,
86
+ * and the MCP-Auth-Proxy comes last (#1328) — on gcp-firebase it forwards
87
+ * the visitor traffic still addressed to it to the gateway, which must
88
+ * already serve the new release by then.
89
+ */
90
+ export const ROLL_ORDER: readonly RollSlug[] = CLOUD_RUN_SLUGS;
91
+
92
+ /** The service account each service's new revision runs as. */
93
+ export function rollServiceAccount(slug: RollSlug, projectId: string, env: string): string {
94
+ switch (slug) {
95
+ case "admin":
96
+ return runServiceAccountEmail(projectId, env);
97
+ case "gateway":
98
+ return gatewayServiceAccountEmail(projectId, env);
99
+ case "mcp-auth-proxy":
100
+ return mcpAuthProxyServiceAccountEmail(projectId, env);
101
+ }
102
+ }
76
103
 
77
104
  /** The install an app-roll acts on. */
78
105
  export interface AppRollInstall {
@@ -110,6 +137,8 @@ export interface ServicePlan {
110
137
  readonly liveTimeout: number | null;
111
138
  /** Whether the service gets CPU only during requests now. */
112
139
  readonly liveCpuIdle: boolean;
140
+ /** Whether the service egresses into the install's VPC now. */
141
+ readonly liveDirectVpc: boolean;
113
142
  }
114
143
 
115
144
  /** A service plan plus the changes its roll applies. */
@@ -139,6 +168,13 @@ export interface AppRollPlan {
139
168
  readonly services: readonly RollPlan[];
140
169
  /** Notes for the operator (e.g. a memory limit left as is). */
141
170
  readonly warnings: readonly string[];
171
+ /**
172
+ * #1338 — the services dial the Cloud SQL instance's IP, which no
173
+ * verify-full connection can use, so their database URLs are not planned.
174
+ * A converge moves them onto the instance's DNS name
175
+ * ({@link EnvAdjustments.databaseHost}); a roll refuses while this holds.
176
+ */
177
+ readonly databaseHostPending?: boolean;
142
178
  }
143
179
 
144
180
  export type PlanResult =
@@ -166,7 +202,7 @@ export async function planAppRoll(
166
202
  ): Promise<PlanResult> {
167
203
  const services: ServicePlan[] = [];
168
204
  for (const slug of ROLL_ORDER) {
169
- const prefix = `caelo-${install.env}-${slug}`;
205
+ const prefix = cloudRunServicePrefix(install.provider, install.env, slug);
170
206
  const lookup = await resolveGcpResourceName(
171
207
  run,
172
208
  "run-service",
@@ -210,6 +246,7 @@ export async function planAppRoll(
210
246
  liveVolumes: liveVolumes(json),
211
247
  liveTimeout: liveTimeoutSeconds(json),
212
248
  liveCpuIdle: liveCpuIdle(json),
249
+ liveDirectVpc: liveDirectVpc(json),
213
250
  });
214
251
  }
215
252
  const env = planEnv(install, services, {});
@@ -265,29 +302,45 @@ export async function planAppRoll(
265
302
  // stack's memory default (never lowered; an unparsable value is kept).
266
303
  const memory = s.slug === "admin" ? planAdminMemory(s.liveMemory) : null;
267
304
  if (memory && !memory.ok) warnings.push(`admin memory left as is: ${memory.error}`);
268
- // #806 — the gateway streams remote-MCP chat turns (stack-contract.ts
269
- // GATEWAY_TIMEOUT_SECONDS). The admin's timeout is the stacks' alone.
305
+ // #806 — the MCP-Auth-Proxy streams remote-MCP chat turns
306
+ // (stack-contract.ts MCP_AUTH_PROXY_TIMEOUT_SECONDS). The other services'
307
+ // timeouts are the stacks' alone.
270
308
  const timeout =
271
- s.slug === "gateway" ? planRequestTimeout(s.liveTimeout, GATEWAY_TIMEOUT_SECONDS) : null;
309
+ s.slug === "mcp-auth-proxy"
310
+ ? planRequestTimeout(s.liveTimeout, MCP_AUTH_PROXY_TIMEOUT_SECONDS)
311
+ : null;
272
312
  const memoryFlags = memory?.ok ? memory.flags : [];
273
313
  const timeoutFlags = timeout?.ok ? timeout.flags : [];
274
314
  const cpu = planCpuIdle(s.liveCpuIdle, SERVICE_CPU_IDLE[s.slug]);
275
315
  const cpuFlags = cpu.ok ? cpu.flags : [];
316
+ // #1328 — the gcp-firebase proxy is the former gateway service: it
317
+ // leaves the VPC in the same revision that drops its database env.
318
+ const vpc = SERVICE_VPC_EGRESS[s.slug] ? null : planNoVpc(s.liveDirectVpc);
319
+ const vpcFlags = vpc?.ok ? vpc.flags : [];
276
320
  return {
277
321
  ...s,
278
322
  envFlags: env.services[s.slug].flags,
279
323
  envChanges: env.services[s.slug].changes,
280
- resourceFlags: [...memoryFlags, ...timeoutFlags, ...cpuFlags],
324
+ resourceFlags: [...memoryFlags, ...timeoutFlags, ...cpuFlags, ...vpcFlags],
281
325
  resourceChanges: [
282
326
  ...memoryFlags.map((f) => `${s.liveMemory ?? "(default)"} → ${f}`),
283
327
  ...(timeout?.ok ? timeout.changes : []),
284
328
  ...(cpu.ok ? cpu.changes : []),
329
+ ...(vpc?.ok ? vpc.changes : []),
285
330
  ],
286
331
  volumeFlags: [...(s.slug === "admin" ? media.flags : []), ...secretFiles[s.slug].flags],
287
332
  volumeChanges: [...(s.slug === "admin" ? media.changes : []), ...secretFiles[s.slug].changes],
288
333
  };
289
334
  });
290
- return { ok: true, plan: { install, services: rolls, warnings } };
335
+ if (env.databaseHostPending) {
336
+ warnings.push(
337
+ "the services still dial the Cloud SQL instance's IP; the converge moves them onto its DNS name for verify-full TLS (#1338) before anything rolls",
338
+ );
339
+ }
340
+ return {
341
+ ok: true,
342
+ plan: { install, services: rolls, warnings, databaseHostPending: env.databaseHostPending },
343
+ };
291
344
  }
292
345
 
293
346
  /** Env the converge half adds to, or keeps out of, the rolls ({@link AppRollHooks.afterMigrations}). */
@@ -299,6 +352,11 @@ export interface EnvAdjustments {
299
352
  * identity env, which only the converge (operator identity) can resolve.
300
353
  */
301
354
  readonly extraEnv?: Readonly<Partial<Record<RollSlug, readonly CloudRunEnvVar[]>>>;
355
+ /**
356
+ * #1338 — the database host the converge moved the services onto (the
357
+ * Cloud SQL instance's DNS name, cloudsql-tls.ts).
358
+ */
359
+ readonly databaseHost?: string;
302
360
  }
303
361
 
304
362
  function planEnv(
@@ -322,6 +380,7 @@ function planEnv(
322
380
  {
323
381
  leaveUntouched: adjust.leaveUntouched ?? [],
324
382
  ...(adjust.extraEnv ? { extraEnv: adjust.extraEnv } : {}),
383
+ ...(adjust.databaseHost ? { databaseHost: adjust.databaseHost } : {}),
325
384
  },
326
385
  );
327
386
  }
@@ -333,11 +392,12 @@ function planEnv(
333
392
  */
334
393
  export function adjustingEnv(plan: AppRollPlan, adjust: EnvAdjustments): AppRollPlan {
335
394
  const extra = Object.values(adjust.extraEnv ?? {}).some((v) => (v?.length ?? 0) > 0);
336
- if ((adjust.leaveUntouched?.length ?? 0) === 0 && !extra) return plan;
395
+ if ((adjust.leaveUntouched?.length ?? 0) === 0 && !extra && !adjust.databaseHost) return plan;
337
396
  const env = planEnv(plan.install, plan.services, adjust);
338
397
  if (!env.ok) return plan;
339
398
  return {
340
399
  ...plan,
400
+ databaseHostPending: env.databaseHostPending,
341
401
  services: plan.services.map((s) => ({
342
402
  ...s,
343
403
  envFlags: env.services[s.slug].flags,
@@ -370,7 +430,7 @@ export interface AppRollHooks {
370
430
  afterMigrations?(): Promise<
371
431
  | ({ readonly ok: true; readonly leaveUntouched: readonly string[] } & Pick<
372
432
  EnvAdjustments,
373
- "extraEnv"
433
+ "extraEnv" | "databaseHost"
374
434
  >)
375
435
  | { readonly ok: false }
376
436
  >;
@@ -470,6 +530,17 @@ export async function executeAppRoll(
470
530
  r.warn("--skip-health-check set — the new revisions get Cloud Run's readiness check only.");
471
531
  }
472
532
 
533
+ // #1338 — the new images refuse a database URL without verify-full, and
534
+ // only a converge can move the services off the instance's IP. Without
535
+ // one (the in-cloud upgrader) the roll stops before it changes anything.
536
+ if (plan.databaseHostPending && !hooks.afterMigrations) {
537
+ r.error(
538
+ "The services still dial the Cloud SQL instance's IP address. This release connects with verify-full TLS, which needs the instance's DNS name: run the converge first (Settings → Updates installs it as a cloud-setup release, or `cms-provision upgrade` from a terminal).",
539
+ );
540
+ r.warn("No migrations ran and no traffic moved.");
541
+ return fail("migrations", false);
542
+ }
543
+
473
544
  // Migrations BEFORE any traffic moves, with the NEW admin image: the old
474
545
  // image does not carry the release's migration files (v0.9.2).
475
546
  const admin = plan.services.find((s) => s.slug === "admin") as RollPlan;
@@ -490,8 +561,15 @@ export async function executeAppRoll(
490
561
  plan = adjustingEnv(plan, {
491
562
  leaveUntouched: converged.leaveUntouched,
492
563
  ...(converged.extraEnv ? { extraEnv: converged.extraEnv } : {}),
564
+ ...(converged.databaseHost ? { databaseHost: converged.databaseHost } : {}),
493
565
  });
494
566
  }
567
+ if (plan.databaseHostPending) {
568
+ r.error(
569
+ "The converge did not move the services onto the Cloud SQL instance's DNS name, so the new revisions could not reach the database with verify-full TLS (#1338). Nothing rolled; fix the database TLS step above and re-run.",
570
+ );
571
+ return fail("converge", true);
572
+ }
495
573
 
496
574
  const rolled: RollPlan[] = [];
497
575
  /** Jobs this roll moved onto the admin's image (#875, #1217), with their prior image. */
@@ -540,10 +618,7 @@ export async function executeAppRoll(
540
618
  region: install.region,
541
619
  projectId: install.projectId,
542
620
  imageRef: p.imageRef,
543
- serviceAccount:
544
- p.slug === "admin"
545
- ? runServiceAccountEmail(install.projectId, install.env)
546
- : gatewayServiceAccountEmail(install.projectId, install.env),
621
+ serviceAccount: rollServiceAccount(p.slug, install.projectId, install.env),
547
622
  envFlags: p.envFlags,
548
623
  resourceFlags: p.resourceFlags,
549
624
  volumeFlags: p.volumeFlags,
@@ -637,9 +712,12 @@ export async function executeAppRoll(
637
712
  s.done(`Healthy: ${deep.detail}`);
638
713
  }
639
714
 
715
+ const digestOf = (slug: RollSlug) =>
716
+ (plan.services.find((s) => s.slug === slug) as RollPlan).digest;
640
717
  const digests: ImageDigests = {
641
- admin: (plan.services.find((s) => s.slug === "admin") as RollPlan).digest,
642
- gateway: (plan.services.find((s) => s.slug === "gateway") as RollPlan).digest,
718
+ admin: digestOf("admin"),
719
+ gateway: digestOf("gateway"),
720
+ "mcp-auth-proxy": digestOf("mcp-auth-proxy"),
643
721
  };
644
722
  await deps.recordDigests(digests);
645
723
  return { ok: true, digests };
@@ -5,7 +5,8 @@
5
5
  * service accounts, secrets or other cloud resources so the install matches
6
6
  * what the release's stacks declare — the gateway's run SA and the
7
7
  * CLI-generated runtime secrets, the stack's IAM/CDN/firewall invariants,
8
- * MCP access through IAP, public_role's Cloud SQL role memberships (#633),
8
+ * MCP access through IAP, the staging database and its gateway login
9
+ * (#1329), the gateway logins' Cloud SQL role memberships (#633),
9
10
  * the gateway's and public_role's database logins, the operator-access sync
10
11
  * job, the quality-audit job (#875), the remote-MCP load-balancer rules and
11
12
  * identity env (#806), and dropping bindings nothing reads any more.
@@ -30,10 +31,16 @@
30
31
 
31
32
  import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
32
33
  import { ensureCaptureJob } from "../capture-job-setup.js";
34
+ import {
35
+ dialsIpAddress,
36
+ ensureCloudSqlVerifyFull,
37
+ liveDatabaseUrls,
38
+ moveServicesToDnsHost,
39
+ } from "../cloudsql-tls.js";
33
40
  import { ensureConvergeJob } from "../converge-job-setup.js";
34
41
  import { findSqlInstance, planPublicRoleSwitch } from "../gateway-credentials.js";
35
42
  import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
36
- import { upgraderServiceAccountEmail } from "../gcp-names.js";
43
+ import { schedulerServiceAccountEmail, upgraderServiceAccountEmail } from "../gcp-names.js";
37
44
  import type { InstallMetadata } from "../install-state.js";
38
45
  import { ensureMcpIapAccess, type IapResource } from "../mcp-iap.js";
39
46
  import { ensureOperatorAccessSync, resolveOperatorAccessTarget } from "../operator-access.js";
@@ -46,6 +53,7 @@ import {
46
53
  plainGeneratedSecretSeed,
47
54
  readSecretReplication,
48
55
  } from "../runtime-secrets.js";
56
+ import { ensureSchedulerTick } from "../scheduler-setup.js";
49
57
  import { selfCommand } from "../self-command.js";
50
58
  import {
51
59
  AUDIT_JOB_ENV_VAR,
@@ -54,9 +62,13 @@ import {
54
62
  CAPTURE_JOB_ENV_VAR,
55
63
  type CloudRunEnvVar,
56
64
  CONVERGE_JOB_ENV_VAR,
65
+ DATABASE_URL_VARS,
66
+ INTERIM_FORWARD_ORIGIN_ENV,
57
67
  MCP_ENV_VAR,
58
68
  OPERATOR_ACCESS_JOB_ENV_VAR,
69
+ parseSchedulerIntervalMinutes,
59
70
  remoteMcpIdentityEnv,
71
+ SCHEDULER_SERVICE_ACCOUNT_ENV_VAR,
60
72
  UPGRADER_JOB_ENV_VAR,
61
73
  } from "../stack-contract.js";
62
74
  import {
@@ -65,10 +77,11 @@ import {
65
77
  liveEnvHasInlinePassword,
66
78
  removeRetiredIamBindings,
67
79
  } from "../stack-converge.js";
80
+ import { ensureStagingDatabase } from "../staging-database.js";
68
81
  import { ensureUpgraderJob, resolveUpgraderTarget } from "../upgrader-setup.js";
69
82
  import { ensureUrlMapRedirectsAccess } from "../url-map-redirects-access.js";
70
83
  import type { AppRollHooks, AppRollPlan, RollPlan, RollSlug } from "./app-roll.js";
71
- import { lookupFailure } from "./gcp-lookup.js";
84
+ import { findServiceUrl, lookupFailure } from "./gcp-lookup.js";
72
85
  import type { UpgradeReporter } from "./reporter.js";
73
86
 
74
87
  /**
@@ -102,6 +115,11 @@ export interface ConvergeDeps {
102
115
  * then fails and the converge stays incomplete.
103
116
  */
104
117
  readonly opsImageRef?: string | null;
118
+ /**
119
+ * #1140 — the install's `schedulerIntervalMinutes` knob (install.json);
120
+ * absent: the default. The scheduler step refuses an out-of-range value.
121
+ */
122
+ readonly schedulerIntervalMinutes?: number;
105
123
  }
106
124
 
107
125
  export interface ConvergeSession {
@@ -139,7 +157,11 @@ export function startConverge(
139
157
  const { reporter: r, run } = deps;
140
158
  const runDeps = { run, ...(deps.sleep ? { sleep: deps.sleep } : {}) };
141
159
  const service = (slug: RollSlug) => plan.services.find((s) => s.slug === slug) as RollPlan;
142
- const services = { admin: service("admin").serviceName, gateway: service("gateway").serviceName };
160
+ const services = {
161
+ admin: service("admin").serviceName,
162
+ gateway: service("gateway").serviceName,
163
+ "mcp-auth-proxy": service("mcp-auth-proxy").serviceName,
164
+ };
143
165
  const target = {
144
166
  provider: install.provider,
145
167
  projectId: install.projectId,
@@ -222,22 +244,48 @@ export function startConverge(
222
244
  return true;
223
245
  }
224
246
 
247
+ async function stagingDatabase(): Promise<boolean> {
248
+ // #1329 — staging's own database and its gateway login, on an install
249
+ // from before them. Before migrations: the cms_public_staging run and
250
+ // migration 0530 need both, and the membership revoke below needs the
251
+ // user. A failure aborts — the new admin refuses to start without the
252
+ // staging database.
253
+ const s = r.step("Ensuring the staging database (cms_public_staging) and its gateway login...");
254
+ const outcomes = await ensureStagingDatabase(
255
+ { projectId: install.projectId, env: install.env, credentials: install.credentials },
256
+ { ...runDeps, ...(deps.http ? { http: deps.http } : {}) },
257
+ );
258
+ const failed = outcomes.filter((o) => o.status === "failed");
259
+ if (failed.length > 0) {
260
+ s.failed("The staging database could not be set up. Aborting upgrade.");
261
+ for (const o of failed) r.error(` FAILED: ${o.id}\n ${o.error ?? ""}`);
262
+ r.warn("No traffic was shifted and no migrations ran. Fix the above and re-run.");
263
+ return false;
264
+ }
265
+ s.done("Staging database and its gateway login ready");
266
+ for (const o of outcomes.filter((o) => o.status === "applied")) {
267
+ r.info(` ${o.id}: ${o.detail ?? "created"}`);
268
+ }
269
+ return true;
270
+ }
271
+
225
272
  async function publicRoleIsolation(): Promise<boolean> {
226
- // #633 — the Admin API made public_role a member of cloudsqlsuperuser.
227
- // Revoked here, before migrations, because the migration job's role
228
- // audit is what verifies it and fails the upgrade while it remains.
229
- const s = r.step("Removing public_role from the Cloud SQL superuser group...");
273
+ // #633 — the Admin API made public_role (and, #1329, public_staging_role)
274
+ // a member of cloudsqlsuperuser. Revoked here, before migrations,
275
+ // because the migration job's role audit is what verifies it and fails
276
+ // the upgrade while it remains.
277
+ const s = r.step("Removing the gateway logins from the Cloud SQL superuser group...");
230
278
  const outcome = await revokePublicRoleMemberships(
231
279
  { projectId: install.projectId, env: install.env, credentials: install.credentials },
232
280
  runDeps,
233
281
  );
234
282
  if (outcome.status === "failed") {
235
- s.failed("public_role's role memberships could not be revoked. Aborting upgrade.");
283
+ s.failed("The gateway logins' role memberships could not be revoked. Aborting upgrade.");
236
284
  r.error(` ${outcome.error ?? ""}`);
237
285
  r.warn("No traffic was shifted and no migrations ran. Fix the above and re-run.");
238
286
  return false;
239
287
  }
240
- s.done(`public_role holds no database roles (${outcome.detail ?? "revoked"})`);
288
+ s.done(`The gateway logins hold no database roles (${outcome.detail ?? "revoked"})`);
241
289
  return true;
242
290
  }
243
291
 
@@ -371,33 +419,53 @@ export function startConverge(
371
419
  }
372
420
 
373
421
  async function remoteMcpEdge(): Promise<void> {
374
- // #806 — on `gcp` the load balancer routes the remote-MCP discovery
375
- // paths to the gateway and Cloud Armor lets agent traffic through on
376
- // /api/mcp/* (remote-mcp-edge.ts). Only remote MCP depends on it, so a
377
- // failure warns.
422
+ // #806 / #1328 — on `gcp` the load balancer routes the remote-MCP paths
423
+ // to the MCP-Auth-Proxy's own backend, whose Cloud Armor policy lets
424
+ // agent traffic through on /api/mcp/* (remote-mcp-edge.ts). Only remote
425
+ // MCP depends on it, so a failure warns. The gateway being rolled in
426
+ // this run no longer answers those paths, so until this succeeds remote
427
+ // MCP is down on the site's domain — said below.
378
428
  if (install.provider !== "gcp") return;
379
- const s = r.step("Ensuring the load balancer's remote-MCP routes and Cloud Armor rule...");
429
+ const s = r.step("Ensuring the MCP-Auth-Proxy's load-balancer route and Cloud Armor rule...");
380
430
  const edge = await ensureRemoteMcpEdge(
381
431
  {
382
432
  provider: install.provider,
383
433
  projectId: install.projectId,
434
+ region: install.region,
384
435
  env: install.env,
385
436
  domain: install.domain,
437
+ mcpAuthProxyImageRef: service("mcp-auth-proxy").imageRef,
386
438
  },
387
439
  { ...runDeps, ...(deps.http ? { http: deps.http } : {}) },
388
440
  );
389
441
  if (edge.ok) {
390
442
  for (const line of edge.done) r.info(` ${line}`);
391
- s.done("Remote-MCP load-balancer rules ready");
443
+ s.done("Remote-MCP load-balancer route ready");
392
444
  return;
393
445
  }
394
- s.warned(`Remote-MCP load-balancer rules not set up: ${edge.error}`);
446
+ s.warned(`Remote-MCP load-balancer route not set up: ${edge.error}`);
395
447
  r.warn(
396
448
  "The upgrade continues; remote MCP (Settings → MCP → Remote) cannot be reached through the site's domain until this succeeds. Re-run upgrade once the cause is fixed.",
397
449
  );
398
450
  incomplete = true;
399
451
  }
400
452
 
453
+ async function interimForwardEnv(): Promise<CloudRunEnvVar[]> {
454
+ // #1328 — on gcp-firebase the proxy is the service the current live
455
+ // release's Firebase rewrite and the staging domain mapping still
456
+ // point at; it forwards that traffic to the data gateway, whose URL is
457
+ // its origin (stack-contract.ts INTERIM_FORWARD_ORIGIN_ENV). Without it
458
+ // the proxy answers the live site's /api/** with 404 until the next
459
+ // Publish live, and the staging host with 404 — so this aborts.
460
+ if (install.provider !== "gcp-firebase") return [];
461
+ const url = await findServiceUrl(run, install.projectId, install.region, services.gateway);
462
+ if (!url) throw new Error(`could not read the URL of ${services.gateway}`);
463
+ return [{ name: INTERIM_FORWARD_ORIGIN_ENV, value: new URL(url).origin }];
464
+ }
465
+
466
+ /** The admin's origin behind IAP, once {@link remoteMcpIdentity} read it. */
467
+ let resolvedAdminUrl: string | null = null;
468
+
401
469
  async function remoteMcpIdentity(): Promise<Record<RollSlug, CloudRunEnvVar[]> | null> {
402
470
  // #806 — CAELO_ADMIN_URL / CAELO_MCP_PUBLIC_URL / CAELO_IAP_AUDIENCE
403
471
  // name identities the env contract cannot (stack-contract.ts
@@ -420,6 +488,7 @@ export function startConverge(
420
488
  runDeps,
421
489
  );
422
490
  if (resolved.ok) {
491
+ resolvedAdminUrl = resolved.identity.adminUrl;
423
492
  s.done(
424
493
  `Google sign-in and remote MCP can be set up under Settings → Google sign-in (${resolved.identity.mcpPublicUrl})`,
425
494
  );
@@ -459,6 +528,71 @@ export function startConverge(
459
528
  return name ? { kind: "backend-services", service: name } : null;
460
529
  }
461
530
 
531
+ /** #1338 — the host the database TLS step moved the services onto. */
532
+ let databaseHost: string | null = null;
533
+
534
+ async function databaseTls(): Promise<boolean> {
535
+ // #1338 — verify-full TLS needs the shared server CA, the instance's
536
+ // DNS name resolving in the VPC, and the services dialling that name
537
+ // (cloudsql-tls.ts). Before migrations: the migration job reads its host
538
+ // from the admin's live URL, and the new images refuse anything less
539
+ // than verify-full. A failure aborts — the new revisions could not
540
+ // reach the database.
541
+ const s = r.step("Setting up verify-full TLS to the database (shared CA, private DNS name)...");
542
+ const liveUrls = plan.services.map((p) => ({
543
+ serviceName: p.serviceName,
544
+ liveUrls: liveDatabaseUrls(p.liveEnv, DATABASE_URL_VARS[p.slug]),
545
+ }));
546
+ // Only this step moves the services off the instance's IP, and only
547
+ // after the instance, its certificate and the DNS name are in shape —
548
+ // so services that dial a host name already went through it.
549
+ const onIp = liveUrls.some((u) => Object.values(u.liveUrls).some((v) => dialsIpAddress(v)));
550
+ if (!onIp) {
551
+ s.done("Database TLS ready: the services already dial the instance's DNS name");
552
+ return true;
553
+ }
554
+ const lookup = await findSqlInstance(install, { run });
555
+ if (lookup.status !== "found") {
556
+ s.failed(
557
+ `${lookupFailure(lookup, "Cloud SQL instance", install.credentials)}. Aborting upgrade.`,
558
+ );
559
+ r.warn(
560
+ "No traffic was shifted and no migrations ran. Re-run upgrade once the instance is found.",
561
+ );
562
+ return false;
563
+ }
564
+ const tlsDeps = { ...runDeps, ...(deps.http ? { http: deps.http } : {}) };
565
+ const tls = await ensureCloudSqlVerifyFull(
566
+ { projectId: install.projectId, env: install.env, sqlInstance: lookup.name },
567
+ tlsDeps,
568
+ );
569
+ for (const line of tls.done) r.info(` ${line}`);
570
+ if (!tls.ok) {
571
+ s.failed(`Database TLS could not be set up: ${tls.error}. Aborting upgrade.`);
572
+ r.warn("No traffic was shifted and no migrations ran. Fix the above and re-run.");
573
+ return false;
574
+ }
575
+ const moved = await moveServicesToDnsHost(
576
+ { projectId: install.projectId, region: install.region },
577
+ liveUrls,
578
+ tls.host,
579
+ tlsDeps,
580
+ );
581
+ for (const line of moved.done) r.info(` ${line}`);
582
+ if (!moved.ok) {
583
+ s.failed(
584
+ `The services could not be moved onto ${tls.host}: ${moved.error}. Aborting upgrade.`,
585
+ );
586
+ r.warn("No migrations ran. Fix the above and re-run.");
587
+ return false;
588
+ }
589
+ databaseHost = tls.host;
590
+ s.done(
591
+ `Database TLS ready: the services dial ${tls.host} (verify-full from the new revisions)`,
592
+ );
593
+ return true;
594
+ }
595
+
462
596
  async function databaseLogins(): Promise<boolean> {
463
597
  // #613 — public_role moves onto its own password right before the
464
598
  // gateway's roll. The gateway has no other database login (#635).
@@ -578,6 +712,77 @@ export function startConverge(
578
712
  leaveUntouched.push(CONVERGE_JOB_ENV_VAR);
579
713
  }
580
714
 
715
+ let schedulerReady = false;
716
+ /** The scheduler's SA exists from an earlier run though this one failed. */
717
+ let schedulerAccountExists = false;
718
+
719
+ async function setUpSchedulerTick(): Promise<
720
+ | { ok: true; done: string[]; intervalMinutes: number }
721
+ | { ok: false; done: string[]; error: string }
722
+ > {
723
+ let intervalMinutes: number;
724
+ try {
725
+ intervalMinutes = parseSchedulerIntervalMinutes(
726
+ deps.schedulerIntervalMinutes,
727
+ "install.json schedulerIntervalMinutes",
728
+ );
729
+ } catch (e) {
730
+ return { ok: false, done: [], error: (e as Error).message };
731
+ }
732
+ const resource = await adminIapResource();
733
+ if (!resource) return { ok: false, done: [], error: "the admin's IAP resource was not found" };
734
+ if (!resolvedAdminUrl)
735
+ return { ok: false, done: [], error: "the admin's URL could not be read" };
736
+ const st = await ensureSchedulerTick(
737
+ {
738
+ projectId: install.projectId,
739
+ region: install.region,
740
+ env: install.env,
741
+ adminUrl: resolvedAdminUrl,
742
+ resource,
743
+ intervalMinutes,
744
+ },
745
+ runDeps,
746
+ );
747
+ return st.ok ? { ok: true, done: st.done, intervalMinutes } : st;
748
+ }
749
+
750
+ async function schedulerTick(): Promise<void> {
751
+ // #1140 — the one Cloud Scheduler job that calls the admin's tick
752
+ // (scheduler-setup.ts). After migrations and before the operator-access
753
+ // step, which must keep the scheduler's account on IAP. The schedule
754
+ // starts right away: until the roll below finishes, a tick reaches the
755
+ // previous admin, which has no such endpoint (404) — that execution
756
+ // fails and nothing runs; the first tick after the roll does the work.
757
+ // A failure costs the periodic work (plugin workers, GC, release check),
758
+ // not the site, so it warns and the admin keeps its current env.
759
+ const s = r.step("Ensuring the scheduler tick (periodic work and plugin workers)...");
760
+ const st = await setUpSchedulerTick();
761
+ for (const step of st.done) r.info(` ${step}`);
762
+ if (st.ok) {
763
+ schedulerReady = true;
764
+ s.done(`Scheduler tick ready (every ${st.intervalMinutes} min)`);
765
+ return;
766
+ }
767
+ s.warned(`Scheduler tick not set up: ${st.error}`);
768
+ // An account set up by an earlier run keeps its IAP access, or the next
769
+ // sync would cut off a tick that still works.
770
+ const exists = await run([
771
+ "iam",
772
+ "service-accounts",
773
+ "describe",
774
+ schedulerServiceAccountEmail(install.projectId, install.env),
775
+ `--project=${install.projectId}`,
776
+ "--format=value(email)",
777
+ ]);
778
+ schedulerAccountExists = exists.ok;
779
+ r.warn(
780
+ "The upgrade continues; plugin workers, update checks and clean-up run late or not at all until this succeeds. Re-run upgrade once the cause is fixed.",
781
+ );
782
+ incomplete = true;
783
+ leaveUntouched.push(SCHEDULER_SERVICE_ACCOUNT_ENV_VAR);
784
+ }
785
+
581
786
  async function operatorAccess(): Promise<void> {
582
787
  // The job that keeps IAP in step with the user list (operator-access.ts).
583
788
  // After migrations: its database user gets the role migration 0239
@@ -605,6 +810,13 @@ export function startConverge(
605
810
  ],
606
811
  }
607
812
  : {}),
813
+ ...(schedulerReady || schedulerAccountExists
814
+ ? {
815
+ iapServiceAccounts: [
816
+ schedulerServiceAccountEmail(install.projectId, install.env),
817
+ ],
818
+ }
819
+ : {}),
608
820
  },
609
821
  runDeps,
610
822
  )
@@ -627,7 +839,9 @@ export function startConverge(
627
839
  if (deps.provisionerIdentity && !(await deps.provisionerIdentity())) return false;
628
840
  if (!(await runtimeIdentities())) return false;
629
841
  if (!(await stackInvariants())) return false;
842
+ if (!(await stagingDatabase())) return false;
630
843
  if (!(await publicRoleIsolation())) return false;
844
+ if (!(await databaseTls())) return false;
631
845
  await mcpAccess();
632
846
  await urlMapRedirectsAccess();
633
847
  await auditJob();
@@ -639,14 +853,30 @@ export function startConverge(
639
853
  hooks: {
640
854
  async afterMigrations() {
641
855
  if (!(await databaseLogins())) return { ok: false };
856
+ // Read-only, first: the scheduler tick needs the admin's URL.
857
+ const identity = await remoteMcpIdentity();
642
858
  await upgraderJob();
643
859
  await convergeJob();
860
+ await schedulerTick();
644
861
  await operatorAccess();
645
- const extraEnv = await remoteMcpIdentity();
862
+ let forward: CloudRunEnvVar[];
863
+ try {
864
+ forward = await interimForwardEnv();
865
+ } catch (e) {
866
+ r.error(
867
+ `The MCP-Auth-Proxy cannot be pointed at the data gateway: ${e instanceof Error ? e.message : String(e)}. Nothing was rolled; re-run upgrade once it is fixed.`,
868
+ );
869
+ return { ok: false };
870
+ }
871
+ const extraEnv: Partial<Record<RollSlug, CloudRunEnvVar[]>> = {
872
+ ...(identity ?? {}),
873
+ "mcp-auth-proxy": [...(identity?.["mcp-auth-proxy"] ?? []), ...forward],
874
+ };
646
875
  return {
647
876
  ok: true,
648
877
  leaveUntouched: [...leaveUntouched],
649
- ...(extraEnv ? { extraEnv } : {}),
878
+ extraEnv,
879
+ ...(databaseHost ? { databaseHost } : {}),
650
880
  };
651
881
  },
652
882
  async beforeRoll(slug) {