@geekmidas/cli 10.0.0-alpha.54 → 10.0.0-alpha.55
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.
- package/README.md +2 -2
- package/dist/{HostingerProvider-GF6NAZAK.mjs → HostingerProvider-cL-6oQ5r.mjs} +2 -2
- package/dist/{HostingerProvider-GF6NAZAK.mjs.map → HostingerProvider-cL-6oQ5r.mjs.map} +1 -1
- package/dist/LegacyStateStore-ByRWQ9s1.mjs +89 -0
- package/dist/LegacyStateStore-ByRWQ9s1.mjs.map +1 -0
- package/dist/LegacyStateStore-C_OFs1gN.cjs +89 -0
- package/dist/LegacyStateStore-C_OFs1gN.cjs.map +1 -0
- package/dist/LocalStateStore-BupdV-VN.mjs +163 -0
- package/dist/LocalStateStore-BupdV-VN.mjs.map +1 -0
- package/dist/LocalStateStore-DDDU0HkI.cjs +163 -0
- package/dist/LocalStateStore-DDDU0HkI.cjs.map +1 -0
- package/dist/{OpenApiTsGenerator-BzIGy8KE.mjs → OpenApiTsGenerator-DKmAorGX.mjs} +34 -11
- package/dist/{OpenApiTsGenerator-BzIGy8KE.mjs.map → OpenApiTsGenerator-DKmAorGX.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BN0PIZxd.cjs → OpenApiTsGenerator-WlUHjimW.cjs} +34 -11
- package/dist/{OpenApiTsGenerator-BN0PIZxd.cjs.map → OpenApiTsGenerator-WlUHjimW.cjs.map} +1 -1
- package/dist/S3StateStore-C7eBae0G.cjs +160 -0
- package/dist/S3StateStore-C7eBae0G.cjs.map +1 -0
- package/dist/S3StateStore-FhzyENrN.mjs +161 -0
- package/dist/S3StateStore-FhzyENrN.mjs.map +1 -0
- package/dist/SSMStateStore-DD7jKrCO.cjs +146 -0
- package/dist/SSMStateStore-DD7jKrCO.cjs.map +1 -0
- package/dist/SSMStateStore-oda9xOGc.mjs +147 -0
- package/dist/SSMStateStore-oda9xOGc.mjs.map +1 -0
- package/dist/{backends-Dfeu5Dgc.mjs → backends-BNrMPlJb.mjs} +2 -2
- package/dist/{backends-Dfeu5Dgc.mjs.map → backends-BNrMPlJb.mjs.map} +1 -1
- package/dist/{backends-D5aDWxuA.cjs → backends-nBauiBkt.cjs} +2 -2
- package/dist/{backends-D5aDWxuA.cjs.map → backends-nBauiBkt.cjs.map} +1 -1
- package/dist/{bundler--fyybf1N.mjs → bundler-URImKj-A.mjs} +2 -2
- package/dist/{bundler--fyybf1N.mjs.map → bundler-URImKj-A.mjs.map} +1 -1
- package/dist/{config-Cvr5fCbn.cjs → config-C7NZG2tr.cjs} +2 -2
- package/dist/{config-Cvr5fCbn.cjs.map → config-C7NZG2tr.cjs.map} +1 -1
- package/dist/{config-SOkDH-n1.mjs → config-XEd-dKVX.mjs} +2 -2
- package/dist/{config-SOkDH-n1.mjs.map → config-XEd-dKVX.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{encryption-O-XdeNxe.mjs → encryption-Bc1cMdeY.mjs} +4 -4
- package/dist/{encryption-O-XdeNxe.mjs.map → encryption-Bc1cMdeY.mjs.map} +1 -1
- package/dist/{encryption-tB4LqLKN.cjs → encryption-DSHYN5ap.cjs} +4 -4
- package/dist/{encryption-tB4LqLKN.cjs.map → encryption-DSHYN5ap.cjs.map} +1 -1
- package/dist/{github-BGx4U61b.cjs → github-CWflxaV4.cjs} +3 -3
- package/dist/{github-BGx4U61b.cjs.map → github-CWflxaV4.cjs.map} +1 -1
- package/dist/{github-U_frvZgD.mjs → github-DWHXTcwL.mjs} +3 -3
- package/dist/{github-U_frvZgD.mjs.map → github-DWHXTcwL.mjs.map} +1 -1
- package/dist/{index-fU3qHy7c.d.cts → index-B8nraKvg.d.mts} +2 -2
- package/dist/{index-DYh5TrsZ.d.mts.map → index-B8nraKvg.d.mts.map} +1 -1
- package/dist/{index-DYh5TrsZ.d.mts → index-BvBQS89a.d.cts} +2 -2
- package/dist/{index-fU3qHy7c.d.cts.map → index-BvBQS89a.d.cts.map} +1 -1
- package/dist/index.cjs +993 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +991 -221
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-DN5VM5vy.cjs → migrate-DHMRgnP6.cjs} +4 -4
- package/dist/{migrate-DN5VM5vy.cjs.map → migrate-DHMRgnP6.cjs.map} +1 -1
- package/dist/{migrate-DWDaaBfo.mjs → migrate-PcQg692O.mjs} +4 -4
- package/dist/{migrate-DWDaaBfo.mjs.map → migrate-PcQg692O.mjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{ready-VkNwfJu7.cjs → ready-BefA4mM0.cjs} +2 -2
- package/dist/{ready-VkNwfJu7.cjs.map → ready-BefA4mM0.cjs.map} +1 -1
- package/dist/{ready-C8wG_d0v.mjs → ready-bES_Jk5v.mjs} +2 -2
- package/dist/{ready-C8wG_d0v.mjs.map → ready-bES_Jk5v.mjs.map} +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{types-DYDT4FfE.d.cts → types-Cgj-cq2m.d.mts} +136 -5
- package/dist/types-Cgj-cq2m.d.mts.map +1 -0
- package/dist/{types-CT0ymlBk.d.mts → types-CmxMhLGp.d.cts} +137 -4
- package/dist/types-CmxMhLGp.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Bwl0728I.mjs → workspace-BE0WHPw9.mjs} +8 -8
- package/dist/{workspace-Bwl0728I.mjs.map → workspace-BE0WHPw9.mjs.map} +1 -1
- package/dist/{workspace-DFYu-iRP.cjs → workspace-CXvYabDJ.cjs} +6 -6
- package/dist/{workspace-DFYu-iRP.cjs.map → workspace-CXvYabDJ.cjs.map} +1 -1
- package/dist/{workspace-bOsks1uk.mjs → workspace-DHK2N1mq.mjs} +22 -3
- package/dist/{workspace-bOsks1uk.mjs.map → workspace-DHK2N1mq.mjs.map} +1 -1
- package/dist/{workspace-BcCiLzTO.cjs → workspace-DL_99LJC.cjs} +22 -3
- package/dist/{workspace-BcCiLzTO.cjs.map → workspace-DL_99LJC.cjs.map} +1 -1
- package/package.json +14 -14
- package/dist/types-CT0ymlBk.d.mts.map +0 -1
- package/dist/types-DYDT4FfE.d.cts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
4
4
|
const require_discover = require("./discover-DXNviAh-.cjs");
|
|
5
|
-
const require_config = require("./config-
|
|
6
|
-
const require_workspace = require("./workspace-
|
|
5
|
+
const require_config = require("./config-C7NZG2tr.cjs");
|
|
6
|
+
const require_workspace = require("./workspace-DL_99LJC.cjs");
|
|
7
7
|
const require_storage = require("./storage-CSGoC3bK.cjs");
|
|
8
8
|
const require_store = require("./store-DQgxT_WR.cjs");
|
|
9
|
-
const require_workspace$1 = require("./workspace-
|
|
10
|
-
const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-
|
|
9
|
+
const require_workspace$1 = require("./workspace-CXvYabDJ.cjs");
|
|
10
|
+
const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-WlUHjimW.cjs");
|
|
11
11
|
const require_openapi = require("./openapi.cjs");
|
|
12
|
-
const require_ready = require("./ready-
|
|
13
|
-
const require_backends = require("./backends-
|
|
12
|
+
const require_ready = require("./ready-BefA4mM0.cjs");
|
|
13
|
+
const require_backends = require("./backends-nBauiBkt.cjs");
|
|
14
14
|
const require_emit = require("./emit-Dmc1nvdS.cjs");
|
|
15
15
|
let commander = require("commander");
|
|
16
16
|
let node_process = require("node:process");
|
|
@@ -20,10 +20,10 @@ let node_fs = require("node:fs");
|
|
|
20
20
|
let node_fs_promises = require("node:fs/promises");
|
|
21
21
|
let node_os = require("node:os");
|
|
22
22
|
let node_path = require("node:path");
|
|
23
|
-
let node_child_process = require("node:child_process");
|
|
24
23
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
25
24
|
let fast_glob = require("fast-glob");
|
|
26
25
|
fast_glob = require_rolldown_runtime.__toESM(fast_glob, 1);
|
|
26
|
+
let node_child_process = require("node:child_process");
|
|
27
27
|
let node_module = require("node:module");
|
|
28
28
|
let chokidar = require("chokidar");
|
|
29
29
|
chokidar = require_rolldown_runtime.__toESM(chokidar, 1);
|
|
@@ -48,7 +48,7 @@ let _geekmidas_constructs_queue = require("@geekmidas/constructs/queue");
|
|
|
48
48
|
let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
|
|
49
49
|
let node_dns_promises = require("node:dns/promises");
|
|
50
50
|
//#region package.json
|
|
51
|
-
var version = "10.0.0-alpha.
|
|
51
|
+
var version = "10.0.0-alpha.55";
|
|
52
52
|
//#endregion
|
|
53
53
|
//#region src/auth/credentials.ts
|
|
54
54
|
/**
|
|
@@ -394,12 +394,12 @@ function withOwningTsconfigJsx(nodeOptions = "") {
|
|
|
394
394
|
* unhelpful.
|
|
395
395
|
*
|
|
396
396
|
* A separate command rather than part of `gkm setup`, for one reason: this
|
|
397
|
-
*
|
|
398
|
-
* trade than one that tells you what to type. Caddy's own `caddy trust`
|
|
399
|
-
* the line in the same place.
|
|
397
|
+
* asks for a password, and a setup that silently escalates on every run is a
|
|
398
|
+
* worse trade than one that tells you what to type. Caddy's own `caddy trust`
|
|
399
|
+
* draws the line in the same place.
|
|
400
400
|
*/
|
|
401
401
|
const logger$11 = console;
|
|
402
|
-
const run = (0, node_util.promisify)(node_child_process.execFile);
|
|
402
|
+
const run$1 = (0, node_util.promisify)(node_child_process.execFile);
|
|
403
403
|
/**
|
|
404
404
|
* How each platform is told about a new authority.
|
|
405
405
|
*
|
|
@@ -410,15 +410,14 @@ const run = (0, node_util.promisify)(node_child_process.execFile);
|
|
|
410
410
|
*/
|
|
411
411
|
function installation(platform, certificate) {
|
|
412
412
|
if (platform === "darwin") return {
|
|
413
|
-
describe: "
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
describe: "your login keychain",
|
|
414
|
+
asks: "macOS asks once — your password or Touch ID — to change trust settings.",
|
|
415
|
+
steps: [["security", [
|
|
416
416
|
"add-trusted-cert",
|
|
417
|
-
"-d",
|
|
418
417
|
"-r",
|
|
419
418
|
"trustRoot",
|
|
420
419
|
"-k",
|
|
421
|
-
"
|
|
420
|
+
(0, node_path.join)((0, node_os.homedir)(), "Library/Keychains/login.keychain-db"),
|
|
422
421
|
certificate
|
|
423
422
|
]]]
|
|
424
423
|
};
|
|
@@ -426,6 +425,7 @@ function installation(platform, certificate) {
|
|
|
426
425
|
const debian = (0, node_fs.existsSync)("/usr/local/share/ca-certificates");
|
|
427
426
|
return {
|
|
428
427
|
describe: debian ? "the system CA bundle" : "the system trust anchors",
|
|
428
|
+
asks: "This needs sudo: the system trust store is the only one.",
|
|
429
429
|
steps: debian ? [["sudo", [
|
|
430
430
|
"cp",
|
|
431
431
|
certificate,
|
|
@@ -449,15 +449,15 @@ async function trustCommand(options = {}) {
|
|
|
449
449
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
450
450
|
const certificate = (0, node_path.join)(workspace.root, require_workspace$1.LOCAL_CA_PATH);
|
|
451
451
|
if (!(0, node_fs.existsSync)(certificate)) throw new NoLocalAuthority(certificate);
|
|
452
|
-
const { describe, steps } = installation(process.platform, certificate);
|
|
452
|
+
const { describe, asks, steps } = installation(process.platform, certificate);
|
|
453
453
|
logger$11.log(`\n🔐 Trusting the local authority in ${describe}`);
|
|
454
454
|
logger$11.log(` ${certificate}\n`);
|
|
455
455
|
if (options.dryRun) {
|
|
456
456
|
for (const [command, args] of steps) logger$11.log(` ${command} ${args.join(" ")}`);
|
|
457
457
|
return;
|
|
458
458
|
}
|
|
459
|
-
logger$11.log(
|
|
460
|
-
for (const [command, args] of steps) await run(command, args, { stdio: "inherit" });
|
|
459
|
+
logger$11.log(` ${asks}\n`);
|
|
460
|
+
for (const [command, args] of steps) await run$1(command, args, { stdio: "inherit" });
|
|
461
461
|
logger$11.log("\n✅ Local https addresses are trusted. Restart your browser.\n");
|
|
462
462
|
}
|
|
463
463
|
/**
|
|
@@ -465,7 +465,7 @@ async function trustCommand(options = {}) {
|
|
|
465
465
|
*
|
|
466
466
|
* A machine's answer, not the project's: whether a laptop installs a root
|
|
467
467
|
* certificate is its owner's call, so the decision is `--yes`, then what this
|
|
468
|
-
* machine previously answered, then a prompt. Never a silent
|
|
468
|
+
* machine previously answered, then a prompt. Never a silent escalation — that is
|
|
469
469
|
* the one thing a setup command should not do on somebody's behalf.
|
|
470
470
|
*
|
|
471
471
|
* Silent and free when the machine already trusts it, which is what makes it
|
|
@@ -482,7 +482,7 @@ async function ensureTrusted(root, url, options = {}) {
|
|
|
482
482
|
const { install } = await (0, prompts.default)({
|
|
483
483
|
type: "confirm",
|
|
484
484
|
name: "install",
|
|
485
|
-
message: "Trust this project's local certificate authority, so a browser accepts its https addresses? (
|
|
485
|
+
message: "Trust this project's local certificate authority, so a browser accepts its https addresses? (asks for your password)",
|
|
486
486
|
initial: false
|
|
487
487
|
});
|
|
488
488
|
if (!install) {
|
|
@@ -523,7 +523,7 @@ async function recordDeclined(root) {
|
|
|
523
523
|
async function isTrusted(url) {
|
|
524
524
|
const { NODE_EXTRA_CA_CERTS: _ignored, ...env } = process.env;
|
|
525
525
|
try {
|
|
526
|
-
await run(process.execPath, [
|
|
526
|
+
await run$1(process.execPath, [
|
|
527
527
|
"--use-system-ca",
|
|
528
528
|
"-e",
|
|
529
529
|
`fetch(${JSON.stringify(url)}).then(()=>0,()=>process.exit(1))`
|
|
@@ -1374,10 +1374,10 @@ function normalizeHooksConfig(config, cwd = process.cwd()) {
|
|
|
1374
1374
|
/**
|
|
1375
1375
|
* What a `--production` build is.
|
|
1376
1376
|
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1377
|
+
* Background work — queues polled, crons scheduled, subscribers drained — is
|
|
1378
|
+
* included here, and `buildApp` takes it back out when the app serves a
|
|
1379
|
+
* RestApi: an API's image answers HTTP and nothing else, and that work belongs
|
|
1380
|
+
* to a Worker. The build still writes their files on every server build.
|
|
1381
1381
|
*
|
|
1382
1382
|
* @internal Exported for testing
|
|
1383
1383
|
*/
|
|
@@ -1771,9 +1771,9 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1771
1771
|
for (const { id, port } of fakes) logger$9.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1772
1772
|
logger$9.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
|
|
1773
1773
|
if (options.migrate || options.seed) {
|
|
1774
|
-
const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-
|
|
1774
|
+
const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-DHMRgnP6.cjs"));
|
|
1775
1775
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1776
|
-
} else await Promise.resolve().then(() => require("./migrate-
|
|
1776
|
+
} else await Promise.resolve().then(() => require("./migrate-DHMRgnP6.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1777
1777
|
logger$9.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1778
1778
|
});
|
|
1779
1779
|
const appUrls = {};
|
|
@@ -2744,6 +2744,147 @@ var TopicGenerator = class extends require_discover.ConstructGenerator {
|
|
|
2744
2744
|
}
|
|
2745
2745
|
};
|
|
2746
2746
|
//#endregion
|
|
2747
|
+
//#region src/run.ts
|
|
2748
|
+
/**
|
|
2749
|
+
* Start a program with an argument array, never a shell string.
|
|
2750
|
+
*
|
|
2751
|
+
* `gkm build`, `gkm docker` and `gkm deploy` pass values that come from the
|
|
2752
|
+
* workspace — image refs, tags, build args, package names — to `docker` and
|
|
2753
|
+
* `turbo`. Joined into a string and handed to a shell, a package named
|
|
2754
|
+
* `x;curl …|sh` or a tag holding `$(…)` became part of the command line, which
|
|
2755
|
+
* matters most where the CLI builds a repository it didn't write: a CI job for a
|
|
2756
|
+
* fork, a shared build runner. Here every value reaches the program as one
|
|
2757
|
+
* `argv` element, whatever it contains.
|
|
2758
|
+
*/
|
|
2759
|
+
/** Long enough for a cold image build; short enough that a hung one ends. */
|
|
2760
|
+
const DEFAULT_TIMEOUT_MS = 18e5;
|
|
2761
|
+
/** How long a timed-out child has to exit on SIGTERM before it is killed. */
|
|
2762
|
+
const KILL_GRACE_MS = 5e3;
|
|
2763
|
+
/** The command line as a person would read it — for messages, never to run. */
|
|
2764
|
+
function display(command, args) {
|
|
2765
|
+
return [command, ...args].join(" ");
|
|
2766
|
+
}
|
|
2767
|
+
/** The program ran and exited unsuccessfully; its own output said why. */
|
|
2768
|
+
var CommandFailed = class extends Error {
|
|
2769
|
+
command;
|
|
2770
|
+
args;
|
|
2771
|
+
exitCode;
|
|
2772
|
+
signal;
|
|
2773
|
+
constructor(command, args, exitCode, signal) {
|
|
2774
|
+
super(`\`${display(command, args)}\` ${exitCode === null ? `was stopped by ${signal ?? "a signal"}` : `exited with code ${exitCode}`}. Its output above says why; run it by hand to see it again.`);
|
|
2775
|
+
this.command = command;
|
|
2776
|
+
this.args = args;
|
|
2777
|
+
this.exitCode = exitCode;
|
|
2778
|
+
this.signal = signal;
|
|
2779
|
+
this.name = "CommandFailed";
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
/** The program ran past its timeout and was killed. */
|
|
2783
|
+
var CommandTimedOut = class extends Error {
|
|
2784
|
+
command;
|
|
2785
|
+
args;
|
|
2786
|
+
timeoutMs;
|
|
2787
|
+
constructor(command, args, timeoutMs) {
|
|
2788
|
+
super(`\`${display(command, args)}\` was still running after ${Math.round(timeoutMs / 1e3)}s and was stopped. Check what it was waiting on — a registry, the Docker daemon, a prompt — and run it again.`);
|
|
2789
|
+
this.command = command;
|
|
2790
|
+
this.args = args;
|
|
2791
|
+
this.timeoutMs = timeoutMs;
|
|
2792
|
+
this.name = "CommandTimedOut";
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
/**
|
|
2796
|
+
* Run `command` with `args`, resolving when it exits 0.
|
|
2797
|
+
*
|
|
2798
|
+
* Rejects with {@link CommandFailed} on any other exit, {@link CommandTimedOut}
|
|
2799
|
+
* once `timeoutMs` passes (after the child is gone), and with the spawn error
|
|
2800
|
+
* itself when the program cannot be started at all — `ENOENT` already names it.
|
|
2801
|
+
*/
|
|
2802
|
+
function run(command, args, options = {}) {
|
|
2803
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...spawnOptions } = options;
|
|
2804
|
+
return new Promise((resolve, reject) => {
|
|
2805
|
+
const child = (0, node_child_process.spawn)(command, [...args], {
|
|
2806
|
+
stdio: "inherit",
|
|
2807
|
+
...spawnOptions,
|
|
2808
|
+
shell: false
|
|
2809
|
+
});
|
|
2810
|
+
let timedOut = false;
|
|
2811
|
+
let killer;
|
|
2812
|
+
const timer = setTimeout(() => {
|
|
2813
|
+
timedOut = true;
|
|
2814
|
+
child.kill("SIGTERM");
|
|
2815
|
+
killer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
2816
|
+
killer.unref?.();
|
|
2817
|
+
}, timeoutMs);
|
|
2818
|
+
timer.unref?.();
|
|
2819
|
+
const settle = () => {
|
|
2820
|
+
clearTimeout(timer);
|
|
2821
|
+
if (killer) clearTimeout(killer);
|
|
2822
|
+
};
|
|
2823
|
+
child.on("error", (error) => {
|
|
2824
|
+
settle();
|
|
2825
|
+
reject(error);
|
|
2826
|
+
});
|
|
2827
|
+
child.on("close", (code, signal) => {
|
|
2828
|
+
settle();
|
|
2829
|
+
if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
|
|
2830
|
+
else if (code === 0) resolve();
|
|
2831
|
+
else reject(new CommandFailed(command, args, code, signal));
|
|
2832
|
+
});
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
//#endregion
|
|
2836
|
+
//#region src/secrets/encryption.ts
|
|
2837
|
+
var encryption_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
2838
|
+
encryptSecrets: () => encryptSecrets,
|
|
2839
|
+
generateDefineOptions: () => generateDefineOptions,
|
|
2840
|
+
keyFingerprint: () => keyFingerprint
|
|
2841
|
+
});
|
|
2842
|
+
/** AES-256-GCM configuration */
|
|
2843
|
+
const ALGORITHM = "aes-256-gcm";
|
|
2844
|
+
const KEY_LENGTH = 32;
|
|
2845
|
+
const IV_LENGTH = 12;
|
|
2846
|
+
/**
|
|
2847
|
+
* Encrypt secrets for embedding in a bundle.
|
|
2848
|
+
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
2849
|
+
*
|
|
2850
|
+
* @param secrets - Key-value pairs to encrypt
|
|
2851
|
+
* @returns Encrypted payload with ephemeral master key
|
|
2852
|
+
*/
|
|
2853
|
+
function encryptSecrets(secrets) {
|
|
2854
|
+
const masterKey = (0, node_crypto.randomBytes)(KEY_LENGTH);
|
|
2855
|
+
const iv = (0, node_crypto.randomBytes)(IV_LENGTH);
|
|
2856
|
+
const plaintext = JSON.stringify(secrets);
|
|
2857
|
+
const cipher = (0, node_crypto.createCipheriv)(ALGORITHM, masterKey, iv);
|
|
2858
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
2859
|
+
const authTag = cipher.getAuthTag();
|
|
2860
|
+
return {
|
|
2861
|
+
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
2862
|
+
iv: iv.toString("hex"),
|
|
2863
|
+
masterKey: masterKey.toString("hex")
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Generate the define options for tsdown/esbuild.
|
|
2868
|
+
* These will be injected at build time.
|
|
2869
|
+
*/
|
|
2870
|
+
function generateDefineOptions(payload) {
|
|
2871
|
+
return {
|
|
2872
|
+
__GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
|
|
2873
|
+
__GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* A short, one-way name for a master key: the first 8 hex characters of its
|
|
2878
|
+
* SHA-256.
|
|
2879
|
+
*
|
|
2880
|
+
* Printed where the key itself used to be, so a person can tell which key a
|
|
2881
|
+
* build used — and match it against the one they hold — without the output
|
|
2882
|
+
* carrying a value that decrypts every secret of the stage.
|
|
2883
|
+
*/
|
|
2884
|
+
function keyFingerprint(masterKey) {
|
|
2885
|
+
return (0, node_crypto.createHash)("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
2886
|
+
}
|
|
2887
|
+
//#endregion
|
|
2747
2888
|
//#region src/build/owners.ts
|
|
2748
2889
|
/**
|
|
2749
2890
|
* What a generated entry imports its runtime from, read off the discovered
|
|
@@ -2874,6 +3015,11 @@ export default app;
|
|
|
2874
3015
|
//#region src/build/index.ts
|
|
2875
3016
|
const logger$8 = console;
|
|
2876
3017
|
/**
|
|
3018
|
+
* Where `gkm build --stage` leaves the key its bundle's secrets were encrypted
|
|
3019
|
+
* with, beside the bundle in `.gkm/server/` and readable by its owner alone.
|
|
3020
|
+
*/
|
|
3021
|
+
const MASTER_KEY_FILE = "master.key";
|
|
3022
|
+
/**
|
|
2877
3023
|
* What a build run at the workspace root reads.
|
|
2878
3024
|
*
|
|
2879
3025
|
* One backend app there, and it is that app's config, so its own `openapi`
|
|
@@ -3089,6 +3235,18 @@ async function buildApp(input) {
|
|
|
3089
3235
|
logger$8.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
|
|
3090
3236
|
return {};
|
|
3091
3237
|
}
|
|
3238
|
+
if (production?.subscribers === "include" && derived.surface) {
|
|
3239
|
+
buildContext.production = {
|
|
3240
|
+
...production,
|
|
3241
|
+
subscribers: "exclude"
|
|
3242
|
+
};
|
|
3243
|
+
const left = [
|
|
3244
|
+
[allCrons.length, "cron"],
|
|
3245
|
+
[allQueues.length, "queue consumer"],
|
|
3246
|
+
[allSubscribers.length, "subscriber"]
|
|
3247
|
+
].filter(([n]) => n > 0).map(([n, what]) => `${n} ${what}${n === 1 ? "" : "s"}`);
|
|
3248
|
+
if (left.length > 0) logger$8.log(`Serving ${derived.surface.id} only: leaving out ${left.join(", ")} — they run in a Worker's image`);
|
|
3249
|
+
}
|
|
3092
3250
|
const result = await buildForTarget(target, buildContext, appRoot, workspaceRoot, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, enableOpenApi, input.skipBundle ?? false, input.stage);
|
|
3093
3251
|
await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), {
|
|
3094
3252
|
openapi: config.openapi,
|
|
@@ -3166,8 +3324,11 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3166
3324
|
})).masterKey;
|
|
3167
3325
|
logger$8.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
|
|
3168
3326
|
if (masterKey) {
|
|
3327
|
+
const keyPath = (0, node_path.join)(outputDir, MASTER_KEY_FILE);
|
|
3328
|
+
await (0, node_fs_promises.writeFile)(keyPath, `${masterKey}\n`, { mode: 384 });
|
|
3329
|
+
await (0, node_fs_promises.chmod)(keyPath, 384);
|
|
3169
3330
|
logger$8.log(`\n🔐 Secrets encrypted for deployment`);
|
|
3170
|
-
logger$8.log(`
|
|
3331
|
+
logger$8.log(` Run with GKM_MASTER_KEY set to the key in ${(0, node_path.relative)(appRoot, keyPath)} (fingerprint ${keyFingerprint(masterKey)})`);
|
|
3171
3332
|
}
|
|
3172
3333
|
}
|
|
3173
3334
|
return {
|
|
@@ -3190,7 +3351,7 @@ function detectPackageManager$2() {
|
|
|
3190
3351
|
return "npm";
|
|
3191
3352
|
}
|
|
3192
3353
|
/**
|
|
3193
|
-
*
|
|
3354
|
+
* The program and argv that run turbo's build.
|
|
3194
3355
|
*
|
|
3195
3356
|
* `filters` names the packages to build. Passing none lets turbo infer its own
|
|
3196
3357
|
* scope from the working directory, which for a workspace root means the
|
|
@@ -3198,14 +3359,24 @@ function detectPackageManager$2() {
|
|
|
3198
3359
|
* `gkm build`, inferring is how you get a build that runs itself forever.
|
|
3199
3360
|
* `workspaceBuildCommand` always names the apps.
|
|
3200
3361
|
*
|
|
3362
|
+
* An array, not a command string: the filters are package names read from the
|
|
3363
|
+
* workspace, and each must reach turbo as one argument whatever it holds. Each
|
|
3364
|
+
* is `--filter=<name>`, so even a name starting with `-` stays a filter value.
|
|
3365
|
+
*
|
|
3201
3366
|
* @internal Exported for testing
|
|
3202
3367
|
*/
|
|
3203
|
-
function
|
|
3204
|
-
const
|
|
3368
|
+
function getTurboArgs(pm, filters = [], { only = false } = {}) {
|
|
3369
|
+
const turbo = [
|
|
3370
|
+
"turbo",
|
|
3371
|
+
"run",
|
|
3372
|
+
"build",
|
|
3373
|
+
...(typeof filters === "string" ? [filters] : filters).map((f) => `--filter=${f}`),
|
|
3374
|
+
...only ? ["--only"] : []
|
|
3375
|
+
];
|
|
3205
3376
|
switch (pm) {
|
|
3206
|
-
case "pnpm": return
|
|
3207
|
-
case "yarn": return
|
|
3208
|
-
case "npm": return
|
|
3377
|
+
case "pnpm": return ["pnpm", ["exec", ...turbo]];
|
|
3378
|
+
case "yarn": return ["yarn", turbo];
|
|
3379
|
+
case "npm": return ["npx", turbo];
|
|
3209
3380
|
}
|
|
3210
3381
|
}
|
|
3211
3382
|
/**
|
|
@@ -3294,27 +3465,16 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3294
3465
|
const { filters, unpackaged } = turboFilters(workspace, { exclude: "backend" });
|
|
3295
3466
|
if (unpackaged.length > 0) throw new Error(`No package.json for workspace app(s): ${unpackaged.join(", ")}. Each app needs one — turbo builds packages, and gkm reads the name back to know which app it is building.`);
|
|
3296
3467
|
const pm = detectPackageManager$2();
|
|
3297
|
-
const
|
|
3468
|
+
const [turboBin, turboArgs] = getTurboArgs(pm, filters, { only: true });
|
|
3298
3469
|
if (filters.length > 0) {
|
|
3299
3470
|
logger$8.log(`\n📦 Using ${pm} with Turbo for the other apps...\n`);
|
|
3300
|
-
logger$8.log(`Running: ${
|
|
3301
|
-
await
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
...process.env,
|
|
3308
|
-
NODE_ENV: options.production ? "production" : "development"
|
|
3309
|
-
}
|
|
3310
|
-
});
|
|
3311
|
-
child.on("close", (code) => {
|
|
3312
|
-
if (code === 0) resolve();
|
|
3313
|
-
else reject(/* @__PURE__ */ new Error(`Turbo build failed with exit code ${code}`));
|
|
3314
|
-
});
|
|
3315
|
-
child.on("error", (err) => {
|
|
3316
|
-
reject(err);
|
|
3317
|
-
});
|
|
3471
|
+
logger$8.log(`Running: ${[turboBin, ...turboArgs].join(" ")}`);
|
|
3472
|
+
await run(turboBin, turboArgs, {
|
|
3473
|
+
cwd: workspace.root,
|
|
3474
|
+
env: {
|
|
3475
|
+
...process.env,
|
|
3476
|
+
NODE_ENV: options.production ? "production" : "development"
|
|
3477
|
+
}
|
|
3318
3478
|
});
|
|
3319
3479
|
}
|
|
3320
3480
|
for (const [appName, app] of apps) {
|
|
@@ -4667,6 +4827,39 @@ function getPublicUrlArgNames(app) {
|
|
|
4667
4827
|
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
4668
4828
|
}
|
|
4669
4829
|
//#endregion
|
|
4830
|
+
//#region src/docker/imageRef.ts
|
|
4831
|
+
/**
|
|
4832
|
+
* An image reference, checked before it reaches `docker`.
|
|
4833
|
+
*
|
|
4834
|
+
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
4835
|
+
* name from the project, a tag from `--tag` — and an argument array already
|
|
4836
|
+
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
4837
|
+
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
4838
|
+
* own grammar (distribution/reference) and anything else is refused by name
|
|
4839
|
+
* before a build starts.
|
|
4840
|
+
*/
|
|
4841
|
+
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
4842
|
+
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
4843
|
+
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
4844
|
+
const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
|
|
4845
|
+
/** Docker refuses a repository name longer than this. */
|
|
4846
|
+
const MAX_NAME_LENGTH = 255;
|
|
4847
|
+
/** A value that is not an image reference was about to be built or pushed. */
|
|
4848
|
+
var ImageRefInvalid = class extends Error {
|
|
4849
|
+
ref;
|
|
4850
|
+
constructor(ref) {
|
|
4851
|
+
super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
|
|
4852
|
+
this.ref = ref;
|
|
4853
|
+
this.name = "ImageRefInvalid";
|
|
4854
|
+
}
|
|
4855
|
+
};
|
|
4856
|
+
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
4857
|
+
function validateImageRef(ref) {
|
|
4858
|
+
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
4859
|
+
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
4860
|
+
return ref;
|
|
4861
|
+
}
|
|
4862
|
+
//#endregion
|
|
4670
4863
|
//#region src/docker/templates.ts
|
|
4671
4864
|
const LOCKFILES = [
|
|
4672
4865
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -5020,6 +5213,10 @@ node_modules
|
|
|
5020
5213
|
.gkm/server/*.ts
|
|
5021
5214
|
!.gkm/server/dist
|
|
5022
5215
|
|
|
5216
|
+
# The key gkm build --stage encrypted secrets with: runtime env only, never
|
|
5217
|
+
# the build context
|
|
5218
|
+
.gkm/server/master.key
|
|
5219
|
+
|
|
5023
5220
|
# IDE and editor
|
|
5024
5221
|
.idea
|
|
5025
5222
|
.vscode
|
|
@@ -5234,10 +5431,6 @@ FROM deps AS builder
|
|
|
5234
5431
|
|
|
5235
5432
|
WORKDIR /app
|
|
5236
5433
|
|
|
5237
|
-
# Build-time args for encrypted secrets
|
|
5238
|
-
ARG GKM_ENCRYPTED_CREDENTIALS=""
|
|
5239
|
-
ARG GKM_CREDENTIALS_IV=""
|
|
5240
|
-
|
|
5241
5434
|
# Copy pruned source
|
|
5242
5435
|
COPY --from=pruner /app/out/full/ ./
|
|
5243
5436
|
|
|
@@ -5246,11 +5439,15 @@ COPY --from=pruner /app/out/full/ ./
|
|
|
5246
5439
|
COPY --from=pruner /app/gkm.config.* ./
|
|
5247
5440
|
COPY --from=pruner /app/tsconfig.* ./
|
|
5248
5441
|
|
|
5249
|
-
# Write encrypted credentials for gkm build to embed
|
|
5250
|
-
|
|
5442
|
+
# Write encrypted credentials for gkm build to embed.
|
|
5443
|
+
# A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
|
|
5444
|
+
# ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
|
|
5445
|
+
# absent when the app has no secrets.
|
|
5446
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
5447
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
5251
5448
|
mkdir -p ${appPath}/.gkm && \
|
|
5252
|
-
|
|
5253
|
-
|
|
5449
|
+
sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
|
|
5450
|
+
sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
|
|
5254
5451
|
fi
|
|
5255
5452
|
|
|
5256
5453
|
# Build any workspace packages the app depends on.
|
|
@@ -5337,10 +5534,6 @@ FROM deps AS builder
|
|
|
5337
5534
|
|
|
5338
5535
|
WORKDIR /app
|
|
5339
5536
|
|
|
5340
|
-
# Build-time args for encrypted secrets
|
|
5341
|
-
ARG GKM_ENCRYPTED_CREDENTIALS=""
|
|
5342
|
-
ARG GKM_CREDENTIALS_IV=""
|
|
5343
|
-
|
|
5344
5537
|
# Copy pruned source
|
|
5345
5538
|
COPY --from=pruner /app/out/full/ ./
|
|
5346
5539
|
|
|
@@ -5348,20 +5541,17 @@ COPY --from=pruner /app/out/full/ ./
|
|
|
5348
5541
|
# Using wildcard to make it optional for single-app projects
|
|
5349
5542
|
COPY --from=pruner /app/tsconfig.* ./
|
|
5350
5543
|
|
|
5351
|
-
# Write encrypted credentials for tsdown to embed via define
|
|
5352
|
-
RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
|
|
5353
|
-
mkdir -p ${appPath}/.gkm && \
|
|
5354
|
-
echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
|
|
5355
|
-
echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
|
|
5356
|
-
fi
|
|
5357
|
-
|
|
5358
5544
|
# Bundle entry point with esbuild (outputs to dist/index.mjs)
|
|
5359
5545
|
# Creates a fully standalone bundle with all dependencies included
|
|
5360
|
-
# Use define to embed credentials if present
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5546
|
+
# Use define to embed credentials if present.
|
|
5547
|
+
# They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
|
|
5548
|
+
# not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
|
|
5549
|
+
# then IV; absent when the app has no secrets.
|
|
5550
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
5551
|
+
cd ${appPath} && \
|
|
5552
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
5553
|
+
CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
|
|
5554
|
+
IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
|
|
5365
5555
|
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
5366
5556
|
--outfile=dist/index.mjs --packages=bundle \
|
|
5367
5557
|
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
|
|
@@ -5512,6 +5702,33 @@ CMD ["nginx", "-g", "daemon off;"]
|
|
|
5512
5702
|
//#endregion
|
|
5513
5703
|
//#region src/docker/index.ts
|
|
5514
5704
|
const logger$6 = console;
|
|
5705
|
+
/** `docker build` failed; what docker printed above says why. */
|
|
5706
|
+
var DockerBuildFailed = class extends Error {
|
|
5707
|
+
imageRef;
|
|
5708
|
+
constructor(imageRef, cause) {
|
|
5709
|
+
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
5710
|
+
this.imageRef = imageRef;
|
|
5711
|
+
this.name = "DockerBuildFailed";
|
|
5712
|
+
}
|
|
5713
|
+
};
|
|
5714
|
+
/** `docker push` failed — usually the registry login or its permissions. */
|
|
5715
|
+
var DockerPushFailed = class extends Error {
|
|
5716
|
+
imageRef;
|
|
5717
|
+
constructor(imageRef, cause) {
|
|
5718
|
+
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
5719
|
+
this.imageRef = imageRef;
|
|
5720
|
+
this.name = "DockerPushFailed";
|
|
5721
|
+
}
|
|
5722
|
+
};
|
|
5723
|
+
/** `gkm docker --push` was asked for with nowhere to push to. */
|
|
5724
|
+
var PushNeedsRegistry = class extends Error {
|
|
5725
|
+
imageName;
|
|
5726
|
+
constructor(imageName) {
|
|
5727
|
+
super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
|
|
5728
|
+
this.imageName = imageName;
|
|
5729
|
+
this.name = "PushNeedsRegistry";
|
|
5730
|
+
}
|
|
5731
|
+
};
|
|
5515
5732
|
/**
|
|
5516
5733
|
* Docker command implementation
|
|
5517
5734
|
* Generates Dockerfile, docker-compose.yml, and related files
|
|
@@ -5615,14 +5832,18 @@ function ensureLockfile(cwd) {
|
|
|
5615
5832
|
async function buildDockerImage(imageName, options) {
|
|
5616
5833
|
const tag = options.tag ?? "latest";
|
|
5617
5834
|
const registry = options.registry;
|
|
5618
|
-
const fullImageName = registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}
|
|
5835
|
+
const fullImageName = validateImageRef(registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`);
|
|
5619
5836
|
logger$6.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
5620
5837
|
const cwd = process.cwd();
|
|
5621
5838
|
const cleanup = ensureLockfile(cwd);
|
|
5622
5839
|
try {
|
|
5623
|
-
|
|
5840
|
+
await run("docker", [
|
|
5841
|
+
"build",
|
|
5842
|
+
"--file=.gkm/docker/Dockerfile",
|
|
5843
|
+
`--tag=${fullImageName}`,
|
|
5844
|
+
"."
|
|
5845
|
+
], {
|
|
5624
5846
|
cwd,
|
|
5625
|
-
stdio: "inherit",
|
|
5626
5847
|
env: {
|
|
5627
5848
|
...process.env,
|
|
5628
5849
|
DOCKER_BUILDKIT: "1"
|
|
@@ -5630,7 +5851,7 @@ async function buildDockerImage(imageName, options) {
|
|
|
5630
5851
|
});
|
|
5631
5852
|
logger$6.log(`✅ Docker image built: ${fullImageName}`);
|
|
5632
5853
|
} catch (error) {
|
|
5633
|
-
throw new
|
|
5854
|
+
throw new DockerBuildFailed(fullImageName, error);
|
|
5634
5855
|
} finally {
|
|
5635
5856
|
cleanup?.();
|
|
5636
5857
|
}
|
|
@@ -5641,17 +5862,14 @@ async function buildDockerImage(imageName, options) {
|
|
|
5641
5862
|
async function pushDockerImage(imageName, options) {
|
|
5642
5863
|
const tag = options.tag ?? "latest";
|
|
5643
5864
|
const registry = options.registry;
|
|
5644
|
-
if (!registry) throw new
|
|
5645
|
-
const fullImageName = `${registry}/${imageName}:${tag}
|
|
5865
|
+
if (!registry) throw new PushNeedsRegistry(imageName);
|
|
5866
|
+
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
5646
5867
|
logger$6.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
5647
5868
|
try {
|
|
5648
|
-
|
|
5649
|
-
cwd: process.cwd(),
|
|
5650
|
-
stdio: "inherit"
|
|
5651
|
-
});
|
|
5869
|
+
await run("docker", ["push", fullImageName], { cwd: process.cwd() });
|
|
5652
5870
|
logger$6.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
5653
5871
|
} catch (error) {
|
|
5654
|
-
throw new
|
|
5872
|
+
throw new DockerPushFailed(fullImageName, error);
|
|
5655
5873
|
}
|
|
5656
5874
|
}
|
|
5657
5875
|
/**
|
|
@@ -5783,6 +6001,11 @@ function printRunInstructions(workspace) {
|
|
|
5783
6001
|
//#region src/deploy/docker.ts
|
|
5784
6002
|
const logger$5 = console;
|
|
5785
6003
|
/**
|
|
6004
|
+
* The id the Dockerfile templates mount the encrypted credentials under:
|
|
6005
|
+
* `RUN --mount=type=secret,id=gkm_credentials`.
|
|
6006
|
+
*/
|
|
6007
|
+
const CREDENTIALS_SECRET_ID = "gkm_credentials";
|
|
6008
|
+
/**
|
|
5786
6009
|
* Get the full image reference
|
|
5787
6010
|
*/
|
|
5788
6011
|
function getImageRef(registry, imageName, tag) {
|
|
@@ -5790,31 +6013,69 @@ function getImageRef(registry, imageName, tag) {
|
|
|
5790
6013
|
return `${imageName}:${tag}`;
|
|
5791
6014
|
}
|
|
5792
6015
|
/**
|
|
6016
|
+
* The `docker build` argv for an image.
|
|
6017
|
+
*
|
|
6018
|
+
* One element per value, so nothing in a ref or a build arg is read by a
|
|
6019
|
+
* shell, and `--opt=value` rather than `--opt value`, so a value that starts
|
|
6020
|
+
* with `-` cannot be taken for one of docker's own flags.
|
|
6021
|
+
*
|
|
6022
|
+
* @internal Exported for testing
|
|
6023
|
+
*/
|
|
6024
|
+
function dockerBuildArgs(options) {
|
|
6025
|
+
const { dockerfilePath, imageRef, buildArgs = [], credentialsFile } = options;
|
|
6026
|
+
return [
|
|
6027
|
+
"build",
|
|
6028
|
+
"--platform=linux/amd64",
|
|
6029
|
+
`--file=${dockerfilePath}`,
|
|
6030
|
+
`--tag=${imageRef}`,
|
|
6031
|
+
...buildArgs.map((arg) => `--build-arg=${arg}`),
|
|
6032
|
+
...credentialsFile ? [`--secret=id=${CREDENTIALS_SECRET_ID},src=${credentialsFile}`] : [],
|
|
6033
|
+
"."
|
|
6034
|
+
];
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* Write the encrypted credentials where `docker build --secret` reads them.
|
|
6038
|
+
*
|
|
6039
|
+
* Two lines — ciphertext, then IV — so a template can split them with `sed`
|
|
6040
|
+
* in any base image. Owner-only, in a directory of its own, and the returned
|
|
6041
|
+
* cleanup removes both whatever the build did.
|
|
6042
|
+
*
|
|
6043
|
+
* @internal Exported for testing
|
|
6044
|
+
*/
|
|
6045
|
+
async function writeCredentialsFile(credentials) {
|
|
6046
|
+
const dir = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "gkm-credentials-"));
|
|
6047
|
+
const path = (0, node_path.join)(dir, CREDENTIALS_SECRET_ID);
|
|
6048
|
+
await (0, node_fs_promises.writeFile)(path, `${credentials.encrypted}\n${credentials.iv}\n`, { mode: 384 });
|
|
6049
|
+
await (0, node_fs_promises.chmod)(path, 384);
|
|
6050
|
+
return {
|
|
6051
|
+
path,
|
|
6052
|
+
cleanup: () => (0, node_fs_promises.rm)(dir, {
|
|
6053
|
+
recursive: true,
|
|
6054
|
+
force: true
|
|
6055
|
+
})
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
/**
|
|
5793
6059
|
* Build Docker image
|
|
5794
6060
|
* @param imageRef - Full image reference (registry/name:tag)
|
|
5795
|
-
* @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
|
|
5796
6061
|
* @param buildArgs - Build arguments to pass to docker build
|
|
6062
|
+
* @param credentials - Encrypted credentials, passed as a build secret
|
|
5797
6063
|
*/
|
|
5798
|
-
async function buildImage(imageRef,
|
|
6064
|
+
async function buildImage(imageRef, buildArgs, credentials) {
|
|
5799
6065
|
logger$5.log(`\n🔨 Building Docker image: ${imageRef}`);
|
|
5800
6066
|
const cwd = process.cwd();
|
|
5801
6067
|
logger$5.log(" Generating Dockerfile for the pre-built bundle...");
|
|
5802
6068
|
await dockerCommand({ slim: true });
|
|
5803
6069
|
const dockerfilePath = (0, node_path.join)(cwd, ".gkm", "docker", "Dockerfile");
|
|
5804
|
-
const
|
|
5805
|
-
const buildArgsString = buildArgs && buildArgs.length > 0 ? buildArgs.map((arg) => `--build-arg "${arg}"`).join(" ") : "";
|
|
6070
|
+
const secret = credentials ? await writeCredentialsFile(credentials) : void 0;
|
|
5806
6071
|
try {
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
].filter(Boolean).join(" ");
|
|
5815
|
-
(0, node_child_process.execSync)(cmd, {
|
|
5816
|
-
cwd: buildCwd,
|
|
5817
|
-
stdio: "inherit",
|
|
6072
|
+
await run("docker", dockerBuildArgs({
|
|
6073
|
+
dockerfilePath,
|
|
6074
|
+
imageRef,
|
|
6075
|
+
buildArgs,
|
|
6076
|
+
credentialsFile: secret?.path
|
|
6077
|
+
}), {
|
|
6078
|
+
cwd,
|
|
5818
6079
|
env: {
|
|
5819
6080
|
...process.env,
|
|
5820
6081
|
DOCKER_BUILDKIT: "1"
|
|
@@ -5822,7 +6083,9 @@ async function buildImage(imageRef, _appName, buildArgs) {
|
|
|
5822
6083
|
});
|
|
5823
6084
|
logger$5.log(`✅ Image built: ${imageRef}`);
|
|
5824
6085
|
} catch (error) {
|
|
5825
|
-
throw new
|
|
6086
|
+
throw new DockerBuildFailed(imageRef, error);
|
|
6087
|
+
} finally {
|
|
6088
|
+
await secret?.cleanup();
|
|
5826
6089
|
}
|
|
5827
6090
|
}
|
|
5828
6091
|
/**
|
|
@@ -5831,23 +6094,20 @@ async function buildImage(imageRef, _appName, buildArgs) {
|
|
|
5831
6094
|
async function pushImage(imageRef) {
|
|
5832
6095
|
logger$5.log(`\n☁️ Pushing image: ${imageRef}`);
|
|
5833
6096
|
try {
|
|
5834
|
-
|
|
5835
|
-
cwd: process.cwd(),
|
|
5836
|
-
stdio: "inherit"
|
|
5837
|
-
});
|
|
6097
|
+
await run("docker", ["push", imageRef], { cwd: process.cwd() });
|
|
5838
6098
|
logger$5.log(`✅ Image pushed: ${imageRef}`);
|
|
5839
6099
|
} catch (error) {
|
|
5840
|
-
throw new
|
|
6100
|
+
throw new DockerPushFailed(imageRef, error);
|
|
5841
6101
|
}
|
|
5842
6102
|
}
|
|
5843
6103
|
/**
|
|
5844
6104
|
* Deploy using Docker (build and optionally push image)
|
|
5845
6105
|
*/
|
|
5846
6106
|
async function deployDocker(options) {
|
|
5847
|
-
const { stage, tag, skipPush, masterKey, config, buildArgs } = options;
|
|
6107
|
+
const { stage, tag, skipPush, masterKey, config, buildArgs, credentials } = options;
|
|
5848
6108
|
const imageName = config.imageName;
|
|
5849
|
-
const imageRef = getImageRef(config.registry, imageName, tag);
|
|
5850
|
-
await buildImage(imageRef,
|
|
6109
|
+
const imageRef = validateImageRef(getImageRef(config.registry, imageName, tag));
|
|
6110
|
+
await buildImage(imageRef, buildArgs, credentials);
|
|
5851
6111
|
if (!skipPush) {
|
|
5852
6112
|
if (!config.registry) logger$5.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
|
|
5853
6113
|
else await pushImage(imageRef);
|
|
@@ -5856,12 +6116,7 @@ async function deployDocker(options) {
|
|
|
5856
6116
|
logger$5.log(`\n📋 Deployment details:`);
|
|
5857
6117
|
logger$5.log(` Image: ${imageRef}`);
|
|
5858
6118
|
logger$5.log(` Stage: ${stage}`);
|
|
5859
|
-
if (masterKey) {
|
|
5860
|
-
logger$5.log(`\n🔐 Deploy with this environment variable:`);
|
|
5861
|
-
logger$5.log(` GKM_MASTER_KEY=${masterKey}`);
|
|
5862
|
-
logger$5.log("\n Example docker run:");
|
|
5863
|
-
logger$5.log(` docker run -e GKM_MASTER_KEY=${masterKey} ${imageRef}`);
|
|
5864
|
-
}
|
|
6119
|
+
if (masterKey) logger$5.log(`\n🔐 Run the container with GKM_MASTER_KEY set to the key with fingerprint ${keyFingerprint(masterKey)}.`);
|
|
5865
6120
|
return {
|
|
5866
6121
|
imageRef,
|
|
5867
6122
|
masterKey
|
|
@@ -5886,9 +6141,53 @@ function applicationName(stage, project, app) {
|
|
|
5886
6141
|
//#endregion
|
|
5887
6142
|
//#region src/deploy/dokploy-api.ts
|
|
5888
6143
|
var dokploy_api_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
6144
|
+
DEFAULT_DOKPLOY_TIMEOUT_MS: () => DEFAULT_DOKPLOY_TIMEOUT_MS,
|
|
5889
6145
|
DokployApi: () => DokployApi,
|
|
5890
|
-
DokployApiError: () => DokployApiError
|
|
6146
|
+
DokployApiError: () => DokployApiError,
|
|
6147
|
+
DokployRequestTimedOut: () => DokployRequestTimedOut
|
|
5891
6148
|
});
|
|
6149
|
+
/** Per request, so a stalled server fails the deploy, not the CI job. */
|
|
6150
|
+
const DEFAULT_DOKPLOY_TIMEOUT_MS = 3e4;
|
|
6151
|
+
/**
|
|
6152
|
+
* Dokploy accepted the connection but did not answer in time.
|
|
6153
|
+
*
|
|
6154
|
+
* Not retried: a request that arrived and stalled may still be acted on, and
|
|
6155
|
+
* sending it again could repeat a deploy or a create the server is already
|
|
6156
|
+
* doing.
|
|
6157
|
+
*/
|
|
6158
|
+
var DokployRequestTimedOut = class extends Error {
|
|
6159
|
+
baseUrl;
|
|
6160
|
+
endpoint;
|
|
6161
|
+
timeoutMs;
|
|
6162
|
+
constructor(baseUrl, endpoint, timeoutMs) {
|
|
6163
|
+
super(`Dokploy at ${baseUrl} did not answer ${endpoint} within ${timeoutMs}ms. Check that the Dokploy server is healthy, or raise \`timeoutMs\` if this call is known to be slow.`);
|
|
6164
|
+
this.baseUrl = baseUrl;
|
|
6165
|
+
this.endpoint = endpoint;
|
|
6166
|
+
this.timeoutMs = timeoutMs;
|
|
6167
|
+
this.name = "DokployRequestTimedOut";
|
|
6168
|
+
}
|
|
6169
|
+
};
|
|
6170
|
+
/**
|
|
6171
|
+
* Waits `ms` between retries, or rejects with the signal's reason the moment
|
|
6172
|
+
* it aborts — a caller who gave up should not sit through the backoff.
|
|
6173
|
+
*/
|
|
6174
|
+
function pause(ms, signal) {
|
|
6175
|
+
return new Promise((resolve, reject) => {
|
|
6176
|
+
if (signal?.aborted) {
|
|
6177
|
+
reject(signal.reason);
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
6180
|
+
const onAbort = () => {
|
|
6181
|
+
clearTimeout(timer);
|
|
6182
|
+
reject(signal?.reason);
|
|
6183
|
+
};
|
|
6184
|
+
const timer = setTimeout(() => {
|
|
6185
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6186
|
+
resolve();
|
|
6187
|
+
}, ms);
|
|
6188
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6189
|
+
});
|
|
6190
|
+
}
|
|
5892
6191
|
var DokployApiError = class extends Error {
|
|
5893
6192
|
status;
|
|
5894
6193
|
statusText;
|
|
@@ -5907,9 +6206,13 @@ var DokployApiError = class extends Error {
|
|
|
5907
6206
|
var DokployApi = class {
|
|
5908
6207
|
baseUrl;
|
|
5909
6208
|
token;
|
|
6209
|
+
timeoutMs;
|
|
6210
|
+
signal;
|
|
5910
6211
|
constructor(options) {
|
|
5911
6212
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
5912
6213
|
this.token = options.token;
|
|
6214
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
6215
|
+
this.signal = options.signal;
|
|
5913
6216
|
}
|
|
5914
6217
|
/**
|
|
5915
6218
|
* Make a GET request to the Dokploy API
|
|
@@ -5929,26 +6232,51 @@ var DokployApi = class {
|
|
|
5929
6232
|
async request(method, endpoint, body) {
|
|
5930
6233
|
const url = `${this.baseUrl}/api/${endpoint}`;
|
|
5931
6234
|
let response;
|
|
6235
|
+
let timeout;
|
|
5932
6236
|
let lastError;
|
|
5933
|
-
for (let attempt = 1; attempt <= 3; attempt++)
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
6237
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
6238
|
+
this.signal?.throwIfAborted();
|
|
6239
|
+
timeout = AbortSignal.timeout(this.timeoutMs);
|
|
6240
|
+
try {
|
|
6241
|
+
response = await fetch(url, {
|
|
6242
|
+
method,
|
|
6243
|
+
headers: {
|
|
6244
|
+
"Content-Type": "application/json",
|
|
6245
|
+
"x-api-key": this.token
|
|
6246
|
+
},
|
|
6247
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
6248
|
+
signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
|
|
6249
|
+
});
|
|
6250
|
+
break;
|
|
6251
|
+
} catch (error) {
|
|
6252
|
+
const stopped = this.stopped(timeout, endpoint);
|
|
6253
|
+
if (stopped) throw stopped;
|
|
6254
|
+
lastError = error;
|
|
6255
|
+
if (attempt === 3) break;
|
|
6256
|
+
await pause(attempt * 750, this.signal);
|
|
6257
|
+
}
|
|
5947
6258
|
}
|
|
5948
|
-
if (!response) {
|
|
6259
|
+
if (!response || !timeout) {
|
|
5949
6260
|
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
5950
6261
|
throw new DokployApiError(`Could not reach Dokploy at ${this.baseUrl} (${endpoint}): ${detail}`, 0, "Network error");
|
|
5951
6262
|
}
|
|
6263
|
+
try {
|
|
6264
|
+
return await this.read(response);
|
|
6265
|
+
} catch (error) {
|
|
6266
|
+
throw this.stopped(timeout, endpoint) ?? error;
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
/**
|
|
6270
|
+
* What a request cut short should reject with: the caller's own reason
|
|
6271
|
+
* when they aborted, `DokployRequestTimedOut` when the deadline passed,
|
|
6272
|
+
* nothing when it was neither.
|
|
6273
|
+
*/
|
|
6274
|
+
stopped(timeout, endpoint) {
|
|
6275
|
+
if (this.signal?.aborted) return this.signal.reason;
|
|
6276
|
+
if (timeout.aborted) return new DokployRequestTimedOut(this.baseUrl, endpoint, this.timeoutMs);
|
|
6277
|
+
}
|
|
6278
|
+
/** Turns a response into its JSON body, or a `DokployApiError`. */
|
|
6279
|
+
async read(response) {
|
|
5952
6280
|
if (!response.ok) {
|
|
5953
6281
|
let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
|
|
5954
6282
|
let issues;
|
|
@@ -6737,7 +7065,11 @@ function isStateProvider(value) {
|
|
|
6737
7065
|
*
|
|
6738
7066
|
* - 'local': LocalStateProvider (default)
|
|
6739
7067
|
* - 'ssm': CachedStateProvider with SSM as source of truth
|
|
7068
|
+
* - 's3': S3StateStore behind the provider interface
|
|
6740
7069
|
* - Custom: Use provided StateProvider implementation
|
|
7070
|
+
*
|
|
7071
|
+
* Deploy moves to `createStateStore` (locks, versions, per-resource records);
|
|
7072
|
+
* this stays for the call sites that read and write whole states.
|
|
6741
7073
|
*/
|
|
6742
7074
|
async function createStateProvider(options) {
|
|
6743
7075
|
const { config, workspaceRoot, workspaceName } = options;
|
|
@@ -6745,6 +7077,11 @@ async function createStateProvider(options) {
|
|
|
6745
7077
|
const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-7HR6SS2t.cjs"));
|
|
6746
7078
|
return new LocalStateProvider(workspaceRoot);
|
|
6747
7079
|
}
|
|
7080
|
+
const { isStateStore } = await Promise.resolve().then(() => StateStore_exports);
|
|
7081
|
+
if (isStateStore(config.provider)) {
|
|
7082
|
+
const { StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7083
|
+
return new StateStoreProvider(config.provider);
|
|
7084
|
+
}
|
|
6748
7085
|
if (isStateProvider(config.provider)) return config.provider;
|
|
6749
7086
|
const provider = config.provider;
|
|
6750
7087
|
if (provider === "local") {
|
|
@@ -6752,7 +7089,10 @@ async function createStateProvider(options) {
|
|
|
6752
7089
|
return new LocalStateProvider(workspaceRoot);
|
|
6753
7090
|
}
|
|
6754
7091
|
if (provider === "ssm") {
|
|
6755
|
-
if (!workspaceName)
|
|
7092
|
+
if (!workspaceName) {
|
|
7093
|
+
const { StateStoreNeedsWorkspaceName } = await Promise.resolve().then(() => StateStore_exports);
|
|
7094
|
+
throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7095
|
+
}
|
|
6756
7096
|
const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-7HR6SS2t.cjs"));
|
|
6757
7097
|
const { SSMStateProvider } = await Promise.resolve().then(() => require("./SSMStateProvider-VrQ-yeqd.cjs"));
|
|
6758
7098
|
const { CachedStateProvider } = await Promise.resolve().then(() => CachedStateProvider_exports);
|
|
@@ -6764,47 +7104,12 @@ async function createStateProvider(options) {
|
|
|
6764
7104
|
profile: ssmConfig.profile
|
|
6765
7105
|
}), local);
|
|
6766
7106
|
}
|
|
6767
|
-
|
|
6768
|
-
}
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
generateDefineOptions: () => generateDefineOptions
|
|
6774
|
-
});
|
|
6775
|
-
/** AES-256-GCM configuration */
|
|
6776
|
-
const ALGORITHM = "aes-256-gcm";
|
|
6777
|
-
const KEY_LENGTH = 32;
|
|
6778
|
-
const IV_LENGTH = 12;
|
|
6779
|
-
/**
|
|
6780
|
-
* Encrypt secrets for embedding in a bundle.
|
|
6781
|
-
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
6782
|
-
*
|
|
6783
|
-
* @param secrets - Key-value pairs to encrypt
|
|
6784
|
-
* @returns Encrypted payload with ephemeral master key
|
|
6785
|
-
*/
|
|
6786
|
-
function encryptSecrets(secrets) {
|
|
6787
|
-
const masterKey = (0, node_crypto.randomBytes)(KEY_LENGTH);
|
|
6788
|
-
const iv = (0, node_crypto.randomBytes)(IV_LENGTH);
|
|
6789
|
-
const plaintext = JSON.stringify(secrets);
|
|
6790
|
-
const cipher = (0, node_crypto.createCipheriv)(ALGORITHM, masterKey, iv);
|
|
6791
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
6792
|
-
const authTag = cipher.getAuthTag();
|
|
6793
|
-
return {
|
|
6794
|
-
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
6795
|
-
iv: iv.toString("hex"),
|
|
6796
|
-
masterKey: masterKey.toString("hex")
|
|
6797
|
-
};
|
|
6798
|
-
}
|
|
6799
|
-
/**
|
|
6800
|
-
* Generate the define options for tsdown/esbuild.
|
|
6801
|
-
* These will be injected at build time.
|
|
6802
|
-
*/
|
|
6803
|
-
function generateDefineOptions(payload) {
|
|
6804
|
-
return {
|
|
6805
|
-
__GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
|
|
6806
|
-
__GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
|
|
6807
|
-
};
|
|
7107
|
+
if (provider === "s3") {
|
|
7108
|
+
const { createStateStore, StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7109
|
+
return new StateStoreProvider(await createStateStore(options));
|
|
7110
|
+
}
|
|
7111
|
+
const { UnknownStateProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7112
|
+
throw new UnknownStateProvider(config);
|
|
6808
7113
|
}
|
|
6809
7114
|
//#endregion
|
|
6810
7115
|
//#region src/deploy/secrets.ts
|
|
@@ -7762,12 +8067,11 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7762
8067
|
}
|
|
7763
8068
|
setApplicationId(state, appName, application.applicationId);
|
|
7764
8069
|
const appSecrets = encryptedSecrets.get(appName);
|
|
7765
|
-
const
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
}
|
|
8070
|
+
const credentials = appSecrets && appSecrets.secretCount > 0 ? {
|
|
8071
|
+
encrypted: appSecrets.payload.encrypted,
|
|
8072
|
+
iv: appSecrets.payload.iv
|
|
8073
|
+
} : void 0;
|
|
8074
|
+
if (appSecrets && credentials) logger$4.log(` Encrypted ${appSecrets.secretCount} secrets`);
|
|
7771
8075
|
const imageName = `${workspace.name}-${appName}`;
|
|
7772
8076
|
const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
|
|
7773
8077
|
logger$4.log(` Building Docker image: ${imageRef}`);
|
|
@@ -7780,7 +8084,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7780
8084
|
imageName,
|
|
7781
8085
|
appName
|
|
7782
8086
|
},
|
|
7783
|
-
|
|
8087
|
+
credentials
|
|
7784
8088
|
});
|
|
7785
8089
|
const backendHost = resolveHost(appName, app, stage, workspace.deploy?.domains, false);
|
|
7786
8090
|
const dependencyUrls = {};
|
|
@@ -8162,6 +8466,11 @@ async function deployListCommand(options) {
|
|
|
8162
8466
|
var CachedStateProvider_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ CachedStateProvider: () => CachedStateProvider });
|
|
8163
8467
|
/**
|
|
8164
8468
|
* Cached state provider that wraps a remote provider with local cache.
|
|
8469
|
+
*
|
|
8470
|
+
* @deprecated for deploys: a read prefers the local copy, so a stale one wins
|
|
8471
|
+
* over the remote without warning, and a write overwrites the remote blindly.
|
|
8472
|
+
* Deploy uses `createStateStore`, which reads SSM directly and writes
|
|
8473
|
+
* conditionally. Kept for `gkm state:pull | push | diff`.
|
|
8165
8474
|
*/
|
|
8166
8475
|
var CachedStateProvider = class {
|
|
8167
8476
|
remote;
|
|
@@ -8212,12 +8521,421 @@ var CachedStateProvider = class {
|
|
|
8212
8521
|
}
|
|
8213
8522
|
};
|
|
8214
8523
|
//#endregion
|
|
8524
|
+
//#region src/deploy/StateStore.ts
|
|
8525
|
+
/**
|
|
8526
|
+
* Deploy state store.
|
|
8527
|
+
*
|
|
8528
|
+
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
8529
|
+
* two runs of the same stage from overwriting each other and nowhere to record
|
|
8530
|
+
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
8531
|
+
* deploy needs to be safe against crashes and concurrent runs:
|
|
8532
|
+
*
|
|
8533
|
+
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
8534
|
+
* holder, instead of racing the first.
|
|
8535
|
+
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
8536
|
+
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
8537
|
+
* underneath it, so nothing is lost silently even without the lock.
|
|
8538
|
+
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
8539
|
+
* run that dies half way leaves behind the ids of everything it created.
|
|
8540
|
+
*
|
|
8541
|
+
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
8542
|
+
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
8543
|
+
* working but cannot lock.
|
|
8544
|
+
*/
|
|
8545
|
+
var StateStore_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
8546
|
+
DocumentStateStore: () => DocumentStateStore,
|
|
8547
|
+
STATE_SCHEMA_VERSION: () => 2,
|
|
8548
|
+
StageStateMissing: () => StageStateMissing,
|
|
8549
|
+
StateLocked: () => StateLocked,
|
|
8550
|
+
StateSchemaTooNew: () => StateSchemaTooNew,
|
|
8551
|
+
StateStoreBusy: () => StateStoreBusy,
|
|
8552
|
+
StateStoreNeedsWorkspaceName: () => StateStoreNeedsWorkspaceName,
|
|
8553
|
+
StateStoreProvider: () => StateStoreProvider,
|
|
8554
|
+
StateStoreWithoutLocking: () => StateStoreWithoutLocking,
|
|
8555
|
+
StateUnreadable: () => StateUnreadable,
|
|
8556
|
+
StateVersionConflict: () => StateVersionConflict,
|
|
8557
|
+
UnknownStateProvider: () => UnknownStateProvider,
|
|
8558
|
+
contentVersion: () => contentVersion,
|
|
8559
|
+
createStateStore: () => createStateStore,
|
|
8560
|
+
decodeDocument: () => decodeDocument,
|
|
8561
|
+
encodeDocument: () => encodeDocument,
|
|
8562
|
+
isStateStore: () => isStateStore,
|
|
8563
|
+
migrateV1: () => migrateV1,
|
|
8564
|
+
newLockHolder: () => newLockHolder
|
|
8565
|
+
});
|
|
8566
|
+
var StateLocked = class extends Error {
|
|
8567
|
+
stage;
|
|
8568
|
+
holder;
|
|
8569
|
+
location;
|
|
8570
|
+
constructor(stage, holder, location) {
|
|
8571
|
+
const by = holder ? `${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}${holder.operation ? ` for '${holder.operation}'` : ""}` : "a run that did not record who it was";
|
|
8572
|
+
super(`Deploy state for stage '${stage}' is locked by ${by} (${location}). Wait for that run to finish; if it crashed, release the lock with \`gkm state:unlock --stage ${stage}\`.`);
|
|
8573
|
+
this.stage = stage;
|
|
8574
|
+
this.holder = holder;
|
|
8575
|
+
this.location = location;
|
|
8576
|
+
this.name = "StateLocked";
|
|
8577
|
+
}
|
|
8578
|
+
};
|
|
8579
|
+
var StateVersionConflict = class extends Error {
|
|
8580
|
+
stage;
|
|
8581
|
+
expectedVersion;
|
|
8582
|
+
actualVersion;
|
|
8583
|
+
constructor(stage, expectedVersion, actualVersion) {
|
|
8584
|
+
super(`Deploy state for stage '${stage}' changed since it was read (expected ${expectedVersion ? `version ${expectedVersion}` : "no state"}, found ${actualVersion ? `version ${actualVersion}` : "no state"}). Another run wrote it: read the state again and re-apply the change, and hold the stage's lock so runs take turns.`);
|
|
8585
|
+
this.stage = stage;
|
|
8586
|
+
this.expectedVersion = expectedVersion;
|
|
8587
|
+
this.actualVersion = actualVersion;
|
|
8588
|
+
this.name = "StateVersionConflict";
|
|
8589
|
+
}
|
|
8590
|
+
};
|
|
8591
|
+
var StageStateMissing = class extends Error {
|
|
8592
|
+
stage;
|
|
8593
|
+
constructor(stage) {
|
|
8594
|
+
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
8595
|
+
this.stage = stage;
|
|
8596
|
+
this.name = "StageStateMissing";
|
|
8597
|
+
}
|
|
8598
|
+
};
|
|
8599
|
+
var StateUnreadable = class extends Error {
|
|
8600
|
+
stage;
|
|
8601
|
+
location;
|
|
8602
|
+
constructor(stage, location, cause) {
|
|
8603
|
+
super(`Deploy state for stage '${stage}' at ${location} is not valid JSON. Restore it from a backup (or the provider's history) rather than deploying without it — a deploy without state recreates every resource.`, { cause });
|
|
8604
|
+
this.stage = stage;
|
|
8605
|
+
this.location = location;
|
|
8606
|
+
this.name = "StateUnreadable";
|
|
8607
|
+
}
|
|
8608
|
+
};
|
|
8609
|
+
var StateSchemaTooNew = class extends Error {
|
|
8610
|
+
stage;
|
|
8611
|
+
schemaVersion;
|
|
8612
|
+
constructor(stage, schemaVersion) {
|
|
8613
|
+
super(`Deploy state for stage '${stage}' has schema version ${String(schemaVersion)}, newer than this @geekmidas/cli understands (2). Upgrade @geekmidas/cli to the version that wrote it.`);
|
|
8614
|
+
this.stage = stage;
|
|
8615
|
+
this.schemaVersion = schemaVersion;
|
|
8616
|
+
this.name = "StateSchemaTooNew";
|
|
8617
|
+
}
|
|
8618
|
+
};
|
|
8619
|
+
/**
|
|
8620
|
+
* Not thrown: emitted (once per store) when a custom `StateProvider` stands
|
|
8621
|
+
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
8622
|
+
* says so.
|
|
8623
|
+
*/
|
|
8624
|
+
var StateStoreWithoutLocking = class extends Error {
|
|
8625
|
+
provider;
|
|
8626
|
+
constructor(provider) {
|
|
8627
|
+
super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
|
|
8628
|
+
this.provider = provider;
|
|
8629
|
+
this.name = "StateStoreWithoutLocking";
|
|
8630
|
+
}
|
|
8631
|
+
};
|
|
8632
|
+
var StateStoreNeedsWorkspaceName = class extends Error {
|
|
8633
|
+
provider;
|
|
8634
|
+
constructor(provider) {
|
|
8635
|
+
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
8636
|
+
this.provider = provider;
|
|
8637
|
+
this.name = "StateStoreNeedsWorkspaceName";
|
|
8638
|
+
}
|
|
8639
|
+
};
|
|
8640
|
+
var UnknownStateProvider = class extends Error {
|
|
8641
|
+
config;
|
|
8642
|
+
constructor(config) {
|
|
8643
|
+
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
8644
|
+
this.config = config;
|
|
8645
|
+
this.name = "UnknownStateProvider";
|
|
8646
|
+
}
|
|
8647
|
+
};
|
|
8648
|
+
/** A lock or write that could not get at the state backend in time. */
|
|
8649
|
+
var StateStoreBusy = class extends Error {
|
|
8650
|
+
stage;
|
|
8651
|
+
location;
|
|
8652
|
+
constructor(stage, location) {
|
|
8653
|
+
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
8654
|
+
this.stage = stage;
|
|
8655
|
+
this.location = location;
|
|
8656
|
+
this.name = "StateStoreBusy";
|
|
8657
|
+
}
|
|
8658
|
+
};
|
|
8659
|
+
function decodeDocument(stage, body, location) {
|
|
8660
|
+
let parsed;
|
|
8661
|
+
try {
|
|
8662
|
+
parsed = JSON.parse(body);
|
|
8663
|
+
} catch (error) {
|
|
8664
|
+
throw new StateUnreadable(stage, location, error);
|
|
8665
|
+
}
|
|
8666
|
+
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
8667
|
+
const schemaVersion = parsed.schemaVersion;
|
|
8668
|
+
if (schemaVersion === void 0) return {
|
|
8669
|
+
schemaVersion: 1,
|
|
8670
|
+
state: parsed
|
|
8671
|
+
};
|
|
8672
|
+
if (schemaVersion === 2) return {
|
|
8673
|
+
schemaVersion: 2,
|
|
8674
|
+
document: parsed
|
|
8675
|
+
};
|
|
8676
|
+
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
8677
|
+
}
|
|
8678
|
+
function encodeDocument(document) {
|
|
8679
|
+
return JSON.stringify(document, null, 2);
|
|
8680
|
+
}
|
|
8681
|
+
/**
|
|
8682
|
+
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
8683
|
+
* so the first journaled deploy after an upgrade adopts them rather than
|
|
8684
|
+
* treating them as unknown.
|
|
8685
|
+
*/
|
|
8686
|
+
function migrateV1(stage, state) {
|
|
8687
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8688
|
+
const resources = {};
|
|
8689
|
+
const seed = (type, name, id) => {
|
|
8690
|
+
if (!id) return;
|
|
8691
|
+
const key = name ? `${type}:${name}` : type;
|
|
8692
|
+
resources[key] = {
|
|
8693
|
+
key,
|
|
8694
|
+
type,
|
|
8695
|
+
id,
|
|
8696
|
+
status: "ready",
|
|
8697
|
+
updatedAt: now
|
|
8698
|
+
};
|
|
8699
|
+
};
|
|
8700
|
+
seed("project", null, state.projectId);
|
|
8701
|
+
seed("environment", null, state.environmentId);
|
|
8702
|
+
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
8703
|
+
seed("postgres", null, state.services?.postgresId);
|
|
8704
|
+
seed("redis", null, state.services?.redisId);
|
|
8705
|
+
seed("backup-destination", null, state.backups?.destinationId);
|
|
8706
|
+
return {
|
|
8707
|
+
schemaVersion: 2,
|
|
8708
|
+
stage,
|
|
8709
|
+
serial: 1,
|
|
8710
|
+
state,
|
|
8711
|
+
resources,
|
|
8712
|
+
updatedAt: now
|
|
8713
|
+
};
|
|
8714
|
+
}
|
|
8715
|
+
function newLockHolder(options) {
|
|
8716
|
+
let owner = "unknown";
|
|
8717
|
+
try {
|
|
8718
|
+
owner = (0, node_os.userInfo)().username;
|
|
8719
|
+
} catch {}
|
|
8720
|
+
return {
|
|
8721
|
+
id: (0, node_crypto.randomUUID)(),
|
|
8722
|
+
owner,
|
|
8723
|
+
host: (0, node_os.hostname)(),
|
|
8724
|
+
pid: process.pid,
|
|
8725
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8726
|
+
operation: options?.operation
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
function contentVersion(body) {
|
|
8730
|
+
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
8731
|
+
}
|
|
8732
|
+
/**
|
|
8733
|
+
* A store over a backend that can read and conditionally write one blob per
|
|
8734
|
+
* stage, and create a lock object only if it does not exist. Migration and
|
|
8735
|
+
* resource records are the same for every such backend, so they live here.
|
|
8736
|
+
*/
|
|
8737
|
+
var DocumentStateStore = class {
|
|
8738
|
+
async lock(stage, options) {
|
|
8739
|
+
const holder = newLockHolder(options);
|
|
8740
|
+
await this.createLock(stage, holder);
|
|
8741
|
+
let released = false;
|
|
8742
|
+
return {
|
|
8743
|
+
stage,
|
|
8744
|
+
holder,
|
|
8745
|
+
release: async () => {
|
|
8746
|
+
if (released) return;
|
|
8747
|
+
released = true;
|
|
8748
|
+
await this.removeLock(stage, holder);
|
|
8749
|
+
}
|
|
8750
|
+
};
|
|
8751
|
+
}
|
|
8752
|
+
async forceUnlock(stage) {
|
|
8753
|
+
const holder = await this.readLock(stage);
|
|
8754
|
+
await this.removeLock(stage, null);
|
|
8755
|
+
return holder;
|
|
8756
|
+
}
|
|
8757
|
+
async read(stage) {
|
|
8758
|
+
const current = await this.readDocument(stage);
|
|
8759
|
+
if (!current) return null;
|
|
8760
|
+
return {
|
|
8761
|
+
state: current.document.state,
|
|
8762
|
+
resources: current.document.resources,
|
|
8763
|
+
version: current.version
|
|
8764
|
+
};
|
|
8765
|
+
}
|
|
8766
|
+
async write(stage, state, options) {
|
|
8767
|
+
if (options.expectedVersion === null) {
|
|
8768
|
+
const document = {
|
|
8769
|
+
schemaVersion: 2,
|
|
8770
|
+
stage,
|
|
8771
|
+
serial: 1,
|
|
8772
|
+
state,
|
|
8773
|
+
resources: {},
|
|
8774
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8775
|
+
};
|
|
8776
|
+
return this.writeRaw(stage, encodeDocument(document), null);
|
|
8777
|
+
}
|
|
8778
|
+
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
8779
|
+
...document,
|
|
8780
|
+
state
|
|
8781
|
+
}));
|
|
8782
|
+
}
|
|
8783
|
+
async putResource(stage, record, options) {
|
|
8784
|
+
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
8785
|
+
...document,
|
|
8786
|
+
resources: {
|
|
8787
|
+
...document.resources,
|
|
8788
|
+
[record.key]: {
|
|
8789
|
+
...record,
|
|
8790
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8793
|
+
}));
|
|
8794
|
+
}
|
|
8795
|
+
async deleteResource(stage, key, options) {
|
|
8796
|
+
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
8797
|
+
const { [key]: _removed, ...resources } = document.resources;
|
|
8798
|
+
return {
|
|
8799
|
+
...document,
|
|
8800
|
+
resources
|
|
8801
|
+
};
|
|
8802
|
+
});
|
|
8803
|
+
}
|
|
8804
|
+
async mutate(stage, expectedVersion, change) {
|
|
8805
|
+
const current = await this.readDocument(stage);
|
|
8806
|
+
if (!current) {
|
|
8807
|
+
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
8808
|
+
throw new StageStateMissing(stage);
|
|
8809
|
+
}
|
|
8810
|
+
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
8811
|
+
const next = change(current.document);
|
|
8812
|
+
next.serial = current.document.serial + 1;
|
|
8813
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8814
|
+
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
8815
|
+
}
|
|
8816
|
+
/**
|
|
8817
|
+
* Reads the stage's document, migrating v1 in place on the way. The
|
|
8818
|
+
* migration is itself a conditional write: if another run migrated (or
|
|
8819
|
+
* wrote) first, its result is read instead.
|
|
8820
|
+
*/
|
|
8821
|
+
async readDocument(stage, attempt = 0) {
|
|
8822
|
+
const raw = await this.readRaw(stage);
|
|
8823
|
+
if (!raw) return null;
|
|
8824
|
+
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
8825
|
+
if (decoded.schemaVersion === 2) return {
|
|
8826
|
+
document: decoded.document,
|
|
8827
|
+
version: raw.version
|
|
8828
|
+
};
|
|
8829
|
+
await this.writeV1Backup(stage, raw.body);
|
|
8830
|
+
const document = migrateV1(stage, decoded.state);
|
|
8831
|
+
try {
|
|
8832
|
+
return {
|
|
8833
|
+
document,
|
|
8834
|
+
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
8835
|
+
};
|
|
8836
|
+
} catch (error) {
|
|
8837
|
+
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
8838
|
+
throw error;
|
|
8839
|
+
}
|
|
8840
|
+
}
|
|
8841
|
+
};
|
|
8842
|
+
function isStateStore(value) {
|
|
8843
|
+
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
8844
|
+
}
|
|
8845
|
+
/**
|
|
8846
|
+
* The store for a workspace's `state` config — the same config
|
|
8847
|
+
* `createStateProvider` reads:
|
|
8848
|
+
*
|
|
8849
|
+
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
8850
|
+
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
8851
|
+
* a stale local copy can never win over it
|
|
8852
|
+
* - `'s3'`: `S3StateStore`
|
|
8853
|
+
* - a `StateStore` object: used as is
|
|
8854
|
+
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
8855
|
+
*/
|
|
8856
|
+
async function createStateStore(options) {
|
|
8857
|
+
const { config, workspaceRoot, workspaceName } = options;
|
|
8858
|
+
if (!config || config.provider === "local") {
|
|
8859
|
+
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-DDDU0HkI.cjs"));
|
|
8860
|
+
return new LocalStateStore(workspaceRoot);
|
|
8861
|
+
}
|
|
8862
|
+
if (isStateStore(config.provider)) return config.provider;
|
|
8863
|
+
if (isStateProvider(config.provider)) {
|
|
8864
|
+
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-C_OFs1gN.cjs"));
|
|
8865
|
+
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
8866
|
+
}
|
|
8867
|
+
if (config.provider === "ssm") {
|
|
8868
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
8869
|
+
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-DD7jKrCO.cjs"));
|
|
8870
|
+
const ssm = config;
|
|
8871
|
+
return SSMStateStore.create({
|
|
8872
|
+
workspaceName,
|
|
8873
|
+
region: ssm.region,
|
|
8874
|
+
profile: ssm.profile
|
|
8875
|
+
});
|
|
8876
|
+
}
|
|
8877
|
+
if (config.provider === "s3") {
|
|
8878
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
8879
|
+
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-C7eBae0G.cjs"));
|
|
8880
|
+
const s3 = config;
|
|
8881
|
+
return S3StateStore.create({
|
|
8882
|
+
workspaceName,
|
|
8883
|
+
bucket: s3.bucket,
|
|
8884
|
+
prefix: s3.prefix,
|
|
8885
|
+
region: s3.region,
|
|
8886
|
+
profile: s3.profile
|
|
8887
|
+
});
|
|
8888
|
+
}
|
|
8889
|
+
throw new UnknownStateProvider(config);
|
|
8890
|
+
}
|
|
8891
|
+
/**
|
|
8892
|
+
* A store behind the `StateProvider` interface, for the call sites that still
|
|
8893
|
+
* read and write whole states (deploy, until it journals). A write is still
|
|
8894
|
+
* conditional on the version this provider last read.
|
|
8895
|
+
*/
|
|
8896
|
+
var StateStoreProvider = class {
|
|
8897
|
+
store;
|
|
8898
|
+
versions = /* @__PURE__ */ new Map();
|
|
8899
|
+
constructor(store) {
|
|
8900
|
+
this.store = store;
|
|
8901
|
+
}
|
|
8902
|
+
async read(stage) {
|
|
8903
|
+
const stored = await this.store.read(stage);
|
|
8904
|
+
this.versions.set(stage, stored?.version ?? null);
|
|
8905
|
+
return stored?.state ?? null;
|
|
8906
|
+
}
|
|
8907
|
+
async write(stage, state) {
|
|
8908
|
+
const expectedVersion = this.versions.has(stage) ? this.versions.get(stage) ?? null : (await this.store.read(stage))?.version ?? null;
|
|
8909
|
+
state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8910
|
+
const version = await this.store.write(stage, state, { expectedVersion });
|
|
8911
|
+
this.versions.set(stage, version);
|
|
8912
|
+
}
|
|
8913
|
+
};
|
|
8914
|
+
//#endregion
|
|
8215
8915
|
//#region src/deploy/state-commands.ts
|
|
8216
8916
|
/**
|
|
8217
8917
|
* State Management CLI Commands
|
|
8218
8918
|
*
|
|
8219
8919
|
* Commands for managing deployment state across local and remote providers.
|
|
8220
8920
|
*/
|
|
8921
|
+
/** What `state:show` prints in place of a secret. */
|
|
8922
|
+
const MASKED = "********";
|
|
8923
|
+
/**
|
|
8924
|
+
* A copy of `state` with every secret replaced by `MASKED`: database
|
|
8925
|
+
* passwords, generated secrets and the backup IAM keys. `state:show` is run
|
|
8926
|
+
* to look up ids, in terminals and CI logs; nothing it prints should be
|
|
8927
|
+
* usable as a credential.
|
|
8928
|
+
*/
|
|
8929
|
+
function maskStateSecrets(state) {
|
|
8930
|
+
const masked = structuredClone(state);
|
|
8931
|
+
for (const credentials of Object.values(masked.appCredentials ?? {})) credentials.dbPassword = MASKED;
|
|
8932
|
+
for (const secrets of Object.values(masked.generatedSecrets ?? {})) for (const name of Object.keys(secrets)) secrets[name] = MASKED;
|
|
8933
|
+
if (masked.backups) {
|
|
8934
|
+
masked.backups.iamAccessKeyId = MASKED;
|
|
8935
|
+
masked.backups.iamSecretAccessKey = MASKED;
|
|
8936
|
+
}
|
|
8937
|
+
return masked;
|
|
8938
|
+
}
|
|
8221
8939
|
/**
|
|
8222
8940
|
* Pull state from remote to local.
|
|
8223
8941
|
* `gkm state:pull --stage=<stage>`
|
|
@@ -8280,19 +8998,41 @@ async function statePushCommand(options) {
|
|
|
8280
8998
|
*/
|
|
8281
8999
|
async function stateShowCommand(options) {
|
|
8282
9000
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
8283
|
-
const
|
|
9001
|
+
const stored = await (await createStateProvider({
|
|
8284
9002
|
config: workspace.state,
|
|
8285
9003
|
workspaceRoot: workspace.root,
|
|
8286
9004
|
workspaceName: workspace.name
|
|
8287
9005
|
})).read(options.stage);
|
|
8288
|
-
if (!
|
|
9006
|
+
if (!stored) {
|
|
8289
9007
|
console.log(`No state found for stage: ${options.stage}`);
|
|
8290
9008
|
return;
|
|
8291
9009
|
}
|
|
9010
|
+
const state = maskStateSecrets(stored);
|
|
8292
9011
|
if (options.json) console.log(JSON.stringify(state, null, 2));
|
|
8293
9012
|
else printStateDetails(state);
|
|
8294
9013
|
}
|
|
8295
9014
|
/**
|
|
9015
|
+
* Release a stage's deploy lock, whoever holds it.
|
|
9016
|
+
* `gkm state:unlock --stage=<stage>`
|
|
9017
|
+
*
|
|
9018
|
+
* For a run that crashed with the lock held. Releasing a lock a live run
|
|
9019
|
+
* holds lets a second run race it, so the holder is printed for the caller
|
|
9020
|
+
* to check.
|
|
9021
|
+
*/
|
|
9022
|
+
async function stateUnlockCommand(options) {
|
|
9023
|
+
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
9024
|
+
const holder = await (await createStateStore({
|
|
9025
|
+
config: workspace.state,
|
|
9026
|
+
workspaceRoot: workspace.root,
|
|
9027
|
+
workspaceName: workspace.name
|
|
9028
|
+
})).forceUnlock(options.stage);
|
|
9029
|
+
if (!holder) {
|
|
9030
|
+
console.log(`Stage ${options.stage} was not locked.`);
|
|
9031
|
+
return;
|
|
9032
|
+
}
|
|
9033
|
+
console.log(`Released the lock on stage ${options.stage}, held by ${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}.`);
|
|
9034
|
+
}
|
|
9035
|
+
/**
|
|
8296
9036
|
* Compare local and remote state.
|
|
8297
9037
|
* `gkm state:diff --stage=<stage>`
|
|
8298
9038
|
*/
|
|
@@ -8376,6 +9116,26 @@ function printStateDetails(state) {
|
|
|
8376
9116
|
if (state.services.postgresId) console.log(` Postgres: ${state.services.postgresId}`);
|
|
8377
9117
|
if (state.services.redisId) console.log(` Redis: ${state.services.redisId}`);
|
|
8378
9118
|
}
|
|
9119
|
+
const credentials = Object.entries(state.appCredentials ?? {});
|
|
9120
|
+
if (credentials.length > 0) {
|
|
9121
|
+
console.log("");
|
|
9122
|
+
console.log("Database Credentials:");
|
|
9123
|
+
for (const [app, { dbUser, dbPassword }] of credentials) console.log(` ${app}: ${dbUser} / ${dbPassword}`);
|
|
9124
|
+
}
|
|
9125
|
+
const generated = Object.entries(state.generatedSecrets ?? {});
|
|
9126
|
+
if (generated.length > 0) {
|
|
9127
|
+
console.log("");
|
|
9128
|
+
console.log("Generated Secrets:");
|
|
9129
|
+
for (const [app, secrets] of generated) for (const [name, value] of Object.entries(secrets)) console.log(` ${app}.${name}: ${value}`);
|
|
9130
|
+
}
|
|
9131
|
+
if (state.backups) {
|
|
9132
|
+
console.log("");
|
|
9133
|
+
console.log("Backups:");
|
|
9134
|
+
console.log(` Bucket: ${state.backups.bucketName}`);
|
|
9135
|
+
console.log(` IAM User: ${state.backups.iamUserName}`);
|
|
9136
|
+
console.log(` IAM Access Key: ${state.backups.iamAccessKeyId}`);
|
|
9137
|
+
console.log(` IAM Secret Key: ${state.backups.iamSecretAccessKey}`);
|
|
9138
|
+
}
|
|
8379
9139
|
if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
|
|
8380
9140
|
console.log("");
|
|
8381
9141
|
console.log("DNS Verified:");
|
|
@@ -9380,26 +10140,26 @@ function loadPackageJson() {
|
|
|
9380
10140
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
9381
10141
|
*/
|
|
9382
10142
|
const GEEKMIDAS_VERSIONS = {
|
|
9383
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
9384
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
9385
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
9386
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
9387
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
9388
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
9389
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
9390
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
9391
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
9392
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
9393
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
9394
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
9395
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
9396
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
9397
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
9398
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
9399
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
9400
|
-
"@geekmidas/studio": "~10.0.0-alpha.
|
|
9401
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
9402
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
10143
|
+
"@geekmidas/audit": "~10.0.0-alpha.55",
|
|
10144
|
+
"@geekmidas/auth": "~10.0.0-alpha.55",
|
|
10145
|
+
"@geekmidas/cache": "~10.0.0-alpha.55",
|
|
10146
|
+
"@geekmidas/client": "~10.0.0-alpha.55",
|
|
10147
|
+
"@geekmidas/cloud": "~10.0.0-alpha.55",
|
|
10148
|
+
"@geekmidas/constructs": "~10.0.0-alpha.55",
|
|
10149
|
+
"@geekmidas/db": "~10.0.0-alpha.55",
|
|
10150
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.55",
|
|
10151
|
+
"@geekmidas/envkit": "~10.0.0-alpha.55",
|
|
10152
|
+
"@geekmidas/errors": "~10.0.0-alpha.55",
|
|
10153
|
+
"@geekmidas/events": "~10.0.0-alpha.55",
|
|
10154
|
+
"@geekmidas/logger": "~10.0.0-alpha.55",
|
|
10155
|
+
"@geekmidas/manifest": "~10.0.0-alpha.55",
|
|
10156
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.55",
|
|
10157
|
+
"@geekmidas/schema": "~10.0.0-alpha.55",
|
|
10158
|
+
"@geekmidas/services": "~10.0.0-alpha.55",
|
|
10159
|
+
"@geekmidas/storage": "~10.0.0-alpha.55",
|
|
10160
|
+
"@geekmidas/studio": "~10.0.0-alpha.55",
|
|
10161
|
+
"@geekmidas/telescope": "~10.0.0-alpha.55",
|
|
10162
|
+
"@geekmidas/testkit": "~10.0.0-alpha.55",
|
|
9403
10163
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
9404
10164
|
};
|
|
9405
10165
|
//#endregion
|
|
@@ -16142,7 +16902,7 @@ function testDatabases(reconciled) {
|
|
|
16142
16902
|
* starts without the reference data the app needs.
|
|
16143
16903
|
*/
|
|
16144
16904
|
async function migrateTestStage(workspace, env) {
|
|
16145
|
-
const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-
|
|
16905
|
+
const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-DHMRgnP6.cjs"));
|
|
16146
16906
|
const { discover } = await Promise.resolve().then(() => require("./discover-DXNviAh-.cjs")).then((n) => n.discover_exports);
|
|
16147
16907
|
const sources = {};
|
|
16148
16908
|
const manifest = await discover({
|
|
@@ -16573,7 +17333,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
16573
17333
|
try {
|
|
16574
17334
|
const globalOptions = program.opts();
|
|
16575
17335
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16576
|
-
const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
17336
|
+
const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-DHMRgnP6.cjs"));
|
|
16577
17337
|
await migrateCommand({
|
|
16578
17338
|
...options,
|
|
16579
17339
|
...construct ? { construct } : {}
|
|
@@ -16587,7 +17347,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
16587
17347
|
try {
|
|
16588
17348
|
const globalOptions = program.opts();
|
|
16589
17349
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16590
|
-
const { seedCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
17350
|
+
const { seedCommand } = await Promise.resolve().then(() => require("./migrate-DHMRgnP6.cjs"));
|
|
16591
17351
|
await seedCommand({
|
|
16592
17352
|
...options,
|
|
16593
17353
|
...construct ? { construct } : {}
|
|
@@ -16601,7 +17361,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
16601
17361
|
try {
|
|
16602
17362
|
const globalOptions = program.opts();
|
|
16603
17363
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16604
|
-
const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
17364
|
+
const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-DHMRgnP6.cjs"));
|
|
16605
17365
|
await migrationCommand({
|
|
16606
17366
|
construct,
|
|
16607
17367
|
...name ? { name } : {}
|
|
@@ -16699,7 +17459,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
|
|
|
16699
17459
|
try {
|
|
16700
17460
|
const globalOptions = program.opts();
|
|
16701
17461
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16702
|
-
const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-
|
|
17462
|
+
const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-DSHYN5ap.cjs"));
|
|
16703
17463
|
await encryptionRotateCommand(construct, options);
|
|
16704
17464
|
} catch (error) {
|
|
16705
17465
|
console.error(formatError(error));
|
|
@@ -16710,7 +17470,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
|
|
|
16710
17470
|
try {
|
|
16711
17471
|
const globalOptions = program.opts();
|
|
16712
17472
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16713
|
-
const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-
|
|
17473
|
+
const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-DSHYN5ap.cjs"));
|
|
16714
17474
|
await encryptionRetireCommand(construct, key, options);
|
|
16715
17475
|
} catch (error) {
|
|
16716
17476
|
console.error(formatError(error));
|
|
@@ -16762,7 +17522,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
16762
17522
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
16763
17523
|
process.exit(1);
|
|
16764
17524
|
}
|
|
16765
|
-
const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-
|
|
17525
|
+
const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-CXvYabDJ.cjs")).then((n) => n.workspace_exports);
|
|
16766
17526
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
16767
17527
|
if (!result) {
|
|
16768
17528
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
@@ -16821,7 +17581,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
|
|
|
16821
17581
|
try {
|
|
16822
17582
|
const globalOptions = program.opts();
|
|
16823
17583
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16824
|
-
const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-
|
|
17584
|
+
const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-CWflxaV4.cjs"));
|
|
16825
17585
|
await deployGithubCommand(options);
|
|
16826
17586
|
} catch (error) {
|
|
16827
17587
|
console.error(formatError(error));
|
|
@@ -16924,6 +17684,16 @@ program.command("state:show").description("Show deployment state for a stage").r
|
|
|
16924
17684
|
process.exit(1);
|
|
16925
17685
|
}
|
|
16926
17686
|
});
|
|
17687
|
+
program.command("state:unlock").description("Release a stage deploy lock left behind by a run that crashed").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
|
|
17688
|
+
try {
|
|
17689
|
+
const globalOptions = program.opts();
|
|
17690
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
17691
|
+
await stateUnlockCommand(options);
|
|
17692
|
+
} catch (error) {
|
|
17693
|
+
console.error(formatError(error));
|
|
17694
|
+
process.exit(1);
|
|
17695
|
+
}
|
|
17696
|
+
});
|
|
16927
17697
|
program.command("state:diff").description("Compare local and remote deployment state").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
|
|
16928
17698
|
try {
|
|
16929
17699
|
const globalOptions = program.opts();
|
|
@@ -16946,12 +17716,19 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
|
|
|
16946
17716
|
});
|
|
16947
17717
|
program.parse();
|
|
16948
17718
|
//#endregion
|
|
17719
|
+
exports.DocumentStateStore = DocumentStateStore;
|
|
17720
|
+
exports.StageStateMissing = StageStateMissing;
|
|
17721
|
+
exports.StateLocked = StateLocked;
|
|
17722
|
+
exports.StateStoreBusy = StateStoreBusy;
|
|
17723
|
+
exports.StateStoreWithoutLocking = StateStoreWithoutLocking;
|
|
17724
|
+
exports.StateVersionConflict = StateVersionConflict;
|
|
16949
17725
|
Object.defineProperty(exports, "appPorts_exports", {
|
|
16950
17726
|
enumerable: true,
|
|
16951
17727
|
get: function() {
|
|
16952
17728
|
return appPorts_exports;
|
|
16953
17729
|
}
|
|
16954
17730
|
});
|
|
17731
|
+
exports.contentVersion = contentVersion;
|
|
16955
17732
|
Object.defineProperty(exports, "encryption_exports", {
|
|
16956
17733
|
enumerable: true,
|
|
16957
17734
|
get: function() {
|
|
@@ -16960,6 +17737,7 @@ Object.defineProperty(exports, "encryption_exports", {
|
|
|
16960
17737
|
});
|
|
16961
17738
|
exports.generateFullstackCustomSecrets = generateFullstackCustomSecrets;
|
|
16962
17739
|
exports.getHostingerToken = getHostingerToken;
|
|
17740
|
+
exports.newLockHolder = newLockHolder;
|
|
16963
17741
|
exports.slug = slug;
|
|
16964
17742
|
exports.stamp = stamp;
|
|
16965
17743
|
|