@askexenow/exe-os 0.9.270 → 0.9.271
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/dist/bin/cli.js +1 -1
- package/dist/bin/stack-update.js +10 -3
- package/dist/bin/vps-health-gate.js +1 -1
- package/dist/chunk-IBMTSEZC.js +230 -0
- package/dist/chunk-O7YO7E2G.js +1512 -0
- package/dist/hooks/manifest.json +1 -1
- package/dist/stack-release-2KSOYDIV.js +712 -0
- package/dist/stack-update-QQA64STQ.js +52 -0
- package/package.json +1 -1
- package/release-notes.json +8 -27
package/dist/bin/cli.js
CHANGED
|
@@ -353,7 +353,7 @@ ID: ${result.id}`);
|
|
|
353
353
|
const { runStackUpdateCli } = await import("./stack-update.js");
|
|
354
354
|
await runStackUpdateCli(args.slice(1));
|
|
355
355
|
} else if (args[0] === "release") {
|
|
356
|
-
const { parseReleaseArgs, runStackRelease } = await import("../stack-release-
|
|
356
|
+
const { parseReleaseArgs, runStackRelease } = await import("../stack-release-2KSOYDIV.js");
|
|
357
357
|
const releaseFlags = parseReleaseArgs(args.slice(1));
|
|
358
358
|
await runStackRelease(releaseFlags);
|
|
359
359
|
} else if (args[0] === "verify-stack") {
|
package/dist/bin/stack-update.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
loadStackManifest,
|
|
11
11
|
patchEnv,
|
|
12
12
|
runStackUpdate
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-O7YO7E2G.js";
|
|
14
14
|
import {
|
|
15
15
|
runVerifyStack
|
|
16
16
|
} from "../chunk-IRHNV4GY.js";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
logResult,
|
|
22
22
|
runHealthGate
|
|
23
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-IBMTSEZC.js";
|
|
24
24
|
import "../chunk-MOZ2YQ54.js";
|
|
25
25
|
import "../chunk-VXIMSRTO.js";
|
|
26
26
|
import "../chunk-LYH5HE24.js";
|
|
@@ -357,6 +357,7 @@ function parseArgs(args) {
|
|
|
357
357
|
dryRun: false,
|
|
358
358
|
check: false,
|
|
359
359
|
rollback: false,
|
|
360
|
+
resume: false,
|
|
360
361
|
deploymentPersona: process.env.EXE_STACK_DEPLOYMENT_PERSONA === "askexe-control-plane" ? "askexe-control-plane" : "customer",
|
|
361
362
|
yes: false,
|
|
362
363
|
allowedBreakingChangeIds: [],
|
|
@@ -371,6 +372,9 @@ function parseArgs(args) {
|
|
|
371
372
|
else if (arg === "--target" || arg === "--to") opts.targetVersion = next();
|
|
372
373
|
else if (arg.startsWith("--target=")) opts.targetVersion = arg.split("=")[1];
|
|
373
374
|
else if (arg.startsWith("--to=")) opts.targetVersion = arg.split("=")[1];
|
|
375
|
+
else if (arg === "--service") opts.serviceName = next();
|
|
376
|
+
else if (arg.startsWith("--service=")) opts.serviceName = arg.split("=").slice(1).join("=");
|
|
377
|
+
else if (arg === "--resume") opts.resume = true;
|
|
374
378
|
else if (arg === "--compose-file") opts.composeFile = next();
|
|
375
379
|
else if (arg.startsWith("--compose-file=")) opts.composeFile = arg.split("=").slice(1).join("=");
|
|
376
380
|
else if (arg === "--env-file" || arg === "--stack-env-file") opts.envFile = next();
|
|
@@ -439,6 +443,8 @@ Options:
|
|
|
439
443
|
--manifest <ref> Stack manifest JSON path or URL (default: update.askexe.com)
|
|
440
444
|
--to, --target <version> Stack version to install (default: stay on installed version;
|
|
441
445
|
uses manifest.latest only on first install)
|
|
446
|
+
--service <name> Update one manifest/compose service only
|
|
447
|
+
--resume Resume a failed update from lock state; retries only incomplete services
|
|
442
448
|
--compose-file <path> docker-compose.yml path (default: ./docker-compose.yml or /opt/exe-stack/docker-compose.yml)
|
|
443
449
|
--stack-env-file <path> .env path (default: ./.env or /opt/exe-stack/.env)
|
|
444
450
|
--env-file <path> Alias; prefer --stack-env-file because Node 22 reserves --env-file
|
|
@@ -641,7 +647,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
641
647
|
const lockFile = opts.lockFile ?? path2.join(path2.dirname(opts.envFile), ".exe-stack-lock.json");
|
|
642
648
|
const installedVersion = readInstalledVersion(lockFile);
|
|
643
649
|
const effectiveTarget = opts.targetVersion ?? installedVersion;
|
|
644
|
-
const plan = createStackUpdatePlan(manifest, envRaw, effectiveTarget);
|
|
650
|
+
const plan = createStackUpdatePlan(manifest, envRaw, effectiveTarget, opts.serviceName ? [opts.serviceName] : void 0);
|
|
645
651
|
assertDeploymentScopeAllowed(plan, opts.deploymentPersona);
|
|
646
652
|
const plannedEnvRaw = patchEnv(envRaw, Object.fromEntries(plan.changes.map((c) => [c.key, c.after])));
|
|
647
653
|
assertProductionDeployGate(plan, plannedEnvRaw, readFileSync2(opts.composeFile, "utf8"), {
|
|
@@ -721,5 +727,6 @@ if (isMainModule(import.meta.url) && (process.argv[1] ?? "").includes("stack-upd
|
|
|
721
727
|
});
|
|
722
728
|
}
|
|
723
729
|
export {
|
|
730
|
+
parseArgs as parseStackUpdateArgs,
|
|
724
731
|
main as runStackUpdateCli
|
|
725
732
|
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/bin/vps-health-gate.ts
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var DEPLOY_LOG = "/opt/exe-stack/deploy-log.jsonl";
|
|
7
|
+
var BACKUP_DIR = "/opt/exe-stack/backups";
|
|
8
|
+
async function checkCRM() {
|
|
9
|
+
const start = Date.now();
|
|
10
|
+
try {
|
|
11
|
+
const status = await httpGet("http://localhost:3000/api");
|
|
12
|
+
return {
|
|
13
|
+
check: "crm",
|
|
14
|
+
status: status >= 200 && status < 400 ? "pass" : "fail",
|
|
15
|
+
message: status >= 200 && status < 400 ? `CRM healthy (HTTP ${status})` : `CRM unhealthy (HTTP ${status})`,
|
|
16
|
+
durationMs: Date.now() - start
|
|
17
|
+
};
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return {
|
|
20
|
+
check: "crm",
|
|
21
|
+
status: "fail",
|
|
22
|
+
message: `CRM unreachable: ${err instanceof Error ? err.message : err}`,
|
|
23
|
+
durationMs: Date.now() - start
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function checkGateway() {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
const status = await httpGet("http://localhost:3100/health");
|
|
31
|
+
return {
|
|
32
|
+
check: "gateway",
|
|
33
|
+
status: status >= 200 && status < 300 ? "pass" : "fail",
|
|
34
|
+
message: status >= 200 && status < 300 ? `Gateway healthy (HTTP ${status})` : `Gateway unhealthy (HTTP ${status})`,
|
|
35
|
+
durationMs: Date.now() - start
|
|
36
|
+
};
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return {
|
|
39
|
+
check: "gateway",
|
|
40
|
+
status: "fail",
|
|
41
|
+
message: `Gateway unreachable: ${err instanceof Error ? err.message : err}`,
|
|
42
|
+
durationMs: Date.now() - start
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function checkPostgres() {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
const databaseUrl = process.env.DATABASE_URL || "postgres://exe@localhost:5432/exedb";
|
|
49
|
+
const result = spawnSync("psql", [databaseUrl, "-c", "SELECT 1"], {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
+
timeout: 1e4
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
check: "postgres",
|
|
56
|
+
status: result.status === 0 ? "pass" : "fail",
|
|
57
|
+
message: result.status === 0 ? "Postgres responding" : `Postgres failed: ${result.stderr?.trim() || `exit ${result.status}`}`,
|
|
58
|
+
durationMs: Date.now() - start
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function checkRawEvents() {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
const databaseUrl = process.env.DATABASE_URL || "postgres://exe@localhost:5432/exedb";
|
|
64
|
+
const result = spawnSync(
|
|
65
|
+
"psql",
|
|
66
|
+
[databaseUrl, "-t", "-c", "SELECT count(*) FROM raw.raw_events"],
|
|
67
|
+
{
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
70
|
+
timeout: 1e4
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
if (result.status !== 0) {
|
|
74
|
+
return {
|
|
75
|
+
check: "raw_events",
|
|
76
|
+
status: "fail",
|
|
77
|
+
message: `raw.raw_events query failed: ${result.stderr?.trim() || `exit ${result.status}`}`,
|
|
78
|
+
durationMs: Date.now() - start
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const count = parseInt(result.stdout?.trim() || "0", 10);
|
|
82
|
+
return {
|
|
83
|
+
check: "raw_events",
|
|
84
|
+
status: count > 0 ? "pass" : "fail",
|
|
85
|
+
message: count > 0 ? `raw.raw_events has ${count} rows` : "raw.raw_events is empty",
|
|
86
|
+
durationMs: Date.now() - start
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function checkGoTrue() {
|
|
90
|
+
const start = Date.now();
|
|
91
|
+
try {
|
|
92
|
+
const status = await httpGet("http://localhost:9999/health");
|
|
93
|
+
return {
|
|
94
|
+
check: "gotrue",
|
|
95
|
+
status: status >= 200 && status < 300 ? "pass" : "fail",
|
|
96
|
+
message: status >= 200 && status < 300 ? `GoTrue healthy (HTTP ${status})` : `GoTrue unhealthy (HTTP ${status})`,
|
|
97
|
+
durationMs: Date.now() - start
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
check: "gotrue",
|
|
102
|
+
status: "fail",
|
|
103
|
+
message: `GoTrue unreachable: ${err instanceof Error ? err.message : err}`,
|
|
104
|
+
durationMs: Date.now() - start
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function runHealthGate() {
|
|
109
|
+
console.log("[health-gate] Running post-deploy health checks...\n");
|
|
110
|
+
const results = [];
|
|
111
|
+
results.push(checkPostgres());
|
|
112
|
+
results.push(checkRawEvents());
|
|
113
|
+
results.push(await checkCRM());
|
|
114
|
+
results.push(await checkGateway());
|
|
115
|
+
results.push(await checkGoTrue());
|
|
116
|
+
const passed = results.every((r) => r.status === "pass");
|
|
117
|
+
for (const r of results) {
|
|
118
|
+
const icon = r.status === "pass" ? "\u2713" : "\u2717";
|
|
119
|
+
const color = r.status === "pass" ? "\x1B[32m" : "\x1B[31m";
|
|
120
|
+
console.log(` ${color}${icon}\x1B[0m ${r.check.padEnd(12)} ${r.message} (${r.durationMs}ms)`);
|
|
121
|
+
}
|
|
122
|
+
console.log("");
|
|
123
|
+
const result = {
|
|
124
|
+
passed,
|
|
125
|
+
results,
|
|
126
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
127
|
+
};
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function logResult(result) {
|
|
131
|
+
mkdirSync(path.dirname(DEPLOY_LOG), { recursive: true });
|
|
132
|
+
const line = JSON.stringify({
|
|
133
|
+
...result,
|
|
134
|
+
type: "health_gate"
|
|
135
|
+
}) + "\n";
|
|
136
|
+
try {
|
|
137
|
+
appendFileSync(DEPLOY_LOG, line);
|
|
138
|
+
console.log(`[health-gate] Result logged to ${DEPLOY_LOG}`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn(`[health-gate] Failed to write deploy log: ${err instanceof Error ? err.message : err}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function restorePreDeployBackup() {
|
|
144
|
+
if (!existsSync(BACKUP_DIR)) return false;
|
|
145
|
+
const backups = readdirSync(BACKUP_DIR).filter((f) => f.startsWith("pg-pre-deploy-") && f.endsWith(".dump")).sort().reverse();
|
|
146
|
+
if (backups.length === 0) {
|
|
147
|
+
console.warn("[health-gate] No pre-deploy backup found to restore");
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const latest = backups[0];
|
|
151
|
+
const filepath = path.join(BACKUP_DIR, latest);
|
|
152
|
+
const databaseUrl = process.env.DATABASE_URL || "postgres://exe@localhost:5432/exedb";
|
|
153
|
+
console.log(`[health-gate] Restoring pre-deploy backup: ${latest}`);
|
|
154
|
+
const result = spawnSync("pg_restore", ["-d", databaseUrl, "--clean", "--if-exists", filepath], {
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
157
|
+
timeout: 3e5
|
|
158
|
+
});
|
|
159
|
+
if (result.status !== 0) {
|
|
160
|
+
console.error(`[health-gate] pg_restore failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
console.log("[health-gate] \u2713 Pre-deploy backup restored");
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
function httpGet(url) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const req = http.request(url, { method: "GET", timeout: 1e4 }, (res) => {
|
|
169
|
+
res.resume();
|
|
170
|
+
resolve(res.statusCode ?? 0);
|
|
171
|
+
});
|
|
172
|
+
req.on("timeout", () => req.destroy(new Error("timeout")));
|
|
173
|
+
req.on("error", reject);
|
|
174
|
+
req.end();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function main(args) {
|
|
178
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
179
|
+
console.log(`
|
|
180
|
+
exe-os vps-health-gate \u2014 Post-deploy health checks
|
|
181
|
+
|
|
182
|
+
Runs 5 checks: Postgres, raw_events, CRM, Gateway, GoTrue.
|
|
183
|
+
If any check fails, triggers rollback and exits with code 1.
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
exe-os vps-health-gate Run health gate
|
|
187
|
+
exe-os vps-health-gate --check-only Run checks without rollback on failure
|
|
188
|
+
`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const checkOnly = args.includes("--check-only");
|
|
192
|
+
const result = await runHealthGate();
|
|
193
|
+
logResult(result);
|
|
194
|
+
if (result.passed) {
|
|
195
|
+
console.log("[health-gate] \u2713 All checks passed \u2014 deploy is healthy");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const failed = result.results.filter((r) => r.status === "fail");
|
|
199
|
+
console.error(`[health-gate] \u2717 ${failed.length} check(s) failed`);
|
|
200
|
+
if (checkOnly) {
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log("[health-gate] Starting rollback...");
|
|
205
|
+
restorePreDeployBackup();
|
|
206
|
+
try {
|
|
207
|
+
const { rollbackStackUpdate, defaultStackPaths } = await import("./stack-update-QQA64STQ.js");
|
|
208
|
+
const paths = defaultStackPaths();
|
|
209
|
+
await rollbackStackUpdate({
|
|
210
|
+
manifestRef: paths.manifestRef,
|
|
211
|
+
composeFile: paths.composeFile,
|
|
212
|
+
envFile: paths.envFile
|
|
213
|
+
});
|
|
214
|
+
console.log("[health-gate] \u2713 Stack rolled back to previous version");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(`[health-gate] Stack rollback failed: ${err instanceof Error ? err.message : err}`);
|
|
217
|
+
}
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
checkCRM,
|
|
223
|
+
checkGateway,
|
|
224
|
+
checkPostgres,
|
|
225
|
+
checkRawEvents,
|
|
226
|
+
checkGoTrue,
|
|
227
|
+
runHealthGate,
|
|
228
|
+
logResult,
|
|
229
|
+
main
|
|
230
|
+
};
|