diff --git a/script/forgejo/install_forgejo.sh b/script/forgejo/install_forgejo.sh index a7c31bf..d2f873b 100755 --- a/script/forgejo/install_forgejo.sh +++ b/script/forgejo/install_forgejo.sh @@ -41,6 +41,11 @@ ADMIN_EMAIL="${FORGEJO_ADMIN_EMAIL:-admin@erplibre.local}" RUN_USER="${FORGEJO_USER:-git}" SKIP_ADMIN="${FORGEJO_SKIP_ADMIN:-0}" +# Ce qui a changé sur le disque pendant ce passage. Le service ne redémarre que +# si quelque chose a bougé : rejouer le script sur une forge saine ne doit pas +# l'interrompre, même deux secondes. +CHANGED=0 + BIN=/usr/local/bin/forgejo CONF_DIR=/etc/forgejo CONF="$CONF_DIR/app.ini" @@ -152,6 +157,7 @@ else esac chmod +x "$src" sudo install -m 0755 "$src" "$BIN" + CHANGED=1 say "${Green}binaire posé : $BIN${Color_Off}" rm -rf "$tmp" trap - EXIT @@ -249,11 +255,13 @@ LEVEL = info CONFEOF sudo chown root:"$RUN_USER" "$CONF" sudo chmod 640 "$CONF" + CHANGED=1 say "${Green}configuration écrite : $CONF${Color_Off}" fi # --- 6. Service ------------------------------------------------------------ -sudo tee "$UNIT" >/dev/null < "$unit_tmp" </dev/null 2>&1 \ - || die "le service refuse de démarrer : sudo journalctl -u forgejo -n 40" +if ! sudo cmp -s "$unit_tmp" "$UNIT" 2>/dev/null; then + sudo install -m 0644 "$unit_tmp" "$UNIT" + sudo systemctl daemon-reload + CHANGED=1 + say "service défini : $UNIT" +fi +rm -f "$unit_tmp" + +sudo systemctl enable forgejo.service >/dev/null 2>&1 || true +# « restart » et NON « enable --now » quand quelque chose a changé : « --now » +# ne touche pas à un service déjà actif, qui garde alors sa configuration en +# MÉMOIRE. Vécu, et le symptôme ne désigne pas la cause : le serveur comparait +# son ancien INTERNAL_TOKEN à celui que le hook venait de lire sur le disque, et +# répondait 403 à son propre hook. Tout push finissait sur « Forgejo: Internal +# Server Error Decoding Failed », le hook ne sachant pas décoder un 403. +if [ "$CHANGED" = 1 ]; then + sudo systemctl restart forgejo.service \ + || die "le service refuse de démarrer : sudo journalctl -u forgejo -n 40" + say "service redémarré (configuration ou binaire modifié)" +elif ! systemctl is-active --quiet forgejo.service; then + sudo systemctl start forgejo.service \ + || die "le service refuse de démarrer : sudo journalctl -u forgejo -n 40" +fi # --- 7. Attendre qu'il RÉPONDE -------------------------------------------- # Une requête HTTP, pas un « systemctl is-active » : le service est « active » @@ -286,7 +314,10 @@ sudo systemctl enable --now forgejo.service >/dev/null 2>&1 \ # silence, au premier tour de la boucle, code 1 sans un mot — vécu. ready=0 for i in $(seq 1 60); do - if curl -fsS -o /dev/null --max-time 3 \ + # « -fs » sans « -S » : dans une boucle de réessai, le message de curl est + # du bruit — « Failed to connect » au premier tour est normal, le service + # vient de redémarrer. C'est le die final qui parle si rien ne répond. + if curl -fs -o /dev/null --max-time 3 \ "http://127.0.0.1:$HTTP_PORT/api/v1/version"; then ready=1 break diff --git a/test/test_qemu_forgejo.py b/test/test_qemu_forgejo.py index c50bba0..a30dbc5 100644 --- a/test/test_qemu_forgejo.py +++ b/test/test_qemu_forgejo.py @@ -339,6 +339,39 @@ class TestTheScriptGuards(unittest.TestCase): self.assertIn("FORGEJO_ADMIN_USER:-erplibre", self.body) self.assertNotIn("FORGEJO_ADMIN_USER:-admin}", self.body) + def test_it_restarts_when_something_changed(self): + """« enable --now » ne touche PAS un service déjà actif : il garde alors + sa configuration en mémoire. Vécu, et le symptôme ne désignait pas la + cause — le serveur comparait son ancien INTERNAL_TOKEN à celui que le + hook venait de lire, répondait 403 à son propre hook, et tout push + finissait sur « Internal Server Error Decoding Failed ».""" + self.assertIn("systemctl restart forgejo.service", self.body) + self.assertNotIn("enable --now forgejo", self.body) + + def test_the_restart_is_conditional(self): + """Rejouer le script sur une forge saine ne doit pas l'interrompre, + même deux secondes.""" + self.assertIn("CHANGED=0", self.body) + self.assertIn('[ "$CHANGED" = 1 ]', self.body) + # Trois évènements le lèvent : binaire posé, config écrite, unité + # modifiée. + self.assertEqual(3, self.body.count("CHANGED=1")) + + def test_the_unit_is_compared_before_being_written(self): + """Sans comparaison, l'unité serait réécrite à l'identique et le + service redémarrerait pour rien à chaque passage.""" + self.assertIn("cmp -s", self.body) + + def test_the_readiness_loop_stays_quiet_while_retrying(self): + """« Failed to connect » au premier tour est normal — le service vient + de redémarrer. C'est le die final qui parle.""" + # La commande est coupée sur deux lignes : on regarde le BLOC de la + # boucle, pas la ligne qui porte l'URL. + start = self.body.index("ready=0") + block = self.body[start : self.body.index('[ "$ready" = 1 ]', start)] + self.assertIn("curl -fs -o /dev/null", block) + self.assertNotIn("-fsS", block) + def test_it_touches_no_package_manager(self): """C'est ce qui le rend portable : le binaire est statique.""" for pm in ("apt-get install", "dnf install", "pacman -S", "zypper"):