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]
