From f7f98ffc1ca44673fa1deaf2635ea3eabb8eb6c6 Mon Sep 17 00:00:00 2001 From: Michael Ventnor Date: Fri, 27 Feb 2026 14:14:05 -0600 Subject: [PATCH 1/2] Bump version, and ensure new versions always released --- .github/workflows/ci.yml | 12 ++++++++++++ pyproject.toml | 2 +- squawk_alembic/__init__.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfe31fa..50d32f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,18 @@ jobs: fi echo "Versions match: $PYPROJECT_VERSION" + if [ "${{ github.event_name }}" = "pull_request" ]; then + git fetch origin main --depth=1 + MAIN_VERSION=$(git show origin/main:pyproject.toml | grep '^version = ' | sed 's/version = "\(.*\)"/\1/' || true) + if [ -z "$MAIN_VERSION" ]; then + echo "::warning::Could not determine version on main, skipping version bump check" + elif [ "$PYPROJECT_VERSION" = "$MAIN_VERSION" ]; then + echo "::error::Version $PYPROJECT_VERSION is the same as on main. Please bump the version." + echo "Run 'make bump VERSION=x.y.z' to update both files." + exit 1 + fi + fi + - name: Run tests run: poetry run pytest tests/ -v diff --git a/pyproject.toml b/pyproject.toml index 0ca6c55..8e3edab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "squawk-alembic" -version = "0.3.0" +version = "0.3.1" description = "Pre-commit hook to lint Alembic migration SQL with squawk" packages = [{include = "squawk_alembic"}] readme = "README.md" diff --git a/squawk_alembic/__init__.py b/squawk_alembic/__init__.py index 493f741..260c070 100644 --- a/squawk_alembic/__init__.py +++ b/squawk_alembic/__init__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.1" From 5ba78fb39bcff580a86341255dfdb0b1c69f6eb6 Mon Sep 17 00:00:00 2001 From: Michael Ventnor Date: Fri, 27 Feb 2026 14:29:59 -0600 Subject: [PATCH 2/2] In CI environments, you must do a shallow fetch even with an origin/ prefix --- squawk_alembic/hook.py | 28 ++++++++--- tests/test_main.py | 102 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/squawk_alembic/hook.py b/squawk_alembic/hook.py index 23cb2a8..58a0595 100644 --- a/squawk_alembic/hook.py +++ b/squawk_alembic/hook.py @@ -133,7 +133,11 @@ def generate_sql(filepath): def validate_branch(branch): - """Validate that a branch name is safe and exists in git.""" + """Validate that a branch name is safe and exists in git. + + For remote refs (origin/...), attempts a shallow fetch when the ref is + missing locally — common in CI shallow clones. + """ if not _BRANCH_RE.match(branch) or ".." in branch: print( f"squawk-alembic: invalid branch name: {branch!r}", @@ -148,13 +152,23 @@ def validate_branch(branch): except FileNotFoundError: print("squawk-alembic: git not found", file=sys.stderr) return False - if result.returncode != 0: - print( - f"squawk-alembic: branch '{branch}' not found in git", - file=sys.stderr, + if result.returncode == 0: + return True + + if branch.startswith("origin/"): + remote_branch = branch.removeprefix("origin/") + fetch = subprocess.run( + ["git", "fetch", "origin", remote_branch, "--depth=1"], + capture_output=True, ) - return False - return True + if fetch.returncode == 0: + return True + + print( + f"squawk-alembic: branch '{branch}' not found in git", + file=sys.stderr, + ) + return False def file_exists_on_branch(filepath, branch): diff --git a/tests/test_main.py b/tests/test_main.py index 3590525..a06cdee 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -35,6 +35,7 @@ def fake_subprocess( squawk_result=None, git_exists_on_branch=False, git_branch_valid=True, + git_fetch_succeeds=False, ): """Return a side_effect function that dispatches based on the command.""" alembic_res = alembic_result or make_result(stdout="CREATE TABLE foo (id int);\n") @@ -44,7 +45,11 @@ def side_effect(cmd, **kwargs): if cmd[0] == "git": if "rev-parse" in cmd: return make_result(returncode=0 if git_branch_valid else 1) - return make_result(returncode=0 if git_exists_on_branch else 1) + if "fetch" in cmd: + return make_result(returncode=0 if git_fetch_succeeds else 1) + if "cat-file" in cmd: + return make_result(returncode=0 if git_exists_on_branch else 1) + return make_result(returncode=1) if cmd[0] == "alembic": return alembic_res if cmd[0] == "squawk": @@ -381,3 +386,98 @@ def upgrade(): assert main() == 0 # No git call, just alembic + squawk = 2 calls assert mock_run.call_count == 2 + + +def test_origin_branch_shallow_fetch_succeeds(repo): + """In CI shallow clones, origin/main may not exist locally; the hook should fetch it.""" + path = write_migration( + repo, + "016_shallow.py", + """ + revision = 'sha001' + down_revision = 'prev001' + + from alembic import op + + def upgrade(): + op.execute("CREATE TABLE foo (id int)") + """, + ) + with ( + patch("sys.argv", ["squawk-alembic", "--diff-branch", "origin/main", path]), + patch( + "subprocess.run", + side_effect=fake_subprocess( + git_branch_valid=False, + git_fetch_succeeds=True, + git_exists_on_branch=False, + ), + ) as mock_run, + ): + assert main() == 0 + # git rev-parse (fail) + git fetch + git cat-file + alembic + squawk = 5 calls + assert mock_run.call_count == 5 + assert mock_run.call_args_list[0][0][0][0] == "git" + assert "fetch" in mock_run.call_args_list[1][0][0] + assert "cat-file" in mock_run.call_args_list[2][0][0] + + +def test_origin_branch_shallow_fetch_fails(repo, capsys): + """When both rev-parse and fetch fail, the hook should error.""" + path = write_migration( + repo, + "017_fetch_fail.py", + """ + revision = 'ff001' + down_revision = 'prev001' + + from alembic import op + + def upgrade(): + op.execute("CREATE TABLE foo (id int)") + """, + ) + with ( + patch("sys.argv", ["squawk-alembic", "--diff-branch", "origin/main", path]), + patch( + "subprocess.run", + side_effect=fake_subprocess( + git_branch_valid=False, + git_fetch_succeeds=False, + ), + ) as mock_run, + ): + assert main() == 1 + # git rev-parse (fail) + git fetch (fail) = 2 calls + assert mock_run.call_count == 2 + captured = capsys.readouterr() + assert "not found in git" in captured.err + + +def test_non_origin_branch_no_fetch_attempted(repo, capsys): + """Non-origin branches should not trigger a fetch attempt.""" + path = write_migration( + repo, + "018_no_fetch.py", + """ + revision = 'nf001' + down_revision = 'prev001' + + from alembic import op + + def upgrade(): + op.execute("CREATE TABLE foo (id int)") + """, + ) + with ( + patch("sys.argv", ["squawk-alembic", "--diff-branch", "main", path]), + patch( + "subprocess.run", + side_effect=fake_subprocess(git_branch_valid=False), + ) as mock_run, + ): + assert main() == 1 + # Only git rev-parse (fail), no fetch attempted + assert mock_run.call_count == 1 + captured = capsys.readouterr() + assert "not found in git" in captured.err