@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
@@ -11,7 +11,12 @@
11
11
  * + AI calls + storage growth on top.
12
12
  */
13
13
 
14
- import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB } from "../stack-contract.js";
14
+ import {
15
+ ADMIN_MEMORY_DEFAULT,
16
+ memoryQuantityMiB,
17
+ parseSchedulerIntervalMinutes,
18
+ SERVICE_CPU_IDLE,
19
+ } from "../stack-contract.js";
15
20
 
16
21
  export interface CostLine {
17
22
  name: string;
@@ -41,6 +46,82 @@ export interface CostEstimateInputs {
41
46
  provider?: "gcp" | "gcp-firebase";
42
47
  /** #607 — the install region; shown in the table (prices are {@link PRICED_REGION} rates). */
43
48
  region: string;
49
+ /** #1140 — the `schedulerIntervalMinutes` knob (1–15); undefined: the default. */
50
+ schedulerIntervalMinutes?: number;
51
+ }
52
+
53
+ /**
54
+ * #1140 — Workflows steps one scheduler tick costs (scheduler-setup.ts
55
+ * schedulerWorkflowSource): the claims `assign`, the signJwt call (a Google
56
+ * API, billed as an internal step), the `return` — and the POST to the
57
+ * admin, the one external HTTP step.
58
+ */
59
+ export const SCHEDULER_TICK_STEPS = { internal: 3, external: 1 } as const;
60
+
61
+ /**
62
+ * Workflows list prices (USD, cloud.google.com/workflows/pricing, as of
63
+ * 2026-11) and the monthly free tier per billing account.
64
+ */
65
+ const WORKFLOWS_PRICE = {
66
+ internalPer1000: 0.01,
67
+ externalPer1000: 0.025,
68
+ freeInternal: 5_000,
69
+ freeExternal: 2_000,
70
+ } as const;
71
+
72
+ /** Cloud Scheduler: $0.10 per job a month; the first 3 per billing account are free. */
73
+ const SCHEDULER_JOB_USD = 0.1;
74
+
75
+ /**
76
+ * #1140 — what the scheduler tick costs a month at `minutes` between ticks:
77
+ * the Cloud Scheduler job (priced as paid — the 3 free jobs are shared
78
+ * with every other project on the billing account) plus the Workflows
79
+ * steps past the free tier. What the tick costs on the admin's side is
80
+ * priced on the admin's own line ({@link adminKeptWarmByTicks}).
81
+ */
82
+ export function schedulerTickMonthlyUsd(minutes: number): number {
83
+ const ticks = Math.ceil((30 * 24 * 60) / parseSchedulerIntervalMinutes(minutes));
84
+ const internal = Math.max(
85
+ 0,
86
+ ticks * SCHEDULER_TICK_STEPS.internal - WORKFLOWS_PRICE.freeInternal,
87
+ );
88
+ const external = Math.max(
89
+ 0,
90
+ ticks * SCHEDULER_TICK_STEPS.external - WORKFLOWS_PRICE.freeExternal,
91
+ );
92
+ const usd =
93
+ SCHEDULER_JOB_USD +
94
+ (internal / 1000) * WORKFLOWS_PRICE.internalPer1000 +
95
+ (external / 1000) * WORKFLOWS_PRICE.externalPer1000;
96
+ return Math.round(usd * 100) / 100;
97
+ }
98
+
99
+ /**
100
+ * Cloud Run instance-based billing (CPU always allocated) list rates, USD,
101
+ * tier-1 region, cloud.google.com/run/pricing as of 2026-11. The free tier
102
+ * is left out: it is shared by every service on the billing account.
103
+ */
104
+ const CLOUD_RUN_INSTANCE_PRICE = { vcpuSecond: 0.000018, gibSecond: 0.000002 } as const;
105
+
106
+ /** Seconds in the 30-day month the table prices. */
107
+ const MONTH_SECONDS = 30 * 24 * 60 * 60;
108
+
109
+ /**
110
+ * #1140 — whether the scheduler tick keeps a scale-to-zero admin instance
111
+ * up around the clock. Cloud Run keeps an idle instance for up to 15
112
+ * minutes after its last request, and the tick comes at most every 15
113
+ * minutes, so with CPU always allocated (`SERVICE_CPU_IDLE.admin` false
114
+ * until #1361) the instance never stops and is billed every second. With
115
+ * request-billed CPU an idle instance costs nothing between ticks.
116
+ */
117
+ export function adminKeptWarmByTicks(adminMinInstances: number): boolean {
118
+ return adminMinInstances === 0 && !SERVICE_CPU_IDLE.admin;
119
+ }
120
+
121
+ /** One always-on admin instance (1 vCPU, `gib` memory) a month at instance-based list rates. */
122
+ export function alwaysOnAdminInstanceUsd(gib: number): number {
123
+ const perSecond = CLOUD_RUN_INSTANCE_PRICE.vcpuSecond + gib * CLOUD_RUN_INSTANCE_PRICE.gibSecond;
124
+ return Math.round(perSecond * MONTH_SECONDS);
44
125
  }
45
126
 
46
127
  /** The region the price table's list rates are for. */
@@ -71,6 +152,7 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
71
152
  const sqlMonthly = Math.round(sqlBase * sqlMultiplier);
72
153
 
73
154
  const provider = inputs.provider ?? "gcp";
155
+ const schedulerMinutes = parseSchedulerIntervalMinutes(inputs.schedulerIntervalMinutes);
74
156
  const adminMemory = inputs.adminMemory ?? ADMIN_MEMORY_DEFAULT;
75
157
  const adminMiB = memoryQuantityMiB(adminMemory);
76
158
  if (adminMiB === null) {
@@ -91,17 +173,20 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
91
173
  },
92
174
  {
93
175
  name: `Cloud Run admin (${adminMemory})`,
94
- // FIXME(issue #1140): ~$1 once the admin runs request-billed CPU
95
- // (SERVICE_CPU_IDLE). Until then each editing session is billed for
96
- // as long as the instance stays up, ~15 minutes past its last request.
97
- monthlyUsd:
98
- inputs.adminMinInstances === 0
176
+ // FIXME(issue #1361): with request-billed CPU (SERVICE_CPU_IDLE) the
177
+ // ticks stop keeping the instance up and this line drops to ~$7 of
178
+ // editing sessions. Until then the scheduler tick (#1140) keeps one
179
+ // instance up around the clock, billed at the always-on rate.
180
+ monthlyUsd: adminKeptWarmByTicks(inputs.adminMinInstances)
181
+ ? alwaysOnAdminInstanceUsd(adminGiB)
182
+ : inputs.adminMinInstances === 0
99
183
  ? 7
100
184
  : Math.round(1 + inputs.adminMinInstances * perMinInstanceUsd),
101
- notes:
102
- inputs.adminMinInstances === 0
103
- ? "scale-to-zero, CPU always allocated while running; ~$7/mo light editorial use"
104
- : `${inputs.adminMinInstances} min-instance${inputs.adminMinInstances > 1 ? "s" : ""} (no cold start)`,
185
+ notes: adminKeptWarmByTicks(inputs.adminMinInstances)
186
+ ? `scale-to-zero, but CPU is always allocated and the scheduler tick (every ${schedulerMinutes} min) comes before Cloud Run's 15-minute idle shutdown, so one instance (1 vCPU, ${adminMemory}) runs around the clock until request-billed CPU lands (#1361)`
187
+ : inputs.adminMinInstances === 0
188
+ ? "scale-to-zero, request-billed CPU; ~$7/mo light editorial use, ticks inside the free tier"
189
+ : `${inputs.adminMinInstances} min-instance${inputs.adminMinInstances > 1 ? "s" : ""} (no cold start); the scheduler tick adds nothing, the instance is up anyway`,
105
190
  },
106
191
  {
107
192
  name: "Quality checks (Cloud Run audit job)",
@@ -116,13 +201,24 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
116
201
  "one short container per server-side screenshot or page render (~20-40 s at 1 vCPU / 2 GiB): ~$0.001 each, a 50-page site import ~$0.05; mostly in the free tier; capture bucket ~$0",
117
202
  },
118
203
  {
119
- name: "Cloud Run gateway",
204
+ // #1328 — on gcp-firebase the visitor gateway is the new
205
+ // `caelo-<env>-data-gateway` service; same billing either way.
206
+ name: provider === "gcp" ? "Cloud Run gateway" : "Cloud Run data gateway",
120
207
  monthlyUsd: inputs.gatewayMinInstances === 0 ? 1 : 1 + inputs.gatewayMinInstances * 15,
121
208
  notes:
122
209
  inputs.gatewayMinInstances === 0
123
210
  ? "scale-to-zero; ~$1/mo light traffic"
124
211
  : `${inputs.gatewayMinInstances} min-instance${inputs.gatewayMinInstances > 1 ? "s" : ""}`,
125
212
  },
213
+ {
214
+ // #1328 — remote MCP's sign-in service, its own service on every
215
+ // install (E14 a of #763): always provisioned, zero minimum instances,
216
+ // CPU only during requests, so it costs nothing while nobody connects.
217
+ name: "Cloud Run MCP-Auth-Proxy",
218
+ monthlyUsd: 0,
219
+ notes:
220
+ "scale-to-zero, request-based CPU; only external AI apps' sign-in and MCP calls wake it, mostly in the free tier",
221
+ },
126
222
  // v0.3.3 — LB / Cloud CDN / Cloud Armor lines apply only to the
127
223
  // gcp variant. gcp-firebase has no LB (Firebase Hosting is the
128
224
  // edge); no Cloud CDN line (Firebase Hosting has its own CDN
@@ -148,12 +244,18 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
148
244
  : "Free tier — rate limit + OWASP basic rules",
149
245
  },
150
246
  {
151
- // #806 — the remote-MCP throttle (src/remote-mcp-edge.ts) is one
152
- // more rule on the gateway's policy; Cloud Armor Standard bills
153
- // each rule at about $1/mo.
154
- name: "Cloud Armor rule: remote MCP throttle",
155
- monthlyUsd: 1,
156
- notes: "Per-IP throttle for /api/mcp/* (remote MCP clients)",
247
+ // #1328 — the MCP-Auth-Proxy's own backend (serverless NEG +
248
+ // backend service, no extra LB fee: same URL map) with its own
249
+ // Cloud Armor policy carrying the remote-MCP throttle
250
+ // (src/remote-mcp-edge.ts) and the default rule. Cloud Armor
251
+ // Standard: $0.006849315/policy-hour (~$5/mo) and
252
+ // $0.001369863/rule-hour (~$1/mo), plus $0.75 per million
253
+ // requests on a global policy — cloud.google.com/armor/pricing,
254
+ // checked 2026-11-02.
255
+ name: "MCP-Auth-Proxy backend + Cloud Armor policy",
256
+ monthlyUsd: 7,
257
+ notes:
258
+ "own policy ($5) with 2 rules ($2): per-IP throttle for remote MCP clients; requests ~$0",
157
259
  },
158
260
  ] satisfies CostLine[])
159
261
  : ([
@@ -175,7 +277,7 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
175
277
  name: "Secret Manager (runtime secrets)",
176
278
  monthlyUsd: 0.2,
177
279
  notes:
178
- "6 active versions free, then ~$0.06 each; the Google sign-in client (#914) and the remote-MCP seal key (#806)",
280
+ "6 active versions free, then ~$0.06 each; the Google sign-in client (#914), the remote-MCP seal key (#806) and the staging database login's password (#1329; the staging database itself shares the Cloud SQL instance at no extra cost)",
179
281
  },
180
282
  {
181
283
  name: "BigQuery edge log sink",
@@ -194,6 +296,11 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
194
296
  notes:
195
297
  "Hourly ~20 s job keeps Google IAP in step with the user list; within the Cloud Run free tier, 1 of 3 free scheduler jobs",
196
298
  },
299
+ {
300
+ name: `Scheduler tick (Cloud Scheduler + Workflows, every ${schedulerMinutes} min)`,
301
+ monthlyUsd: schedulerTickMonthlyUsd(schedulerMinutes),
302
+ notes: `one job starts a ${SCHEDULER_TICK_STEPS.internal + SCHEDULER_TICK_STEPS.external}-step workflow that calls the admin's periodic work and plugin workers; ~${Math.ceil((30 * 24 * 60) / schedulerMinutes)} ticks/mo; what the ticks cost on the admin is on the admin line`,
303
+ },
197
304
  ];
198
305
 
199
306
  // Cents, so a sub-dollar line (the KMS key) doesn't print float noise.
@@ -30,6 +30,7 @@ import { randomBytes } from "node:crypto";
30
30
  import { existsSync, readFileSync } from "node:fs";
31
31
  import { homedir } from "node:os";
32
32
  import { join } from "node:path";
33
+ import { CLOUD_SQL_CA_BUNDLE, verifyFullQuery } from "@caelo-cms/shared";
33
34
  import { cancel, confirm, isCancel, log, note, select, spinner } from "@clack/prompts";
34
35
  import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
35
36
  import { checkAdcImpersonation } from "../adc-impersonation.js";
@@ -67,7 +68,11 @@ import {
67
68
  REQUIRED_API_LIST,
68
69
  serviceAccountExists,
69
70
  } from "../gcloud.js";
70
- import { GCP_STACK_ENV, upgraderServiceAccountEmail } from "../gcp-names.js";
71
+ import {
72
+ GCP_STACK_ENV,
73
+ schedulerServiceAccountEmail,
74
+ upgraderServiceAccountEmail,
75
+ } from "../gcp-names.js";
71
76
  import { INFRA_CONTRACT_VERSION } from "../infra-contract.js";
72
77
  import {
73
78
  INSTALL_JOB_NAME,
@@ -115,9 +120,12 @@ import {
115
120
  resolveStackRemoteMcpIdentity,
116
121
  } from "../remote-mcp.js";
117
122
  import type { RunMode } from "../run-mode.js";
123
+ import { ensureSchedulerTick } from "../scheduler-setup.js";
118
124
  import { selfCommand } from "../self-command.js";
119
125
  import {
120
126
  ABSENT_DEFAULT_FIREWALL_RULES,
127
+ cloudSqlPrivateDnsName,
128
+ parseSchedulerIntervalMinutes,
121
129
  type RemoteMcpIdentity,
122
130
  sameRemoteMcpIdentity,
123
131
  } from "../stack-contract.js";
@@ -134,6 +142,7 @@ import {
134
142
  import { acquireStateLock, GcsStateStore } from "../state-store.js";
135
143
  import { createReleaseImageForms, platformDigestsOf } from "../upgrade/image-forms.js";
136
144
  import {
145
+ releaseImageRef,
137
146
  releaseVersionForDigest,
138
147
  resolveOpsImageRef,
139
148
  verifyCosignAll,
@@ -231,7 +240,15 @@ export async function runGcpWizard(opts: GcpWizardOpts): Promise<void> {
231
240
  // drops the LB / Cloud CDN / Cloud Armor lines (saves ~$19/mo
232
241
  // vs gcp). Default 'gcp' for backwards compatibility with
233
242
  // installs that don't set the field.
234
- const estimate = estimateGcpCost({ ...GCP_STACK_KNOB_DEFAULTS, provider, region });
243
+ const estimate = estimateGcpCost({
244
+ ...GCP_STACK_KNOB_DEFAULTS,
245
+ provider,
246
+ region,
247
+ // #1140 — the tick's line follows the knob the wizard recorded.
248
+ ...(meta?.schedulerIntervalMinutes !== undefined
249
+ ? { schedulerIntervalMinutes: meta.schedulerIntervalMinutes }
250
+ : {}),
251
+ });
235
252
  note(
236
253
  [
237
254
  bold(
@@ -357,7 +374,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
357
374
  const deployed = isStepDone(installId, `pulumi-up-${projectId}`);
358
375
  if (deployed) {
359
376
  // The kept release must be able to use the env the stack deploys.
360
- const runtimeEnv = await checkDeployedRuntimeEnv({ projectId, region });
377
+ const runtimeEnv = await checkDeployedRuntimeEnv({ provider, projectId, region });
361
378
  if (!runtimeEnv.ok) {
362
379
  cancel(runtimeEnv.error);
363
380
  process.exit(1);
@@ -366,7 +383,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
366
383
  const choice = await chooseImageDigests({
367
384
  recorded: recordedImageDigests(meta),
368
385
  deployed,
369
- readLive: () => readDeployedImageDigests({ projectId, region }),
386
+ readLive: () => readDeployedImageDigests({ provider, projectId, region }),
370
387
  resolveLatest: resolveImageDigests,
371
388
  });
372
389
  if (!choice.ok) {
@@ -520,6 +537,18 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
520
537
  // === 11.45. The converge job: releases that change the cloud setup (#819) ===
521
538
  const convergeReady = await stepConvergeJob(projectId, region, releaseVersion);
522
539
 
540
+ // === 11.47. The scheduler tick: periodic work + plugin workers (#1140) ===
541
+ // Before operator access, which keeps the scheduler's account on IAP.
542
+ const schedulerReady = await stepSchedulerTick(
543
+ projectId,
544
+ region,
545
+ provider,
546
+ ownerEmail,
547
+ identity?.identity.adminUrl ?? null,
548
+ meta.schedulerIntervalMinutes,
549
+ );
550
+ const keepScheduler = schedulerReady || (await schedulerExists(projectId));
551
+
523
552
  // === 11.5. Operator access: IAP follows the Caelo user list ===
524
553
  await stepOperatorAccessSync(
525
554
  projectId,
@@ -527,18 +556,27 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
527
556
  provider,
528
557
  ownerEmail,
529
558
  keepSigner ? [`serviceAccount:${upgraderServiceAccountEmail(projectId, GCP_STACK_ENV)}`] : [],
559
+ keepScheduler ? [schedulerServiceAccountEmail(projectId, GCP_STACK_ENV)] : [],
530
560
  );
531
561
 
532
562
  // === 11.6. Remote MCP (#806): LB rules (gcp) + a check of the identity env ===
533
563
  // The env itself came with `pulumi up` (step 10.05, #1082).
534
- const remoteMcpReady = await stepRemoteMcp(projectId, provider, domain, identity);
564
+ const remoteMcpReady = await stepRemoteMcp(
565
+ projectId,
566
+ region,
567
+ provider,
568
+ domain,
569
+ identity,
570
+ imageDigests,
571
+ );
535
572
 
536
573
  // A fresh install ran every converge step above, so it is at this CLI's
537
574
  // infrastructure contract — recorded only when the upgrader and the
538
575
  // converge job are in place (the steps contract bumps 2 and 9 added) and
539
- // remote MCP's steps (#806, bump 10) succeeded. With the release version,
540
- // the admin can install the next update itself.
541
- if (upgraderReady && convergeReady && remoteMcpReady) {
576
+ // remote MCP's steps (#806, bump 10) and the scheduler tick (#1140, bump
577
+ // 18) succeeded. With the release version, the admin can install the next
578
+ // update itself.
579
+ if (upgraderReady && convergeReady && remoteMcpReady && schedulerReady) {
542
580
  const version = releaseVersion;
543
581
  // #1025 — the linux/amd64 forms Cloud Run reports, next to the indexes,
544
582
  // so the upgrader recognises what serves without a registry lookup.
@@ -603,8 +641,12 @@ async function resolveImageDigests(): Promise<ImageDigests> {
603
641
  const project = "caelo-website";
604
642
  const registryRegion = "europe-west1";
605
643
  const repo = "caelo-cms-images";
606
- const out = { admin: "", gateway: "" };
607
- for (const service of ["admin", "gateway"] as const) {
644
+ const out: Record<"admin" | "gateway" | "mcp-auth-proxy", string> = {
645
+ admin: "",
646
+ gateway: "",
647
+ "mcp-auth-proxy": "",
648
+ };
649
+ for (const service of ["admin", "gateway", "mcp-auth-proxy"] as const) {
608
650
  const s = spinner();
609
651
  s.start(`Resolving ${service}:latest → digest...`);
610
652
  // gcloud's --filter='tag=latest' is in a transitional state (warns +
@@ -984,7 +1026,7 @@ async function stepGrantBaseRoles(
984
1026
  */
985
1027
  async function stepPublicRoleIsolation(projectId: string): Promise<void> {
986
1028
  const s = spinner();
987
- s.start("Removing public_role from the Cloud SQL superuser group...");
1029
+ s.start("Removing the gateway logins from the Cloud SQL superuser group...");
988
1030
  const r = await revokePublicRoleMemberships({
989
1031
  projectId,
990
1032
  env: GCP_STACK_ENV,
@@ -996,7 +1038,7 @@ async function stepPublicRoleIsolation(projectId: string): Promise<void> {
996
1038
  cancel("Aborted.");
997
1039
  process.exit(1);
998
1040
  }
999
- s.stop(green("public_role holds no database roles"));
1041
+ s.stop(green("The gateway logins (public_role, public_staging_role) hold no database roles"));
1000
1042
  }
1001
1043
 
1002
1044
  /**
@@ -1202,6 +1244,78 @@ async function upgraderExists(projectId: string): Promise<boolean> {
1202
1244
  return r.ok;
1203
1245
  }
1204
1246
 
1247
+ /** Whether the scheduler tick's account exists from an earlier run (it keeps its IAP access). */
1248
+ async function schedulerExists(projectId: string): Promise<boolean> {
1249
+ const r = await gcloud([
1250
+ "iam",
1251
+ "service-accounts",
1252
+ "describe",
1253
+ schedulerServiceAccountEmail(projectId, GCP_STACK_ENV),
1254
+ `--project=${projectId}`,
1255
+ "--format=value(email)",
1256
+ ]);
1257
+ return r.ok;
1258
+ }
1259
+
1260
+ /**
1261
+ * #1140 — set up the scheduler tick (scheduler-setup.ts): the one Cloud
1262
+ * Scheduler job that calls the admin's tick every `schedulerIntervalMinutes`
1263
+ * through IAP. After migrations (the admin answers the tick once its tables
1264
+ * exist) and before operator access. Not fatal: the site works without it,
1265
+ * but plugin workers, update checks and clean-up do not run, so a failure
1266
+ * warns loudly and the install is not recorded at this contract — the next
1267
+ * wizard pass or `upgrade` retries. Idempotent.
1268
+ *
1269
+ * @returns whether the tick is in place
1270
+ */
1271
+ async function stepSchedulerTick(
1272
+ projectId: string,
1273
+ region: string,
1274
+ provider: "gcp" | "gcp-firebase",
1275
+ ownerEmail: string,
1276
+ adminUrl: string | null,
1277
+ recordedInterval: number | undefined,
1278
+ ): Promise<boolean> {
1279
+ const s = spinner();
1280
+ s.start("Setting up the scheduler tick (periodic work and plugin workers)...");
1281
+ const failed = (error: string) => {
1282
+ s.stop(yellow(`Scheduler tick not set up: ${error}`));
1283
+ log.warn(
1284
+ `The install works, but plugin workers (newsletter sends, rating aggregates), update checks and clean-up do not run until this succeeds. Re-run the installer or ${bold(selfCommand("upgrade"))} once the cause is fixed.`,
1285
+ );
1286
+ return false;
1287
+ };
1288
+ let intervalMinutes: number;
1289
+ try {
1290
+ intervalMinutes = parseSchedulerIntervalMinutes(
1291
+ recordedInterval,
1292
+ "install.json schedulerIntervalMinutes",
1293
+ );
1294
+ } catch (e) {
1295
+ return failed((e as Error).message);
1296
+ }
1297
+ if (!adminUrl) return failed("the admin's URL could not be read (see the remote-MCP step above)");
1298
+ const resolved = await resolveOperatorAccessTarget({
1299
+ provider,
1300
+ projectId,
1301
+ region,
1302
+ env: GCP_STACK_ENV,
1303
+ ownerEmail,
1304
+ });
1305
+ if (!resolved.ok) return failed(resolved.error);
1306
+ const r = await ensureSchedulerTick({
1307
+ projectId,
1308
+ region,
1309
+ env: GCP_STACK_ENV,
1310
+ adminUrl,
1311
+ resource: resolved.target.resource,
1312
+ intervalMinutes,
1313
+ });
1314
+ if (!r.ok) return failed(r.error);
1315
+ s.stop(green(`Scheduler tick ready (every ${intervalMinutes} min, ${r.done.length} steps)`));
1316
+ return true;
1317
+ }
1318
+
1205
1319
  /**
1206
1320
  * Set up the operator-access sync job (operator-access.ts): the only
1207
1321
  * principal that may change who passes IAP, run as the operator's gcloud
@@ -1214,6 +1328,7 @@ async function stepOperatorAccessSync(
1214
1328
  provider: "gcp" | "gcp-firebase",
1215
1329
  ownerEmail: string,
1216
1330
  mcpSigners: readonly string[],
1331
+ iapServiceAccounts: readonly string[],
1217
1332
  ): Promise<void> {
1218
1333
  const s = spinner();
1219
1334
  s.start("Setting up the operator-access sync job (Google IAP follows the user list)...");
@@ -1225,7 +1340,7 @@ async function stepOperatorAccessSync(
1225
1340
  ownerEmail,
1226
1341
  });
1227
1342
  const r = resolved.ok
1228
- ? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners })
1343
+ ? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners, iapServiceAccounts })
1229
1344
  : { ok: false as const, done: [], error: resolved.error };
1230
1345
  if (!r.ok) {
1231
1346
  s.stop(red(`Failed: ${r.error}`));
@@ -1309,9 +1424,11 @@ async function stepResolveRemoteMcpIdentity(
1309
1424
  */
1310
1425
  async function stepRemoteMcp(
1311
1426
  projectId: string,
1427
+ region: string,
1312
1428
  provider: "gcp" | "gcp-firebase",
1313
1429
  domain: string,
1314
1430
  identity: ResolvedRemoteMcpIdentity | null,
1431
+ imageDigests: ImageDigests,
1315
1432
  ): Promise<boolean> {
1316
1433
  const s = spinner();
1317
1434
  s.start(
@@ -1325,11 +1442,22 @@ async function stepRemoteMcp(
1325
1442
  return false;
1326
1443
  };
1327
1444
  if (!identity) return failed("its URLs and IAP audience could not be read (see above)");
1328
- const edge = await ensureRemoteMcpEdge({ provider, projectId, env: GCP_STACK_ENV, domain });
1445
+ const proxyDigest = imageDigests["mcp-auth-proxy"];
1446
+ if (!proxyDigest) return failed("the release records no MCP-Auth-Proxy image");
1447
+ const edge = await ensureRemoteMcpEdge({
1448
+ provider,
1449
+ projectId,
1450
+ region,
1451
+ env: GCP_STACK_ENV,
1452
+ domain,
1453
+ mcpAuthProxyImageRef: releaseImageRef("mcp-auth-proxy", proxyDigest),
1454
+ });
1329
1455
  if (!edge.ok) return failed(edge.error);
1330
1456
  const check = await checkRemoteMcpIdentityEnv(identity.target, identity.identity);
1331
1457
  if (!check.ok) return failed(check.error);
1332
- const missing = [...check.drift.admin, ...check.drift.gateway].map((c) => c.name);
1458
+ const missing = Object.values(check.drift)
1459
+ .flat()
1460
+ .map((c) => c.name);
1333
1461
  if (missing.length > 0) {
1334
1462
  return failed(`pulumi up left ${[...new Set(missing)].join(", ")} out of the services' env`);
1335
1463
  }
@@ -1905,8 +2033,10 @@ async function stepDrainLegacyCaeloAdminUser(projectId: string, region: string):
1905
2033
  pgPass,
1906
2034
  ]);
1907
2035
 
1908
- // Resolve the SQL instance's private IP from gcloud (avoids reading
1909
- // Pulumi outputs from inside the wizard).
2036
+ // Resolve the SQL instance's DNS name from gcloud (avoids reading
2037
+ // Pulumi outputs from inside the wizard). #1338: the job connects with
2038
+ // verify-full, and the shared-CA certificate names the DNS name, not the
2039
+ // private IP.
1910
2040
  const sqlDescr = await gcloud([
1911
2041
  "sql",
1912
2042
  "instances",
@@ -1914,16 +2044,28 @@ async function stepDrainLegacyCaeloAdminUser(projectId: string, region: string):
1914
2044
  "caelo-production-pg-1d65811",
1915
2045
  "--project",
1916
2046
  projectId,
1917
- "--format=value(ipAddresses[0].ipAddress)",
2047
+ "--format=json(dnsNames)",
1918
2048
  ]);
1919
- const pgHost = (sqlDescr.stdout || "").trim();
2049
+ let pgHost: string | null = null;
2050
+ try {
2051
+ pgHost = cloudSqlPrivateDnsName(
2052
+ (
2053
+ JSON.parse(sqlDescr.stdout || "{}") as {
2054
+ dnsNames?: { name?: string; connectionType?: string }[];
2055
+ }
2056
+ ).dnsNames,
2057
+ );
2058
+ } catch {
2059
+ pgHost = null;
2060
+ }
1920
2061
  if (!pgHost) {
1921
- s.stop(yellow("Could not resolve Cloud SQL private IP — skipping drain"));
2062
+ s.stop(yellow("Could not resolve the Cloud SQL instance's DNS name — skipping drain"));
1922
2063
  return;
1923
2064
  }
1924
2065
 
1925
- const adminPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_admin`;
1926
- const publicPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_public`;
2066
+ const tls = verifyFullQuery(CLOUD_SQL_CA_BUNDLE);
2067
+ const adminPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_admin?${tls}`;
2068
+ const publicPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_public?${tls}`;
1927
2069
 
1928
2070
  // The cleanup script — uses bun's globalThis.Bun.SQL (already proven
1929
2071
  // pattern in hooks.server.ts + the migration runner). DO block makes
@@ -34,7 +34,7 @@ config:
34
34
  description: RDS max_connections (parameter group); every service's pool size is computed from it (docs/dev/connection-budget.md).
35
35
  caelo-aws:adminMaxInstances:
36
36
  type: string
37
- default: "5"
37
+ default: "4"
38
38
  description: Most admin tasks at once; part of the connection budget.
39
39
  caelo-aws:gatewayMaxInstances:
40
40
  type: string