Skip to content

Commit b4adccd

Browse files
authored
feat: exit code breakdown + PostgreSQL CI (#8)
mrt check exit codes (breaking change, ESLint convention): 0 = no issues 1 = warnings only (previously exit 0 without --strict) 2 = errors found (previously exit 1) 2 = warnings + --strict (previously exit 1) Why: CI scripts can now distinguish must-fix (2) from should-review (1) if mrt check; then deploy; fi → still works (0 = success) if [ $? -eq 2 ]; then block_merge; fi → errors only if [ $? -ge 1 ]; then notify; fi → any issue PostgreSQL CI (new test-postgres job): - Runs postgres:16 service container - tests/test_postgres.py: 4 integration tests - reversible migration on PG - noop downgrade detection on PG - SmartSeeder insert+verify on PG (NullPool + quoting) - check_all() 2-migration chain on PG - Skips gracefully when TEST_DATABASE_URL not set (local dev) - SQLite job ignores test_postgres.py to avoid duplication
1 parent 603c75a commit b4adccd

4 files changed

Lines changed: 335 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
run: pip install -e ".[dev]" coverage
4747

4848
- name: Run tests
49-
run: coverage run -m pytest tests/ -v
49+
run: coverage run -m pytest tests/ -v --ignore=tests/test_postgres.py
5050

5151
- name: Generate coverage report
5252
run: coverage xml
@@ -59,3 +59,36 @@ jobs:
5959
files: coverage.xml
6060
flags: unittests
6161
fail_ci_if_error: false
62+
63+
test-postgres:
64+
runs-on: ubuntu-latest
65+
66+
services:
67+
postgres:
68+
image: postgres:16
69+
env:
70+
POSTGRES_USER: mrt
71+
POSTGRES_PASSWORD: mrt
72+
POSTGRES_DB: mrt_test
73+
options: >-
74+
--health-cmd pg_isready
75+
--health-interval 10s
76+
--health-timeout 5s
77+
--health-retries 5
78+
ports:
79+
- 5432:5432
80+
81+
steps:
82+
- uses: actions/checkout@v5
83+
84+
- uses: actions/setup-python@v6
85+
with:
86+
python-version: "3.13"
87+
88+
- name: Install
89+
run: pip install -e ".[dev,postgres]" coverage
90+
91+
- name: Run PostgreSQL tests
92+
env:
93+
TEST_DATABASE_URL: postgresql://mrt:mrt@localhost:5432/mrt_test
94+
run: coverage run -m pytest tests/test_postgres.py -v

pytest_mrt/cli.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def version_cmd() -> None:
4242
@app.command("check")
4343
def check(
4444
versions_dir: str = typer.Argument(help="Path to Alembic versions directory"),
45-
strict: bool = typer.Option(False, "--strict", help="Exit 1 on warnings too"),
45+
strict: bool = typer.Option(False, "--strict", help="Treat warnings as errors (exit 2)"),
4646
fmt: str = typer.Option("table", "--format", "-f", help="Output format: table | json"),
4747
) -> None:
4848
"""Statically analyze migrations for rollback risk patterns (Alembic and Django)."""
@@ -75,7 +75,12 @@ def check(
7575
]
7676
sys.stdout.write(json.dumps(output, indent=2) + "\n")
7777
has_errors = any(w.severity == "error" for w in warnings)
78-
raise typer.Exit(1 if has_errors or (strict and warnings) else 0)
78+
has_warns = any(w.severity == "warning" for w in warnings)
79+
if has_errors or (strict and has_warns):
80+
raise typer.Exit(2)
81+
if has_warns:
82+
raise typer.Exit(1)
83+
raise typer.Exit(0)
7984

8085
if not warnings:
8186
console.print("[green]✓ No rollback risks detected.[/green]")
@@ -103,13 +108,13 @@ def check(
103108
console.print(
104109
f"[red]{len(errors)} error(s)[/red], [yellow]{len(warns)} warning(s)[/yellow]"
105110
)
106-
raise typer.Exit(1)
111+
raise typer.Exit(2)
107112
elif warns and strict:
108-
console.print(f"[yellow]{len(warns)} warning(s)[/yellow] (--strict mode)")
109-
raise typer.Exit(1)
113+
console.print(f"[yellow]{len(warns)} warning(s)[/yellow] (--strict: treated as errors)")
114+
raise typer.Exit(2)
110115
else:
111116
console.print(f"[yellow]{len(warns)} warning(s)[/yellow] — review before deploying")
112-
raise typer.Exit(0)
117+
raise typer.Exit(1)
113118

114119

115120
# ──────────────────────────────────────────────

tests/test_cli.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ def test_check_safe_migrations_exits_0(tmp_path, versions_dir):
7373
assert "No rollback risks" in result.output
7474

7575

76-
def test_check_risky_migration_exits_1(tmp_path, versions_dir):
76+
def test_check_risky_migration_exits_2(tmp_path, versions_dir):
7777
_risky_migration(versions_dir)
7878
result = runner.invoke(app, ["check", str(versions_dir)])
79-
assert result.exit_code == 1
79+
assert result.exit_code == 2
8080

8181

8282
def test_check_shows_pattern_name(tmp_path, versions_dir):
@@ -102,6 +102,7 @@ def test_check_json_format(tmp_path, versions_dir):
102102
assert len(data) > 0
103103
assert "pattern" in data[0]
104104
assert "severity" in data[0]
105+
assert result.exit_code == 2 # errors → exit 2
105106

106107

107108
def test_check_json_safe_exits_0(tmp_path, versions_dir):
@@ -126,8 +127,9 @@ def downgrade():
126127
"""))
127128
result_normal = runner.invoke(app, ["check", str(versions_dir)])
128129
result_strict = runner.invoke(app, ["check", str(versions_dir), "--strict"])
129-
# strict mode should exit 1 when there are warnings
130-
assert result_strict.exit_code >= result_normal.exit_code
130+
# warnings only: exit 1 normally, exit 2 with --strict
131+
assert result_normal.exit_code == 1
132+
assert result_strict.exit_code == 2
131133

132134

133135
def test_check_detects_django_migrations(tmp_path):

0 commit comments

Comments
 (0)