fix: rafraîchir les manifests du dashboard après déploiement
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 40s
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 40s
This commit is contained in:
@@ -13,6 +13,7 @@ const CATEGORIES = new Set(['ITINOVA', 'SANTINOVA', 'INFRA']);
|
||||
const ENVIRONMENTS = new Set(['recette', 'prod']);
|
||||
const AI_BILLING = new Set(['free', 'paid']);
|
||||
const AI_SOURCES = new Set(['application', 'referential']);
|
||||
const DEFAULT_MANIFEST_REFRESH_INTERVAL_MS = 15_000;
|
||||
|
||||
function isHttpsUrl(value) {
|
||||
if (typeof value !== 'string' || value.length === 0) return false;
|
||||
@@ -171,13 +172,14 @@ function discoverApps({ appsBasePath, environment }) {
|
||||
* Toute valeur obsolète est remplacée : seules les applications réellement
|
||||
* déployées et correctement déclarées restent disponibles.
|
||||
*/
|
||||
function refreshApps(config, logger = console) {
|
||||
function refreshApps(config, logger = console, now = Date.now()) {
|
||||
const result = discoverApps({
|
||||
appsBasePath: config.appsBasePath,
|
||||
environment: config.environment,
|
||||
});
|
||||
|
||||
config.apps.splice(0, config.apps.length, ...result.apps);
|
||||
config.appsRefreshedAt = now;
|
||||
for (const issue of result.errors) {
|
||||
logger.warn(`[app-registry] Manifeste ignoré (${issue.directory}) : ${issue.error}`);
|
||||
}
|
||||
@@ -186,6 +188,24 @@ function refreshApps(config, logger = console) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rafraîchit le registre après expiration, sans parcourir les applications à
|
||||
* chaque requête. Les déploiements deviennent ainsi visibles sans redémarrage.
|
||||
*/
|
||||
function refreshAppsIfStale(config, {
|
||||
logger = console,
|
||||
now = Date.now(),
|
||||
maxAgeMs = config.appManifestRefreshIntervalMs || DEFAULT_MANIFEST_REFRESH_INTERVAL_MS,
|
||||
} = {}) {
|
||||
const hasFreshCache = Array.isArray(config.apps) && config.apps.length > 0 &&
|
||||
Number.isFinite(config.appsRefreshedAt) &&
|
||||
now - config.appsRefreshedAt < maxAgeMs;
|
||||
if (hasFreshCache) {
|
||||
return { apps: config.apps, errors: [], refreshed: false };
|
||||
}
|
||||
return { ...refreshApps(config, logger, now), refreshed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit un manifeste de manière atomique puis le valide avant publication.
|
||||
*/
|
||||
@@ -216,6 +236,7 @@ function findAppByRepo(config, repositoryName) {
|
||||
module.exports = {
|
||||
discoverApps,
|
||||
refreshApps,
|
||||
refreshAppsIfStale,
|
||||
writeAppManifest,
|
||||
findAppDirectoryByRepo,
|
||||
findAppByRepo,
|
||||
|
||||
@@ -10,12 +10,24 @@ const { checkAllApps, addDeploymentLog, updateDeploymentLog, getDeploymentLogs,
|
||||
const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator');
|
||||
const { readCiAlerts } = require('./ci-alerts');
|
||||
const config = require('./config');
|
||||
const { refreshAppsIfStale } = require('./app-registry');
|
||||
|
||||
// Nettoyer les déploiements orphelins au démarrage (entrées "running" restantes d'un crash précédent)
|
||||
cleanupOrphanedDeployments();
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Les manifests sont la source de vérité ; leur cache est rafraîchi après un
|
||||
// court délai pour refléter les déploiements sans alourdir chaque requête API.
|
||||
router.use((req, res, next) => {
|
||||
try {
|
||||
refreshAppsIfStale(config);
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Configuration multer pour l'upload de fichiers ZIP
|
||||
const uploadDir = '/tmp/dashboard-uploads';
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
|
||||
@@ -7,7 +7,7 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { discoverApps, refreshApps } = require('../src/app-registry');
|
||||
const { discoverApps, refreshApps, refreshAppsIfStale } = require('../src/app-registry');
|
||||
|
||||
function writeManifest(basePath, directory, manifest) {
|
||||
const appPath = path.join(basePath, directory);
|
||||
@@ -113,6 +113,33 @@ try {
|
||||
'app.json doit prévaloir sur toute définition statique'
|
||||
);
|
||||
|
||||
// Un déploiement met à jour app.json : le dashboard doit le détecter sans
|
||||
// redémarrage, mais sans relire les manifests à chaque requête.
|
||||
refreshApps(config, console, 1_000);
|
||||
writeManifest(sandbox, 'contacts', {
|
||||
id: 'itinova-contacts',
|
||||
name: 'Itinova Contacts mis à jour',
|
||||
category: 'ITINOVA',
|
||||
urls: {
|
||||
recette: 'https://contacts-itinova.recette.santinova-soft.org',
|
||||
prod: 'https://contacts-itinova.santinova-soft.org',
|
||||
},
|
||||
});
|
||||
const cachedRefresh = refreshAppsIfStale(config, { now: 1_999, maxAgeMs: 1_000 });
|
||||
assert.equal(cachedRefresh.refreshed, false, 'le cache reste utilisable avant son expiration');
|
||||
assert.equal(
|
||||
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
|
||||
'Itinova Contacts',
|
||||
'le cache ne doit pas relire les manifests avant son expiration'
|
||||
);
|
||||
const expiredRefresh = refreshAppsIfStale(config, { now: 2_000, maxAgeMs: 1_000 });
|
||||
assert.equal(expiredRefresh.refreshed, true, 'le manifeste est relu lorsque le cache expire');
|
||||
assert.equal(
|
||||
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
|
||||
'Itinova Contacts mis à jour',
|
||||
'la modification de manifeste doit apparaître sans redémarrage du dashboard'
|
||||
);
|
||||
|
||||
console.log('OK app-registry');
|
||||
} finally {
|
||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user