feat: persist repository rename identity
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
parent
13f7a10018
commit
8988a093f2
10 changed files with 1454 additions and 4 deletions
475
migrations/versions/f3c4d5e6a7b8_repository_rename_identity.py
Normal file
475
migrations/versions/f3c4d5e6a7b8_repository_rename_identity.py
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
"""repository identity, protected slugs, and rename journal (STATE-WP-0085-T02)
|
||||
|
||||
The existing repository UUID remains the durable State Hub identity. This
|
||||
migration adds only new rows around it: an explicit unverified Forge identity,
|
||||
one canonical slug record, and empty operation history. It deliberately does
|
||||
not infer Forge coordinates from a slug, remote URL, checkout, or Git commit.
|
||||
|
||||
Revision ID: f3c4d5e6a7b8
|
||||
Revises: e2b3c4d5f6a7
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
|
||||
revision = "f3c4d5e6a7b8"
|
||||
down_revision = "e2b3c4d5f6a7"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"repository_forge_identities",
|
||||
sa.Column(
|
||||
"id",
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
server_default=sa.text("gen_random_uuid()"),
|
||||
),
|
||||
sa.Column(
|
||||
"repo_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("provider", sa.String(length=40), nullable=True),
|
||||
sa.Column("forge_instance", sa.String(length=255), nullable=True),
|
||||
sa.Column("forge_owner", sa.String(length=255), nullable=True),
|
||||
sa.Column("forge_repository_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column(
|
||||
"verification_state",
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
server_default="unverified",
|
||||
),
|
||||
sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("verified_by", sa.String(length=160), nullable=True),
|
||||
sa.Column(
|
||||
"verification_evidence", JSONB(), nullable=False, server_default="{}"
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"repo_id", name="uq_repository_forge_identities_repo_id"
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"id",
|
||||
"repo_id",
|
||||
"verification_state",
|
||||
name="uq_repository_forge_identity_verified_ref",
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"provider",
|
||||
"forge_instance",
|
||||
"forge_owner",
|
||||
"forge_repository_id",
|
||||
name="uq_repository_forge_identity_tuple",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"verification_state IN ('unverified', 'verified')",
|
||||
name="ck_repository_forge_identity_state",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"forge_repository_id IS NULL OR forge_repository_id > 0",
|
||||
name="ck_repository_forge_id_positive",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"verification_state != 'verified' OR "
|
||||
"(provider IS NOT NULL AND forge_instance IS NOT NULL "
|
||||
"AND forge_owner IS NOT NULL AND forge_repository_id IS NOT NULL "
|
||||
"AND verified_at IS NOT NULL AND verified_by IS NOT NULL)",
|
||||
name="ck_repository_forge_verified_complete",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"verification_state = 'verified' OR "
|
||||
"(verified_at IS NULL AND verified_by IS NULL)",
|
||||
name="ck_repository_forge_unverified_has_no_attestation",
|
||||
),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"repository_rename_operations",
|
||||
sa.Column(
|
||||
"id",
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
server_default=sa.text("gen_random_uuid()"),
|
||||
),
|
||||
sa.Column(
|
||||
"repo_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("forge_identity_id", UUID(as_uuid=True), nullable=False),
|
||||
sa.Column(
|
||||
"forge_identity_state",
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
server_default="verified",
|
||||
),
|
||||
sa.Column("expected_provider", sa.String(length=40), nullable=False),
|
||||
sa.Column(
|
||||
"expected_forge_instance", sa.String(length=255), nullable=False
|
||||
),
|
||||
sa.Column("expected_forge_owner", sa.String(length=255), nullable=False),
|
||||
sa.Column("expected_forge_repository_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("expected_source_commit", sa.String(length=64), nullable=False),
|
||||
sa.Column("expected_default_branch", sa.String(length=255), nullable=False),
|
||||
sa.Column("old_slug", sa.String(length=100), nullable=False),
|
||||
sa.Column("new_slug", sa.String(length=100), nullable=False),
|
||||
sa.Column("old_coordinates", JSONB(), nullable=False),
|
||||
sa.Column("new_coordinates", JSONB(), nullable=False),
|
||||
sa.Column(
|
||||
"phase", sa.String(length=32), nullable=False, server_default="draft"
|
||||
),
|
||||
sa.Column("actor", sa.String(length=160), nullable=False),
|
||||
sa.Column("phase_changed_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("preflighted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("preflight_expires_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("rolled_back_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("evidence", JSONB(), nullable=False, server_default="{}"),
|
||||
sa.Column("error_code", sa.String(length=80), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("error_details", JSONB(), nullable=True),
|
||||
sa.Column("error_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"phase IN ('draft', 'preflighted', 'forge-renamed', "
|
||||
"'statehub-rebound', 'source-synced', 'consumers-verified', "
|
||||
"'completed', 'rollback-preflight', 'rolled-back')",
|
||||
name="ck_repository_rename_phase",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"old_slug != new_slug", name="ck_repository_rename_distinct_slugs"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"expected_forge_repository_id > 0",
|
||||
name="ck_repository_rename_forge_id_positive",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"forge_identity_state = 'verified'",
|
||||
name="ck_repository_rename_verified_identity",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"phase = 'draft' OR preflighted_at IS NOT NULL",
|
||||
name="ck_repository_rename_preflight_timestamp",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"phase != 'completed' OR completed_at IS NOT NULL",
|
||||
name="ck_repository_rename_completed_timestamp",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"phase != 'rolled-back' OR rolled_back_at IS NOT NULL",
|
||||
name="ck_repository_rename_rollback_timestamp",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["forge_identity_id", "repo_id", "forge_identity_state"],
|
||||
[
|
||||
"repository_forge_identities.id",
|
||||
"repository_forge_identities.repo_id",
|
||||
"repository_forge_identities.verification_state",
|
||||
],
|
||||
name="fk_repository_rename_verified_identity",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_repository_rename_operations_repo_id",
|
||||
"repository_rename_operations",
|
||||
["repo_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_repository_rename_operations_old_slug",
|
||||
"repository_rename_operations",
|
||||
["old_slug"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_repository_rename_operations_new_slug",
|
||||
"repository_rename_operations",
|
||||
["new_slug"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_repository_rename_phase",
|
||||
"repository_rename_operations",
|
||||
["phase"],
|
||||
)
|
||||
op.create_index(
|
||||
"uq_repository_rename_active_repo",
|
||||
"repository_rename_operations",
|
||||
["repo_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("phase NOT IN ('completed', 'rolled-back')"),
|
||||
)
|
||||
op.create_index(
|
||||
"uq_repository_rename_active_new_slug",
|
||||
"repository_rename_operations",
|
||||
["new_slug"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("phase NOT IN ('completed', 'rolled-back')"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"repository_slugs",
|
||||
sa.Column(
|
||||
"id",
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
server_default=sa.text("gen_random_uuid()"),
|
||||
),
|
||||
sa.Column(
|
||||
"repo_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("slug", sa.String(length=100), nullable=False),
|
||||
sa.Column("kind", sa.String(length=16), nullable=False),
|
||||
sa.Column(
|
||||
"protected", sa.Boolean(), nullable=False, server_default=sa.true()
|
||||
),
|
||||
sa.Column(
|
||||
"source_operation_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("repository_rename_operations.id", ondelete="RESTRICT"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.UniqueConstraint("slug", name="uq_repository_slugs_slug"),
|
||||
sa.CheckConstraint(
|
||||
"kind IN ('canonical', 'alias')", name="ck_repository_slug_kind"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"kind != 'alias' OR protected",
|
||||
name="ck_repository_slug_alias_protected",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_repository_slugs_source_operation_id",
|
||||
"repository_slugs",
|
||||
["source_operation_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_repository_slugs_repo_kind",
|
||||
"repository_slugs",
|
||||
["repo_id", "kind"],
|
||||
)
|
||||
op.create_index(
|
||||
"uq_repository_slugs_one_canonical",
|
||||
"repository_slugs",
|
||||
["repo_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("kind = 'canonical'"),
|
||||
)
|
||||
|
||||
# Explicitly mark every existing repository unverified. Do not parse or
|
||||
# infer Forge coordinates from current mutable fields.
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO repository_forge_identities
|
||||
(id, repo_id, verification_state, verification_evidence,
|
||||
created_at, updated_at)
|
||||
SELECT gen_random_uuid(), id, 'unverified', '{}'::jsonb, now(), now()
|
||||
FROM managed_repos
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE FUNCTION guard_repository_forge_identity_history()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RAISE EXCEPTION 'repository Forge identity rows are durable';
|
||||
END IF;
|
||||
IF OLD.verification_state = 'verified' AND ROW(
|
||||
NEW.repo_id,
|
||||
NEW.provider,
|
||||
NEW.forge_instance,
|
||||
NEW.forge_owner,
|
||||
NEW.forge_repository_id,
|
||||
NEW.verification_state,
|
||||
NEW.verified_at,
|
||||
NEW.verified_by,
|
||||
NEW.verification_evidence
|
||||
) IS DISTINCT FROM ROW(
|
||||
OLD.repo_id,
|
||||
OLD.provider,
|
||||
OLD.forge_instance,
|
||||
OLD.forge_owner,
|
||||
OLD.forge_repository_id,
|
||||
OLD.verification_state,
|
||||
OLD.verified_at,
|
||||
OLD.verified_by,
|
||||
OLD.verification_evidence
|
||||
) THEN
|
||||
RAISE EXCEPTION 'verified repository Forge identity is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TRIGGER repository_forge_identity_history_guard
|
||||
BEFORE UPDATE OR DELETE ON repository_forge_identities
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_repository_forge_identity_history()
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE FUNCTION guard_repository_slug_history()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RAISE EXCEPTION 'repository slug rows are durable';
|
||||
END IF;
|
||||
IF NEW.repo_id IS DISTINCT FROM OLD.repo_id
|
||||
OR NEW.slug IS DISTINCT FROM OLD.slug THEN
|
||||
RAISE EXCEPTION 'repository slug ownership is immutable';
|
||||
END IF;
|
||||
IF OLD.kind = 'alias' AND ROW(
|
||||
NEW.kind, NEW.protected, NEW.source_operation_id
|
||||
) IS DISTINCT FROM ROW(
|
||||
OLD.kind, OLD.protected, OLD.source_operation_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'protected repository alias is immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TRIGGER repository_slug_history_guard
|
||||
BEFORE UPDATE OR DELETE ON repository_slugs
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_repository_slug_history()
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE FUNCTION guard_repository_rename_operation_history()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RAISE EXCEPTION 'repository rename operation rows are durable';
|
||||
END IF;
|
||||
IF ROW(
|
||||
NEW.repo_id,
|
||||
NEW.forge_identity_id,
|
||||
NEW.forge_identity_state,
|
||||
NEW.expected_provider,
|
||||
NEW.expected_forge_instance,
|
||||
NEW.expected_forge_owner,
|
||||
NEW.expected_forge_repository_id,
|
||||
NEW.expected_source_commit,
|
||||
NEW.expected_default_branch,
|
||||
NEW.old_slug,
|
||||
NEW.new_slug,
|
||||
NEW.old_coordinates,
|
||||
NEW.new_coordinates,
|
||||
NEW.actor,
|
||||
NEW.created_at
|
||||
) IS DISTINCT FROM ROW(
|
||||
OLD.repo_id,
|
||||
OLD.forge_identity_id,
|
||||
OLD.forge_identity_state,
|
||||
OLD.expected_provider,
|
||||
OLD.expected_forge_instance,
|
||||
OLD.expected_forge_owner,
|
||||
OLD.expected_forge_repository_id,
|
||||
OLD.expected_source_commit,
|
||||
OLD.expected_default_branch,
|
||||
OLD.old_slug,
|
||||
OLD.new_slug,
|
||||
OLD.old_coordinates,
|
||||
OLD.new_coordinates,
|
||||
OLD.actor,
|
||||
OLD.created_at
|
||||
) THEN
|
||||
RAISE EXCEPTION 'repository rename operation assertions are immutable';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TRIGGER repository_rename_operation_history_guard
|
||||
BEFORE UPDATE OR DELETE ON repository_rename_operations
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_repository_rename_operation_history()
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO repository_slugs
|
||||
(id, repo_id, slug, kind, protected, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), id, slug, 'canonical', true, now(), now()
|
||||
FROM managed_repos
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# These tables are additive. Dropping them restores the prior schema and
|
||||
# cannot rewrite or delete any pre-existing work or telemetry row.
|
||||
op.drop_table("repository_slugs")
|
||||
op.drop_table("repository_rename_operations")
|
||||
op.drop_table("repository_forge_identities")
|
||||
op.execute(sa.text("DROP FUNCTION guard_repository_slug_history()"))
|
||||
op.execute(sa.text("DROP FUNCTION guard_repository_rename_operation_history()"))
|
||||
op.execute(sa.text("DROP FUNCTION guard_repository_forge_identity_history()"))
|
||||
Loading…
Add table
Add a link
Reference in a new issue