fix(deploy): déléguer le redéploiement du dashboard à l hôte

This commit is contained in:
Manus AI
2026-08-18 08:17:06 +00:00
parent abba8e352a
commit fe0a81fc98
10 changed files with 2583 additions and 4 deletions

View File

@@ -4,6 +4,8 @@ Le script `healthcheck-apps.sh` est exécuté par le timer systemd toutes les ci
Le dernier rapport est écrit dans `/var/lib/manus-apps-health/latest.txt`. Les anomalies sont aussi consignées dans le journal système avec le tag `manus-apps-health`.
Les pushes Gitea du dépôt `manus-dashboard` ne recréent jamais le conteneur depuis lui-même. Le webhook dépose une demande atomique, surveillée par `manus-dashboard-self-deploy.path`, puis le service hôte exécute le redéploiement. Cette séparation évite larrêt du client Docker avant le démarrage du conteneur de remplacement.
Après mise à jour Git, installer ou actualiser le service avec :
```bash

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Déploie le dashboard depuis lhôte systemd, jamais depuis le conteneur lui-même.
set -Eeuo pipefail
readonly APP_DIR="/opt/manus-deploy/apps/manus-dashboard"
readonly REQUEST_FILE="${APP_DIR}/.deployment-request"
readonly PROCESSING_FILE="${APP_DIR}/.deployment-request.processing"
readonly LOCK_FILE="/run/manus-dashboard-self-deploy.lock"
exec 9>"${LOCK_FILE}"
flock -n 9 || exit 0
[[ -f "${REQUEST_FILE}" ]] || exit 0
mv "${REQUEST_FILE}" "${PROCESSING_FILE}"
restore_request() {
[[ -f "${PROCESSING_FILE}" ]] && mv "${PROCESSING_FILE}" "${REQUEST_FILE}"
}
trap restore_request ERR
cd "${APP_DIR}"
git pull --ff-only origin main
docker compose up -d --build
rm -f "${PROCESSING_FILE}"
logger -p daemon.info -t manus-dashboard-self-deploy "Dashboard redéployé depuis le service hôte"

View File

@@ -7,9 +7,13 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
install -D -m 0750 "${SCRIPT_DIR}/healthcheck-apps.sh" /opt/manus-deploy/scripts/healthcheck-apps.sh
install -D -m 0644 "${SCRIPT_DIR}/manus-apps-health.service" /etc/systemd/system/manus-apps-health.service
install -D -m 0644 "${SCRIPT_DIR}/manus-apps-health.timer" /etc/systemd/system/manus-apps-health.timer
install -D -m 0750 "${SCRIPT_DIR}/deploy-dashboard-from-host.sh" /opt/manus-deploy/scripts/deploy-dashboard-from-host.sh
install -D -m 0644 "${SCRIPT_DIR}/manus-dashboard-self-deploy.service" /etc/systemd/system/manus-dashboard-self-deploy.service
install -D -m 0644 "${SCRIPT_DIR}/manus-dashboard-self-deploy.path" /etc/systemd/system/manus-dashboard-self-deploy.path
systemctl daemon-reload
systemctl enable --now manus-apps-health.timer
systemctl enable --now manus-dashboard-self-deploy.path
# Un échec de contrôle doit rester visible dans systemd, sans bloquer linstallation du timer.
systemctl start manus-apps-health.service || true
systemctl status manus-apps-health.service --no-pager || true

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Surveille les demandes de redéploiement du Manus Dashboard
[Path]
PathExists=/opt/manus-deploy/apps/manus-dashboard/.deployment-request
Unit=manus-dashboard-self-deploy.service
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,13 @@
[Unit]
Description=Redéploiement hôte du Manus Dashboard
After=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/opt/manus-deploy/scripts/deploy-dashboard-from-host.sh
Restart=on-failure
RestartSec=1min
TimeoutStartSec=10min
StandardOutput=journal
StandardError=journal

View File

@@ -25,10 +25,10 @@ RUN apk add --no-cache docker-cli docker-cli-compose git curl unzip bash openssh
RUN git config --global user.email "admin@santinova-soft.org" && \
git config --global user.name "Manus Dashboard"
# Copy backend
COPY backend/package.json ./backend/
# Installer les dépendances backend depuis un verrou versionné pour un build déterministe.
COPY backend/package.json backend/package-lock.json ./backend/
WORKDIR /app/backend
RUN npm install --production
RUN npm ci --omit=dev
COPY backend/ ./

2483
src/backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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"
"test": "node test/app-registry.test.js && node test/healthcheck.test.js && node test/webhook.test.js"
},
"dependencies": {
"express": "^4.21.0",

View File

@@ -17,10 +17,33 @@ const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '';
const APPS_BASE_PATH = process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps';
const DEPLOY_SCRIPT = '/opt/manus-deploy/scripts/deploy-app.sh';
const LOG_DIR = '/var/log/manus-deploy';
const DASHBOARD_APP_ID = 'manus-dashboard';
const DASHBOARD_DEPLOY_REQUEST = path.join(APPS_BASE_PATH, DASHBOARD_APP_ID, '.deployment-request');
// Déploiements en cours (évite les doubles déclenchements)
const deployingApps = new Set();
/**
* Le conteneur ne peut pas se recréer lui-même : Docker interrompt son client
* `docker compose` au moment où le conteneur courant est arrêté. Ce cas est
* donc délégué au service systemd hôte manus-dashboard-self-deploy.path.
*/
function isHostManagedDeployment(appName) {
return appName === DASHBOARD_APP_ID;
}
function requestDashboardHostDeployment({ branch, commitHash, committer }) {
const temporaryRequest = `${DASHBOARD_DEPLOY_REQUEST}.${process.pid}.tmp`;
const request = {
branch,
commitHash,
committer,
requestedAt: new Date().toISOString(),
};
fs.writeFileSync(temporaryRequest, JSON.stringify(request, null, 2), { mode: 0o600 });
fs.renameSync(temporaryRequest, DASHBOARD_DEPLOY_REQUEST);
}
function verifyGiteaSignature(req) {
if (!WEBHOOK_SECRET) {
console.warn('[Webhook] AVERTISSEMENT: WEBHOOK_SECRET non défini. Validation désactivée.');
@@ -103,6 +126,15 @@ router.post('/gitea', (req, res) => {
if (!appName) return res.status(200).json({ message: `Dépôt ${repoName} non déployé ou sans manifeste valide` });
if (branch !== 'main') return res.status(200).json({ message: `Branch ${branch} ignorée` });
if (isHostManagedDeployment(appName)) {
requestDashboardHostDeployment({ branch, commitHash, committer });
return res.status(202).json({
message: 'Redéploiement du dashboard confié au service hôte',
commit: commitHash,
branch,
});
}
res.status(202).json({ message: `Déploiement de ${appName} déclenché`, commit: commitHash, branch, committer });
// Récupérer la fonction broadcast si disponible globalement
@@ -135,3 +167,4 @@ router.get('/status', (req, res) => {
});
module.exports = router;
module.exports.isHostManagedDeployment = isHostManagedDeployment;

View File

@@ -0,0 +1,11 @@
/*
* Un conteneur ne peut pas recréer fiablement son propre conteneur Docker :
* le client `docker compose` est arrêté en même temps que lui. Le webhook doit
* donc déléguer ce cas précis à un service systemd hôte.
*/
const assert = require('node:assert/strict');
const { isHostManagedDeployment } = require('../src/webhook');
assert.equal(isHostManagedDeployment('manus-dashboard'), true);
assert.equal(isHostManagedDeployment('itinova-contacts'), false);
console.log('OK webhook');