feat: version 2.2.0 -- boton real de actualizacion (auto-update)
POST /api/updater/apply dispara scripts/apply-update.sh (backup + git pull + migraciones + build + restart) en un proceso separado. Apagado por defecto: solo se activa si el servidor define UPDATE_APP_DIR/UPDATE_BACKEND_SERVICE/UPDATE_FRONTEND_SERVICE en su backend/.env. Sin esa config, el panel se comporta igual que antes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,7 @@ import * as service from './service.js';
|
||||
export const getUpdateStatus = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.checkForUpdates());
|
||||
});
|
||||
|
||||
export const applyUpdate = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.applyUpdate(req.currentUser.id));
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requireRole, CAN_MANAGE_SETTINGS } from '../../middleware/auth.js';
|
||||
import { getUpdateStatus } from './controller.js';
|
||||
import { getUpdateStatus, applyUpdate } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth, requireRole(...CAN_MANAGE_SETTINGS));
|
||||
router.get('/status', getUpdateStatus);
|
||||
router.post('/apply', applyUpdate);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as repo from './repository.js';
|
||||
import { env } from '../../config/env.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { logAudit } from '../../utils/audit.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const VERSION_FILE = path.resolve(__dirname, '../../../../VERSION');
|
||||
@@ -62,17 +66,22 @@ function getLocalMigrationNames() {
|
||||
return fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql'));
|
||||
}
|
||||
|
||||
function isApplyAvailable() {
|
||||
return !!(env.update.appDir && env.update.backendService && env.update.frontendService);
|
||||
}
|
||||
|
||||
export async function checkForUpdates() {
|
||||
const currentVersion = getLocalVersion();
|
||||
const changelogText = await repo.fetchChangelog();
|
||||
const allEntries = parseChangelog(changelogText);
|
||||
const applyAvailable = isApplyAvailable();
|
||||
|
||||
const pending = allEntries
|
||||
.filter((e) => compareVersions(e.version, currentVersion) > 0)
|
||||
.sort((a, b) => compareVersions(a.version, b.version));
|
||||
|
||||
if (pending.length === 0) {
|
||||
return { currentVersion, upToDate: true, latestVersion: currentVersion, pending: [], hasMigrations: false };
|
||||
return { currentVersion, upToDate: true, latestVersion: currentVersion, pending: [], hasMigrations: false, applyAvailable };
|
||||
}
|
||||
|
||||
let previousInChain = currentVersion;
|
||||
@@ -98,5 +107,43 @@ export async function checkForUpdates() {
|
||||
overallCriticality,
|
||||
hasMigrations,
|
||||
pending: pendingWithMeta,
|
||||
applyAvailable,
|
||||
};
|
||||
}
|
||||
|
||||
let applyInProgress = false;
|
||||
|
||||
// Dispara el script externo (fuera de source/, ver scripts/apply-update.sh) que hace el
|
||||
// fetch+reset+build+migracion+restart real. Se lanza "detached" porque el propio backend se
|
||||
// reinicia al final — el request HTTP ya respondio "empezo" mucho antes de que eso pase.
|
||||
export async function applyUpdate(actorId) {
|
||||
if (!isApplyAvailable()) {
|
||||
throw new HttpError(400, 'La auto-actualizacion no esta configurada en este servidor (falta UPDATE_APP_DIR en backend/.env)');
|
||||
}
|
||||
const status = await checkForUpdates();
|
||||
if (status.upToDate) {
|
||||
throw new HttpError(400, 'El sistema ya esta en la ultima version');
|
||||
}
|
||||
if (applyInProgress) {
|
||||
throw new HttpError(409, 'Ya hay una actualizacion en curso');
|
||||
}
|
||||
|
||||
applyInProgress = true;
|
||||
const scriptPath = path.join(env.update.appDir, 'scripts', 'apply-update.sh');
|
||||
const child = spawn('bash', [scriptPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: {
|
||||
...process.env,
|
||||
APP_DIR: env.update.appDir,
|
||||
BACKEND_SERVICE: env.update.backendService,
|
||||
FRONTEND_SERVICE: env.update.frontendService,
|
||||
},
|
||||
});
|
||||
child.unref();
|
||||
child.on('exit', () => { applyInProgress = false; });
|
||||
|
||||
await logAudit({ userId: actorId, action: 'apply_update', entityType: 'updater', entityId: null, detail: { from: status.currentVersion, to: status.latestVersion } });
|
||||
|
||||
return { started: true, fromVersion: status.currentVersion, toVersion: status.latestVersion };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user