NepsenX commited on
Commit
95f0734
Β·
verified Β·
1 Parent(s): 3f09a34

Update src/index.ts

Browse files
Files changed (1) hide show
  1. src/index.ts +146 -108
src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
  import * as http from 'node:http';
2
- import { exec, spawn, ChildProcess } from 'node:child_process';
3
  import * as fs from 'node:fs';
4
  import * as path from 'node:path';
5
  import { fileURLToPath } from 'node:url';
@@ -104,57 +104,113 @@ async function cloneOrPull(repoUrl: string, targetDir: string): Promise<void> {
104
  }
105
 
106
  // ============================================================
107
- // 🧠 READ EXPOSE PORT FROM RES1's Dockerfile
108
  // ============================================================
109
- async function getExposedPort(versionDir: string): Promise<number> {
110
- const dockerfilePath = path.join(versionDir, 'Dockerfile');
111
- if (!fs.existsSync(dockerfilePath)) {
112
- console.warn('⚠️ No Dockerfile found in Res1. Using default port 7000.');
113
- return 7000;
114
- }
115
- const content = fs.readFileSync(dockerfilePath, 'utf-8');
116
- const match = content.match(/EXPOSE\s+(\d+)/);
117
- if (match && match[1]) {
118
- const port = parseInt(match[1], 10);
119
- console.log(`βœ… Found EXPOSE port ${port} in Res1 Dockerfile`);
120
- return port;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
- console.warn('⚠️ No EXPOSE directive found. Using default port 7000.');
123
- return 7000;
124
  }
125
 
126
  // ============================================================
127
- // πŸ€– DEPLOY AGENT (subprocess, not Docker)
128
  // ============================================================
129
- function deployAgent(versionDir: string, port: number): Promise<ChildProcess> {
130
- return new Promise((resolve, reject) => {
131
- console.log(`πŸš€ Starting agent on port ${port} from ${versionDir}...`);
132
- exec(`npm install && npm run build`, { cwd: versionDir }, (err) => {
133
- if (err) return reject(`Build failed: ${err.message}`);
134
- const proc = spawn('npm', ['start'], {
135
- cwd: versionDir,
136
- env: { ...process.env, PORT: String(port) },
137
- stdio: 'pipe'
138
- });
139
- proc.stdout.on('data', (data) => console.log(`[AGENT:${port}] ${data}`));
140
- proc.stderr.on('data', (data) => console.error(`[AGENT ERR:${port}] ${data}`));
141
- setTimeout(async () => {
142
- try {
143
- const res = await axios.get(`http://localhost:${port}/health`, { timeout: 3000 });
144
- if (res.status === 200) {
145
- console.log(`βœ… Health check passed on port ${port}!`);
146
- resolve(proc);
147
- } else reject(`Health check failed on port ${port}`);
148
- } catch (err: any) {
149
- reject(`Health check error on port ${port}: ${err.message}`);
150
- }
151
- }, 8000);
152
- });
153
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  }
155
 
156
  // ============================================================
157
- // MEMORY COPY
158
  // ============================================================
159
  function copyMemoryFolder(srcPath: string, dstPath: string): void {
160
  const srcMemory = path.join(srcPath, 'memory');
@@ -178,8 +234,7 @@ function loadFailedVersions(): string[] {
178
  try {
179
  if (fs.existsSync(FAILED_VERSIONS_FILE)) {
180
  const data = fs.readFileSync(FAILED_VERSIONS_FILE, 'utf-8');
181
- const parsed = JSON.parse(data);
182
- if (Array.isArray(parsed)) return parsed;
183
  }
184
  } catch (e) {
185
  console.warn('⚠️ Failed to load failed-versions.json');
@@ -189,8 +244,7 @@ function loadFailedVersions(): string[] {
189
 
190
  function saveFailedVersion(sha: string): void {
191
  try {
192
- let failed = loadFailedVersions();
193
- failed = failed.filter(v => v !== sha);
194
  failed.push(sha);
195
  if (failed.length > MAX_FAILED_VERSIONS) {
196
  failed = failed.slice(-MAX_FAILED_VERSIONS);
@@ -203,15 +257,13 @@ function saveFailedVersion(sha: string): void {
203
  }
204
 
205
  function isVersionFailed(sha: string): boolean {
206
- const failed = loadFailedVersions();
207
- return failed.includes(sha);
208
  }
209
 
210
  function clearFailedVersion(sha: string): void {
211
  try {
212
- const failed = loadFailedVersions();
213
- const updated = failed.filter(v => v !== sha);
214
- fs.writeFileSync(FAILED_VERSIONS_FILE, JSON.stringify(updated, null, 2));
215
  console.log(`πŸ—‘οΈ Failed version ${sha} removed from tracking list.`);
216
  } catch (e) {
217
  console.error('❌ Failed to clear failed version:', e);
@@ -221,14 +273,14 @@ function clearFailedVersion(sha: string): void {
221
  // ============================================================
222
  // STATE
223
  // ============================================================
224
- let prodProcess: ChildProcess | null = null;
225
  let prodVersionPath: string | null = null;
226
  let prodCommitSha: string | null = null;
227
- let prodPort: number | null = null;
228
  let isUpdating: boolean = false;
229
 
230
  // ============================================================
231
- // 🧠 MAIN UPDATE LOGIC
232
  // ============================================================
233
  async function checkAndUpdate(): Promise<void> {
234
  if (isUpdating) {
@@ -237,10 +289,10 @@ async function checkAndUpdate(): Promise<void> {
237
  }
238
 
239
  isUpdating = true;
240
- let testProcess: ChildProcess | null = null;
241
  let testVersionPath: string | null = null;
242
  let testCommitSha: string | null = null;
243
- let testPort: number | null = null;
244
 
245
  try {
246
  console.log('πŸ” Checking for updates...');
@@ -251,7 +303,6 @@ async function checkAndUpdate(): Promise<void> {
251
  console.log(`πŸ“Œ Latest SHA: ${latestSha.substring(0, 7)}`);
252
  console.log(`πŸ“Œ Current Prod SHA: ${prodCommitSha ? prodCommitSha.substring(0, 7) : 'none'}`);
253
 
254
- // 1. New code?
255
  if (latestSha === prodCommitSha) {
256
  console.log('βœ… No new code. Current version is already running.');
257
  if (prodCommitSha && isVersionFailed(prodCommitSha)) {
@@ -261,23 +312,18 @@ async function checkAndUpdate(): Promise<void> {
261
  return;
262
  }
263
 
264
- // 2. Previously failed?
265
  if (isVersionFailed(latestSha)) {
266
  console.log(`⏳ Version ${latestSha.substring(0, 7)} is marked as FAILED. Waiting for a newer version.`);
267
  isUpdating = false;
268
  return;
269
  }
270
 
271
- // 3. New version detected – clone and test
272
- console.log(`πŸ†• New version: ${latestSha.substring(0, 7)}. Deploying test on dynamic port...`);
273
  testCommitSha = latestSha;
274
  testVersionPath = path.join(CONFIG.VERSIONS_DIR, `v2_${Date.now()}`);
275
  await cloneOrPull(CONFIG.RES1_REPO, testVersionPath);
276
 
277
- // Read EXPOSE port from Res1 Dockerfile
278
- testPort = await getExposedPort(testVersionPath);
279
- console.log(`πŸ”„ Test agent will run on port ${testPort}`);
280
-
281
  // Copy memory from production if exists
282
  if (prodVersionPath) {
283
  copyMemoryFolder(prodVersionPath, testVersionPath);
@@ -285,79 +331,71 @@ async function checkAndUpdate(): Promise<void> {
285
  console.log('⚠️ No previous version. Starting fresh without memory copy.');
286
  }
287
 
288
- // Deploy test agent
289
- testProcess = await deployAgent(testVersionPath, testPort);
290
- console.log(`βœ… Test agent passed health check on port ${testPort}!`);
291
-
292
- // Stop test process (we'll promote it)
293
- if (testProcess) {
294
- console.log('πŸ›‘ Stopping test agent...');
295
- testProcess.kill('SIGTERM');
296
- await new Promise(resolve => setTimeout(resolve, 2000));
297
- if (!testProcess.killed) testProcess.kill('SIGKILL');
298
- testProcess = null;
 
 
 
 
299
  }
300
 
301
- // 4. Promote to production
302
- console.log(`πŸš€ Promoting to production on port ${testPort}...`);
303
- const newProdProc = await deployAgent(testVersionPath, testPort);
304
-
305
- // Stop old production
306
- if (prodProcess) {
307
- console.log('πŸ›‘ Stopping old production...');
308
- prodProcess.kill('SIGTERM');
309
- setTimeout(() => {
310
- if (!prodProcess?.killed) prodProcess?.kill('SIGKILL');
311
- }, 3000);
312
  }
313
 
314
  // Update state
315
- prodProcess = newProdProc;
316
  prodVersionPath = testVersionPath;
317
  prodCommitSha = testCommitSha;
318
- prodPort = testPort;
319
  testVersionPath = null;
320
  testCommitSha = null;
321
- testPort = null;
322
 
323
  if (prodCommitSha && isVersionFailed(prodCommitSha)) {
324
  clearFailedVersion(prodCommitSha);
325
  }
326
 
327
- console.log(`βœ… Successfully deployed V2 (${latestSha.substring(0, 7)}) on port ${prodPort}`);
328
 
329
  } catch (error: any) {
330
  console.error('❌ Deployment failed:', error);
331
 
332
- // Stop test process if running
333
- if (testProcess) {
334
- console.log('πŸ›‘ Stopping failed test agent...');
335
- testProcess.kill('SIGTERM');
336
- setTimeout(() => {
337
- if (!testProcess?.killed) testProcess?.kill('SIGKILL');
338
- }, 2000);
339
- testProcess = null;
340
  }
341
 
342
- // Save failed version
343
  if (testCommitSha) {
344
  saveFailedVersion(testCommitSha);
345
  }
346
 
347
- // Send email
348
  await sendEmail(
349
  'Deployment FAILED',
350
- `Version: ${testCommitSha ? testCommitSha.substring(0, 7) : 'unknown'}\nError: ${error.message}\nPort ${prodPort || '?'} is unchanged.`
351
  );
352
 
353
- // Keep old production
354
- if (prodProcess) {
355
- console.log(`πŸ”„ Keeping old version running on port ${prodPort}`);
356
  } else {
357
  console.log('⚠️ No previous version available.');
358
  }
359
 
360
- // Clean up test directory
361
  if (testVersionPath && fs.existsSync(testVersionPath)) {
362
  try {
363
  fs.rmSync(testVersionPath, { recursive: true, force: true });
@@ -389,8 +427,8 @@ const server = http.createServer(async (req, res) => {
389
  const failed = loadFailedVersions();
390
  res.writeHead(200, { 'Content-Type': 'application/json' });
391
  res.end(JSON.stringify({
392
- prodRunning: prodProcess !== null,
393
- prodPort: prodPort,
394
  prodVersion: prodCommitSha ? prodCommitSha.substring(0, 7) : 'none',
395
  isUpdating,
396
  failedVersions: failed.map(s => s.substring(0, 7))
 
1
  import * as http from 'node:http';
2
+ import { exec, spawn } from 'node:child_process';
3
  import * as fs from 'node:fs';
4
  import * as path from 'node:path';
5
  import { fileURLToPath } from 'node:url';
 
104
  }
105
 
106
  // ============================================================
107
+ // 🐳 DOCKER UTILITIES FOR RES1
108
  // ============================================================
109
+ function execDocker(cmd: string): Promise<string> {
110
+ return new Promise((resolve, reject) => {
111
+ exec(cmd, (error, stdout, stderr) => {
112
+ if (error) reject(stderr || error.message);
113
+ else resolve(stdout.trim());
114
+ });
115
+ });
116
+ }
117
+
118
+ async function buildAndRunRes1(
119
+ versionDir: string,
120
+ containerName: string,
121
+ portMapping: string // e.g., "7000:7000" or "2000:2000"
122
+ ): Promise<number> {
123
+ const imageTag = `oracus-${Date.now()}`;
124
+ console.log(`🐳 Building Docker image ${imageTag} from ${versionDir}...`);
125
+ await execDocker(`docker build -t ${imageTag} ${versionDir}`);
126
+
127
+ console.log(`🐳 Running container ${containerName} with port ${portMapping}...`);
128
+ await execDocker(
129
+ `docker run -d --name ${containerName} -p ${portMapping} ${imageTag}`
130
+ );
131
+
132
+ // Find the actual host port mapped
133
+ const inspect = await execDocker(
134
+ `docker inspect ${containerName} --format='{{range $p, $conf := .NetworkSettings.Ports}}{{(index $conf 0).HostPort}} {{end}}'`
135
+ );
136
+ const ports = inspect.trim().split(/\s+/);
137
+ if (ports.length === 0) throw new Error('No ports exposed');
138
+ const hostPort = parseInt(ports[0], 10);
139
+ console.log(`βœ… Container running on host port ${hostPort}`);
140
+ return hostPort;
141
+ }
142
+
143
+ async function stopAndRemoveContainer(containerName: string): Promise<void> {
144
+ try {
145
+ await execDocker(`docker stop ${containerName}`);
146
+ await execDocker(`docker rm ${containerName}`);
147
+ console.log(`🧹 Removed container ${containerName}`);
148
+ } catch (e) {
149
+ console.warn(`⚠️ Could not remove container ${containerName}:`, e);
150
  }
 
 
151
  }
152
 
153
  // ============================================================
154
+ // πŸ€– DEPLOY AGENT (USING DOCKER)
155
  // ============================================================
156
+ async function deployAgentWithDocker(
157
+ versionDir: string,
158
+ containerName: string,
159
+ port: number
160
+ ): Promise<number> {
161
+ // Res1's Dockerfile exposes ports; we map the same port internally
162
+ // but let Docker assign a random host port (-P) or we can map explicitly.
163
+ // Here we use explicit mapping: hostPort:containerPort
164
+ // Since Res1 listens on the port we give (via PORT env), we map that.
165
+ // But Res1's Dockerfile may have multiple EXPOSE; we'll use the first one.
166
+ // Simpler: just use -P (random host port) and inspect.
167
+ // But we need health check on that port.
168
+ // We'll build with PORT env set in the container.
169
+ // Actually, we can pass -e PORT=7000 during docker run.
170
+ // But Res1's code reads PORT env. So we set it.
171
+ const imageTag = `oracus-${Date.now()}`;
172
+ console.log(`🐳 Building Docker image ${imageTag} from ${versionDir}...`);
173
+ await execDocker(`docker build -t ${imageTag} ${versionDir}`);
174
+
175
+ console.log(`🐳 Running container ${containerName} with PORT=${port}...`);
176
+ // Run with -P to let Docker assign random host ports, but we also set PORT env.
177
+ await execDocker(
178
+ `docker run -d --name ${containerName} -P -e PORT=${port} ${imageTag}`
179
+ );
180
+
181
+ // Inspect to get mapped host port for the container's exposed port
182
+ const inspect = await execDocker(
183
+ `docker inspect ${containerName} --format='{{range $p, $conf := .NetworkSettings.Ports}}{{(index $conf 0).HostPort}} {{end}}'`
184
+ );
185
+ const ports = inspect.trim().split(/\s+/);
186
+ if (ports.length === 0) throw new Error('No ports exposed by container');
187
+ const hostPort = parseInt(ports[0], 10);
188
+ console.log(`βœ… Container running on host port ${hostPort}`);
189
+
190
+ // Health check on hostPort
191
+ console.log(`⏳ Waiting for health check on port ${hostPort}...`);
192
+ let healthy = false;
193
+ for (let attempt = 0; attempt < 20; attempt++) {
194
+ try {
195
+ const res = await axios.get(`http://localhost:${hostPort}/health`, { timeout: 2000 });
196
+ if (res.status === 200) {
197
+ healthy = true;
198
+ break;
199
+ }
200
+ } catch (e) {
201
+ // ignore
202
+ }
203
+ await new Promise(r => setTimeout(r, 1000));
204
+ }
205
+ if (!healthy) {
206
+ throw new Error(`Health check failed on port ${hostPort}`);
207
+ }
208
+ console.log(`βœ… Health check passed on port ${hostPort}`);
209
+ return hostPort;
210
  }
211
 
212
  // ============================================================
213
+ // MEMORY COPY (between version directories)
214
  // ============================================================
215
  function copyMemoryFolder(srcPath: string, dstPath: string): void {
216
  const srcMemory = path.join(srcPath, 'memory');
 
234
  try {
235
  if (fs.existsSync(FAILED_VERSIONS_FILE)) {
236
  const data = fs.readFileSync(FAILED_VERSIONS_FILE, 'utf-8');
237
+ return JSON.parse(data);
 
238
  }
239
  } catch (e) {
240
  console.warn('⚠️ Failed to load failed-versions.json');
 
244
 
245
  function saveFailedVersion(sha: string): void {
246
  try {
247
+ let failed = loadFailedVersions().filter(v => v !== sha);
 
248
  failed.push(sha);
249
  if (failed.length > MAX_FAILED_VERSIONS) {
250
  failed = failed.slice(-MAX_FAILED_VERSIONS);
 
257
  }
258
 
259
  function isVersionFailed(sha: string): boolean {
260
+ return loadFailedVersions().includes(sha);
 
261
  }
262
 
263
  function clearFailedVersion(sha: string): void {
264
  try {
265
+ const failed = loadFailedVersions().filter(v => v !== sha);
266
+ fs.writeFileSync(FAILED_VERSIONS_FILE, JSON.stringify(failed, null, 2));
 
267
  console.log(`πŸ—‘οΈ Failed version ${sha} removed from tracking list.`);
268
  } catch (e) {
269
  console.error('❌ Failed to clear failed version:', e);
 
273
  // ============================================================
274
  // STATE
275
  // ============================================================
276
+ let prodContainerName: string | null = null;
277
  let prodVersionPath: string | null = null;
278
  let prodCommitSha: string | null = null;
279
+ let prodHostPort: number | null = null;
280
  let isUpdating: boolean = false;
281
 
282
  // ============================================================
283
+ // 🧠 MAIN UPDATE LOGIC (Docker-based)
284
  // ============================================================
285
  async function checkAndUpdate(): Promise<void> {
286
  if (isUpdating) {
 
289
  }
290
 
291
  isUpdating = true;
292
+ let testContainerName: string | null = null;
293
  let testVersionPath: string | null = null;
294
  let testCommitSha: string | null = null;
295
+ let testHostPort: number | null = null;
296
 
297
  try {
298
  console.log('πŸ” Checking for updates...');
 
303
  console.log(`πŸ“Œ Latest SHA: ${latestSha.substring(0, 7)}`);
304
  console.log(`πŸ“Œ Current Prod SHA: ${prodCommitSha ? prodCommitSha.substring(0, 7) : 'none'}`);
305
 
 
306
  if (latestSha === prodCommitSha) {
307
  console.log('βœ… No new code. Current version is already running.');
308
  if (prodCommitSha && isVersionFailed(prodCommitSha)) {
 
312
  return;
313
  }
314
 
 
315
  if (isVersionFailed(latestSha)) {
316
  console.log(`⏳ Version ${latestSha.substring(0, 7)} is marked as FAILED. Waiting for a newer version.`);
317
  isUpdating = false;
318
  return;
319
  }
320
 
321
+ console.log(`πŸ†• New version: ${latestSha.substring(0, 7)}. Deploying test container on port 2000...`);
322
+
323
  testCommitSha = latestSha;
324
  testVersionPath = path.join(CONFIG.VERSIONS_DIR, `v2_${Date.now()}`);
325
  await cloneOrPull(CONFIG.RES1_REPO, testVersionPath);
326
 
 
 
 
 
327
  // Copy memory from production if exists
328
  if (prodVersionPath) {
329
  copyMemoryFolder(prodVersionPath, testVersionPath);
 
331
  console.log('⚠️ No previous version. Starting fresh without memory copy.');
332
  }
333
 
334
+ // Deploy test container using Res1's Dockerfile, mapping internal 2000 to host
335
+ // But we use -P, so we don't need to specify host port; Docker assigns random.
336
+ // However, Res1's Dockerfile must have EXPOSE 2000. It does.
337
+ // We'll run with PORT=2000 env.
338
+ testContainerName = `oracus-test-${Date.now()}`;
339
+ testHostPort = await deployAgentWithDocker(testVersionPath, testContainerName, 2000);
340
+ console.log(`βœ… Test container running on host port ${testHostPort} (internal 2000)`);
341
+
342
+ // Promote to production (port 7000)
343
+ console.log(`πŸš€ Promoting to production (internal port 7000)...`);
344
+
345
+ // Stop and remove test container
346
+ if (testContainerName) {
347
+ await stopAndRemoveContainer(testContainerName);
348
+ testContainerName = null;
349
  }
350
 
351
+ // Build and run production container with internal port 7000
352
+ const prodContainerNameNew = `oracus-prod-${Date.now()}`;
353
+ const prodHostPortNew = await deployAgentWithDocker(testVersionPath, prodContainerNameNew, 7000);
354
+ console.log(`βœ… Production container running on host port ${prodHostPortNew} (internal 7000)`);
355
+
356
+ // Stop old production container
357
+ if (prodContainerName) {
358
+ await stopAndRemoveContainer(prodContainerName);
 
 
 
359
  }
360
 
361
  // Update state
362
+ prodContainerName = prodContainerNameNew;
363
  prodVersionPath = testVersionPath;
364
  prodCommitSha = testCommitSha;
365
+ prodHostPort = prodHostPortNew;
366
  testVersionPath = null;
367
  testCommitSha = null;
368
+ testHostPort = null;
369
 
370
  if (prodCommitSha && isVersionFailed(prodCommitSha)) {
371
  clearFailedVersion(prodCommitSha);
372
  }
373
 
374
+ console.log(`βœ… Successfully deployed V2 (${latestSha.substring(0, 7)}) on host port ${prodHostPort}`);
375
 
376
  } catch (error: any) {
377
  console.error('❌ Deployment failed:', error);
378
 
379
+ if (testContainerName) {
380
+ await stopAndRemoveContainer(testContainerName);
381
+ testContainerName = null;
 
 
 
 
 
382
  }
383
 
 
384
  if (testCommitSha) {
385
  saveFailedVersion(testCommitSha);
386
  }
387
 
 
388
  await sendEmail(
389
  'Deployment FAILED',
390
+ `Version: ${testCommitSha ? testCommitSha.substring(0, 7) : 'unknown'}\nError: ${error.message}\nPort ${prodHostPort || '?'} is unchanged.`
391
  );
392
 
393
+ if (prodContainerName) {
394
+ console.log(`πŸ”„ Keeping old version running on host port ${prodHostPort}`);
 
395
  } else {
396
  console.log('⚠️ No previous version available.');
397
  }
398
 
 
399
  if (testVersionPath && fs.existsSync(testVersionPath)) {
400
  try {
401
  fs.rmSync(testVersionPath, { recursive: true, force: true });
 
427
  const failed = loadFailedVersions();
428
  res.writeHead(200, { 'Content-Type': 'application/json' });
429
  res.end(JSON.stringify({
430
+ prodRunning: prodContainerName !== null,
431
+ prodHostPort: prodHostPort,
432
  prodVersion: prodCommitSha ? prodCommitSha.substring(0, 7) : 'none',
433
  isUpdating,
434
  failedVersions: failed.map(s => s.substring(0, 7))