feat: обновление с Git из админки (/admin/system)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
const { execFile, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function resolveShopRoot() {
|
||||
const candidates = [
|
||||
process.env.SHOP_ROOT,
|
||||
path.resolve(__dirname, '../..'),
|
||||
'/opt/shop',
|
||||
].filter(Boolean);
|
||||
|
||||
for (const dir of candidates) {
|
||||
const resolved = path.resolve(dir);
|
||||
if (fs.existsSync(path.join(resolved, 'package.json'))) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isUpdateEnabled() {
|
||||
if (process.env.ADMIN_UPDATE_ENABLED === '0') return false;
|
||||
const root = resolveShopRoot();
|
||||
if (!root) return false;
|
||||
if (!fs.existsSync(path.join(root, '.git'))) return false;
|
||||
if (process.platform === 'win32') return false;
|
||||
return fs.existsSync(path.join(root, 'scripts', 'admin-web-update.sh'));
|
||||
}
|
||||
|
||||
async function gitCmd(args, cwd) {
|
||||
const { stdout, stderr } = await execFileAsync('git', args, {
|
||||
cwd,
|
||||
maxBuffer: 1024 * 1024,
|
||||
timeout: 90000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
});
|
||||
return `${stdout}${stderr}`.trim();
|
||||
}
|
||||
|
||||
async function getGitInfo({ fetchRemote = false } = {}) {
|
||||
const root = resolveShopRoot();
|
||||
const pkg = root ? JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) : null;
|
||||
|
||||
if (!root || !fs.existsSync(path.join(root, '.git'))) {
|
||||
return {
|
||||
available: false,
|
||||
packageVersion: pkg?.version || null,
|
||||
shopRoot: root,
|
||||
reason: 'Каталог не является git-репозиторием. Задайте SHOP_ROOT в .env.',
|
||||
};
|
||||
}
|
||||
|
||||
const info = {
|
||||
available: true,
|
||||
shopRoot: root,
|
||||
packageVersion: pkg?.version || null,
|
||||
branch: null,
|
||||
commit: null,
|
||||
commitShort: null,
|
||||
commitSubject: null,
|
||||
commitDate: null,
|
||||
dirty: false,
|
||||
behind: null,
|
||||
updateEnabled: isUpdateEnabled(),
|
||||
platform: process.platform,
|
||||
};
|
||||
|
||||
try {
|
||||
info.branch = await gitCmd(['branch', '--show-current'], root);
|
||||
if (!info.branch) {
|
||||
info.branch = '(detached)';
|
||||
info.commitShort = await gitCmd(['rev-parse', '--short', 'HEAD'], root);
|
||||
}
|
||||
info.commit = await gitCmd(['rev-parse', 'HEAD'], root);
|
||||
info.commitShort = info.commitShort || (await gitCmd(['rev-parse', '--short', 'HEAD'], root));
|
||||
info.commitSubject = await gitCmd(['log', '-1', '--pretty=%s'], root);
|
||||
info.commitDate = await gitCmd(['log', '-1', '--pretty=%ci'], root);
|
||||
const status = await gitCmd(['status', '--porcelain'], root);
|
||||
info.dirty = status.length > 0;
|
||||
} catch (err) {
|
||||
info.available = false;
|
||||
info.reason = err.message;
|
||||
return info;
|
||||
}
|
||||
|
||||
if (fetchRemote) {
|
||||
try {
|
||||
await gitCmd(['fetch', 'origin'], root);
|
||||
const behind = await gitCmd(
|
||||
['rev-list', '--count', 'HEAD..origin/main'],
|
||||
root
|
||||
);
|
||||
info.behind = parseInt(behind, 10) || 0;
|
||||
} catch (err) {
|
||||
info.fetchError = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
function runDeployUpdate() {
|
||||
const root = resolveShopRoot();
|
||||
const scriptPath = path.join(root, 'scripts', 'admin-web-update.sh');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const useSudo = process.env.ADMIN_UPDATE_USE_SUDO === '1';
|
||||
const cmd = useSudo ? 'sudo' : 'bash';
|
||||
const args = useSudo ? ['-n', scriptPath] : [scriptPath];
|
||||
|
||||
const child = spawn(cmd, args, {
|
||||
cwd: root,
|
||||
env: { ...process.env, SHOP_ROOT: root },
|
||||
timeout: 300000,
|
||||
});
|
||||
|
||||
let output = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({
|
||||
ok: false,
|
||||
code: -1,
|
||||
output: `${output}\n${err.message}`.trim(),
|
||||
});
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({
|
||||
ok: code === 0,
|
||||
code: code ?? 1,
|
||||
output: output.trim() || '(нет вывода)',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveShopRoot,
|
||||
isUpdateEnabled,
|
||||
getGitInfo,
|
||||
runDeployUpdate,
|
||||
};
|
||||
Reference in New Issue
Block a user