feat: add /release-deleted endpoint for Gitea release deletion handler
When a release is deleted in Gitea (e.g., to retire an extension), the platform DB still showed the extension as installable. This endpoint removes the version row, recomputes latest_version, and deactivates the extension if no versions remain — making it disappear from the install catalog without touching the repo. Called by n8n workflow NwrRbHNAXFVOLHCQ on Gitea release.deleted event. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ Builds extensions from Gitea (no local files needed).
|
||||
|
||||
Pipeline endpoints (called by n8n):
|
||||
POST /publish — Download from Gitea, build ZIP, publish to Composer Registry + Release
|
||||
POST /release-deleted — Remove a deleted release from DB; deactivate extension if no versions remain
|
||||
POST /deploy — Deploy extension to a specific customer via EspoCRM API
|
||||
POST /deploy-all — Deploy to all licensed customers
|
||||
POST /sync — Sync installed extensions from EspoCRM servers with platform DB
|
||||
@@ -752,6 +753,8 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
path = self.path.split('?')[0]
|
||||
if path == '/publish':
|
||||
self._handle_publish(body)
|
||||
elif path == '/release-deleted':
|
||||
self._handle_release_deleted(body)
|
||||
elif path == '/deploy':
|
||||
self._handle_deploy(body)
|
||||
elif path == '/deploy-all':
|
||||
@@ -900,6 +903,39 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
result = build_and_publish(module, version)
|
||||
self._respond(200, result)
|
||||
|
||||
def _handle_release_deleted(self, body):
|
||||
"""Triggered by Gitea release-deleted webhook (via n8n).
|
||||
Removes the version row, recomputes latest_version, and deactivates the extension if no versions remain.
|
||||
Body: { module: str, version: str (optional — if omitted, only recomputes/deactivates) }"""
|
||||
module = body.get('module', '')
|
||||
version = body.get('version', '')
|
||||
if not module:
|
||||
self._respond(400, {'error': 'module required'}); return
|
||||
ext = query("SELECT id FROM extensions WHERE name = %s", (module,), fetch='one')
|
||||
if not ext:
|
||||
self._respond(404, {'error': f'Extension {module} not found'}); return
|
||||
ext_id = ext['id']
|
||||
if version:
|
||||
query("DELETE FROM extension_versions WHERE extension_id = %s AND version = %s",
|
||||
(ext_id, version), fetch=None)
|
||||
remaining = query("SELECT COUNT(*) AS cnt FROM extension_versions WHERE extension_id = %s",
|
||||
(ext_id,), fetch='one')['cnt']
|
||||
if remaining == 0:
|
||||
query("UPDATE extensions SET is_active = false, latest_version = NULL, updated_at = NOW() WHERE id = %s",
|
||||
(ext_id,), fetch=None)
|
||||
self._respond(200, {'status': 'ok', 'module': module, 'version': version,
|
||||
'action': 'deactivated', 'remaining_versions': 0})
|
||||
return
|
||||
query("""UPDATE extensions e SET latest_version = sub.max_ver, updated_at = NOW()
|
||||
FROM (SELECT extension_id, version as max_ver FROM extension_versions ev1
|
||||
WHERE ev1.extension_id = %s
|
||||
AND NOT EXISTS (SELECT 1 FROM extension_versions ev2
|
||||
WHERE ev2.extension_id = ev1.extension_id
|
||||
AND string_to_array(ev2.version, '.')::int[] > string_to_array(ev1.version, '.')::int[])
|
||||
) sub WHERE e.id = sub.extension_id""", (ext_id,), fetch=None)
|
||||
self._respond(200, {'status': 'ok', 'module': module, 'version': version,
|
||||
'action': 'version_removed', 'remaining_versions': remaining})
|
||||
|
||||
def _handle_deploy(self, body):
|
||||
cid, module, version = body.get('customer_id'), body.get('module',''), body.get('version','')
|
||||
if not all([cid, module, version]):
|
||||
|
||||
Reference in New Issue
Block a user