@askexenow/exe-os 0.9.268 → 0.9.269
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/deploy/compose/docker-compose.yml +13 -6
- package/dist/bin/stack-update.js +2 -2
- package/dist/bin/vps-health-gate.js +1 -1
- package/dist/chunk-AHVGHSWX.js +230 -0
- package/dist/chunk-UHKKI5MD.js +1391 -0
- package/dist/hooks/manifest.json +1 -1
- package/dist/stack-update-YJ433OLM.js +52 -0
- package/package.json +1 -1
- package/release-notes.json +31 -14
|
@@ -543,8 +543,11 @@ services:
|
|
|
543
543
|
restart: unless-stopped
|
|
544
544
|
# Token mode: `tunnel run --token <TOKEN>` — no local credentials file needed.
|
|
545
545
|
# The token embeds the tunnel ID + credentials; routes are managed in the CF dashboard.
|
|
546
|
+
# --metrics exposes a local HTTP endpoint used by the healthcheck.
|
|
546
547
|
command: >
|
|
547
|
-
tunnel
|
|
548
|
+
tunnel
|
|
549
|
+
--metrics localhost:2000
|
|
550
|
+
run
|
|
548
551
|
--token ${CLOUDFLARE_TUNNEL_TOKEN}
|
|
549
552
|
depends_on:
|
|
550
553
|
exe-crm:
|
|
@@ -560,11 +563,15 @@ services:
|
|
|
560
563
|
ipv4_address: 10.42.0.31
|
|
561
564
|
frontend:
|
|
562
565
|
ipv4_address: 10.43.0.31
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
566
|
+
healthcheck:
|
|
567
|
+
# `tunnel info` requires API credentials unavailable in token mode.
|
|
568
|
+
# The command above exposes cloudflared's local metrics endpoint; use it
|
|
569
|
+
# for container liveness without hitting the Cloudflare control plane.
|
|
570
|
+
test: ["CMD-SHELL", "pgrep -x cloudflared && wget -q --spider http://localhost:2000/ready || exit 1"]
|
|
571
|
+
interval: 30s
|
|
572
|
+
timeout: 5s
|
|
573
|
+
start_period: 30s
|
|
574
|
+
retries: 5
|
|
568
575
|
logging:
|
|
569
576
|
driver: json-file
|
|
570
577
|
options: { max-size: "10m", max-file: "3" }
|
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-UHKKI5MD.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-AHVGHSWX.js";
|
|
24
24
|
import "../chunk-MOZ2YQ54.js";
|
|
25
25
|
import "../chunk-VXIMSRTO.js";
|
|
26
26
|
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-YJ433OLM.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
|
+
};
|