A push to `dev` now becomes a live demo, and a failed build never takes the demo down. - Four tables (pipeline_runs, pipeline_steps, releases, app_runtimes) and their migration; alembic upgrade/downgrade/check all clean. - DeliveryService implements DeliveryHooks: on_app_created provisions the demo port + host + first build (idempotently); on_branch_pushed queues a run for `dev` and supersedes older *queued* runs, leaving a running one alone. - The 8-step pipeline. Blue/green is real: the replacement starts on a staging port, is healthchecked, and only then does the route move — then the previous container is stopped. A failure removes the replacement, releases its port and leaves the old container serving. - The demo port ledger is the DATABASE, not the router adapters' in-memory maps: the API and the delivery worker are separate processes. - Runs are claimed with a conditional UPDATE, so two workers never build the same run. - Live logs are batched (1 event / 500 ms, 200 lines) and redacted as a whole tail — per-chunk redaction misses a secret split across pipe reads, and the *stored* tail leaked until this was fixed. - delivery worker: drain the queue, then poll remotes for pushes made outside the engine. - 7 routes; reads need membership, actions need projects.manage and are audited. Tests: 415 API (+21, 1 docker-marked skipped); ruff, mypy, alembic check clean. Report: docs/plan/reports/agent-3-phase-3.md
148 lines
5.7 KiB
Python
148 lines
5.7 KiB
Python
"""delivery infra: pipeline runs, steps, releases, app runtimes
|
|
|
|
Revision ID: a3b4c5d6e7f3
|
|
Revises: a0f0e1d2c3b4
|
|
Create Date: 2026-10-13 23:59:00.000000
|
|
|
|
Owned by the delivery workstream (PLAN-v2 §6). ``down_revision`` is the v2
|
|
foundation; Claude adds the merge revision when the three branches land.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "a3b4c5d6e7f3"
|
|
down_revision: str | None = "a0f0e1d2c3b4"
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"pipeline_runs",
|
|
sa.Column("id", sa.String(length=32), primary_key=True),
|
|
sa.Column(
|
|
"organization_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("organizations.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"project_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("projects.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("trigger", sa.String(length=16), nullable=False, server_default="engine"),
|
|
sa.Column("branch", sa.String(length=120), nullable=False, server_default="dev"),
|
|
sa.Column("sha", sa.String(length=64), nullable=False, server_default=""),
|
|
sa.Column("status", sa.String(length=16), nullable=False, server_default="queued"),
|
|
sa.Column("error_summary", sa.Text(), nullable=False, server_default=""),
|
|
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
)
|
|
op.create_index("ix_pipeline_runs_organization_id", "pipeline_runs", ["organization_id"])
|
|
op.create_index("ix_pipeline_runs_project_id", "pipeline_runs", ["project_id"])
|
|
op.create_index(
|
|
"ix_pipeline_run_project_status", "pipeline_runs", ["project_id", "status"]
|
|
)
|
|
op.create_index(
|
|
"ix_pipeline_run_org_created", "pipeline_runs", ["organization_id", "created_at"]
|
|
)
|
|
|
|
op.create_table(
|
|
"pipeline_steps",
|
|
sa.Column("id", sa.String(length=32), primary_key=True),
|
|
sa.Column(
|
|
"run_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("pipeline_runs.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("index", sa.Integer(), nullable=False),
|
|
sa.Column("name", sa.String(length=48), nullable=False),
|
|
sa.Column("status", sa.String(length=16), nullable=False, server_default="pending"),
|
|
sa.Column("exit_code", sa.Integer(), nullable=True),
|
|
sa.Column("log_tail", sa.Text(), nullable=False, server_default=""),
|
|
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
|
)
|
|
op.create_index("ix_pipeline_steps_run_id", "pipeline_steps", ["run_id"])
|
|
op.create_index("ix_pipeline_step_run_index", "pipeline_steps", ["run_id", "index"])
|
|
|
|
op.create_table(
|
|
"releases",
|
|
sa.Column("id", sa.String(length=32), primary_key=True),
|
|
sa.Column(
|
|
"organization_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("organizations.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"project_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("projects.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"run_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("pipeline_runs.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("sha", sa.String(length=64), nullable=False),
|
|
sa.Column("artifact_path", sa.String(length=512), nullable=False, server_default=""),
|
|
sa.Column(
|
|
"is_demo_live", sa.Boolean(), nullable=False, server_default=sa.false()
|
|
),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
)
|
|
op.create_index("ix_releases_organization_id", "releases", ["organization_id"])
|
|
op.create_index("ix_releases_project_id", "releases", ["project_id"])
|
|
op.create_index("ix_releases_run_id", "releases", ["run_id"])
|
|
op.create_index("ix_release_project_created", "releases", ["project_id", "created_at"])
|
|
|
|
op.create_table(
|
|
"app_runtimes",
|
|
sa.Column(
|
|
"project_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("projects.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
),
|
|
sa.Column(
|
|
"organization_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("organizations.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("demo_container_id", sa.String(length=160), nullable=True),
|
|
sa.Column("host_port", sa.Integer(), nullable=True),
|
|
sa.Column("demo_host", sa.String(length=255), nullable=False, server_default=""),
|
|
sa.Column(
|
|
"status", sa.String(length=16), nullable=False, server_default="provisioning"
|
|
),
|
|
sa.Column(
|
|
"current_release_id",
|
|
sa.String(length=32),
|
|
sa.ForeignKey("releases.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
)
|
|
op.create_index("ix_app_runtimes_organization_id", "app_runtimes", ["organization_id"])
|
|
op.create_index("ix_app_runtime_org", "app_runtimes", ["organization_id"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("app_runtimes")
|
|
op.drop_table("releases")
|
|
op.drop_table("pipeline_steps")
|
|
op.drop_table("pipeline_runs")
|