Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9ab211c38 | |||
| 287c4b5372 | |||
| ba29572e93 |
@@ -1,241 +0,0 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: git.tielogic.xyz
|
||||
IMAGE_PREFIX: git.tielogic.xyz/adriano/cerbero-mcp
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: ruff lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install deps
|
||||
run: uv sync --frozen --group dev
|
||||
- name: Ruff check
|
||||
run: uv run ruff check services/
|
||||
|
||||
typecheck:
|
||||
name: mypy mcp_common
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install deps
|
||||
run: uv sync --frozen --group dev
|
||||
- name: Mypy on mcp_common (gating)
|
||||
run: uv run mypy services/common/src/mcp_common
|
||||
- name: Mypy on services (warn-only)
|
||||
run: uv run mypy services/ || true
|
||||
|
||||
test:
|
||||
name: pytest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install deps
|
||||
run: uv sync --frozen --group dev
|
||||
- name: Pytest full suite
|
||||
run: uv run pytest services/ --tb=short
|
||||
|
||||
validate-config:
|
||||
name: validate compose + Caddyfile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate dev compose
|
||||
run: docker compose -f docker-compose.yml config -q
|
||||
|
||||
- name: Validate prod compose
|
||||
run: docker compose -f docker-compose.prod.yml config -q
|
||||
env:
|
||||
ACME_EMAIL: test@example.com
|
||||
WRITE_ALLOWLIST: "127.0.0.1/32"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build gateway image (local, no push)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./gateway
|
||||
file: gateway/Dockerfile
|
||||
tags: cerbero-gateway:validate
|
||||
load: true
|
||||
|
||||
- name: Validate Caddyfile syntax
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "$PWD/gateway/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||
-e ACME_EMAIL=test@example.com \
|
||||
-e WRITE_ALLOWLIST="127.0.0.1/32" \
|
||||
cerbero-gateway:validate \
|
||||
caddy validate --config /etc/caddy/Caddyfile
|
||||
|
||||
build-and-push:
|
||||
name: build & push to registry
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test, validate-config]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Compute short SHA
|
||||
id: meta
|
||||
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build & push base image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/base.Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/base:latest
|
||||
${{ env.IMAGE_PREFIX }}/base:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:base
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:base,mode=max
|
||||
|
||||
- name: Build & push gateway
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./gateway
|
||||
file: gateway/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/gateway:latest
|
||||
${{ env.IMAGE_PREFIX }}/gateway:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:gateway
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:gateway,mode=max
|
||||
|
||||
- name: Build & push mcp-deribit
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/mcp-deribit.Dockerfile
|
||||
build-args: |
|
||||
BASE_IMAGE=git.tielogic.xyz/adriano/cerbero-mcp/base
|
||||
BASE_TAG=latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/mcp-deribit:latest
|
||||
${{ env.IMAGE_PREFIX }}/mcp-deribit:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-deribit
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-deribit,mode=max
|
||||
|
||||
- name: Build & push mcp-bybit
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/mcp-bybit.Dockerfile
|
||||
build-args: |
|
||||
BASE_IMAGE=git.tielogic.xyz/adriano/cerbero-mcp/base
|
||||
BASE_TAG=latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/mcp-bybit:latest
|
||||
${{ env.IMAGE_PREFIX }}/mcp-bybit:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-bybit
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-bybit,mode=max
|
||||
|
||||
- name: Build & push mcp-hyperliquid
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/mcp-hyperliquid.Dockerfile
|
||||
build-args: |
|
||||
BASE_IMAGE=git.tielogic.xyz/adriano/cerbero-mcp/base
|
||||
BASE_TAG=latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/mcp-hyperliquid:latest
|
||||
${{ env.IMAGE_PREFIX }}/mcp-hyperliquid:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-hyperliquid
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-hyperliquid,mode=max
|
||||
|
||||
- name: Build & push mcp-alpaca
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/mcp-alpaca.Dockerfile
|
||||
build-args: |
|
||||
BASE_IMAGE=git.tielogic.xyz/adriano/cerbero-mcp/base
|
||||
BASE_TAG=latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/mcp-alpaca:latest
|
||||
${{ env.IMAGE_PREFIX }}/mcp-alpaca:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-alpaca
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-alpaca,mode=max
|
||||
|
||||
- name: Build & push mcp-macro
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/mcp-macro.Dockerfile
|
||||
build-args: |
|
||||
BASE_IMAGE=git.tielogic.xyz/adriano/cerbero-mcp/base
|
||||
BASE_TAG=latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/mcp-macro:latest
|
||||
${{ env.IMAGE_PREFIX }}/mcp-macro:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-macro
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-macro,mode=max
|
||||
|
||||
- name: Build & push mcp-sentiment
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/mcp-sentiment.Dockerfile
|
||||
build-args: |
|
||||
BASE_IMAGE=git.tielogic.xyz/adriano/cerbero-mcp/base
|
||||
BASE_TAG=latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/mcp-sentiment:latest
|
||||
${{ env.IMAGE_PREFIX }}/mcp-sentiment:sha-${{ steps.meta.outputs.sha }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-sentiment
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/buildcache:mcp-sentiment,mode=max
|
||||
+108
-78
@@ -1,66 +1,72 @@
|
||||
# Deployment Cerbero_mcp
|
||||
|
||||
Guida operativa per il deploy della suite MCP su un VPS pubblico.
|
||||
L'architettura è: Gitea ospita codice + container registry; il VPS produzione
|
||||
non builda nulla, ma fa pull dei container già pronti dalla registry e usa
|
||||
Watchtower per il rollover automatico delle versioni.
|
||||
L'architettura è: Gitea ospita codice + container registry; le immagini
|
||||
vengono buildate e pushate dalla **macchina di sviluppo** (laptop) verso
|
||||
il registry; il VPS produzione non builda nulla, fa solo pull dei
|
||||
container già pronti e usa Watchtower per il rollover automatico.
|
||||
|
||||
```
|
||||
┌─────────────────────────┐ ┌──────────────────────────────────┐
|
||||
│ Gitea git.tielogic.xyz │ │ VPS produzione │
|
||||
│ │ │ cerbero-mcp.tielogic.xyz │
|
||||
│ ┌──────────────────┐ │ push │ │
|
||||
│ │ Cerbero-mcp repo │───┼─CI/CD──▶│ ┌────────────────────────────┐ │
|
||||
│ └──────────────────┘ │ image │ │ docker compose │ │
|
||||
│ ┌──────────────────┐ │ │ │ (docker-compose.prod.yml) │ │
|
||||
│ │ Container reg. │◀──┼─ pull ──┤ │ gateway, mcp-* │ │
|
||||
│ └──────────────────┘ │ │ │ watchtower (poll 5min) │ │
|
||||
│ ┌──────────────────┐ │ │ └────────────────────────────┘ │
|
||||
│ │ Actions runner │ │ │ │
|
||||
│ └──────────────────┘ │ │ │
|
||||
└─────────────────────────┘ └──────────────────────────────────┘
|
||||
┌──────────────────────────┐ ┌─────────────────────────┐ ┌──────────────────────────────────┐
|
||||
│ Laptop dev │ │ Gitea git.tielogic.xyz │ │ VPS produzione │
|
||||
│ │ │ │ │ cerbero-mcp.tielogic.xyz │
|
||||
│ build-push.sh ──push──▶ │───▶│ ┌────────────────────┐ │ │ │
|
||||
│ (8 image) │ │ │ Container registry │ │ │ ┌────────────────────────────┐ │
|
||||
│ git push ─────────────▶ │───▶│ └────────────────────┘ │◀──┼──┤ docker compose │ │
|
||||
│ │ │ ┌────────────────────┐ │ pull │ (docker-compose.prod.yml) │ │
|
||||
│ │ │ │ Cerbero-mcp repo │ │ │ │ gateway, mcp-* │ │
|
||||
│ │ │ └────────────────────┘ │ │ │ watchtower (poll 5min) │ │
|
||||
│ │ │ │ │ └────────────────────────────┘ │
|
||||
└──────────────────────────┘ └─────────────────────────┘ └──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 1. Pipeline CI/CD (Gitea Actions)
|
||||
Niente CI/CD su Gitea — qualità e build sono responsabilità del laptop
|
||||
prima del push (lint/test in locale, poi `scripts/build-push.sh`).
|
||||
|
||||
`.gitea/workflows/ci.yml` ad ogni push su `main` esegue 5 job:
|
||||
## 1. Build & push image (dal laptop)
|
||||
|
||||
1. **lint** (`ruff check services/`) — gating, 0 violations.
|
||||
2. **typecheck** (`mypy services/common/src/mcp_common`) — gating sul
|
||||
modulo comune, warn-only sui servizi.
|
||||
3. **test** (`pytest services/`) — gating, 478 test verdi.
|
||||
4. **validate-config** — `docker compose config -q` su `docker-compose.yml`
|
||||
e `docker-compose.prod.yml` + `caddy validate --config Caddyfile`.
|
||||
5. **build-and-push** — solo su push a `main` (skip su PR):
|
||||
- `docker login git.tielogic.xyz` con `${{ secrets.REGISTRY_TOKEN }}`
|
||||
(PAT scope `write:package`, configurato user-level su Gitea).
|
||||
- Builda e pusha **8 image** al registry con tag `:latest` + `:sha-X`:
|
||||
- `git.tielogic.xyz/adriano/cerbero-mcp/base`
|
||||
- `git.tielogic.xyz/adriano/cerbero-mcp/gateway`
|
||||
- `git.tielogic.xyz/adriano/cerbero-mcp/mcp-{deribit,bybit,hyperliquid,alpaca,macro,sentiment}`
|
||||
- Cache Docker buildx via registry stesso (`buildcache:<name>` per ognuna)
|
||||
→ run successivi 5-10× più veloci.
|
||||
Lo script `scripts/build-push.sh` builda e pusha le 8 image al registry
|
||||
Gitea, replicando il vecchio job CI ma in locale. Pre-requisiti:
|
||||
|
||||
Le PR fanno girare solo `lint`+`typecheck`+`test`+`validate-config`,
|
||||
niente build/push (gating per merge).
|
||||
- `docker` + `buildx` sul laptop.
|
||||
- Personal Access Token Gitea con scope `write:package` (User Settings
|
||||
→ Applications → Generate Token).
|
||||
|
||||
### Setup runner Gitea
|
||||
```bash
|
||||
export GITEA_PAT='<PAT_write:package>'
|
||||
export GITEA_USER=adriano
|
||||
|
||||
Il runner act_runner deve girare:
|
||||
- Sulla stessa Docker network di Gitea (`gitea_gitea-internal` o equivalente)
|
||||
altrimenti `actions/checkout@v4` non risolve `gitea:3000` per il clone.
|
||||
- Con bind di `/var/run/docker.sock` per buildare image dai workflow.
|
||||
- Image dei job mappata a `docker.gitea.com/runner-images:ubuntu-22.04`
|
||||
(default Gitea, contiene docker CLI + buildx + git + curl + node).
|
||||
- Label: almeno `ubuntu-latest` (usato dal workflow). Altre label
|
||||
(`tielogic-ci`, `ubuntu-22.04`, ecc.) opzionali.
|
||||
# Tutte le 8 image (base + gateway + 6 mcp-*)
|
||||
./scripts/build-push.sh
|
||||
|
||||
### Setup secret REGISTRY_TOKEN
|
||||
# Solo specifiche (es. dopo modifica a un singolo servizio)
|
||||
./scripts/build-push.sh base mcp-bybit
|
||||
```
|
||||
|
||||
Crea un Personal Access Token Gitea con scope `write:package` (User
|
||||
Settings → Applications → Generate Token). Aggiungilo come secret a
|
||||
livello user (User Settings → Secrets → New Secret) con nome
|
||||
`REGISTRY_TOKEN`. Tutti i tuoi repo ereditano il secret automaticamente.
|
||||
Lo script:
|
||||
- Fa `docker login git.tielogic.xyz`.
|
||||
- Builda con `docker buildx build --push` (cache buildx locale del
|
||||
laptop, niente cache registry: build successivi rapidi senza pesare
|
||||
sul registry).
|
||||
- Tagga `:latest` + `:sha-<short_HEAD>`.
|
||||
- Per le mcp-* passa `BASE_IMAGE`/`BASE_TAG` come build-arg in modo da
|
||||
ereditare dall'image `base` appena pushata.
|
||||
|
||||
Ordine consigliato: builda `base` prima delle `mcp-*` (lo script lo fa
|
||||
di default se chiamato senza argomenti).
|
||||
|
||||
## 1b. Quality gate locale (consigliato prima del push)
|
||||
|
||||
Prima di `build-push.sh` esegui in locale i check che prima girava il CI:
|
||||
|
||||
```bash
|
||||
uv run ruff check services/
|
||||
uv run mypy services/common/src/mcp_common
|
||||
uv run pytest services/ --tb=short
|
||||
docker compose -f docker-compose.prod.yml config -q
|
||||
```
|
||||
|
||||
Tutti devono essere verdi prima di pushare image al registry.
|
||||
|
||||
## 2a. Topologia: standalone vs behind-Traefik
|
||||
|
||||
@@ -89,34 +95,47 @@ Caddy non bind su host, ascolta plain HTTP `:80` interno alla
|
||||
`traefik` network. Traefik fa routing per `Host(cerbero-mcp.tielogic.xyz)`,
|
||||
TLS, ACME. Adatto a VPS condiviso con altri servizi (Gitea, ecc.).
|
||||
|
||||
## 2. Deploy automatizzato (script)
|
||||
## 2. Deploy automatizzato (script no-clone)
|
||||
|
||||
Il modo più rapido è `scripts/deploy.sh`, idempotente. Esegui sul VPS:
|
||||
Il modo più rapido è `scripts/deploy-noclone.sh`, idempotente. Sul VPS
|
||||
**non** viene clonato il repo: lo script scarica via raw HTTP solo i
|
||||
file strettamente necessari al runtime (compose, Caddyfile, public
|
||||
assets). Esegui sul VPS:
|
||||
|
||||
```bash
|
||||
# Prerequisiti
|
||||
export GITEA_PAT="<PAT con scope read:package>"
|
||||
export GITEA_USER=adriano
|
||||
mkdir -p ~/cerbero-secrets
|
||||
# Copia (via scp dal posto sicuro) i secret in ~/cerbero-secrets/:
|
||||
# deribit.json bybit.json hyperliquid.json alpaca.json
|
||||
# macro.json sentiment.json core.token observer.token
|
||||
|
||||
# Clone temporaneo solo per lo script
|
||||
curl -sL -o /tmp/deploy.sh \
|
||||
https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main/scripts/deploy.sh
|
||||
chmod +x /tmp/deploy.sh
|
||||
# Crea la dir di deploy e mettici i secrets via scp dal posto sicuro
|
||||
sudo mkdir -p /docker/cerbero_mcp/secrets
|
||||
sudo chown -R "$USER" /docker/cerbero_mcp
|
||||
# scp deribit.json bybit.json hyperliquid.json alpaca.json \
|
||||
# macro.json sentiment.json core.token observer.token \
|
||||
# vps:/docker/cerbero_mcp/secrets/
|
||||
|
||||
# Run (richiede sudo per /opt/cerbero-mcp e /var/log/cerbero-mcp)
|
||||
/tmp/deploy.sh
|
||||
# Behind Traefik (opzionale, solo se VPS condiviso con Gitea o altri)
|
||||
# export BEHIND_TRAEFIK=true
|
||||
# export TRAEFIK_NETWORK=gitea_traefik-public
|
||||
|
||||
curl -sL -o /tmp/deploy-noclone.sh \
|
||||
https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main/scripts/deploy-noclone.sh
|
||||
chmod +x /tmp/deploy-noclone.sh
|
||||
/tmp/deploy-noclone.sh
|
||||
```
|
||||
|
||||
Lo script esegue: docker login registry → clone/pull repo in
|
||||
`/opt/cerbero-mcp` → copia secrets con `chmod 600` → genera `.env`
|
||||
iniziale (testnet) → crea `/var/log/cerbero-mcp` con permessi `1000:1000`
|
||||
→ pull image dal registry → `docker compose up -d` → smoke test pubblico.
|
||||
Lo script esegue: docker login registry → scarica `docker-compose.prod.yml`,
|
||||
`docker-compose.traefik.yml`, `gateway/Caddyfile`, `gateway/public/*` in
|
||||
`/docker/cerbero_mcp/` → chmod 600 sui secrets → genera `.env` iniziale
|
||||
(testnet) → crea `/var/log/cerbero-mcp` con permessi `1000:1000` → pull
|
||||
image dal registry → `docker compose up -d` → smoke test pubblico.
|
||||
|
||||
Per aggiornare in seguito: ri-esegui lo stesso script (preserva `.env`).
|
||||
Per aggiornare in seguito: ri-esegui lo stesso script (preserva `.env`
|
||||
e secrets, ricarica config dal branch `main` aggiornato).
|
||||
|
||||
**Override paths**: `DEPLOY_DIR` (default `/docker/cerbero_mcp`),
|
||||
`SECRETS_SRC` (default `$DEPLOY_DIR/secrets`), `AUDIT_LOG_DIR` (default
|
||||
`/var/log/cerbero-mcp`).
|
||||
|
||||
### Modalità behind-Traefik
|
||||
|
||||
@@ -234,12 +253,23 @@ echo "$GITEA_PAT" | docker login git.tielogic.xyz -u <gitea-username> --password
|
||||
Le credenziali vengono salvate in `~/.docker/config.json`. Watchtower lo
|
||||
bind-monta in sola lettura per fare i pull autenticati.
|
||||
|
||||
### b) Clona repository (solo per i file di compose, secret e Caddyfile)
|
||||
### b) Crea dir di deploy e scarica i file di config
|
||||
|
||||
Sul VPS NON serve clonare il repo. Bastano i file di compose, il
|
||||
`Caddyfile` e i public assets del gateway:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/cerbero-mcp && sudo chown $USER /opt/cerbero-mcp
|
||||
cd /opt/cerbero-mcp
|
||||
git clone ssh://git@git.tielogic.xyz:222/Adriano/Cerbero-mcp.git .
|
||||
sudo mkdir -p /docker/cerbero_mcp/{secrets,gateway/public}
|
||||
sudo chown -R "$USER" /docker/cerbero_mcp
|
||||
cd /docker/cerbero_mcp
|
||||
|
||||
BASE=https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main
|
||||
curl -fsSL -o docker-compose.prod.yml $BASE/docker-compose.prod.yml
|
||||
curl -fsSL -o docker-compose.traefik.yml $BASE/docker-compose.traefik.yml
|
||||
curl -fsSL -o gateway/Caddyfile $BASE/gateway/Caddyfile
|
||||
curl -fsSL -o gateway/public/index.html $BASE/gateway/public/index.html
|
||||
curl -fsSL -o gateway/public/status.js $BASE/gateway/public/status.js
|
||||
curl -fsSL -o gateway/public/style.css $BASE/gateway/public/style.css
|
||||
```
|
||||
|
||||
Il VPS NON ha bisogno di buildare; usa `docker-compose.prod.yml` che fa solo
|
||||
@@ -258,7 +288,7 @@ chmod 600 secrets/*
|
||||
|
||||
### d) `.env` con configurazione runtime
|
||||
|
||||
Crea `/opt/cerbero-mcp/.env`:
|
||||
Crea `/docker/cerbero_mcp/.env`:
|
||||
|
||||
```bash
|
||||
# Gateway
|
||||
@@ -390,14 +420,14 @@ Applicalo come middleware al router Gitea.
|
||||
|
||||
## 11. Aggiornamento del compose stesso (file YAML)
|
||||
|
||||
Watchtower aggiorna le **image**, non il `docker-compose.prod.yml`. Se cambi
|
||||
struttura (nuovi servizi, nuove env var) devi:
|
||||
Watchtower aggiorna le **image**, non `docker-compose.prod.yml` né
|
||||
`Caddyfile`. Se cambi struttura (nuovi servizi, nuove env var, modifiche
|
||||
al gateway), ri-esegui sul VPS lo script no-clone, che ri-scarica i file
|
||||
di config dal branch `main` di Gitea e applica:
|
||||
|
||||
```bash
|
||||
cd /opt/cerbero-mcp
|
||||
git pull
|
||||
docker compose -f docker-compose.prod.yml --env-file .env up -d
|
||||
/tmp/deploy-noclone.sh
|
||||
```
|
||||
|
||||
Per automatizzare anche questo serve un cron job o uno step CD push-based
|
||||
(vedi backlog).
|
||||
Lo script è idempotente: preserva `.env` e `secrets/`, aggiorna solo i
|
||||
file di config + fa `pull` + `up -d`.
|
||||
|
||||
@@ -53,27 +53,29 @@ OI history. **Nuovi**: `get_funding_arb_spread` (opportunità arb compatte),
|
||||
`get_liquidation_heatmap` (heuristic da OI delta + funding extreme),
|
||||
`get_cointegration_pairs` (Engle-Granger su coppie crypto).
|
||||
|
||||
## CI/CD pipeline
|
||||
## Build & deploy pipeline
|
||||
|
||||
Pipeline Gitea Actions in `.gitea/workflows/ci.yml` esegue ad ogni push
|
||||
su `main`:
|
||||
Niente CI/CD su Gitea: la build delle 8 image è responsabilità della
|
||||
macchina di sviluppo, fatta da `scripts/build-push.sh`. Il flusso è:
|
||||
|
||||
1. **lint** — `ruff check services/`
|
||||
2. **typecheck** — `mypy mcp_common` (gating) + servizi (warn-only)
|
||||
3. **test** — `pytest services/` (478 test)
|
||||
4. **validate-config** — sintassi compose + Caddyfile
|
||||
5. **build-and-push** — solo su `main`: builda e pusha 8 image al
|
||||
container registry Gitea (`git.tielogic.xyz/adriano/cerbero-mcp/*`)
|
||||
con tag `:latest` + `:sha-XXXXXXX` per rollback puntuali.
|
||||
1. **Quality gate locale** (sul laptop, prima di pushare):
|
||||
- `uv run ruff check services/`
|
||||
- `uv run mypy services/common/src/mcp_common`
|
||||
- `uv run pytest services/`
|
||||
- `docker compose -f docker-compose.prod.yml config -q`
|
||||
2. **Build & push** (sul laptop):
|
||||
```bash
|
||||
export GITEA_PAT='<PAT_write:package>'
|
||||
./scripts/build-push.sh # tutte le 8 image
|
||||
./scripts/build-push.sh base mcp-bybit # solo specifiche
|
||||
```
|
||||
Tagga `:latest` + `:sha-<short_HEAD>` per rollback puntuali. Cache
|
||||
buildx via registry stesso (run successivi 5-10× più veloci).
|
||||
3. **Auto-rollover su VPS**: Watchtower polla il registry ogni 5 min e
|
||||
aggiorna i container quando il digest del tag `:latest` cambia.
|
||||
|
||||
Le PR fanno girare solo i 4 job di gating (lint/test/validate). Il job
|
||||
`build-and-push` è skippato.
|
||||
|
||||
Sul VPS produzione, **Watchtower** polla il registry ogni 5 min e
|
||||
auto-aggiorna i container quando il digest del tag `:latest` cambia.
|
||||
|
||||
Vedi [`DEPLOYMENT.md`](DEPLOYMENT.md) per setup runner, secret
|
||||
`REGISTRY_TOKEN`, deploy VPS, smoke test, rollback.
|
||||
Vedi [`DEPLOYMENT.md`](DEPLOYMENT.md) per build & push, deploy VPS
|
||||
no-clone (`scripts/deploy-noclone.sh`), smoke test, rollback.
|
||||
|
||||
## Avvio locale (dev)
|
||||
|
||||
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cerbero_mcp — build & push image al registry Gitea da macchina locale.
|
||||
#
|
||||
# Sostituisce il job CI `build-and-push` di .gitea/workflows/ci.yml.
|
||||
# Usalo dopo `git push` (o senza, se vuoi pushare un build "dirty").
|
||||
# Watchtower sul VPS pulla automaticamente entro WATCHTOWER_POLL_INTERVAL.
|
||||
#
|
||||
# Pre-requisiti:
|
||||
# - docker + buildx
|
||||
# - PAT Gitea con scope `write:package` in env $GITEA_PAT
|
||||
# - $GITEA_USER (default: adriano)
|
||||
#
|
||||
# Uso:
|
||||
# ./scripts/build-push.sh # tutte le image
|
||||
# ./scripts/build-push.sh base gateway # solo specifiche
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${REGISTRY:-git.tielogic.xyz}"
|
||||
IMAGE_PREFIX="${IMAGE_PREFIX:-$REGISTRY/adriano/cerbero-mcp}"
|
||||
GITEA_USER="${GITEA_USER:-adriano}"
|
||||
SHA="$(git rev-parse --short HEAD)"
|
||||
|
||||
# Ordine di build: base prima (parent delle mcp-*), poi le altre.
|
||||
ALL_TARGETS=(base gateway mcp-deribit mcp-bybit mcp-hyperliquid mcp-alpaca mcp-macro mcp-sentiment)
|
||||
TARGETS=("${@:-${ALL_TARGETS[@]}}")
|
||||
|
||||
command -v docker >/dev/null || { echo "FATAL: docker non installato"; exit 1; }
|
||||
docker buildx version >/dev/null || { echo "FATAL: docker buildx non disponibile"; exit 1; }
|
||||
|
||||
# Login solo se non già autenticato sul registry. Per primo login fai:
|
||||
# echo "<PAT>" | docker login $REGISTRY -u $GITEA_USER --password-stdin
|
||||
if grep -q "\"$REGISTRY\"" ~/.docker/config.json 2>/dev/null; then
|
||||
echo "=== docker già loggato su $REGISTRY (skip login) ==="
|
||||
elif [ -n "${GITEA_PAT:-}" ]; then
|
||||
echo "=== docker login $REGISTRY ==="
|
||||
echo "$GITEA_PAT" | docker login "$REGISTRY" -u "$GITEA_USER" --password-stdin
|
||||
else
|
||||
echo "FATAL: non autenticato su $REGISTRY e GITEA_PAT non settata."
|
||||
echo " Esegui una volta: docker login $REGISTRY -u $GITEA_USER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_one() {
|
||||
local name="$1"
|
||||
local context file
|
||||
case "$name" in
|
||||
base)
|
||||
context="."; file="docker/base.Dockerfile" ;;
|
||||
gateway)
|
||||
context="./gateway"; file="gateway/Dockerfile" ;;
|
||||
mcp-*)
|
||||
context="."; file="docker/${name}.Dockerfile" ;;
|
||||
*)
|
||||
echo "FATAL: target sconosciuto '$name'"; exit 1 ;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "FATAL: Dockerfile non trovato: $file"; exit 1
|
||||
fi
|
||||
|
||||
local tag_latest="$IMAGE_PREFIX/$name:latest"
|
||||
local tag_sha="$IMAGE_PREFIX/$name:sha-$SHA"
|
||||
|
||||
echo "=== [$name] build & push ==="
|
||||
local args=(buildx build --push
|
||||
-f "$file"
|
||||
-t "$tag_latest"
|
||||
-t "$tag_sha"
|
||||
)
|
||||
if [[ "$name" == mcp-* ]]; then
|
||||
args+=(--build-arg "BASE_IMAGE=$IMAGE_PREFIX/base"
|
||||
--build-arg "BASE_TAG=latest")
|
||||
fi
|
||||
args+=("$context")
|
||||
|
||||
docker "${args[@]}"
|
||||
echo " pushed: $tag_latest"
|
||||
echo " pushed: $tag_sha"
|
||||
}
|
||||
|
||||
for t in "${TARGETS[@]}"; do
|
||||
build_one "$t"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=== Tutto pushato (commit $SHA) ==="
|
||||
echo "VPS Watchtower farà pull entro WATCHTOWER_POLL_INTERVAL (default 5min)."
|
||||
echo "Per forzare subito:"
|
||||
echo " ssh <vps> 'cd /docker/cerbero_mcp && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d'"
|
||||
@@ -1,34 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cerbero_mcp — deploy script per VPS produzione.
|
||||
#
|
||||
# Sul VPS NON viene clonato il repo: lo script scarica solo i file
|
||||
# strettamente necessari al runtime (compose, Caddyfile, public assets)
|
||||
# via raw HTTP da Gitea. Le image vengono pullate pre-built dal registry
|
||||
# Gitea (buildate dal laptop dev con scripts/build-push.sh).
|
||||
#
|
||||
# Pre-requisiti sul VPS (NON gestiti da questo script):
|
||||
# 1. Docker Engine ≥ 24 + plugin docker compose installati.
|
||||
# 2. DNS A record `cerbero-mcp.tielogic.xyz` → IP del VPS.
|
||||
# 2. DNS A record `cerbero-mcp.tielogic.xyz` → IP del VPS (warn-only).
|
||||
# 3. Porte 80 e 443 aperte sul firewall (per ACME + traffico HTTPS).
|
||||
# 4. PAT Gitea con scope `read:package`, salvato in env `$GITEA_PAT`.
|
||||
# 5. Username Gitea in env `$GITEA_USER` (default: adriano).
|
||||
# 6. Secret JSON exchange + token bearer disponibili in $SECRETS_SRC
|
||||
# (default: ~/cerbero-secrets/), che lo script copierà in
|
||||
# $DEPLOY_DIR/secrets/ con permessi 600.
|
||||
# (default: $DEPLOY_DIR/secrets/), che lo script copierà in
|
||||
# $DEPLOY_DIR/secrets/ con permessi 600 (ignorato se SECRETS_SRC == DEPLOY_DIR/secrets).
|
||||
#
|
||||
# Idempotente: rieseguibile per aggiornamenti.
|
||||
# Idempotente: rieseguibile per aggiornamenti (riscarica i file di config
|
||||
# dal branch corrente, NON tocca .env esistente).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-/opt/cerbero-mcp}"
|
||||
SECRETS_SRC="${SECRETS_SRC:-$HOME/cerbero-secrets}"
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-/docker/cerbero_mcp}"
|
||||
SECRETS_SRC="${SECRETS_SRC:-$DEPLOY_DIR/secrets}"
|
||||
GITEA_USER="${GITEA_USER:-adriano}"
|
||||
GITEA_REPO_URL="${GITEA_REPO_URL:-ssh://git@git.tielogic.xyz:222/Adriano/Cerbero-mcp.git}"
|
||||
GITEA_RAW_BASE="${GITEA_RAW_BASE:-https://git.tielogic.xyz/Adriano/Cerbero-mcp/raw/branch/main}"
|
||||
REGISTRY="${REGISTRY:-git.tielogic.xyz}"
|
||||
DOMAIN="${DOMAIN:-cerbero-mcp.tielogic.xyz}"
|
||||
AUDIT_LOG_DIR="${AUDIT_LOG_DIR:-/var/log/cerbero-mcp}"
|
||||
|
||||
echo "=== Cerbero_mcp deploy → $DEPLOY_DIR (domain $DOMAIN) ==="
|
||||
echo "=== Cerbero_mcp deploy (no-clone) → $DEPLOY_DIR (domain $DOMAIN) ==="
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# 1. Verifica pre-requisiti
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
command -v docker >/dev/null || { echo "FATAL: docker non installato"; exit 1; }
|
||||
command -v curl >/dev/null || { echo "FATAL: curl non installato"; exit 1; }
|
||||
docker compose version >/dev/null || { echo "FATAL: docker compose plugin assente"; exit 1; }
|
||||
|
||||
if [ -z "${GITEA_PAT:-}" ]; then
|
||||
@@ -36,13 +43,6 @@ if [ -z "${GITEA_PAT:-}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SECRETS_SRC" ]; then
|
||||
echo "FATAL: secrets src dir $SECRETS_SRC non esiste."
|
||||
echo " Atteso contenere: deribit.json bybit.json hyperliquid.json alpaca.json"
|
||||
echo " macro.json sentiment.json core.token observer.token"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check DNS resolution (warning only, non blocca)
|
||||
ip_resolved=$(getent hosts "$DOMAIN" | awk '{print $1}' | head -1 || true)
|
||||
if [ -z "$ip_resolved" ]; then
|
||||
@@ -58,25 +58,42 @@ echo "=== docker login $REGISTRY ==="
|
||||
echo "$GITEA_PAT" | docker login "$REGISTRY" -u "$GITEA_USER" --password-stdin
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# 3. Setup dir + clone/pull repo
|
||||
# 3. Setup dir + scarica i file di config dal repo (no clone)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
sudo mkdir -p "$DEPLOY_DIR"
|
||||
sudo chown "$USER:$USER" "$DEPLOY_DIR"
|
||||
mkdir -p "$DEPLOY_DIR/secrets" "$DEPLOY_DIR/gateway/public"
|
||||
|
||||
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||
echo "=== Aggiornamento repo $DEPLOY_DIR ==="
|
||||
git -C "$DEPLOY_DIR" pull --ff-only
|
||||
else
|
||||
echo "=== Clone repo $GITEA_REPO_URL → $DEPLOY_DIR ==="
|
||||
git clone "$GITEA_REPO_URL" "$DEPLOY_DIR"
|
||||
fi
|
||||
# File di config necessari al runtime. Scaricati come raw da Gitea.
|
||||
# Idempotente: ricarica sempre la versione di main.
|
||||
download() {
|
||||
local rel="$1"
|
||||
local dst="$DEPLOY_DIR/$rel"
|
||||
echo " fetch: $rel"
|
||||
curl -fsSL -o "$dst" "$GITEA_RAW_BASE/$rel" \
|
||||
|| { echo "FATAL: download $rel fallito"; exit 1; }
|
||||
}
|
||||
|
||||
echo "=== Download config da $GITEA_RAW_BASE ==="
|
||||
download docker-compose.prod.yml
|
||||
download docker-compose.traefik.yml
|
||||
download gateway/Caddyfile
|
||||
download gateway/public/index.html
|
||||
download gateway/public/status.js
|
||||
download gateway/public/style.css
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# 4. Copia secrets con permessi 600
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
mkdir -p secrets
|
||||
if [ "$(realpath "$SECRETS_SRC")" != "$(realpath "$DEPLOY_DIR/secrets")" ]; then
|
||||
if [ ! -d "$SECRETS_SRC" ]; then
|
||||
echo "FATAL: secrets src dir $SECRETS_SRC non esiste."
|
||||
echo " Atteso contenere: deribit.json bybit.json hyperliquid.json alpaca.json"
|
||||
echo " macro.json sentiment.json core.token observer.token"
|
||||
exit 1
|
||||
fi
|
||||
echo "=== Copia secrets da $SECRETS_SRC ==="
|
||||
for f in deribit.json bybit.json hyperliquid.json alpaca.json macro.json sentiment.json core.token observer.token; do
|
||||
if [ -f "$SECRETS_SRC/$f" ]; then
|
||||
@@ -87,6 +104,13 @@ for f in deribit.json bybit.json hyperliquid.json alpaca.json macro.json sentime
|
||||
echo " WARN: $SECRETS_SRC/$f assente — il servizio relativo fallirà al boot."
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "=== Secrets già in $DEPLOY_DIR/secrets — solo chmod 600 ==="
|
||||
for f in deribit.json bybit.json hyperliquid.json alpaca.json macro.json sentiment.json core.token observer.token; do
|
||||
[ -f "secrets/$f" ] && chmod 600 "secrets/$f" && echo " ok: secrets/$f" \
|
||||
|| echo " WARN: secrets/$f assente — il servizio relativo fallirà al boot."
|
||||
done
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# 5. Crea/aggiorna .env (preserva esistente)
|
||||
@@ -129,7 +153,7 @@ fi
|
||||
# 6. Audit log dir host (volume bind)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
sudo mkdir -p "$AUDIT_LOG_DIR"
|
||||
sudo chown 1000:1000 "$AUDIT_LOG_DIR" # uid del container app
|
||||
sudo chown 1000:1000 "$AUDIT_LOG_DIR"
|
||||
echo "Audit log dir: $AUDIT_LOG_DIR (chown 1000:1000)"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
@@ -154,11 +178,11 @@ docker compose "${COMPOSE_FILES[@]}" --env-file .env ps
|
||||
|
||||
echo
|
||||
echo "=== Smoke test (health check via gateway pubblico) ==="
|
||||
sleep 10 # tempo per Caddy di richiedere cert
|
||||
sleep 10
|
||||
if curl -sf -o /dev/null -m 10 "https://$DOMAIN/mcp-macro/health"; then
|
||||
echo " ✅ https://$DOMAIN/mcp-macro/health → 200"
|
||||
echo " OK: https://$DOMAIN/mcp-macro/health → 200"
|
||||
else
|
||||
echo " ⚠️ https://$DOMAIN/mcp-macro/health non risponde (DNS o cert non ancora pronti?)"
|
||||
echo " WARN: https://$DOMAIN/mcp-macro/health non risponde (DNS o cert non ancora pronti?)"
|
||||
echo " Riprova fra 30s o controlla: docker compose -f docker-compose.prod.yml logs gateway"
|
||||
fi
|
||||
|
||||
@@ -169,3 +193,4 @@ echo " Logs: docker compose ${COMPOSE_FILES[*]} --env-file .env logs -f <ser
|
||||
echo " Audit: tail -f $AUDIT_LOG_DIR/*.audit.jsonl"
|
||||
echo " Restart: docker compose ${COMPOSE_FILES[*]} --env-file .env restart <service>"
|
||||
echo " Stop: docker compose ${COMPOSE_FILES[*]} --env-file .env down"
|
||||
echo " Update: ri-esegui questo script (riscarica config + pull image)"
|
||||
@@ -4,7 +4,6 @@ import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_common.app_factory import ExchangeAppSpec, run_exchange_main
|
||||
from mcp_common.environment import EnvironmentInfo
|
||||
|
||||
@@ -109,8 +108,10 @@ def test_run_exchange_main_aborts_on_mainnet_without_confirmation(tmp_path, monk
|
||||
monkeypatch.delenv("STRICT_MAINNET", raising=False)
|
||||
|
||||
spec = _make_spec()
|
||||
with pytest.raises(EnvironmentMismatchError):
|
||||
with patch("mcp_common.app_factory.uvicorn.run"):
|
||||
with (
|
||||
pytest.raises(EnvironmentMismatchError),
|
||||
patch("mcp_common.app_factory.uvicorn.run"),
|
||||
):
|
||||
run_exchange_main(spec)
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ def test_audit_write_op_no_principal(captured_records):
|
||||
def test_audit_write_op_writes_to_file_when_AUDIT_LOG_FILE_set(tmp_path, monkeypatch):
|
||||
"""Con env AUDIT_LOG_FILE settato, una riga JSON appare nel file."""
|
||||
import json
|
||||
|
||||
from mcp_common import audit as audit_mod
|
||||
|
||||
audit_file = tmp_path / "audit.jsonl"
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from mcp_common.environment import (
|
||||
EnvironmentInfo,
|
||||
EnvironmentMismatchError,
|
||||
consistency_check,
|
||||
resolve_environment,
|
||||
@@ -123,9 +124,8 @@ def test_alpaca_paper_flag_key(monkeypatch):
|
||||
# ───────── consistency_check ─────────
|
||||
|
||||
|
||||
def _info(env: str, exchange: str = "deribit") -> "EnvironmentInfo":
|
||||
def _info(env: str, exchange: str = "deribit") -> EnvironmentInfo:
|
||||
"""Helper costruisce EnvironmentInfo per test."""
|
||||
from mcp_common.environment import EnvironmentInfo
|
||||
return EnvironmentInfo(
|
||||
exchange=exchange,
|
||||
environment=env,
|
||||
|
||||
Reference in New Issue
Block a user