This commit is contained in:
2026-04-10 01:46:19 +03:00
parent ccbbf64ddb
commit da6d18f6d5
8 changed files with 480 additions and 125 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import os
from typing import Any
import requests
import yaml
@@ -40,6 +41,7 @@ from backend.auth import ( # noqa: E402
)
from backend.db import engine, get_db # noqa: E402
from backend.models import AutonomousPipeline, Base, User, UserChatMessage # noqa: E402
from backend.pipeline_migrate import migrate_definition # noqa: E402
class GenerationRequest(BaseModel):
@@ -71,9 +73,90 @@ class LowCodePhaseIn(BaseModel):
jenkins: JenkinsPhaseConfigIn = Field(default_factory=JenkinsPhaseConfigIn)
class GateCheckIn(BaseModel):
"""Условие для перехода конвейера на следующий этап (имя переменной → ожидаемое значение)."""
variable: str = ""
expected: str = ""
class StageIn(BaseModel):
"""Этап (например стенд): внутри него выполняются фазы; checks — гейты перед выходом на следующий этап."""
id: str
name: str = ""
description: str = ""
checks: list[GateCheckIn] = Field(default_factory=list)
phases: list[LowCodePhaseIn] = Field(default_factory=list)
class AutonomousPipelineSave(BaseModel):
title: str = ""
phases: list[LowCodePhaseIn] = Field(default_factory=list)
stages: list[StageIn] = Field(default_factory=list)
class ValidateGateChecksIn(BaseModel):
"""Проверка условий перехода: для каждого элемента checks сравнивается values[variable] с expected."""
checks: list[GateCheckIn] = Field(default_factory=list)
values: dict[str, Any] = Field(default_factory=dict)
class GateCheckFailure(BaseModel):
variable: str
reason: str # "missing" | "mismatch"
expected: str | None = None
actual: str | None = None
class ValidateGateChecksOut(BaseModel):
ok: bool
failures: list[GateCheckFailure] = Field(default_factory=list)
def evaluate_gate_checks(
checks: list[GateCheckIn],
values: dict[str, Any],
) -> tuple[bool, list[GateCheckFailure]]:
"""Сравнивает ожидания с фактическими значениями (строки после strip). Строки с пустым variable игнорируются."""
normalized: dict[str, str] = {}
for k, v in values.items():
key = str(k).strip()
if not key:
continue
if v is None:
normalized[key] = ""
else:
normalized[key] = str(v).strip()
failures: list[GateCheckFailure] = []
for c in checks:
var = (c.variable or "").strip()
if not var:
continue
exp = (c.expected or "").strip()
if var not in normalized:
failures.append(
GateCheckFailure(
variable=var,
reason="missing",
expected=exp,
actual=None,
)
)
continue
act = normalized[var]
if act != exp:
failures.append(
GateCheckFailure(
variable=var,
reason="mismatch",
expected=exp,
actual=act,
)
)
ok = len(failures) == 0
return ok, failures
class JenkinsTriggerRequest(BaseModel):
@@ -199,15 +282,15 @@ def get_autonomous_pipeline(
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
):
if user is None:
return {"title": "", "phases": [], "updated_at": None}
return {"title": "", "stages": [], "updated_at": None}
row = db.get(AutonomousPipeline, user.id)
if row is None:
return {"title": "", "phases": [], "updated_at": None}
defn = row.definition if isinstance(row.definition, dict) else {}
phases = defn.get("phases", [])
return {"title": "", "stages": [], "updated_at": None}
defn = migrate_definition(row.definition if isinstance(row.definition, dict) else {})
stages = defn.get("stages", [])
return {
"title": row.title,
"phases": phases,
"stages": stages,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}
@@ -222,22 +305,23 @@ def put_autonomous_pipeline(
raise HTTPException(status_code=401, detail="Authentication required")
try:
row = db.get(AutonomousPipeline, user.id)
dumped = [p.model_dump() for p in body.phases]
dumped = [s.model_dump() for s in body.stages]
new_def = {"schema_version": 2, "stages": dumped}
if row is None:
row = AutonomousPipeline(
user_id=user.id,
title=body.title,
definition={"schema_version": 1, "phases": dumped},
definition=new_def,
)
db.add(row)
else:
row.title = body.title
row.definition = {"schema_version": 1, "phases": dumped}
row.definition = new_def
db.commit()
db.refresh(row)
return {
"title": row.title,
"phases": row.definition.get("phases", []),
"stages": row.definition.get("stages", []),
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}
except SQLAlchemyError as e:
@@ -245,6 +329,18 @@ def put_autonomous_pipeline(
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
@app.post("/pipelines/autonomous/validate-gates", response_model=ValidateGateChecksOut)
def validate_autonomous_gates(
body: ValidateGateChecksIn,
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
):
"""Проверяет, что для каждого условия в `checks` в `values` есть совпадающее значение."""
if user is None:
raise HTTPException(status_code=401, detail="Authentication required")
ok, failures = evaluate_gate_checks(body.checks, body.values)
return ValidateGateChecksOut(ok=ok, failures=failures)
@app.post("/jenkins/trigger")
def jenkins_trigger(
req: JenkinsTriggerRequest,

View File

@@ -0,0 +1,29 @@
"""Миграция черновика конвейера: плоский список фаз → этапы (стенды) с фазами и проверками."""
from __future__ import annotations
import uuid
from typing import Any
def migrate_definition(defn: Any) -> dict:
"""Приводит definition к schema_version 2 с ключом stages."""
if not isinstance(defn, dict):
return {"schema_version": 2, "stages": []}
if defn.get("stages"):
return defn
phases = defn.get("phases") or []
if not phases:
return {"schema_version": 2, "stages": []}
return {
"schema_version": 2,
"stages": [
{
"id": str(uuid.uuid4()),
"name": "Этап 1",
"description": "Импорт: ранее фазы задавались без этапов (стендов).",
"checks": [],
"phases": phases,
}
],
}