feat: bake bicep catalog into image; fix dict-based modules parsing
Some checks failed
Build and Deploy iLSP / build-and-deploy (push) Has been cancelled
Build and Deploy iLSP / test (push) Successful in 18s

- Remove all DevOpsMCP/aiohttp runtime deps from BicepModuleCatalog
- BicepModuleCatalog.load() reads bicep_modules_catalog.json from disk at startup (sync)
- Fix _load_catalog: catalog uses dict {path: {versions, schema}} not a list
- server.py: call BicepModuleCatalog.load() synchronously, not via asyncio.gather
- Dockerfile: COPY bicep_modules_catalog.json into both builder + runtime stages
- Health endpoint now reports bicep_modules: 27

Verified locally: make run-quick → health returns pypi_packages:40 bicep_modules:27
This commit is contained in:
Henrik Jess Nielsen
2026-05-10 13:40:48 +02:00
parent 44da791f5a
commit 6b38cbd70c
4 changed files with 12658 additions and 95 deletions

View File

@@ -11,11 +11,14 @@ import asyncio
import logging
import os
import signal
import subprocess
import sys
import threading
from aiohttp import web
from .python_lsp.catalog import PypiCatalog
from .bicep_lsp.modules import BicepModuleCatalog
from .bicep_lsp.proxy import serve_bicep
logger = logging.getLogger(__name__)
@@ -25,58 +28,47 @@ BICEP_LSP_PORT = int(os.getenv("BICEP_LSP_PORT", "2088"))
HEALTH_PORT = int(os.getenv("HEALTH_PORT", "2089"))
def _serve_python_lsp(port: int) -> None:
"""Start pylsp in TCP server mode; restart on unexpected exit (blocking)."""
while True:
proc = subprocess.Popen(
[sys.executable, "-m", "pylsp", "--tcp", "--host", "0.0.0.0", "--port", str(port)],
)
logger.info("Python LSP (pylsp) listening on TCP :%d PID=%d", port, proc.pid)
proc.wait()
logger.warning("pylsp exited (code %s) — restarting", proc.returncode)
async def _health_app() -> web.Application:
app = web.Application()
async def health(_: web.Request) -> web.Response:
pypi_count = len(PypiCatalog._packages)
from .bicep_lsp.modules import BicepModuleCatalog
bicep_count = len(BicepModuleCatalog._modules)
return web.json_response({
"status": "ok",
"pypi_packages": pypi_count,
"bicep_modules": bicep_count,
"pypi_packages": len(PypiCatalog._packages),
"bicep_modules": len(BicepModuleCatalog._modules),
})
app.router.add_get("/health", health)
return app
async def _serve_python_lsp(port: int) -> None:
"""Start pylsp in TCP server mode."""
import subprocess, sys
proc = await asyncio.create_subprocess_exec(
sys.executable, "-m", "pylsp",
"--tcp", "--host", "0.0.0.0", "--port", str(port),
)
logger.info("Python LSP (pylsp) listening on TCP :%d PID=%d", port, proc.pid)
await proc.wait()
logger.warning("pylsp exited — restarting")
async def main_async() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logging.getLogger("ilsp.bicep_lsp.proxy").setLevel(logging.DEBUG)
# Pre-warm caches
# Pre-warm catalogs before accepting connections
logger.info("Pre-warming catalogs…")
from .bicep_lsp.modules import BicepModuleCatalog
await asyncio.gather(
PypiCatalog.start_background_refresh(),
BicepModuleCatalog.start_background_refresh(),
)
BicepModuleCatalog.load()
await PypiCatalog.start_background_refresh()
# Start Bicep LSP proxy in a daemon thread (blocking socket server)
threading.Thread(
target=serve_bicep,
args=(BICEP_LSP_PORT,),
daemon=True,
).start()
# Both LSP servers run in daemon threads (blocking socket/process loops)
threading.Thread(target=_serve_python_lsp, args=(PYTHON_LSP_PORT,), daemon=True).start()
threading.Thread(target=serve_bicep, args=(BICEP_LSP_PORT,), daemon=True).start()
# Build health app
# Health HTTP server — keeps the process alive via the asyncio event loop
health_app = await _health_app()
runner = web.AppRunner(health_app)
await runner.setup()
@@ -84,8 +76,8 @@ async def main_async() -> None:
await site.start()
logger.info("Health endpoint on http://0.0.0.0:%d/health", HEALTH_PORT)
# Run remaining asyncio services
await _serve_python_lsp(PYTHON_LSP_PORT)
# Wait indefinitely; signal handlers in main() will stop the loop
await asyncio.Event().wait()
def main() -> None: