@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.
- package/README.md +24 -0
- package/dist/index.js +104 -29
- 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:
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|