Files
ModelForge/backend/alembic/versions/20260826_0018_m12_lifecycle.py
T

432 lines
18 KiB
Python

"""Add evidence-bound lifecycle, rollback, retention and cleanup records.
Revision ID: 20260826_0018
Revises: 20260826_0017
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0018"
down_revision: str | None = "20260826_0017"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def id_column() -> sa.Column[object]:
return sa.Column("id", sa.Uuid(), primary_key=True, nullable=False)
def created_at() -> sa.Column[object]:
return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now())
def upgrade() -> None:
op.create_table(
"lifecycle_subjects",
id_column(),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("environment", sa.String(32), nullable=False),
sa.Column("state", sa.String(48), nullable=False),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("superseded_by_ref", sa.String(255)),
created_at(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_subject_version"),
sa.UniqueConstraint(
"target_type", "target_ref", "environment", name="uq_lifecycle_subject_target"
),
)
op.create_index("ix_lifecycle_subjects_target_type", "lifecycle_subjects", ["target_type"])
op.create_index("ix_lifecycle_subjects_target_ref", "lifecycle_subjects", ["target_ref"])
op.create_index("ix_lifecycle_subjects_environment", "lifecycle_subjects", ["environment"])
op.create_index("ix_lifecycle_subjects_state", "lifecycle_subjects", ["state"])
op.create_table(
"lifecycle_policy_revisions",
id_column(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("scope", sa.String(48), nullable=False),
sa.Column("requirements", sa.JSON(), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by", sa.String(255), nullable=False),
created_at(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_lifecycle_policy_revision"),
sa.UniqueConstraint("fingerprint", name="uq_lifecycle_policy_fingerprint"),
)
op.create_index("ix_lifecycle_policy_revisions_key", "lifecycle_policy_revisions", ["key"])
op.create_index("ix_lifecycle_policy_revisions_scope", "lifecycle_policy_revisions", ["scope"])
op.create_index(
"ix_lifecycle_policy_revisions_active", "lifecycle_policy_revisions", ["active"]
)
op.create_table(
"lifecycle_approval_requests",
id_column(),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"subject_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_subjects.id", ondelete="RESTRICT"),
),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("environment", sa.String(32), nullable=False),
sa.Column("requested_transition", sa.String(48), nullable=False),
sa.Column("evidence_snapshot", sa.JSON(), nullable=False),
sa.Column("evidence_fingerprint", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
sa.Column("warnings", sa.JSON(), nullable=False),
sa.Column("requested_by", sa.String(255), nullable=False),
sa.Column("approved_by", sa.String(255)),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True)),
sa.Column("stale_at", sa.DateTime(timezone=True)),
sa.Column("decided_at", sa.DateTime(timezone=True)),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
created_at(),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_approval_version"),
)
for column in (
"policy_revision_id",
"subject_id",
"target_type",
"target_ref",
"environment",
"evidence_fingerprint",
"status",
"expires_at",
"created_at",
):
op.create_index(
f"ix_lifecycle_approval_requests_{column}", "lifecycle_approval_requests", [column]
)
op.create_table(
"lifecycle_approval_evidence",
sa.Column(
"approval_request_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"),
primary_key=True,
),
sa.Column("evidence_type", sa.String(64), primary_key=True),
sa.Column("evidence_ref", sa.String(255), primary_key=True),
sa.Column("evidence_digest", sa.String(64), nullable=False),
)
op.create_index(
"ix_lifecycle_approval_evidence_evidence_ref",
"lifecycle_approval_evidence",
["evidence_ref"],
)
op.create_table(
"lifecycle_promotion_plans",
id_column(),
sa.Column(
"approval_request_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"subject_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_subjects.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("current_state", sa.String(48), nullable=False),
sa.Column("desired_state", sa.String(48), nullable=False),
sa.Column("migration_class", sa.String(32), nullable=False),
sa.Column(
"candidate_deployment_id",
sa.Uuid(),
sa.ForeignKey("capability_deployments.id", ondelete="RESTRICT"),
),
sa.Column("rollback_target_ref", sa.String(255), nullable=False),
sa.Column("project_consumers", sa.JSON(), nullable=False),
sa.Column("affected_identities", sa.JSON(), nullable=False),
sa.Column("impact_analysis", sa.JSON(), nullable=False),
sa.Column(
"migration_id",
sa.Uuid(),
sa.ForeignKey("embedding_migrations.id", ondelete="RESTRICT"),
),
sa.Column("canary_strategy", sa.JSON(), nullable=False),
sa.Column("drain_strategy", sa.JSON(), nullable=False),
sa.Column("health_gates", sa.JSON(), nullable=False),
sa.Column("automatic_abort_conditions", sa.JSON(), nullable=False),
sa.Column("plan_fingerprint", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="DRAFT"),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("created_by", sa.String(255), nullable=False),
sa.Column("approved_by", sa.String(255)),
sa.Column("immutable_at", sa.DateTime(timezone=True)),
created_at(),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_plan_version"),
sa.UniqueConstraint("plan_fingerprint", name="uq_lifecycle_plan_fingerprint"),
)
for column in (
"approval_request_id",
"subject_id",
"migration_class",
"candidate_deployment_id",
"migration_id",
"status",
"created_at",
):
op.create_index(
f"ix_lifecycle_promotion_plans_{column}", "lifecycle_promotion_plans", [column]
)
op.create_table(
"lifecycle_rollback_snapshots",
id_column(),
sa.Column(
"promotion_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("snapshot", sa.JSON(), nullable=False),
sa.Column("snapshot_digest", sa.String(64), nullable=False),
created_at(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("promotion_plan_id", name="uq_lifecycle_rollback_plan"),
sa.UniqueConstraint("snapshot_digest", name="uq_lifecycle_rollback_digest"),
)
op.create_index(
"ix_lifecycle_rollback_snapshots_promotion_plan_id",
"lifecycle_rollback_snapshots",
["promotion_plan_id"],
)
op.create_table(
"lifecycle_operations",
id_column(),
sa.Column(
"promotion_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("stage", sa.String(32), nullable=False),
sa.Column("idempotency_key", sa.String(128), nullable=False),
sa.Column("expected_subject_version", sa.Integer(), nullable=False),
sa.Column("requester", sa.String(255), nullable=False),
sa.Column("approver", sa.String(255), nullable=False),
sa.Column("executor", sa.String(255), nullable=False),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_details", sa.JSON(), nullable=False),
sa.Column("rollback_duration_ms", sa.Float()),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_operation_version"),
sa.UniqueConstraint("idempotency_key", name="uq_lifecycle_operation_idempotency"),
)
op.create_index(
"ix_lifecycle_operations_promotion_plan_id", "lifecycle_operations", ["promotion_plan_id"]
)
op.create_index("ix_lifecycle_operations_stage", "lifecycle_operations", ["stage"])
op.create_table(
"lifecycle_canary_runs",
id_column(),
sa.Column(
"operation_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_operations.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("mode", sa.String(48), nullable=False),
sa.Column("traffic_percent", sa.Integer(), nullable=False, server_default="0"),
sa.Column("eligible_clients", sa.JSON(), nullable=False),
sa.Column("eligible_projects", sa.JSON(), nullable=False),
sa.Column("target_request_count", sa.Integer(), nullable=False),
sa.Column("thresholds", sa.JSON(), nullable=False),
sa.Column("status", sa.String(48), nullable=False),
sa.Column("request_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("error_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("latency_p95_ms", sa.Float()),
sa.Column("abort_trigger", sa.String(64)),
sa.Column("result", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("operation_id", name="uq_lifecycle_canary_operation"),
)
op.create_index(
"ix_lifecycle_canary_runs_operation_id", "lifecycle_canary_runs", ["operation_id"]
)
op.create_index("ix_lifecycle_canary_runs_status", "lifecycle_canary_runs", ["status"])
op.create_table(
"retention_policy_revisions",
id_column(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("minimum_rollback_days", sa.Integer(), nullable=False),
sa.Column("requirements", sa.JSON(), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by", sa.String(255), nullable=False),
created_at(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_retention_policy_revision"),
sa.UniqueConstraint("fingerprint", name="uq_retention_policy_fingerprint"),
)
op.create_index("ix_retention_policy_revisions_key", "retention_policy_revisions", ["key"])
op.create_index(
"ix_retention_policy_revisions_active", "retention_policy_revisions", ["active"]
)
op.create_table(
"lifecycle_retention_records",
id_column(),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("retention_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("state", sa.String(32), nullable=False),
sa.Column("retained_until", sa.DateTime(timezone=True)),
sa.Column("legal_hold", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("reason", sa.Text(), nullable=False),
created_at(),
sa.UniqueConstraint("target_type", "target_ref", name="uq_lifecycle_retention_target"),
)
for column in ("policy_revision_id", "target_type", "target_ref", "state", "retained_until"):
op.create_index(
f"ix_lifecycle_retention_records_{column}", "lifecycle_retention_records", [column]
)
op.create_table(
"lifecycle_cleanup_plans",
id_column(),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("action", sa.String(48), nullable=False),
sa.Column("dependencies", sa.JSON(), nullable=False),
sa.Column("dependency_digest", sa.String(64), nullable=False),
sa.Column("reclaimable_bytes", sa.BigInteger(), nullable=False),
sa.Column("retention_state", sa.String(32), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
created_at(),
sa.Column("executed_at", sa.DateTime(timezone=True)),
)
for column in ("target_type", "target_ref", "status", "created_at"):
op.create_index(f"ix_lifecycle_cleanup_plans_{column}", "lifecycle_cleanup_plans", [column])
op.create_table(
"artifact_location_removal_records",
id_column(),
sa.Column(
"artifact_location_id",
sa.Uuid(),
sa.ForeignKey("artifact_locations.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"cleanup_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_cleanup_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("prior_status", sa.String(32), nullable=False),
sa.Column("prior_observed_sha256", sa.String(64)),
sa.Column("prior_size_bytes", sa.BigInteger()),
sa.Column("removed_by", sa.String(255), nullable=False),
sa.Column("removed_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index(
"ix_artifact_location_removal_records_artifact_location_id",
"artifact_location_removal_records",
["artifact_location_id"],
)
op.create_index(
"ix_artifact_location_removal_records_cleanup_plan_id",
"artifact_location_removal_records",
["cleanup_plan_id"],
)
op.create_table(
"lifecycle_events",
id_column(),
sa.Column("event_type", sa.String(64), nullable=False),
sa.Column("object_type", sa.String(48), nullable=False),
sa.Column("object_ref", sa.String(255), nullable=False),
sa.Column("from_state", sa.String(48)),
sa.Column("to_state", sa.String(48)),
sa.Column("actor", sa.String(255), nullable=False),
sa.Column("actor_role", sa.String(32), nullable=False),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_policy_revisions.id", ondelete="RESTRICT"),
),
sa.Column("evidence_ids", sa.JSON(), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("change_id", sa.String(64), nullable=False),
sa.Column("details", sa.JSON(), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in (
"event_type",
"object_type",
"object_ref",
"policy_revision_id",
"change_id",
"occurred_at",
):
op.create_index(f"ix_lifecycle_events_{column}", "lifecycle_events", [column])
op.create_index(
"uq_capability_deployment_one_production_stable",
"capability_deployments",
["capability_contract_id"],
unique=True,
postgresql_where=sa.text("production = true AND status = 'stable'"),
sqlite_where=sa.text("production = 1 AND status = 'stable'"),
)
def downgrade() -> None:
op.drop_index(
"uq_capability_deployment_one_production_stable",
table_name="capability_deployments",
)
for table in (
"lifecycle_events",
"artifact_location_removal_records",
"lifecycle_cleanup_plans",
"lifecycle_retention_records",
"retention_policy_revisions",
"lifecycle_canary_runs",
"lifecycle_operations",
"lifecycle_rollback_snapshots",
"lifecycle_promotion_plans",
"lifecycle_approval_evidence",
"lifecycle_approval_requests",
"lifecycle_policy_revisions",
"lifecycle_subjects",
):
op.drop_table(table)