@envsync-cloud/deploy-cli 0.6.14 → 0.6.17

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 -30
  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:
@@ -956,9 +1005,7 @@ ${renderEnvList({
956
1005
  environment:
957
1006
  ${renderEnvList({
958
1007
  HYPERDX_APP_URL: `https://${hosts.obs}`,
959
- HYPERDX_APP_PORT: "443",
960
1008
  HYPERDX_API_URL: `https://${hosts.obs}`,
961
- HYPERDX_API_PORT: "443",
962
1009
  FRONTEND_URL: `https://${hosts.obs}`
963
1010
  })}
964
1011
  volumes:
@@ -1058,6 +1105,14 @@ function ensureRepoCheckout(config) {
1058
1105
  logDryRun(`Would ensure repo checkout at ${REPO_ROOT}`);
1059
1106
  return;
1060
1107
  }
1108
+ if (hasExplicitRepoOverride()) {
1109
+ if (!exists(path.join(REPO_ROOT, ".git"))) {
1110
+ throw new Error(`ENVSYNC_REPO_ROOT is set but no git repo was found at ${REPO_ROOT}`);
1111
+ }
1112
+ logInfo(`Using local repo override at ${REPO_ROOT}`);
1113
+ logSuccess("Local repo override is ready");
1114
+ return;
1115
+ }
1061
1116
  ensureDir(REPO_ROOT);
1062
1117
  if (!exists(path.join(REPO_ROOT, ".git"))) {
1063
1118
  logCommand("git", ["clone", config.source.repo_url, REPO_ROOT]);
@@ -1495,16 +1550,31 @@ function cleanupBootstrapState(config) {
1495
1550
  const refreshedContainers = listManagedContainers(config);
1496
1551
  if (refreshedContainers.length > 0) {
1497
1552
  logCommand("docker", ["rm", "-f", ...refreshedContainers]);
1498
- run("docker", ["rm", "-f", ...refreshedContainers]);
1553
+ const removed = runIgnoringAbsent("docker", ["rm", "-f", ...refreshedContainers], {
1554
+ absentPatterns: ["no such container", "not found"]
1555
+ });
1556
+ if (!removed) {
1557
+ logInfo("Managed containers were already absent");
1558
+ }
1499
1559
  }
1500
1560
  if (commandSucceeds("docker", ["network", "inspect", networkName])) {
1501
1561
  logCommand("docker", ["network", "rm", networkName]);
1502
- run("docker", ["network", "rm", networkName]);
1562
+ const removed = runIgnoringAbsent("docker", ["network", "rm", networkName], {
1563
+ absentPatterns: ["network", "not found", "no such network"]
1564
+ });
1565
+ if (!removed) {
1566
+ logInfo(`Network ${networkName} was already absent`);
1567
+ }
1503
1568
  }
1504
1569
  for (const volumeName of volumeNames) {
1505
1570
  if (commandSucceeds("docker", ["volume", "inspect", volumeName])) {
1506
1571
  logCommand("docker", ["volume", "rm", "-f", volumeName]);
1507
- run("docker", ["volume", "rm", "-f", volumeName]);
1572
+ const removed = runIgnoringAbsent("docker", ["volume", "rm", "-f", volumeName], {
1573
+ absentPatterns: ["no such volume", "not found"]
1574
+ });
1575
+ if (!removed) {
1576
+ logInfo(`Volume ${volumeName} was already absent`);
1577
+ }
1508
1578
  }
1509
1579
  }
1510
1580
  logSuccess("Existing EnvSync deployment resources removed");
@@ -1575,6 +1645,8 @@ async function cmdSetup() {
1575
1645
  services: {
1576
1646
  stack_name: "envsync",
1577
1647
  api_port: 4e3,
1648
+ public_http_port: 80,
1649
+ public_https_port: 443,
1578
1650
  clickstack_ui_port: 8080,
1579
1651
  clickstack_otlp_http_port: 4318,
1580
1652
  clickstack_otlp_grpc_port: 4317,
@@ -1623,9 +1695,9 @@ async function cmdSetup() {
1623
1695
  async function cmdBootstrap() {
1624
1696
  logSection("Bootstrap");
1625
1697
  const { config, generated } = loadState();
1626
- const nextGenerated = ensureGeneratedRuntimeState(generated);
1698
+ const nextGenerated = ensureGeneratedRuntimeState(resetBootstrapGeneratedState(generated));
1627
1699
  const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
1628
- logInfo(`Release version: ${config.release.version}`);
1700
+ logReleaseContext(config);
1629
1701
  assertSwarmManager();
1630
1702
  if (currentOptions.dryRun) {
1631
1703
  logWarn("Dry-run mode: bootstrap reset will be previewed but not executed.");
@@ -1698,7 +1770,7 @@ async function cmdBootstrap() {
1698
1770
  async function cmdDeploy() {
1699
1771
  logSection("Deploy");
1700
1772
  const { config, generated } = loadState();
1701
- logInfo(`Release version: ${config.release.version}`);
1773
+ logReleaseContext(config);
1702
1774
  assertSwarmManager();
1703
1775
  assertBootstrapState(generated);
1704
1776
  if (!currentOptions.dryRun) {
@@ -1795,6 +1867,7 @@ async function cmdHealth(asJson) {
1795
1867
  async function cmdUpgrade() {
1796
1868
  logSection("Upgrade");
1797
1869
  const { config } = loadState();
1870
+ logReleaseContext(config);
1798
1871
  config.images = {
1799
1872
  ...config.images,
1800
1873
  ...versionedImages(config.release.version)
@@ -1808,6 +1881,7 @@ async function cmdUpgrade() {
1808
1881
  async function cmdUpgradeDeps() {
1809
1882
  logSection("Upgrade Dependencies");
1810
1883
  const { config } = loadState();
1884
+ logReleaseContext(config);
1811
1885
  config.images.traefik = "traefik:v3.6.6";
1812
1886
  config.images.clickstack = "clickhouse/clickstack-all-in-one:latest";
1813
1887
  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.14",
3
+ "version": "0.6.17",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {