cloud-ide-cide 2.0.34 → 2.0.36
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/deployer/node/upload-api.js +198 -49
- package/package.json +1 -1
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Reads config from: ../.cide/config.json (token, paths)
|
|
6
6
|
* Writes state to: ../.cide/deployments.json
|
|
7
|
-
*
|
|
8
|
-
* Stores
|
|
7
|
+
* Extracts code to: {server_root}/{appCode}/ (live app directory)
|
|
8
|
+
* Stores zips in: ../.cide/releases/{appCode}/{version}.zip (for rollback)
|
|
9
9
|
* Logs to: ../.cide/logs/deploy.log
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* After upload/rollback the listener automatically:
|
|
12
|
+
* 1. Runs npm install in the app directory
|
|
13
|
+
* 2. Stops the previous app process (if any)
|
|
14
|
+
* 3. Starts the app with `node server.js`
|
|
15
|
+
* 4. Tracks the app PID for future stop/restart
|
|
11
16
|
*
|
|
12
17
|
* Endpoints (all except /health require Authorization: Bearer <token>):
|
|
13
18
|
*
|
|
14
|
-
* POST /upload (multipart/form-data) — upload &
|
|
19
|
+
* POST /upload (multipart/form-data) — upload, deploy & start app
|
|
15
20
|
* GET /history?appCode=xxx — deployment history
|
|
16
|
-
* POST /rollback { appCode, version } —
|
|
21
|
+
* POST /rollback { appCode, version } — rollback & restart app
|
|
22
|
+
* POST /app/stop { appCode } — stop a running app
|
|
23
|
+
* POST /app/start { appCode } — start a stopped app
|
|
24
|
+
* GET /app/status?appCode=xxx — check app process status
|
|
17
25
|
* GET /health — uptime check (no auth)
|
|
18
26
|
*/
|
|
19
27
|
|
|
@@ -22,6 +30,7 @@ const multer = require('multer');
|
|
|
22
30
|
const path = require('path');
|
|
23
31
|
const fs = require('fs');
|
|
24
32
|
const AdmZip = require('adm-zip');
|
|
33
|
+
const { execSync, spawn } = require('child_process');
|
|
25
34
|
|
|
26
35
|
const app = express();
|
|
27
36
|
app.use(express.json());
|
|
@@ -36,10 +45,9 @@ try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { /* no
|
|
|
36
45
|
const TOKEN = config.token || process.env.CIDE_UPLOAD_TOKEN || '';
|
|
37
46
|
const PORT = process.env.CIDE_UPLOAD_PORT || process.env.PORT || 4500;
|
|
38
47
|
const RELEASES = config.paths?.releases || path.join(CIDE_DIR, 'releases');
|
|
39
|
-
const BACKUPS = config.paths?.backups || path.join(CIDE_DIR, 'backups');
|
|
40
48
|
const LOGS = config.paths?.logs || path.join(CIDE_DIR, 'logs');
|
|
41
|
-
const CURRENT_DIR = config.paths?.current || path.join(path.dirname(CIDE_DIR), 'current');
|
|
42
49
|
const DEPLOYMENTS = path.join(CIDE_DIR, 'deployments.json');
|
|
50
|
+
const SERVER_ROOT = config.serverRoot || path.dirname(CIDE_DIR);
|
|
43
51
|
|
|
44
52
|
const upload = multer({ dest: path.join(CIDE_DIR, 'tmp') });
|
|
45
53
|
|
|
@@ -59,35 +67,117 @@ function appendLog(message) {
|
|
|
59
67
|
try { fs.appendFileSync(logFile, line); } catch { /* logs dir may not exist yet */ }
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
// ── App process management ───────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function getAppPidFile(appCode) {
|
|
73
|
+
return path.join(CIDE_DIR, 'logs', `${appCode}.pid`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAppLogFile(appCode) {
|
|
77
|
+
return path.join(LOGS, `${appCode}.log`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readAppPid(appCode) {
|
|
64
81
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} catch { /* doesn't exist yet */ }
|
|
82
|
+
const pid = parseInt(fs.readFileSync(getAppPidFile(appCode), 'utf8').trim(), 10);
|
|
83
|
+
return isNaN(pid) ? null : pid;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
72
88
|
|
|
89
|
+
function isProcessRunning(pid) {
|
|
73
90
|
try {
|
|
74
|
-
|
|
91
|
+
process.kill(pid, 0);
|
|
92
|
+
return true;
|
|
75
93
|
} catch {
|
|
76
|
-
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stopApp(appCode) {
|
|
99
|
+
const pid = readAppPid(appCode);
|
|
100
|
+
if (!pid) return { stopped: false, reason: 'no pid file' };
|
|
101
|
+
|
|
102
|
+
if (!isProcessRunning(pid)) {
|
|
103
|
+
try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
|
|
104
|
+
return { stopped: false, reason: 'not running (stale pid cleaned)' };
|
|
77
105
|
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid, 'SIGTERM');
|
|
109
|
+
// Give it a moment to die, then force kill if needed
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
if (isProcessRunning(pid)) {
|
|
112
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
113
|
+
}
|
|
114
|
+
}, 3000);
|
|
115
|
+
try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
|
|
116
|
+
appendLog(`STOP app=${appCode} pid=${pid}`);
|
|
117
|
+
return { stopped: true, pid };
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return { stopped: false, reason: e.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function startApp(appCode) {
|
|
124
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
125
|
+
const entryFile = path.join(appDir, 'server.js');
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(entryFile)) {
|
|
128
|
+
return { started: false, reason: `Entry file not found: ${entryFile}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Stop existing process first
|
|
132
|
+
stopApp(appCode);
|
|
133
|
+
|
|
134
|
+
const logFile = getAppLogFile(appCode);
|
|
135
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
136
|
+
const out = fs.openSync(logFile, 'a');
|
|
137
|
+
const err = fs.openSync(logFile, 'a');
|
|
138
|
+
|
|
139
|
+
const child = spawn('node', [entryFile], {
|
|
140
|
+
cwd: appDir,
|
|
141
|
+
detached: true,
|
|
142
|
+
stdio: ['ignore', out, err],
|
|
143
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
child.unref();
|
|
147
|
+
|
|
148
|
+
// Save PID
|
|
149
|
+
fs.writeFileSync(getAppPidFile(appCode), String(child.pid));
|
|
150
|
+
appendLog(`START app=${appCode} pid=${child.pid} entry=${entryFile}`);
|
|
151
|
+
|
|
152
|
+
return { started: true, pid: child.pid, logFile };
|
|
78
153
|
}
|
|
79
154
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
155
|
+
function installAndStart(appCode) {
|
|
156
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
157
|
+
const steps = [];
|
|
158
|
+
|
|
159
|
+
// npm install if package.json exists
|
|
160
|
+
const pkgPath = path.join(appDir, 'package.json');
|
|
161
|
+
if (fs.existsSync(pkgPath)) {
|
|
162
|
+
try {
|
|
163
|
+
execSync('npm install --production', { cwd: appDir, stdio: 'pipe', shell: true, timeout: 120000 });
|
|
164
|
+
steps.push('npm install: ok');
|
|
165
|
+
appendLog(`INSTALL app=${appCode} npm install ok`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
steps.push(`npm install: failed (${e.message})`);
|
|
168
|
+
appendLog(`INSTALL app=${appCode} npm install FAILED: ${e.message}`);
|
|
89
169
|
}
|
|
90
170
|
}
|
|
171
|
+
|
|
172
|
+
// Start the app
|
|
173
|
+
const result = startApp(appCode);
|
|
174
|
+
if (result.started) {
|
|
175
|
+
steps.push(`started: pid ${result.pid}`);
|
|
176
|
+
} else {
|
|
177
|
+
steps.push(`start failed: ${result.reason}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { steps, ...result };
|
|
91
181
|
}
|
|
92
182
|
|
|
93
183
|
// ── Auth middleware ───────────────────────────────────────────────────────────
|
|
@@ -122,31 +212,35 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
|
|
|
122
212
|
}
|
|
123
213
|
|
|
124
214
|
const version = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
125
|
-
const releasePath = path.join(RELEASES, appCode, version);
|
|
126
|
-
const currentPath = path.join(CURRENT_DIR, appCode);
|
|
127
215
|
|
|
128
|
-
// ──
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
216
|
+
// ── Live app directory at server root ────────────────────────────────
|
|
217
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
218
|
+
|
|
219
|
+
// ── Save zip to releases for rollback ────────────────────────────────
|
|
220
|
+
const releaseDir = path.join(RELEASES, appCode);
|
|
221
|
+
fs.mkdirSync(releaseDir, { recursive: true });
|
|
222
|
+
const releaseZipPath = path.join(releaseDir, `${version}.zip`);
|
|
223
|
+
fs.copyFileSync(req.file.path, releaseZipPath);
|
|
132
224
|
|
|
133
|
-
// ── Extract
|
|
225
|
+
// ── Extract directly to {server_root}/{appCode}/ ─────────────────────
|
|
226
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
134
227
|
const zip = new AdmZip(req.file.path);
|
|
135
|
-
|
|
136
|
-
zip.extractAllTo(releasePath, true);
|
|
228
|
+
zip.extractAllTo(appDir, true);
|
|
137
229
|
fs.unlinkSync(req.file.path);
|
|
138
230
|
|
|
231
|
+
// ── Install dependencies & start the app ─────────────────────────────
|
|
232
|
+
const appResult = installAndStart(appCode);
|
|
233
|
+
|
|
139
234
|
// ── Track state ──────────────────────────────────────────────────────
|
|
140
235
|
const deployments = loadDeployments();
|
|
141
236
|
const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
|
|
142
237
|
|
|
143
|
-
switchCurrent(currentPath, releasePath);
|
|
144
|
-
|
|
145
|
-
// ── Record ───────────────────────────────────────────────────────────
|
|
146
238
|
const record = {
|
|
147
239
|
version,
|
|
148
240
|
environment,
|
|
149
|
-
|
|
241
|
+
appDir,
|
|
242
|
+
releaseZip: releaseZipPath,
|
|
243
|
+
appPid: appResult.pid || null,
|
|
150
244
|
deployedAt: new Date().toISOString(),
|
|
151
245
|
action: 'upload',
|
|
152
246
|
message: deployMsg,
|
|
@@ -164,12 +258,14 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
|
|
|
164
258
|
|
|
165
259
|
res.json({
|
|
166
260
|
status: 'SUCCESS',
|
|
167
|
-
message: '
|
|
261
|
+
message: 'Uploaded, installed & started.',
|
|
168
262
|
appCode,
|
|
169
263
|
environment,
|
|
170
264
|
releaseVersion: version,
|
|
171
265
|
previousVersion,
|
|
172
|
-
|
|
266
|
+
appDir,
|
|
267
|
+
releaseZip: releaseZipPath,
|
|
268
|
+
appProcess: appResult,
|
|
173
269
|
completedAt: new Date().toISOString(),
|
|
174
270
|
});
|
|
175
271
|
} catch (err) {
|
|
@@ -211,21 +307,29 @@ app.post('/rollback', authGuard, (req, res) => {
|
|
|
211
307
|
return res.status(422).json({ status: 'FAILED', message: 'Missing required fields: appCode, version' });
|
|
212
308
|
}
|
|
213
309
|
|
|
214
|
-
const
|
|
215
|
-
if (!fs.existsSync(
|
|
216
|
-
return res.status(404).json({ status: 'FAILED', message: `Release not found: ${version}` });
|
|
310
|
+
const releaseZipPath = path.join(RELEASES, appCode, `${version}.zip`);
|
|
311
|
+
if (!fs.existsSync(releaseZipPath)) {
|
|
312
|
+
return res.status(404).json({ status: 'FAILED', message: `Release zip not found: ${version}` });
|
|
217
313
|
}
|
|
218
314
|
|
|
219
|
-
const
|
|
315
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
220
316
|
const deployments = loadDeployments();
|
|
221
317
|
const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
|
|
222
318
|
|
|
223
|
-
|
|
319
|
+
// Re-extract the zip to the app directory
|
|
320
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
321
|
+
const zip = new AdmZip(releaseZipPath);
|
|
322
|
+
zip.extractAllTo(appDir, true);
|
|
323
|
+
|
|
324
|
+
// Install dependencies & restart the app
|
|
325
|
+
const appResult = installAndStart(appCode);
|
|
224
326
|
|
|
225
327
|
const record = {
|
|
226
328
|
version,
|
|
227
329
|
environment: deployments.apps?.[appCode]?.current?.environment || 'production',
|
|
228
|
-
|
|
330
|
+
appDir,
|
|
331
|
+
releaseZip: releaseZipPath,
|
|
332
|
+
appPid: appResult.pid || null,
|
|
229
333
|
deployedAt: new Date().toISOString(),
|
|
230
334
|
action: 'rollback',
|
|
231
335
|
previousVersion,
|
|
@@ -242,10 +346,11 @@ app.post('/rollback', authGuard, (req, res) => {
|
|
|
242
346
|
|
|
243
347
|
res.json({
|
|
244
348
|
status: 'SUCCESS',
|
|
245
|
-
message: `Rolled back to
|
|
349
|
+
message: `Rolled back to ${version} & restarted.`,
|
|
246
350
|
appCode,
|
|
247
351
|
rolledBackTo: version,
|
|
248
352
|
previousVersion,
|
|
353
|
+
appProcess: appResult,
|
|
249
354
|
completedAt: new Date().toISOString(),
|
|
250
355
|
});
|
|
251
356
|
} catch (err) {
|
|
@@ -253,6 +358,49 @@ app.post('/rollback', authGuard, (req, res) => {
|
|
|
253
358
|
}
|
|
254
359
|
});
|
|
255
360
|
|
|
361
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
362
|
+
// ── App management ────────────────────────────────────────────────────────────
|
|
363
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
364
|
+
|
|
365
|
+
app.post('/app/stop', authGuard, (req, res) => {
|
|
366
|
+
const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
367
|
+
if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
|
|
368
|
+
|
|
369
|
+
const result = stopApp(appCode);
|
|
370
|
+
res.json({ status: 'SUCCESS', appCode, ...result });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
app.post('/app/start', authGuard, (req, res) => {
|
|
374
|
+
const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
375
|
+
if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
|
|
376
|
+
|
|
377
|
+
const result = startApp(appCode);
|
|
378
|
+
res.json({ status: result.started ? 'SUCCESS' : 'FAILED', appCode, ...result });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
app.get('/app/status', authGuard, (req, res) => {
|
|
382
|
+
const appCode = (req.query.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
383
|
+
if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
|
|
384
|
+
|
|
385
|
+
const pid = readAppPid(appCode);
|
|
386
|
+
const running = pid ? isProcessRunning(pid) : false;
|
|
387
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
388
|
+
const logFile = getAppLogFile(appCode);
|
|
389
|
+
|
|
390
|
+
if (pid && !running) {
|
|
391
|
+
try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
res.json({
|
|
395
|
+
status: 'SUCCESS',
|
|
396
|
+
appCode,
|
|
397
|
+
running,
|
|
398
|
+
pid: running ? pid : null,
|
|
399
|
+
appDir,
|
|
400
|
+
logFile,
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
256
404
|
// ── Health check ──────────────────────────────────────────────────────────────
|
|
257
405
|
app.get('/health', (_req, res) => {
|
|
258
406
|
res.json({ status: 'ok', uptime: process.uptime() });
|
|
@@ -262,4 +410,5 @@ app.get('/health', (_req, res) => {
|
|
|
262
410
|
app.listen(PORT, () => {
|
|
263
411
|
console.log(`Cloud IDE upload-api listening on port ${PORT}`);
|
|
264
412
|
console.log(`.cide dir: ${CIDE_DIR}`);
|
|
413
|
+
console.log(`Server root: ${SERVER_ROOT}`);
|
|
265
414
|
});
|