@askexenow/exe-os 0.9.283 → 0.9.284
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/stack-update.js +2 -2
- package/dist/bin/vps-health-gate.js +1 -1
- package/dist/chunk-AQPTGQYH.js +230 -0
- package/dist/chunk-OJQ34M77.js +1876 -0
- package/dist/hooks/manifest.json +1 -1
- package/dist/stack-update-TRZRI6V7.js +74 -0
- package/package.json +1 -1
- package/release-notes.json +61 -59
package/dist/bin/stack-update.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runStackUpdate,
|
|
18
18
|
saveServiceSelection,
|
|
19
19
|
verifyManifestImagesAvailable
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-OJQ34M77.js";
|
|
21
21
|
import {
|
|
22
22
|
runVerifyStack
|
|
23
23
|
} from "../chunk-IRHNV4GY.js";
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
logResult,
|
|
29
29
|
runHealthGate
|
|
30
|
-
} from "../chunk-
|
|
30
|
+
} from "../chunk-AQPTGQYH.js";
|
|
31
31
|
import "../chunk-YMKUXZIG.js";
|
|
32
32
|
import "../chunk-T3B5RK4H.js";
|
|
33
33
|
import "../chunk-LYH5HE24.js";
|
|
@@ -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-TRZRI6V7.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
|
+
};
|