Skip to content

feat(server): bundle migrations in wheel and add agent-control-migrate#209

Open
abhinav-galileo wants to merge 4 commits intomainfrom
abhi/migrate-console-and-package-migrations
Open

feat(server): bundle migrations in wheel and add agent-control-migrate#209
abhinav-galileo wants to merge 4 commits intomainfrom
abhi/migrate-console-and-package-migrations

Conversation

@abhinav-galileo
Copy link
Copy Markdown
Collaborator

@abhinav-galileo abhinav-galileo commented May 4, 2026

Summary

Ship Alembic migration scripts and alembic.ini inside the installed wheel via hatch force-include, and expose a small agent-control-migrate console script that runs alembic upgrade head against the bundled config.

Lets non-Docker consumers (e.g. wheel-based deployments) run migrations from a vanilla install without vendoring migration files.

Scope

  • User-facing/API changes: new agent-control-migrate console script (subcommands: upgrade, downgrade, current, history, heads).
  • Internal changes: wheel layout adds agent_control_server/_alembic/ and agent_control_server/_alembic.ini (private, underscore-prefixed).
  • Out of scope: source-tree dev workflow (Makefile / docker-entrypoint.sh) is unchanged - existing alembic upgrade head from server/ still works.

Risk and Rollout

  • Risk level: low. Source-tree behavior unchanged; only adds new wheel content and a new entry point.
  • Rollback plan: revert; no schema or runtime change.

Testing

  • Added unit tests for the migrate entry point (server/unit_tests/test_migrate.py).
  • Verified the built wheel contains agent_control_server/_alembic/versions/*.py and agent_control_server/_alembic.ini.
  • Ran ruff check (server and root configs).
  • Manually verified agent-control-migrate against a real database.

Checklist

  • No doc updates needed (private bundle paths).

Ships server/alembic/ and alembic.ini inside the installed package via
hatch force-include, and exposes a small console script that runs
'alembic upgrade head' against the bundled config. Lets non-Docker
consumers run migrations from a wheel install without vendoring.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 98.24561% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
server/src/agent_control_server/migrate.py 98.24% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Adds success-path coverage for upgrade/downgrade/current/history/heads
via monkeypatch-and-assert against alembic.command, plus a build-time
guard that the source paths hatch force-include reads from still
exist in-tree (catches accidental wheel regressions).
Drops the implicit '-1' default for 'agent-control-migrate downgrade'.
Downgrade is destructive; an operational CLI should not roll back the
schema on a typo or incomplete command. Users must now pass an explicit
revision (e.g. 'downgrade -1' or 'downgrade <rev>').
@abhinav-galileo abhinav-galileo marked this pull request as ready for review May 4, 2026 12:34
``upgrade``, ``downgrade``, ``current``, ``history``, ``heads``.
"""
args = list(argv) if argv is not None else sys.argv[1:]
if not args:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unhandled exceptions from Alembic commands propagate as tracebacks

In migrate.py: lines42-65 none of the command.*() calls are wrapped in try/except. If the DB is unreachable (OperationalError), a migration is in conflict (CommandError), or the lock can't be acquired, the exception propagates uncaught. The raise SystemExit(main()) entry point never executes, so the caller gets a traceback + exit code 1.

For an operational deploy CLI this is fragile — deploy pipelines typically expect a clean error message and a predictable non-zero exit code. Recommend wrapping each command dispatch in:

try:
    command.upgrade(cfg, ...)
except Exception as exc:
    print(f"agent-control-migrate: {exc}", file=sys.stderr)
    return 1

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f21c959: alembic dispatch is now wrapped in try/except Exception; failures print agent-control-migrate: <reason> to stderr and return rc=1 instead of leaking a traceback. Covered by test_main_returns_nonzero_for_command_errors.

elif op == "heads":
command.heads(cfg)
else:
print(f"agent-control-migrate: unknown command '{op}'", file=sys.stderr)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--help/-h prints "unknown command" instead of usage
agent-control-migrate --help falls into the else branch and exits with code 2. Operational teams will hit this immediately. A simple check for op in ("-h", "--help") printing the supported-commands string would fix it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f21c959: rewrote the parser as argparse with subparsers, so --help/-h prints usage natively (rc=0). Covered by test_main_help_prints_usage.

"""
args = list(argv) if argv is not None else sys.argv[1:]
if not args:
args = ["upgrade", "head"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra arguments to upgrade/downgrade are silently dropped:
command.upgrade(cfg, rest[0] if rest else "head"). Running agent-control-migrate upgrade rev1 extra silently ignores extra. A typo like agent-control-migrate upgrade -1 (intended as a downgrade) passes "-1" to upgrade silently. Consider rejecting len(rest) > 1 with an error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f21c959: argparse now rejects extra positional args with a usage error. Covered by test_main_rejects_extra_positional_args. --sql for upgrade/downgrade is also wired through to alembic.command.upgrade(..., sql=True).

@@ -0,0 +1,115 @@
"""Unit tests for the bundled-migrations entry point.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test gap: bare upgrade keyword with no revision is untested
covers main([]) (default → upgrade head) and main(["upgrade", "abc123"]) (explicit revision), but not main(["upgrade"]) (bare keyword → should also call upgrade head). That path is exercised by rest[0] if rest else "head" but has no dedicated test case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f21c959: added test_main_bare_upgrade_runs_upgrade_head covering main(["upgrade"])upgrade head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants