"""identity_sync_targets.alert_recipient_ids — per-target failure-alert recipients. Revision ID: 308 Revises: 307 Create Date: 2026-06-19 Adds a nullable JSON column holding a list of ``identity_persons.id`` (UUID strings) who receive the run-failure alert card for that target. NULL / empty list falls back to the IT-admin broadcast in the notifier, so existing targets keep alerting (to admins) without a backfill. Idempotent + re-runnable. Motivation: an ``event:hire_confirm`` Google Workspace dept push failed with a 403 and produced NO alert — the only failure-notification path was the cron dispatcher's raise-on-failure, which event / manual runs never traverse. The fix routes the alert at run finalisation (covers all trigger sources) and lets admins pick who gets paged per target. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "308" down_revision = "307" branch_labels = None depends_on = None _TABLE = "identity_sync_targets" _COLUMN = "alert_recipient_ids" def _has_column(bind, table: str, column: str) -> bool: insp = sa.inspect(bind) return any(c["name"] == column for c in insp.get_columns(table)) def upgrade() -> None: bind = op.get_bind() if not _has_column(bind, _TABLE, _COLUMN): op.add_column( _TABLE, sa.Column(_COLUMN, sa.JSON(), nullable=True), ) def downgrade() -> None: bind = op.get_bind() if _has_column(bind, _TABLE, _COLUMN): op.drop_column(_TABLE, _COLUMN)