refactor: Extract deploy logic to castle-core and add deploy API endpoint

- Extract all deploy logic from CLI into castle-core/deploy.py as a reusable deploy() function
  * Resolves services/jobs to DeployedComponents
  * Generates systemd units and Caddyfile
  * Copies frontend assets and reloads systemd
  * Returns DeployResult with deployed count and messages
- Simplify CLI deploy command to a thin wrapper calling castle_core.deploy()
- Add POST /deploy endpoint to castle-api for remote deployment management
  * Supports optional {name} body param to deploy one or all services
  * Returns deployment result as JSON
- Register deploy router in castle-api main.py
- Update README with new deploy endpoint documentation

This enables full remote management: build, test, lint, and deploy any castle
node over HTTP (e.g. POST http://node:9000/api/deploy)
This commit is contained in:
2026-04-27 22:01:39 -07:00
parent e1a9386177
commit 691216b04e
7 changed files with 539 additions and 387 deletions

View File

@@ -0,0 +1,49 @@
"""Deploy API endpoint."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from castle_core.deploy import deploy
router = APIRouter(tags=["deploy"])
class DeployRequest(BaseModel):
"""Optional request body for deploy."""
name: str | None = None
class DeployResponse(BaseModel):
"""Response from a deploy operation."""
status: str
deployed_count: int
messages: list[str]
@router.post("/deploy", response_model=DeployResponse)
def run_deploy(request: DeployRequest | None = None) -> DeployResponse:
"""Deploy services and jobs from castle.yaml to runtime.
Resolves env vars and secrets, generates systemd units and Caddyfile,
copies frontend build outputs, and reloads systemd.
Optionally pass a name to deploy a single service or job.
"""
target_name = request.name if request else None
try:
result = deploy(target_name=target_name)
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
return DeployResponse(
status="ok",
deployed_count=result.deployed_count,
messages=result.messages,
)