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). 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