diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index 22e6600..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -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 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index fc0b148..625fee9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,66 +1,71 @@ # 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:` 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='' +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 via registry stesso, + `buildcache:` per ognuna → build successivi 5-10× più veloci). +- Tagga `:latest` + `:sha-`. +- 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 +94,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="" 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 +252,23 @@ echo "$GITEA_PAT" | docker login git.tielogic.xyz -u --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 +287,7 @@ chmod 600 secrets/* ### d) `.env` con configurazione runtime -Crea `/opt/cerbero-mcp/.env`: +Crea `/docker/cerbero_mcp/.env`: ```bash # Gateway @@ -390,14 +419,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`. diff --git a/README.md b/README.md index 32bf47f..98100e2 100644 --- a/README.md +++ b/README.md @@ -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='' + ./scripts/build-push.sh # tutte le 8 image + ./scripts/build-push.sh base mcp-bybit # solo specifiche + ``` + Tagga `:latest` + `:sha-` 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) diff --git a/scripts/build-push.sh b/scripts/build-push.sh new file mode 100755 index 0000000..0f0f0b3 --- /dev/null +++ b/scripts/build-push.sh @@ -0,0 +1,88 @@ +#!/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[@]}}") + +if [ -z "${GITEA_PAT:-}" ]; then + echo "FATAL: env GITEA_PAT non settata (PAT scope write:package)." + exit 1 +fi + +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; } + +echo "=== docker login $REGISTRY ===" +echo "$GITEA_PAT" | docker login "$REGISTRY" -u "$GITEA_USER" --password-stdin + +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" + local cache_ref="$IMAGE_PREFIX/buildcache:$name" + + echo "=== [$name] build & push ===" + local args=(buildx build --push + -f "$file" + -t "$tag_latest" + -t "$tag_sha" + --cache-from "type=registry,ref=$cache_ref" + --cache-to "type=registry,ref=$cache_ref,mode=max" + ) + 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 'cd /docker/cerbero_mcp && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d'" diff --git a/scripts/deploy-noclone.sh b/scripts/deploy-noclone.sh new file mode 100755 index 0000000..2082c2b --- /dev/null +++ b/scripts/deploy-noclone.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# Cerbero_mcp — deploy script (no-clone) per VPS produzione. +# +# Variante di deploy.sh che NON clona il repo; scarica solo i file +# strettamente necessari al runtime (compose, Caddyfile, public assets) +# via raw HTTP da Gitea. Image pre-built dal registry come deploy.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 (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: $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 (riscarica i file di config +# dal branch corrente, NON tocca .env esistente). + +set -euo pipefail + +DEPLOY_DIR="${DEPLOY_DIR:-/docker/cerbero_mcp}" +SECRETS_SRC="${SECRETS_SRC:-$DEPLOY_DIR/secrets}" +GITEA_USER="${GITEA_USER:-adriano}" +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 (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 + echo "FATAL: env GITEA_PAT non settata. Export del PAT con scope read:package prima." + 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 + echo "WARN: $DOMAIN non risolve via DNS — TLS Let's Encrypt fallirà finché DNS non propaga." +else + echo "DNS $DOMAIN → $ip_resolved" +fi + +# ────────────────────────────────────────────────────────────── +# 2. Login al container registry +# ────────────────────────────────────────────────────────────── +echo "=== docker login $REGISTRY ===" +echo "$GITEA_PAT" | docker login "$REGISTRY" -u "$GITEA_USER" --password-stdin + +# ────────────────────────────────────────────────────────────── +# 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" + +# 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 +# ────────────────────────────────────────────────────────────── +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 + cp "$SECRETS_SRC/$f" "secrets/$f" + chmod 600 "secrets/$f" + echo " ok: secrets/$f" + else + 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) +# ────────────────────────────────────────────────────────────── +if [ ! -f .env ]; then + echo "=== Creazione .env iniziale (testnet di default) ===" + cat > .env <" +echo " Audit: tail -f $AUDIT_LOG_DIR/*.audit.jsonl" +echo " Restart: docker compose ${COMPOSE_FILES[*]} --env-file .env restart " +echo " Stop: docker compose ${COMPOSE_FILES[*]} --env-file .env down" +echo " Update: ri-esegui questo script (riscarica config + pull image)" diff --git a/services/common/tests/test_app_factory.py b/services/common/tests/test_app_factory.py index 2e1538f..67d347d 100644 --- a/services/common/tests/test_app_factory.py +++ b/services/common/tests/test_app_factory.py @@ -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,9 +108,11 @@ 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"): - run_exchange_main(spec) + with ( + pytest.raises(EnvironmentMismatchError), + patch("mcp_common.app_factory.uvicorn.run"), + ): + run_exchange_main(spec) def test_run_exchange_main_strict_mainnet_disabled_via_env(tmp_path, monkeypatch): diff --git a/services/common/tests/test_audit.py b/services/common/tests/test_audit.py index df78ad8..2c4a631 100644 --- a/services/common/tests/test_audit.py +++ b/services/common/tests/test_audit.py @@ -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" diff --git a/services/common/tests/test_environment.py b/services/common/tests/test_environment.py index 7f83b8b..9840c2c 100644 --- a/services/common/tests/test_environment.py +++ b/services/common/tests/test_environment.py @@ -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,