From 44d28f07936c874ae742673f40dfcbcb1602d247 Mon Sep 17 00:00:00 2001 From: Quang <20378quang@gmail.com> Date: Sat, 17 Oct 2026 12:30:36 -0400 Subject: [PATCH 1/2] fix(migrations): apply alembic.ini logging so a migration run reports what it did env.py never loaded the logging section alembic.ini declares, so 'alembic upgrade head' (the compose migrate step) printed nothing and an operator could not tell whether it applied six revisions or none. Load it only when Alembic was started from an ini file, so a Config built in code (the tests, schema.py) keeps the caller's logging untouched. --- src/hymical_forms/migrations/env.py | 9 +++++++ tests/test_alembic_command.py | 37 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/test_alembic_command.py diff --git a/src/hymical_forms/migrations/env.py b/src/hymical_forms/migrations/env.py index 6f53f05..c0120da 100644 --- a/src/hymical_forms/migrations/env.py +++ b/src/hymical_forms/migrations/env.py @@ -9,6 +9,8 @@ from __future__ import annotations +from logging.config import fileConfig + from alembic import context from sqlalchemy import Connection @@ -16,6 +18,13 @@ from hymical_forms.db import create_engine_from_url from hymical_forms.models import Base +# Only the alembic command reads an ini file. Without this its logging section is +# never applied and a migration run prints nothing, so an operator cannot tell +# what it did. A Config built in code (the tests, schema.py) has no file and +# leaves the caller's logging alone. +if context.config.config_file_name is not None: + fileConfig(context.config.config_file_name, disable_existing_loggers=False) + target_metadata = Base.metadata diff --git a/tests/test_alembic_command.py b/tests/test_alembic_command.py new file mode 100644 index 0000000..4a9c4e5 --- /dev/null +++ b/tests/test_alembic_command.py @@ -0,0 +1,37 @@ +""" +the alembic command an operator runs to migrate, as the container image runs it +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parent.parent + + +def test_the_alembic_command_reports_each_revision_it_applies(tmp_path: Path) -> None: + # The one-shot migrate step is otherwise silent, so an operator cannot tell + # from its output whether it applied six revisions or none. Revision 0001 is + # used because SQLite cannot replay the chain to head (see AGENTS.md). + database = tmp_path / "forms.db" + completed = subprocess.run( + [ + sys.executable, + "-m", + "alembic", + "-x", + f"database_url=sqlite:///{database.as_posix()}", + "upgrade", + "0001", + ], + cwd=REPOSITORY_ROOT, + capture_output=True, + text=True, + timeout=120, + check=False, + ) + + assert completed.returncode == 0, completed.stderr + assert "Running upgrade -> 0001" in completed.stderr From 88d6c79a5159ee0bbff830de9cb245c6cf0ceb98 Mon Sep 17 00:00:00 2001 From: Quang <20378quang@gmail.com> Date: Sat, 17 Oct 2026 12:30:36 -0400 Subject: [PATCH 2/2] docs(deployment): run operator one-offs with --no-deps The api service depends on migrate, so 'docker compose run --rm api ...' ran 'alembic upgrade head' before every CLI command. After a build, a scheduled retention cleanup would migrate the database under the old api and worker, against the documented deploy order. With --no-deps the CLI only checks the schema revision, as the API and worker do. --- docs/operations/deployment.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md index 0156ee3..6729a2e 100644 --- a/docs/operations/deployment.md +++ b/docs/operations/deployment.md @@ -59,20 +59,29 @@ Keys are minted by the operator CLI and never over HTTP. Run it as a one-off container against the same database: ```bash -docker compose run --rm api python -m hymical_forms.cli create-key --name admin +docker compose run --rm --no-deps api python -m hymical_forms.cli create-key --name admin ``` The key is printed once. Store it; it cannot be shown again. See [Authentication](../api/authentication.md). +!!! warning "Keep `--no-deps` on every one-off command" + + The `api` service depends on `migrate`, so `docker compose run` without + `--no-deps` runs `alembic upgrade head` before the command. After a + `docker compose build`, that would migrate the database under the old `api` + and `worker`, against the [deploy order](#deploy-order). With `--no-deps` the + command only checks the schema revision, like the API and the worker do, and + refuses to run against one it does not expect. + ### Retention cleanup Nothing is deleted automatically. Run the sweep the same way, from whatever scheduler you already use: ```bash -docker compose run --rm api python -m hymical_forms.cli cleanup-submissions --dry-run -docker compose run --rm api python -m hymical_forms.cli cleanup-submissions +docker compose run --rm --no-deps api python -m hymical_forms.cli cleanup-submissions --dry-run +docker compose run --rm --no-deps api python -m hymical_forms.cli cleanup-submissions ``` See [Submission retention](retention.md).