This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit e77fbe04325e3e42949569bf5402040e5d0252f9
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Feb 23 16:58:32 2026 +0000

    Add a database model for the quarantined upload phase
---
 atr/models/sql.py                               | 80 +++++++++++++++++++++++++
 migrations/versions/0051_2026.02.23_5e288b2d.py | 60 +++++++++++++++++++
 2 files changed, 140 insertions(+)

diff --git a/atr/models/sql.py b/atr/models/sql.py
index a0937bf3..e54ae03c 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -164,6 +164,11 @@ class ProjectStatus(enum.StrEnum):
     STANDING = "standing"
 
 
+class QuarantineStatus(enum.Enum):
+    PENDING = "PENDING"
+    FAILED = "FAILED"
+
+
 class ReleasePhase(enum.StrEnum):
     # TODO: Rename these to the UI names?
     # COMPOSE, VOTE, FINISH, "DISTRIBUTE"
@@ -233,6 +238,13 @@ def pydantic_example(value: Any) -> 
dict[Literal["json_schema_extra"], dict[str,
     return {"json_schema_extra": {"example": value}}
 
 
+class QuarantineFileEntryV1(schema.Strict):
+    version: Literal[1] = 1
+    rel_path: str
+    size_bytes: int
+    content_hash: str
+
+
 class VoteEntry(schema.Strict):
     result: bool = schema.Field(alias="result", **pydantic_example(True))
     summary: str = schema.Field(alias="summary", **pydantic_example("This is a 
summary"))
@@ -307,6 +319,24 @@ class ResultsJSON(sqlalchemy.types.TypeDecorator):
             return None
 
 
+_QUARANTINE_FILE_METADATA_ADAPTER: Final = 
pydantic.TypeAdapter(list[QuarantineFileEntryV1])
+
+
+class QuarantineFileMetadataJSON(sqlalchemy.types.TypeDecorator):
+    impl = sqlalchemy.JSON
+    cache_ok = True
+
+    def process_bind_param(self, value, dialect):
+        if value is None:
+            return None
+        return _QUARANTINE_FILE_METADATA_ADAPTER.dump_python(value)
+
+    def process_result_value(self, value, dialect):
+        if value is None:
+            return None
+        return _QUARANTINE_FILE_METADATA_ADAPTER.validate_python(value)
+
+
 # SQL models
 
 
@@ -1081,6 +1111,54 @@ class PublicSigningKey(sqlmodel.SQLModel, table=True):
             self.expires = 
datetime.datetime.fromisoformat(self.expires.rstrip("Z"))
 
 
+# Quarantined: Release
+class Quarantined(sqlmodel.SQLModel, table=True):
+    id: int | None = sqlmodel.Field(default=None, primary_key=True)
+
+    # M-1: Quarantined -> Release
+    release_name: str = sqlmodel.Field(
+        foreign_key="release.name", ondelete="CASCADE", index=True, 
**example("example-0.0.1")
+    )
+    release: Release = sqlmodel.Relationship(
+        sa_relationship_kwargs={
+            "foreign_keys": "[Quarantined.release_name]",
+        },
+    )
+
+    asf_uid: str = sqlmodel.Field(**example("user"))
+    prior_revision_name: str | None = sqlmodel.Field(default=None, 
**example("example-0.0.1 00005"))
+    status: QuarantineStatus = sqlmodel.Field(
+        default=QuarantineStatus.PENDING, index=True, 
**example(QuarantineStatus.PENDING)
+    )
+    token: str = sqlmodel.Field(**example("0123456789abcdef0123456789abcdef"))
+    created: datetime.datetime = sqlmodel.Field(
+        default_factory=lambda: datetime.datetime.now(datetime.UTC),
+        sa_column=sqlalchemy.Column(UTCDateTime, nullable=False),
+        **example(datetime.datetime(2025, 5, 1, 1, 2, 3, tzinfo=datetime.UTC)),
+    )
+    completed: datetime.datetime | None = sqlmodel.Field(
+        default=None,
+        sa_column=sqlalchemy.Column(UTCDateTime, nullable=True),
+        **example(datetime.datetime(2025, 5, 1, 1, 32, 3, 
tzinfo=datetime.UTC)),
+    )
+    failure_reason: str | None = sqlmodel.Field(default=None, **example("Path 
traversal found in archive member"))
+    file_metadata: list[QuarantineFileEntryV1] | None = sqlmodel.Field(
+        default=None, sa_column=sqlalchemy.Column(QuarantineFileMetadataJSON)
+    )
+    use_check_cache: bool = sqlmodel.Field(default=True, **example(True))
+    description: str | None = sqlmodel.Field(default=None, **example("Upload 
from web compose flow"))
+
+    def model_post_init(self, _context):
+        if isinstance(self.created, str):
+            self.created = 
datetime.datetime.fromisoformat(self.created.rstrip("Z"))
+
+        if isinstance(self.completed, str):
+            self.completed = 
datetime.datetime.fromisoformat(self.completed.rstrip("Z"))
+
+        if isinstance(self.status, str):
+            self.status = QuarantineStatus(self.status)
+
+
 # ReleasePolicy: Project
 class ReleasePolicy(sqlmodel.SQLModel, table=True):
     id: int = sqlmodel.Field(default=None, primary_key=True)
@@ -1202,6 +1280,8 @@ class Revision(sqlmodel.SQLModel, table=True):
 
     description: str | None = sqlmodel.Field(default=None, **example("This is 
a description"))
     tag: str | None = sqlmodel.Field(default=None, **example("rc1"))
+    use_check_cache: bool = sqlmodel.Field(default=True, **example(True))
+    was_quarantined: bool = sqlmodel.Field(default=False, **example(False))
 
     def model_post_init(self, _context):
         if isinstance(self.created, str):
diff --git a/migrations/versions/0051_2026.02.23_5e288b2d.py 
b/migrations/versions/0051_2026.02.23_5e288b2d.py
new file mode 100644
index 00000000..0125d624
--- /dev/null
+++ b/migrations/versions/0051_2026.02.23_5e288b2d.py
@@ -0,0 +1,60 @@
+"""Add a model for the quarantined phase
+
+Revision ID: 0051_2026.02.23_5e288b2d
+Revises: 0050_2026.02.17_7406bb29
+Create Date: 2026-02-23 16:53:27.702822+00:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+import atr.models.sql as sql
+
+revision: str = "0051_2026.02.23_5e288b2d"
+down_revision: str | None = "0050_2026.02.17_7406bb29"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "quarantined",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("release_name", sa.String(), nullable=False),
+        sa.Column("asf_uid", sa.String(), nullable=False),
+        sa.Column("prior_revision_name", sa.String(), nullable=True),
+        sa.Column("status", sa.Enum("PENDING", "FAILED", 
name="quarantinestatus"), nullable=False),
+        sa.Column("token", sa.String(), nullable=False),
+        sa.Column("created", sql.UTCDateTime(timezone=True), nullable=False),
+        sa.Column("completed", sql.UTCDateTime(timezone=True), nullable=True),
+        sa.Column("failure_reason", sa.String(), nullable=True),
+        sa.Column("file_metadata", sa.JSON(), nullable=True),
+        sa.Column("use_check_cache", sa.Boolean(), nullable=False, 
server_default=sa.true()),
+        sa.Column("description", sa.String(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["release_name"],
+            ["release.name"],
+            name=op.f("fk_quarantined_release_name_release"),
+            ondelete="CASCADE",
+        ),
+        sa.PrimaryKeyConstraint("id", name=op.f("pk_quarantined")),
+    )
+    with op.batch_alter_table("quarantined", schema=None) as batch_op:
+        batch_op.create_index(batch_op.f("ix_quarantined_release_name"), 
["release_name"], unique=False)
+        batch_op.create_index(batch_op.f("ix_quarantined_status"), ["status"], 
unique=False)
+
+    with op.batch_alter_table("revision", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("was_quarantined", sa.Boolean(), 
nullable=False, server_default=sa.false()))
+
+
+def downgrade() -> None:
+    with op.batch_alter_table("revision", schema=None) as batch_op:
+        batch_op.drop_column("was_quarantined")
+
+    with op.batch_alter_table("quarantined", schema=None) as batch_op:
+        batch_op.drop_index(batch_op.f("ix_quarantined_status"))
+        batch_op.drop_index(batch_op.f("ix_quarantined_release_name"))
+
+    op.drop_table("quarantined")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to