@caelo-cms/provisioning 0.11.10 → 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 (212) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adopt-existing.d.ts +6 -2
  4. package/dist/adopt-existing.d.ts.map +1 -1
  5. package/dist/adopt-existing.js +57 -0
  6. package/dist/adopt-existing.js.map +1 -1
  7. package/dist/agent/command.d.ts +5 -2
  8. package/dist/agent/command.d.ts.map +1 -1
  9. package/dist/agent/command.js +1 -1
  10. package/dist/agent/command.js.map +1 -1
  11. package/dist/agent-db.js +25 -8
  12. package/dist/cli-routing.js +1 -1
  13. package/dist/cli-routing.js.map +1 -1
  14. package/dist/cli.js +59 -7
  15. package/dist/cli.js.map +1 -1
  16. package/dist/cloudsql-tls.d.ts +30 -0
  17. package/dist/cloudsql-tls.d.ts.map +1 -1
  18. package/dist/cloudsql-tls.js +7 -1
  19. package/dist/cloudsql-tls.js.map +1 -1
  20. package/dist/compose.d.ts +14 -0
  21. package/dist/compose.d.ts.map +1 -1
  22. package/dist/compose.js +54 -1
  23. package/dist/compose.js.map +1 -1
  24. package/dist/converge-db-trust.d.ts +44 -0
  25. package/dist/converge-db-trust.d.ts.map +1 -0
  26. package/dist/converge-db-trust.js +158 -0
  27. package/dist/converge-db-trust.js.map +1 -0
  28. package/dist/converge-job-gcp.d.ts +5 -2
  29. package/dist/converge-job-gcp.d.ts.map +1 -1
  30. package/dist/converge-job-gcp.js +9 -2
  31. package/dist/converge-job-gcp.js.map +1 -1
  32. package/dist/converge-job-preflight.d.ts.map +1 -1
  33. package/dist/converge-job-preflight.js +8 -0
  34. package/dist/converge-job-preflight.js.map +1 -1
  35. package/dist/converge-job.d.ts +17 -0
  36. package/dist/converge-job.d.ts.map +1 -1
  37. package/dist/converge-job.js +12 -0
  38. package/dist/converge-job.js.map +1 -1
  39. package/dist/gcp-names.d.ts +13 -0
  40. package/dist/gcp-names.d.ts.map +1 -1
  41. package/dist/gcp-names.js +21 -0
  42. package/dist/gcp-names.js.map +1 -1
  43. package/dist/index.d.ts +2 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/infra-contract.d.ts +1 -1
  48. package/dist/infra-contract.d.ts.map +1 -1
  49. package/dist/infra-contract.js +22 -1
  50. package/dist/infra-contract.js.map +1 -1
  51. package/dist/install-job.d.ts +4 -2
  52. package/dist/install-job.d.ts.map +1 -1
  53. package/dist/install-job.js +4 -1
  54. package/dist/install-job.js.map +1 -1
  55. package/dist/install-state.d.ts +7 -0
  56. package/dist/install-state.d.ts.map +1 -1
  57. package/dist/install-state.js.map +1 -1
  58. package/dist/lifecycle.d.ts.map +1 -1
  59. package/dist/lifecycle.js +3 -0
  60. package/dist/lifecycle.js.map +1 -1
  61. package/dist/migration-runner.d.ts.map +1 -1
  62. package/dist/migration-runner.js +8 -3
  63. package/dist/migration-runner.js.map +1 -1
  64. package/dist/migration-targets.d.ts +11 -0
  65. package/dist/migration-targets.d.ts.map +1 -0
  66. package/dist/migration-targets.js +12 -0
  67. package/dist/migration-targets.js.map +1 -0
  68. package/dist/operator-access.d.ts +10 -0
  69. package/dist/operator-access.d.ts.map +1 -1
  70. package/dist/operator-access.js +5 -0
  71. package/dist/operator-access.js.map +1 -1
  72. package/dist/pool-budget-stack.d.ts +13 -1
  73. package/dist/pool-budget-stack.d.ts.map +1 -1
  74. package/dist/pool-budget-stack.js +11 -1
  75. package/dist/pool-budget-stack.js.map +1 -1
  76. package/dist/public-role-isolation.d.ts +7 -3
  77. package/dist/public-role-isolation.d.ts.map +1 -1
  78. package/dist/public-role-isolation.js +25 -13
  79. package/dist/public-role-isolation.js.map +1 -1
  80. package/dist/release-contract.d.ts.map +1 -1
  81. package/dist/release-contract.js +3 -0
  82. package/dist/release-contract.js.map +1 -1
  83. package/dist/runtime-secrets.d.ts +2 -0
  84. package/dist/runtime-secrets.d.ts.map +1 -1
  85. package/dist/runtime-secrets.js +19 -15
  86. package/dist/runtime-secrets.js.map +1 -1
  87. package/dist/scheduler-setup.d.ts +75 -0
  88. package/dist/scheduler-setup.d.ts.map +1 -0
  89. package/dist/scheduler-setup.js +331 -0
  90. package/dist/scheduler-setup.js.map +1 -0
  91. package/dist/self-hosted-roles.d.ts +16 -9
  92. package/dist/self-hosted-roles.d.ts.map +1 -1
  93. package/dist/self-hosted-roles.js +49 -15
  94. package/dist/self-hosted-roles.js.map +1 -1
  95. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  96. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  97. package/dist/self-hosted-scheduler-interval.js +68 -0
  98. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  99. package/dist/self-hosted-upgrade.d.ts +7 -4
  100. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  101. package/dist/self-hosted-upgrade.js +9 -5
  102. package/dist/self-hosted-upgrade.js.map +1 -1
  103. package/dist/stack-contract.d.ts +67 -6
  104. package/dist/stack-contract.d.ts.map +1 -1
  105. package/dist/stack-contract.js +87 -6
  106. package/dist/stack-contract.js.map +1 -1
  107. package/dist/staging-database.d.ts +22 -0
  108. package/dist/staging-database.d.ts.map +1 -0
  109. package/dist/staging-database.js +117 -0
  110. package/dist/staging-database.js.map +1 -0
  111. package/dist/upgrade/converge.d.ts +5 -0
  112. package/dist/upgrade/converge.d.ts.map +1 -1
  113. package/dist/upgrade/converge.js +110 -10
  114. package/dist/upgrade/converge.js.map +1 -1
  115. package/dist/upgrade/upgrade.d.ts +6 -0
  116. package/dist/upgrade/upgrade.d.ts.map +1 -1
  117. package/dist/upgrade/upgrade.js +3 -0
  118. package/dist/upgrade/upgrade.js.map +1 -1
  119. package/dist/upgrader-gcp.d.ts +5 -4
  120. package/dist/upgrader-gcp.d.ts.map +1 -1
  121. package/dist/upgrader-gcp.js +5 -4
  122. package/dist/upgrader-gcp.js.map +1 -1
  123. package/dist/wizard.d.ts +7 -0
  124. package/dist/wizard.d.ts.map +1 -1
  125. package/dist/wizard.js +11 -0
  126. package/dist/wizard.js.map +1 -1
  127. package/dist/wizards/gcp-cost.d.ts +31 -0
  128. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  129. package/dist/wizards/gcp-cost.js +81 -11
  130. package/dist/wizards/gcp-cost.js.map +1 -1
  131. package/dist/wizards/gcp.d.ts.map +1 -1
  132. package/dist/wizards/gcp.js +87 -11
  133. package/dist/wizards/gcp.js.map +1 -1
  134. package/package.json +2 -2
  135. package/src/adapter.ts +6 -0
  136. package/src/admin-memory.test.ts +1 -1
  137. package/src/adopt-existing.test.ts +46 -0
  138. package/src/adopt-existing.ts +58 -2
  139. package/src/agent/command.ts +6 -3
  140. package/src/caddy-redirects.test.ts +1 -1
  141. package/src/cli-routing.test.ts +1 -1
  142. package/src/cli-routing.ts +1 -1
  143. package/src/cli.ts +63 -8
  144. package/src/cloudsql-tls.test.ts +30 -0
  145. package/src/cloudsql-tls.ts +10 -3
  146. package/src/compose.ts +68 -0
  147. package/src/converge-db-trust.test.ts +237 -0
  148. package/src/converge-db-trust.ts +212 -0
  149. package/src/converge-job-gcp.ts +9 -1
  150. package/src/converge-job-preflight.test.ts +5 -0
  151. package/src/converge-job-preflight.ts +8 -0
  152. package/src/converge-job.test.ts +70 -0
  153. package/src/converge-job.ts +32 -0
  154. package/src/database-tls-builders.test.ts +3 -2
  155. package/src/deployed-release.test.ts +3 -0
  156. package/src/gateway-env-isolation.test.ts +40 -7
  157. package/src/gcp-names.ts +25 -0
  158. package/src/index.test.ts +3 -3
  159. package/src/index.ts +6 -0
  160. package/src/infra-contract.test.ts +5 -3
  161. package/src/infra-contract.ts +22 -1
  162. package/src/install-job.ts +4 -1
  163. package/src/install-state.ts +7 -0
  164. package/src/lifecycle.ts +3 -0
  165. package/src/migration-runner.ts +14 -9
  166. package/src/migration-targets.ts +12 -0
  167. package/src/operator-access.test.ts +15 -0
  168. package/src/operator-access.ts +14 -0
  169. package/src/pool-budget-stack.ts +15 -0
  170. package/src/pool-budget-stacks.test.ts +38 -13
  171. package/src/public-role-isolation.test.ts +18 -8
  172. package/src/public-role-isolation.ts +31 -17
  173. package/src/release-contract.ts +3 -0
  174. package/src/runtime-secrets.test.ts +18 -8
  175. package/src/runtime-secrets.ts +37 -18
  176. package/src/scheduler-setup.test.ts +201 -0
  177. package/src/scheduler-setup.ts +375 -0
  178. package/src/self-hosted-backup.test.ts +1 -0
  179. package/src/self-hosted-compose.test.ts +72 -1
  180. package/src/self-hosted-roles.integration.test.ts +2 -0
  181. package/src/self-hosted-roles.test.ts +49 -19
  182. package/src/self-hosted-roles.ts +60 -20
  183. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  184. package/src/self-hosted-scheduler-interval.ts +79 -0
  185. package/src/self-hosted-start.test.ts +1 -0
  186. package/src/self-hosted-upgrade.test.ts +2 -1
  187. package/src/self-hosted-upgrade.ts +14 -8
  188. package/src/stack-contract.test.ts +31 -3
  189. package/src/stack-contract.ts +113 -6
  190. package/src/stack-converge-secrets.test.ts +2 -0
  191. package/src/stack-converge.test.ts +6 -4
  192. package/src/staging-database.test.ts +114 -0
  193. package/src/staging-database.ts +148 -0
  194. package/src/upgrade/converge.ts +130 -9
  195. package/src/upgrade/upgrade.test.ts +85 -2
  196. package/src/upgrade/upgrade.ts +9 -0
  197. package/src/upgrader-gcp.ts +5 -4
  198. package/src/wizard.ts +19 -0
  199. package/src/wizards/gcp-cost.test.ts +54 -1
  200. package/src/wizards/gcp-cost.ts +101 -11
  201. package/src/wizards/gcp.ts +109 -8
  202. package/stacks/aws/Pulumi.yaml +1 -1
  203. package/stacks/aws/index.ts +41 -1
  204. package/stacks/azure/index.ts +51 -3
  205. package/stacks/gcp/Pulumi.yaml +1 -1
  206. package/stacks/gcp/README.md +1 -0
  207. package/stacks/gcp/index.ts +50 -1
  208. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  209. package/stacks/gcp-firebase/index.ts +50 -1
  210. package/stacks/self-hosted/Pulumi.yaml +3 -0
  211. package/stacks/self-hosted/README.md +4 -0
  212. package/stacks/self-hosted/index.ts +29 -4
@@ -68,7 +68,11 @@ import {
68
68
  REQUIRED_API_LIST,
69
69
  serviceAccountExists,
70
70
  } from "../gcloud.js";
71
- import { GCP_STACK_ENV, upgraderServiceAccountEmail } from "../gcp-names.js";
71
+ import {
72
+ GCP_STACK_ENV,
73
+ schedulerServiceAccountEmail,
74
+ upgraderServiceAccountEmail,
75
+ } from "../gcp-names.js";
72
76
  import { INFRA_CONTRACT_VERSION } from "../infra-contract.js";
73
77
  import {
74
78
  INSTALL_JOB_NAME,
@@ -116,10 +120,12 @@ import {
116
120
  resolveStackRemoteMcpIdentity,
117
121
  } from "../remote-mcp.js";
118
122
  import type { RunMode } from "../run-mode.js";
123
+ import { ensureSchedulerTick } from "../scheduler-setup.js";
119
124
  import { selfCommand } from "../self-command.js";
120
125
  import {
121
126
  ABSENT_DEFAULT_FIREWALL_RULES,
122
127
  cloudSqlPrivateDnsName,
128
+ parseSchedulerIntervalMinutes,
123
129
  type RemoteMcpIdentity,
124
130
  sameRemoteMcpIdentity,
125
131
  } from "../stack-contract.js";
@@ -234,7 +240,15 @@ export async function runGcpWizard(opts: GcpWizardOpts): Promise<void> {
234
240
  // drops the LB / Cloud CDN / Cloud Armor lines (saves ~$19/mo
235
241
  // vs gcp). Default 'gcp' for backwards compatibility with
236
242
  // installs that don't set the field.
237
- 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
+ });
238
252
  note(
239
253
  [
240
254
  bold(
@@ -523,6 +537,18 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
523
537
  // === 11.45. The converge job: releases that change the cloud setup (#819) ===
524
538
  const convergeReady = await stepConvergeJob(projectId, region, releaseVersion);
525
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
+
526
552
  // === 11.5. Operator access: IAP follows the Caelo user list ===
527
553
  await stepOperatorAccessSync(
528
554
  projectId,
@@ -530,6 +556,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
530
556
  provider,
531
557
  ownerEmail,
532
558
  keepSigner ? [`serviceAccount:${upgraderServiceAccountEmail(projectId, GCP_STACK_ENV)}`] : [],
559
+ keepScheduler ? [schedulerServiceAccountEmail(projectId, GCP_STACK_ENV)] : [],
533
560
  );
534
561
 
535
562
  // === 11.6. Remote MCP (#806): LB rules (gcp) + a check of the identity env ===
@@ -546,9 +573,10 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
546
573
  // A fresh install ran every converge step above, so it is at this CLI's
547
574
  // infrastructure contract — recorded only when the upgrader and the
548
575
  // converge job are in place (the steps contract bumps 2 and 9 added) and
549
- // remote MCP's steps (#806, bump 10) succeeded. With the release version,
550
- // the admin can install the next update itself.
551
- 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) {
552
580
  const version = releaseVersion;
553
581
  // #1025 — the linux/amd64 forms Cloud Run reports, next to the indexes,
554
582
  // so the upgrader recognises what serves without a registry lookup.
@@ -998,7 +1026,7 @@ async function stepGrantBaseRoles(
998
1026
  */
999
1027
  async function stepPublicRoleIsolation(projectId: string): Promise<void> {
1000
1028
  const s = spinner();
1001
- s.start("Removing public_role from the Cloud SQL superuser group...");
1029
+ s.start("Removing the gateway logins from the Cloud SQL superuser group...");
1002
1030
  const r = await revokePublicRoleMemberships({
1003
1031
  projectId,
1004
1032
  env: GCP_STACK_ENV,
@@ -1010,7 +1038,7 @@ async function stepPublicRoleIsolation(projectId: string): Promise<void> {
1010
1038
  cancel("Aborted.");
1011
1039
  process.exit(1);
1012
1040
  }
1013
- 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"));
1014
1042
  }
1015
1043
 
1016
1044
  /**
@@ -1216,6 +1244,78 @@ async function upgraderExists(projectId: string): Promise<boolean> {
1216
1244
  return r.ok;
1217
1245
  }
1218
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
+
1219
1319
  /**
1220
1320
  * Set up the operator-access sync job (operator-access.ts): the only
1221
1321
  * principal that may change who passes IAP, run as the operator's gcloud
@@ -1228,6 +1328,7 @@ async function stepOperatorAccessSync(
1228
1328
  provider: "gcp" | "gcp-firebase",
1229
1329
  ownerEmail: string,
1230
1330
  mcpSigners: readonly string[],
1331
+ iapServiceAccounts: readonly string[],
1231
1332
  ): Promise<void> {
1232
1333
  const s = spinner();
1233
1334
  s.start("Setting up the operator-access sync job (Google IAP follows the user list)...");
@@ -1239,7 +1340,7 @@ async function stepOperatorAccessSync(
1239
1340
  ownerEmail,
1240
1341
  });
1241
1342
  const r = resolved.ok
1242
- ? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners })
1343
+ ? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners, iapServiceAccounts })
1243
1344
  : { ok: false as const, done: [], error: resolved.error };
1244
1345
  if (!r.ok) {
1245
1346
  s.stop(red(`Failed: ${r.error}`));
@@ -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
@@ -44,8 +44,10 @@ import {
44
44
  } from "../../dist/aws-task-roles.js";
45
45
  import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
46
46
  import {
47
+ DEFAULT_ADMIN_MAX_INSTANCES,
47
48
  DEFAULT_GATEWAY_MAX_INSTANCES,
48
49
  DEFAULT_MAX_CONNECTIONS,
50
+ DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
49
51
  stackPoolBudget,
50
52
  } from "../../dist/pool-budget-stack.js";
51
53
  import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB } from "../../dist/stack-contract.js";
@@ -86,7 +88,10 @@ const maxConnections = Number.parseInt(
86
88
  cfg.get("maxConnections") ?? String(DEFAULT_MAX_CONNECTIONS.aws),
87
89
  10,
88
90
  );
89
- const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
91
+ const adminMaxInstances = Number.parseInt(
92
+ cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
93
+ 10,
94
+ );
90
95
  const gatewayMaxInstances = Number.parseInt(
91
96
  cfg.get("gatewayMaxInstances") ?? String(DEFAULT_GATEWAY_MAX_INSTANCES),
92
97
  10,
@@ -95,6 +100,8 @@ const poolBudget = stackPoolBudget("aws", {
95
100
  maxConnections,
96
101
  adminMaxInstances,
97
102
  gatewayMaxInstances,
103
+ // #1329 — the staging gateway joins the budget with the staging database.
104
+ stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
98
105
  });
99
106
  if (_adminMemoryMiB === null) {
100
107
  throw new Error(`caelo-aws:adminMemory "${adminMemory}" is not a memory quantity (e.g. 2Gi)`);
@@ -131,6 +138,9 @@ const postgresPassword = pulumi.secret(randomHex(32));
131
138
  // #635) never holds admin_role's. bootstrap.sh creates the roles with these
132
139
  // (ADMIN_ROLE_PASSWORD / PUBLIC_ROLE_PASSWORD).
133
140
  const publicRolePassword = pulumi.secret(randomHex(32));
141
+ // #1329 — the staging gateway's own login on cms_public_staging (created
142
+ // with the database by bootstrap.sh, PUBLIC_STAGING_ROLE_PASSWORD).
143
+ const publicStagingRolePassword = pulumi.secret(randomHex(32));
134
144
  const csrfSecret = pulumi.secret(randomHex(32));
135
145
  const cookieSecret = pulumi.secret(randomHex(32));
136
146
  const anthropicApiKey = pulumi.secret(process.env.ANTHROPIC_API_KEY ?? "");
@@ -151,6 +161,8 @@ function secret(name: string, value: pulumi.Output<string>): aws.secretsmanager.
151
161
 
152
162
  const pgSecret = secret("postgres-password", postgresPassword);
153
163
  const publicRoleSecret = secret("public-role-password", publicRolePassword);
164
+ // #1329 — read by nothing yet: the staging gateway's task (#749) will.
165
+ const _publicStagingRoleSecret = secret("public-staging-role-password", publicStagingRolePassword);
154
166
  const csrfSecretRes = secret("csrf-secret", csrfSecret);
155
167
  const cookieSecretRes = secret("cookie-secret", cookieSecret);
156
168
  const anthropicSecretRes = secret("anthropic-api-key", anthropicApiKey);
@@ -213,6 +225,20 @@ const publicDatabaseUrl = pulumi
213
225
  .apply(
214
226
  ([host, port, pw]) => `postgresql://public_role:${pw}@${host}:${port}/cms_public?${RDS_TLS}`,
215
227
  );
228
+ // #1329 — the admin's pool on the staging database, and the staging
229
+ // gateway's only login (no task definition reads them before #749).
230
+ const publicStagingAdminDatabaseUrl = pulumi
231
+ .all([db.address, db.port, postgresPassword])
232
+ .apply(
233
+ ([host, port, pw]) =>
234
+ `postgresql://caelo_admin:${pw}@${host}:${port}/cms_public_staging?${RDS_TLS}`,
235
+ );
236
+ const publicStagingDatabaseUrl = pulumi
237
+ .all([db.address, db.port, publicStagingRolePassword])
238
+ .apply(
239
+ ([host, port, pw]) =>
240
+ `postgresql://public_staging_role:${pw}@${host}:${port}/cms_public_staging?${RDS_TLS}`,
241
+ );
216
242
 
217
243
  // === 4. S3 buckets ===
218
244
  const mediaBucket = new aws.s3.BucketV2(`${namePrefix}-media`, {
@@ -449,6 +475,16 @@ new aws.cloudwatch.LogDelivery(
449
475
  // refuses every server-side render with that gap
450
476
  // (capture/capture-backend.ts) instead of running a browser in its own task.
451
477
  //
478
+ // FIXME(issue #749): no scheduler tick yet (#1140). On GCP one Cloud
479
+ // Scheduler job calls the admin's /api/internal/scheduler/tick every
480
+ // `schedulerIntervalMinutes` (src/scheduler-setup.ts), and the admin runs
481
+ // its periodic work and plugin workers only on that tick; self-hosted has a
482
+ // scheduler container. The AWS equivalent is one EventBridge Scheduler
483
+ // schedule that runs a small Fargate task (or Lambda) POSTing the same
484
+ // endpoint through the ALB's Cognito gate, with the same 1–15 minute knob.
485
+ // Nothing calls the tick here because no admin task exists on AWS yet; it
486
+ // lands with the admin task in #749.
487
+ //
452
488
  // FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
453
489
  // GCP Settings → Updates hands app-roll releases to the upgrader job and
454
490
  // cloud-setup releases to the converge job; on AWS the admin refuses a
@@ -551,6 +587,7 @@ const dnsRecordsRequired: DnsRecord[] = [
551
587
  const out: CloudAdapterOutputs = {
552
588
  adminDatabaseUrl: adminDatabaseUrl as unknown as string,
553
589
  publicDatabaseUrl: publicDatabaseUrl as unknown as string,
590
+ publicStagingDatabaseUrl: publicStagingDatabaseUrl as unknown as string,
554
591
  mediaStorageUrl: pulumi.interpolate`s3://${mediaBucket.bucket}` as unknown as string,
555
592
  mediaCdnBaseUrl:
556
593
  pulumi.interpolate`https://${distribution.domainName}/media` as unknown as string,
@@ -565,6 +602,9 @@ const out: CloudAdapterOutputs = {
565
602
 
566
603
  export const adminDatabaseUrlOut = out.adminDatabaseUrl;
567
604
  export const publicDatabaseUrlOut = out.publicDatabaseUrl;
605
+ export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
606
+ // #1329 — what the admin task definition (#749) sets as PUBLIC_STAGING_ADMIN_DATABASE_URL.
607
+ export const publicStagingAdminDatabaseUrlOut = publicStagingAdminDatabaseUrl;
568
608
  export const mediaStorageUrlOut = out.mediaStorageUrl;
569
609
  export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
570
610
  export const bootstrapUrlOut = out.bootstrapUrl;
@@ -41,10 +41,15 @@ import * as pulumi from "@pulumi/pulumi";
41
41
  import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
42
42
  import { azureFrontDoorDnsRecords } from "../../dist/azure-front-door.js";
43
43
  import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
44
- import { DEFAULT_MAX_CONNECTIONS, stackPoolBudget } from "../../dist/pool-budget-stack.js";
44
+ import {
45
+ DEFAULT_MAX_CONNECTIONS,
46
+ DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
47
+ stackPoolBudget,
48
+ } from "../../dist/pool-budget-stack.js";
45
49
  import {
46
50
  ADMIN_MEMORY_DEFAULT,
47
51
  azureContainerAppResources,
52
+ GATEWAY_ENVIRONMENT_ENV,
48
53
  GATEWAY_TRUSTED_PROXY_HOPS,
49
54
  MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
50
55
  REMOTE_MCP_MODE,
@@ -88,6 +93,8 @@ const poolBudget = stackPoolBudget("azure", {
88
93
  maxConnections,
89
94
  adminMaxInstances: ADMIN_SIDE_APPS * MAX_REPLICAS,
90
95
  gatewayMaxInstances: MAX_REPLICAS,
96
+ // #1329 — the staging gateway joins the budget with the staging database.
97
+ stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
91
98
  });
92
99
  // #553 — the admin runs the Lighthouse quality audit; same knob + default
93
100
  // as every adapter (stack-contract.ts). Container Apps only accepts fixed
@@ -116,6 +123,9 @@ const postgresPassword = pulumi.secret(randomHex(32));
116
123
  // #635) never holds admin_role's. bootstrap.sh creates the roles with these
117
124
  // (ADMIN_ROLE_PASSWORD / PUBLIC_ROLE_PASSWORD).
118
125
  const publicRolePassword = pulumi.secret(randomHex(32));
126
+ // #1329 — the staging gateway's own login on cms_public_staging (created
127
+ // with the database by bootstrap.sh, PUBLIC_STAGING_ROLE_PASSWORD).
128
+ const publicStagingRolePassword = pulumi.secret(randomHex(32));
119
129
  const csrfSecret = pulumi.secret(randomHex(32));
120
130
  const cookieSecret = pulumi.secret(randomHex(32));
121
131
  const anthropicApiKey = pulumi.secret(process.env.ANTHROPIC_API_KEY ?? "");
@@ -145,6 +155,8 @@ function keyVaultSecret(shortName: string, value: pulumi.Output<string>): azure.
145
155
 
146
156
  const _pgSecret = keyVaultSecret("pg-password", postgresPassword);
147
157
  keyVaultSecret("public-role-password", publicRolePassword);
158
+ // #1329 — read by nothing yet: the staging gateway's app (#750) will.
159
+ keyVaultSecret("public-staging-role-password", publicStagingRolePassword);
148
160
  keyVaultSecret("csrf-secret", csrfSecret);
149
161
  keyVaultSecret("cookie-secret", cookieSecret);
150
162
  keyVaultSecret("anthropic-api-key", anthropicApiKey);
@@ -195,6 +207,13 @@ const _pgPublic = new azure.dbforpostgresql.Database(`${namePrefix}-cms-public-d
195
207
  databaseName: "cms_public",
196
208
  charset: "UTF8",
197
209
  });
210
+ // #1329 — staging's own data, the twin of cms_public on the same server.
211
+ const _pgPublicStaging = new azure.dbforpostgresql.Database(`${namePrefix}-cms-public-staging-db`, {
212
+ resourceGroupName: rg.name,
213
+ serverName: pgServer.name,
214
+ databaseName: "cms_public_staging",
215
+ charset: "UTF8",
216
+ });
198
217
 
199
218
  // #1338 — verify-full to the server's FQDN. Flexible Server's certificate chains
200
219
  // to public roots (DigiCert Global Root G2, Microsoft RSA Root CA 2017) that
@@ -213,6 +232,19 @@ const publicAdminDatabaseUrl = pulumi
213
232
  const publicDatabaseUrl = pulumi
214
233
  .all([pgServer.fullyQualifiedDomainName, publicRolePassword])
215
234
  .apply(([host, pw]) => `postgresql://public_role:${pw}@${host}:5432/cms_public?${AZURE_TLS}`);
235
+ // #1329 — the admin's pool on the staging database (plugin schemas, the
236
+ // staging half of the projection), and the staging gateway's only login.
237
+ const publicStagingAdminDatabaseUrl = pulumi
238
+ .all([pgServer.fullyQualifiedDomainName, postgresPassword])
239
+ .apply(
240
+ ([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_public_staging?${AZURE_TLS}`,
241
+ );
242
+ const publicStagingDatabaseUrl = pulumi
243
+ .all([pgServer.fullyQualifiedDomainName, publicStagingRolePassword])
244
+ .apply(
245
+ ([host, pw]) =>
246
+ `postgresql://public_staging_role:${pw}@${host}:5432/cms_public_staging?${AZURE_TLS}`,
247
+ );
216
248
 
217
249
  // === 4. Storage account + containers ===
218
250
  const storage = new azure.storage.StorageAccount(`${namePrefix}-st`, {
@@ -409,6 +441,15 @@ const logWorkspace = new azure.operationalinsights.Workspace(`${namePrefix}-logs
409
441
  // refuses every server-side render with that gap
410
442
  // (capture/capture-backend.ts) instead of running a browser in its own app.
411
443
  //
444
+ // FIXME(issue #750): no scheduler tick yet (#1140). On GCP one Cloud
445
+ // Scheduler job calls the admin's /api/internal/scheduler/tick every
446
+ // `schedulerIntervalMinutes` (src/scheduler-setup.ts), and the admin runs
447
+ // its periodic work and plugin workers only on that tick; self-hosted has a
448
+ // scheduler container. The Azure equivalent is a scheduled Container Apps
449
+ // job (or Logic App recurrence) POSTing the same endpoint behind Easy Auth,
450
+ // with the same 1–15 minute knob. Nothing calls the tick here because no
451
+ // admin app exists on Azure yet; it lands with the admin in #750.
452
+ //
412
453
  // FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
413
454
  // GCP Settings → Updates hands app-roll releases to the upgrader job and
414
455
  // cloud-setup releases to the converge job; on Azure the admin refuses a
@@ -503,6 +544,7 @@ function containerApp(args: ContainerAppArgs): azure.app.ContainerApp {
503
544
  const adminDatabaseEnv = [
504
545
  { name: "ADMIN_DATABASE_URL", value: adminDatabaseUrl },
505
546
  { name: "PUBLIC_ADMIN_DATABASE_URL", value: publicAdminDatabaseUrl },
547
+ { name: "PUBLIC_STAGING_ADMIN_DATABASE_URL", value: publicStagingAdminDatabaseUrl },
506
548
  ...poolBudget.env.admin,
507
549
  ];
508
550
  const adminApp = containerApp({
@@ -519,8 +561,12 @@ const _gatewayApp = containerApp({
519
561
  { name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl },
520
562
  ...poolBudget.env.gateway,
521
563
  ],
522
- // Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts).
523
- extraEnv: [{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS.azure) }],
564
+ // Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts),
565
+ // and (#1329) that this is the live site's gateway.
566
+ extraEnv: [
567
+ { name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS.azure) },
568
+ { name: GATEWAY_ENVIRONMENT_ENV, value: "live" },
569
+ ],
524
570
  });
525
571
  // #1328 — the MCP-Auth-Proxy, its own Container App and so its own FQDN:
526
572
  // remote MCP's sign-in pages never share an origin with the site's module
@@ -565,6 +611,7 @@ const dnsRecordsRequired = pulumi
565
611
  const out: CloudAdapterOutputs = {
566
612
  adminDatabaseUrl: adminDatabaseUrl as unknown as string,
567
613
  publicDatabaseUrl: publicDatabaseUrl as unknown as string,
614
+ publicStagingDatabaseUrl: publicStagingDatabaseUrl as unknown as string,
568
615
  mediaStorageUrl:
569
616
  pulumi.interpolate`https://${storage.name}.blob.core.windows.net/media` as unknown as string,
570
617
  mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
@@ -578,6 +625,7 @@ const out: CloudAdapterOutputs = {
578
625
 
579
626
  export const adminDatabaseUrlOut = out.adminDatabaseUrl;
580
627
  export const publicDatabaseUrlOut = out.publicDatabaseUrl;
628
+ export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
581
629
  export const mediaStorageUrlOut = out.mediaStorageUrl;
582
630
  export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
583
631
  export const bootstrapUrlOut = out.bootstrapUrl;
@@ -55,7 +55,7 @@ config:
55
55
  description: Admin Cloud Run memory. 2Gi leaves room for the Lighthouse quality audit next to editor requests; billed only while an instance runs.
56
56
  caelo-gcp:adminMaxInstances:
57
57
  type: string
58
- default: "5"
58
+ default: "4"
59
59
  description: |
60
60
  Admin Cloud Run max instances. Each instance gets an equal share of
61
61
  the connection budget (docs/dev/connection-budget.md): at the
@@ -12,6 +12,7 @@ Pulumi stack provisioning Caelo on GCP managed services. Implements the shared `
12
12
  | Container runtime | Four Cloud Run services (admin / gateway / orchestrator / runner) |
13
13
  | Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret and public-role-password (created by the CLI before `pulumi up`, see `src/runtime-secrets.ts`). Cloud Run reads each as a `secretKeyRef`; database URLs carry no password. The gateway's service account reads only `public-role-password` (#613, #635: the gateway has no cms_admin login). |
14
14
  | Edge-log sink | Cloud Logging project sink → BigQuery dataset `<env>_edge_logs` (queryable by P12A analytics plugin) |
15
+ | Scheduler tick (CLI-owned, #1140) | One Cloud Scheduler job `caelo-<env>-scheduler-tick` starts the Workflows workflow of the same name every `schedulerIntervalMinutes` (1–15, default 5; `--scheduler-interval-minutes` on the installer, recorded in install.json). The workflow signs an IAP credential as `caelo-<env>-scheduler` and POSTs the admin's `/api/internal/scheduler/tick`, which runs the due periodic work and plugin workers. Set up by the installer and `upgrade` (`src/scheduler-setup.ts`), not by this stack. |
15
16
 
16
17
  The Caelo runtime never knows it's on GCP — every Cloud Run service consumes plain `DATABASE_URL` / `MEDIA_STORAGE_URL` / `SECRETS_PROVIDER` env vars.
17
18
 
@@ -33,8 +33,10 @@ import {
33
33
  } from "../../dist/gcp-names.js";
34
34
  import { createSiteUrlMap } from "../../dist/gcp-url-map.js";
35
35
  import {
36
+ DEFAULT_ADMIN_MAX_INSTANCES,
36
37
  DEFAULT_GATEWAY_MAX_INSTANCES,
37
38
  DEFAULT_MAX_CONNECTIONS,
39
+ DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
38
40
  stackPoolBudget,
39
41
  } from "../../dist/pool-budget-stack.js";
40
42
  import {
@@ -119,7 +121,10 @@ const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
119
121
  // against the original 25-slot Postgres default — connection exhaustion
120
122
  // took the admin offline. v0.2.49 bakes the post-incident shape so fresh
121
123
  // installs don't trip the same wall.
122
- const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
124
+ const adminMaxInstances = Number.parseInt(
125
+ cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
126
+ 10,
127
+ );
123
128
  const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
124
129
  // #1254 — the gateway's scale-out is part of the connection budget: each
125
130
  // instance holds a cms_public pool.
@@ -137,6 +142,8 @@ const poolBudget = stackPoolBudget("gcp", {
137
142
  maxConnections,
138
143
  adminMaxInstances,
139
144
  gatewayMaxInstances,
145
+ // #1329 — the staging gateway (P3b-2) is budgeted from this release on.
146
+ stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
140
147
  });
141
148
  const wafAdaptiveProtection = cfg.getBoolean("wafAdaptiveProtection") ?? false;
142
149
  const backupRetentionDays = Number.parseInt(cfg.get("backupRetentionDays") ?? "7", 10);
@@ -281,6 +288,8 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
281
288
  "tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
282
289
  "staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
283
290
  "public-role-password": gcpSecretId(env, "public-role-password"),
291
+ // #1329 — the staging gateway's login, never the live one's.
292
+ "public-staging-role-password": gcpSecretId(env, "public-staging-role-password"),
284
293
  // #914 / #806 — the Owner-set Google sign-in client (a placeholder until
285
294
  // then; IAP login, IAP programmatic access and the remote-MCP broker) and
286
295
  // the broker's seal key, both CLI-generated.
@@ -297,6 +306,14 @@ const publicRolePassword = pulumi.secret(
297
306
  { provider: gcpProvider },
298
307
  ).secretData,
299
308
  );
309
+ // #1329 — public_staging_role's own password (CLI-generated like the one
310
+ // above): the staging gateway's only login, on cms_public_staging only.
311
+ const publicStagingRolePassword = pulumi.secret(
312
+ gcp.secretmanager.getSecretVersionOutput(
313
+ { project, secret: gcpSecretId(env, "public-staging-role-password") },
314
+ { provider: gcpProvider },
315
+ ).secretData,
316
+ );
300
317
 
301
318
  // =========================================================================
302
319
  // Tier 4 — Cloud SQL Postgres (private IP only; HA + retention configurable)
@@ -371,6 +388,14 @@ const pgPublicUser = new gcp.sql.User(
371
388
  { instance: sqlInstance.name, name: "public_role", password: publicRolePassword },
372
389
  opts,
373
390
  );
391
+ // #1329 — the staging gateway's login: an Admin API user like public_role,
392
+ // so the same membership revoke (public-role-isolation.ts) and the same
393
+ // audit apply; migration 0530 gives it CONNECT on cms_public_staging only.
394
+ new gcp.sql.User(
395
+ `${namePrefix}-pg-public-staging`,
396
+ { instance: sqlInstance.name, name: "public_staging_role", password: publicStagingRolePassword },
397
+ opts,
398
+ );
374
399
 
375
400
  const cmsAdminDb = new gcp.sql.Database(
376
401
  `${namePrefix}-cms-admin-db`,
@@ -382,6 +407,13 @@ const cmsPublicDb = new gcp.sql.Database(
382
407
  { instance: sqlInstance.name, name: "cms_public" },
383
408
  opts,
384
409
  );
410
+ // #1329 — staging's own data: the twin of cms_public on the same instance
411
+ // (no extra cost), reached by the staging gateway only.
412
+ const cmsPublicStagingDb = new gcp.sql.Database(
413
+ `${namePrefix}-cms-public-staging-db`,
414
+ { instance: sqlInstance.name, name: "cms_public_staging" },
415
+ opts,
416
+ );
385
417
 
386
418
  // #1338 — the instance's DNS name, resolved to its private IP by a private
387
419
  // zone in the VPC (Cloud SQL assigns the name, it does not resolve it).
@@ -767,6 +799,8 @@ const envContractInputs = {
767
799
  admin: dbUrls.admin,
768
800
  publicAdmin: dbUrls.publicAdmin,
769
801
  public: dbUrls.public,
802
+ publicStagingAdmin: dbUrls.publicStagingAdmin,
803
+ publicStaging: dbUrls.publicStaging,
770
804
  },
771
805
  } as const;
772
806
 
@@ -841,6 +875,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
841
875
  pgAdminUser,
842
876
  cmsAdminDb,
843
877
  cmsPublicDb,
878
+ cmsPublicStagingDb,
844
879
  // The env contract names the buckets as strings; keep them ordered
845
880
  // before the services that use them.
846
881
  mediaBucket,
@@ -1155,6 +1190,18 @@ new gcp.iap.WebBackendServiceIamMember(
1155
1190
  // (packages/provisioning/src/capture-job-setup.ts), for the same reason as
1156
1191
  // the audit job above.
1157
1192
 
1193
+ // =========================================================================
1194
+ // Scheduler tick — deliberately NOT here
1195
+ // =========================================================================
1196
+ //
1197
+ // #1140 — one Cloud Scheduler job starts a Cloud Workflows execution every
1198
+ // `schedulerIntervalMinutes` (1–15, default 5); the workflow signs an IAP
1199
+ // credential as the scheduler's own service account and POSTs the admin's
1200
+ // /api/internal/scheduler/tick, where the admin runs its periodic work and
1201
+ // plugin workers from its due-table. The CLI owns the account, its grants,
1202
+ // the workflow and the job (packages/provisioning/src/scheduler-setup.ts),
1203
+ // for the same reason as the audit job above.
1204
+
1158
1205
  // =========================================================================
1159
1206
  // Operator access — deliberately NOT here
1160
1207
  // =========================================================================
@@ -1325,6 +1372,7 @@ const out: CloudAdapterOutputs = {
1325
1372
  // Password-less — the password lives only in Secret Manager.
1326
1373
  adminDatabaseUrl: dbUrls.admin as unknown as string,
1327
1374
  publicDatabaseUrl: dbUrls.public as unknown as string,
1375
+ publicStagingDatabaseUrl: dbUrls.publicStaging as unknown as string,
1328
1376
  mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
1329
1377
  mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
1330
1378
  bootstrapUrl:
@@ -1338,6 +1386,7 @@ const out: CloudAdapterOutputs = {
1338
1386
 
1339
1387
  export const adminDatabaseUrlOut = out.adminDatabaseUrl;
1340
1388
  export const publicDatabaseUrlOut = out.publicDatabaseUrl;
1389
+ export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
1341
1390
  export const mediaStorageUrlOut = out.mediaStorageUrl;
1342
1391
  export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
1343
1392
  export const bootstrapUrlOut = out.bootstrapUrl;
@@ -58,7 +58,7 @@ config:
58
58
  description: Admin Cloud Run memory. 2Gi leaves room for the Lighthouse quality audit next to editor requests; billed only while an instance runs.
59
59
  caelo-gcp-firebase:adminMaxInstances:
60
60
  type: string
61
- default: "5"
61
+ default: "4"
62
62
  description: Admin Cloud Run max instances.
63
63
  caelo-gcp-firebase:maxConnections:
64
64
  type: string