@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.
- package/README.md +24 -0
- package/dist/index.js +104 -30
- 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
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
var
|
|
16
|
-
var REPO_ROOT = "
|
|
17
|
-
var DEPLOY_ENV = "
|
|
18
|
-
var DEPLOY_YAML = "
|
|
19
|
-
var VERSIONS_LOCK = "
|
|
20
|
-
var STACK_FILE = "
|
|
21
|
-
var BOOTSTRAP_BASE_STACK_FILE = "
|
|
22
|
-
var BOOTSTRAP_STACK_FILE = "
|
|
23
|
-
var TRAEFIK_DYNAMIC_FILE = "
|
|
24
|
-
var KEYCLOAK_REALM_FILE = "
|
|
25
|
-
var NGINX_WEB_CONF = "
|
|
26
|
-
var NGINX_LANDING_CONF = "
|
|
27
|
-
var OTEL_AGENT_CONF = "
|
|
28
|
-
var CLICKSTACK_CLICKHOUSE_CONF = "
|
|
29
|
-
var INTERNAL_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:
|
|
857
|
+
published: ${config.services.public_http_port}
|
|
809
858
|
protocol: tcp
|
|
810
859
|
mode: host
|
|
811
860
|
- target: 443
|
|
812
|
-
published:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|