diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0c7fb..e295778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.1.5] - 2026-07-31 + +### La versión ahora es comprobable, y el sello deja de llegar tarde + +Actualizar a 1.1.4 fallaba con *"no escribió `config\.version` tras la actualización"* aunque el +binario nuevo sí estuviera instalado y corriendo desde la ruta correcta. La causa era el **orden** +dentro de `ensure_runtime_layout()`: el sello de versión se escribía al final, detrás del +re-despliegue de las dependencias embebidas (7-Zip y ODBC). Y al cambiar de versión esas deps se +re-copian **enteras**, así que el sello quedaba por detrás de esa copia *y* del desempaquetado del +onefile de ~254 MB, con el antivirus escaneando cada archivo. El PANEL se rendía esperándolo y daba +por fallida una actualización que iba bien. + +- **El sello se escribe lo primero**, en cuanto existen las carpetas. Es además más honesto sobre lo + que significa —"qué binario está corriendo"—, que es cierto desde que el proceso arranca. Si el + despliegue de deps fallara después, su propio sello (que sigue yendo al final) lo detecta y el + siguiente arranque reintenta. +- **La versión va en la primera línea del log de arranque.** Permite comprobar qué binario corre de + verdad mirando solo `config/logs`, sin depender del sello ni del reporte al panel. Verificar una + actualización ya no obliga a creerse lo que diga otro sistema. + ## [1.1.4] - 2026-07-31 Recoge todo lo que sigue de esta sección y **sustituye a 1.1.3**, cuyo artefacto publicado se generó diff --git a/app/__init__.py b/app/__init__.py index 7e73427..ab2ec11 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,5 +4,5 @@ # package-release.sh (nombres de artefacto y release.json) y el reporte al PANEL. # Formato obligatorio: puntos y números, monotónico creciente — el PANEL compara # versiones como tuplas de enteros para detectar si hay una más nueva. -__version__ = "1.1.4" +__version__ = "1.1.5" __author__ = "Aduanasoft" diff --git a/app/config/bootstrap.py b/app/config/bootstrap.py index 20e2bee..e7dafd5 100644 --- a/app/config/bootstrap.py +++ b/app/config/bootstrap.py @@ -175,16 +175,30 @@ def ensure_runtime_layout() -> None: ): directory.mkdir(parents=True, exist_ok=True) - _write_env_if_missing() - # Los sellos se ubican bajo el CONFIG_DIR vigente (no la ruta absoluta precalculada) # para que respeten el monkeypatch de las pruebas y no escriban en el config/ real. deps_stamp_path = CONFIG_DIR / BUNDLED_STAMP_PATH.name version_stamp_path = CONFIG_DIR / VERSION_PATH.name - # Las deps embebidas se re-despliegan cuando el build trae otras versiones. El sello - # se escribe DESPUÉS de copiar: si la copia falla a medias, el próximo arranque lo - # reintenta en lugar de quedar marcado como al día. + # Sello de versión: lo lee el instalador remoto del PANEL por SFTP para verificar el + # despliegue (en Windows el .exe es console=False y no tiene stdout confiable). + # + # Se escribe LO PRIMERO, en cuanto existen las carpetas. Antes iba al final, detrás del + # re-despliegue de las deps embebidas, y eso lo hacía inservible justo cuando más importa: al + # cambiar de versión las deps se re-copian enteras, así que el sello quedaba por detrás de esa + # copia y del desempaquetado del onefile. El PANEL se rendía esperándolo y daba por fallida una + # actualización que en realidad iba bien. + # + # Escribirlo aquí es además más honesto sobre lo que el sello significa: "qué binario está + # corriendo", que es cierto desde que el proceso arranca. Si el despliegue de deps fallara + # después, el sello de DEPS —que sí va al final— lo detecta y el próximo arranque reintenta. + _write_stamp(version_stamp_path, __version__) + + _write_env_if_missing() + + # Las deps embebidas se re-despliegan cuando el build trae otras versiones. Su sello se + # escribe DESPUÉS de copiar: si la copia falla a medias, el próximo arranque lo reintenta + # en lugar de quedar marcado como al día. deps_stamp = _bundled_deps_stamp() refresh_deps = _read_stamp(deps_stamp_path) != deps_stamp _copy_bundled_tree(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR, refresh_deps) @@ -193,10 +207,6 @@ def ensure_runtime_layout() -> None: if refresh_deps: _write_stamp(deps_stamp_path, deps_stamp) - # Sello de versión: lo lee el instalador remoto del PANEL por SFTP para verificar el - # despliegue (en Windows el .exe es console=False y no tiene stdout confiable). - _write_stamp(version_stamp_path, __version__) - from ..constants import DB_PATH if not DB_PATH.exists(): diff --git a/runner.py b/runner.py index 7b54d81..f1eedb1 100644 --- a/runner.py +++ b/runner.py @@ -193,8 +193,14 @@ def _run() -> int: panel_ok = is_panel_configured() start_engine = args.start_engine or launch.start_engine + from app import __version__ + app_logger.info("=" * 80) - app_logger.info("CloudRestoreAS - Iniciando aplicación") + # La versión va en la PRIMERA línea del arranque a propósito: es la forma de comprobar qué + # binario está corriendo de verdad sin depender del sello ni del reporte al panel, mirando + # solo config/logs. Sin esto, verificar una actualización obligaba a creerse lo que dijera + # otro sistema. + app_logger.info(f"CloudRestoreAS {__version__} - Iniciando aplicación") app_logger.info(f"Directorio app: {ROOT_DIR}") if qt_platform == "offscreen": app_logger.info("Modo: sin display → plataforma Qt 'offscreen' (motor headless)") diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c53775f..a6a29e8 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -5,6 +5,7 @@ from pathlib import Path import pytest +from app.config import bootstrap from app.config.bootstrap import ensure_runtime_layout from app.config.env_loader import ( apply_env_overrides, @@ -112,6 +113,41 @@ def test_bootstrap_escribe_sello_de_version(tmp_path: Path, monkeypatch: pytest. assert (config_dir / ".version").read_text(encoding="utf-8").strip() == __version__ +def test_sello_de_version_se_escribe_antes_de_copiar_las_deps( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + """ + El sello tiene que existir ANTES de re-desplegar 7-Zip y ODBC. + + Iba al final, y eso lo volvía inservible justo cuando más importa: al cambiar de versión las + deps embebidas se re-copian enteras, así que el sello quedaba por detrás de esa copia y del + desempaquetado del onefile de ~254 MB con el antivirus escaneando. El PANEL se rendía + esperándolo y reportaba como fallida una actualización que en realidad iba bien. + """ + from app import __version__ + + config_dir = _redirect_bootstrap(tmp_path, monkeypatch) + + # Se observa el estado del sello EN EL MOMENTO en que empieza la copia de deps. + visto: dict[str, str | None] = {} + real = bootstrap._copy_bundled_tree + + def espia(*args, **kwargs): + sello = config_dir / ".version" + visto.setdefault( + "al_copiar", + sello.read_text(encoding="utf-8").strip() if sello.exists() else None, + ) + return real(*args, **kwargs) + + monkeypatch.setattr("app.config.bootstrap._copy_bundled_tree", espia) + ensure_runtime_layout() + + assert visto["al_copiar"] == __version__, ( + "el sello de versión debe existir antes de empezar a copiar las deps embebidas" + ) + + def test_bootstrap_redespliega_deps_cuando_cambia_el_manifiesto( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ):