Ajouter le dispatcher e-mail des alertes CI

This commit is contained in:
Manus Déploiement Santinova
2026-08-21 20:28:13 +00:00
parent c693da6273
commit 5c3d644ab8
10 changed files with 154 additions and 3 deletions

View File

@@ -6,7 +6,7 @@
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "node test/app-registry.test.js && node test/healthcheck.test.js && node test/webhook.test.js && node test/gitea.test.js && node test/gitea-auth.test.js && node test/token-rotation.test.js && node test/ci-alerts.test.js && node test/webhook-setup.test.js && node test/dashboard-alert-token.test.js"
"test": "node test/app-registry.test.js && node test/healthcheck.test.js && node test/webhook.test.js && node test/gitea.test.js && node test/gitea-auth.test.js && node test/token-rotation.test.js && node test/ci-alerts.test.js && node test/ci-email-alert.test.js && node test/webhook-setup.test.js && node test/dashboard-alert-token.test.js"
},
"dependencies": {
"express": "^4.21.0",

View File

@@ -6,6 +6,40 @@ const LOG_DIR = '/var/log/manus-deploy';
const ALERTS_FILE = path.join(LOG_DIR, 'ci-alerts.json');
const MAX_ALERTS = 100;
function formatEmailAlert(alert) {
return [
'Une validation CI a échoué sur l’infrastructure Santinova.',
'',
`Dépôt : ${alert.repository}`,
`Workflow : ${alert.workflow}`,
`Conclusion : ${alert.conclusion}`,
`Commit : ${alert.commit}`,
`Date : ${alert.occurredAt}`,
alert.url ? `Détail : ${alert.url}` : null,
].filter(Boolean).join('\n');
}
function enqueueDsiEmail(alert) {
if (!config.dsiAlerts.email) return false;
const queueRoot = config.dsiAlerts.emailQueue;
const safeId = String(alert.id).replace(/[^a-zA-Z0-9._-]/g, '_');
const queueFile = queueRoot.endsWith('.json')
? queueRoot
: path.join(queueRoot, `${safeId}.json`);
const temporary = `${queueFile}.${process.pid}.tmp`;
const payload = {
id: alert.id,
to: config.dsiAlerts.email,
subject: `[Santinova] Échec CI — ${alert.repository}`,
text: formatEmailAlert(alert),
createdAt: new Date().toISOString(),
};
fs.mkdirSync(path.dirname(queueFile), { recursive: true, mode: 0o700 });
fs.writeFileSync(temporary, JSON.stringify(payload, null, 2), { mode: 0o600 });
fs.renameSync(temporary, queueFile);
return true;
}
function normalizeWorkflowFailure(payload, deliveryId = null) {
if (payload?.action !== 'completed') return null;
const run = payload?.workflow_run || {};
@@ -46,8 +80,9 @@ function saveCiAlert(alert) {
}
async function notifyDsi(alert) {
if (enqueueDsiEmail(alert)) return true;
if (!config.dsiAlerts.token) {
console.error('[CI Alert] IA_REFERENTIEL_TOKEN absent : notification DSI non envoyée.');
console.error('[CI Alert] Aucun canal direct DSI configuré : e-mail et IA_REFERENTIEL_TOKEN absents.');
return false;
}
try {
@@ -79,4 +114,4 @@ async function notifyDsi(alert) {
}
}
module.exports = { normalizeWorkflowFailure, readCiAlerts, saveCiAlert, notifyDsi };
module.exports = { normalizeWorkflowFailure, readCiAlerts, saveCiAlert, notifyDsi, enqueueDsiEmail };

View File

@@ -33,6 +33,8 @@ module.exports = {
dsiAlerts: {
url: process.env.IA_REFERENTIEL_URL || 'https://referentiel-ia.recette.santinova-soft.org',
token: process.env.IA_REFERENTIEL_TOKEN || null,
email: process.env.DSI_ALERT_EMAIL || null,
emailQueue: process.env.CI_ALERT_EMAIL_QUEUE || '/opt/manus-deploy/secrets/ci-alerts/outbox',
},
appsBasePath: process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps',

View File

@@ -0,0 +1,31 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'ci-email-alert-'));
process.env.DSI_ALERT_EMAIL = 'dsi@example.invalid';
process.env.CI_ALERT_EMAIL_QUEUE = path.join(temporaryDirectory, 'queued-alert.json');
const { enqueueDsiEmail } = require('../src/ci-alerts');
const queued = enqueueDsiEmail({
id: 'ci-email-test',
repository: 'manus-admin/referentiel-ia',
workflow: 'Validation CI',
commit: 'abc1234',
conclusion: 'failure',
url: 'https://git.recette.santinova-soft.org/actions/runs/42',
occurredAt: '2026-08-21T18:00:00Z',
});
assert.equal(queued, true);
const payload = JSON.parse(fs.readFileSync(process.env.CI_ALERT_EMAIL_QUEUE, 'utf8'));
assert.equal(payload.to, 'dsi@example.invalid');
assert.equal(payload.id, 'ci-email-test');
assert.match(payload.subject, /Échec CI/);
assert.match(payload.text, /referentiel-ia/);
assert.equal(fs.statSync(process.env.CI_ALERT_EMAIL_QUEUE).mode & 0o777, 0o600);
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
console.log('OK ci email alert');

View File

@@ -19,6 +19,8 @@ services:
- WEBHOOK_SECRET=${WEBHOOK_SECRET:?WEBHOOK_SECRET requis dans le fichier .env serveur}
- IA_REFERENTIEL_URL=${IA_REFERENTIEL_URL:-https://referentiel-ia.recette.santinova-soft.org}
- IA_REFERENTIEL_TOKEN=${IA_REFERENTIEL_TOKEN:-}
- DSI_ALERT_EMAIL=${DSI_ALERT_EMAIL:-}
- CI_ALERT_EMAIL_QUEUE=/opt/manus-deploy/secrets/ci-alerts/outbox
- APPS_BASE_PATH=/opt/manus-deploy/apps
- INFRA_BASE_PATH=/opt/manus-deploy/infrastructure
- HEALTH_CHECK_INTERVAL=30000