kin: auto-commit after pipeline
This commit is contained in:
parent
17d7806838
commit
eab9e951ab
12 changed files with 1696 additions and 5 deletions
116
web/api.py
116
web/api.py
|
|
@ -226,12 +226,19 @@ class ProjectCreate(BaseModel):
|
|||
return self
|
||||
|
||||
|
||||
VALID_DEPLOY_RUNTIMES = {"docker", "node", "python", "static"}
|
||||
|
||||
|
||||
class ProjectPatch(BaseModel):
|
||||
execution_mode: str | None = None
|
||||
autocommit_enabled: bool | None = None
|
||||
auto_test_enabled: bool | None = None
|
||||
obsidian_vault_path: str | None = None
|
||||
deploy_command: str | None = None
|
||||
deploy_host: str | None = None
|
||||
deploy_path: str | None = None
|
||||
deploy_runtime: str | None = None
|
||||
deploy_restart_cmd: str | None = None
|
||||
test_command: str | None = None
|
||||
project_type: str | None = None
|
||||
ssh_host: str | None = None
|
||||
|
|
@ -246,6 +253,8 @@ def patch_project(project_id: str, body: ProjectPatch):
|
|||
body.execution_mode, body.autocommit_enabled is not None,
|
||||
body.auto_test_enabled is not None,
|
||||
body.obsidian_vault_path, body.deploy_command is not None,
|
||||
body.deploy_host is not None, body.deploy_path is not None,
|
||||
body.deploy_runtime is not None, body.deploy_restart_cmd is not None,
|
||||
body.test_command is not None,
|
||||
body.project_type, body.ssh_host is not None,
|
||||
body.ssh_user is not None, body.ssh_key_path is not None,
|
||||
|
|
@ -257,6 +266,8 @@ def patch_project(project_id: str, body: ProjectPatch):
|
|||
raise HTTPException(400, f"Invalid execution_mode '{body.execution_mode}'. Must be one of: {', '.join(VALID_EXECUTION_MODES)}")
|
||||
if body.project_type is not None and body.project_type not in VALID_PROJECT_TYPES:
|
||||
raise HTTPException(400, f"Invalid project_type '{body.project_type}'. Must be one of: {', '.join(VALID_PROJECT_TYPES)}")
|
||||
if body.deploy_runtime is not None and body.deploy_runtime != "" and body.deploy_runtime not in VALID_DEPLOY_RUNTIMES:
|
||||
raise HTTPException(400, f"Invalid deploy_runtime '{body.deploy_runtime}'. Must be one of: {', '.join(sorted(VALID_DEPLOY_RUNTIMES))}")
|
||||
conn = get_conn()
|
||||
p = models.get_project(conn, project_id)
|
||||
if not p:
|
||||
|
|
@ -274,6 +285,14 @@ def patch_project(project_id: str, body: ProjectPatch):
|
|||
if body.deploy_command is not None:
|
||||
# Empty string = sentinel for clearing (decision #68)
|
||||
fields["deploy_command"] = None if body.deploy_command == "" else body.deploy_command
|
||||
if body.deploy_host is not None:
|
||||
fields["deploy_host"] = None if body.deploy_host == "" else body.deploy_host
|
||||
if body.deploy_path is not None:
|
||||
fields["deploy_path"] = None if body.deploy_path == "" else body.deploy_path
|
||||
if body.deploy_runtime is not None:
|
||||
fields["deploy_runtime"] = None if body.deploy_runtime == "" else body.deploy_runtime
|
||||
if body.deploy_restart_cmd is not None:
|
||||
fields["deploy_restart_cmd"] = None if body.deploy_restart_cmd == "" else body.deploy_restart_cmd
|
||||
if body.test_command is not None:
|
||||
fields["test_command"] = body.test_command
|
||||
if body.project_type is not None:
|
||||
|
|
@ -325,19 +344,42 @@ def sync_obsidian_endpoint(project_id: str):
|
|||
|
||||
@app.post("/api/projects/{project_id}/deploy")
|
||||
def deploy_project(project_id: str):
|
||||
"""Execute deploy_command for a project. Returns stdout/stderr/exit_code.
|
||||
"""Deploy a project using structured runtime steps or legacy deploy_command.
|
||||
|
||||
# WARNING: shell=True — deploy_command is admin-only, set in Settings by the project owner.
|
||||
New-style (deploy_runtime set): uses core/deploy.py — structured steps,
|
||||
cascades to dependent projects via project_links.
|
||||
Legacy fallback (only deploy_command set): runs deploy_command via shell.
|
||||
|
||||
WARNING: shell=True — deploy commands are admin-only, set in Settings by the project owner.
|
||||
"""
|
||||
import time
|
||||
from core.deploy import deploy_with_dependents
|
||||
|
||||
conn = get_conn()
|
||||
p = models.get_project(conn, project_id)
|
||||
conn.close()
|
||||
if not p:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Project '{project_id}' not found")
|
||||
|
||||
deploy_runtime = p.get("deploy_runtime")
|
||||
deploy_command = p.get("deploy_command")
|
||||
if not deploy_command:
|
||||
raise HTTPException(400, "deploy_command not set for this project")
|
||||
|
||||
if not deploy_runtime and not deploy_command:
|
||||
conn.close()
|
||||
raise HTTPException(400, "Neither deploy_runtime nor deploy_command is set for this project")
|
||||
|
||||
if deploy_runtime:
|
||||
# New structured deploy with dependency cascade
|
||||
try:
|
||||
result = deploy_with_dependents(conn, project_id)
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
raise HTTPException(500, f"Deploy failed: {e}")
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
# Legacy fallback: run deploy_command via shell
|
||||
conn.close()
|
||||
cwd = p.get("path") or None
|
||||
start = time.monotonic()
|
||||
try:
|
||||
|
|
@ -386,6 +428,57 @@ def create_project(body: ProjectCreate):
|
|||
return p
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Project Links (KIN-079)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ProjectLinkCreate(BaseModel):
|
||||
from_project: str
|
||||
to_project: str
|
||||
type: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@app.post("/api/project-links")
|
||||
def create_project_link(body: ProjectLinkCreate):
|
||||
"""Create a project dependency link."""
|
||||
conn = get_conn()
|
||||
if not models.get_project(conn, body.from_project):
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Project '{body.from_project}' not found")
|
||||
if not models.get_project(conn, body.to_project):
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Project '{body.to_project}' not found")
|
||||
link = models.create_project_link(
|
||||
conn, body.from_project, body.to_project, body.type, body.description
|
||||
)
|
||||
conn.close()
|
||||
return link
|
||||
|
||||
|
||||
@app.get("/api/projects/{project_id}/links")
|
||||
def get_project_links(project_id: str):
|
||||
"""Get all project links where project is from or to."""
|
||||
conn = get_conn()
|
||||
if not models.get_project(conn, project_id):
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Project '{project_id}' not found")
|
||||
links = models.get_project_links(conn, project_id)
|
||||
conn.close()
|
||||
return links
|
||||
|
||||
|
||||
@app.delete("/api/project-links/{link_id}", status_code=204)
|
||||
def delete_project_link(link_id: int):
|
||||
"""Delete a project link by id."""
|
||||
conn = get_conn()
|
||||
deleted = models.delete_project_link(conn, link_id)
|
||||
conn.close()
|
||||
if not deleted:
|
||||
raise HTTPException(404, f"Link {link_id} not found")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phases (KIN-059)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -670,6 +763,19 @@ def patch_task(task_id: str, body: TaskPatch):
|
|||
return t
|
||||
|
||||
|
||||
@app.get("/api/pipelines/{pipeline_id}/logs")
|
||||
def get_pipeline_logs(pipeline_id: int, since_id: int = 0):
|
||||
"""Get pipeline log entries after since_id (for live console polling)."""
|
||||
conn = get_conn()
|
||||
row = conn.execute("SELECT id FROM pipelines WHERE id = ?", (pipeline_id,)).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Pipeline {pipeline_id} not found")
|
||||
logs = models.get_pipeline_logs(conn, pipeline_id, since_id=since_id)
|
||||
conn.close()
|
||||
return logs
|
||||
|
||||
|
||||
@app.get("/api/tasks/{task_id}/pipeline")
|
||||
def get_task_pipeline(task_id: str):
|
||||
"""Get agent_logs for a task (pipeline steps)."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue