Nicolas Cantu 8813498de4 Fix SSH connection errors during deployment
**Motivations:**
- SSH ControlMaster connection errors were causing deployment failures
- Connection reset errors were not handled properly
- No retry mechanism for failed SSH connections

**Root causes:**
- SSH ControlMaster socket could become stale or be closed prematurely
- No validation of connection before use
- No cleanup of dead connections
- Silent failures in conditional checks

**Correctifs:**
- Added connection validation before each SSH command
- Implemented automatic cleanup of dead SSH connections
- Added retry mechanism (up to 3 attempts) with connection cleanup
- Enhanced SSH options for better connection stability (ConnectTimeout, ServerAliveInterval, ServerAliveCountMax)
- Improved error handling in Git repository verification step with explicit error detection and recovery

**Evolutions:**
- Enhanced SSH connection management with robust error handling
- Better error messages to distinguish connection errors from other failures

**Pages affectées:**
- deploy.sh: Enhanced ssh_exec() function, added helper functions, improved error handling
- fixKnowledge/ssh-connection-errors-deployment.md: Documentation of the problem, root cause, and solution
2026-01-06 14:22:24 +01:00

287 lines
9.6 KiB
Bash

#!/bin/bash
set -e
# Configuration
SERVER="debian@92.243.27.35"
APP_NAME="zapwall"
DOMAIN="zapwall.fr"
APP_DIR="/var/www/${DOMAIN}"
GIT_REPO="https://git.4nkweb.com/4nk/story-research-zapwall.git"
# Configuration SSH pour connexion persistante (évite MaxStartups)
# Utiliser un chemin temporaire sans espaces pour ControlPath
SSH_CONTROL_DIR="/tmp/ssh_control_$$"
mkdir -p "${SSH_CONTROL_DIR}"
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/debian_92.243.27.35_22"
# Fonction pour nettoyer une connexion SSH morte
cleanup_dead_ssh() {
ssh -O exit -o ControlPath="${SSH_CONTROL_PATH}" ${SERVER} 2>/dev/null || true
rm -f "${SSH_CONTROL_PATH}" 2>/dev/null || true
}
# Fonction pour vérifier si la connexion SSH maître est valide
check_ssh_connection() {
ssh -O check -o ControlPath="${SSH_CONTROL_PATH}" ${SERVER} 2>/dev/null || return 1
}
# Fonction pour exécuter une commande SSH avec connexion persistante et gestion d'erreurs robuste
ssh_exec() {
local max_retries=3
local retry_count=0
while [ $retry_count -lt $max_retries ]; do
# Vérifier si la connexion maître existe et est valide
if [ -S "${SSH_CONTROL_PATH}" ]; then
if ! check_ssh_connection; then
# Connexion morte, nettoyer avant de réessayer
cleanup_dead_ssh
fi
fi
# Exécuter la commande SSH
if ssh -o ControlMaster=auto \
-o ControlPath="${SSH_CONTROL_PATH}" \
-o ControlPersist=300 \
-o ConnectTimeout=10 \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
${SERVER} "$@" 2>&1; then
return 0
else
local exit_code=$?
retry_count=$((retry_count + 1))
if [ $retry_count -lt $max_retries ]; then
# Nettoyer la connexion morte avant de réessayer
cleanup_dead_ssh
sleep 1
else
# Dernière tentative échouée, retourner le code d'erreur
return $exit_code
fi
fi
done
}
# Nettoyer les connexions SSH persistantes et le répertoire temporaire à la fin
cleanup_ssh() {
cleanup_dead_ssh
rm -rf "${SSH_CONTROL_DIR}" 2>/dev/null || true
}
trap cleanup_ssh EXIT
# Vérifier qu'un message de commit est fourni
if [ -z "$1" ]; then
echo "Erreur: Un message de commit est requis"
echo ""
echo "Usage: ./deploy.sh \"Message de commit\""
echo ""
echo "Exemple: ./deploy.sh \"Fix: Correction du bug de connexion\""
exit 1
fi
COMMIT_MESSAGE="$1"
echo "=== Déploiement de ${DOMAIN} ==="
echo ""
# Détecter la branche courante
BRANCH=$(git branch --show-current 2>/dev/null || echo "main")
echo "Branche courante: ${BRANCH}"
echo "Message de commit: ${COMMIT_MESSAGE}"
echo ""
# Commit et push des modifications locales
echo "1. Préparation du commit local..."
HAS_CHANGES=false
HAS_UNPUSHED_COMMITS=false
# Vérifier s'il y a des modifications non commitées
if ! git diff --quiet || ! git diff --cached --quiet; then
HAS_CHANGES=true
fi
# Vérifier s'il y a des commits non poussés
if git rev-parse --abbrev-ref ${BRANCH}@{upstream} >/dev/null 2>&1; then
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse @{u})
if [ "$LOCAL" != "$REMOTE" ]; then
HAS_UNPUSHED_COMMITS=true
fi
else
# Pas de branche distante configurée, vérifier s'il y a des commits locaux
if git rev-list --count origin/${BRANCH}..HEAD >/dev/null 2>&1; then
UNPUSHED_COUNT=$(git rev-list --count origin/${BRANCH}..HEAD 2>/dev/null || echo "0")
if [ "$UNPUSHED_COUNT" -gt 0 ]; then
HAS_UNPUSHED_COMMITS=true
fi
fi
fi
if [ "$HAS_CHANGES" = true ]; then
echo " ✓ Modifications détectées"
echo ""
echo "2. Ajout des modifications..."
git add -A
echo " ✓ Fichiers ajoutés"
echo ""
echo "3. Création du commit..."
git commit -m "${COMMIT_MESSAGE}"
echo " ✓ Commit créé"
HAS_UNPUSHED_COMMITS=true
fi
if [ "$HAS_UNPUSHED_COMMITS" = true ]; then
echo ""
echo "4. Push vers le dépôt distant..."
git push origin ${BRANCH}
echo " ✓ Push effectué"
else
echo " ⚠ Aucune modification à commiter ni commit à pousser"
fi
# Vérifier si Git est initialisé sur le serveur
echo ""
echo "5. Vérification du dépôt Git sur le serveur..."
GIT_STATUS_OUTPUT=$(ssh_exec "cd ${APP_DIR} && git status >/dev/null 2>&1 && echo 'OK' || echo 'NOT_INIT'")
if echo "$GIT_STATUS_OUTPUT" | grep -q "OK"; then
echo " ✓ Dépôt Git détecté"
elif echo "$GIT_STATUS_OUTPUT" | grep -q "NOT_INIT"; then
echo " ⚠ Dépôt Git non initialisé, initialisation..."
ssh_exec "cd ${APP_DIR} && git init && git remote add origin ${GIT_REPO} 2>/dev/null || git remote set-url origin ${GIT_REPO}"
ssh_exec "cd ${APP_DIR} && git checkout -b ${BRANCH} 2>/dev/null || true"
else
echo " ✗ Erreur de connexion SSH lors de la vérification du dépôt Git"
echo " Tentative de nettoyage et nouvelle connexion..."
cleanup_dead_ssh
sleep 2
# Réessayer une fois après nettoyage
if ssh_exec "cd ${APP_DIR} && git status >/dev/null 2>&1"; then
echo " ✓ Dépôt Git détecté après réessai"
else
echo " ⚠ Dépôt Git non initialisé, initialisation..."
ssh_exec "cd ${APP_DIR} && git init && git remote add origin ${GIT_REPO} 2>/dev/null || git remote set-url origin ${GIT_REPO}"
ssh_exec "cd ${APP_DIR} && git checkout -b ${BRANCH} 2>/dev/null || true"
fi
fi
# Récupérer les dernières modifications
echo ""
echo "6. Récupération des dernières modifications..."
ssh_exec "cd ${APP_DIR} && git fetch origin"
# Sauvegarder les modifications locales sur le serveur
echo ""
echo "7. Sauvegarde des modifications locales sur le serveur..."
STASH_OUTPUT=$(ssh_exec "cd ${APP_DIR} && git stash push -u -m 'Auto-stash before deploy - $(date +%Y-%m-%d_%H:%M:%S)' 2>&1" || echo "No changes to stash")
if echo "$STASH_OUTPUT" | grep -q "No local changes"; then
echo " ✓ Aucune modification locale à sauvegarder"
else
echo " ✓ Modifications locales sauvegardées"
fi
# Nettoyer les fichiers non suivis
echo ""
echo "8. Nettoyage des fichiers non suivis..."
ssh_exec "cd ${APP_DIR} && git clean -fd || true"
# Vérifier que la branche existe
echo ""
echo "9. Vérification de la branche ${BRANCH}..."
if ssh_exec "cd ${APP_DIR} && git ls-remote --heads origin ${BRANCH} | grep -q ${BRANCH}"; then
echo " ✓ Branche ${BRANCH} trouvée"
else
echo " ✗ Branche ${BRANCH} non trouvée sur le dépôt distant"
echo ""
echo " Branches disponibles:"
AVAILABLE_BRANCHES=$(ssh_exec "cd ${APP_DIR} && git ls-remote --heads origin | sed 's/.*refs\\/heads\\///'")
echo "$AVAILABLE_BRANCHES" | sed 's/^/ - /'
echo ""
echo " Erreur: La branche '${BRANCH}' n'existe pas sur le dépôt distant."
echo " Vérifiez que vous avez bien poussé la branche avec 'git push origin ${BRANCH}'"
exit 1
fi
# Mise à jour depuis la branche
echo ""
echo "10. Mise à jour depuis la branche ${BRANCH}..."
ssh_exec "cd ${APP_DIR} && git checkout ${BRANCH} 2>/dev/null || git checkout -b ${BRANCH} origin/${BRANCH}"
ssh_exec "cd ${APP_DIR} && git pull origin ${BRANCH}"
# Afficher le dernier commit
echo ""
echo "11. Dernier commit:"
ssh_exec "cd ${APP_DIR} && git log -1 --oneline"
# Copier next.config.js local vers le serveur (pour ignorer ESLint pendant le build)
echo ""
echo "12. Mise à jour de next.config.js pour ignorer ESLint pendant le build..."
if [ -f "next.config.js" ]; then
cat next.config.js | ssh_exec "cat > ${APP_DIR}/next.config.js"
echo " ✓ next.config.js mis à jour"
else
echo " ⚠ next.config.js local non trouvé, utilisation de celui du serveur"
fi
# Mettre à jour les dépendances aux dernières versions
echo ""
echo "13. Mise à jour des dépendances aux dernières versions..."
ssh_exec "cd ${APP_DIR} && npx -y npm-check-updates -u || true"
# Installer les dépendances
echo ""
echo "14. Installation des dépendances..."
ssh_exec "cd ${APP_DIR} && npm install"
# Construire l'application
echo ""
echo "15. Construction de l'application..."
ssh_exec "cd ${APP_DIR} && npm run build"
# Redémarrer le service
echo ""
echo "16. Redémarrage du service ${APP_NAME}..."
ssh_exec "sudo systemctl restart ${APP_NAME}"
sleep 3
# Vérifier que le service fonctionne
echo ""
echo "17. Vérification du service..."
if ssh_exec "sudo systemctl is-active ${APP_NAME} >/dev/null"; then
echo " ✓ Service actif"
echo ""
echo " Statut du service:"
ssh_exec "sudo systemctl status ${APP_NAME} --no-pager | head -10"
else
echo " ✗ Service inactif, vérification des logs..."
ssh_exec "sudo journalctl -u ${APP_NAME} --no-pager -n 30"
exit 1
fi
# Vérifier que le port est en écoute
echo ""
echo "18. Vérification du port 3001..."
if ssh_exec "sudo ss -tuln | grep -q ':3001 '"; then
echo " ✓ Port 3001 en écoute"
else
echo " ⚠ Port 3001 non encore en écoute, attente..."
sleep 5
if ssh_exec "sudo ss -tuln | grep -q ':3001 '"; then
echo " ✓ Port 3001 maintenant en écoute"
else
echo " ✗ Port 3001 toujours non en écoute"
exit 1
fi
fi
echo ""
echo "=== Déploiement terminé avec succès ==="
echo ""
echo "Site disponible sur: https://${DOMAIN}/"
echo ""
echo "Commandes utiles:"
echo " Voir les logs: ssh ${SERVER} 'sudo journalctl -u ${APP_NAME} -f'"
echo " Voir les stashes: ssh ${SERVER} 'cd ${APP_DIR} && git stash list'"
echo " Restaurer un stash: ssh ${SERVER} 'cd ${APP_DIR} && git stash pop'"