@envsync-cloud/deploy-cli 0.6.13 → 0.6.15

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 +24 -0
  2. package/dist/index.js +104 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,8 @@ npx @envsync-cloud/deploy-cli setup
65
65
 
66
66
  `setup` requires an exact release version such as `0.6.2`. Channel names like `stable` and `latest` are not accepted for self-hosted installs.
67
67
 
68
+ The configured target release comes from `/etc/envsync/deploy.yaml`. Running a newer CLI package with `bunx @envsync-cloud/deploy-cli@<version> ...` does not change the pinned target release by itself.
69
+
68
70
  Bootstrap infra, migrations, RustFS, and OpenFGA:
69
71
 
70
72
  ```bash
@@ -73,6 +75,8 @@ npx @envsync-cloud/deploy-cli bootstrap
73
75
 
74
76
  `bootstrap` is destructive. It removes the existing EnvSync stack, matching containers, network, and managed volumes before rebuilding, and requires typing `yes` to continue. Use `--force` to bypass the prompt in automation or other non-interactive environments.
75
77
 
78
+ During destructive bootstrap, stable generated secrets are preserved, but persisted OpenFGA store/model IDs are cleared before re-initialization so a fresh OpenFGA database cannot reuse stale IDs from a previous run.
79
+
76
80
  Deploy the pending API and frontend services:
77
81
 
78
82
  ```bash
@@ -117,6 +121,26 @@ npx @envsync-cloud/deploy-cli bootstrap --force
117
121
  npx @envsync-cloud/deploy-cli deploy --dry-run
118
122
  ```
119
123
 
124
+ ## Local Smoke Testing
125
+
126
+ Use the repo-local smoke harness to test unpublished self-hosted deploy-cli changes without publishing to GitHub or npm:
127
+
128
+ ```bash
129
+ bun run selfhost:smoke
130
+ ```
131
+
132
+ The smoke harness:
133
+ - runs the local `packages/deploy-cli/src/index.ts` directly
134
+ - uses disposable roots under `.tmp/selfhost-smoke`
135
+ - sets `ENVSYNC_REPO_ROOT` to the current workspace so no repo clone/fetch/checkout happens
136
+ - uses high host ports instead of `80/443`
137
+
138
+ Advanced local test overrides supported by the deploy-cli:
139
+ - `ENVSYNC_HOST_ROOT`
140
+ - `ENVSYNC_ETC_ROOT`
141
+ - `ENVSYNC_TRAEFIK_STATE_ROOT`
142
+ - `ENVSYNC_REPO_ROOT`
143
+
120
144
  ## Links
121
145
 
122
146
  - Repository: https://github.com/EnvSync-Cloud/envsync
package/dist/index.js CHANGED
@@ -7,26 +7,26 @@ import fs from "fs";
7
7
  import path from "path";
8
8
  import readline from "readline";
9
9
  import chalk from "chalk";
10
- var HOST_ROOT = "/opt/envsync";
11
- var DEPLOY_ROOT = "/opt/envsync/deploy";
12
- var RELEASES_ROOT = "/opt/envsync/releases";
13
- var BACKUPS_ROOT = "/opt/envsync/backups";
14
- var ETC_ROOT = "/etc/envsync";
15
- var TRAEFIK_STATE_ROOT = "/var/lib/envsync/traefik";
16
- var REPO_ROOT = "/opt/envsync/repo";
17
- var DEPLOY_ENV = "/etc/envsync/deploy.env";
18
- var DEPLOY_YAML = "/etc/envsync/deploy.yaml";
19
- var VERSIONS_LOCK = "/opt/envsync/deploy/versions.lock.json";
20
- var STACK_FILE = "/opt/envsync/deploy/docker-stack.yaml";
21
- var BOOTSTRAP_BASE_STACK_FILE = "/opt/envsync/deploy/docker-stack.bootstrap.base.yaml";
22
- var BOOTSTRAP_STACK_FILE = "/opt/envsync/deploy/docker-stack.bootstrap.yaml";
23
- var TRAEFIK_DYNAMIC_FILE = "/opt/envsync/deploy/traefik-dynamic.yaml";
24
- var KEYCLOAK_REALM_FILE = "/opt/envsync/deploy/keycloak-realm.envsync.json";
25
- var NGINX_WEB_CONF = "/opt/envsync/deploy/nginx-web.conf";
26
- var NGINX_LANDING_CONF = "/opt/envsync/deploy/nginx-landing.conf";
27
- var OTEL_AGENT_CONF = "/opt/envsync/deploy/otel-agent.yaml";
28
- var CLICKSTACK_CLICKHOUSE_CONF = "/opt/envsync/deploy/clickhouse-listen.xml";
29
- var INTERNAL_CONFIG_JSON = "/opt/envsync/deploy/config.json";
10
+ var HOST_ROOT = process.env.ENVSYNC_HOST_ROOT ?? "/opt/envsync";
11
+ var ETC_ROOT = process.env.ENVSYNC_ETC_ROOT ?? "/etc/envsync";
12
+ var TRAEFIK_STATE_ROOT = process.env.ENVSYNC_TRAEFIK_STATE_ROOT ?? "/var/lib/envsync/traefik";
13
+ var DEPLOY_ROOT = path.join(HOST_ROOT, "deploy");
14
+ var RELEASES_ROOT = path.join(HOST_ROOT, "releases");
15
+ var BACKUPS_ROOT = path.join(HOST_ROOT, "backups");
16
+ var REPO_ROOT = process.env.ENVSYNC_REPO_ROOT ?? path.join(HOST_ROOT, "repo");
17
+ var DEPLOY_ENV = path.join(ETC_ROOT, "deploy.env");
18
+ var DEPLOY_YAML = path.join(ETC_ROOT, "deploy.yaml");
19
+ var VERSIONS_LOCK = path.join(DEPLOY_ROOT, "versions.lock.json");
20
+ var STACK_FILE = path.join(DEPLOY_ROOT, "docker-stack.yaml");
21
+ var BOOTSTRAP_BASE_STACK_FILE = path.join(DEPLOY_ROOT, "docker-stack.bootstrap.base.yaml");
22
+ var BOOTSTRAP_STACK_FILE = path.join(DEPLOY_ROOT, "docker-stack.bootstrap.yaml");
23
+ var TRAEFIK_DYNAMIC_FILE = path.join(DEPLOY_ROOT, "traefik-dynamic.yaml");
24
+ var KEYCLOAK_REALM_FILE = path.join(DEPLOY_ROOT, "keycloak-realm.envsync.json");
25
+ var NGINX_WEB_CONF = path.join(DEPLOY_ROOT, "nginx-web.conf");
26
+ var NGINX_LANDING_CONF = path.join(DEPLOY_ROOT, "nginx-landing.conf");
27
+ var OTEL_AGENT_CONF = path.join(DEPLOY_ROOT, "otel-agent.yaml");
28
+ var CLICKSTACK_CLICKHOUSE_CONF = path.join(DEPLOY_ROOT, "clickhouse-listen.xml");
29
+ var INTERNAL_CONFIG_JSON = path.join(DEPLOY_ROOT, "config.json");
30
30
  var STACK_VOLUMES = [
31
31
  "postgres_data",
32
32
  "redis_data",
@@ -109,6 +109,28 @@ function commandSucceeds(cmd, args, opts = {}) {
109
109
  });
110
110
  return result.status === 0;
111
111
  }
112
+ function runIgnoringAbsent(cmd, args, opts = {}) {
113
+ const result = spawnSync(cmd, args, {
114
+ cwd: opts.cwd,
115
+ env: { ...process.env, ...opts.env },
116
+ stdio: "pipe",
117
+ encoding: "utf8"
118
+ });
119
+ if (result.status === 0) {
120
+ const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
121
+ if (stdout) console.log(stdout);
122
+ return true;
123
+ }
124
+ const combined = `${typeof result.stdout === "string" ? result.stdout : ""}
125
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
126
+ const absentPatterns = (opts.absentPatterns ?? []).map((pattern) => pattern.toLowerCase());
127
+ if (absentPatterns.some((pattern) => combined.includes(pattern))) {
128
+ return false;
129
+ }
130
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
131
+ throw new Error(`Command failed: ${cmd} ${args.join(" ")}${stderr ? `
132
+ ${stderr}` : ""}`);
133
+ }
112
134
  function ensureDir(dir) {
113
135
  fs.mkdirSync(dir, { recursive: true });
114
136
  }
@@ -256,6 +278,19 @@ function getDeployCliVersion() {
256
278
  return process.env.npm_package_version ?? "0.0.0";
257
279
  }
258
280
  }
281
+ function hasExplicitRepoOverride() {
282
+ return typeof process.env.ENVSYNC_REPO_ROOT === "string" && process.env.ENVSYNC_REPO_ROOT.length > 0;
283
+ }
284
+ function logReleaseContext(config) {
285
+ const cliVersion = getDeployCliVersion();
286
+ logInfo(`Configured release version from ${DEPLOY_YAML}: ${config.release.version}`);
287
+ logInfo(`Running deploy-cli version: ${cliVersion}`);
288
+ if (cliVersion !== config.release.version) {
289
+ logWarn(
290
+ `The running deploy-cli version does not change the configured release target. This run will deploy the version pinned in ${DEPLOY_YAML}.`
291
+ );
292
+ }
293
+ }
259
294
  function assertSemverVersion(version, label = "release version") {
260
295
  if (!SEMVER_VERSION_RE.test(version)) {
261
296
  throw new Error(`Invalid ${label} '${version}'. Expected an exact semver like 0.6.2.`);
@@ -332,6 +367,8 @@ function normalizeConfig(raw) {
332
367
  services: {
333
368
  stack_name: requireDefined(raw.services?.stack_name, "services.stack_name"),
334
369
  api_port: requireDefined(raw.services?.api_port, "services.api_port"),
370
+ public_http_port: raw.services?.public_http_port ?? 80,
371
+ public_https_port: raw.services?.public_https_port ?? 443,
335
372
  clickstack_ui_port: requireDefined(raw.services?.clickstack_ui_port, "services.clickstack_ui_port"),
336
373
  clickstack_otlp_http_port: requireDefined(raw.services?.clickstack_otlp_http_port, "services.clickstack_otlp_http_port"),
337
374
  clickstack_otlp_grpc_port: requireDefined(raw.services?.clickstack_otlp_grpc_port, "services.clickstack_otlp_grpc_port"),
@@ -475,6 +512,18 @@ function ensureGeneratedRuntimeState(generated) {
475
512
  bootstrap: generated.bootstrap
476
513
  });
477
514
  }
515
+ function resetBootstrapGeneratedState(generated) {
516
+ return normalizeGeneratedState({
517
+ openfga: {
518
+ store_id: "",
519
+ model_id: ""
520
+ },
521
+ secrets: generated.secrets,
522
+ bootstrap: {
523
+ completed_at: ""
524
+ }
525
+ });
526
+ }
478
527
  function keycloakImageTag(image) {
479
528
  return image.split(":").slice(1).join(":") || "local";
480
529
  }
@@ -805,11 +854,11 @@ services:
805
854
  - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
806
855
  ports:
807
856
  - target: 80
808
- published: 80
857
+ published: ${config.services.public_http_port}
809
858
  protocol: tcp
810
859
  mode: host
811
860
  - target: 443
812
- published: 443
861
+ published: ${config.services.public_https_port}
813
862
  protocol: tcp
814
863
  mode: host
815
864
  volumes:
@@ -1058,6 +1107,14 @@ function ensureRepoCheckout(config) {
1058
1107
  logDryRun(`Would ensure repo checkout at ${REPO_ROOT}`);
1059
1108
  return;
1060
1109
  }
1110
+ if (hasExplicitRepoOverride()) {
1111
+ if (!exists(path.join(REPO_ROOT, ".git"))) {
1112
+ throw new Error(`ENVSYNC_REPO_ROOT is set but no git repo was found at ${REPO_ROOT}`);
1113
+ }
1114
+ logInfo(`Using local repo override at ${REPO_ROOT}`);
1115
+ logSuccess("Local repo override is ready");
1116
+ return;
1117
+ }
1061
1118
  ensureDir(REPO_ROOT);
1062
1119
  if (!exists(path.join(REPO_ROOT, ".git"))) {
1063
1120
  logCommand("git", ["clone", config.source.repo_url, REPO_ROOT]);
@@ -1495,16 +1552,31 @@ function cleanupBootstrapState(config) {
1495
1552
  const refreshedContainers = listManagedContainers(config);
1496
1553
  if (refreshedContainers.length > 0) {
1497
1554
  logCommand("docker", ["rm", "-f", ...refreshedContainers]);
1498
- run("docker", ["rm", "-f", ...refreshedContainers]);
1555
+ const removed = runIgnoringAbsent("docker", ["rm", "-f", ...refreshedContainers], {
1556
+ absentPatterns: ["no such container", "not found"]
1557
+ });
1558
+ if (!removed) {
1559
+ logInfo("Managed containers were already absent");
1560
+ }
1499
1561
  }
1500
1562
  if (commandSucceeds("docker", ["network", "inspect", networkName])) {
1501
1563
  logCommand("docker", ["network", "rm", networkName]);
1502
- run("docker", ["network", "rm", networkName]);
1564
+ const removed = runIgnoringAbsent("docker", ["network", "rm", networkName], {
1565
+ absentPatterns: ["network", "not found", "no such network"]
1566
+ });
1567
+ if (!removed) {
1568
+ logInfo(`Network ${networkName} was already absent`);
1569
+ }
1503
1570
  }
1504
1571
  for (const volumeName of volumeNames) {
1505
1572
  if (commandSucceeds("docker", ["volume", "inspect", volumeName])) {
1506
1573
  logCommand("docker", ["volume", "rm", "-f", volumeName]);
1507
- run("docker", ["volume", "rm", "-f", volumeName]);
1574
+ const removed = runIgnoringAbsent("docker", ["volume", "rm", "-f", volumeName], {
1575
+ absentPatterns: ["no such volume", "not found"]
1576
+ });
1577
+ if (!removed) {
1578
+ logInfo(`Volume ${volumeName} was already absent`);
1579
+ }
1508
1580
  }
1509
1581
  }
1510
1582
  logSuccess("Existing EnvSync deployment resources removed");
@@ -1575,6 +1647,8 @@ async function cmdSetup() {
1575
1647
  services: {
1576
1648
  stack_name: "envsync",
1577
1649
  api_port: 4e3,
1650
+ public_http_port: 80,
1651
+ public_https_port: 443,
1578
1652
  clickstack_ui_port: 8080,
1579
1653
  clickstack_otlp_http_port: 4318,
1580
1654
  clickstack_otlp_grpc_port: 4317,
@@ -1623,9 +1697,9 @@ async function cmdSetup() {
1623
1697
  async function cmdBootstrap() {
1624
1698
  logSection("Bootstrap");
1625
1699
  const { config, generated } = loadState();
1626
- const nextGenerated = ensureGeneratedRuntimeState(generated);
1700
+ const nextGenerated = ensureGeneratedRuntimeState(resetBootstrapGeneratedState(generated));
1627
1701
  const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
1628
- logInfo(`Release version: ${config.release.version}`);
1702
+ logReleaseContext(config);
1629
1703
  assertSwarmManager();
1630
1704
  if (currentOptions.dryRun) {
1631
1705
  logWarn("Dry-run mode: bootstrap reset will be previewed but not executed.");
@@ -1664,7 +1738,6 @@ async function cmdBootstrap() {
1664
1738
  waitForHttpService(config, "keycloak management readiness", "http://keycloak:9000/health/ready", 180);
1665
1739
  waitForHttpService(config, "openfga", "http://openfga:8090/stores");
1666
1740
  waitForTcpService(config, "minikms", "minikms", 50051);
1667
- waitForTcpService(config, "clickstack ui", "clickstack", 8080, 180);
1668
1741
  const initResult = runBootstrapInit(config);
1669
1742
  const persistedGenerated = normalizeGeneratedState({
1670
1743
  openfga: {
@@ -1699,7 +1772,7 @@ async function cmdBootstrap() {
1699
1772
  async function cmdDeploy() {
1700
1773
  logSection("Deploy");
1701
1774
  const { config, generated } = loadState();
1702
- logInfo(`Release version: ${config.release.version}`);
1775
+ logReleaseContext(config);
1703
1776
  assertSwarmManager();
1704
1777
  assertBootstrapState(generated);
1705
1778
  if (!currentOptions.dryRun) {
@@ -1796,6 +1869,7 @@ async function cmdHealth(asJson) {
1796
1869
  async function cmdUpgrade() {
1797
1870
  logSection("Upgrade");
1798
1871
  const { config } = loadState();
1872
+ logReleaseContext(config);
1799
1873
  config.images = {
1800
1874
  ...config.images,
1801
1875
  ...versionedImages(config.release.version)
@@ -1809,6 +1883,7 @@ async function cmdUpgrade() {
1809
1883
  async function cmdUpgradeDeps() {
1810
1884
  logSection("Upgrade Dependencies");
1811
1885
  const { config } = loadState();
1886
+ logReleaseContext(config);
1812
1887
  config.images.traefik = "traefik:v3.6.6";
1813
1888
  config.images.clickstack = "clickhouse/clickstack-all-in-one:latest";
1814
1889
  config.images.otel_agent = "otel/opentelemetry-collector-contrib:0.111.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {