fix(deploy): déléguer le redéploiement du dashboard à l hôte
This commit is contained in:
@@ -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`.
|
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 l’arrêt du client Docker avant le démarrage du conteneur de remplacement.
|
||||||
|
|
||||||
Après mise à jour Git, installer ou actualiser le service avec :
|
Après mise à jour Git, installer ou actualiser le service avec :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
24
ops/deploy-dashboard-from-host.sh
Normal file
24
ops/deploy-dashboard-from-host.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Déploie le dashboard depuis l’hô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"
|
||||||
@@ -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 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.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 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 daemon-reload
|
||||||
systemctl enable --now manus-apps-health.timer
|
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 l’installation du timer.
|
# Un échec de contrôle doit rester visible dans systemd, sans bloquer l’installation du timer.
|
||||||
systemctl start manus-apps-health.service || true
|
systemctl start manus-apps-health.service || true
|
||||||
systemctl status manus-apps-health.service --no-pager || true
|
systemctl status manus-apps-health.service --no-pager || true
|
||||||
|
|||||||
9
ops/manus-dashboard-self-deploy.path
Normal file
9
ops/manus-dashboard-self-deploy.path
Normal 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
|
||||||
13
ops/manus-dashboard-self-deploy.service
Normal file
13
ops/manus-dashboard-self-deploy.service
Normal 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
|
||||||
@@ -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" && \
|
RUN git config --global user.email "admin@santinova-soft.org" && \
|
||||||
git config --global user.name "Manus Dashboard"
|
git config --global user.name "Manus Dashboard"
|
||||||
|
|
||||||
# Copy backend
|
# Installer les dépendances backend depuis un verrou versionné pour un build déterministe.
|
||||||
COPY backend/package.json ./backend/
|
COPY backend/package.json backend/package-lock.json ./backend/
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
RUN npm install --production
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
COPY backend/ ./
|
COPY backend/ ./
|
||||||
|
|
||||||
|
|||||||
2483
src/backend/package-lock.json
generated
Normal file
2483
src/backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"dev": "nodemon 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": {
|
"dependencies": {
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|||||||
@@ -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 APPS_BASE_PATH = process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps';
|
||||||
const DEPLOY_SCRIPT = '/opt/manus-deploy/scripts/deploy-app.sh';
|
const DEPLOY_SCRIPT = '/opt/manus-deploy/scripts/deploy-app.sh';
|
||||||
const LOG_DIR = '/var/log/manus-deploy';
|
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)
|
// Déploiements en cours (évite les doubles déclenchements)
|
||||||
const deployingApps = new Set();
|
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) {
|
function verifyGiteaSignature(req) {
|
||||||
if (!WEBHOOK_SECRET) {
|
if (!WEBHOOK_SECRET) {
|
||||||
console.warn('[Webhook] AVERTISSEMENT: WEBHOOK_SECRET non défini. Validation désactivée.');
|
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 (!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 (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 });
|
res.status(202).json({ message: `Déploiement de ${appName} déclenché`, commit: commitHash, branch, committer });
|
||||||
|
|
||||||
// Récupérer la fonction broadcast si disponible globalement
|
// Récupérer la fonction broadcast si disponible globalement
|
||||||
@@ -135,3 +167,4 @@ router.get('/status', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
module.exports.isHostManagedDeployment = isHostManagedDeployment;
|
||||||
|
|||||||
11
src/backend/test/webhook.test.js
Normal file
11
src/backend/test/webhook.test.js
Normal 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');
|
||||||
Reference in New Issue
Block a user