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`.
|
||||
|
||||
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 :
|
||||
|
||||
```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 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 l’installation du timer.
|
||||
systemctl start manus-apps-health.service || 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" && \
|
||||
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
2483
src/backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
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