@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
@@ -56,15 +56,21 @@ import type {
56
56
  StoredUpdatePolicy,
57
57
  } from "@caelo-cms/shared";
58
58
  import type { GcloudRunner, Sleep } from "./gcloud-retry.js";
59
- import { gcpNamePrefix, upgraderJobName } from "./gcp-names.js";
59
+ import { upgraderJobName } from "./gcp-names.js";
60
60
  import type { InstallMetadata } from "./install-state.js";
61
- import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
61
+ import {
62
+ manifestServiceImages,
63
+ type ReleaseManifest,
64
+ type ReleaseManifestResult,
65
+ } from "./release-manifest.js";
66
+ import { cloudRunServicePrefix } from "./stack-contract.js";
62
67
  import { StateLockHeldError } from "./state-store.js";
63
68
  import {
64
69
  type AppRollPlan,
65
70
  executeAppRoll,
66
71
  planAppRoll,
67
72
  type ReleaseImage,
73
+ ROLL_ORDER,
68
74
  type RollSlug,
69
75
  } from "./upgrade/app-roll.js";
70
76
  import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
@@ -389,7 +395,7 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
389
395
  // install.json must describe what actually serves: if a previous run
390
396
  // rolled but could not record it, the recorded version is stale and
391
397
  // the downgrade / minFromVersion checks would judge against it.
392
- const serving = await servingDigests(deps);
398
+ const serving = await servingDigests(deps, meta.provider);
393
399
  if (!serving.ok) {
394
400
  return stopWith("failed", ["lookup"], `Nothing was changed: ${serving.error}.`);
395
401
  }
@@ -424,6 +430,9 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
424
430
  imageDigests: {
425
431
  admin: found.manifest.digests.admin,
426
432
  gateway: found.manifest.digests.gateway,
433
+ ...(found.manifest.digests["mcp-auth-proxy"]
434
+ ? { "mcp-auth-proxy": found.manifest.digests["mcp-auth-proxy"] }
435
+ : {}),
427
436
  },
428
437
  ...(found.platformDigests ? { platformDigests: found.platformDigests } : {}),
429
438
  releaseVersion: found.manifest.version,
@@ -500,13 +509,10 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
500
509
  r.info("The upgrader has every permission this update needs.");
501
510
 
502
511
  const m = decision.manifest;
503
- const images: Record<RollSlug, ReleaseImage> = {
504
- admin: { digest: m.digests.admin, imageRef: releaseImageRef("admin", m.digests.admin) },
505
- gateway: {
506
- digest: m.digests.gateway,
507
- imageRef: releaseImageRef("gateway", m.digests.gateway),
508
- },
509
- };
512
+ const resolved = manifestServiceImages(m);
513
+ if (!resolved.ok)
514
+ return stopWith("failed", ["manifest"], `Nothing was changed: ${resolved.error}.`);
515
+ const images: Record<RollSlug, ReleaseImage> = resolved.images;
510
516
  const planned = await planAppRoll(
511
517
  {
512
518
  provider: meta.provider,
@@ -694,7 +700,10 @@ export async function recordUpdateEnd(
694
700
  }
695
701
 
696
702
  /** Every digest the revisions serving traffic are known by, per service. */
697
- async function servingDigests(deps: UpgraderJobDeps): Promise<
703
+ async function servingDigests(
704
+ deps: UpgraderJobDeps,
705
+ provider: InstallMetadata["provider"],
706
+ ): Promise<
698
707
  | {
699
708
  ok: true;
700
709
  digests: ServingDigests;
@@ -705,8 +714,12 @@ async function servingDigests(deps: UpgraderJobDeps): Promise<
705
714
  > {
706
715
  const digests: Partial<Record<RollSlug, readonly string[]>> = {};
707
716
  const services: Partial<Record<RollSlug, string>> = {};
708
- for (const slug of ["admin", "gateway"] as const) {
709
- const prefix = `${gcpNamePrefix(deps.env)}-${slug}`;
717
+ if (provider !== "gcp" && provider !== "gcp-firebase") {
718
+ return { ok: false, error: `the upgrader runs on Google Cloud installs only, not ${provider}` };
719
+ }
720
+ for (const slug of ROLL_ORDER) {
721
+ // #1328 — which service plays which role differs per provider.
722
+ const prefix = cloudRunServicePrefix(provider, deps.env, slug);
710
723
  const service = await resolveGcpResourceName(
711
724
  deps.run,
712
725
  "run-service",
@@ -739,7 +752,7 @@ async function templateDigests(
739
752
  services: Readonly<Record<RollSlug, string>>,
740
753
  ): Promise<ServingDigests> {
741
754
  const out: Partial<Record<RollSlug, readonly string[]>> = {};
742
- for (const slug of ["admin", "gateway"] as const) {
755
+ for (const slug of ROLL_ORDER) {
743
756
  const d = await findTemplateImageDigest(deps.run, deps.projectId, deps.region, services[slug]);
744
757
  out[slug] = d ? [d] : [];
745
758
  }
@@ -29,13 +29,21 @@ import {
29
29
  } from "./upgrader-records.js";
30
30
 
31
31
  const D = (c: string) => `sha256:${c.repeat(64)}`;
32
+ // #1328 — the MCP-Auth-Proxy's index; serving revisions name it as is here,
33
+ // so it matches without a registry lookup.
34
+ const PROXY_INDEX = `sha256:${"e".repeat(64)}`;
32
35
  const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
33
36
 
34
37
  const V064: ReleaseManifest = {
35
38
  manifestVersion: 1,
36
39
  version: "0.10.64",
37
40
  channel: "stable",
38
- digests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, ops: D("c") },
41
+ digests: {
42
+ admin: ADMIN_INDEX,
43
+ gateway: GATEWAY_INDEX,
44
+ ops: D("c"),
45
+ "mcp-auth-proxy": PROXY_INDEX,
46
+ },
39
47
  minFromVersion: "0.10.0",
40
48
  infraContractVersion: 13,
41
49
  migrations: { contract: false, lastContractVersion: null, head: "0338_x" },
@@ -51,13 +59,17 @@ const META: InstallMetadata = {
51
59
  ownerEmail: "owner@acme.com",
52
60
  region: "europe-west1",
53
61
  createdAt: "2026-01-01T00:00:00Z",
54
- imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX },
62
+ imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, "mcp-auth-proxy": PROXY_INDEX },
55
63
  releaseVersion: "0.10.64",
56
64
  infraContractVersion: 13,
57
65
  };
58
66
 
59
67
  /** What the serving revisions read back as on searchviu-website (spec = status). */
60
- const LIVE_SERVING: ServingDigests = { admin: [ADMIN_AMD64], gateway: [GATEWAY_AMD64] };
68
+ const LIVE_SERVING: ServingDigests = {
69
+ admin: [ADMIN_AMD64],
70
+ gateway: [GATEWAY_AMD64],
71
+ "mcp-auth-proxy": [PROXY_INDEX],
72
+ };
61
73
 
62
74
  function deps(
63
75
  opts: {
@@ -112,16 +124,38 @@ describe("recordMatchesServing", () => {
112
124
  const both = { ...META, platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 } };
113
125
  expect(recordMatchesServing(both, LIVE_SERVING)).toBe(true);
114
126
  // Still by the index when a revision names it.
115
- expect(recordMatchesServing(META, { admin: [ADMIN_INDEX], gateway: [GATEWAY_INDEX] })).toBe(
116
- true,
117
- );
127
+ expect(
128
+ recordMatchesServing(META, {
129
+ admin: [ADMIN_INDEX],
130
+ gateway: [GATEWAY_INDEX],
131
+ "mcp-auth-proxy": [PROXY_INDEX],
132
+ }),
133
+ ).toBe(true);
118
134
  });
119
135
 
120
136
  it("both services must match, and no record never matches", () => {
121
137
  const both = { ...META, platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 } };
122
- expect(recordMatchesServing(both, { admin: [ADMIN_AMD64], gateway: [D("6")] })).toBe(false);
138
+ expect(
139
+ recordMatchesServing(both, {
140
+ admin: [ADMIN_AMD64],
141
+ gateway: [D("6")],
142
+ "mcp-auth-proxy": [PROXY_INDEX],
143
+ }),
144
+ ).toBe(false);
123
145
  expect(recordMatchesServing({}, LIVE_SERVING)).toBe(false);
124
146
  });
147
+
148
+ it("#1328: an install.json from before the MCP-Auth-Proxy (no proxy digest) never matches", () => {
149
+ const { "mcp-auth-proxy": _none, ...old } = META.imageDigests as NonNullable<
150
+ InstallMetadata["imageDigests"]
151
+ >;
152
+ const both = {
153
+ ...META,
154
+ imageDigests: old,
155
+ platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 },
156
+ };
157
+ expect(recordMatchesServing(both, LIVE_SERVING)).toBe(false);
158
+ });
125
159
  });
126
160
 
127
161
  describe("findServingRelease", () => {
@@ -141,12 +175,12 @@ describe("findServingRelease", () => {
141
175
  it("an attestation manifest of the index is not a serving image of the release", async () => {
142
176
  const r = await findServingRelease(
143
177
  META,
144
- { admin: [ADMIN_ATTESTATION], gateway: [GATEWAY_AMD64] },
178
+ { admin: [ADMIN_ATTESTATION], gateway: [GATEWAY_AMD64], "mcp-auth-proxy": [PROXY_INDEX] },
145
179
  deps(),
146
180
  );
147
181
  expect(r.ok).toBe(false);
148
182
  expect(r.ok ? "" : r.reason).toContain(
149
- "The gateway serves v0.10.64's image, the admin does not.",
183
+ "The gateway and the MCP-Auth-Proxy serve v0.10.64's image, the admin does not.",
150
184
  );
151
185
  });
152
186
 
@@ -166,7 +200,11 @@ describe("findServingRelease", () => {
166
200
  });
167
201
 
168
202
  it("a digest in no signed index is still refused with the current message", async () => {
169
- const r = await findServingRelease(META, { admin: [D("7")], gateway: [D("6")] }, deps());
203
+ const r = await findServingRelease(
204
+ META,
205
+ { admin: [D("7")], gateway: [D("6")], "mcp-auth-proxy": [PROXY_INDEX] },
206
+ deps(),
207
+ );
170
208
  expect(r).toMatchObject({ ok: false, retryable: false });
171
209
  expect(r.ok ? "" : r.reason).toContain(
172
210
  "are not the ones install.json names, and they do not belong to one signed Caelo release",
@@ -190,13 +228,60 @@ describe("findServingRelease", () => {
190
228
 
191
229
  it("finds the release by the template's index when install.json names none", async () => {
192
230
  const { releaseVersion: _none, ...unversioned } = META;
193
- const meta = { ...unversioned, imageDigests: { admin: D("9"), gateway: D("8") } };
231
+ const meta = {
232
+ ...unversioned,
233
+ imageDigests: { admin: D("9"), gateway: D("8"), "mcp-auth-proxy": D("5") },
234
+ };
194
235
  const d = deps({
195
236
  tags: { admin: { "0.10.64": ADMIN_INDEX } },
196
- hints: { admin: [ADMIN_INDEX], gateway: [GATEWAY_INDEX] },
237
+ hints: { admin: [ADMIN_INDEX], gateway: [GATEWAY_INDEX], "mcp-auth-proxy": [PROXY_INDEX] },
197
238
  });
198
239
  const r = await findServingRelease(meta, LIVE_SERVING, d);
199
240
  expect(r.ok && r.manifest.version).toBe("0.10.64");
200
241
  expect(d.fetched).toEqual(["0.10.64"]);
201
242
  });
202
243
  });
244
+
245
+ describe("findServingRelease — the MCP-Auth-Proxy (#1328)", () => {
246
+ it("records the proxy's signed platform form with the others", async () => {
247
+ const PROXY_AMD64 = `sha256:${"f".repeat(64)}`;
248
+ const signed: ReleaseManifest = {
249
+ ...V064,
250
+ platformDigests: {
251
+ admin: ADMIN_AMD64,
252
+ gateway: GATEWAY_AMD64,
253
+ ops: D("3"),
254
+ "mcp-auth-proxy": PROXY_AMD64,
255
+ },
256
+ };
257
+ const r = await findServingRelease(
258
+ META,
259
+ { ...LIVE_SERVING, "mcp-auth-proxy": [PROXY_AMD64] },
260
+ deps({ manifests: { "0.10.64": signed } }),
261
+ );
262
+ expect(r).toMatchObject({
263
+ ok: true,
264
+ platformDigests: {
265
+ admin: ADMIN_AMD64,
266
+ gateway: GATEWAY_AMD64,
267
+ "mcp-auth-proxy": PROXY_AMD64,
268
+ },
269
+ });
270
+ });
271
+
272
+ it("a release from before the proxy cannot be what serves (its manifest names no proxy)", async () => {
273
+ const { "mcp-auth-proxy": _none, ...digests } = V064.digests;
274
+ const old: ReleaseManifest = { ...V064, digests };
275
+ const r = await findServingRelease(META, LIVE_SERVING, deps({ manifests: { "0.10.64": old } }));
276
+ expect(r.ok).toBe(false);
277
+ });
278
+
279
+ it("a proxy of another release is refused", async () => {
280
+ const r = await findServingRelease(
281
+ META,
282
+ { ...LIVE_SERVING, "mcp-auth-proxy": [D("4")] },
283
+ deps(),
284
+ );
285
+ expect(r.ok).toBe(false);
286
+ });
287
+ });
@@ -30,14 +30,16 @@
30
30
  import type { GcloudRunner } from "./gcloud-retry.js";
31
31
  import type { ImageDigests, InstallMetadata } from "./install-state.js";
32
32
  import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
33
- import type { RollSlug } from "./upgrade/app-roll.js";
33
+ import { ROLL_ORDER, type RollSlug } from "./upgrade/app-roll.js";
34
34
  import type { ReleaseImageForms } from "./upgrade/image-forms.js";
35
35
  import { releaseVersionForDigest } from "./upgrade/release-images.js";
36
36
 
37
37
  /** Every digest each serving image is known by (gcp-lookup.ts servingImageDigests). */
38
38
  export type ServingDigests = Readonly<Record<RollSlug, readonly string[]>>;
39
39
 
40
- const SLUGS: readonly RollSlug[] = ["admin", "gateway"];
40
+ // #1328 — the MCP-Auth-Proxy serves on every install this code runs on (the
41
+ // upgrader runs only on installs converged to its contract).
42
+ const SLUGS: readonly RollSlug[] = ROLL_ORDER;
41
43
 
42
44
  /**
43
45
  * Whether install.json names the images that serve: per service, the
@@ -50,11 +52,14 @@ export function recordMatchesServing(
50
52
  ): boolean {
51
53
  const { imageDigests, platformDigests } = recorded;
52
54
  if (!imageDigests) return false;
53
- return SLUGS.every(
54
- (slug) =>
55
- serving[slug].includes(imageDigests[slug]) ||
56
- (platformDigests !== undefined && serving[slug].includes(platformDigests[slug])),
57
- );
55
+ return SLUGS.every((slug) => {
56
+ const index = imageDigests[slug];
57
+ const platform = platformDigests?.[slug];
58
+ return (
59
+ (index !== undefined && serving[slug].includes(index)) ||
60
+ (platform !== undefined && serving[slug].includes(platform))
61
+ );
62
+ });
58
63
  }
59
64
 
60
65
  /** What {@link findServingRelease} learned. */
@@ -139,7 +144,7 @@ export async function findServingRelease(
139
144
  if (judged.matching.length > 0) {
140
145
  const other = SLUGS.filter((s) => !judged.matching.includes(s));
141
146
  partial.push(
142
- `the ${judged.matching.join(", ")} serves v${m.version}'s image, the ${other.join(", ")} does not`,
147
+ `${serviceList(judged.matching)} ${judged.matching.length > 1 ? "serve" : "serves"} v${m.version}'s image, ${serviceList(other)} ${other.length > 1 ? "do" : "does"} not`,
143
148
  );
144
149
  }
145
150
  }
@@ -162,7 +167,7 @@ export async function findServingRelease(
162
167
  ...(unverified.length > 0
163
168
  ? [`These release records could not be verified: ${unverified.join("; ")}.`]
164
169
  : []),
165
- "Nothing was changed. Check who deployed these images (the Cloud Run revision history of both services); an upgrade with Cloud Shell puts a signed release back.",
170
+ "Nothing was changed. Check who deployed these images (the Cloud Run revision history of the services); an upgrade with Cloud Shell puts a signed release back.",
166
171
  ].join(" "),
167
172
  };
168
173
  }
@@ -189,7 +194,10 @@ async function judgeRelease(
189
194
  const matching: RollSlug[] = [];
190
195
  const platform: Partial<Record<RollSlug, string>> = {};
191
196
  for (const slug of SLUGS) {
192
- if (serving[slug].includes(m.digests[slug])) {
197
+ const index = m.digests[slug];
198
+ // #1328 — a release from before the proxy cannot be what serves here.
199
+ if (!index) continue;
200
+ if (serving[slug].includes(index)) {
193
201
  matching.push(slug);
194
202
  const signed = m.platformDigests?.[slug];
195
203
  if (signed) platform[slug] = signed;
@@ -205,10 +213,12 @@ async function judgeRelease(
205
213
  }
206
214
  }
207
215
  const { admin, gateway } = platform;
216
+ const proxy = platform["mcp-auth-proxy"];
208
217
  return {
209
218
  kind: "judged",
210
219
  matching,
211
- platformDigests: admin && gateway ? { admin, gateway } : null,
220
+ platformDigests:
221
+ admin && gateway ? { admin, gateway, ...(proxy ? { "mcp-auth-proxy": proxy } : {}) } : null,
212
222
  };
213
223
  }
214
224
 
@@ -245,3 +255,10 @@ async function* candidateVersions(
245
255
  function capitalize(s: string): string {
246
256
  return s.charAt(0).toUpperCase() + s.slice(1);
247
257
  }
258
+
259
+ /** `the admin`, `the gateway and the MCP-Auth-Proxy`, … for the refusal text. */
260
+ function serviceList(slugs: readonly RollSlug[]): string {
261
+ const names = slugs.map((s) => (s === "mcp-auth-proxy" ? "the MCP-Auth-Proxy" : `the ${s}`));
262
+ if (names.length <= 1) return names[0] ?? "";
263
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
264
+ }
@@ -81,6 +81,8 @@ describe("ensureUpgraderJob", () => {
81
81
  "projects add-iam-policy-binding acme → projects/acme/roles/caeloUpgraderSqlBackup",
82
82
  "iam service-accounts add-iam-policy-binding caelo-production-run-sa@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
83
83
  "iam service-accounts add-iam-policy-binding caelo-production-gateway-sa@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
84
+ // #1328 — the MCP-Auth-Proxy's revisions run as its own account.
85
+ "iam service-accounts add-iam-policy-binding caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
84
86
  "iam service-accounts add-iam-policy-binding caelo-production-upgrader@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
85
87
  "iam service-accounts add-iam-policy-binding caelo-mcp@acme.iam.gserviceaccount.com → roles/iam.serviceAccountTokenCreator",
86
88
  "iam service-accounts add-iam-policy-binding caelo-production-audit@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
@@ -27,11 +27,11 @@
27
27
  * PULUMI_*) and so act as the provisioner (#883 review). The custom role
28
28
  * has no `runWithOverrides` and no `*.setIamPolicy`. Updating another
29
29
  * job (the converge job's image, say) additionally needs `actAs` on its
30
- * service account, which the upgrader holds only on the admin, gateway
31
- * and its own. A grant of `roles/run.developer` left by an earlier
30
+ * service account, which the upgrader holds only on the admin, gateway,
31
+ * MCP-Auth-Proxy and its own. A grant of `roles/run.developer` left by an earlier
32
32
  * setup is removed.
33
- * - `roles/iam.serviceAccountUser` on exactly the admin's and the
34
- * gateway's runtime service accounts (a revision runs as them), on
33
+ * - `roles/iam.serviceAccountUser` on exactly the admin's, the
34
+ * gateway's and the MCP-Auth-Proxy's (#1328) runtime service accounts (a revision runs as them), on
35
35
  * itself (its own job) and on the quality-audit and capture jobs'
36
36
  * accounts (the app-roll moves those jobs' images, #875, #1217) — not
37
37
  * on the project.
@@ -79,6 +79,7 @@ import {
79
79
  auditServiceAccountEmail,
80
80
  captureServiceAccountEmail,
81
81
  gatewayServiceAccountEmail,
82
+ mcpAuthProxyServiceAccountEmail,
82
83
  mcpIapServiceAccountEmail,
83
84
  runServiceAccountEmail,
84
85
  upgraderJobName,
@@ -367,6 +368,11 @@ export async function ensureUpgraderJob(
367
368
  role: "roles/iam.serviceAccountUser",
368
369
  why: "gateway revisions run as it",
369
370
  },
371
+ {
372
+ account: mcpAuthProxyServiceAccountEmail(projectId, env),
373
+ role: "roles/iam.serviceAccountUser",
374
+ why: "MCP-Auth-Proxy revisions run as it (#1328)",
375
+ },
370
376
  { account: sa, role: "roles/iam.serviceAccountUser", why: "the job updates its own image" },
371
377
  {
372
378
  account: auditServiceAccountEmail(projectId, env),
package/src/wizard.ts CHANGED
@@ -72,6 +72,13 @@ export interface WizardOptions {
72
72
  * install a different value is refused.
73
73
  */
74
74
  region?: string;
75
+ /**
76
+ * #1140 — `--scheduler-interval-minutes` (1–15, already validated): how
77
+ * often the scheduler tick calls the admin. Recorded in install.json, so
78
+ * every later `upgrade` keeps it; omitted keeps what is recorded (or the
79
+ * default, 5).
80
+ */
81
+ schedulerIntervalMinutes?: number;
75
82
  /**
76
83
  * #696 — where `pulumi up` and the rest of a GCP install run
77
84
  * (run-mode.ts): `job` (default in Cloud Shell) or `local`.
@@ -192,6 +199,18 @@ export async function runWizard(opts: WizardOptions = {}): Promise<void> {
192
199
  }
193
200
  }
194
201
 
202
+ // #1140 — the scheduler knob, on a new install and on a resumed one alike
203
+ // (every later `upgrade` reads it from install.json).
204
+ if (opts.schedulerIntervalMinutes !== undefined) {
205
+ const recorded = readMetadata(installId);
206
+ if (recorded) {
207
+ writeMetadata(installId, {
208
+ ...recorded,
209
+ schedulerIntervalMinutes: opts.schedulerIntervalMinutes,
210
+ });
211
+ }
212
+ }
213
+
195
214
  // 3.5. #656 — a re-run of an install an earlier CLI set up: drop the
196
215
  // secrets that CLI checkpointed (database passwords, the setup link)
197
216
  // before any step writes progress.json again, and say what to rotate.
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * #1328 — the pre-flight cost table shows the MCP-Auth-Proxy on both GCP
5
+ * variants, its own Cloud Armor policy on `gcp`, and the gcp-firebase data
6
+ * gateway (CLAUDE.md §11.C: every billable resource has a cost line).
7
+ */
8
+
9
+ import { describe, expect, it } from "bun:test";
10
+ import { SERVICE_CPU_IDLE } from "../stack-contract.js";
11
+ import {
12
+ adminKeptWarmByTicks,
13
+ alwaysOnAdminInstanceUsd,
14
+ type CostEstimateInputs,
15
+ estimateGcpCost,
16
+ schedulerTickMonthlyUsd,
17
+ } from "./gcp-cost.js";
18
+
19
+ const inputs: CostEstimateInputs = {
20
+ cloudSqlTier: "db-f1-micro",
21
+ cloudSqlHa: false,
22
+ adminMinInstances: 0,
23
+ gatewayMinInstances: 0,
24
+ wafAdaptiveProtection: false,
25
+ region: "europe-west1",
26
+ };
27
+
28
+ function names(provider: "gcp" | "gcp-firebase"): string[] {
29
+ return estimateGcpCost({ ...inputs, provider }).lines.map((l) => l.name);
30
+ }
31
+
32
+ describe("cost table: MCP-Auth-Proxy (#1328)", () => {
33
+ it("lists the proxy's Cloud Run service at $0 idle on both variants", () => {
34
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
35
+ const line = estimateGcpCost({ ...inputs, provider }).lines.find(
36
+ (l) => l.name === "Cloud Run MCP-Auth-Proxy",
37
+ );
38
+ expect(line?.monthlyUsd).toBe(0);
39
+ expect(line?.notes).toContain("scale-to-zero");
40
+ }
41
+ });
42
+
43
+ it("prices the proxy's own Cloud Armor policy on gcp only (policy + 2 rules)", () => {
44
+ const gcp = estimateGcpCost({ ...inputs, provider: "gcp" }).lines.find((l) =>
45
+ l.name.startsWith("MCP-Auth-Proxy backend"),
46
+ );
47
+ expect(gcp?.monthlyUsd).toBe(7);
48
+ expect(names("gcp-firebase").some((n) => n.startsWith("MCP-Auth-Proxy backend"))).toBe(false);
49
+ // The throttle moved off the gateway's policy: no separate rule line.
50
+ expect(names("gcp")).not.toContain("Cloud Armor rule: remote MCP throttle");
51
+ });
52
+
53
+ it("names the gcp-firebase visitor gateway as the data gateway", () => {
54
+ expect(names("gcp-firebase")).toContain("Cloud Run data gateway");
55
+ expect(names("gcp")).toContain("Cloud Run gateway");
56
+ });
57
+ });
58
+
59
+ describe("cost table: scheduler tick (#1140)", () => {
60
+ it("lists the tick at the default 5 minutes: well under a dollar", () => {
61
+ const line = estimateGcpCost(inputs).lines.find((l) => l.name.startsWith("Scheduler tick"));
62
+ expect(line?.name).toBe("Scheduler tick (Cloud Scheduler + Workflows, every 5 min)");
63
+ // 8,640 ticks: 25,920 internal steps (20,920 billed) + 8,640 external
64
+ // (6,640 billed) + the scheduler job.
65
+ expect(line?.monthlyUsd).toBe(0.48);
66
+ });
67
+
68
+ it("follows the knob: one minute stays a few dollars, fifteen little more than the job", () => {
69
+ expect(schedulerTickMonthlyUsd(1)).toBe(2.38);
70
+ expect(schedulerTickMonthlyUsd(15)).toBe(0.16);
71
+ const fast = estimateGcpCost({ ...inputs, schedulerIntervalMinutes: 1 }).lines.find((l) =>
72
+ l.name.startsWith("Scheduler tick"),
73
+ );
74
+ expect(fast?.name).toContain("every 1 min");
75
+ });
76
+
77
+ it("prices the admin kept up around the clock while its CPU is always allocated (#1361)", () => {
78
+ // The tick (at most every 15 minutes) beats Cloud Run's 15-minute idle
79
+ // shutdown, so a scale-to-zero admin never stops.
80
+ expect(SERVICE_CPU_IDLE.admin).toBe(false);
81
+ expect(adminKeptWarmByTicks(0)).toBe(true);
82
+ // 2,592,000 s x (1 vCPU x $0.000018 + 2 GiB x $0.000002).
83
+ expect(alwaysOnAdminInstanceUsd(2)).toBe(57);
84
+ const admin = estimateGcpCost(inputs).lines.find((l) => l.name.startsWith("Cloud Run admin"));
85
+ expect(admin?.monthlyUsd).toBe(57);
86
+ expect(admin?.notes).toContain("around the clock");
87
+ expect(admin?.notes).toContain("#1361");
88
+ });
89
+
90
+ it("adds nothing on the admin with a min instance, which is up anyway", () => {
91
+ expect(adminKeptWarmByTicks(1)).toBe(false);
92
+ const admin = estimateGcpCost({ ...inputs, adminMinInstances: 1 }).lines.find((l) =>
93
+ l.name.startsWith("Cloud Run admin"),
94
+ );
95
+ expect(admin?.notes).toContain("the scheduler tick adds nothing");
96
+ });
97
+
98
+ it("refuses an interval outside 1-15 minutes", () => {
99
+ expect(() => estimateGcpCost({ ...inputs, schedulerIntervalMinutes: 20 })).toThrow(
100
+ "between 1 and 15",
101
+ );
102
+ });
103
+ });