[IMP] migration: offer the COW checker at the error prompt

A failed update_addons_all left one option, « [1] to redo the command » —
useless against the failure that actually causes it, since redoing an upgrade
whose COW copy has drifted fails again identically. The prompt now offers to
run reset_stale_cow_views on the database concerned:

  [1] to redo the command
  [2] Check the COW views that drifted (technolibre_…_upgrade_15)

Choosing [2] runs the checker and comes back to the same prompt, so the usual
sequence — look, repair, redo — happens without leaving the migration.

The database is read from the failed command itself, since the executor is not
told which one it is. Three forms are recognised: « -d <db> », « --database
<db> », and the positional argument of the addons scripts. No database found,
no [2] offered — « make format » gets the old prompt unchanged.

The option only ever REPORTS. Resetting a copy can erase a real customisation,
so it stays a separate deliberate command, printed with the exact line to run
and a reminder to read the diff first.

Verified: the six command forms parsed correctly (and two that hold no
database at all), [2] invoking the tool then re-offering the choice, no [2] on
a command without a database, and the whole chain against the real
_upgrade_15, where it prints the two drifted copies followed by the reset
line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-08-02 04:36:25 -04:00
parent 188dad9960
commit 2997dfeb26
2 changed files with 90 additions and 9 deletions

View file

@ -1609,6 +1609,23 @@ TRANSLATIONS = {
"fr": "Rien de désinstallé.",
"en": "Nothing uninstalled.",
},
"Check the COW views that drifted": {
"fr": "Vérifier les vues COW en retard sur leur vue module",
"en": "Check the COW views that drifted",
},
"Tool not found": {
"fr": "Outil introuvable",
"en": "Tool not found",
},
"To reset one of them onto its module view:": {
"fr": "Pour réinitialiser l'une d'elles sur sa vue module :",
"en": "To reset one of them onto its module view:",
},
"Read the diff first: a copy can hold a customisation.": {
"fr": "Lire le diff d'abord : une copie peut porter une"
" personnalisation.",
"en": "Read the diff first: a copy can hold a customisation.",
},
"Migration statistics (read-only)": {
"fr": "📊 Statistiques de la migration (lecture seule)",
"en": "📊 Migration statistics (read-only)",

View file

@ -6,6 +6,7 @@ import datetime
import json
import logging
import os
import re
import shutil
import sys
import zipfile
@ -312,6 +313,52 @@ class TodoUpgrade:
answer = input(t("Choice (0-3, default 1): ")).strip()
return {"0": None, "2": "cli", "3": "stats"}.get(answer, "tui")
@staticmethod
def database_from_command(cmd):
"""Nom de base visé par une commande, ou "" si indécelable.
Sert à proposer le bon outil au bon moment quand une commande échoue.
On reconnaît les trois formes du dépôt : « -d <base> », « --database
<base> », et l'argument positionnel des scripts addons
(`update_addons_all.sh <base>`, `install_addons*.sh <base> <modules>`).
"""
if not cmd:
return ""
match = re.search(r"(?:^|\s)(?:-d|--database)[=\s]+([\w.-]+)", cmd)
if match:
return match.group(1)
match = re.search(
r"\./script/addons/\w+\.sh\s+([\w.-]+)",
cmd,
)
return match.group(1) if match else ""
def check_stale_cow_views(self, database_name):
"""Lance le détecteur de copies COW en retard sur leur vue module.
Purement consultatif : il n'écrit rien sans « --reset … --apply », que
l'on propose seulement après avoir montré le diff — réinitialiser une
copie peut effacer une personnalisation réelle."""
script_path = os.path.join(
PATH_MIGRATION_GLOBAL, "reset_stale_cow_views.py"
)
if not os.path.exists(script_path):
print(f"⚠️ {t('Tool not found')}: {script_path}")
return
status, _cmd = self.todo_upgrade_execute(
f"{PYTHON_BIN} ./{script_path} -d {database_name}",
wait_at_error=False,
)
if status:
# Sortie 1 = des écarts ont été trouvés (le script les a listés).
warn = t("Read the diff first: a copy can hold a customisation.")
print(f"\n💡 {t('To reset one of them onto its module view:')}")
print(
f" {PYTHON_BIN} ./{script_path} -d {database_name}"
f" --reset <key> --apply"
)
print(f" {warn}")
def show_stats(self):
"""Écran de statistiques, en lecture seule : rien n'est écrit, ni
dans la base ni dans le journal de migration."""
@ -2650,17 +2697,34 @@ class TodoUpgrade:
# failure, never as a success (defence in depth: exec_command_live now
# always sets one, but a silent None must not skip this prompt).
if (status is None or status) and wait_at_error:
print("[1] to redo the command")
wait_status = (
input(
"💬 Error detected, press to continue or ctrl+c to stop : "
database_name = self.database_from_command(cmd)
while True:
print("[1] to redo the command")
if database_name:
print(
f"[2] {t('Check the COW views that drifted')}"
f" ({database_name})"
)
wait_status = (
input(
"💬 Error detected, press to continue or ctrl+c to"
" stop : "
)
.strip()
.lower()
)
.strip()
.lower()
)
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
if wait_status == "2" and database_name:
# Le motif d'échec le plus fréquent ici est « Element
# <xpath …> cannot be located in parent view » : une copie
# COW en retard sur sa vue module. On propose l'outil sur
# place, puis on repose le choix pour rejouer.
self.check_stale_cow_views(database_name)
continue
break
if wait_status == "1":
return self.todo_upgrade_execute(