@envsync-cloud/deploy-cli 0.6.4 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +118 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,6 +71,8 @@ Bootstrap infra, migrations, RustFS, and OpenFGA:
71
71
  npx @envsync-cloud/deploy-cli bootstrap
72
72
  ```
73
73
 
74
+ `bootstrap` is destructive. It removes the existing EnvSync stack, matching containers, network, and managed volumes before rebuilding, and requires typing `ARE YOU SURE?` to continue.
75
+
74
76
  Deploy the pending API and frontend services:
75
77
 
76
78
  ```bash
@@ -79,7 +81,7 @@ npx @envsync-cloud/deploy-cli deploy
79
81
 
80
82
  The staged flow is:
81
83
  - `setup` writes desired config
82
- - `bootstrap` starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
84
+ - `bootstrap` resets the existing EnvSync deployment, then starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
83
85
  - `deploy` starts the pending API and frontend services
84
86
 
85
87
  Check service health:
package/dist/index.js CHANGED
@@ -69,6 +69,9 @@ function logInfo(message) {
69
69
  function logSuccess(message) {
70
70
  console.log(`${chalk.green("[ok]")} ${message}`);
71
71
  }
72
+ function logWarn(message) {
73
+ console.log(`${chalk.yellow("[warn]")} ${message}`);
74
+ }
72
75
  function logDryRun(message) {
73
76
  console.log(`${chalk.magenta("[dry-run]")} ${message}`);
74
77
  }
@@ -216,6 +219,18 @@ async function ask(question, fallback = "") {
216
219
  });
217
220
  });
218
221
  }
222
+ async function askRequired(question) {
223
+ if (!process.stdin.isTTY) {
224
+ throw new Error(`${question} confirmation requires an interactive terminal.`);
225
+ }
226
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
227
+ return await new Promise((resolve) => {
228
+ rl.question(`${question} `, (answer) => {
229
+ rl.close();
230
+ resolve(answer.trim());
231
+ });
232
+ });
233
+ }
219
234
  function sleepSeconds(seconds) {
220
235
  spawnSync("sleep", [`${seconds}`], { stdio: "ignore" });
221
236
  }
@@ -364,6 +379,7 @@ function emptyGeneratedState() {
364
379
  },
365
380
  secrets: {
366
381
  s3_secret_key: "",
382
+ keycloak_db_password: "",
367
383
  keycloak_web_client_secret: "",
368
384
  keycloak_api_client_secret: "",
369
385
  openfga_db_password: "",
@@ -384,6 +400,7 @@ function normalizeGeneratedState(raw) {
384
400
  },
385
401
  secrets: {
386
402
  s3_secret_key: raw?.secrets?.s3_secret_key ?? defaults.secrets.s3_secret_key,
403
+ keycloak_db_password: raw?.secrets?.keycloak_db_password ?? defaults.secrets.keycloak_db_password,
387
404
  keycloak_web_client_secret: raw?.secrets?.keycloak_web_client_secret ?? defaults.secrets.keycloak_web_client_secret,
388
405
  keycloak_api_client_secret: raw?.secrets?.keycloak_api_client_secret ?? defaults.secrets.keycloak_api_client_secret,
389
406
  openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
@@ -426,6 +443,7 @@ function mergeGeneratedState(env, generated) {
426
443
  },
427
444
  secrets: {
428
445
  s3_secret_key: env.S3_SECRET_KEY ?? normalized.secrets.s3_secret_key,
446
+ keycloak_db_password: env.KEYCLOAK_DB_PASSWORD ?? normalized.secrets.keycloak_db_password,
429
447
  keycloak_web_client_secret: env.KEYCLOAK_WEB_CLIENT_SECRET ?? normalized.secrets.keycloak_web_client_secret,
430
448
  keycloak_api_client_secret: env.KEYCLOAK_API_CLIENT_SECRET ?? normalized.secrets.keycloak_api_client_secret,
431
449
  openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
@@ -446,6 +464,7 @@ function ensureGeneratedRuntimeState(generated) {
446
464
  openfga: generated.openfga,
447
465
  secrets: {
448
466
  s3_secret_key: generated.secrets.s3_secret_key || randomSecret(16),
467
+ keycloak_db_password: generated.secrets.keycloak_db_password || "",
449
468
  keycloak_web_client_secret: generated.secrets.keycloak_web_client_secret || randomSecret(),
450
469
  keycloak_api_client_secret: generated.secrets.keycloak_api_client_secret || randomSecret(),
451
470
  openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
@@ -488,6 +507,7 @@ function buildRuntimeEnv(config, generated) {
488
507
  KEYCLOAK_REALM: config.auth.keycloak_realm,
489
508
  KEYCLOAK_ADMIN_USER: config.auth.admin_user,
490
509
  KEYCLOAK_ADMIN_PASSWORD: config.auth.admin_password,
510
+ KEYCLOAK_DB_PASSWORD: generated.secrets.keycloak_db_password || config.auth.admin_password,
491
511
  KEYCLOAK_WEB_CLIENT_ID: config.auth.web_client_id,
492
512
  KEYCLOAK_WEB_CLIENT_SECRET: generated.secrets.keycloak_web_client_secret,
493
513
  KEYCLOAK_CLI_CLIENT_ID: config.auth.cli_client_id,
@@ -781,7 +801,7 @@ ${renderEnvList({
781
801
  environment:
782
802
  ${renderEnvList({
783
803
  POSTGRES_USER: "keycloak",
784
- POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
804
+ POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_DB_PASSWORD,
785
805
  POSTGRES_DB: "keycloak"
786
806
  })}
787
807
  volumes:
@@ -798,7 +818,7 @@ ${renderEnvList({
798
818
  KC_DB: "postgres",
799
819
  KC_DB_URL: "jdbc:postgresql://keycloak_db:5432/keycloak",
800
820
  KC_DB_USERNAME: "keycloak",
801
- KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
821
+ KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_DB_PASSWORD,
802
822
  KC_BOOTSTRAP_ADMIN_USERNAME: config.auth.admin_user,
803
823
  KC_BOOTSTRAP_ADMIN_PASSWORD: config.auth.admin_password,
804
824
  KC_HTTP_ENABLED: "true",
@@ -1251,6 +1271,95 @@ function listStackServices(config) {
1251
1271
  }
1252
1272
  return services;
1253
1273
  }
1274
+ function listManagedContainers(config) {
1275
+ const output = tryRun("docker", ["ps", "-aq", "--filter", `name=^/${config.services.stack_name}_`], { quiet: true });
1276
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1277
+ }
1278
+ function stackExists(config) {
1279
+ const output = tryRun("docker", ["stack", "ls", "--format", "{{.Name}}"], { quiet: true });
1280
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).includes(config.services.stack_name);
1281
+ }
1282
+ function waitForStackRemoval(config, timeoutSeconds = 60) {
1283
+ if (currentOptions.dryRun) {
1284
+ logDryRun(`Would wait for stack ${config.services.stack_name} to be removed`);
1285
+ return;
1286
+ }
1287
+ const deadline = Date.now() + timeoutSeconds * 1e3;
1288
+ while (Date.now() < deadline) {
1289
+ if (!stackExists(config)) {
1290
+ return;
1291
+ }
1292
+ sleepSeconds(2);
1293
+ }
1294
+ throw new Error(`Timed out waiting for stack ${config.services.stack_name} to be removed`);
1295
+ }
1296
+ async function confirmBootstrapReset(config) {
1297
+ const volumeNames = STACK_VOLUMES.map((volume) => stackVolumeName(config, volume));
1298
+ const containerIds = listManagedContainers(config);
1299
+ const networkName = stackNetworkName(config);
1300
+ logWarn("Bootstrap will delete existing EnvSync Docker resources before rebuilding infra.");
1301
+ logWarn(`Stack: ${config.services.stack_name}`);
1302
+ logWarn(`Network: ${networkName}`);
1303
+ logWarn(`Volumes: ${volumeNames.join(", ")}`);
1304
+ if (containerIds.length > 0) {
1305
+ logWarn(`Containers: ${containerIds.join(", ")}`);
1306
+ } else {
1307
+ logWarn("Containers: none currently matched");
1308
+ }
1309
+ logWarn("This removes existing deployment data for the managed EnvSync services.");
1310
+ const response = await askRequired(chalk.bold.red('Type "ARE YOU SURE?" to continue:'));
1311
+ if (response !== "ARE YOU SURE?") {
1312
+ throw new Error("Bootstrap aborted. Confirmation did not match 'ARE YOU SURE?'.");
1313
+ }
1314
+ logSuccess("Destructive bootstrap reset confirmed");
1315
+ }
1316
+ function cleanupBootstrapState(config) {
1317
+ const volumeNames = STACK_VOLUMES.map((volume) => stackVolumeName(config, volume));
1318
+ const containerIds = listManagedContainers(config);
1319
+ const networkName = stackNetworkName(config);
1320
+ logStep("Removing existing EnvSync deployment resources");
1321
+ if (currentOptions.dryRun) {
1322
+ if (stackExists(config)) {
1323
+ logDryRun(`Would remove stack ${config.services.stack_name}`);
1324
+ logCommand("docker", ["stack", "rm", config.services.stack_name]);
1325
+ } else {
1326
+ logDryRun(`No existing stack named ${config.services.stack_name} found`);
1327
+ }
1328
+ if (containerIds.length > 0) {
1329
+ logDryRun(`Would remove containers: ${containerIds.join(", ")}`);
1330
+ logCommand("docker", ["rm", "-f", ...containerIds]);
1331
+ }
1332
+ logDryRun(`Would remove network ${networkName} if present`);
1333
+ logCommand("docker", ["network", "rm", networkName]);
1334
+ for (const volumeName of volumeNames) {
1335
+ logDryRun(`Would remove volume ${volumeName}`);
1336
+ logCommand("docker", ["volume", "rm", "-f", volumeName]);
1337
+ }
1338
+ logSuccess("Bootstrap cleanup preview completed");
1339
+ return;
1340
+ }
1341
+ if (stackExists(config)) {
1342
+ logCommand("docker", ["stack", "rm", config.services.stack_name]);
1343
+ run("docker", ["stack", "rm", config.services.stack_name]);
1344
+ waitForStackRemoval(config);
1345
+ }
1346
+ const refreshedContainers = listManagedContainers(config);
1347
+ if (refreshedContainers.length > 0) {
1348
+ logCommand("docker", ["rm", "-f", ...refreshedContainers]);
1349
+ run("docker", ["rm", "-f", ...refreshedContainers]);
1350
+ }
1351
+ if (commandSucceeds("docker", ["network", "inspect", networkName])) {
1352
+ logCommand("docker", ["network", "rm", networkName]);
1353
+ run("docker", ["network", "rm", networkName]);
1354
+ }
1355
+ for (const volumeName of volumeNames) {
1356
+ if (commandSucceeds("docker", ["volume", "inspect", volumeName])) {
1357
+ logCommand("docker", ["volume", "rm", "-f", volumeName]);
1358
+ run("docker", ["volume", "rm", "-f", volumeName]);
1359
+ }
1360
+ }
1361
+ logSuccess("Existing EnvSync deployment resources removed");
1362
+ }
1254
1363
  function serviceHealth(services, name) {
1255
1364
  return services.get(`${name}`) ?? "missing";
1256
1365
  }
@@ -1369,6 +1478,12 @@ async function cmdBootstrap() {
1369
1478
  const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
1370
1479
  logInfo(`Release version: ${config.release.version}`);
1371
1480
  assertSwarmManager();
1481
+ if (currentOptions.dryRun) {
1482
+ logWarn("Dry-run mode: bootstrap reset will be previewed but not executed.");
1483
+ } else {
1484
+ await confirmBootstrapReset(config);
1485
+ }
1486
+ cleanupBootstrapState(config);
1372
1487
  ensureRepoCheckout(config);
1373
1488
  writeDeployArtifacts(config, nextGenerated);
1374
1489
  buildKeycloakImage(config.images.keycloak);
@@ -1384,7 +1499,7 @@ async function cmdBootstrap() {
1384
1499
  waitForPostgresService(config, "postgres", "postgres", "postgres", "envsync-postgres");
1385
1500
  waitForRedisService(config);
1386
1501
  waitForTcpService(config, "rustfs", "rustfs", 9e3);
1387
- waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.KEYCLOAK_ADMIN_PASSWORD);
1502
+ waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.KEYCLOAK_DB_PASSWORD);
1388
1503
  waitForPostgresService(config, "openfga", "openfga_db", "openfga", runtimeEnv.OPENFGA_DB_PASSWORD);
1389
1504
  waitForPostgresService(config, "minikms", "minikms_db", "postgres", runtimeEnv.MINIKMS_DB_PASSWORD);
1390
1505
  runOpenFgaMigrate(config, runtimeEnv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {