update
This commit is contained in:
BIN
backend/__pycache__/auth.cpython-313.pyc
Normal file
BIN
backend/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/db.cpython-313.pyc
Normal file
BIN
backend/__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/main.cpython-313.pyc
Normal file
BIN
backend/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/pipeline_migrate.cpython-313.pyc
Normal file
BIN
backend/__pycache__/pipeline_migrate.cpython-313.pyc
Normal file
Binary file not shown.
116
backend/main.py
116
backend/main.py
@@ -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,
|
||||
|
||||
29
backend/pipeline_migrate.py
Normal file
29
backend/pipeline_migrate.py
Normal 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,
|
||||
}
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user