@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.mjs
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
3
3
|
import { i as ConstructGenerator, t as discover } from "./discover-Cf2jAcgF.mjs";
|
|
4
|
-
import { d as parseModuleConfig, f as appKey, l as loadWorkspaceConfig, n as NotABackendApp, o as loadAppConfig, p as derivedApps, r as NotInAnApp, s as loadConfig, t as ConfigNotFound, u as loadWorkspaceSettings } from "./config-
|
|
5
|
-
import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-
|
|
4
|
+
import { d as parseModuleConfig, f as appKey, l as loadWorkspaceConfig, n as NotABackendApp, o as loadAppConfig, p as derivedApps, r as NotInAnApp, s as loadConfig, t as ConfigNotFound, u as loadWorkspaceSettings } from "./config-XEd-dKVX.mjs";
|
|
5
|
+
import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-DHK2N1mq.mjs";
|
|
6
6
|
import { a as withCustomSecret, i as toEmbeddableSecrets, n as maskPassword, t as initStageSecrets } from "./storage-BzaUxmqe.mjs";
|
|
7
7
|
import { a as getKeyPath, i as FileSecretsStore, n as secretsStoreFor } from "./store-DeGATCS-.mjs";
|
|
8
|
-
import { C as loadEnvFiles, S as isPortAvailable, T as prepareEntryCredentials, _ as fakeKey, a as reconcileWorkspace, b as createEntryWrapper, c as LOCAL_CA_PATH, d as postgresDatabaseNames, f as quoteIdentifier, g as describeServices, h as rootSite, i as derivedContainers, l as forgetState, m as resourceName, n as backendsOf$1, p as pgClient, r as constructGlobs, s as COMPOSE_PATH, t as FAKE_ENV, u as applyPostgres, v as primaryPortKey, w as loadSecretsForApp, x as findAvailablePort, y as createCredentialsPreload } from "./workspace-
|
|
9
|
-
import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-
|
|
8
|
+
import { C as loadEnvFiles, S as isPortAvailable, T as prepareEntryCredentials, _ as fakeKey, a as reconcileWorkspace, b as createEntryWrapper, c as LOCAL_CA_PATH, d as postgresDatabaseNames, f as quoteIdentifier, g as describeServices, h as rootSite, i as derivedContainers, l as forgetState, m as resourceName, n as backendsOf$1, p as pgClient, r as constructGlobs, s as COMPOSE_PATH, t as FAKE_ENV, u as applyPostgres, v as primaryPortKey, w as loadSecretsForApp, x as findAvailablePort, y as createCredentialsPreload } from "./workspace-BE0WHPw9.mjs";
|
|
9
|
+
import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-DKmAorGX.mjs";
|
|
10
10
|
import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
|
|
11
|
-
import { a as writeTestHarness, c as driversFor, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as SubscriberGenerator, s as cacheBackendsIn, t as TEST_READY_ENV } from "./ready-
|
|
12
|
-
import { i as storageBackendFor, n as eventsBackendFor, r as providerOf, t as cacheBackendFor } from "./backends-
|
|
11
|
+
import { a as writeTestHarness, c as driversFor, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as SubscriberGenerator, s as cacheBackendsIn, t as TEST_READY_ENV } from "./ready-bES_Jk5v.mjs";
|
|
12
|
+
import { i as storageBackendFor, n as eventsBackendFor, r as providerOf, t as cacheBackendFor } from "./backends-BNrMPlJb.mjs";
|
|
13
13
|
import { n as withCompute, r as withRoutes, t as manifestModule } from "./emit-C1e8qD08.mjs";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import { stdin, stdout } from "node:process";
|
|
17
17
|
import * as readline from "node:readline/promises";
|
|
18
18
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
|
-
import { homedir } from "node:os";
|
|
19
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
|
+
import { homedir, hostname, tmpdir, userInfo } from "node:os";
|
|
21
21
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
|
|
22
|
-
import { execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
23
22
|
import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, externalApiUrl, kebabCase, migrationFolder, mobileOrigins, provideKey, provisionOrder, schemeBase, scopedName, serviceKey } from "@geekmidas/manifest";
|
|
24
23
|
import fg from "fast-glob";
|
|
24
|
+
import { execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
25
25
|
import chokidar from "chokidar";
|
|
26
|
-
import { createCipheriv, createHash, randomBytes } from "node:crypto";
|
|
26
|
+
import { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28
28
|
import { generateKeyring } from "@geekmidas/constructs/encryption";
|
|
29
29
|
import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
|
|
@@ -42,7 +42,7 @@ import { Queue } from "@geekmidas/constructs/queue";
|
|
|
42
42
|
import { Topic } from "@geekmidas/constructs/topic";
|
|
43
43
|
import { lookup } from "node:dns/promises";
|
|
44
44
|
//#region package.json
|
|
45
|
-
var version = "10.0.0-alpha.
|
|
45
|
+
var version = "10.0.0-alpha.55";
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region src/auth/credentials.ts
|
|
48
48
|
/**
|
|
@@ -388,12 +388,12 @@ function withOwningTsconfigJsx(nodeOptions = "") {
|
|
|
388
388
|
* unhelpful.
|
|
389
389
|
*
|
|
390
390
|
* A separate command rather than part of `gkm setup`, for one reason: this
|
|
391
|
-
*
|
|
392
|
-
* trade than one that tells you what to type. Caddy's own `caddy trust`
|
|
393
|
-
* the line in the same place.
|
|
391
|
+
* asks for a password, and a setup that silently escalates on every run is a
|
|
392
|
+
* worse trade than one that tells you what to type. Caddy's own `caddy trust`
|
|
393
|
+
* draws the line in the same place.
|
|
394
394
|
*/
|
|
395
395
|
const logger$11 = console;
|
|
396
|
-
const run = promisify(execFile);
|
|
396
|
+
const run$1 = promisify(execFile);
|
|
397
397
|
/**
|
|
398
398
|
* How each platform is told about a new authority.
|
|
399
399
|
*
|
|
@@ -404,15 +404,14 @@ const run = promisify(execFile);
|
|
|
404
404
|
*/
|
|
405
405
|
function installation(platform, certificate) {
|
|
406
406
|
if (platform === "darwin") return {
|
|
407
|
-
describe: "
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
describe: "your login keychain",
|
|
408
|
+
asks: "macOS asks once — your password or Touch ID — to change trust settings.",
|
|
409
|
+
steps: [["security", [
|
|
410
410
|
"add-trusted-cert",
|
|
411
|
-
"-d",
|
|
412
411
|
"-r",
|
|
413
412
|
"trustRoot",
|
|
414
413
|
"-k",
|
|
415
|
-
"
|
|
414
|
+
join(homedir(), "Library/Keychains/login.keychain-db"),
|
|
416
415
|
certificate
|
|
417
416
|
]]]
|
|
418
417
|
};
|
|
@@ -420,6 +419,7 @@ function installation(platform, certificate) {
|
|
|
420
419
|
const debian = existsSync("/usr/local/share/ca-certificates");
|
|
421
420
|
return {
|
|
422
421
|
describe: debian ? "the system CA bundle" : "the system trust anchors",
|
|
422
|
+
asks: "This needs sudo: the system trust store is the only one.",
|
|
423
423
|
steps: debian ? [["sudo", [
|
|
424
424
|
"cp",
|
|
425
425
|
certificate,
|
|
@@ -443,15 +443,15 @@ async function trustCommand(options = {}) {
|
|
|
443
443
|
const { workspace } = await loadWorkspaceConfig();
|
|
444
444
|
const certificate = join(workspace.root, LOCAL_CA_PATH);
|
|
445
445
|
if (!existsSync(certificate)) throw new NoLocalAuthority(certificate);
|
|
446
|
-
const { describe, steps } = installation(process.platform, certificate);
|
|
446
|
+
const { describe, asks, steps } = installation(process.platform, certificate);
|
|
447
447
|
logger$11.log(`\n🔐 Trusting the local authority in ${describe}`);
|
|
448
448
|
logger$11.log(` ${certificate}\n`);
|
|
449
449
|
if (options.dryRun) {
|
|
450
450
|
for (const [command, args] of steps) logger$11.log(` ${command} ${args.join(" ")}`);
|
|
451
451
|
return;
|
|
452
452
|
}
|
|
453
|
-
logger$11.log(
|
|
454
|
-
for (const [command, args] of steps) await run(command, args, { stdio: "inherit" });
|
|
453
|
+
logger$11.log(` ${asks}\n`);
|
|
454
|
+
for (const [command, args] of steps) await run$1(command, args, { stdio: "inherit" });
|
|
455
455
|
logger$11.log("\n✅ Local https addresses are trusted. Restart your browser.\n");
|
|
456
456
|
}
|
|
457
457
|
/**
|
|
@@ -459,7 +459,7 @@ async function trustCommand(options = {}) {
|
|
|
459
459
|
*
|
|
460
460
|
* A machine's answer, not the project's: whether a laptop installs a root
|
|
461
461
|
* certificate is its owner's call, so the decision is `--yes`, then what this
|
|
462
|
-
* machine previously answered, then a prompt. Never a silent
|
|
462
|
+
* machine previously answered, then a prompt. Never a silent escalation — that is
|
|
463
463
|
* the one thing a setup command should not do on somebody's behalf.
|
|
464
464
|
*
|
|
465
465
|
* Silent and free when the machine already trusts it, which is what makes it
|
|
@@ -476,7 +476,7 @@ async function ensureTrusted(root, url, options = {}) {
|
|
|
476
476
|
const { install } = await prompts({
|
|
477
477
|
type: "confirm",
|
|
478
478
|
name: "install",
|
|
479
|
-
message: "Trust this project's local certificate authority, so a browser accepts its https addresses? (
|
|
479
|
+
message: "Trust this project's local certificate authority, so a browser accepts its https addresses? (asks for your password)",
|
|
480
480
|
initial: false
|
|
481
481
|
});
|
|
482
482
|
if (!install) {
|
|
@@ -517,7 +517,7 @@ async function recordDeclined(root) {
|
|
|
517
517
|
async function isTrusted(url) {
|
|
518
518
|
const { NODE_EXTRA_CA_CERTS: _ignored, ...env } = process.env;
|
|
519
519
|
try {
|
|
520
|
-
await run(process.execPath, [
|
|
520
|
+
await run$1(process.execPath, [
|
|
521
521
|
"--use-system-ca",
|
|
522
522
|
"-e",
|
|
523
523
|
`fetch(${JSON.stringify(url)}).then(()=>0,()=>process.exit(1))`
|
|
@@ -1368,10 +1368,10 @@ function normalizeHooksConfig(config, cwd = process.cwd()) {
|
|
|
1368
1368
|
/**
|
|
1369
1369
|
* What a `--production` build is.
|
|
1370
1370
|
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1371
|
+
* Background work — queues polled, crons scheduled, subscribers drained — is
|
|
1372
|
+
* included here, and `buildApp` takes it back out when the app serves a
|
|
1373
|
+
* RestApi: an API's image answers HTTP and nothing else, and that work belongs
|
|
1374
|
+
* to a Worker. The build still writes their files on every server build.
|
|
1375
1375
|
*
|
|
1376
1376
|
* @internal Exported for testing
|
|
1377
1377
|
*/
|
|
@@ -1765,9 +1765,9 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1765
1765
|
for (const { id, port } of fakes) logger$9.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1766
1766
|
logger$9.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
|
|
1767
1767
|
if (options.migrate || options.seed) {
|
|
1768
|
-
const { prepareDevDatabases } = await import("./migrate-
|
|
1768
|
+
const { prepareDevDatabases } = await import("./migrate-PcQg692O.mjs");
|
|
1769
1769
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1770
|
-
} else await import("./migrate-
|
|
1770
|
+
} else await import("./migrate-PcQg692O.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1771
1771
|
logger$9.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1772
1772
|
});
|
|
1773
1773
|
const appUrls = {};
|
|
@@ -2738,6 +2738,147 @@ var TopicGenerator = class extends ConstructGenerator {
|
|
|
2738
2738
|
}
|
|
2739
2739
|
};
|
|
2740
2740
|
//#endregion
|
|
2741
|
+
//#region src/run.ts
|
|
2742
|
+
/**
|
|
2743
|
+
* Start a program with an argument array, never a shell string.
|
|
2744
|
+
*
|
|
2745
|
+
* `gkm build`, `gkm docker` and `gkm deploy` pass values that come from the
|
|
2746
|
+
* workspace — image refs, tags, build args, package names — to `docker` and
|
|
2747
|
+
* `turbo`. Joined into a string and handed to a shell, a package named
|
|
2748
|
+
* `x;curl …|sh` or a tag holding `$(…)` became part of the command line, which
|
|
2749
|
+
* matters most where the CLI builds a repository it didn't write: a CI job for a
|
|
2750
|
+
* fork, a shared build runner. Here every value reaches the program as one
|
|
2751
|
+
* `argv` element, whatever it contains.
|
|
2752
|
+
*/
|
|
2753
|
+
/** Long enough for a cold image build; short enough that a hung one ends. */
|
|
2754
|
+
const DEFAULT_TIMEOUT_MS = 18e5;
|
|
2755
|
+
/** How long a timed-out child has to exit on SIGTERM before it is killed. */
|
|
2756
|
+
const KILL_GRACE_MS = 5e3;
|
|
2757
|
+
/** The command line as a person would read it — for messages, never to run. */
|
|
2758
|
+
function display(command, args) {
|
|
2759
|
+
return [command, ...args].join(" ");
|
|
2760
|
+
}
|
|
2761
|
+
/** The program ran and exited unsuccessfully; its own output said why. */
|
|
2762
|
+
var CommandFailed = class extends Error {
|
|
2763
|
+
command;
|
|
2764
|
+
args;
|
|
2765
|
+
exitCode;
|
|
2766
|
+
signal;
|
|
2767
|
+
constructor(command, args, exitCode, signal) {
|
|
2768
|
+
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.`);
|
|
2769
|
+
this.command = command;
|
|
2770
|
+
this.args = args;
|
|
2771
|
+
this.exitCode = exitCode;
|
|
2772
|
+
this.signal = signal;
|
|
2773
|
+
this.name = "CommandFailed";
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
/** The program ran past its timeout and was killed. */
|
|
2777
|
+
var CommandTimedOut = class extends Error {
|
|
2778
|
+
command;
|
|
2779
|
+
args;
|
|
2780
|
+
timeoutMs;
|
|
2781
|
+
constructor(command, args, timeoutMs) {
|
|
2782
|
+
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.`);
|
|
2783
|
+
this.command = command;
|
|
2784
|
+
this.args = args;
|
|
2785
|
+
this.timeoutMs = timeoutMs;
|
|
2786
|
+
this.name = "CommandTimedOut";
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
/**
|
|
2790
|
+
* Run `command` with `args`, resolving when it exits 0.
|
|
2791
|
+
*
|
|
2792
|
+
* Rejects with {@link CommandFailed} on any other exit, {@link CommandTimedOut}
|
|
2793
|
+
* once `timeoutMs` passes (after the child is gone), and with the spawn error
|
|
2794
|
+
* itself when the program cannot be started at all — `ENOENT` already names it.
|
|
2795
|
+
*/
|
|
2796
|
+
function run(command, args, options = {}) {
|
|
2797
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...spawnOptions } = options;
|
|
2798
|
+
return new Promise((resolve, reject) => {
|
|
2799
|
+
const child = spawn(command, [...args], {
|
|
2800
|
+
stdio: "inherit",
|
|
2801
|
+
...spawnOptions,
|
|
2802
|
+
shell: false
|
|
2803
|
+
});
|
|
2804
|
+
let timedOut = false;
|
|
2805
|
+
let killer;
|
|
2806
|
+
const timer = setTimeout(() => {
|
|
2807
|
+
timedOut = true;
|
|
2808
|
+
child.kill("SIGTERM");
|
|
2809
|
+
killer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
2810
|
+
killer.unref?.();
|
|
2811
|
+
}, timeoutMs);
|
|
2812
|
+
timer.unref?.();
|
|
2813
|
+
const settle = () => {
|
|
2814
|
+
clearTimeout(timer);
|
|
2815
|
+
if (killer) clearTimeout(killer);
|
|
2816
|
+
};
|
|
2817
|
+
child.on("error", (error) => {
|
|
2818
|
+
settle();
|
|
2819
|
+
reject(error);
|
|
2820
|
+
});
|
|
2821
|
+
child.on("close", (code, signal) => {
|
|
2822
|
+
settle();
|
|
2823
|
+
if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
|
|
2824
|
+
else if (code === 0) resolve();
|
|
2825
|
+
else reject(new CommandFailed(command, args, code, signal));
|
|
2826
|
+
});
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
//#endregion
|
|
2830
|
+
//#region src/secrets/encryption.ts
|
|
2831
|
+
var encryption_exports = /* @__PURE__ */ __exportAll({
|
|
2832
|
+
encryptSecrets: () => encryptSecrets,
|
|
2833
|
+
generateDefineOptions: () => generateDefineOptions,
|
|
2834
|
+
keyFingerprint: () => keyFingerprint
|
|
2835
|
+
});
|
|
2836
|
+
/** AES-256-GCM configuration */
|
|
2837
|
+
const ALGORITHM = "aes-256-gcm";
|
|
2838
|
+
const KEY_LENGTH = 32;
|
|
2839
|
+
const IV_LENGTH = 12;
|
|
2840
|
+
/**
|
|
2841
|
+
* Encrypt secrets for embedding in a bundle.
|
|
2842
|
+
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
2843
|
+
*
|
|
2844
|
+
* @param secrets - Key-value pairs to encrypt
|
|
2845
|
+
* @returns Encrypted payload with ephemeral master key
|
|
2846
|
+
*/
|
|
2847
|
+
function encryptSecrets(secrets) {
|
|
2848
|
+
const masterKey = randomBytes(KEY_LENGTH);
|
|
2849
|
+
const iv = randomBytes(IV_LENGTH);
|
|
2850
|
+
const plaintext = JSON.stringify(secrets);
|
|
2851
|
+
const cipher = createCipheriv(ALGORITHM, masterKey, iv);
|
|
2852
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
2853
|
+
const authTag = cipher.getAuthTag();
|
|
2854
|
+
return {
|
|
2855
|
+
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
2856
|
+
iv: iv.toString("hex"),
|
|
2857
|
+
masterKey: masterKey.toString("hex")
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Generate the define options for tsdown/esbuild.
|
|
2862
|
+
* These will be injected at build time.
|
|
2863
|
+
*/
|
|
2864
|
+
function generateDefineOptions(payload) {
|
|
2865
|
+
return {
|
|
2866
|
+
__GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
|
|
2867
|
+
__GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* A short, one-way name for a master key: the first 8 hex characters of its
|
|
2872
|
+
* SHA-256.
|
|
2873
|
+
*
|
|
2874
|
+
* Printed where the key itself used to be, so a person can tell which key a
|
|
2875
|
+
* build used — and match it against the one they hold — without the output
|
|
2876
|
+
* carrying a value that decrypts every secret of the stage.
|
|
2877
|
+
*/
|
|
2878
|
+
function keyFingerprint(masterKey) {
|
|
2879
|
+
return createHash("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
2880
|
+
}
|
|
2881
|
+
//#endregion
|
|
2741
2882
|
//#region src/build/owners.ts
|
|
2742
2883
|
/**
|
|
2743
2884
|
* What a generated entry imports its runtime from, read off the discovered
|
|
@@ -2868,6 +3009,11 @@ export default app;
|
|
|
2868
3009
|
//#region src/build/index.ts
|
|
2869
3010
|
const logger$8 = console;
|
|
2870
3011
|
/**
|
|
3012
|
+
* Where `gkm build --stage` leaves the key its bundle's secrets were encrypted
|
|
3013
|
+
* with, beside the bundle in `.gkm/server/` and readable by its owner alone.
|
|
3014
|
+
*/
|
|
3015
|
+
const MASTER_KEY_FILE = "master.key";
|
|
3016
|
+
/**
|
|
2871
3017
|
* What a build run at the workspace root reads.
|
|
2872
3018
|
*
|
|
2873
3019
|
* One backend app there, and it is that app's config, so its own `openapi`
|
|
@@ -3083,6 +3229,18 @@ async function buildApp(input) {
|
|
|
3083
3229
|
logger$8.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
|
|
3084
3230
|
return {};
|
|
3085
3231
|
}
|
|
3232
|
+
if (production?.subscribers === "include" && derived.surface) {
|
|
3233
|
+
buildContext.production = {
|
|
3234
|
+
...production,
|
|
3235
|
+
subscribers: "exclude"
|
|
3236
|
+
};
|
|
3237
|
+
const left = [
|
|
3238
|
+
[allCrons.length, "cron"],
|
|
3239
|
+
[allQueues.length, "queue consumer"],
|
|
3240
|
+
[allSubscribers.length, "subscriber"]
|
|
3241
|
+
].filter(([n]) => n > 0).map(([n, what]) => `${n} ${what}${n === 1 ? "" : "s"}`);
|
|
3242
|
+
if (left.length > 0) logger$8.log(`Serving ${derived.surface.id} only: leaving out ${left.join(", ")} — they run in a Worker's image`);
|
|
3243
|
+
}
|
|
3086
3244
|
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);
|
|
3087
3245
|
await generateOpenApi(allEndpoints.map(({ construct }) => construct), {
|
|
3088
3246
|
openapi: config.openapi,
|
|
@@ -3141,7 +3299,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3141
3299
|
let masterKey;
|
|
3142
3300
|
if (context.production?.bundle && !skipBundle) {
|
|
3143
3301
|
logger$8.log(`\n📦 Bundling production server...`);
|
|
3144
|
-
const { bundleServer } = await import("./bundler
|
|
3302
|
+
const { bundleServer } = await import("./bundler-URImKj-A.mjs");
|
|
3145
3303
|
const allConstructs = [
|
|
3146
3304
|
...endpoints.map((e) => e.construct),
|
|
3147
3305
|
...functions.map((f) => f.construct),
|
|
@@ -3160,8 +3318,11 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3160
3318
|
})).masterKey;
|
|
3161
3319
|
logger$8.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
|
|
3162
3320
|
if (masterKey) {
|
|
3321
|
+
const keyPath = join(outputDir, MASTER_KEY_FILE);
|
|
3322
|
+
await writeFile(keyPath, `${masterKey}\n`, { mode: 384 });
|
|
3323
|
+
await chmod(keyPath, 384);
|
|
3163
3324
|
logger$8.log(`\n🔐 Secrets encrypted for deployment`);
|
|
3164
|
-
logger$8.log(`
|
|
3325
|
+
logger$8.log(` Run with GKM_MASTER_KEY set to the key in ${relative(appRoot, keyPath)} (fingerprint ${keyFingerprint(masterKey)})`);
|
|
3165
3326
|
}
|
|
3166
3327
|
}
|
|
3167
3328
|
return {
|
|
@@ -3184,7 +3345,7 @@ function detectPackageManager$2() {
|
|
|
3184
3345
|
return "npm";
|
|
3185
3346
|
}
|
|
3186
3347
|
/**
|
|
3187
|
-
*
|
|
3348
|
+
* The program and argv that run turbo's build.
|
|
3188
3349
|
*
|
|
3189
3350
|
* `filters` names the packages to build. Passing none lets turbo infer its own
|
|
3190
3351
|
* scope from the working directory, which for a workspace root means the
|
|
@@ -3192,14 +3353,24 @@ function detectPackageManager$2() {
|
|
|
3192
3353
|
* `gkm build`, inferring is how you get a build that runs itself forever.
|
|
3193
3354
|
* `workspaceBuildCommand` always names the apps.
|
|
3194
3355
|
*
|
|
3356
|
+
* An array, not a command string: the filters are package names read from the
|
|
3357
|
+
* workspace, and each must reach turbo as one argument whatever it holds. Each
|
|
3358
|
+
* is `--filter=<name>`, so even a name starting with `-` stays a filter value.
|
|
3359
|
+
*
|
|
3195
3360
|
* @internal Exported for testing
|
|
3196
3361
|
*/
|
|
3197
|
-
function
|
|
3198
|
-
const
|
|
3362
|
+
function getTurboArgs(pm, filters = [], { only = false } = {}) {
|
|
3363
|
+
const turbo = [
|
|
3364
|
+
"turbo",
|
|
3365
|
+
"run",
|
|
3366
|
+
"build",
|
|
3367
|
+
...(typeof filters === "string" ? [filters] : filters).map((f) => `--filter=${f}`),
|
|
3368
|
+
...only ? ["--only"] : []
|
|
3369
|
+
];
|
|
3199
3370
|
switch (pm) {
|
|
3200
|
-
case "pnpm": return
|
|
3201
|
-
case "yarn": return
|
|
3202
|
-
case "npm": return
|
|
3371
|
+
case "pnpm": return ["pnpm", ["exec", ...turbo]];
|
|
3372
|
+
case "yarn": return ["yarn", turbo];
|
|
3373
|
+
case "npm": return ["npx", turbo];
|
|
3203
3374
|
}
|
|
3204
3375
|
}
|
|
3205
3376
|
/**
|
|
@@ -3288,27 +3459,16 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3288
3459
|
const { filters, unpackaged } = turboFilters(workspace, { exclude: "backend" });
|
|
3289
3460
|
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.`);
|
|
3290
3461
|
const pm = detectPackageManager$2();
|
|
3291
|
-
const
|
|
3462
|
+
const [turboBin, turboArgs] = getTurboArgs(pm, filters, { only: true });
|
|
3292
3463
|
if (filters.length > 0) {
|
|
3293
3464
|
logger$8.log(`\n📦 Using ${pm} with Turbo for the other apps...\n`);
|
|
3294
|
-
logger$8.log(`Running: ${
|
|
3295
|
-
await
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
...process.env,
|
|
3302
|
-
NODE_ENV: options.production ? "production" : "development"
|
|
3303
|
-
}
|
|
3304
|
-
});
|
|
3305
|
-
child.on("close", (code) => {
|
|
3306
|
-
if (code === 0) resolve();
|
|
3307
|
-
else reject(/* @__PURE__ */ new Error(`Turbo build failed with exit code ${code}`));
|
|
3308
|
-
});
|
|
3309
|
-
child.on("error", (err) => {
|
|
3310
|
-
reject(err);
|
|
3311
|
-
});
|
|
3465
|
+
logger$8.log(`Running: ${[turboBin, ...turboArgs].join(" ")}`);
|
|
3466
|
+
await run(turboBin, turboArgs, {
|
|
3467
|
+
cwd: workspace.root,
|
|
3468
|
+
env: {
|
|
3469
|
+
...process.env,
|
|
3470
|
+
NODE_ENV: options.production ? "production" : "development"
|
|
3471
|
+
}
|
|
3312
3472
|
});
|
|
3313
3473
|
}
|
|
3314
3474
|
for (const [appName, app] of apps) {
|
|
@@ -4238,7 +4398,7 @@ async function createDnsProvider(options) {
|
|
|
4238
4398
|
if (isDnsProvider(config.provider)) return config.provider;
|
|
4239
4399
|
const provider = config.provider;
|
|
4240
4400
|
if (provider === "hostinger") {
|
|
4241
|
-
const { HostingerProvider } = await import("./HostingerProvider-
|
|
4401
|
+
const { HostingerProvider } = await import("./HostingerProvider-cL-6oQ5r.mjs");
|
|
4242
4402
|
return new HostingerProvider();
|
|
4243
4403
|
}
|
|
4244
4404
|
if (provider === "route53") {
|
|
@@ -4655,6 +4815,39 @@ function getPublicUrlArgNames(app) {
|
|
|
4655
4815
|
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
4656
4816
|
}
|
|
4657
4817
|
//#endregion
|
|
4818
|
+
//#region src/docker/imageRef.ts
|
|
4819
|
+
/**
|
|
4820
|
+
* An image reference, checked before it reaches `docker`.
|
|
4821
|
+
*
|
|
4822
|
+
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
4823
|
+
* name from the project, a tag from `--tag` — and an argument array already
|
|
4824
|
+
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
4825
|
+
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
4826
|
+
* own grammar (distribution/reference) and anything else is refused by name
|
|
4827
|
+
* before a build starts.
|
|
4828
|
+
*/
|
|
4829
|
+
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
4830
|
+
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
4831
|
+
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
4832
|
+
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,})?$`);
|
|
4833
|
+
/** Docker refuses a repository name longer than this. */
|
|
4834
|
+
const MAX_NAME_LENGTH = 255;
|
|
4835
|
+
/** A value that is not an image reference was about to be built or pushed. */
|
|
4836
|
+
var ImageRefInvalid = class extends Error {
|
|
4837
|
+
ref;
|
|
4838
|
+
constructor(ref) {
|
|
4839
|
+
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.`);
|
|
4840
|
+
this.ref = ref;
|
|
4841
|
+
this.name = "ImageRefInvalid";
|
|
4842
|
+
}
|
|
4843
|
+
};
|
|
4844
|
+
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
4845
|
+
function validateImageRef(ref) {
|
|
4846
|
+
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
4847
|
+
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
4848
|
+
return ref;
|
|
4849
|
+
}
|
|
4850
|
+
//#endregion
|
|
4658
4851
|
//#region src/docker/templates.ts
|
|
4659
4852
|
const LOCKFILES = [
|
|
4660
4853
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -5008,6 +5201,10 @@ node_modules
|
|
|
5008
5201
|
.gkm/server/*.ts
|
|
5009
5202
|
!.gkm/server/dist
|
|
5010
5203
|
|
|
5204
|
+
# The key gkm build --stage encrypted secrets with: runtime env only, never
|
|
5205
|
+
# the build context
|
|
5206
|
+
.gkm/server/master.key
|
|
5207
|
+
|
|
5011
5208
|
# IDE and editor
|
|
5012
5209
|
.idea
|
|
5013
5210
|
.vscode
|
|
@@ -5222,10 +5419,6 @@ FROM deps AS builder
|
|
|
5222
5419
|
|
|
5223
5420
|
WORKDIR /app
|
|
5224
5421
|
|
|
5225
|
-
# Build-time args for encrypted secrets
|
|
5226
|
-
ARG GKM_ENCRYPTED_CREDENTIALS=""
|
|
5227
|
-
ARG GKM_CREDENTIALS_IV=""
|
|
5228
|
-
|
|
5229
5422
|
# Copy pruned source
|
|
5230
5423
|
COPY --from=pruner /app/out/full/ ./
|
|
5231
5424
|
|
|
@@ -5234,11 +5427,15 @@ COPY --from=pruner /app/out/full/ ./
|
|
|
5234
5427
|
COPY --from=pruner /app/gkm.config.* ./
|
|
5235
5428
|
COPY --from=pruner /app/tsconfig.* ./
|
|
5236
5429
|
|
|
5237
|
-
# Write encrypted credentials for gkm build to embed
|
|
5238
|
-
|
|
5430
|
+
# Write encrypted credentials for gkm build to embed.
|
|
5431
|
+
# A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
|
|
5432
|
+
# ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
|
|
5433
|
+
# absent when the app has no secrets.
|
|
5434
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
5435
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
5239
5436
|
mkdir -p ${appPath}/.gkm && \
|
|
5240
|
-
|
|
5241
|
-
|
|
5437
|
+
sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
|
|
5438
|
+
sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
|
|
5242
5439
|
fi
|
|
5243
5440
|
|
|
5244
5441
|
# Build any workspace packages the app depends on.
|
|
@@ -5325,10 +5522,6 @@ FROM deps AS builder
|
|
|
5325
5522
|
|
|
5326
5523
|
WORKDIR /app
|
|
5327
5524
|
|
|
5328
|
-
# Build-time args for encrypted secrets
|
|
5329
|
-
ARG GKM_ENCRYPTED_CREDENTIALS=""
|
|
5330
|
-
ARG GKM_CREDENTIALS_IV=""
|
|
5331
|
-
|
|
5332
5525
|
# Copy pruned source
|
|
5333
5526
|
COPY --from=pruner /app/out/full/ ./
|
|
5334
5527
|
|
|
@@ -5336,20 +5529,17 @@ COPY --from=pruner /app/out/full/ ./
|
|
|
5336
5529
|
# Using wildcard to make it optional for single-app projects
|
|
5337
5530
|
COPY --from=pruner /app/tsconfig.* ./
|
|
5338
5531
|
|
|
5339
|
-
# Write encrypted credentials for tsdown to embed via define
|
|
5340
|
-
RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
|
|
5341
|
-
mkdir -p ${appPath}/.gkm && \
|
|
5342
|
-
echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
|
|
5343
|
-
echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
|
|
5344
|
-
fi
|
|
5345
|
-
|
|
5346
5532
|
# Bundle entry point with esbuild (outputs to dist/index.mjs)
|
|
5347
5533
|
# Creates a fully standalone bundle with all dependencies included
|
|
5348
|
-
# Use define to embed credentials if present
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5534
|
+
# Use define to embed credentials if present.
|
|
5535
|
+
# They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
|
|
5536
|
+
# not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
|
|
5537
|
+
# then IV; absent when the app has no secrets.
|
|
5538
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
5539
|
+
cd ${appPath} && \
|
|
5540
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
5541
|
+
CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
|
|
5542
|
+
IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
|
|
5353
5543
|
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
5354
5544
|
--outfile=dist/index.mjs --packages=bundle \
|
|
5355
5545
|
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
|
|
@@ -5500,6 +5690,33 @@ CMD ["nginx", "-g", "daemon off;"]
|
|
|
5500
5690
|
//#endregion
|
|
5501
5691
|
//#region src/docker/index.ts
|
|
5502
5692
|
const logger$6 = console;
|
|
5693
|
+
/** `docker build` failed; what docker printed above says why. */
|
|
5694
|
+
var DockerBuildFailed = class extends Error {
|
|
5695
|
+
imageRef;
|
|
5696
|
+
constructor(imageRef, cause) {
|
|
5697
|
+
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
5698
|
+
this.imageRef = imageRef;
|
|
5699
|
+
this.name = "DockerBuildFailed";
|
|
5700
|
+
}
|
|
5701
|
+
};
|
|
5702
|
+
/** `docker push` failed — usually the registry login or its permissions. */
|
|
5703
|
+
var DockerPushFailed = class extends Error {
|
|
5704
|
+
imageRef;
|
|
5705
|
+
constructor(imageRef, cause) {
|
|
5706
|
+
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
5707
|
+
this.imageRef = imageRef;
|
|
5708
|
+
this.name = "DockerPushFailed";
|
|
5709
|
+
}
|
|
5710
|
+
};
|
|
5711
|
+
/** `gkm docker --push` was asked for with nowhere to push to. */
|
|
5712
|
+
var PushNeedsRegistry = class extends Error {
|
|
5713
|
+
imageName;
|
|
5714
|
+
constructor(imageName) {
|
|
5715
|
+
super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
|
|
5716
|
+
this.imageName = imageName;
|
|
5717
|
+
this.name = "PushNeedsRegistry";
|
|
5718
|
+
}
|
|
5719
|
+
};
|
|
5503
5720
|
/**
|
|
5504
5721
|
* Docker command implementation
|
|
5505
5722
|
* Generates Dockerfile, docker-compose.yml, and related files
|
|
@@ -5603,14 +5820,18 @@ function ensureLockfile(cwd) {
|
|
|
5603
5820
|
async function buildDockerImage(imageName, options) {
|
|
5604
5821
|
const tag = options.tag ?? "latest";
|
|
5605
5822
|
const registry = options.registry;
|
|
5606
|
-
const fullImageName = registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}
|
|
5823
|
+
const fullImageName = validateImageRef(registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`);
|
|
5607
5824
|
logger$6.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
5608
5825
|
const cwd = process.cwd();
|
|
5609
5826
|
const cleanup = ensureLockfile(cwd);
|
|
5610
5827
|
try {
|
|
5611
|
-
|
|
5828
|
+
await run("docker", [
|
|
5829
|
+
"build",
|
|
5830
|
+
"--file=.gkm/docker/Dockerfile",
|
|
5831
|
+
`--tag=${fullImageName}`,
|
|
5832
|
+
"."
|
|
5833
|
+
], {
|
|
5612
5834
|
cwd,
|
|
5613
|
-
stdio: "inherit",
|
|
5614
5835
|
env: {
|
|
5615
5836
|
...process.env,
|
|
5616
5837
|
DOCKER_BUILDKIT: "1"
|
|
@@ -5618,7 +5839,7 @@ async function buildDockerImage(imageName, options) {
|
|
|
5618
5839
|
});
|
|
5619
5840
|
logger$6.log(`✅ Docker image built: ${fullImageName}`);
|
|
5620
5841
|
} catch (error) {
|
|
5621
|
-
throw new
|
|
5842
|
+
throw new DockerBuildFailed(fullImageName, error);
|
|
5622
5843
|
} finally {
|
|
5623
5844
|
cleanup?.();
|
|
5624
5845
|
}
|
|
@@ -5629,17 +5850,14 @@ async function buildDockerImage(imageName, options) {
|
|
|
5629
5850
|
async function pushDockerImage(imageName, options) {
|
|
5630
5851
|
const tag = options.tag ?? "latest";
|
|
5631
5852
|
const registry = options.registry;
|
|
5632
|
-
if (!registry) throw new
|
|
5633
|
-
const fullImageName = `${registry}/${imageName}:${tag}
|
|
5853
|
+
if (!registry) throw new PushNeedsRegistry(imageName);
|
|
5854
|
+
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
5634
5855
|
logger$6.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
5635
5856
|
try {
|
|
5636
|
-
|
|
5637
|
-
cwd: process.cwd(),
|
|
5638
|
-
stdio: "inherit"
|
|
5639
|
-
});
|
|
5857
|
+
await run("docker", ["push", fullImageName], { cwd: process.cwd() });
|
|
5640
5858
|
logger$6.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
5641
5859
|
} catch (error) {
|
|
5642
|
-
throw new
|
|
5860
|
+
throw new DockerPushFailed(fullImageName, error);
|
|
5643
5861
|
}
|
|
5644
5862
|
}
|
|
5645
5863
|
/**
|
|
@@ -5771,6 +5989,11 @@ function printRunInstructions(workspace) {
|
|
|
5771
5989
|
//#region src/deploy/docker.ts
|
|
5772
5990
|
const logger$5 = console;
|
|
5773
5991
|
/**
|
|
5992
|
+
* The id the Dockerfile templates mount the encrypted credentials under:
|
|
5993
|
+
* `RUN --mount=type=secret,id=gkm_credentials`.
|
|
5994
|
+
*/
|
|
5995
|
+
const CREDENTIALS_SECRET_ID = "gkm_credentials";
|
|
5996
|
+
/**
|
|
5774
5997
|
* Get the full image reference
|
|
5775
5998
|
*/
|
|
5776
5999
|
function getImageRef(registry, imageName, tag) {
|
|
@@ -5778,31 +6001,69 @@ function getImageRef(registry, imageName, tag) {
|
|
|
5778
6001
|
return `${imageName}:${tag}`;
|
|
5779
6002
|
}
|
|
5780
6003
|
/**
|
|
6004
|
+
* The `docker build` argv for an image.
|
|
6005
|
+
*
|
|
6006
|
+
* One element per value, so nothing in a ref or a build arg is read by a
|
|
6007
|
+
* shell, and `--opt=value` rather than `--opt value`, so a value that starts
|
|
6008
|
+
* with `-` cannot be taken for one of docker's own flags.
|
|
6009
|
+
*
|
|
6010
|
+
* @internal Exported for testing
|
|
6011
|
+
*/
|
|
6012
|
+
function dockerBuildArgs(options) {
|
|
6013
|
+
const { dockerfilePath, imageRef, buildArgs = [], credentialsFile } = options;
|
|
6014
|
+
return [
|
|
6015
|
+
"build",
|
|
6016
|
+
"--platform=linux/amd64",
|
|
6017
|
+
`--file=${dockerfilePath}`,
|
|
6018
|
+
`--tag=${imageRef}`,
|
|
6019
|
+
...buildArgs.map((arg) => `--build-arg=${arg}`),
|
|
6020
|
+
...credentialsFile ? [`--secret=id=${CREDENTIALS_SECRET_ID},src=${credentialsFile}`] : [],
|
|
6021
|
+
"."
|
|
6022
|
+
];
|
|
6023
|
+
}
|
|
6024
|
+
/**
|
|
6025
|
+
* Write the encrypted credentials where `docker build --secret` reads them.
|
|
6026
|
+
*
|
|
6027
|
+
* Two lines — ciphertext, then IV — so a template can split them with `sed`
|
|
6028
|
+
* in any base image. Owner-only, in a directory of its own, and the returned
|
|
6029
|
+
* cleanup removes both whatever the build did.
|
|
6030
|
+
*
|
|
6031
|
+
* @internal Exported for testing
|
|
6032
|
+
*/
|
|
6033
|
+
async function writeCredentialsFile(credentials) {
|
|
6034
|
+
const dir = await mkdtemp(join(tmpdir(), "gkm-credentials-"));
|
|
6035
|
+
const path = join(dir, CREDENTIALS_SECRET_ID);
|
|
6036
|
+
await writeFile(path, `${credentials.encrypted}\n${credentials.iv}\n`, { mode: 384 });
|
|
6037
|
+
await chmod(path, 384);
|
|
6038
|
+
return {
|
|
6039
|
+
path,
|
|
6040
|
+
cleanup: () => rm(dir, {
|
|
6041
|
+
recursive: true,
|
|
6042
|
+
force: true
|
|
6043
|
+
})
|
|
6044
|
+
};
|
|
6045
|
+
}
|
|
6046
|
+
/**
|
|
5781
6047
|
* Build Docker image
|
|
5782
6048
|
* @param imageRef - Full image reference (registry/name:tag)
|
|
5783
|
-
* @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
|
|
5784
6049
|
* @param buildArgs - Build arguments to pass to docker build
|
|
6050
|
+
* @param credentials - Encrypted credentials, passed as a build secret
|
|
5785
6051
|
*/
|
|
5786
|
-
async function buildImage(imageRef,
|
|
6052
|
+
async function buildImage(imageRef, buildArgs, credentials) {
|
|
5787
6053
|
logger$5.log(`\n🔨 Building Docker image: ${imageRef}`);
|
|
5788
6054
|
const cwd = process.cwd();
|
|
5789
6055
|
logger$5.log(" Generating Dockerfile for the pre-built bundle...");
|
|
5790
6056
|
await dockerCommand({ slim: true });
|
|
5791
6057
|
const dockerfilePath = join(cwd, ".gkm", "docker", "Dockerfile");
|
|
5792
|
-
const
|
|
5793
|
-
const buildArgsString = buildArgs && buildArgs.length > 0 ? buildArgs.map((arg) => `--build-arg "${arg}"`).join(" ") : "";
|
|
6058
|
+
const secret = credentials ? await writeCredentialsFile(credentials) : void 0;
|
|
5794
6059
|
try {
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
].filter(Boolean).join(" ");
|
|
5803
|
-
execSync(cmd, {
|
|
5804
|
-
cwd: buildCwd,
|
|
5805
|
-
stdio: "inherit",
|
|
6060
|
+
await run("docker", dockerBuildArgs({
|
|
6061
|
+
dockerfilePath,
|
|
6062
|
+
imageRef,
|
|
6063
|
+
buildArgs,
|
|
6064
|
+
credentialsFile: secret?.path
|
|
6065
|
+
}), {
|
|
6066
|
+
cwd,
|
|
5806
6067
|
env: {
|
|
5807
6068
|
...process.env,
|
|
5808
6069
|
DOCKER_BUILDKIT: "1"
|
|
@@ -5810,7 +6071,9 @@ async function buildImage(imageRef, _appName, buildArgs) {
|
|
|
5810
6071
|
});
|
|
5811
6072
|
logger$5.log(`✅ Image built: ${imageRef}`);
|
|
5812
6073
|
} catch (error) {
|
|
5813
|
-
throw new
|
|
6074
|
+
throw new DockerBuildFailed(imageRef, error);
|
|
6075
|
+
} finally {
|
|
6076
|
+
await secret?.cleanup();
|
|
5814
6077
|
}
|
|
5815
6078
|
}
|
|
5816
6079
|
/**
|
|
@@ -5819,23 +6082,20 @@ async function buildImage(imageRef, _appName, buildArgs) {
|
|
|
5819
6082
|
async function pushImage(imageRef) {
|
|
5820
6083
|
logger$5.log(`\n☁️ Pushing image: ${imageRef}`);
|
|
5821
6084
|
try {
|
|
5822
|
-
|
|
5823
|
-
cwd: process.cwd(),
|
|
5824
|
-
stdio: "inherit"
|
|
5825
|
-
});
|
|
6085
|
+
await run("docker", ["push", imageRef], { cwd: process.cwd() });
|
|
5826
6086
|
logger$5.log(`✅ Image pushed: ${imageRef}`);
|
|
5827
6087
|
} catch (error) {
|
|
5828
|
-
throw new
|
|
6088
|
+
throw new DockerPushFailed(imageRef, error);
|
|
5829
6089
|
}
|
|
5830
6090
|
}
|
|
5831
6091
|
/**
|
|
5832
6092
|
* Deploy using Docker (build and optionally push image)
|
|
5833
6093
|
*/
|
|
5834
6094
|
async function deployDocker(options) {
|
|
5835
|
-
const { stage, tag, skipPush, masterKey, config, buildArgs } = options;
|
|
6095
|
+
const { stage, tag, skipPush, masterKey, config, buildArgs, credentials } = options;
|
|
5836
6096
|
const imageName = config.imageName;
|
|
5837
|
-
const imageRef = getImageRef(config.registry, imageName, tag);
|
|
5838
|
-
await buildImage(imageRef,
|
|
6097
|
+
const imageRef = validateImageRef(getImageRef(config.registry, imageName, tag));
|
|
6098
|
+
await buildImage(imageRef, buildArgs, credentials);
|
|
5839
6099
|
if (!skipPush) {
|
|
5840
6100
|
if (!config.registry) logger$5.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
|
|
5841
6101
|
else await pushImage(imageRef);
|
|
@@ -5844,12 +6104,7 @@ async function deployDocker(options) {
|
|
|
5844
6104
|
logger$5.log(`\n📋 Deployment details:`);
|
|
5845
6105
|
logger$5.log(` Image: ${imageRef}`);
|
|
5846
6106
|
logger$5.log(` Stage: ${stage}`);
|
|
5847
|
-
if (masterKey) {
|
|
5848
|
-
logger$5.log(`\n🔐 Deploy with this environment variable:`);
|
|
5849
|
-
logger$5.log(` GKM_MASTER_KEY=${masterKey}`);
|
|
5850
|
-
logger$5.log("\n Example docker run:");
|
|
5851
|
-
logger$5.log(` docker run -e GKM_MASTER_KEY=${masterKey} ${imageRef}`);
|
|
5852
|
-
}
|
|
6107
|
+
if (masterKey) logger$5.log(`\n🔐 Run the container with GKM_MASTER_KEY set to the key with fingerprint ${keyFingerprint(masterKey)}.`);
|
|
5853
6108
|
return {
|
|
5854
6109
|
imageRef,
|
|
5855
6110
|
masterKey
|
|
@@ -5874,9 +6129,53 @@ function applicationName(stage, project, app) {
|
|
|
5874
6129
|
//#endregion
|
|
5875
6130
|
//#region src/deploy/dokploy-api.ts
|
|
5876
6131
|
var dokploy_api_exports = /* @__PURE__ */ __exportAll({
|
|
6132
|
+
DEFAULT_DOKPLOY_TIMEOUT_MS: () => DEFAULT_DOKPLOY_TIMEOUT_MS,
|
|
5877
6133
|
DokployApi: () => DokployApi,
|
|
5878
|
-
DokployApiError: () => DokployApiError
|
|
6134
|
+
DokployApiError: () => DokployApiError,
|
|
6135
|
+
DokployRequestTimedOut: () => DokployRequestTimedOut
|
|
5879
6136
|
});
|
|
6137
|
+
/** Per request, so a stalled server fails the deploy, not the CI job. */
|
|
6138
|
+
const DEFAULT_DOKPLOY_TIMEOUT_MS = 3e4;
|
|
6139
|
+
/**
|
|
6140
|
+
* Dokploy accepted the connection but did not answer in time.
|
|
6141
|
+
*
|
|
6142
|
+
* Not retried: a request that arrived and stalled may still be acted on, and
|
|
6143
|
+
* sending it again could repeat a deploy or a create the server is already
|
|
6144
|
+
* doing.
|
|
6145
|
+
*/
|
|
6146
|
+
var DokployRequestTimedOut = class extends Error {
|
|
6147
|
+
baseUrl;
|
|
6148
|
+
endpoint;
|
|
6149
|
+
timeoutMs;
|
|
6150
|
+
constructor(baseUrl, endpoint, timeoutMs) {
|
|
6151
|
+
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.`);
|
|
6152
|
+
this.baseUrl = baseUrl;
|
|
6153
|
+
this.endpoint = endpoint;
|
|
6154
|
+
this.timeoutMs = timeoutMs;
|
|
6155
|
+
this.name = "DokployRequestTimedOut";
|
|
6156
|
+
}
|
|
6157
|
+
};
|
|
6158
|
+
/**
|
|
6159
|
+
* Waits `ms` between retries, or rejects with the signal's reason the moment
|
|
6160
|
+
* it aborts — a caller who gave up should not sit through the backoff.
|
|
6161
|
+
*/
|
|
6162
|
+
function pause(ms, signal) {
|
|
6163
|
+
return new Promise((resolve, reject) => {
|
|
6164
|
+
if (signal?.aborted) {
|
|
6165
|
+
reject(signal.reason);
|
|
6166
|
+
return;
|
|
6167
|
+
}
|
|
6168
|
+
const onAbort = () => {
|
|
6169
|
+
clearTimeout(timer);
|
|
6170
|
+
reject(signal?.reason);
|
|
6171
|
+
};
|
|
6172
|
+
const timer = setTimeout(() => {
|
|
6173
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6174
|
+
resolve();
|
|
6175
|
+
}, ms);
|
|
6176
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6177
|
+
});
|
|
6178
|
+
}
|
|
5880
6179
|
var DokployApiError = class extends Error {
|
|
5881
6180
|
status;
|
|
5882
6181
|
statusText;
|
|
@@ -5895,9 +6194,13 @@ var DokployApiError = class extends Error {
|
|
|
5895
6194
|
var DokployApi = class {
|
|
5896
6195
|
baseUrl;
|
|
5897
6196
|
token;
|
|
6197
|
+
timeoutMs;
|
|
6198
|
+
signal;
|
|
5898
6199
|
constructor(options) {
|
|
5899
6200
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
5900
6201
|
this.token = options.token;
|
|
6202
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
6203
|
+
this.signal = options.signal;
|
|
5901
6204
|
}
|
|
5902
6205
|
/**
|
|
5903
6206
|
* Make a GET request to the Dokploy API
|
|
@@ -5917,26 +6220,51 @@ var DokployApi = class {
|
|
|
5917
6220
|
async request(method, endpoint, body) {
|
|
5918
6221
|
const url = `${this.baseUrl}/api/${endpoint}`;
|
|
5919
6222
|
let response;
|
|
6223
|
+
let timeout;
|
|
5920
6224
|
let lastError;
|
|
5921
|
-
for (let attempt = 1; attempt <= 3; attempt++)
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
6225
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
6226
|
+
this.signal?.throwIfAborted();
|
|
6227
|
+
timeout = AbortSignal.timeout(this.timeoutMs);
|
|
6228
|
+
try {
|
|
6229
|
+
response = await fetch(url, {
|
|
6230
|
+
method,
|
|
6231
|
+
headers: {
|
|
6232
|
+
"Content-Type": "application/json",
|
|
6233
|
+
"x-api-key": this.token
|
|
6234
|
+
},
|
|
6235
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
6236
|
+
signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
|
|
6237
|
+
});
|
|
6238
|
+
break;
|
|
6239
|
+
} catch (error) {
|
|
6240
|
+
const stopped = this.stopped(timeout, endpoint);
|
|
6241
|
+
if (stopped) throw stopped;
|
|
6242
|
+
lastError = error;
|
|
6243
|
+
if (attempt === 3) break;
|
|
6244
|
+
await pause(attempt * 750, this.signal);
|
|
6245
|
+
}
|
|
5935
6246
|
}
|
|
5936
|
-
if (!response) {
|
|
6247
|
+
if (!response || !timeout) {
|
|
5937
6248
|
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
5938
6249
|
throw new DokployApiError(`Could not reach Dokploy at ${this.baseUrl} (${endpoint}): ${detail}`, 0, "Network error");
|
|
5939
6250
|
}
|
|
6251
|
+
try {
|
|
6252
|
+
return await this.read(response);
|
|
6253
|
+
} catch (error) {
|
|
6254
|
+
throw this.stopped(timeout, endpoint) ?? error;
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
/**
|
|
6258
|
+
* What a request cut short should reject with: the caller's own reason
|
|
6259
|
+
* when they aborted, `DokployRequestTimedOut` when the deadline passed,
|
|
6260
|
+
* nothing when it was neither.
|
|
6261
|
+
*/
|
|
6262
|
+
stopped(timeout, endpoint) {
|
|
6263
|
+
if (this.signal?.aborted) return this.signal.reason;
|
|
6264
|
+
if (timeout.aborted) return new DokployRequestTimedOut(this.baseUrl, endpoint, this.timeoutMs);
|
|
6265
|
+
}
|
|
6266
|
+
/** Turns a response into its JSON body, or a `DokployApiError`. */
|
|
6267
|
+
async read(response) {
|
|
5940
6268
|
if (!response.ok) {
|
|
5941
6269
|
let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
|
|
5942
6270
|
let issues;
|
|
@@ -6725,7 +7053,11 @@ function isStateProvider(value) {
|
|
|
6725
7053
|
*
|
|
6726
7054
|
* - 'local': LocalStateProvider (default)
|
|
6727
7055
|
* - 'ssm': CachedStateProvider with SSM as source of truth
|
|
7056
|
+
* - 's3': S3StateStore behind the provider interface
|
|
6728
7057
|
* - Custom: Use provided StateProvider implementation
|
|
7058
|
+
*
|
|
7059
|
+
* Deploy moves to `createStateStore` (locks, versions, per-resource records);
|
|
7060
|
+
* this stays for the call sites that read and write whole states.
|
|
6729
7061
|
*/
|
|
6730
7062
|
async function createStateProvider(options) {
|
|
6731
7063
|
const { config, workspaceRoot, workspaceName } = options;
|
|
@@ -6733,6 +7065,11 @@ async function createStateProvider(options) {
|
|
|
6733
7065
|
const { LocalStateProvider } = await import("./LocalStateProvider-D7QyJoLM.mjs");
|
|
6734
7066
|
return new LocalStateProvider(workspaceRoot);
|
|
6735
7067
|
}
|
|
7068
|
+
const { isStateStore } = await Promise.resolve().then(() => StateStore_exports);
|
|
7069
|
+
if (isStateStore(config.provider)) {
|
|
7070
|
+
const { StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7071
|
+
return new StateStoreProvider(config.provider);
|
|
7072
|
+
}
|
|
6736
7073
|
if (isStateProvider(config.provider)) return config.provider;
|
|
6737
7074
|
const provider = config.provider;
|
|
6738
7075
|
if (provider === "local") {
|
|
@@ -6740,7 +7077,10 @@ async function createStateProvider(options) {
|
|
|
6740
7077
|
return new LocalStateProvider(workspaceRoot);
|
|
6741
7078
|
}
|
|
6742
7079
|
if (provider === "ssm") {
|
|
6743
|
-
if (!workspaceName)
|
|
7080
|
+
if (!workspaceName) {
|
|
7081
|
+
const { StateStoreNeedsWorkspaceName } = await Promise.resolve().then(() => StateStore_exports);
|
|
7082
|
+
throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7083
|
+
}
|
|
6744
7084
|
const { LocalStateProvider } = await import("./LocalStateProvider-D7QyJoLM.mjs");
|
|
6745
7085
|
const { SSMStateProvider } = await import("./SSMStateProvider-B0Hu1MKZ.mjs");
|
|
6746
7086
|
const { CachedStateProvider } = await Promise.resolve().then(() => CachedStateProvider_exports);
|
|
@@ -6752,47 +7092,12 @@ async function createStateProvider(options) {
|
|
|
6752
7092
|
profile: ssmConfig.profile
|
|
6753
7093
|
}), local);
|
|
6754
7094
|
}
|
|
6755
|
-
|
|
6756
|
-
}
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
generateDefineOptions: () => generateDefineOptions
|
|
6762
|
-
});
|
|
6763
|
-
/** AES-256-GCM configuration */
|
|
6764
|
-
const ALGORITHM = "aes-256-gcm";
|
|
6765
|
-
const KEY_LENGTH = 32;
|
|
6766
|
-
const IV_LENGTH = 12;
|
|
6767
|
-
/**
|
|
6768
|
-
* Encrypt secrets for embedding in a bundle.
|
|
6769
|
-
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
6770
|
-
*
|
|
6771
|
-
* @param secrets - Key-value pairs to encrypt
|
|
6772
|
-
* @returns Encrypted payload with ephemeral master key
|
|
6773
|
-
*/
|
|
6774
|
-
function encryptSecrets(secrets) {
|
|
6775
|
-
const masterKey = randomBytes(KEY_LENGTH);
|
|
6776
|
-
const iv = randomBytes(IV_LENGTH);
|
|
6777
|
-
const plaintext = JSON.stringify(secrets);
|
|
6778
|
-
const cipher = createCipheriv(ALGORITHM, masterKey, iv);
|
|
6779
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
6780
|
-
const authTag = cipher.getAuthTag();
|
|
6781
|
-
return {
|
|
6782
|
-
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
6783
|
-
iv: iv.toString("hex"),
|
|
6784
|
-
masterKey: masterKey.toString("hex")
|
|
6785
|
-
};
|
|
6786
|
-
}
|
|
6787
|
-
/**
|
|
6788
|
-
* Generate the define options for tsdown/esbuild.
|
|
6789
|
-
* These will be injected at build time.
|
|
6790
|
-
*/
|
|
6791
|
-
function generateDefineOptions(payload) {
|
|
6792
|
-
return {
|
|
6793
|
-
__GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
|
|
6794
|
-
__GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
|
|
6795
|
-
};
|
|
7095
|
+
if (provider === "s3") {
|
|
7096
|
+
const { createStateStore, StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7097
|
+
return new StateStoreProvider(await createStateStore(options));
|
|
7098
|
+
}
|
|
7099
|
+
const { UnknownStateProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7100
|
+
throw new UnknownStateProvider(config);
|
|
6796
7101
|
}
|
|
6797
7102
|
//#endregion
|
|
6798
7103
|
//#region src/deploy/secrets.ts
|
|
@@ -7750,12 +8055,11 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7750
8055
|
}
|
|
7751
8056
|
setApplicationId(state, appName, application.applicationId);
|
|
7752
8057
|
const appSecrets = encryptedSecrets.get(appName);
|
|
7753
|
-
const
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
}
|
|
8058
|
+
const credentials = appSecrets && appSecrets.secretCount > 0 ? {
|
|
8059
|
+
encrypted: appSecrets.payload.encrypted,
|
|
8060
|
+
iv: appSecrets.payload.iv
|
|
8061
|
+
} : void 0;
|
|
8062
|
+
if (appSecrets && credentials) logger$4.log(` Encrypted ${appSecrets.secretCount} secrets`);
|
|
7759
8063
|
const imageName = `${workspace.name}-${appName}`;
|
|
7760
8064
|
const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
|
|
7761
8065
|
logger$4.log(` Building Docker image: ${imageRef}`);
|
|
@@ -7768,7 +8072,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7768
8072
|
imageName,
|
|
7769
8073
|
appName
|
|
7770
8074
|
},
|
|
7771
|
-
|
|
8075
|
+
credentials
|
|
7772
8076
|
});
|
|
7773
8077
|
const backendHost = resolveHost(appName, app, stage, workspace.deploy?.domains, false);
|
|
7774
8078
|
const dependencyUrls = {};
|
|
@@ -8150,6 +8454,11 @@ async function deployListCommand(options) {
|
|
|
8150
8454
|
var CachedStateProvider_exports = /* @__PURE__ */ __exportAll({ CachedStateProvider: () => CachedStateProvider });
|
|
8151
8455
|
/**
|
|
8152
8456
|
* Cached state provider that wraps a remote provider with local cache.
|
|
8457
|
+
*
|
|
8458
|
+
* @deprecated for deploys: a read prefers the local copy, so a stale one wins
|
|
8459
|
+
* over the remote without warning, and a write overwrites the remote blindly.
|
|
8460
|
+
* Deploy uses `createStateStore`, which reads SSM directly and writes
|
|
8461
|
+
* conditionally. Kept for `gkm state:pull | push | diff`.
|
|
8153
8462
|
*/
|
|
8154
8463
|
var CachedStateProvider = class {
|
|
8155
8464
|
remote;
|
|
@@ -8200,12 +8509,421 @@ var CachedStateProvider = class {
|
|
|
8200
8509
|
}
|
|
8201
8510
|
};
|
|
8202
8511
|
//#endregion
|
|
8512
|
+
//#region src/deploy/StateStore.ts
|
|
8513
|
+
/**
|
|
8514
|
+
* Deploy state store.
|
|
8515
|
+
*
|
|
8516
|
+
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
8517
|
+
* two runs of the same stage from overwriting each other and nowhere to record
|
|
8518
|
+
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
8519
|
+
* deploy needs to be safe against crashes and concurrent runs:
|
|
8520
|
+
*
|
|
8521
|
+
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
8522
|
+
* holder, instead of racing the first.
|
|
8523
|
+
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
8524
|
+
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
8525
|
+
* underneath it, so nothing is lost silently even without the lock.
|
|
8526
|
+
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
8527
|
+
* run that dies half way leaves behind the ids of everything it created.
|
|
8528
|
+
*
|
|
8529
|
+
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
8530
|
+
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
8531
|
+
* working but cannot lock.
|
|
8532
|
+
*/
|
|
8533
|
+
var StateStore_exports = /* @__PURE__ */ __exportAll({
|
|
8534
|
+
DocumentStateStore: () => DocumentStateStore,
|
|
8535
|
+
STATE_SCHEMA_VERSION: () => 2,
|
|
8536
|
+
StageStateMissing: () => StageStateMissing,
|
|
8537
|
+
StateLocked: () => StateLocked,
|
|
8538
|
+
StateSchemaTooNew: () => StateSchemaTooNew,
|
|
8539
|
+
StateStoreBusy: () => StateStoreBusy,
|
|
8540
|
+
StateStoreNeedsWorkspaceName: () => StateStoreNeedsWorkspaceName,
|
|
8541
|
+
StateStoreProvider: () => StateStoreProvider,
|
|
8542
|
+
StateStoreWithoutLocking: () => StateStoreWithoutLocking,
|
|
8543
|
+
StateUnreadable: () => StateUnreadable,
|
|
8544
|
+
StateVersionConflict: () => StateVersionConflict,
|
|
8545
|
+
UnknownStateProvider: () => UnknownStateProvider,
|
|
8546
|
+
contentVersion: () => contentVersion,
|
|
8547
|
+
createStateStore: () => createStateStore,
|
|
8548
|
+
decodeDocument: () => decodeDocument,
|
|
8549
|
+
encodeDocument: () => encodeDocument,
|
|
8550
|
+
isStateStore: () => isStateStore,
|
|
8551
|
+
migrateV1: () => migrateV1,
|
|
8552
|
+
newLockHolder: () => newLockHolder
|
|
8553
|
+
});
|
|
8554
|
+
var StateLocked = class extends Error {
|
|
8555
|
+
stage;
|
|
8556
|
+
holder;
|
|
8557
|
+
location;
|
|
8558
|
+
constructor(stage, holder, location) {
|
|
8559
|
+
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";
|
|
8560
|
+
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}\`.`);
|
|
8561
|
+
this.stage = stage;
|
|
8562
|
+
this.holder = holder;
|
|
8563
|
+
this.location = location;
|
|
8564
|
+
this.name = "StateLocked";
|
|
8565
|
+
}
|
|
8566
|
+
};
|
|
8567
|
+
var StateVersionConflict = class extends Error {
|
|
8568
|
+
stage;
|
|
8569
|
+
expectedVersion;
|
|
8570
|
+
actualVersion;
|
|
8571
|
+
constructor(stage, expectedVersion, actualVersion) {
|
|
8572
|
+
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.`);
|
|
8573
|
+
this.stage = stage;
|
|
8574
|
+
this.expectedVersion = expectedVersion;
|
|
8575
|
+
this.actualVersion = actualVersion;
|
|
8576
|
+
this.name = "StateVersionConflict";
|
|
8577
|
+
}
|
|
8578
|
+
};
|
|
8579
|
+
var StageStateMissing = class extends Error {
|
|
8580
|
+
stage;
|
|
8581
|
+
constructor(stage) {
|
|
8582
|
+
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
8583
|
+
this.stage = stage;
|
|
8584
|
+
this.name = "StageStateMissing";
|
|
8585
|
+
}
|
|
8586
|
+
};
|
|
8587
|
+
var StateUnreadable = class extends Error {
|
|
8588
|
+
stage;
|
|
8589
|
+
location;
|
|
8590
|
+
constructor(stage, location, cause) {
|
|
8591
|
+
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 });
|
|
8592
|
+
this.stage = stage;
|
|
8593
|
+
this.location = location;
|
|
8594
|
+
this.name = "StateUnreadable";
|
|
8595
|
+
}
|
|
8596
|
+
};
|
|
8597
|
+
var StateSchemaTooNew = class extends Error {
|
|
8598
|
+
stage;
|
|
8599
|
+
schemaVersion;
|
|
8600
|
+
constructor(stage, schemaVersion) {
|
|
8601
|
+
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.`);
|
|
8602
|
+
this.stage = stage;
|
|
8603
|
+
this.schemaVersion = schemaVersion;
|
|
8604
|
+
this.name = "StateSchemaTooNew";
|
|
8605
|
+
}
|
|
8606
|
+
};
|
|
8607
|
+
/**
|
|
8608
|
+
* Not thrown: emitted (once per store) when a custom `StateProvider` stands
|
|
8609
|
+
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
8610
|
+
* says so.
|
|
8611
|
+
*/
|
|
8612
|
+
var StateStoreWithoutLocking = class extends Error {
|
|
8613
|
+
provider;
|
|
8614
|
+
constructor(provider) {
|
|
8615
|
+
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.`);
|
|
8616
|
+
this.provider = provider;
|
|
8617
|
+
this.name = "StateStoreWithoutLocking";
|
|
8618
|
+
}
|
|
8619
|
+
};
|
|
8620
|
+
var StateStoreNeedsWorkspaceName = class extends Error {
|
|
8621
|
+
provider;
|
|
8622
|
+
constructor(provider) {
|
|
8623
|
+
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
8624
|
+
this.provider = provider;
|
|
8625
|
+
this.name = "StateStoreNeedsWorkspaceName";
|
|
8626
|
+
}
|
|
8627
|
+
};
|
|
8628
|
+
var UnknownStateProvider = class extends Error {
|
|
8629
|
+
config;
|
|
8630
|
+
constructor(config) {
|
|
8631
|
+
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
8632
|
+
this.config = config;
|
|
8633
|
+
this.name = "UnknownStateProvider";
|
|
8634
|
+
}
|
|
8635
|
+
};
|
|
8636
|
+
/** A lock or write that could not get at the state backend in time. */
|
|
8637
|
+
var StateStoreBusy = class extends Error {
|
|
8638
|
+
stage;
|
|
8639
|
+
location;
|
|
8640
|
+
constructor(stage, location) {
|
|
8641
|
+
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
8642
|
+
this.stage = stage;
|
|
8643
|
+
this.location = location;
|
|
8644
|
+
this.name = "StateStoreBusy";
|
|
8645
|
+
}
|
|
8646
|
+
};
|
|
8647
|
+
function decodeDocument(stage, body, location) {
|
|
8648
|
+
let parsed;
|
|
8649
|
+
try {
|
|
8650
|
+
parsed = JSON.parse(body);
|
|
8651
|
+
} catch (error) {
|
|
8652
|
+
throw new StateUnreadable(stage, location, error);
|
|
8653
|
+
}
|
|
8654
|
+
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
8655
|
+
const schemaVersion = parsed.schemaVersion;
|
|
8656
|
+
if (schemaVersion === void 0) return {
|
|
8657
|
+
schemaVersion: 1,
|
|
8658
|
+
state: parsed
|
|
8659
|
+
};
|
|
8660
|
+
if (schemaVersion === 2) return {
|
|
8661
|
+
schemaVersion: 2,
|
|
8662
|
+
document: parsed
|
|
8663
|
+
};
|
|
8664
|
+
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
8665
|
+
}
|
|
8666
|
+
function encodeDocument(document) {
|
|
8667
|
+
return JSON.stringify(document, null, 2);
|
|
8668
|
+
}
|
|
8669
|
+
/**
|
|
8670
|
+
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
8671
|
+
* so the first journaled deploy after an upgrade adopts them rather than
|
|
8672
|
+
* treating them as unknown.
|
|
8673
|
+
*/
|
|
8674
|
+
function migrateV1(stage, state) {
|
|
8675
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8676
|
+
const resources = {};
|
|
8677
|
+
const seed = (type, name, id) => {
|
|
8678
|
+
if (!id) return;
|
|
8679
|
+
const key = name ? `${type}:${name}` : type;
|
|
8680
|
+
resources[key] = {
|
|
8681
|
+
key,
|
|
8682
|
+
type,
|
|
8683
|
+
id,
|
|
8684
|
+
status: "ready",
|
|
8685
|
+
updatedAt: now
|
|
8686
|
+
};
|
|
8687
|
+
};
|
|
8688
|
+
seed("project", null, state.projectId);
|
|
8689
|
+
seed("environment", null, state.environmentId);
|
|
8690
|
+
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
8691
|
+
seed("postgres", null, state.services?.postgresId);
|
|
8692
|
+
seed("redis", null, state.services?.redisId);
|
|
8693
|
+
seed("backup-destination", null, state.backups?.destinationId);
|
|
8694
|
+
return {
|
|
8695
|
+
schemaVersion: 2,
|
|
8696
|
+
stage,
|
|
8697
|
+
serial: 1,
|
|
8698
|
+
state,
|
|
8699
|
+
resources,
|
|
8700
|
+
updatedAt: now
|
|
8701
|
+
};
|
|
8702
|
+
}
|
|
8703
|
+
function newLockHolder(options) {
|
|
8704
|
+
let owner = "unknown";
|
|
8705
|
+
try {
|
|
8706
|
+
owner = userInfo().username;
|
|
8707
|
+
} catch {}
|
|
8708
|
+
return {
|
|
8709
|
+
id: randomUUID(),
|
|
8710
|
+
owner,
|
|
8711
|
+
host: hostname(),
|
|
8712
|
+
pid: process.pid,
|
|
8713
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8714
|
+
operation: options?.operation
|
|
8715
|
+
};
|
|
8716
|
+
}
|
|
8717
|
+
function contentVersion(body) {
|
|
8718
|
+
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
8719
|
+
}
|
|
8720
|
+
/**
|
|
8721
|
+
* A store over a backend that can read and conditionally write one blob per
|
|
8722
|
+
* stage, and create a lock object only if it does not exist. Migration and
|
|
8723
|
+
* resource records are the same for every such backend, so they live here.
|
|
8724
|
+
*/
|
|
8725
|
+
var DocumentStateStore = class {
|
|
8726
|
+
async lock(stage, options) {
|
|
8727
|
+
const holder = newLockHolder(options);
|
|
8728
|
+
await this.createLock(stage, holder);
|
|
8729
|
+
let released = false;
|
|
8730
|
+
return {
|
|
8731
|
+
stage,
|
|
8732
|
+
holder,
|
|
8733
|
+
release: async () => {
|
|
8734
|
+
if (released) return;
|
|
8735
|
+
released = true;
|
|
8736
|
+
await this.removeLock(stage, holder);
|
|
8737
|
+
}
|
|
8738
|
+
};
|
|
8739
|
+
}
|
|
8740
|
+
async forceUnlock(stage) {
|
|
8741
|
+
const holder = await this.readLock(stage);
|
|
8742
|
+
await this.removeLock(stage, null);
|
|
8743
|
+
return holder;
|
|
8744
|
+
}
|
|
8745
|
+
async read(stage) {
|
|
8746
|
+
const current = await this.readDocument(stage);
|
|
8747
|
+
if (!current) return null;
|
|
8748
|
+
return {
|
|
8749
|
+
state: current.document.state,
|
|
8750
|
+
resources: current.document.resources,
|
|
8751
|
+
version: current.version
|
|
8752
|
+
};
|
|
8753
|
+
}
|
|
8754
|
+
async write(stage, state, options) {
|
|
8755
|
+
if (options.expectedVersion === null) {
|
|
8756
|
+
const document = {
|
|
8757
|
+
schemaVersion: 2,
|
|
8758
|
+
stage,
|
|
8759
|
+
serial: 1,
|
|
8760
|
+
state,
|
|
8761
|
+
resources: {},
|
|
8762
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8763
|
+
};
|
|
8764
|
+
return this.writeRaw(stage, encodeDocument(document), null);
|
|
8765
|
+
}
|
|
8766
|
+
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
8767
|
+
...document,
|
|
8768
|
+
state
|
|
8769
|
+
}));
|
|
8770
|
+
}
|
|
8771
|
+
async putResource(stage, record, options) {
|
|
8772
|
+
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
8773
|
+
...document,
|
|
8774
|
+
resources: {
|
|
8775
|
+
...document.resources,
|
|
8776
|
+
[record.key]: {
|
|
8777
|
+
...record,
|
|
8778
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
}));
|
|
8782
|
+
}
|
|
8783
|
+
async deleteResource(stage, key, options) {
|
|
8784
|
+
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
8785
|
+
const { [key]: _removed, ...resources } = document.resources;
|
|
8786
|
+
return {
|
|
8787
|
+
...document,
|
|
8788
|
+
resources
|
|
8789
|
+
};
|
|
8790
|
+
});
|
|
8791
|
+
}
|
|
8792
|
+
async mutate(stage, expectedVersion, change) {
|
|
8793
|
+
const current = await this.readDocument(stage);
|
|
8794
|
+
if (!current) {
|
|
8795
|
+
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
8796
|
+
throw new StageStateMissing(stage);
|
|
8797
|
+
}
|
|
8798
|
+
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
8799
|
+
const next = change(current.document);
|
|
8800
|
+
next.serial = current.document.serial + 1;
|
|
8801
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8802
|
+
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
8803
|
+
}
|
|
8804
|
+
/**
|
|
8805
|
+
* Reads the stage's document, migrating v1 in place on the way. The
|
|
8806
|
+
* migration is itself a conditional write: if another run migrated (or
|
|
8807
|
+
* wrote) first, its result is read instead.
|
|
8808
|
+
*/
|
|
8809
|
+
async readDocument(stage, attempt = 0) {
|
|
8810
|
+
const raw = await this.readRaw(stage);
|
|
8811
|
+
if (!raw) return null;
|
|
8812
|
+
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
8813
|
+
if (decoded.schemaVersion === 2) return {
|
|
8814
|
+
document: decoded.document,
|
|
8815
|
+
version: raw.version
|
|
8816
|
+
};
|
|
8817
|
+
await this.writeV1Backup(stage, raw.body);
|
|
8818
|
+
const document = migrateV1(stage, decoded.state);
|
|
8819
|
+
try {
|
|
8820
|
+
return {
|
|
8821
|
+
document,
|
|
8822
|
+
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
8823
|
+
};
|
|
8824
|
+
} catch (error) {
|
|
8825
|
+
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
8826
|
+
throw error;
|
|
8827
|
+
}
|
|
8828
|
+
}
|
|
8829
|
+
};
|
|
8830
|
+
function isStateStore(value) {
|
|
8831
|
+
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
8832
|
+
}
|
|
8833
|
+
/**
|
|
8834
|
+
* The store for a workspace's `state` config — the same config
|
|
8835
|
+
* `createStateProvider` reads:
|
|
8836
|
+
*
|
|
8837
|
+
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
8838
|
+
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
8839
|
+
* a stale local copy can never win over it
|
|
8840
|
+
* - `'s3'`: `S3StateStore`
|
|
8841
|
+
* - a `StateStore` object: used as is
|
|
8842
|
+
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
8843
|
+
*/
|
|
8844
|
+
async function createStateStore(options) {
|
|
8845
|
+
const { config, workspaceRoot, workspaceName } = options;
|
|
8846
|
+
if (!config || config.provider === "local") {
|
|
8847
|
+
const { LocalStateStore } = await import("./LocalStateStore-BupdV-VN.mjs");
|
|
8848
|
+
return new LocalStateStore(workspaceRoot);
|
|
8849
|
+
}
|
|
8850
|
+
if (isStateStore(config.provider)) return config.provider;
|
|
8851
|
+
if (isStateProvider(config.provider)) {
|
|
8852
|
+
const { LegacyStateStore } = await import("./LegacyStateStore-ByRWQ9s1.mjs");
|
|
8853
|
+
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
8854
|
+
}
|
|
8855
|
+
if (config.provider === "ssm") {
|
|
8856
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
8857
|
+
const { SSMStateStore } = await import("./SSMStateStore-oda9xOGc.mjs");
|
|
8858
|
+
const ssm = config;
|
|
8859
|
+
return SSMStateStore.create({
|
|
8860
|
+
workspaceName,
|
|
8861
|
+
region: ssm.region,
|
|
8862
|
+
profile: ssm.profile
|
|
8863
|
+
});
|
|
8864
|
+
}
|
|
8865
|
+
if (config.provider === "s3") {
|
|
8866
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
8867
|
+
const { S3StateStore } = await import("./S3StateStore-FhzyENrN.mjs");
|
|
8868
|
+
const s3 = config;
|
|
8869
|
+
return S3StateStore.create({
|
|
8870
|
+
workspaceName,
|
|
8871
|
+
bucket: s3.bucket,
|
|
8872
|
+
prefix: s3.prefix,
|
|
8873
|
+
region: s3.region,
|
|
8874
|
+
profile: s3.profile
|
|
8875
|
+
});
|
|
8876
|
+
}
|
|
8877
|
+
throw new UnknownStateProvider(config);
|
|
8878
|
+
}
|
|
8879
|
+
/**
|
|
8880
|
+
* A store behind the `StateProvider` interface, for the call sites that still
|
|
8881
|
+
* read and write whole states (deploy, until it journals). A write is still
|
|
8882
|
+
* conditional on the version this provider last read.
|
|
8883
|
+
*/
|
|
8884
|
+
var StateStoreProvider = class {
|
|
8885
|
+
store;
|
|
8886
|
+
versions = /* @__PURE__ */ new Map();
|
|
8887
|
+
constructor(store) {
|
|
8888
|
+
this.store = store;
|
|
8889
|
+
}
|
|
8890
|
+
async read(stage) {
|
|
8891
|
+
const stored = await this.store.read(stage);
|
|
8892
|
+
this.versions.set(stage, stored?.version ?? null);
|
|
8893
|
+
return stored?.state ?? null;
|
|
8894
|
+
}
|
|
8895
|
+
async write(stage, state) {
|
|
8896
|
+
const expectedVersion = this.versions.has(stage) ? this.versions.get(stage) ?? null : (await this.store.read(stage))?.version ?? null;
|
|
8897
|
+
state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8898
|
+
const version = await this.store.write(stage, state, { expectedVersion });
|
|
8899
|
+
this.versions.set(stage, version);
|
|
8900
|
+
}
|
|
8901
|
+
};
|
|
8902
|
+
//#endregion
|
|
8203
8903
|
//#region src/deploy/state-commands.ts
|
|
8204
8904
|
/**
|
|
8205
8905
|
* State Management CLI Commands
|
|
8206
8906
|
*
|
|
8207
8907
|
* Commands for managing deployment state across local and remote providers.
|
|
8208
8908
|
*/
|
|
8909
|
+
/** What `state:show` prints in place of a secret. */
|
|
8910
|
+
const MASKED = "********";
|
|
8911
|
+
/**
|
|
8912
|
+
* A copy of `state` with every secret replaced by `MASKED`: database
|
|
8913
|
+
* passwords, generated secrets and the backup IAM keys. `state:show` is run
|
|
8914
|
+
* to look up ids, in terminals and CI logs; nothing it prints should be
|
|
8915
|
+
* usable as a credential.
|
|
8916
|
+
*/
|
|
8917
|
+
function maskStateSecrets(state) {
|
|
8918
|
+
const masked = structuredClone(state);
|
|
8919
|
+
for (const credentials of Object.values(masked.appCredentials ?? {})) credentials.dbPassword = MASKED;
|
|
8920
|
+
for (const secrets of Object.values(masked.generatedSecrets ?? {})) for (const name of Object.keys(secrets)) secrets[name] = MASKED;
|
|
8921
|
+
if (masked.backups) {
|
|
8922
|
+
masked.backups.iamAccessKeyId = MASKED;
|
|
8923
|
+
masked.backups.iamSecretAccessKey = MASKED;
|
|
8924
|
+
}
|
|
8925
|
+
return masked;
|
|
8926
|
+
}
|
|
8209
8927
|
/**
|
|
8210
8928
|
* Pull state from remote to local.
|
|
8211
8929
|
* `gkm state:pull --stage=<stage>`
|
|
@@ -8268,19 +8986,41 @@ async function statePushCommand(options) {
|
|
|
8268
8986
|
*/
|
|
8269
8987
|
async function stateShowCommand(options) {
|
|
8270
8988
|
const { workspace } = await loadWorkspaceConfig();
|
|
8271
|
-
const
|
|
8989
|
+
const stored = await (await createStateProvider({
|
|
8272
8990
|
config: workspace.state,
|
|
8273
8991
|
workspaceRoot: workspace.root,
|
|
8274
8992
|
workspaceName: workspace.name
|
|
8275
8993
|
})).read(options.stage);
|
|
8276
|
-
if (!
|
|
8994
|
+
if (!stored) {
|
|
8277
8995
|
console.log(`No state found for stage: ${options.stage}`);
|
|
8278
8996
|
return;
|
|
8279
8997
|
}
|
|
8998
|
+
const state = maskStateSecrets(stored);
|
|
8280
8999
|
if (options.json) console.log(JSON.stringify(state, null, 2));
|
|
8281
9000
|
else printStateDetails(state);
|
|
8282
9001
|
}
|
|
8283
9002
|
/**
|
|
9003
|
+
* Release a stage's deploy lock, whoever holds it.
|
|
9004
|
+
* `gkm state:unlock --stage=<stage>`
|
|
9005
|
+
*
|
|
9006
|
+
* For a run that crashed with the lock held. Releasing a lock a live run
|
|
9007
|
+
* holds lets a second run race it, so the holder is printed for the caller
|
|
9008
|
+
* to check.
|
|
9009
|
+
*/
|
|
9010
|
+
async function stateUnlockCommand(options) {
|
|
9011
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
9012
|
+
const holder = await (await createStateStore({
|
|
9013
|
+
config: workspace.state,
|
|
9014
|
+
workspaceRoot: workspace.root,
|
|
9015
|
+
workspaceName: workspace.name
|
|
9016
|
+
})).forceUnlock(options.stage);
|
|
9017
|
+
if (!holder) {
|
|
9018
|
+
console.log(`Stage ${options.stage} was not locked.`);
|
|
9019
|
+
return;
|
|
9020
|
+
}
|
|
9021
|
+
console.log(`Released the lock on stage ${options.stage}, held by ${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}.`);
|
|
9022
|
+
}
|
|
9023
|
+
/**
|
|
8284
9024
|
* Compare local and remote state.
|
|
8285
9025
|
* `gkm state:diff --stage=<stage>`
|
|
8286
9026
|
*/
|
|
@@ -8364,6 +9104,26 @@ function printStateDetails(state) {
|
|
|
8364
9104
|
if (state.services.postgresId) console.log(` Postgres: ${state.services.postgresId}`);
|
|
8365
9105
|
if (state.services.redisId) console.log(` Redis: ${state.services.redisId}`);
|
|
8366
9106
|
}
|
|
9107
|
+
const credentials = Object.entries(state.appCredentials ?? {});
|
|
9108
|
+
if (credentials.length > 0) {
|
|
9109
|
+
console.log("");
|
|
9110
|
+
console.log("Database Credentials:");
|
|
9111
|
+
for (const [app, { dbUser, dbPassword }] of credentials) console.log(` ${app}: ${dbUser} / ${dbPassword}`);
|
|
9112
|
+
}
|
|
9113
|
+
const generated = Object.entries(state.generatedSecrets ?? {});
|
|
9114
|
+
if (generated.length > 0) {
|
|
9115
|
+
console.log("");
|
|
9116
|
+
console.log("Generated Secrets:");
|
|
9117
|
+
for (const [app, secrets] of generated) for (const [name, value] of Object.entries(secrets)) console.log(` ${app}.${name}: ${value}`);
|
|
9118
|
+
}
|
|
9119
|
+
if (state.backups) {
|
|
9120
|
+
console.log("");
|
|
9121
|
+
console.log("Backups:");
|
|
9122
|
+
console.log(` Bucket: ${state.backups.bucketName}`);
|
|
9123
|
+
console.log(` IAM User: ${state.backups.iamUserName}`);
|
|
9124
|
+
console.log(` IAM Access Key: ${state.backups.iamAccessKeyId}`);
|
|
9125
|
+
console.log(` IAM Secret Key: ${state.backups.iamSecretAccessKey}`);
|
|
9126
|
+
}
|
|
8367
9127
|
if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
|
|
8368
9128
|
console.log("");
|
|
8369
9129
|
console.log("DNS Verified:");
|
|
@@ -9368,26 +10128,26 @@ function loadPackageJson() {
|
|
|
9368
10128
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
9369
10129
|
*/
|
|
9370
10130
|
const GEEKMIDAS_VERSIONS = {
|
|
9371
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
9372
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
9373
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
9374
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
9375
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
9376
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
9377
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
9378
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
9379
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
9380
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
9381
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
9382
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
9383
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
9384
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
9385
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
9386
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
9387
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
9388
|
-
"@geekmidas/studio": "~10.0.0-alpha.
|
|
9389
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
9390
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
10131
|
+
"@geekmidas/audit": "~10.0.0-alpha.55",
|
|
10132
|
+
"@geekmidas/auth": "~10.0.0-alpha.55",
|
|
10133
|
+
"@geekmidas/cache": "~10.0.0-alpha.55",
|
|
10134
|
+
"@geekmidas/client": "~10.0.0-alpha.55",
|
|
10135
|
+
"@geekmidas/cloud": "~10.0.0-alpha.55",
|
|
10136
|
+
"@geekmidas/constructs": "~10.0.0-alpha.55",
|
|
10137
|
+
"@geekmidas/db": "~10.0.0-alpha.55",
|
|
10138
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.55",
|
|
10139
|
+
"@geekmidas/envkit": "~10.0.0-alpha.55",
|
|
10140
|
+
"@geekmidas/errors": "~10.0.0-alpha.55",
|
|
10141
|
+
"@geekmidas/events": "~10.0.0-alpha.55",
|
|
10142
|
+
"@geekmidas/logger": "~10.0.0-alpha.55",
|
|
10143
|
+
"@geekmidas/manifest": "~10.0.0-alpha.55",
|
|
10144
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.55",
|
|
10145
|
+
"@geekmidas/schema": "~10.0.0-alpha.55",
|
|
10146
|
+
"@geekmidas/services": "~10.0.0-alpha.55",
|
|
10147
|
+
"@geekmidas/storage": "~10.0.0-alpha.55",
|
|
10148
|
+
"@geekmidas/studio": "~10.0.0-alpha.55",
|
|
10149
|
+
"@geekmidas/telescope": "~10.0.0-alpha.55",
|
|
10150
|
+
"@geekmidas/testkit": "~10.0.0-alpha.55",
|
|
9391
10151
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
9392
10152
|
};
|
|
9393
10153
|
//#endregion
|
|
@@ -16130,7 +16890,7 @@ function testDatabases(reconciled) {
|
|
|
16130
16890
|
* starts without the reference data the app needs.
|
|
16131
16891
|
*/
|
|
16132
16892
|
async function migrateTestStage(workspace, env) {
|
|
16133
|
-
const { migrateAndSeed } = await import("./migrate-
|
|
16893
|
+
const { migrateAndSeed } = await import("./migrate-PcQg692O.mjs");
|
|
16134
16894
|
const { discover } = await import("./discover-Cf2jAcgF.mjs").then((n) => n.n);
|
|
16135
16895
|
const sources = {};
|
|
16136
16896
|
const manifest = await discover({
|
|
@@ -16561,7 +17321,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
16561
17321
|
try {
|
|
16562
17322
|
const globalOptions = program.opts();
|
|
16563
17323
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16564
|
-
const { migrateCommand } = await import("./migrate-
|
|
17324
|
+
const { migrateCommand } = await import("./migrate-PcQg692O.mjs");
|
|
16565
17325
|
await migrateCommand({
|
|
16566
17326
|
...options,
|
|
16567
17327
|
...construct ? { construct } : {}
|
|
@@ -16575,7 +17335,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
16575
17335
|
try {
|
|
16576
17336
|
const globalOptions = program.opts();
|
|
16577
17337
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16578
|
-
const { seedCommand } = await import("./migrate-
|
|
17338
|
+
const { seedCommand } = await import("./migrate-PcQg692O.mjs");
|
|
16579
17339
|
await seedCommand({
|
|
16580
17340
|
...options,
|
|
16581
17341
|
...construct ? { construct } : {}
|
|
@@ -16589,7 +17349,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
16589
17349
|
try {
|
|
16590
17350
|
const globalOptions = program.opts();
|
|
16591
17351
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16592
|
-
const { migrationCommand } = await import("./migrate-
|
|
17352
|
+
const { migrationCommand } = await import("./migrate-PcQg692O.mjs");
|
|
16593
17353
|
await migrationCommand({
|
|
16594
17354
|
construct,
|
|
16595
17355
|
...name ? { name } : {}
|
|
@@ -16687,7 +17447,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
|
|
|
16687
17447
|
try {
|
|
16688
17448
|
const globalOptions = program.opts();
|
|
16689
17449
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16690
|
-
const { encryptionRotateCommand } = await import("./encryption-
|
|
17450
|
+
const { encryptionRotateCommand } = await import("./encryption-Bc1cMdeY.mjs");
|
|
16691
17451
|
await encryptionRotateCommand(construct, options);
|
|
16692
17452
|
} catch (error) {
|
|
16693
17453
|
console.error(formatError(error));
|
|
@@ -16698,7 +17458,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
|
|
|
16698
17458
|
try {
|
|
16699
17459
|
const globalOptions = program.opts();
|
|
16700
17460
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16701
|
-
const { encryptionRetireCommand } = await import("./encryption-
|
|
17461
|
+
const { encryptionRetireCommand } = await import("./encryption-Bc1cMdeY.mjs");
|
|
16702
17462
|
await encryptionRetireCommand(construct, key, options);
|
|
16703
17463
|
} catch (error) {
|
|
16704
17464
|
console.error(formatError(error));
|
|
@@ -16750,7 +17510,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
16750
17510
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
16751
17511
|
process.exit(1);
|
|
16752
17512
|
}
|
|
16753
|
-
const { derivedContainers } = await import("./workspace-
|
|
17513
|
+
const { derivedContainers } = await import("./workspace-BE0WHPw9.mjs").then((n) => n.o);
|
|
16754
17514
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
16755
17515
|
if (!result) {
|
|
16756
17516
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
@@ -16809,7 +17569,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
|
|
|
16809
17569
|
try {
|
|
16810
17570
|
const globalOptions = program.opts();
|
|
16811
17571
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16812
|
-
const { deployGithubCommand } = await import("./github-
|
|
17572
|
+
const { deployGithubCommand } = await import("./github-DWHXTcwL.mjs");
|
|
16813
17573
|
await deployGithubCommand(options);
|
|
16814
17574
|
} catch (error) {
|
|
16815
17575
|
console.error(formatError(error));
|
|
@@ -16912,6 +17672,16 @@ program.command("state:show").description("Show deployment state for a stage").r
|
|
|
16912
17672
|
process.exit(1);
|
|
16913
17673
|
}
|
|
16914
17674
|
});
|
|
17675
|
+
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) => {
|
|
17676
|
+
try {
|
|
17677
|
+
const globalOptions = program.opts();
|
|
17678
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
17679
|
+
await stateUnlockCommand(options);
|
|
17680
|
+
} catch (error) {
|
|
17681
|
+
console.error(formatError(error));
|
|
17682
|
+
process.exit(1);
|
|
17683
|
+
}
|
|
17684
|
+
});
|
|
16915
17685
|
program.command("state:diff").description("Compare local and remote deployment state").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
|
|
16916
17686
|
try {
|
|
16917
17687
|
const globalOptions = program.opts();
|
|
@@ -16934,6 +17704,6 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
|
|
|
16934
17704
|
});
|
|
16935
17705
|
program.parse();
|
|
16936
17706
|
//#endregion
|
|
16937
|
-
export {
|
|
17707
|
+
export { StageStateMissing as a, StateStoreWithoutLocking as c, newLockHolder as d, encryption_exports as f, DocumentStateStore as i, StateVersionConflict as l, getHostingerToken as m, slug as n, StateLocked as o, appPorts_exports as p, stamp as r, StateStoreBusy as s, generateFullstackCustomSecrets as t, contentVersion as u };
|
|
16938
17708
|
|
|
16939
17709
|
//# sourceMappingURL=index.mjs.map
|