"""ORM tables owned by Agent 3 (delivery & infra). Use ``from app.core.database import Base`` and the helpers in ``app.infrastructure.db.columns`` (``pk()``, ``created()``). Do NOT import from ``app.infrastructure.db.models`` here: that module imports this one (import cycle). Foreign keys to existing tables are written as plain strings, e.g. ``ForeignKey("organizations.id", ondelete="CASCADE")``. See docs/plan/PLAN-v2.md §6. Four tables carry the whole CI/CD story: * ``pipeline_runs`` — one row per build. ``(project_id, status)`` is indexed because the worker's hot query is "the queued runs, oldest first". * ``pipeline_steps`` — the eight steps of a run, with a bounded log tail. The full output is streamed live and never stored; a build log is unbounded and a database is the wrong place for it. * ``releases`` — every sha that passed, so a demo rollback is a pointer move rather than a rebuild. * ``app_runtimes`` — the demo container a project currently has. One row per project (it is the primary key), because D5 gives each app exactly one demo container; blue/green is a temporary second container, not a second row. """ from __future__ import annotations from datetime import datetime from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base from app.domain.base import utcnow from app.infrastructure.db.columns import created, pk #: 64 KB, matching the plan's cap on a stored step log tail. MAX_LOG_TAIL_CHARS = 65_536 class PipelineRunRow(Base): __tablename__ = "pipeline_runs" __table_args__ = ( Index("ix_pipeline_run_project_status", "project_id", "status"), Index("ix_pipeline_run_org_created", "organization_id", "created_at"), ) id: Mapped[str] = pk() organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) project_id: Mapped[str] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True ) trigger: Mapped[str] = mapped_column(String(16), default="engine", nullable=False) branch: Mapped[str] = mapped_column(String(120), default="dev", nullable=False) sha: Mapped[str] = mapped_column(String(64), default="", nullable=False) status: Mapped[str] = mapped_column(String(16), default="queued", nullable=False) error_summary: Mapped[str] = mapped_column(Text, default="", nullable=False) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = created() class PipelineStepRow(Base): __tablename__ = "pipeline_steps" __table_args__ = (Index("ix_pipeline_step_run_index", "run_id", "index"),) id: Mapped[str] = pk() run_id: Mapped[str] = mapped_column( ForeignKey("pipeline_runs.id", ondelete="CASCADE"), nullable=False, index=True ) index: Mapped[int] = mapped_column(Integer, nullable=False) name: Mapped[str] = mapped_column(String(48), nullable=False) status: Mapped[str] = mapped_column(String(16), default="pending", nullable=False) exit_code: Mapped[int | None] = mapped_column(Integer, nullable=True) log_tail: Mapped[str] = mapped_column(Text, default="", nullable=False) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class ReleaseRow(Base): __tablename__ = "releases" __table_args__ = (Index("ix_release_project_created", "project_id", "created_at"),) id: Mapped[str] = pk() organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) project_id: Mapped[str] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True ) run_id: Mapped[str] = mapped_column( ForeignKey("pipeline_runs.id", ondelete="CASCADE"), nullable=False, index=True ) sha: Mapped[str] = mapped_column(String(64), nullable=False) artifact_path: Mapped[str] = mapped_column(String(512), default="", nullable=False) #: Exactly one release per project is live; the switch is a transaction. is_demo_live: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) created_at: Mapped[datetime] = created() class AppRuntimeRow(Base): """The demo container for one project (one row per project, by design).""" __tablename__ = "app_runtimes" __table_args__ = (Index("ix_app_runtime_org", "organization_id"),) project_id: Mapped[str] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True ) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) demo_container_id: Mapped[str | None] = mapped_column(String(160), nullable=True) host_port: Mapped[int | None] = mapped_column(Integer, nullable=True) demo_host: Mapped[str] = mapped_column(String(255), default="", nullable=False) status: Mapped[str] = mapped_column(String(16), default="provisioning", nullable=False) current_release_id: Mapped[str | None] = mapped_column( ForeignKey("releases.id", ondelete="SET NULL"), nullable=True ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False ) __all__ = [ "AppRuntimeRow", "MAX_LOG_TAIL_CHARS", "PipelineRunRow", "PipelineStepRow", "ReleaseRow", ]