"""Add ``environment`` (dev/staging/prod) to ``cmdb_cloud_scopes``. Revision ID: 076 Revises: 075 Create Date: 2026-04-28 Production cloud scopes should not be auto-revoked when a grant's TTL expires — silently dropping prod access is too disruptive. The expiry sweeper now reads this column and skips revocation for ``environment='prod'`` scopes (notifying the owner instead). Dev/staging keep the existing revoke-on-expiry default. Backfill: every existing scope is set to ``'dev'`` since prod must be opted-in deliberately by an admin to prevent accidental "all my scopes are now exempt from cleanup" outcomes. Idempotent: ``add_column`` is guarded so the migration is safe to re-run. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "076" down_revision = "075" branch_labels = None depends_on = None def _column_exists(table: str, column: str) -> bool: bind = op.get_bind() insp = sa.inspect(bind) return any(c["name"] == column for c in insp.get_columns(table)) def upgrade() -> None: if _column_exists("cmdb_cloud_scopes", "environment"): return op.add_column( "cmdb_cloud_scopes", sa.Column( "environment", sa.String(16), nullable=False, server_default="dev", ), ) # Drop the server_default after the table fills in — the application # layer handles defaults via the SQLAlchemy column default. Keeping # the SQL default would mask future "we forgot to set environment" # bugs in the codepath. op.alter_column( "cmdb_cloud_scopes", "environment", existing_type=sa.String(16), existing_nullable=False, server_default=None, ) def downgrade() -> None: if not _column_exists("cmdb_cloud_scopes", "environment"): return op.drop_column("cmdb_cloud_scopes", "environment")