[ENV-LADDER] Add per-project ordered environment ladder column + shim

Replace the single default_branch with an ordered environments ladder
(list[{name,branch}]; first=head=PR target, last=prod=release target,
middle=intermediates). Migration 073 adds nullable JSONB projects.environments;
a read-time shim (roboco/models/env_branches.py) synthesizes a degenerate
single-branch ladder from default_branch when environments is null, so
behavior is unchanged until the operator declares a real split in the panel.
Wired through Project/ProjectCreate/ProjectUpdate models (normalize_environments
validator: rejects empty name/branch + dup branches), ProjectTable column,
API request/response schemas, create+update routes + service. EnvSyncEngine
+ consumer rewires follow.
This commit is contained in:
Renn F
2026-07-15 02:46:40 +02:00
parent 3338f88e1b
commit 8621d01d54
7 changed files with 213 additions and 0 deletions
@@ -0,0 +1,42 @@
"""Per-project ordered environment ladder column.
Replaces the single ``default_branch`` as the source of truth for a project's
PR target (head rung) and release target (prod rung). The ladder is an ordered
``list[{name, branch}]``: index 0 = head (where dev/cell/leaf PRs land), index
-1 = prod (where the gated release executor commits + tags), middle rungs =
intermediates (qa/stag). The ``EnvSyncEngine`` cascades prod→…→head so dev
never falls behind prod, and the CEO-gated release promotes the full chain
head→…→prod before bumping.
Additive and nullable: an unset (null) ``environments`` falls back to a
degenerate single-branch ladder synthesized from ``default_branch`` at read
time (``roboco/services/env_branches.py``), so every existing project keeps
behaving byte-for-byte as before until the operator declares a real ladder
in the panel. ``default_branch`` is retained as the legacy/shim source.
Revision ID: 073_project_environments
Revises: 072_project_sandbox_extensions
Create Date: 2026-07-15
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision = "073_project_environments"
down_revision = "072_project_sandbox_extensions"
branch_labels: dict[str, str] | None = None
depends_on: dict[str, str] | None = None
def upgrade() -> None:
op.add_column(
"projects",
sa.Column("environments", postgresql.JSONB(), nullable=True),
)
def downgrade() -> None:
op.drop_column("projects", "environments")