fa-mcp-sdk 0.2.84 → 0.2.87
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/cli-template/update.cjs
CHANGED
|
@@ -1,631 +1,642 @@
|
|
|
1
|
-
// noinspection UnnecessaryLocalVariableJS
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execSync, spawn } = require('child_process');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const version = '2025.11.
|
|
9
|
-
console.log(`Update script version: ${version}`);
|
|
10
|
-
|
|
11
|
-
// Имя этой папки
|
|
12
|
-
const scriptDirName = require('path').basename(__dirname);
|
|
13
|
-
// Смена рабочей директории на директорию скрипта
|
|
14
|
-
process.chdir(__dirname);
|
|
15
|
-
const CWD = process.cwd();
|
|
16
|
-
const VON = path.resolve(path.join(CWD, '..'));
|
|
17
|
-
|
|
18
|
-
// Default configuration
|
|
19
|
-
const DEFAULT_CONFIG = {
|
|
20
|
-
branch: 'master',
|
|
21
|
-
email: '',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Colors for terminal output
|
|
25
|
-
const colors = {
|
|
26
|
-
reset: '\x1b[0m',
|
|
27
|
-
bright: '\x1b[1m',
|
|
28
|
-
dim: '\x1b[2m',
|
|
29
|
-
red: '\x1b[31m',
|
|
30
|
-
green: '\x1b[32m',
|
|
31
|
-
yellow: '\x1b[33m',
|
|
32
|
-
blue: '\x1b[34m',
|
|
33
|
-
magenta: '\x1b[35m',
|
|
34
|
-
cyan: '\x1b[36m',
|
|
35
|
-
white: '\x1b[37m',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const color = {};
|
|
39
|
-
const colorG = {};
|
|
40
|
-
['cyan', 'green', 'magenta', 'red', 'yellow'].forEach((col) => {
|
|
41
|
-
const firstLetter = col[0];
|
|
42
|
-
color[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.reset}`;
|
|
43
|
-
color[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.reset}`;
|
|
44
|
-
colorG[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.green}`;
|
|
45
|
-
colorG[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.green}`;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Echo functions with colors
|
|
49
|
-
const echo = {
|
|
50
|
-
c: (text) => console.log(color.c(text)),
|
|
51
|
-
lc: (text) => console.log(color.lc(text)),
|
|
52
|
-
g: (text) => console.log(color.g(text)),
|
|
53
|
-
lg: (text) => console.log(color.lg(text)),
|
|
54
|
-
m: (text) => console.log(color.m(text)),
|
|
55
|
-
lm: (text) => console.log(color.lm(text)),
|
|
56
|
-
r: (text) => console.log(color.r(text)),
|
|
57
|
-
lr: (text) => console.log(color.lr(text)),
|
|
58
|
-
y: (text) => console.log(color.y(text)),
|
|
59
|
-
ly: (text) => console.log(color.ly(text)),
|
|
60
|
-
lg_no_newline: (msg) => process.stdout.write(color.lg(msg)),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
let logBuffer = '';
|
|
64
|
-
|
|
65
|
-
// Global variable to store NVM environment
|
|
66
|
-
let setupScript = '';
|
|
67
|
-
let nodeVersion = null;
|
|
68
|
-
const DEFAULT_NODE_VERSION = '22.17.1';
|
|
69
|
-
|
|
70
|
-
const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0].replace('T', '');
|
|
71
|
-
const ytdl = timestamp.slice(2, 14); // YYMMDDHHMMSS format
|
|
72
|
-
const runTimeLogFile = path.join(VON, `deploy__${scriptDirName}__processing__${ytdl}.log`);
|
|
73
|
-
const lastDeployLogFile = path.join(VON, `deploy__${scriptDirName}__last_deploy.log`);
|
|
74
|
-
const cumulativeLogFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
|
|
75
|
-
|
|
76
|
-
const clearColors = (text) => text.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
|
|
77
|
-
const clearHtmlColors = (text) => text.replace(/<\/?(red|y|g|r|status)>/g, '');
|
|
78
|
-
|
|
79
|
-
const logIt = (msg, isTitle) => {
|
|
80
|
-
if (isTitle) {
|
|
81
|
-
const lng = 60 - (msg.length + 2);
|
|
82
|
-
const left = Math.floor(lng / 2);
|
|
83
|
-
const right = lng - left;
|
|
84
|
-
msg = `${'='.repeat(left)} ${msg} ${'='.repeat(right)}`;
|
|
85
|
-
}
|
|
86
|
-
const msg4console = clearHtmlColors(msg);
|
|
87
|
-
echo.g(msg4console);
|
|
88
|
-
logBuffer += `${msg}\n`;
|
|
89
|
-
fs.appendFileSync(runTimeLogFile, `${clearColors(msg4console)}\n`);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const logError = (msg) => {
|
|
93
|
-
console.error(color.r(msg));
|
|
94
|
-
const msg2 = `[ERROR] ${msg}`;
|
|
95
|
-
logBuffer += `<red>${msg2}</red>\n`;
|
|
96
|
-
fs.appendFileSync(cumulativeLogFile, `${msg2}\n`);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const nowPretty = () => new Date().toISOString().replace('T', ' ').substring(0, 19) + 'Z';
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Truncate cumulative log file if it exceeds 2MB, keeping last 10KB
|
|
103
|
-
*/
|
|
104
|
-
const truncateCumulativeLogIfNeeded = () => {
|
|
105
|
-
const logFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
|
|
106
|
-
const maxFileSize = 2 * 1024 * 1024; // 2MB
|
|
107
|
-
const keepSize = 10 * 1024; // 10KB
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
if (fs.existsSync(logFile)) {
|
|
111
|
-
const stats = fs.statSync(logFile);
|
|
112
|
-
if (stats.size > maxFileSize) {
|
|
113
|
-
// Read last 10KB
|
|
114
|
-
const fd = fs.openSync(logFile, 'r');
|
|
115
|
-
const buffer = Buffer.alloc(keepSize);
|
|
116
|
-
|
|
117
|
-
// Position to last 10KB
|
|
118
|
-
fs.readSync(fd, buffer, 0, keepSize, stats.size - keepSize);
|
|
119
|
-
fs.closeSync(fd);
|
|
120
|
-
|
|
121
|
-
// Write back only the last 10KB
|
|
122
|
-
const tailContent = buffer.toString('utf8').replace(/^[\r\n]*/, ''); // Remove leading newlines
|
|
123
|
-
fs.writeFileSync(logFile, tailContent);
|
|
124
|
-
|
|
125
|
-
log(`Cumulative log truncated to ${Math.round(tailContent.length / 1024)}KB`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
logError(`Failed to truncate cumulative log: ${error.message}`);
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const logTryUpdate = (updateReason = '') => {
|
|
134
|
-
truncateCumulativeLogIfNeeded();
|
|
135
|
-
updateReason = updateReason ? `Update reason: ${updateReason}` : '';
|
|
136
|
-
const message = updateReason || nowPretty();
|
|
137
|
-
fs.appendFileSync(cumulativeLogFile, `${message}\n`);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Execute command in NVM environment
|
|
142
|
-
*/
|
|
143
|
-
function execCommand (command, options = {}, withSetupScript = false) {
|
|
144
|
-
// If we have NVM setup, wrap the command
|
|
145
|
-
const fullCommand = (setupScript && withSetupScript) ? `${setupScript} && ${command}` : command;
|
|
146
|
-
try {
|
|
147
|
-
const result = execSync(fullCommand, {
|
|
148
|
-
encoding: 'utf8',
|
|
149
|
-
stdio: options.silent ? 'inherit' : 'pipe',
|
|
150
|
-
shell: '/bin/bash',
|
|
151
|
-
...options,
|
|
152
|
-
});
|
|
153
|
-
return result;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
if (options.throwOnError !== false) {
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
return error.stdout || '';
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function execWithNODE (command, options = {}) {
|
|
163
|
-
return execCommand(command, options, true);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Load NVM environment and get Node.js version
|
|
168
|
-
*/
|
|
169
|
-
function loadNVMEnvironment () {
|
|
170
|
-
try {
|
|
171
|
-
if (fs.existsSync('.envrc')) {
|
|
172
|
-
const envrcContent = fs.readFileSync('.envrc', 'utf8');
|
|
173
|
-
|
|
174
|
-
// Extract Node.js version from .envrc for logging
|
|
175
|
-
const nodeVersionMatch = envrcContent.match(/nvm use\s+([0-9.]+)/);
|
|
176
|
-
const nodeV = nodeVersionMatch ? nodeVersionMatch[1] : null;
|
|
177
|
-
|
|
178
|
-
if (nodeV) {
|
|
179
|
-
nodeVersion = nodeV;
|
|
180
|
-
}
|
|
181
|
-
setupScript = 'source .envrc';
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
logError('Error loading .envrc file');
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Parse command line arguments
|
|
190
|
-
*/
|
|
191
|
-
function parseArgs () {
|
|
192
|
-
const pArgs = process.argv.slice(2);
|
|
193
|
-
const args = {
|
|
194
|
-
expectedBranch: null,
|
|
195
|
-
help: false,
|
|
196
|
-
force: false,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
for (let i = 0; i < pArgs.length; i++) {
|
|
200
|
-
const arg = pArgs[i];
|
|
201
|
-
switch (arg) {
|
|
202
|
-
case '-b':
|
|
203
|
-
case '--branch':
|
|
204
|
-
args.expectedBranch = pArgs[++i];
|
|
205
|
-
break;
|
|
206
|
-
case '-f':
|
|
207
|
-
case '--force':
|
|
208
|
-
args.force = true;
|
|
209
|
-
break;
|
|
210
|
-
case '-?':
|
|
211
|
-
case '--help':
|
|
212
|
-
args.help = true;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return args;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Show help information
|
|
222
|
-
*/
|
|
223
|
-
function showHelp () {
|
|
224
|
-
console.log(`
|
|
225
|
-
================================================================================
|
|
226
|
-
Project update and rebuild
|
|
227
|
-
|
|
228
|
-
Usage:
|
|
229
|
-
node update.js [Options]
|
|
230
|
-
|
|
231
|
-
Options:
|
|
232
|
-
|
|
233
|
-
-b|--branch
|
|
234
|
-
GIT branch name. Default - master
|
|
235
|
-
-l|--log
|
|
236
|
-
Switch to log display mode after completion
|
|
237
|
-
-?|--help
|
|
238
|
-
Display help
|
|
239
|
-
|
|
240
|
-
Example: node update.js -b production -l
|
|
241
|
-
================================================================================
|
|
242
|
-
`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Parse simple YAML content (key: value format)
|
|
247
|
-
*/
|
|
248
|
-
function parseSimpleYAML (content) {
|
|
249
|
-
const config = {};
|
|
250
|
-
const lines = content.split('\n');
|
|
251
|
-
|
|
252
|
-
for (const line of lines) {
|
|
253
|
-
const trimmed = line.trim();
|
|
254
|
-
// Skip empty lines and comments
|
|
255
|
-
if (trimmed && !trimmed.startsWith('#')) {
|
|
256
|
-
// Parse key: value pairs
|
|
257
|
-
const colonIndex = trimmed.indexOf(':');
|
|
258
|
-
if (colonIndex > 0) {
|
|
259
|
-
const [, key, valueRaw] = trimmed.match(/^\s*([^:]+?)\s*:\s*(.*)\s*$/) || [];
|
|
260
|
-
let value = valueRaw ? valueRaw.replace(/^(['"])(.*)\1$/, '$2') : '';
|
|
261
|
-
// Handle empty values
|
|
262
|
-
if (value === 'null' || value === '~') {
|
|
263
|
-
value = '';
|
|
264
|
-
}
|
|
265
|
-
config[key] = value;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return config;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Load configuration from YAML file
|
|
275
|
-
*/
|
|
276
|
-
function loadConfig () {
|
|
277
|
-
// Load NVM environment from .envrc
|
|
278
|
-
loadNVMEnvironment();
|
|
279
|
-
|
|
280
|
-
const configFile = path.join(process.cwd(), 'deploy', 'config.yml');
|
|
281
|
-
|
|
282
|
-
// Get Node.js version from NVM environment if available
|
|
283
|
-
if (!fs.existsSync(configFile)) {
|
|
284
|
-
return { ...DEFAULT_CONFIG };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
const configContent = fs.readFileSync(configFile, 'utf8');
|
|
289
|
-
const config = parseSimpleYAML(configContent);
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
branch: config.branch || DEFAULT_CONFIG.branch,
|
|
293
|
-
nodeVersion: config.nodeVersion,
|
|
294
|
-
email: config.email || DEFAULT_CONFIG.email,
|
|
295
|
-
};
|
|
296
|
-
} catch (error) {
|
|
297
|
-
console.warn(`Warning: Could not parse config file ${configFile}:`, error.message);
|
|
298
|
-
return { ...DEFAULT_CONFIG };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Get service name from package.json and .env
|
|
304
|
-
*/
|
|
305
|
-
function getServiceName () {
|
|
306
|
-
let serviceName = '';
|
|
307
|
-
let
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
function
|
|
345
|
-
try {
|
|
346
|
-
execCommand(`
|
|
347
|
-
return true;
|
|
348
|
-
} catch {
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
logIt(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
logIt('BUILD
|
|
479
|
-
execWithNODE(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1
|
+
// noinspection UnnecessaryLocalVariableJS
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync, spawn } = require('child_process');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const version = '2025.11.24-0743';
|
|
9
|
+
console.log(`Update script version: ${version}`);
|
|
10
|
+
|
|
11
|
+
// Имя этой папки
|
|
12
|
+
const scriptDirName = require('path').basename(__dirname);
|
|
13
|
+
// Смена рабочей директории на директорию скрипта
|
|
14
|
+
process.chdir(__dirname);
|
|
15
|
+
const CWD = process.cwd();
|
|
16
|
+
const VON = path.resolve(path.join(CWD, '..'));
|
|
17
|
+
|
|
18
|
+
// Default configuration
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
branch: 'master',
|
|
21
|
+
email: '',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Colors for terminal output
|
|
25
|
+
const colors = {
|
|
26
|
+
reset: '\x1b[0m',
|
|
27
|
+
bright: '\x1b[1m',
|
|
28
|
+
dim: '\x1b[2m',
|
|
29
|
+
red: '\x1b[31m',
|
|
30
|
+
green: '\x1b[32m',
|
|
31
|
+
yellow: '\x1b[33m',
|
|
32
|
+
blue: '\x1b[34m',
|
|
33
|
+
magenta: '\x1b[35m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
white: '\x1b[37m',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const color = {};
|
|
39
|
+
const colorG = {};
|
|
40
|
+
['cyan', 'green', 'magenta', 'red', 'yellow'].forEach((col) => {
|
|
41
|
+
const firstLetter = col[0];
|
|
42
|
+
color[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.reset}`;
|
|
43
|
+
color[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.reset}`;
|
|
44
|
+
colorG[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.green}`;
|
|
45
|
+
colorG[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.green}`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Echo functions with colors
|
|
49
|
+
const echo = {
|
|
50
|
+
c: (text) => console.log(color.c(text)),
|
|
51
|
+
lc: (text) => console.log(color.lc(text)),
|
|
52
|
+
g: (text) => console.log(color.g(text)),
|
|
53
|
+
lg: (text) => console.log(color.lg(text)),
|
|
54
|
+
m: (text) => console.log(color.m(text)),
|
|
55
|
+
lm: (text) => console.log(color.lm(text)),
|
|
56
|
+
r: (text) => console.log(color.r(text)),
|
|
57
|
+
lr: (text) => console.log(color.lr(text)),
|
|
58
|
+
y: (text) => console.log(color.y(text)),
|
|
59
|
+
ly: (text) => console.log(color.ly(text)),
|
|
60
|
+
lg_no_newline: (msg) => process.stdout.write(color.lg(msg)),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let logBuffer = '';
|
|
64
|
+
|
|
65
|
+
// Global variable to store NVM environment
|
|
66
|
+
let setupScript = '';
|
|
67
|
+
let nodeVersion = null;
|
|
68
|
+
const DEFAULT_NODE_VERSION = '22.17.1';
|
|
69
|
+
|
|
70
|
+
const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0].replace('T', '');
|
|
71
|
+
const ytdl = timestamp.slice(2, 14); // YYMMDDHHMMSS format
|
|
72
|
+
const runTimeLogFile = path.join(VON, `deploy__${scriptDirName}__processing__${ytdl}.log`);
|
|
73
|
+
const lastDeployLogFile = path.join(VON, `deploy__${scriptDirName}__last_deploy.log`);
|
|
74
|
+
const cumulativeLogFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
|
|
75
|
+
|
|
76
|
+
const clearColors = (text) => text.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
|
|
77
|
+
const clearHtmlColors = (text) => text.replace(/<\/?(red|y|g|r|status)>/g, '');
|
|
78
|
+
|
|
79
|
+
const logIt = (msg, isTitle) => {
|
|
80
|
+
if (isTitle) {
|
|
81
|
+
const lng = 60 - (msg.length + 2);
|
|
82
|
+
const left = Math.floor(lng / 2);
|
|
83
|
+
const right = lng - left;
|
|
84
|
+
msg = `${'='.repeat(left)} ${msg} ${'='.repeat(right)}`;
|
|
85
|
+
}
|
|
86
|
+
const msg4console = clearHtmlColors(msg);
|
|
87
|
+
echo.g(msg4console);
|
|
88
|
+
logBuffer += `${msg}\n`;
|
|
89
|
+
fs.appendFileSync(runTimeLogFile, `${clearColors(msg4console)}\n`);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const logError = (msg) => {
|
|
93
|
+
console.error(color.r(msg));
|
|
94
|
+
const msg2 = `[ERROR] ${msg}`;
|
|
95
|
+
logBuffer += `<red>${msg2}</red>\n`;
|
|
96
|
+
fs.appendFileSync(cumulativeLogFile, `${msg2}\n`);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const nowPretty = () => new Date().toISOString().replace('T', ' ').substring(0, 19) + 'Z';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Truncate cumulative log file if it exceeds 2MB, keeping last 10KB
|
|
103
|
+
*/
|
|
104
|
+
const truncateCumulativeLogIfNeeded = () => {
|
|
105
|
+
const logFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
|
|
106
|
+
const maxFileSize = 2 * 1024 * 1024; // 2MB
|
|
107
|
+
const keepSize = 10 * 1024; // 10KB
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(logFile)) {
|
|
111
|
+
const stats = fs.statSync(logFile);
|
|
112
|
+
if (stats.size > maxFileSize) {
|
|
113
|
+
// Read last 10KB
|
|
114
|
+
const fd = fs.openSync(logFile, 'r');
|
|
115
|
+
const buffer = Buffer.alloc(keepSize);
|
|
116
|
+
|
|
117
|
+
// Position to last 10KB
|
|
118
|
+
fs.readSync(fd, buffer, 0, keepSize, stats.size - keepSize);
|
|
119
|
+
fs.closeSync(fd);
|
|
120
|
+
|
|
121
|
+
// Write back only the last 10KB
|
|
122
|
+
const tailContent = buffer.toString('utf8').replace(/^[\r\n]*/, ''); // Remove leading newlines
|
|
123
|
+
fs.writeFileSync(logFile, tailContent);
|
|
124
|
+
|
|
125
|
+
log(`Cumulative log truncated to ${Math.round(tailContent.length / 1024)}KB`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logError(`Failed to truncate cumulative log: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const logTryUpdate = (updateReason = '') => {
|
|
134
|
+
truncateCumulativeLogIfNeeded();
|
|
135
|
+
updateReason = updateReason ? `Update reason: ${updateReason}` : '';
|
|
136
|
+
const message = updateReason || nowPretty();
|
|
137
|
+
fs.appendFileSync(cumulativeLogFile, `${message}\n`);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execute command in NVM environment
|
|
142
|
+
*/
|
|
143
|
+
function execCommand (command, options = {}, withSetupScript = false) {
|
|
144
|
+
// If we have NVM setup, wrap the command
|
|
145
|
+
const fullCommand = (setupScript && withSetupScript) ? `${setupScript} && ${command}` : command;
|
|
146
|
+
try {
|
|
147
|
+
const result = execSync(fullCommand, {
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
stdio: options.silent ? 'inherit' : 'pipe',
|
|
150
|
+
shell: '/bin/bash',
|
|
151
|
+
...options,
|
|
152
|
+
});
|
|
153
|
+
return result;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (options.throwOnError !== false) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
return error.stdout || '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function execWithNODE (command, options = {}) {
|
|
163
|
+
return execCommand(command, options, true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Load NVM environment and get Node.js version
|
|
168
|
+
*/
|
|
169
|
+
function loadNVMEnvironment () {
|
|
170
|
+
try {
|
|
171
|
+
if (fs.existsSync('.envrc')) {
|
|
172
|
+
const envrcContent = fs.readFileSync('.envrc', 'utf8');
|
|
173
|
+
|
|
174
|
+
// Extract Node.js version from .envrc for logging
|
|
175
|
+
const nodeVersionMatch = envrcContent.match(/nvm use\s+([0-9.]+)/);
|
|
176
|
+
const nodeV = nodeVersionMatch ? nodeVersionMatch[1] : null;
|
|
177
|
+
|
|
178
|
+
if (nodeV) {
|
|
179
|
+
nodeVersion = nodeV;
|
|
180
|
+
}
|
|
181
|
+
setupScript = 'source .envrc';
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logError('Error loading .envrc file');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse command line arguments
|
|
190
|
+
*/
|
|
191
|
+
function parseArgs () {
|
|
192
|
+
const pArgs = process.argv.slice(2);
|
|
193
|
+
const args = {
|
|
194
|
+
expectedBranch: null,
|
|
195
|
+
help: false,
|
|
196
|
+
force: false,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < pArgs.length; i++) {
|
|
200
|
+
const arg = pArgs[i];
|
|
201
|
+
switch (arg) {
|
|
202
|
+
case '-b':
|
|
203
|
+
case '--branch':
|
|
204
|
+
args.expectedBranch = pArgs[++i];
|
|
205
|
+
break;
|
|
206
|
+
case '-f':
|
|
207
|
+
case '--force':
|
|
208
|
+
args.force = true;
|
|
209
|
+
break;
|
|
210
|
+
case '-?':
|
|
211
|
+
case '--help':
|
|
212
|
+
args.help = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return args;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Show help information
|
|
222
|
+
*/
|
|
223
|
+
function showHelp () {
|
|
224
|
+
console.log(`
|
|
225
|
+
================================================================================
|
|
226
|
+
Project update and rebuild
|
|
227
|
+
|
|
228
|
+
Usage:
|
|
229
|
+
node update.js [Options]
|
|
230
|
+
|
|
231
|
+
Options:
|
|
232
|
+
|
|
233
|
+
-b|--branch
|
|
234
|
+
GIT branch name. Default - master
|
|
235
|
+
-l|--log
|
|
236
|
+
Switch to log display mode after completion
|
|
237
|
+
-?|--help
|
|
238
|
+
Display help
|
|
239
|
+
|
|
240
|
+
Example: node update.js -b production -l
|
|
241
|
+
================================================================================
|
|
242
|
+
`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Parse simple YAML content (key: value format)
|
|
247
|
+
*/
|
|
248
|
+
function parseSimpleYAML (content) {
|
|
249
|
+
const config = {};
|
|
250
|
+
const lines = content.split('\n');
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
const trimmed = line.trim();
|
|
254
|
+
// Skip empty lines and comments
|
|
255
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
256
|
+
// Parse key: value pairs
|
|
257
|
+
const colonIndex = trimmed.indexOf(':');
|
|
258
|
+
if (colonIndex > 0) {
|
|
259
|
+
const [, key, valueRaw] = trimmed.match(/^\s*([^:]+?)\s*:\s*(.*)\s*$/) || [];
|
|
260
|
+
let value = valueRaw ? valueRaw.replace(/^(['"])(.*)\1$/, '$2') : '';
|
|
261
|
+
// Handle empty values
|
|
262
|
+
if (value === 'null' || value === '~') {
|
|
263
|
+
value = '';
|
|
264
|
+
}
|
|
265
|
+
config[key] = value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return config;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Load configuration from YAML file
|
|
275
|
+
*/
|
|
276
|
+
function loadConfig () {
|
|
277
|
+
// Load NVM environment from .envrc
|
|
278
|
+
loadNVMEnvironment();
|
|
279
|
+
|
|
280
|
+
const configFile = path.join(process.cwd(), 'deploy', 'config.yml');
|
|
281
|
+
|
|
282
|
+
// Get Node.js version from NVM environment if available
|
|
283
|
+
if (!fs.existsSync(configFile)) {
|
|
284
|
+
return { ...DEFAULT_CONFIG };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const configContent = fs.readFileSync(configFile, 'utf8');
|
|
289
|
+
const config = parseSimpleYAML(configContent);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
branch: config.branch || DEFAULT_CONFIG.branch,
|
|
293
|
+
nodeVersion: config.nodeVersion,
|
|
294
|
+
email: config.email || DEFAULT_CONFIG.email,
|
|
295
|
+
};
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.warn(`Warning: Could not parse config file ${configFile}:`, error.message);
|
|
298
|
+
return { ...DEFAULT_CONFIG };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get service name from package.json and .env
|
|
304
|
+
*/
|
|
305
|
+
function getServiceName () {
|
|
306
|
+
let serviceName = '';
|
|
307
|
+
let serviceNameAlt = '';
|
|
308
|
+
let serviceInstance = '';
|
|
309
|
+
try {
|
|
310
|
+
if (fs.existsSync('.env')) {
|
|
311
|
+
const envContent = fs.readFileSync('.env', 'utf8');
|
|
312
|
+
let match = envContent.match(/^SERVICE_NAME=([^\r\n]+)/m);
|
|
313
|
+
if (match) {
|
|
314
|
+
serviceName = match[1].trim();
|
|
315
|
+
}
|
|
316
|
+
match = envContent.match(/^SERVICE_NAME_ALT=([^\r\n]+)/m);
|
|
317
|
+
if (match) {
|
|
318
|
+
serviceNameAlt = match[1].trim();
|
|
319
|
+
}
|
|
320
|
+
match = envContent.match(/^SERVICE_INSTANCE=([^\r\n]+)/m);
|
|
321
|
+
if (match) {
|
|
322
|
+
serviceInstance = `--${match[1].trim()}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!serviceName) {
|
|
326
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
327
|
+
serviceName = packageJson.name;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
serviceNameForPM2: `${serviceName}${serviceInstance}`,
|
|
332
|
+
serviceName,
|
|
333
|
+
serviceNameForSystemd: serviceNameAlt || serviceName,
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Error getting service name:', error.message);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if systemctl service exists
|
|
343
|
+
*/
|
|
344
|
+
function systemctlServiceExists (serviceName) {
|
|
345
|
+
try {
|
|
346
|
+
const res = execCommand(`systemctl list-unit-files "${serviceName}.service"`);
|
|
347
|
+
return true;
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function pm2ServiceExists (serviceName) {
|
|
354
|
+
try {
|
|
355
|
+
const res = execCommand(`pm2 id "${serviceName}"`);
|
|
356
|
+
return /\[\s*\d\s*]/.test(res);
|
|
357
|
+
} catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get git repository information
|
|
364
|
+
*/
|
|
365
|
+
function getRepoInfo () {
|
|
366
|
+
try {
|
|
367
|
+
const branch = execCommand('git rev-parse --abbrev-ref HEAD').trim();
|
|
368
|
+
const headHash = execCommand('git rev-parse HEAD').trim();
|
|
369
|
+
// const headShortHash = execCommand('git rev-parse --short HEAD').trim();
|
|
370
|
+
const headCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${headHash}`).trim();
|
|
371
|
+
const headDdate = execCommand(`git log -n 1 --format="%at" ${headHash} | xargs -I{} date -d @{} +%d.%m.%Y_%H:%M:%S`).trim();
|
|
372
|
+
execCommand(`git fetch origin ${branch} --prune`);
|
|
373
|
+
|
|
374
|
+
const upstreamHash = execCommand(`git rev-parse ${branch}@{upstream}`).trim();
|
|
375
|
+
// const upstreamShortHash = execCommand(`git rev-parse --short ${branch}@{upstream}`).trim();
|
|
376
|
+
// const upstreamCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${upstreamHash}`).trim();
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
branch,
|
|
380
|
+
headDdate,
|
|
381
|
+
headHash,
|
|
382
|
+
headCommitMessage,
|
|
383
|
+
upstreamHash,
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error('Error getting repo info:', error.message);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const colorizeHTML = (text) => text
|
|
392
|
+
.replace(/<red>/g, '<span style="color:#ff0000;">')
|
|
393
|
+
.replace(/<\/red>/g, '</span>')
|
|
394
|
+
.replace(/<y>/g, '<span style="background-color:#ffff00;">')
|
|
395
|
+
.replace(/<\/y>/g, '</span>')
|
|
396
|
+
.replace(/<g>/g, '<span style="background-color:#00ff00;">')
|
|
397
|
+
.replace(/<\/g>/g, '</span>')
|
|
398
|
+
.replace(/<r>/g, '<span style="background-color:#ff0000; color:#ffffff;">')
|
|
399
|
+
.replace(/<\/r>/g, '</span>')
|
|
400
|
+
.replace(/\[ERROR]/g, '<span style="color:#ffffff; background-color: #ff0000">[ERROR]</span>');
|
|
401
|
+
|
|
402
|
+
async function sendBuildNotification (emails, status, body, serviceName) {
|
|
403
|
+
if (!emails) { return; }
|
|
404
|
+
let s = '';
|
|
405
|
+
if (status === 'FAIL') {
|
|
406
|
+
s = `<r>FAIL</r> `;
|
|
407
|
+
} else if (status === 'SUCCESS') {
|
|
408
|
+
s = `<g>SUCCESS</g> `;
|
|
409
|
+
}
|
|
410
|
+
body = body.replace('<status>', s);
|
|
411
|
+
|
|
412
|
+
// Create HTML email content
|
|
413
|
+
const hostname = os.hostname();
|
|
414
|
+
const htmlContent = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
415
|
+
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
|
416
|
+
<head>
|
|
417
|
+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
|
418
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
419
|
+
<title>${status} Update ${serviceName} (on ${hostname})</title>
|
|
420
|
+
</head>
|
|
421
|
+
<body>
|
|
422
|
+
<pre>
|
|
423
|
+
${colorizeHTML(clearColors(body))}
|
|
424
|
+
</pre></body></html>`;
|
|
425
|
+
|
|
426
|
+
// Send to each email address
|
|
427
|
+
const emailArray = emails.split(',').map((email) => email.trim()).filter((email) => email);
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < emailArray.length; i++) {
|
|
430
|
+
const emailAddress = emailArray[i];
|
|
431
|
+
try {
|
|
432
|
+
logIt(`Sending update notification to: ${emailAddress}`);
|
|
433
|
+
const subject = `${status} Update: ${serviceName} (on ${hostname})`;
|
|
434
|
+
|
|
435
|
+
// Подаем тело письма через stdin, задаем Content-Type
|
|
436
|
+
const command = `mail -a "Content-Type: text/html; charset=UTF-8" -s "${subject.replace(/"/g, '\\"')}" "${emailAddress}"`;
|
|
437
|
+
const child = spawn('/bin/bash', ['-lc', command], { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
438
|
+
child.stdin.write(htmlContent);
|
|
439
|
+
child.stdin.end();
|
|
440
|
+
|
|
441
|
+
await new Promise((resolve, reject) => {
|
|
442
|
+
child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`mail exit code ${code}`))));
|
|
443
|
+
child.on('error', reject);
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error(`Failed to send email to ${emailAddress}:`, error.message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const printCurrenBranch = () => {
|
|
452
|
+
const i = getRepoInfo();
|
|
453
|
+
logIt(`Current branch: ${colorG.lg(i.branch)}
|
|
454
|
+
Last commit: ${colorG.lg(i.headHash)}, date: ${colorG.lg(i.headDdate)}
|
|
455
|
+
Commit message: ${colorG.lg(i.headCommitMessage)}`);
|
|
456
|
+
return i;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
let scriptsDirName = fs.existsSync(path.join(CWD, '_sh/npm/yarn-ci.sh')) ? '_sh' : 'scripts';
|
|
460
|
+
|
|
461
|
+
const reinstallDependencies = () => {
|
|
462
|
+
logIt('CLEAN INSTALL DEPENDENCIES', true);
|
|
463
|
+
|
|
464
|
+
execCommand('rm -rf node_modules/');
|
|
465
|
+
execWithNODE('yarn install --frozen-lockfile');
|
|
466
|
+
logIt('Dependencies installed');
|
|
467
|
+
|
|
468
|
+
// Patch node modules if patch file exists
|
|
469
|
+
const patchFile = path.join(scriptsDirName, 'patch_node_modules.js');
|
|
470
|
+
if (fs.existsSync(patchFile)) {
|
|
471
|
+
logIt('PATCH NODE MODULES', true);
|
|
472
|
+
execWithNODE(`node --no-node-snapshot ${patchFile}`);
|
|
473
|
+
logIt('Node modules patched');
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const compile = () => {
|
|
478
|
+
logIt('TYPESCRIPT BUILD', true);
|
|
479
|
+
execWithNODE('yarn cb', { silent: true });
|
|
480
|
+
logIt('TypeScript build completed');
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const buildQuasar = () => {
|
|
484
|
+
if (!fs.existsSync(path.join(CWD, 'quasar.config.js'))) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
logIt('BUILD QUASAR', true);
|
|
488
|
+
execWithNODE(`node ./${scriptsDirName}/quasar-prepare-color-vars.mjs`);
|
|
489
|
+
const result = execWithNODE('yarn quasar build');
|
|
490
|
+
logIt(result);
|
|
491
|
+
logIt('Quasar build completed');
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const restartService = ({ serviceName, serviceNameForPM2, serviceNameForSystemd }, args) => {
|
|
495
|
+
let srvc = '';
|
|
496
|
+
if (systemctlServiceExists(serviceNameForSystemd)) {
|
|
497
|
+
srvc = 'systemctl';
|
|
498
|
+
logIt(`Restarting service ${serviceNameForSystemd} via ${srvc}`, true);
|
|
499
|
+
execCommand(`${srvc} restart "${serviceNameForSystemd}"`);
|
|
500
|
+
} else if (pm2ServiceExists(serviceNameForPM2)) {
|
|
501
|
+
srvc = 'pm2';
|
|
502
|
+
logIt(`Restarting service ${serviceNameForPM2} via ${srvc}`, true);
|
|
503
|
+
execCommand(`${srvc} restart "${serviceNameForPM2}"`);
|
|
504
|
+
} else {
|
|
505
|
+
logIt(`Service ${serviceName} not found in systemctl or PM2`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
logIt(`Service restarted`);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Main update function
|
|
513
|
+
*/
|
|
514
|
+
async function main () {
|
|
515
|
+
logTryUpdate();
|
|
516
|
+
fs.writeFileSync(runTimeLogFile, '');
|
|
517
|
+
const args = parseArgs();
|
|
518
|
+
|
|
519
|
+
if (args.help) {
|
|
520
|
+
showHelp();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Get service information
|
|
525
|
+
const { serviceName, serviceNameForPM2, serviceNameForSystemd } = getServiceName();
|
|
526
|
+
|
|
527
|
+
logIt(`<status>Update <y>${colorG.y(serviceName)}</y> ${nowPretty()}`);
|
|
528
|
+
|
|
529
|
+
logIt(`Working directory: ${colorG.y(CWD)}`);
|
|
530
|
+
// Load configuration
|
|
531
|
+
const config = loadConfig();
|
|
532
|
+
|
|
533
|
+
let from = ' DEFAULT';
|
|
534
|
+
if (nodeVersion) {
|
|
535
|
+
from = ' .envrc';
|
|
536
|
+
} else if (config.nodeVersion) {
|
|
537
|
+
nodeVersion = config.nodeVersion;
|
|
538
|
+
from = ' deploy/config.yaml';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
logIt(`Using Node.js version: ${nodeVersion || DEFAULT_NODE_VERSION}${from}`);
|
|
542
|
+
|
|
543
|
+
// Override branch if specified in arguments
|
|
544
|
+
const expectedBranch = args.expectedBranch || config.branch;
|
|
545
|
+
let updateDeployedLogFile = false;
|
|
546
|
+
try {
|
|
547
|
+
// 1) Если есть локальные изменения — откатить
|
|
548
|
+
const hasChanges = execCommand('git status --porcelain').trim().length > 0;
|
|
549
|
+
if (hasChanges) {
|
|
550
|
+
logIt(`Found uncommited changes. Reset to HEAD...`);
|
|
551
|
+
execCommand('git reset --hard HEAD');
|
|
552
|
+
execCommand(`git clean -fd`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let needUpdate = false;
|
|
556
|
+
let updateReason = args.force ? 'force' : '';
|
|
557
|
+
const repoInfo = getRepoInfo();
|
|
558
|
+
let { branch, headHash, upstreamHash } = repoInfo;
|
|
559
|
+
|
|
560
|
+
// 2) Если ветка не та — жестко переключиться на голову удаленной expectedBranch
|
|
561
|
+
const expectedUpstream = `origin/${expectedBranch}`;
|
|
562
|
+
if (branch !== expectedBranch) {
|
|
563
|
+
needUpdate = true;
|
|
564
|
+
updateReason += `${updateReason ? '. ' : ''}branch !== expectedBranch (${branch} != ${expectedBranch})`;
|
|
565
|
+
logIt(`Switch to branch ${expectedBranch}...`);
|
|
566
|
+
execCommand(`git fetch origin ${expectedBranch} --prune`);
|
|
567
|
+
execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
|
|
568
|
+
execCommand(`git reset --hard ${expectedUpstream}`);
|
|
569
|
+
execCommand(`git clean -fd`);
|
|
570
|
+
const i = printCurrenBranch();
|
|
571
|
+
branch = i.branch;
|
|
572
|
+
headHash = i.headHash;
|
|
573
|
+
upstreamHash = i.upstreamHash;
|
|
574
|
+
if (branch !== expectedBranch) {
|
|
575
|
+
throw new Error(`Failed to switch to branch ${expectedBranch}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (headHash !== upstreamHash) {
|
|
580
|
+
// 3) Ветка та же, но надо подтянуть изменения
|
|
581
|
+
needUpdate = true;
|
|
582
|
+
updateReason += `${updateReason ? '. ' : ''}headHash !== upstreamHash (${headHash} != ${upstreamHash})`;
|
|
583
|
+
printCurrenBranch();
|
|
584
|
+
logIt(`FOUND CHANGES. UPDATE branch ${expectedBranch}...`);
|
|
585
|
+
execCommand(`git fetch origin ${expectedBranch} --prune`);
|
|
586
|
+
execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
|
|
587
|
+
execCommand(`git reset --hard ${expectedUpstream}`);
|
|
588
|
+
execCommand(`git clean -fd`);
|
|
589
|
+
printCurrenBranch();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (needUpdate || args.force) {
|
|
593
|
+
updateDeployedLogFile = true;
|
|
594
|
+
logTryUpdate(updateReason);
|
|
595
|
+
//reinstallDependencies();
|
|
596
|
+
//compile();
|
|
597
|
+
//buildQuasar();
|
|
598
|
+
restartService({ serviceName, serviceNameForPM2, serviceNameForSystemd }, args);
|
|
599
|
+
|
|
600
|
+
// Add completion info to build log
|
|
601
|
+
logIt(`Update completed successfully at ${new Date().toISOString().replace('T', ' ').substring(0, 19)}`);
|
|
602
|
+
// Send build notification if email is configured
|
|
603
|
+
if (config.email) {
|
|
604
|
+
await sendBuildNotification(config.email, 'SUCCESS', logBuffer, serviceName);
|
|
605
|
+
} else {
|
|
606
|
+
logIt('EMAIL not found');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} catch (err) {
|
|
610
|
+
const message = String(err.message).includes(err.stderr)
|
|
611
|
+
? err.message
|
|
612
|
+
: [err.stderr, err.message].join('\n');
|
|
613
|
+
logError(message);
|
|
614
|
+
if (config.email) {
|
|
615
|
+
await sendBuildNotification(config.email, 'FAIL', logBuffer, serviceName);
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
logIt('#FINISH#');
|
|
619
|
+
if (updateDeployedLogFile) {
|
|
620
|
+
fs.copyFileSync(runTimeLogFile, lastDeployLogFile);
|
|
621
|
+
}
|
|
622
|
+
execCommand(`rm -rf "${runTimeLogFile}"`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Handle process termination gracefully
|
|
627
|
+
process.on('SIGINT', () => {
|
|
628
|
+
console.log('\nUpdate process interrupted');
|
|
629
|
+
process.exit(1);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
process.on('SIGTERM', () => {
|
|
633
|
+
console.log('\nUpdate process terminated');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
main().then(() => {
|
|
638
|
+
process.exit(0);
|
|
639
|
+
}).catch((error) => {
|
|
640
|
+
console.error('Update failed:', error.message);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
});
|