@geekmidas/cli 10.0.0-alpha.73 → 10.0.0-alpha.75

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 (138) hide show
  1. package/dist/{LegacyStateStore-D_78eaHM.cjs → LegacyStateStore-BFnXUFlx.cjs} +2 -2
  2. package/dist/{LegacyStateStore-D_78eaHM.cjs.map → LegacyStateStore-BFnXUFlx.cjs.map} +1 -1
  3. package/dist/{LegacyStateStore-DTo7z924.mjs → LegacyStateStore-cpt3K1vx.mjs} +2 -2
  4. package/dist/{LegacyStateStore-DTo7z924.mjs.map → LegacyStateStore-cpt3K1vx.mjs.map} +1 -1
  5. package/dist/{LocalStateStore-CNjgkRk4.mjs → LocalStateStore-RH9F2ZEy.mjs} +2 -2
  6. package/dist/{LocalStateStore-CNjgkRk4.mjs.map → LocalStateStore-RH9F2ZEy.mjs.map} +1 -1
  7. package/dist/{LocalStateStore-BhrlQIXD.cjs → LocalStateStore-zQGZ9b4J.cjs} +2 -2
  8. package/dist/{LocalStateStore-BhrlQIXD.cjs.map → LocalStateStore-zQGZ9b4J.cjs.map} +1 -1
  9. package/dist/{S3StateStore-oCxRs6HM.cjs → S3StateStore-CT-ZMlQY.cjs} +2 -2
  10. package/dist/{S3StateStore-oCxRs6HM.cjs.map → S3StateStore-CT-ZMlQY.cjs.map} +1 -1
  11. package/dist/{S3StateStore-g2P7T_Mo.mjs → S3StateStore-DBeBc0c0.mjs} +2 -2
  12. package/dist/{S3StateStore-g2P7T_Mo.mjs.map → S3StateStore-DBeBc0c0.mjs.map} +1 -1
  13. package/dist/{SSMStateStore-DXp0m7XI.cjs → SSMStateStore-HmsH3c4v.cjs} +2 -2
  14. package/dist/{SSMStateStore-DXp0m7XI.cjs.map → SSMStateStore-HmsH3c4v.cjs.map} +1 -1
  15. package/dist/{SSMStateStore-CYenunmK.mjs → SSMStateStore-rnzNfOaV.mjs} +2 -2
  16. package/dist/{SSMStateStore-CYenunmK.mjs.map → SSMStateStore-rnzNfOaV.mjs.map} +1 -1
  17. package/dist/{SubscriberGenerator-DZk_pKDh.mjs → SubscriberGenerator-5looQ3gB.mjs} +18 -2
  18. package/dist/SubscriberGenerator-5looQ3gB.mjs.map +1 -0
  19. package/dist/{SubscriberGenerator-Bixm28fN.cjs → SubscriberGenerator-CajwOa5W.cjs} +29 -1
  20. package/dist/SubscriberGenerator-CajwOa5W.cjs.map +1 -0
  21. package/dist/{add-C1ojB2Yy.cjs → add-CHBin8TF.cjs} +2 -2
  22. package/dist/{add-C1ojB2Yy.cjs.map → add-CHBin8TF.cjs.map} +1 -1
  23. package/dist/{add-BiPlBUq3.mjs → add-Cvzdw8cf.mjs} +2 -2
  24. package/dist/{add-BiPlBUq3.mjs.map → add-Cvzdw8cf.mjs.map} +1 -1
  25. package/dist/{bundler-C35h0WKt.cjs → bundler-_8AEZhrs.cjs} +5 -5
  26. package/dist/{bundler-C35h0WKt.cjs.map → bundler-_8AEZhrs.cjs.map} +1 -1
  27. package/dist/{bundler-C2LZoLdP.mjs → bundler-xjp5RwRS.mjs} +5 -5
  28. package/dist/{bundler-C2LZoLdP.mjs.map → bundler-xjp5RwRS.mjs.map} +1 -1
  29. package/dist/{cli-DJVL2sL9.cjs → cli-B4ktxAsM.cjs} +3 -3
  30. package/dist/{cli-DJVL2sL9.cjs.map → cli-B4ktxAsM.cjs.map} +1 -1
  31. package/dist/{cli-BfdfyTF6.mjs → cli-BTGfA0B7.mjs} +3 -3
  32. package/dist/{cli-BfdfyTF6.mjs.map → cli-BTGfA0B7.mjs.map} +1 -1
  33. package/dist/{compose-9II-0IsK.mjs → compose-DdH2JQJD.mjs} +418 -159
  34. package/dist/compose-DdH2JQJD.mjs.map +1 -0
  35. package/dist/{compose-CNnw8meq.cjs → compose-UKyv_1yU.cjs} +440 -157
  36. package/dist/compose-UKyv_1yU.cjs.map +1 -0
  37. package/dist/{config-RLuR2o9A.d.cts → config-B38YWicj.d.cts} +3 -3
  38. package/dist/{config-B8OR_TrO.d.mts.map → config-B38YWicj.d.cts.map} +1 -1
  39. package/dist/{config-B8OR_TrO.d.mts → config-DA0kRTQe.d.mts} +3 -3
  40. package/dist/{config-RLuR2o9A.d.cts.map → config-DA0kRTQe.d.mts.map} +1 -1
  41. package/dist/config.d.cts +3 -3
  42. package/dist/config.d.mts +3 -3
  43. package/dist/deploy/public.cjs +7 -6
  44. package/dist/deploy/public.d.cts +14 -3
  45. package/dist/deploy/public.d.cts.map +1 -1
  46. package/dist/deploy/public.d.mts +14 -3
  47. package/dist/deploy/public.d.mts.map +1 -1
  48. package/dist/deploy/public.mjs +7 -7
  49. package/dist/{deploy-nmov4j0B.mjs → deploy-CuSsiFIc.mjs} +21 -5
  50. package/dist/deploy-CuSsiFIc.mjs.map +1 -0
  51. package/dist/{deploy-BitZLdnY.cjs → deploy-XUZoA1us.cjs} +26 -4
  52. package/dist/deploy-XUZoA1us.cjs.map +1 -0
  53. package/dist/{dokploy-Ce_VTytN.cjs → dokploy-Cvgg_pID.cjs} +2 -2
  54. package/dist/{dokploy-Ce_VTytN.cjs.map → dokploy-Cvgg_pID.cjs.map} +1 -1
  55. package/dist/{dokploy-CZ1vZt21.mjs → dokploy-DZXTQX10.mjs} +2 -2
  56. package/dist/{dokploy-CZ1vZt21.mjs.map → dokploy-DZXTQX10.mjs.map} +1 -1
  57. package/dist/drivers-BdvtCKc7.cjs +200 -0
  58. package/dist/drivers-BdvtCKc7.cjs.map +1 -0
  59. package/dist/drivers-Ch1rJitn.mjs +177 -0
  60. package/dist/drivers-Ch1rJitn.mjs.map +1 -0
  61. package/dist/{encryption-CmfPICSN.mjs → encryption-JkeMPugf.mjs} +2 -2
  62. package/dist/{encryption-CmfPICSN.mjs.map → encryption-JkeMPugf.mjs.map} +1 -1
  63. package/dist/{encryption-CDGRdcMr.cjs → encryption-w-Ehh9DW.cjs} +2 -2
  64. package/dist/{encryption-CDGRdcMr.cjs.map → encryption-w-Ehh9DW.cjs.map} +1 -1
  65. package/dist/{engine-B37qBO5W.mjs → engine-CmozkHxq.mjs} +192 -4
  66. package/dist/engine-CmozkHxq.mjs.map +1 -0
  67. package/dist/{engine-DL2-PW6m.cjs → engine-zpWBMsQq.cjs} +195 -7
  68. package/dist/engine-zpWBMsQq.cjs.map +1 -0
  69. package/dist/{github-C6qF8kHg.cjs → github-BGdJ-Lnm.cjs} +2 -2
  70. package/dist/{github-C6qF8kHg.cjs.map → github-BGdJ-Lnm.cjs.map} +1 -1
  71. package/dist/{github-hjHHm7jM.mjs → github-BNngHoId.mjs} +2 -2
  72. package/dist/{github-hjHHm7jM.mjs.map → github-BNngHoId.mjs.map} +1 -1
  73. package/dist/{index-DHyTqGG3.d.mts → index-B-1lLGzy.d.mts} +2 -2
  74. package/dist/{index-DHyTqGG3.d.mts.map → index-B-1lLGzy.d.mts.map} +1 -1
  75. package/dist/{index-Dg-UZ4Hm.d.cts → index-vMidfqhU.d.cts} +2 -2
  76. package/dist/{index-Dg-UZ4Hm.d.cts.map → index-vMidfqhU.d.cts.map} +1 -1
  77. package/dist/index.cjs +89 -52
  78. package/dist/index.cjs.map +1 -1
  79. package/dist/index.mjs +86 -49
  80. package/dist/index.mjs.map +1 -1
  81. package/dist/{migrate-CFFC3esE.mjs → migrate-BLL1RTAj.mjs} +2 -2
  82. package/dist/{migrate-CFFC3esE.mjs.map → migrate-BLL1RTAj.mjs.map} +1 -1
  83. package/dist/{migrate-kA_j1TPK.cjs → migrate-CgjlYeun.cjs} +2 -2
  84. package/dist/{migrate-kA_j1TPK.cjs.map → migrate-CgjlYeun.cjs.map} +1 -1
  85. package/dist/openapi.d.cts +1 -1
  86. package/dist/openapi.d.mts +1 -1
  87. package/dist/{ready-BIuMw6wH.cjs → ready-BB9xS4S9.cjs} +6 -188
  88. package/dist/ready-BB9xS4S9.cjs.map +1 -0
  89. package/dist/{ready-C0U0NYfB.mjs → ready-ZZoNskrr.mjs} +5 -169
  90. package/dist/ready-ZZoNskrr.mjs.map +1 -0
  91. package/dist/reconcile/public.d.cts +1 -1
  92. package/dist/reconcile/public.d.mts +1 -1
  93. package/dist/{resolve-CV4eQkDa.mjs → resolve-DZNx7rLy.mjs} +3 -3
  94. package/dist/{resolve-CV4eQkDa.mjs.map → resolve-DZNx7rLy.mjs.map} +1 -1
  95. package/dist/{resolve-E5RfY0bm.cjs → resolve-fevXn-wZ.cjs} +3 -3
  96. package/dist/{resolve-E5RfY0bm.cjs.map → resolve-fevXn-wZ.cjs.map} +1 -1
  97. package/dist/{rollback-C-V-Rf4a.mjs → rollback-CLi5Pln_.mjs} +4 -4
  98. package/dist/{rollback-C-V-Rf4a.mjs.map → rollback-CLi5Pln_.mjs.map} +1 -1
  99. package/dist/{rollback-QGwomD64.cjs → rollback-Dw-FW3X2.cjs} +4 -4
  100. package/dist/{rollback-QGwomD64.cjs.map → rollback-Dw-FW3X2.cjs.map} +1 -1
  101. package/dist/{runtime-CWYLIeCU.d.mts → runtime-BzZD5HHi.d.cts} +2 -2
  102. package/dist/{runtime-CWYLIeCU.d.mts.map → runtime-BzZD5HHi.d.cts.map} +1 -1
  103. package/dist/{runtime-CmuQOT6A.d.cts → runtime-Cm6J6L7n.d.mts} +2 -2
  104. package/dist/{runtime-CmuQOT6A.d.cts.map → runtime-Cm6J6L7n.d.mts.map} +1 -1
  105. package/dist/{sniffer-BFmdZpZ2.mjs → sniffer-BmCvVyhq.mjs} +59 -7
  106. package/dist/sniffer-BmCvVyhq.mjs.map +1 -0
  107. package/dist/{sniffer-C-T7dSMN.cjs → sniffer-bMzrX4cO.cjs} +75 -5
  108. package/dist/{sniffer-C-T7dSMN.cjs.map → sniffer-bMzrX4cO.cjs.map} +1 -1
  109. package/dist/{state-DtS2HEfi.mjs → state-BrWIOce_.mjs} +60 -293
  110. package/dist/state-BrWIOce_.mjs.map +1 -0
  111. package/dist/{state-DMjFPOhf.cjs → state-kA4Fkufx.cjs} +70 -321
  112. package/dist/state-kA4Fkufx.cjs.map +1 -0
  113. package/dist/target/public.cjs +1 -1
  114. package/dist/target/public.d.cts +2 -2
  115. package/dist/target/public.d.mts +2 -2
  116. package/dist/target/public.mjs +1 -1
  117. package/dist/{types-BLdCL4fB.d.mts → types-Dp9lgx_S.d.mts} +6 -2
  118. package/dist/{types-BLdCL4fB.d.mts.map → types-Dp9lgx_S.d.mts.map} +1 -1
  119. package/dist/{types-DwCQCkC-.d.cts → types-TzCChNrA.d.cts} +6 -2
  120. package/dist/{types-DwCQCkC-.d.cts.map → types-TzCChNrA.d.cts.map} +1 -1
  121. package/dist/vitest.cjs +1 -1
  122. package/dist/vitest.mjs +1 -1
  123. package/dist/workspace/index.d.cts +2 -2
  124. package/dist/workspace/index.d.mts +2 -2
  125. package/package.json +17 -17
  126. package/dist/SubscriberGenerator-Bixm28fN.cjs.map +0 -1
  127. package/dist/SubscriberGenerator-DZk_pKDh.mjs.map +0 -1
  128. package/dist/compose-9II-0IsK.mjs.map +0 -1
  129. package/dist/compose-CNnw8meq.cjs.map +0 -1
  130. package/dist/deploy-BitZLdnY.cjs.map +0 -1
  131. package/dist/deploy-nmov4j0B.mjs.map +0 -1
  132. package/dist/engine-B37qBO5W.mjs.map +0 -1
  133. package/dist/engine-DL2-PW6m.cjs.map +0 -1
  134. package/dist/ready-BIuMw6wH.cjs.map +0 -1
  135. package/dist/ready-C0U0NYfB.mjs.map +0 -1
  136. package/dist/sniffer-BFmdZpZ2.mjs.map +0 -1
  137. package/dist/state-DMjFPOhf.cjs.map +0 -1
  138. package/dist/state-DtS2HEfi.mjs.map +0 -1
@@ -4,13 +4,14 @@ import { r as discover } from "./discover-BPelzeGW.mjs";
4
4
  import { t as appKey } from "./derive-B4vDpzgP.mjs";
5
5
  import { M as LOGS_PORT, N as resolveLogs } from "./workspace-C7NqCsQF.mjs";
6
6
  import { t as defineTarget } from "./define-DR8xGYfL.mjs";
7
- import { $ as constructGlobs, C as validateImageRef, Ct as composeBuildPaths, D as isOtlpDestination, E as resolveHost, Et as WORKER_PORT, F as devServicesUsed, H as workerUnits, It as portKeys, Lt as primaryPortKey, Nt as DEFAULT_IMAGES, O as otelEnv, Ot as findBuildRoot, P as assertExternalServices, St as hostFor, T as isMainFrontendApp, Tt as TURBO_VERSION, U as encryptSecrets, V as suppliedOnly, _t as workerEnvKeys, a as recordRelease, at as applyBuckets, b as ensureDockerignore, bt as planFor, ct as bucketPolicies, ft as composeFor, g as credentialsFileContent, gt as networkEnv, h as credentialsBuildArg, ht as appEnvKeys, m as withGeneratedSecrets, mt as pgClient, nn as imageRef, ot as applyPolicies, p as DeployJournal, pt as bucketClient, sn as initStageSecrets, st as applyPostgres, t as createEmptyState, ut as postgresStatements, v as appDockerfile, vt as envFor, w as NoDomainForStage, wt as imageLayout, x as workerDockerfile, yt as localRolePassword, z as reportDevServices } from "./state-DtS2HEfi.mjs";
8
- import { i as DEFAULT_EVENTS, r as DEFAULT_CACHE } from "./SubscriberGenerator-DZk_pKDh.mjs";
7
+ import { $t as imageRef, At as DEFAULT_IMAGES, B as suppliedOnly, C as NoDomainForStage, Ct as WORKER_PORT, D as otelEnv, E as isOtlpDestination, N as assertExternalServices, Nt as portKeys, P as devServicesUsed, Pt as primaryPortKey, R as reportDevServices, S as validateImageRef, St as TURBO_VERSION, T as resolveHost, Tt as findBuildRoot, V as workerUnits, Y as constructGlobs, _ as appDockerfile, _t as planFor, a as recordRelease, b as workerDockerfile, bt as composeBuildPaths, ct as composeFor, dt as appEnvKeys, gt as localRolePassword, ht as envFor, in as initStageSecrets, it as bucketPolicies, lt as bucketClient, m as withGeneratedSecrets, mt as workerEnvKeys, nt as applyPolicies, ot as postgresStatements, p as DeployJournal, pt as networkEnv, rt as applyPostgres, t as createEmptyState, tt as applyBuckets, ut as pgClient, w as isMainFrontendApp, xt as imageLayout, y as ensureDockerignore, yt as hostFor } from "./state-BrWIOce_.mjs";
8
+ import { i as DEFAULT_EVENTS } from "./SubscriberGenerator-5looQ3gB.mjs";
9
+ import { i as installedFrom } from "./drivers-Ch1rJitn.mjs";
9
10
  import { u as migrateDatabases } from "./databases-hUHTuwhx.mjs";
10
11
  import { n as assertStageCredentials } from "./credentialSchemas-BJqRJNK4.mjs";
11
12
  import { dirname, join } from "node:path";
12
13
  import { existsSync } from "node:fs";
13
- import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { chmod, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
14
15
  import { hostname, userInfo } from "node:os";
15
16
  import { provideKey, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
16
17
  import { spawn } from "node:child_process";
@@ -63,6 +64,33 @@ var ServicePortUnknown = class extends Error {
63
64
  this.name = "ServicePortUnknown";
64
65
  }
65
66
  };
67
+ /**
68
+ * Whether `ref` names a registry on this machine's loopback — which the
69
+ * daemon pulls from and pushes to over plain HTTP by default, and `docker
70
+ * manifest inspect` reaches only when told `--insecure` (without it, a local
71
+ * registry's image is reported as no such manifest).
72
+ */
73
+ function isLoopbackRegistry(ref) {
74
+ const first = ref.split("/")[0];
75
+ if (!ref.includes("/") || !/[.:]|^localhost$/.test(first)) return false;
76
+ const host = first.replace(/:\d+$/, "");
77
+ return host === "localhost" || /^127(?:\.\d{1,3}){3}$/.test(host);
78
+ }
79
+ /** `docker push` finished without saying what digest the registry stored. */
80
+ var PushDigestUnknown = class extends Error {
81
+ ref;
82
+ output;
83
+ constructor(ref, output) {
84
+ super(`Pushed ${ref}, but docker did not say which digest the registry stored (it said: ${output.trim().split("\n").slice(-3).join(" / ") || "nothing"}). Check the push with \`docker buildx imagetools inspect ${ref}\`.`);
85
+ this.ref = ref;
86
+ this.output = output;
87
+ this.name = "PushDigestUnknown";
88
+ }
89
+ };
90
+ /** The digest `docker push` reports last: `<tag>: digest: sha256:… size: …`. */
91
+ function pushedDigest(output) {
92
+ return [...output.matchAll(/digest: (sha256:[0-9a-f]{64})/g)].at(-1)?.[1];
93
+ }
66
94
  function compose(stack, args) {
67
95
  return [
68
96
  "compose",
@@ -126,6 +154,7 @@ const dockerCompose = {
126
154
  const { code, stdout, stderr } = await capture("docker", [
127
155
  "manifest",
128
156
  "inspect",
157
+ ...isLoopbackRegistry(ref) ? ["--insecure"] : [],
129
158
  ref
130
159
  ]);
131
160
  if (code === 0) return {
@@ -195,6 +224,15 @@ const dockerCompose = {
195
224
  to
196
225
  ]), runOptions(stack));
197
226
  },
227
+ async push(stack, ref) {
228
+ const output = await runOutput("docker", ["push", ref], {
229
+ cwd: stack.cwd,
230
+ ...stack.signal ? { signal: stack.signal } : {}
231
+ });
232
+ const digest = pushedDigest(output);
233
+ if (!digest) throw new PushDigestUnknown(ref, output);
234
+ return digest;
235
+ },
198
236
  async digest(ref) {
199
237
  const { code, stdout } = await capture("docker", [
200
238
  "image",
@@ -265,6 +303,92 @@ async function assertImagesExist(docker, tag, images) {
265
303
  const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
266
304
  if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
267
305
  }
306
+ /**
307
+ * A push or a pull with no `deploy.registry` to push to or pull from.
308
+ *
309
+ * Without one an image is named `<namespace>/<project>-<app>:<tag>`, which
310
+ * Docker resolves to Docker Hub — so a release would be pushed to, or pulled
311
+ * from, somebody else's account. It is never a fallback.
312
+ */
313
+ var RegistryRequired = class extends Error {
314
+ operation;
315
+ ref;
316
+ constructor(operation, ref) {
317
+ super(`${operation === "push" ? "Pushing" : "Pulling"} images needs a registry, and gkm.config.ts sets no deploy.registry: '${ref}' would be ${operation === "push" ? "pushed to" : "pulled from"} Docker Hub. Set deploy.registry (e.g. registry: 'ghcr.io/acme') in gkm.config.ts.` + (operation === "pull" ? " To run images built here instead, pass --build." : ""));
318
+ this.operation = operation;
319
+ this.ref = ref;
320
+ this.name = "RegistryRequired";
321
+ }
322
+ };
323
+ /** An image pinned to its digest: `<ref>@sha256:…`. */
324
+ function pinnedRef(ref, digest) {
325
+ return `${ref.split("@")[0]}@${digest}`;
326
+ }
327
+ /** A digests file with no entry for an app the stack runs. */
328
+ var ImageDigestMissing = class extends Error {
329
+ file;
330
+ apps;
331
+ constructor(file, apps) {
332
+ super(`${file} pins no digest for ${apps.join(", ")}. Use the file the same release's \`gkm compose --build --push --digests-file\` wrote, or run without --digests-file to pull by tag.`);
333
+ this.file = file;
334
+ this.apps = apps;
335
+ this.name = "ImageDigestMissing";
336
+ }
337
+ };
338
+ /** A digests file entry that is not this app's image. */
339
+ var ImageDigestMismatch = class extends Error {
340
+ app;
341
+ expected;
342
+ found;
343
+ constructor(app, expected, found) {
344
+ super(`The digests file pins '${app}' to ${found}, but this release runs ${expected}. It was written for another registry, project, stage or tag; use the file this stage's push of this tag wrote.`);
345
+ this.app = app;
346
+ this.expected = expected;
347
+ this.found = found;
348
+ this.name = "ImageDigestMismatch";
349
+ }
350
+ };
351
+ /** A digests file that is not a JSON object of app to pinned ref. */
352
+ var ImageDigestsInvalid = class extends Error {
353
+ file;
354
+ detail;
355
+ constructor(file, detail) {
356
+ super(`${file} is not a digests file (${detail}). It should be the JSON object \`gkm compose --build --push --digests-file\` writes: { "api": "<registry>/<image>:<tag>@sha256:…" }.`);
357
+ this.file = file;
358
+ this.detail = detail;
359
+ this.name = "ImageDigestsInvalid";
360
+ }
361
+ };
362
+ /** Parse a digests file's content, checking its shape. */
363
+ function parseDigests(file, content) {
364
+ let parsed;
365
+ try {
366
+ parsed = JSON.parse(content);
367
+ } catch (error) {
368
+ throw new ImageDigestsInvalid(file, error instanceof Error ? error.message : String(error));
369
+ }
370
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ImageDigestsInvalid(file, "not a JSON object");
371
+ for (const [app, value] of Object.entries(parsed)) if (typeof value !== "string" || !/@sha256:[0-9a-f]{64}$/.test(value)) throw new ImageDigestsInvalid(file, `'${app}' is not an <image>:<tag>@sha256:… ref`);
372
+ return parsed;
373
+ }
374
+ /**
375
+ * Each image pinned to the digest the file names for it.
376
+ *
377
+ * @throws {ImageDigestMissing} naming every app the file has no entry for
378
+ * @throws {ImageDigestMismatch} when an entry is another image or tag
379
+ */
380
+ function pinImages(file, digests, images) {
381
+ const missing = images.filter((image) => !digests[image.app]).map((image) => image.app);
382
+ if (missing.length > 0) throw new ImageDigestMissing(file, missing);
383
+ return images.map((image) => {
384
+ const pinned = digests[image.app];
385
+ if (pinned.split("@")[0] !== image.ref) throw new ImageDigestMismatch(image.app, image.ref, pinned);
386
+ return {
387
+ ...image,
388
+ ref: pinned
389
+ };
390
+ });
391
+ }
268
392
  //#endregion
269
393
  //#region src/compose/logs.ts
270
394
  /**
@@ -444,6 +568,152 @@ function logsAccess(logs, ctx) {
444
568
  ];
445
569
  }
446
570
  //#endregion
571
+ //#region src/compose/redis.ts
572
+ /**
573
+ * The stack's Redis: what every declared cache lives in when a workspace runs
574
+ * as a `gkm compose` stack, on the local stage and on a deployed one alike.
575
+ *
576
+ * A cache is the one store whose contents can always be rebuilt, so unlike
577
+ * Mailpit and MinIO it is a production service, not a dev stand-in: it runs
578
+ * for every stage, on the compose network only, with nothing published. It
579
+ * is bounded (`maxmemory`, evicting the least recently used key), persisted
580
+ * (an append-only file on a named volume, kept by `--down`) and password
581
+ * protected — the stage's own password, generated once like its seed.
582
+ *
583
+ * A stage that sets a cache's URL in its secrets — a managed Redis — keeps
584
+ * it, and a stack whose every cache is set that way runs no Redis at all.
585
+ *
586
+ * Pure: the stack decides it, and the phases keep the password.
587
+ */
588
+ /**
589
+ * Where a stack's caches live: its own Redis, whatever the deploy target's
590
+ * default is. A stack is one machine running containers, and a Redis beside
591
+ * the apps is cheaper for a cache than a table in the database they share.
592
+ */
593
+ const STACK_CACHE = "redis";
594
+ /** Its compose service name — the host every cache URL points at. */
595
+ const REDIS_SERVICE = "redis";
596
+ /** The port it answers on, inside the network. */
597
+ const REDIS_PORT = 6379;
598
+ /** Reconcile's pin, so `gkm dev` and the stack run one Redis. */
599
+ const REDIS_IMAGE = DEFAULT_IMAGES.redis;
600
+ /**
601
+ * How much it may hold before it evicts. A cache that grows until the box
602
+ * runs out of memory takes the apps with it; 256 MB is a lot of sessions and
603
+ * little of a small server. A project's `docker-compose.<stage>.yml` can
604
+ * replace the command to change it.
605
+ */
606
+ const REDIS_MAXMEMORY = "256mb";
607
+ /** The stage secret holding its password. */
608
+ const REDIS_PASSWORD_KEY = "REDIS_PASSWORD";
609
+ /** Redis's own count of logical databases, before `--databases` raises it. */
610
+ const REDIS_DATABASES = 16;
611
+ /** A deployed stage that runs the stack's Redis and has no password yet. */
612
+ var RedisPasswordMissing = class extends Error {
613
+ stage;
614
+ constructor(stage) {
615
+ super(`The stage '${stage}' runs the stack's Redis and has no ${REDIS_PASSWORD_KEY} in its secrets. Run gkm compose --stage ${stage} without --dry-run once to generate it, or set one: gkm secrets:set ${REDIS_PASSWORD_KEY} '…' --stage ${stage}`);
616
+ this.stage = stage;
617
+ this.name = "RedisPasswordMissing";
618
+ }
619
+ };
620
+ /**
621
+ * Whether a stage's stack runs Redis: a cache is declared and the stage's
622
+ * secrets do not set the URL of every one.
623
+ */
624
+ function runsRedis(manifest, custom) {
625
+ return Object.values(manifest).some((declaration) => declaration?.kind === "cache" && (declaration.provides ?? []).some((key) => custom[key] === void 0));
626
+ }
627
+ /** A password: 256 random bits, URL-safe, so it sits in a URL unescaped. */
628
+ function generateRedisPassword() {
629
+ return randomBytes(32).toString("base64url");
630
+ }
631
+ /**
632
+ * The stage's secrets with a generated Redis password, where it has none.
633
+ * Pure, like `withGeneratedSecrets`: the caller keeps the result once the run
634
+ * goes ahead.
635
+ */
636
+ function withRedisPassword(secrets) {
637
+ if (secrets.custom?.["REDIS_PASSWORD"]) return {
638
+ secrets,
639
+ generated: []
640
+ };
641
+ return {
642
+ secrets: {
643
+ ...secrets,
644
+ custom: {
645
+ ...secrets.custom,
646
+ [REDIS_PASSWORD_KEY]: generateRedisPassword()
647
+ },
648
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
649
+ },
650
+ generated: [REDIS_PASSWORD_KEY]
651
+ };
652
+ }
653
+ /**
654
+ * The stack's Redis for one stage, or nothing where no cache needs it.
655
+ *
656
+ * Each cache it holds gets a logical database of its own, by the order of
657
+ * their ids, so two caches never read each other's keys.
658
+ */
659
+ function stackRedis(options) {
660
+ const { plan, stage, local, custom } = options;
661
+ const caches = plan.resources.filter((r) => r.kind === "cache" && custom[r.envKey] === void 0).sort((a, b) => a.id.localeCompare(b.id));
662
+ if (caches.length === 0) return void 0;
663
+ const set = custom[REDIS_PASSWORD_KEY];
664
+ if (!set && !local) throw new RedisPasswordMissing(stage);
665
+ const password = set ?? "geekmidas";
666
+ const auth = encodeURIComponent(password);
667
+ return {
668
+ password,
669
+ passwordFromSecrets: Boolean(set),
670
+ urls: Object.fromEntries(caches.map((cache, index) => [cache.envKey, `redis://:${auth}@${REDIS_SERVICE}:${REDIS_PORT}/${index}`])),
671
+ env: {
672
+ [REDIS_PASSWORD_KEY]: password,
673
+ REDISCLI_AUTH: password
674
+ },
675
+ databases: Math.max(REDIS_DATABASES, caches.length)
676
+ };
677
+ }
678
+ /**
679
+ * Redis's service: bounded, persisted and password protected, on the
680
+ * network alone.
681
+ *
682
+ * The command runs the image's own entrypoint with `redis-server` first, so
683
+ * it still drops to the `redis` user and owns `/data`; the password is read
684
+ * from the env file by the shell (`$$` is compose's escape for `$`), so
685
+ * neither the compose file nor the container's command holds it.
686
+ */
687
+ function redisService(redis) {
688
+ const flags = [
689
+ `--requirepass "$$${REDIS_PASSWORD_KEY}"`,
690
+ `--maxmemory ${REDIS_MAXMEMORY}`,
691
+ "--maxmemory-policy allkeys-lru",
692
+ "--appendonly yes",
693
+ ...redis.databases > REDIS_DATABASES ? [`--databases ${redis.databases}`] : []
694
+ ];
695
+ return {
696
+ image: REDIS_IMAGE,
697
+ restart: "unless-stopped",
698
+ command: [
699
+ "sh",
700
+ "-c",
701
+ `exec docker-entrypoint.sh redis-server ${flags.join(" ")}`
702
+ ],
703
+ env_file: [{
704
+ path: `./${REDIS_SERVICE}.env`,
705
+ format: "raw"
706
+ }],
707
+ volumes: ["redis-data:/data"],
708
+ healthcheck: {
709
+ test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"],
710
+ interval: "5s",
711
+ timeout: "3s",
712
+ retries: 10
713
+ }
714
+ };
715
+ }
716
+ //#endregion
447
717
  //#region src/compose/caddyfile.ts
448
718
  /** The Caddyfile for a stack's edge. */
449
719
  function edgeCaddyfile(sites, options) {
@@ -640,7 +910,7 @@ function composeStack(input) {
640
910
  const plan = planFor(manifest, stage, provisionOrder(manifest), {
641
911
  localStage: workspace.stages.local,
642
912
  events: DEFAULT_EVENTS.server,
643
- cache: DEFAULT_CACHE.server,
913
+ cache: STACK_CACHE,
644
914
  edge: false
645
915
  });
646
916
  const byId = new Map(plan.resources.map((r) => [r.id, r]));
@@ -700,7 +970,13 @@ function composeStack(input) {
700
970
  allow: input.allowDevServices ?? [],
701
971
  ...domain ? { domain } : {}
702
972
  });
703
- const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0), ...logs ? [LOGS_SERVICE] : []].sort();
973
+ const redis = stackRedis({
974
+ plan,
975
+ stage,
976
+ local,
977
+ custom
978
+ });
979
+ const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0).filter((c) => c !== "redis" || redis !== void 0), ...logs ? [LOGS_SERVICE] : []].sort();
704
980
  const storage = infra.includes("minio") ? (() => {
705
981
  const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
706
982
  return {
@@ -764,6 +1040,7 @@ function composeStack(input) {
764
1040
  return value;
765
1041
  }
766
1042
  if (custom[key] !== void 0) return custom[key];
1043
+ if (owner?.kind === "cache") return redis?.urls[key];
767
1044
  const surface = surfaceUrls.get(key);
768
1045
  if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
769
1046
  return outside[key];
@@ -796,6 +1073,7 @@ function composeStack(input) {
796
1073
  if (app.build && Object.keys(args).length > 0) app.build.args = args;
797
1074
  continue;
798
1075
  }
1076
+ if (input.buildOnly) continue;
799
1077
  const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
800
1078
  const values = valuesFor(app, keys);
801
1079
  const trusted = provideKey(app.id, "trustedOrigins");
@@ -816,7 +1094,7 @@ function composeStack(input) {
816
1094
  };
817
1095
  for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
818
1096
  }
819
- for (const worker of workers) {
1097
+ for (const worker of input.buildOnly ? [] : workers) {
820
1098
  const keys = workerEnvKeys(manifest, worker.id, input.runnables) ?? /* @__PURE__ */ new Set();
821
1099
  worker.env = {
822
1100
  ...valuesFor({
@@ -844,11 +1122,13 @@ function composeStack(input) {
844
1122
  master: credential.master,
845
1123
  ...storage ? { storage } : {},
846
1124
  ...logs ? { logs } : {},
1125
+ ...redis ? { redis } : {},
847
1126
  https,
848
1127
  http,
849
1128
  composeDir: join(workspace.root, stackDir(stage)),
850
1129
  buildRoot: layout.buildRoot,
851
- workspaceRoot: workspace.root
1130
+ workspaceRoot: workspace.root,
1131
+ envFiles: !input.buildOnly
852
1132
  });
853
1133
  const caddyfile = edgeCaddyfile([
854
1134
  ...apps.map((app) => ({
@@ -868,7 +1148,8 @@ function composeStack(input) {
868
1148
  dockerfiles[app.build.dockerfile] = appDockerfile(app.name, workspace.apps[app.name], {
869
1149
  layout,
870
1150
  workspaceRoot: workspace.root,
871
- manifest
1151
+ manifest,
1152
+ cache: STACK_CACHE
872
1153
  });
873
1154
  }
874
1155
  for (const worker of workers) {
@@ -879,7 +1160,8 @@ function composeStack(input) {
879
1160
  app: worker.host
880
1161
  }, workspace.apps[worker.host], {
881
1162
  layout,
882
- workspaceRoot: workspace.root
1163
+ workspaceRoot: workspace.root,
1164
+ cache: STACK_CACHE
883
1165
  });
884
1166
  }
885
1167
  return {
@@ -896,7 +1178,8 @@ function composeStack(input) {
896
1178
  credential,
897
1179
  ...storage ? { storage } : {},
898
1180
  devServices: local ? [] : devServicesUsed(services),
899
- ...logs ? { logs } : {}
1181
+ ...logs ? { logs } : {},
1182
+ ...redis ? { redis } : {}
900
1183
  };
901
1184
  }
902
1185
  /**
@@ -1036,6 +1319,7 @@ function stackFile(options) {
1036
1319
  };
1037
1320
  minio.ports = ["127.0.0.1::9000"];
1038
1321
  }
1322
+ if (services["redis"] && options.redis) services[REDIS_SERVICE] = redisService(options.redis);
1039
1323
  if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
1040
1324
  for (const app of apps) services[app.name] = {
1041
1325
  image: app.ref,
@@ -1095,10 +1379,13 @@ function stackFile(options) {
1095
1379
  retries: 5
1096
1380
  }
1097
1381
  };
1098
- for (const service of Object.values(services)) service.logging = {
1099
- driver: LOG_ROTATION.driver,
1100
- options: { ...LOG_ROTATION.options }
1101
- };
1382
+ for (const service of Object.values(services)) {
1383
+ if (!options.envFiles) delete service.env_file;
1384
+ service.logging = {
1385
+ driver: LOG_ROTATION.driver,
1386
+ options: { ...LOG_ROTATION.options }
1387
+ };
1388
+ }
1102
1389
  return {
1103
1390
  name: project,
1104
1391
  services,
@@ -1121,93 +1408,6 @@ function defaultLayout(root) {
1121
1408
  gkmPaths: ["gkm.config.*"]
1122
1409
  };
1123
1410
  }
1124
- /** The id the stack's compose file names an app's build credentials by. */
1125
- function credentialsSecret(app) {
1126
- return `${app}_credentials`;
1127
- }
1128
- /** The file beside the compose file an app's build credentials are read from. */
1129
- function credentialsFile(app) {
1130
- return `${app}.credentials`;
1131
- }
1132
- /**
1133
- * The stack with each backend's image built with its encrypted credentials:
1134
- * a BuildKit secret per app — the file `credentialsFile` names, mounted as
1135
- * `gkm_credentials` — and the hash naming them as a build arg, so a new key
1136
- * rebuilds the layer that embeds them. Each backend's env file gains the
1137
- * `GKM_MASTER_KEY` that decrypts them, the way a Dokploy deploy injects it.
1138
- *
1139
- * Pure: the caller encrypted them.
1140
- */
1141
- function withBuildCredentials(stack, credentials) {
1142
- const services = { ...stack.compose.services };
1143
- const secrets = { ...stack.compose.secrets };
1144
- const apps = stack.apps.map((app) => {
1145
- const own = credentials[app.name];
1146
- const service = services[app.name];
1147
- if (!own || !service?.build) return app;
1148
- const [key, value] = own.buildArg.split("=");
1149
- services[app.name] = {
1150
- ...service,
1151
- build: {
1152
- ...service.build,
1153
- args: {
1154
- ...service.build.args,
1155
- [key]: value
1156
- },
1157
- secrets: [{
1158
- source: credentialsSecret(app.name),
1159
- target: "gkm_credentials"
1160
- }]
1161
- }
1162
- };
1163
- secrets[credentialsSecret(app.name)] = { file: `./${credentialsFile(app.name)}` };
1164
- return {
1165
- ...app,
1166
- env: {
1167
- ...app.env,
1168
- GKM_MASTER_KEY: own.masterKey
1169
- }
1170
- };
1171
- });
1172
- const workers = stack.workers.map((worker) => {
1173
- const own = credentials[worker.name];
1174
- const service = services[worker.name];
1175
- if (!own || !service?.build) return worker;
1176
- const [key, value] = own.buildArg.split("=");
1177
- services[worker.name] = {
1178
- ...service,
1179
- build: {
1180
- ...service.build,
1181
- args: {
1182
- ...service.build.args,
1183
- [key]: value
1184
- },
1185
- secrets: [{
1186
- source: credentialsSecret(worker.name),
1187
- target: "gkm_credentials"
1188
- }]
1189
- }
1190
- };
1191
- secrets[credentialsSecret(worker.name)] = { file: `./${credentialsFile(worker.name)}` };
1192
- return {
1193
- ...worker,
1194
- env: {
1195
- ...worker.env,
1196
- GKM_MASTER_KEY: own.masterKey
1197
- }
1198
- };
1199
- });
1200
- return {
1201
- ...stack,
1202
- apps,
1203
- workers,
1204
- compose: {
1205
- ...stack.compose,
1206
- services,
1207
- ...Object.keys(secrets).length ? { secrets } : {}
1208
- }
1209
- };
1210
- }
1211
1411
  /** Render an app's env file: one `KEY=value` per line, read raw by compose. */
1212
1412
  function envFile(env) {
1213
1413
  return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
@@ -1303,6 +1503,20 @@ var ComposeAppsUnhealthy = class extends Error {
1303
1503
  * injected, so the rules are asserted without a daemon; the defaults are the
1304
1504
  * real ones.
1305
1505
  */
1506
+ /** `--push` without `--build`: there is nothing built here to push. */
1507
+ var ComposePushNeedsBuild = class extends Error {
1508
+ constructor() {
1509
+ super("--push pushes the images this checkout builds, so it needs --build (and never --pull): gkm compose --stage <stage> --build --push --tag <tag>.");
1510
+ this.name = "ComposePushNeedsBuild";
1511
+ }
1512
+ };
1513
+ /** Digests to run by, on a run that pulls nothing. */
1514
+ var ComposePinNeedsPull = class extends Error {
1515
+ constructor() {
1516
+ super("--digests-file pins the images a release pulls, so it needs --tag (or --pull) — or, with --build --push, it is where the pushed digests are written.");
1517
+ this.name = "ComposePinNeedsPull";
1518
+ }
1519
+ };
1306
1520
  /** A port variable that does not hold a port. */
1307
1521
  var EdgePortInvalid = class extends Error {
1308
1522
  variable;
@@ -1381,10 +1595,15 @@ const defaultDeps = {
1381
1595
  * every image it names exists. Nothing is written: a missing image stops the
1382
1596
  * run here, before a file, a secret or a container is touched.
1383
1597
  */
1384
- async function validateCompose(ctx, deps, forced) {
1598
+ async function validateCompose(ctx, deps, options = {}) {
1385
1599
  const { workspace, stage, identity, tag } = ctx;
1386
1600
  const root = workspace.root;
1387
- const mode = forced ?? (ctx.tagGiven ? "pull" : "build");
1601
+ const mode = options.mode ?? (ctx.tagGiven ? "pull" : "build");
1602
+ const push = options.push === true;
1603
+ if (push && mode !== "build") throw new ComposePushNeedsBuild();
1604
+ if (options.pin && mode !== "pull") throw new ComposePinNeedsPull();
1605
+ const registry = workspace.deploy?.registry;
1606
+ if (!registry && (push || mode === "pull")) throw new RegistryRequired(push ? "push" : "pull", imageRef(identity, ctx.apps[0] ?? workspace.name, void 0, tag));
1388
1607
  const runnables = {};
1389
1608
  const background = {};
1390
1609
  const manifest = await discover({
@@ -1394,14 +1613,13 @@ async function validateCompose(ctx, deps, forced) {
1394
1613
  background
1395
1614
  });
1396
1615
  const { secrets, generated } = await stageSecrets(ctx, manifest);
1397
- await assertStageCredentials({
1616
+ if (!push) await assertStageCredentials({
1398
1617
  root,
1399
1618
  patterns: constructGlobs(workspace),
1400
1619
  manifest,
1401
1620
  stage,
1402
1621
  supplied: secrets?.custom ?? {}
1403
1622
  });
1404
- const masterKeys = {};
1405
1623
  const layout = mode === "build" ? imageLayout(workspace) : void 0;
1406
1624
  const composed = composeStack({
1407
1625
  workspace,
@@ -1413,51 +1631,19 @@ async function validateCompose(ctx, deps, forced) {
1413
1631
  images: {
1414
1632
  mode,
1415
1633
  tag,
1416
- ...workspace.deploy?.registry ? { registry: workspace.deploy.registry } : {}
1634
+ ...registry ? { registry } : {}
1417
1635
  },
1418
1636
  secrets,
1419
1637
  allowDevServices: ctx.allowDevServices,
1420
1638
  ports: edgePorts(deps.env),
1421
- ...layout ? { layout } : {}
1639
+ ...layout ? { layout } : {},
1640
+ ...push ? { buildOnly: true } : {}
1422
1641
  });
1423
1642
  reportDevServices(ctx, composed.devServices);
1424
- const credentials = {};
1425
- if (mode === "build") {
1426
- for (const app of composed.apps) {
1427
- if (app.kind !== "rest-api" || !app.env || !app.build) continue;
1428
- const { encrypted, iv, masterKey } = encryptSecrets(app.env);
1429
- credentials[app.name] = {
1430
- encrypted,
1431
- iv
1432
- };
1433
- ctx.secrets.mask(masterKey);
1434
- masterKeys[app.name] = {
1435
- masterKey,
1436
- buildArg: credentialsBuildArg({
1437
- encrypted,
1438
- iv
1439
- })
1440
- };
1441
- }
1442
- for (const worker of composed.workers) {
1443
- if (!worker.env || !worker.build) continue;
1444
- const { encrypted, iv, masterKey } = encryptSecrets(worker.env);
1445
- credentials[worker.name] = {
1446
- encrypted,
1447
- iv
1448
- };
1449
- ctx.secrets.mask(masterKey);
1450
- masterKeys[worker.name] = {
1451
- masterKey,
1452
- buildArg: credentialsBuildArg({
1453
- encrypted,
1454
- iv
1455
- })
1456
- };
1457
- }
1458
- }
1459
- const stack = withBuildCredentials(composed, masterKeys);
1643
+ if (mode === "build") assertRedisClient(workspace, composed);
1644
+ const stack = options.pin ? withPinnedImages(composed, options.pin.file, options.pin.digests) : composed;
1460
1645
  if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, [...stack.apps, ...stack.workers]);
1646
+ if (stack.redis && !stack.local) ctx.secrets.mask(stack.redis.password);
1461
1647
  if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
1462
1648
  ctx.secrets.mask(stack.logs.password);
1463
1649
  ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
@@ -1466,7 +1652,8 @@ async function validateCompose(ctx, deps, forced) {
1466
1652
  const override = stackOverrideFile(root, stage);
1467
1653
  const overrides = existsSync(override) ? [override] : [];
1468
1654
  if (overrides.length > 0) ctx.logger.info(`🧩 Merging docker-compose.${stage}.yml over the generated stack`);
1469
- const previous = Object.fromEntries(Object.entries((await ctx.state.read(stage))?.state.releases ?? {}).map(([name, releases]) => {
1655
+ const recorded = push ? void 0 : await ctx.state.read(stage);
1656
+ const previous = Object.fromEntries(Object.entries(recorded?.state.releases ?? {}).map(([name, releases]) => {
1470
1657
  const { ref, tag, digest } = releases.current;
1471
1658
  return [name, {
1472
1659
  ref,
@@ -1476,6 +1663,7 @@ async function validateCompose(ctx, deps, forced) {
1476
1663
  }));
1477
1664
  return {
1478
1665
  mode,
1666
+ push,
1479
1667
  stack,
1480
1668
  dir,
1481
1669
  ref: {
@@ -1490,11 +1678,32 @@ async function validateCompose(ctx, deps, forced) {
1490
1678
  generated,
1491
1679
  previous,
1492
1680
  files: [],
1493
- credentials,
1494
1681
  images: {},
1495
1682
  changes: []
1496
1683
  };
1497
1684
  }
1685
+ /** A stack whose caches are in Redis, built for an app that has no client. */
1686
+ var RedisClientMissing = class extends Error {
1687
+ apps;
1688
+ constructor(apps) {
1689
+ super(`The stack keeps every cache in Redis, and ${apps.join(", ")} ${apps.length === 1 ? "does" : "do"} not depend on ioredis, the client the generated server reaches it with. Add it to the workspace: pnpm add ioredis`);
1690
+ this.apps = apps;
1691
+ this.name = "RedisClientMissing";
1692
+ }
1693
+ };
1694
+ /**
1695
+ * Every backend and worker image built for a stack with a cache can resolve
1696
+ * `ioredis` — listed by its app or a directory above it, the way Node and
1697
+ * the image's install find it.
1698
+ */
1699
+ function assertRedisClient(workspace, stack) {
1700
+ if (!stack.plan.resources.some((r) => r.kind === "cache")) return;
1701
+ const missing = [.../* @__PURE__ */ new Set([...stack.apps.filter((app) => app.kind === "rest-api").map((a) => a.name), ...stack.workers.map((worker) => worker.host)])].filter((name) => {
1702
+ const app = workspace.apps[name];
1703
+ return app && !installedFrom(join(workspace.root, app.path), "ioredis");
1704
+ }).sort();
1705
+ if (missing.length > 0) throw new RedisClientMissing(missing);
1706
+ }
1498
1707
  /**
1499
1708
  * The stage's secrets — and, for a deployed stage, everything it generates
1500
1709
  * once: its seed, and each declared secret and keyring. Kept only once the
@@ -1512,8 +1721,16 @@ async function stageSecrets(ctx, manifest) {
1512
1721
  secrets: withSeed.secrets,
1513
1722
  generated: []
1514
1723
  };
1515
- const secrets = withLogs.secrets;
1516
- const generated = [...withSeed.generated, ...withLogs.generated];
1724
+ const withRedis = runsRedis(manifest, withLogs.secrets.custom ?? {}) ? withRedisPassword(withLogs.secrets) : {
1725
+ secrets: withLogs.secrets,
1726
+ generated: []
1727
+ };
1728
+ const secrets = withRedis.secrets;
1729
+ const generated = [
1730
+ ...withSeed.generated,
1731
+ ...withLogs.generated,
1732
+ ...withRedis.generated
1733
+ ];
1517
1734
  if (secrets.seed) ctx.secrets.mask(secrets.seed);
1518
1735
  for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
1519
1736
  return {
@@ -1522,13 +1739,36 @@ async function stageSecrets(ctx, manifest) {
1522
1739
  };
1523
1740
  }
1524
1741
  /**
1742
+ * The stack with each app's and worker's image pinned to the digest the file
1743
+ * names: the refs it is checked, pulled and recorded by, and the image each
1744
+ * compose service runs.
1745
+ */
1746
+ function withPinnedImages(stack, file, digests) {
1747
+ const apps = pinImages(file, digests, stack.apps);
1748
+ const workers = pinImages(file, digests, stack.workers);
1749
+ const services = { ...stack.compose.services };
1750
+ for (const image of [...apps, ...workers]) services[image.name] = {
1751
+ ...services[image.name],
1752
+ image: image.ref
1753
+ };
1754
+ return {
1755
+ ...stack,
1756
+ apps,
1757
+ workers,
1758
+ compose: {
1759
+ ...stack.compose,
1760
+ services
1761
+ }
1762
+ };
1763
+ }
1764
+ /**
1525
1765
  * A dry run: the stack's files, written so they can be read, and what a real
1526
1766
  * run would do — no image is built or pulled, no container touched, nothing
1527
1767
  * recorded and no secret kept.
1528
1768
  */
1529
1769
  async function planCompose(ctx, run) {
1530
1770
  if (run.generated.length > 0) ctx.logger.info(`🔑 "${ctx.stage}" has no ${run.generated.join(", ")} yet; this dry run used values it did not keep.`);
1531
- run.files = await writeStack(ctx.cwd, run.dir, run.stack, run.credentials);
1771
+ run.files = await writeStack(ctx.cwd, run.dir, run.stack);
1532
1772
  printPlan(ctx, run);
1533
1773
  const planned = (change) => {
1534
1774
  run.changes.push(change);
@@ -1537,7 +1777,7 @@ async function planCompose(ctx, run) {
1537
1777
  ...change
1538
1778
  });
1539
1779
  };
1540
- for (const service of run.stack.infra) planned({
1780
+ for (const service of run.push ? [] : run.stack.infra) planned({
1541
1781
  key: `service:${service}`,
1542
1782
  resourceType: "service",
1543
1783
  action: "ensure"
@@ -1549,6 +1789,7 @@ async function planCompose(ctx, run) {
1549
1789
  action: run.mode === "build" ? "build" : "reuse",
1550
1790
  id: app.ref
1551
1791
  });
1792
+ if (run.push) continue;
1552
1793
  planned({
1553
1794
  key: `service:${app.name}`,
1554
1795
  resourceType: "service",
@@ -1562,13 +1803,14 @@ async function planCompose(ctx, run) {
1562
1803
  action: run.mode === "build" ? "build" : "reuse",
1563
1804
  id: worker.ref
1564
1805
  });
1806
+ if (run.push) continue;
1565
1807
  planned({
1566
1808
  key: `service:${worker.name}`,
1567
1809
  resourceType: "service",
1568
1810
  action: run.previous[worker.name] ? "reuse" : "create"
1569
1811
  });
1570
1812
  }
1571
- planned({
1813
+ if (!run.push) planned({
1572
1814
  key: "service:caddy",
1573
1815
  resourceType: "service",
1574
1816
  action: "ensure"
@@ -1581,11 +1823,12 @@ async function planCompose(ctx, run) {
1581
1823
  */
1582
1824
  async function provisionCompose(ctx, run, deps) {
1583
1825
  const { stack, ref } = run;
1826
+ if (run.push) return;
1584
1827
  if (run.generated.length > 0 && run.secrets) {
1585
1828
  await ctx.secrets.write(run.secrets);
1586
1829
  ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
1587
1830
  }
1588
- run.files = await writeStack(ctx.cwd, run.dir, stack, run.credentials);
1831
+ run.files = await writeStack(ctx.cwd, run.dir, stack);
1589
1832
  printPlan(ctx, run);
1590
1833
  if (stack.infra.length > 0) {
1591
1834
  ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
@@ -1653,6 +1896,10 @@ async function buildCompose(ctx, run, deps) {
1653
1896
  const { stack, ref } = run;
1654
1897
  const images = [...stack.apps, ...stack.workers];
1655
1898
  const apps = images.map((image) => image.app);
1899
+ if (run.push) {
1900
+ run.files = await writeStack(ctx.cwd, run.dir, stack);
1901
+ printPlan(ctx, run);
1902
+ }
1656
1903
  if (run.mode === "build") {
1657
1904
  ctx.logger.info("\n🐳 Building images…");
1658
1905
  await deps.docker.build(ref, apps);
@@ -1660,8 +1907,10 @@ async function buildCompose(ctx, run, deps) {
1660
1907
  ctx.logger.info(`\n🐳 Pulling ${ctx.tag}…`);
1661
1908
  await deps.docker.pull(ref, apps);
1662
1909
  }
1910
+ if (run.push) ctx.logger.info("\n📤 Pushing images…");
1663
1911
  for (const image of images) {
1664
- const digest = await deps.docker.digest(image.ref);
1912
+ const digest = run.push ? await deps.docker.push(ref, image.ref) : await deps.docker.digest(image.ref);
1913
+ if (run.push) ctx.logger.info(` ${image.app.padEnd(12)} ${image.ref}@${digest}`);
1665
1914
  run.images[image.app] = {
1666
1915
  ref: image.ref,
1667
1916
  tag: image.tag,
@@ -1687,6 +1936,7 @@ async function buildCompose(ctx, run, deps) {
1687
1936
  */
1688
1937
  async function releaseCompose(ctx, run, deps) {
1689
1938
  const { stack, ref } = run;
1939
+ if (run.push) return;
1690
1940
  ctx.logger.info("\n🚀 Starting the stack…");
1691
1941
  await deps.docker.up(ref);
1692
1942
  if (stack.local) await deps.docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", caFile(run)).catch(() => {});
@@ -1745,6 +1995,7 @@ async function recordImages(ctx, run) {
1745
1995
  */
1746
1996
  async function verifyCompose(ctx, run, deps) {
1747
1997
  const { stack } = run;
1998
+ if (run.push) return;
1748
1999
  const ca = stack.local ? await readFile(caFile(run), "utf-8").catch(() => void 0) : void 0;
1749
2000
  ctx.logger.info("\n🩺 Checking each app through Caddy…");
1750
2001
  const results = await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
@@ -1924,7 +2175,7 @@ function applied(ctx, run, change) {
1924
2175
  });
1925
2176
  }
1926
2177
  /** Write the stack's files: owner-only, since the env files hold secrets. */
1927
- async function writeStack(root, dir, stack, credentials = {}) {
2178
+ async function writeStack(root, dir, stack) {
1928
2179
  await mkdir(dir, {
1929
2180
  recursive: true,
1930
2181
  mode: 448
@@ -1940,8 +2191,9 @@ async function writeStack(root, dir, stack, credentials = {}) {
1940
2191
  await write(join(dir, "docker-compose.yml"), composeYaml(stack));
1941
2192
  await write(join(dir, "Caddyfile"), stack.caddyfile);
1942
2193
  for (const app of [...stack.apps, ...stack.workers]) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
1943
- if (stack.logs) await write(join(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
1944
- for (const [name, payload] of Object.entries(credentials)) await write(join(dir, credentialsFile(name)), credentialsFileContent(payload), 384);
2194
+ if (stack.logs && stack.compose.services["openobserve"]?.env_file) await write(join(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
2195
+ if (stack.redis && stack.compose.services["redis"]?.env_file) await write(join(dir, `${REDIS_SERVICE}.env`), envFile(stack.redis.env), 384);
2196
+ for (const entry of await readdir(dir)) if (entry.endsWith(".credentials")) await rm(join(dir, entry));
1945
2197
  for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
1946
2198
  await ensureDockerignore(findBuildRoot(root));
1947
2199
  return files;
@@ -1991,13 +2243,16 @@ function printPlan(ctx, run) {
1991
2243
  * digest in the stage's state
1992
2244
  * - `verify`: each app asked through Caddy over HTTPS
1993
2245
  *
2246
+ * With `push` (`gkm compose --build --push`), run with `buildOnly`, it builds
2247
+ * every image and pushes it to `deploy.registry`, and does nothing else.
2248
+ *
1994
2249
  * `gkm compose` is this target with a few more switches (`--build`, `--pull`,
1995
2250
  * `--down`), run through the same `deploy()`.
1996
2251
  */
1997
2252
  var compose_exports = /* @__PURE__ */ __exportAll({ composeTarget: () => composeTarget });
1998
2253
  /** The compose target, with its Docker, bundler and probe as given. */
1999
2254
  function composeTarget(options = {}) {
2000
- const { mode, report, ...given } = options;
2255
+ const { mode, push, pin, report, ...given } = options;
2001
2256
  const deps = {
2002
2257
  ...defaultDeps,
2003
2258
  ...given
@@ -2013,7 +2268,11 @@ function composeTarget(options = {}) {
2013
2268
  },
2014
2269
  credentials: [],
2015
2270
  tag: ({ cwd }) => deps.revision(cwd),
2016
- validate: (ctx) => validateCompose(ctx, deps, mode),
2271
+ validate: (ctx) => validateCompose(ctx, deps, {
2272
+ ...mode ? { mode } : {},
2273
+ ...push ? { push } : {},
2274
+ ...pin ? { pin } : {}
2275
+ }),
2017
2276
  plan: (ctx, run) => planCompose(ctx, run),
2018
2277
  provision: (ctx, run) => provisionCompose(ctx, run, deps),
2019
2278
  build: (ctx, run) => buildCompose(ctx, run, deps),
@@ -2026,6 +2285,6 @@ function composeTarget(options = {}) {
2026
2285
  });
2027
2286
  }
2028
2287
  //#endregion
2029
- export { stackDir as a, composeProject as i, compose_exports as n, dockerCompose as o, stackOverrideFile as r, composeTarget as t };
2288
+ export { stackOverrideFile as a, parseDigests as c, ComposePushNeedsBuild as i, pinnedRef as l, compose_exports as n, composeProject as o, ComposePinNeedsPull as r, stackDir as s, composeTarget as t, dockerCompose as u };
2030
2289
 
2031
- //# sourceMappingURL=compose-9II-0IsK.mjs.map
2290
+ //# sourceMappingURL=compose-DdH2JQJD.mjs.map