feat(ci): afficher tendances et médianes des workflows

This commit is contained in:
Manus AI
2026-08-20 15:50:20 +00:00
parent c3bf677972
commit a455d3dd13
5 changed files with 152 additions and 5 deletions

View File

@@ -114,6 +114,53 @@ async function getWorkflowRunForCommit(owner, repo, commitHash) {
return runs.find((run) => run.commit && run.commit.startsWith(commitHash)) || null;
}
/** Retourne la médiane d'une série numérique non vide. */
function median(values) {
if (!values.length) return null;
const sorted = [...values].sort((left, right) => left - right);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[middle] : Math.round((sorted[middle - 1] + sorted[middle]) / 2);
}
/**
* Construit une synthèse déterministe sur les derniers runs CI.
* Les comparaisons ne sont calculées quavec deux fenêtres dau moins deux runs
* pour ne pas transformer une variation isolée en tendance.
*/
function calculateCiSummary(runs) {
const completed = runs.filter((run) => run.status === 'completed');
const successful = completed.filter((run) => run.conclusion === 'success');
const durations = completed.map((run) => run.durationSeconds).filter(Number.isFinite);
const windowSize = Math.floor(durations.length / 2);
const recent = windowSize >= 2 ? durations.slice(0, windowSize) : [];
const previous = windowSize >= 2 ? durations.slice(windowSize, windowSize * 2) : [];
const recentMedian = median(recent);
const previousMedian = median(previous);
const changePercent = previousMedian && recentMedian !== null
? Math.round(((recentMedian - previousMedian) / previousMedian) * 100)
: null;
const trend = changePercent === null ? 'unknown'
: changePercent <= -10 ? 'faster'
: changePercent >= 10 ? 'slower'
: 'stable';
return {
sampleSize: completed.length,
successful: successful.length,
failed: completed.length - successful.length,
successRate: completed.length ? Math.round((successful.length / completed.length) * 100) : null,
medianDurationSeconds: median(durations),
recentMedianDurationSeconds: recentMedian,
previousMedianDurationSeconds: previousMedian,
trend,
changePercent,
};
}
async function getWorkflowRunSummary(owner, repo) {
return calculateCiSummary(await getWorkflowRuns(owner, repo, 30));
}
/**
* Récupérer tous les dépôts (avec retry en cas d'erreur DNS)
*/
@@ -158,8 +205,10 @@ module.exports = {
getCommits,
getBranches,
getWorkflowRuns,
getWorkflowRunSummary,
getWorkflowRunForCommit,
normalizeWorkflowRun,
calculateCiSummary,
listRepos,
createRepo,
giteaClient,

View File

@@ -4,7 +4,7 @@ const path = require('path');
const fs = require('fs');
const { authenticate, authMiddleware } = require('./auth');
const { docker, getContainerInfo, getContainerLogs, listContainers, redeployApp, gitPull, startContainer, stopContainer, restartContainer, getServerMetrics } = require('./docker');
const { getRepo, getCommits, getBranches, getWorkflowRuns, listRepos } = require('./gitea');
const { getRepo, getCommits, getBranches, getWorkflowRuns, getWorkflowRunSummary, listRepos } = require('./gitea');
const { checkAllApps, addDeploymentLog, updateDeploymentLog, getDeploymentLogs, getAllStatuses, getAppStatus, cleanupOrphanedDeployments } = require('./healthcheck');
const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator');
const config = require('./config');
@@ -409,6 +409,15 @@ router.get('/gitea/repos/:owner/:repo/actions/runs', authMiddleware, async (req,
}
});
// Synthèse historique : taux de succès, médiane et tendance des validations CI.
router.get('/gitea/repos/:owner/:repo/actions/summary', authMiddleware, async (req, res) => {
try {
res.json(await getWorkflowRunSummary(req.params.owner, req.params.repo));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ============ PUBLIC STATUS ROUTE (sans authentification) ============
// Utilisé par le portail applicatif pour griser les tuiles des apps arrêtées
router.get('/public/status', async (req, res) => {

View File

@@ -1,5 +1,5 @@
const assert = require('node:assert/strict');
const { normalizeWorkflowRun } = require('../src/gitea');
const { normalizeWorkflowRun, calculateCiSummary } = require('../src/gitea');
const run = normalizeWorkflowRun({
id: 42,
@@ -14,4 +14,14 @@ const run = normalizeWorkflowRun({
assert.equal(run.commit, 'abcdef123456');
assert.equal(run.durationSeconds, 125);
assert.equal(run.conclusion, 'success');
const summary = calculateCiSummary([
{ status: 'completed', conclusion: 'success', durationSeconds: 90 },
{ status: 'completed', conclusion: 'success', durationSeconds: 110 },
{ status: 'completed', conclusion: 'failure', durationSeconds: 120 },
{ status: 'completed', conclusion: 'success', durationSeconds: 100 },
]);
assert.equal(summary.successRate, 75);
assert.equal(summary.medianDurationSeconds, 105);
assert.equal(summary.trend, 'stable');
console.log('OK gitea');