Files
ServerKit/backend/app/api/deployments.py
T
Juan DenisandClaude Opus 4.8 afd042896b refactor(unify): §3 — canonical DeploymentJob links + unified /deployments surface
Additive foundation + one read namespace for the three parallel deploy records
(versioned build Deployment, webhook GitDeployment, DeploymentJob executions).

Backend:
- DeploymentJob gains deployment_id / git_deployment_id / webhook_id FKs +
  commit_hash / image_tag / container_id, so it can be the canonical execution
  record pointing at the release ledgers it produces (surfaced in to_dict).
  Migration 039 (idempotent, additive).
- New /api/v1/deployments surface:
    GET /apps/:id/history?source=build|webhook|all  → federated, source-tagged
    GET /:id                                         → build Deployment detail
  and the DeploymentJob surface re-mounted at /api/v1/deployments/jobs (alias of
  /deployment-jobs). Per-system blueprints (/builds, /deploy, /git) unchanged.

Deliberately NOT done (plan §3 Phase 1/2 deep merge, with rationale): folding
GitDeployment into Deployment+DeploymentJob and async-ifying the currently
synchronous build/webhook deploys. Recon confirmed those paths are synchronous
with distinct failure/timeout semantics and GitDeployment carries rollback state
(snapshot_data, rolled_back_to_id); flipping them to the queue can't be verified
without a live build/runner, so it's deferred behind the additive links above.

Verified: 6 new tests + previews/config-snapshot regression pass (35).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:52:04 -04:00

75 lines
2.9 KiB
Python

"""Unified deployment surface (§3 unification).
One read namespace over the deploy records that today live in three places:
versioned build ``Deployment`` rows, webhook-triggered ``GitDeployment`` rows,
and the canonical ``DeploymentJob`` execution records (mounted here under
``/jobs``). The per-system blueprints (/builds, /deploy, /git, /deployment-jobs)
are unchanged; this is an additive, federated view so the frontend can read one
history instead of merging two sources.
"""
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.models import Application, User
from app.models.deployment import Deployment
from app.models.webhook import GitDeployment
deployments_bp = Blueprint('deployments', __name__)
def _access(app_id):
from app.services.resource_grant_service import ResourceGrantService
user = User.query.get(get_jwt_identity())
app = Application.query.get(app_id)
if not app:
return None, (jsonify({'error': 'Application not found'}), 404)
if not ResourceGrantService.can_access_app(user, app):
return None, (jsonify({'error': 'Access denied'}), 403)
return app, None
@deployments_bp.route('/apps/<int:app_id>/history', methods=['GET'])
@jwt_required()
def app_history(app_id):
"""Federated deploy history for an app: versioned build Deployments +
webhook GitDeployments, each tagged with ``source``, newest first.
Query: ?source=build|webhook|all (default all), ?limit= (<=500)."""
app, err = _access(app_id)
if err:
return err
source = (request.args.get('source') or 'all').lower()
try:
limit = min(int(request.args.get('limit', 100)), 500)
except (TypeError, ValueError):
limit = 100
items = []
if source in ('all', 'build'):
for d in (Deployment.query.filter_by(app_id=app_id)
.order_by(Deployment.created_at.desc()).limit(limit).all()):
row = d.to_dict()
row['source'] = 'build'
items.append(row)
if source in ('all', 'webhook'):
for g in (GitDeployment.query.filter_by(app_id=app_id)
.order_by(GitDeployment.created_at.desc()).limit(limit).all()):
row = g.to_dict()
row['source'] = 'webhook'
items.append(row)
items.sort(key=lambda r: r.get('created_at') or '', reverse=True)
items = items[:limit]
return jsonify({'deployments': items, 'count': len(items)}), 200
@deployments_bp.route('/<int:deployment_id>', methods=['GET'])
@jwt_required()
def get_deployment(deployment_id):
"""A versioned build Deployment by id (the release ledger detail)."""
d = Deployment.query.get(deployment_id)
if not d:
return jsonify({'error': 'Deployment not found'}), 404
_, err = _access(d.app_id)
if err:
return err
return jsonify({'deployment': d.to_dict(include_logs=True)}), 200