From f2185c186ad1e69bfe348d0aa0261b9795e88493 Mon Sep 17 00:00:00 2001 From: Alexandre LEPETIT <10155402+aelttil@users.noreply.github.com> Date: Mon, 11 May 2026 15:57:10 +0200 Subject: [PATCH 1/4] ci: add GitHub Actions workflow for GHCR build & push + README badges --- .github/workflows/build.yml | 80 +++++++++++++++++++++++++++++++++++++ README.md | 7 ++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d74e81a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,80 @@ +name: CI — Build & Push + +# Déclencheurs : +# - push sur n'importe quelle branche → image taguée avec le nom de branche +# - push d'un tag v* → image taguée vX.Y.Z + vX.Y + latest +on: + push: + branches: + - "**" + tags: + - "v*" + +env: + REGISTRY: ghcr.io + # github.repository → "cloud-temple/graph-memory" + # Image finale → ghcr.io/cloud-temple/graph-memory: + IMAGE_NAME: ${{ github.repository }} + +jobs: + # ───────────────────────────────────────────────────────────────────────── + # Job — Build multi-arch + push GHCR + # + # Stratégie de tags automatique (via docker/metadata-action) : + # - branche main → ghcr.io/cloud-temple/graph-memory:main + # - branche dev → ghcr.io/cloud-temple/graph-memory:dev + # - branche feature/xx → ghcr.io/cloud-temple/graph-memory:feature-xx + # - tag v2.1.1 → ghcr.io/cloud-temple/graph-memory:2.1.1 :2.1 :latest + # ───────────────────────────────────────────────────────────────────────── + build: + name: Build & Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # Requis pour pousser vers GHCR + + steps: + - uses: actions/checkout@v4 + + # Multi-arch : amd64 natif + arm64 via QEMU (Mac M-series, Pi, etc.) + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Authentification GHCR — GITHUB_TOKEN suffit, aucun secret à configurer + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Génération automatique des tags et labels OCI + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Branches → tag avec le nom de branche (main, dev, feature-xxx, …) + type=ref,event=branch + # Tags semver → version complète (ex: v2.1.1 → 2.1.1) + type=semver,pattern={{version}} + # Tags semver → major.minor (ex: v2.1.1 → 2.1) + type=semver,pattern={{major}}.{{minor}} + # :latest uniquement sur les tags vX.Y.Z (pas sur les branches) + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64/v8 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # Cache BuildKit via GitHub Actions Cache → rebuilds ~3× plus rapides + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 0f7cf16..ff0dce8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ Service de mémoire persistante basé sur un **graphe de connaissances** pour le Développé par **[Cloud Temple](https://www.cloud-temple.com)**. +[![CI](https://github.com/Cloud-Temple/graph-memory/actions/workflows/build.yml/badge.svg)](https://github.com/Cloud-Temple/graph-memory/actions/workflows/build.yml) +[![Docker](https://img.shields.io/badge/ghcr.io-cloud--temple%2Fgraph--memory-blue?logo=docker)](https://ghcr.io/cloud-temple/graph-memory) +[![Version](https://img.shields.io/badge/version-2.1.1-blue.svg)]() +[![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)]() +[![MCP](https://img.shields.io/badge/protocol-MCP-purple.svg)]() +[![Python](https://img.shields.io/badge/python-3.11+-yellow.svg)]() +

Graph Memory — Visualisation du graphe de connaissances

From a0846303a84390d23e19ed3b3308b1b097527fc5 Mon Sep 17 00:00:00 2001 From: Alexandre LEPETIT <10155402+aelttil@users.noreply.github.com> Date: Tue, 12 May 2026 11:40:53 +0200 Subject: [PATCH 2/4] feat: add optional PROXY_URL for outbound HTTP proxy support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a custom PROXY_URL environment variable (optional) to route outbound S3 and LLM calls through an HTTP proxy without affecting other Python libraries that auto-read HTTP_PROXY/HTTPS_PROXY OS variables. Changes: - config.py: new proxy_url field + model_validator fail-fast validation (must start with http:// or https:// if set) - storage.py: proxy injected into both boto3 clients (SigV2 and SigV4) via Config(proxies=...) — logged at startup if active - extractor.py: proxy injected into AsyncOpenAI via a pre-configured httpx.AsyncClient(proxy=httpx.Proxy(...)) - embedder.py: idem for EmbeddingService - .env.example: new commented entry #PROXY_URL=http://10.185.132.250:3128 - README.md: PROXY_URL row added to optional variables table - CHANGELOG.md: fix heading typo + [Unreleased] section --- .env.example | 9 +++++++++ CHANGELOG.md | 15 ++++++++++++++- README.md | 1 + src/mcp_memory/config.py | 22 +++++++++++++++++++++- src/mcp_memory/core/embedder.py | 22 +++++++++++++++++++--- src/mcp_memory/core/extractor.py | 22 +++++++++++++++++++--- src/mcp_memory/core/storage.py | 25 ++++++++++++++++++++----- 7 files changed, 103 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 6be118d..4f24d30 100644 --- a/.env.example +++ b/.env.example @@ -174,3 +174,12 @@ ADMIN_BOOTSTRAP_KEY=change_me_in_production # Timeout requêtes Neo4j en secondes (défaut: 30) # NEO4J_QUERY_TIMEOUT_SECONDS=30 + +# ============================================================================= +# Proxy HTTP sortant (optionnel) +# ============================================================================= +# Proxy HTTP pour les appels S3 (boto3), LLM et embeddings (httpx/openai). +# Variable CUSTOM — pas HTTP_PROXY/HTTPS_PROXY — injectée manuellement pour +# ne pas affecter toutes les autres libs Python. +# ⚠️ Non supporté pour Neo4j (driver bolt) et Qdrant (client natif). +# #PROXY_URL=http://10.185.132.250:3128 diff --git a/CHANGELOG.md b/CHANGELOG.md index ad74f67..6f9acf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,17 @@ -l# Changelog +# Changelog + +## [Unreleased] + +### Added +- **PROXY_URL** — variable d'environnement optionnelle pour router les appels + S3 (boto3 SigV2 + SigV4), LLM extraction (`ExtractorService`) et embeddings + (`EmbeddingService`) via un proxy HTTP sortant. + - Injectée manuellement (`boto3.Config(proxies=...)`, `httpx.AsyncClient(proxy=...)`) + pour ne **pas** affecter les autres libs Python qui lisent automatiquement + `HTTP_PROXY` / `HTTPS_PROXY`. + - Validation fail-fast au démarrage : doit commencer par `http://` ou `https://`. + - Non supporté pour Neo4j (driver bolt) et Qdrant (client natif sans param proxy). + - Log au démarrage si active : `[StorageService] S3 requests via proxy …` ## [2.1.1] - 2026-04-03 diff --git a/README.md b/README.md index ff0dce8..0c0ddc0 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,7 @@ cp .env.example .env | `RAG_CHUNK_LIMIT` | `8` | Nombre max de chunks retournés par Qdrant | | `CHUNK_SIZE` | `500` | Taille cible en tokens par chunk | | `CHUNK_OVERLAP` | `50` | Tokens de chevauchement entre chunks | +| `PROXY_URL` | _(aucun)_ | Proxy HTTP sortant pour S3, LLM et embeddings (ex: `http://10.185.132.250:3128`). Variable custom — pas `HTTP_PROXY` — injectée manuellement pour ne pas forcer le proxy sur toutes les libs Python. Non supporté pour Neo4j et Qdrant. | Voir `.env.example` pour la liste complète. diff --git a/src/mcp_memory/config.py b/src/mcp_memory/config.py index 167877b..3002869 100644 --- a/src/mcp_memory/config.py +++ b/src/mcp_memory/config.py @@ -8,6 +8,7 @@ from functools import lru_cache from typing import Optional +from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -105,7 +106,26 @@ class Settings(BaseSettings): extraction_timeout_seconds: int = 600 # 10 min par appel LLM (gros docs avec chain-of-thought) s3_upload_timeout_seconds: int = 60 neo4j_query_timeout_seconds: int = 30 - + + # ========================================================================= + # Proxy HTTP sortant (optionnel) + # ========================================================================= + # Variable CUSTOM — pas HTTP_PROXY/HTTPS_PROXY — pour ne pas forcer le proxy + # sur toutes les libs Python (boto3, httpx, requests…). + # Injecté manuellement dans boto3 (S3), httpx (LLM extraction + embeddings). + # Non supporté pour Neo4j et Qdrant (clients sans paramètre proxy explicite). + proxy_url: str = "" + + @model_validator(mode="after") + def _validate_proxy_url(self) -> "Settings": + """Vérifie que PROXY_URL est une URL valide si renseignée.""" + if self.proxy_url and not self.proxy_url.startswith(("http://", "https://")): + raise ValueError( + f"PROXY_URL must start with http:// or https://, " + f"got '{self.proxy_url[:50]}'" + ) + return self + @property def llmaas_base_url(self) -> str: """URL complète pour le client OpenAI (compatible OpenAI).""" diff --git a/src/mcp_memory/core/embedder.py b/src/mcp_memory/core/embedder.py index 8f047e4..870e44d 100644 --- a/src/mcp_memory/core/embedder.py +++ b/src/mcp_memory/core/embedder.py @@ -13,6 +13,7 @@ import sys from typing import Optional, List +import httpx from openai import AsyncOpenAI from openai import APIError, APITimeoutError from tenacity import retry, stop_after_attempt, wait_exponential @@ -29,16 +30,31 @@ class EmbeddingService: """ def __init__(self): - """Initialise le client OpenAI pour les embeddings.""" + """Initialise le client OpenAI pour les embeddings. + + Si PROXY_URL est définie, les appels d'embedding transitent par ce proxy + via un httpx.AsyncClient dédié. On n'utilise pas HTTP_PROXY/HTTPS_PROXY + pour ne pas affecter les autres libs Python. + """ settings = get_settings() - + + proxy_url = settings.proxy_url.strip() or None + _http_client = ( + httpx.AsyncClient(proxy=httpx.Proxy(proxy_url)) + if proxy_url + else None + ) + # Utilise le même client OpenAI que l'extracteur # L'API LLMaaS Cloud Temple est compatible OpenAI self._client = AsyncOpenAI( base_url=settings.llmaas_base_url, api_key=settings.llmaas_api_key, - timeout=60.0 + timeout=60.0, + **({"http_client": _http_client} if _http_client else {}), ) + if proxy_url: + print(f"[EmbeddingService] embedding requests via proxy {proxy_url}", file=sys.stderr) self._model = settings.llmaas_embedding_model self._dimensions = settings.llmaas_embedding_dimensions diff --git a/src/mcp_memory/core/extractor.py b/src/mcp_memory/core/extractor.py index 2f227cc..a9280fb 100644 --- a/src/mcp_memory/core/extractor.py +++ b/src/mcp_memory/core/extractor.py @@ -11,6 +11,7 @@ from typing import Optional, List from tenacity import retry, stop_after_attempt, wait_exponential +import httpx from openai import AsyncOpenAI from openai import APIError, APITimeoutError @@ -66,14 +67,29 @@ class ExtractorService: """ def __init__(self): - """Initialise le client OpenAI compatible.""" + """Initialise le client OpenAI compatible. + + Si PROXY_URL est définie, les appels LLM transitent par ce proxy + via un httpx.AsyncClient dédié. On n'utilise pas HTTP_PROXY/HTTPS_PROXY + pour ne pas affecter les autres libs Python. + """ settings = get_settings() - + + proxy_url = settings.proxy_url.strip() or None + _http_client = ( + httpx.AsyncClient(proxy=httpx.Proxy(proxy_url)) + if proxy_url + else None + ) + self._client = AsyncOpenAI( base_url=settings.llmaas_base_url, api_key=settings.llmaas_api_key, - timeout=settings.extraction_timeout_seconds + timeout=settings.extraction_timeout_seconds, + **({"http_client": _http_client} if _http_client else {}), ) + if proxy_url: + print(f"[ExtractorService] LLM requests via proxy {proxy_url}", file=sys.stderr) self._model = settings.llmaas_model self._max_tokens = settings.llmaas_max_tokens self._temperature = settings.llmaas_temperature diff --git a/src/mcp_memory/core/storage.py b/src/mcp_memory/core/storage.py index 40120be..d20f36a 100644 --- a/src/mcp_memory/core/storage.py +++ b/src/mcp_memory/core/storage.py @@ -40,15 +40,23 @@ def __init__(self): # Région Dell ECS Cloud Temple region = settings.s3_region_name if settings.s3_region_name else "fr1" + # Proxy HTTP sortant — utilise PROXY_URL (variable custom) plutôt que + # HTTP_PROXY/HTTPS_PROXY pour ne pas affecter les autres libs Python. + proxy_url = settings.proxy_url.strip() or None + _proxies: dict | None = ( + {"http": proxy_url, "https": proxy_url} if proxy_url else None + ) + # Client SigV2 pour PUT/GET/DELETE (opérations sur objets) # Tests validés: PUT ✅, GET ✅, DELETE ✅ config_v2 = Config( region_name=region, signature_version='s3', # SigV2 legacy s3={'addressing_style': 'path'}, - retries={'max_attempts': 3, 'mode': 'adaptive'} + retries={'max_attempts': 3, 'mode': 'adaptive'}, + **({"proxies": _proxies} if _proxies else {}), ) - + self._client_v2 = boto3.client( 's3', endpoint_url=settings.s3_endpoint_url, @@ -57,16 +65,17 @@ def __init__(self): region_name=region, config=config_v2 ) - + # Client SigV4 pour HEAD/LIST (opérations métadonnées) # Utilisé en fallback si SigV2 échoue sur ces opérations config_v4 = Config( region_name=region, signature_version='s3v4', s3={'addressing_style': 'path', 'payload_signing_enabled': False}, - retries={'max_attempts': 3, 'mode': 'adaptive'} + retries={'max_attempts': 3, 'mode': 'adaptive'}, + **({"proxies": _proxies} if _proxies else {}), ) - + self._client_v4 = boto3.client( 's3', endpoint_url=settings.s3_endpoint_url, @@ -75,6 +84,12 @@ def __init__(self): region_name=region, config=config_v4 ) + + if proxy_url: + print( + f"[StorageService] S3 requests via proxy {proxy_url}", + file=__import__("sys").stderr, + ) # Client par défaut (SigV2 pour compatibilité maximale) self._client = self._client_v2 From 0ce329967f64c3731bf73543ad7ba50fba7d2da2 Mon Sep 17 00:00:00 2001 From: Alexandre LEPETIT <10155402+aelttil@users.noreply.github.com> Date: Thu, 14 May 2026 18:19:10 +0200 Subject: [PATCH 3/4] fix(docker): remove hardcoded HEALTHCHECK, python-based healthcheck in compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: suppression du HEALTHCHECK hardcodé sur le port 8002. Le port est configurable via MCP_SERVER_PORT ; la responsabilité du healthcheck appartient à l'orchestrateur (compose, Swarm, K8s). - docker-compose.yml: healthcheck via python urllib (disponible dans toute image Python) au lieu de curl. - docker-compose.yml: variable PROXY_URL documentée (commentée). - CHANGELOG.md: sections Fixed + Changed dans [Unreleased]. --- CHANGELOG.md | 18 ++++++++++++++++++ Dockerfile | 6 +++--- docker-compose.yml | 8 +++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9acf1..587d028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,24 @@ - Non supporté pour Neo4j (driver bolt) et Qdrant (client natif sans param proxy). - Log au démarrage si active : `[StorageService] S3 requests via proxy …` +### Fixed +- **`Dockerfile` : suppression du `HEALTHCHECK`** — Le healthcheck était hardcodé sur + le port 8002 dans l'image, provoquant l'arrêt du container (ExitCode=0 via SIGTERM + Swarm) dès que `MCP_SERVER_PORT` était différent de 8002. La responsabilité du + healthcheck appartient à l'orchestrateur (`docker-compose.yml`, Swarm stack, Kubernetes + probes) qui connaît la configuration réelle du déploiement. +- **`docker-compose.yml` : healthcheck via `python` au lieu de `curl`** — Plus de + dépendance à un binaire externe ; disponible dans toute image Python de base. + +### Changed +- **`ci/build.yml` : `:X.Y` et `:latest` réservés aux releases stables** — Les tags + pré-release (`v2.1.1-rc.1`, `v2.1.1-dev`, …) ne reçoivent plus d'alias `:X.Y` ni + `:latest`. Un step `Detect stable tag` détecte si le tag Git est de la forme + `vX.Y.Z` (sans tiret) pour activer ces alias. Conforme à la convention SemVer : + un tag avec suffixe = pré-release = ne pas pointer `latest`. + +--- + ## [2.1.1] - 2026-04-03 ### 🐛 Fix `document_get(include_content=True)` sur fichiers binaires diff --git a/Dockerfile b/Dockerfile index e763008..3509f37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,9 +52,9 @@ EXPOSE 8002 # Passer en utilisateur non-root USER mcp -# Healthcheck via /health endpoint (léger, pas de fork Python) -HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD curl -sf http://localhost:8002/health -o /dev/null 2>/dev/null +# NOTE: Pas de HEALTHCHECK dans l'image — le port est configurable via MCP_SERVER_PORT. +# La responsabilité du healthcheck appartient à l'orchestrateur (docker-compose.yml, +# Swarm stack, Kubernetes liveness probe) qui connaît le port réel de déploiement. # Point d'entrée ENTRYPOINT ["python", "-m", "src.mcp_memory.server"] diff --git a/docker-compose.yml b/docker-compose.yml index 969726d..dafa256 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,10 +117,12 @@ services: # Server config - MCP_SERVER_DEBUG=${MCP_SERVER_DEBUG:-false} - ADMIN_BOOTSTRAP_KEY=${ADMIN_BOOTSTRAP_KEY} + # Proxy HTTP sortant (optionnel — ne pas définir si absent) + # - PROXY_URL=${PROXY_URL:-} healthcheck: - # /health est un endpoint léger qui retourne le status du service + version. - # Avec Streamable HTTP (v1.4.0+), l'ancien endpoint /sse n'existe plus. - test: ["CMD-SHELL", "curl -sf http://localhost:8002/health -o /dev/null 2>/dev/null"] + # python est toujours disponible dans l'image — pas de dépendance à curl. + # Adapter le port si MCP_SERVER_PORT est surchargé. + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8002/health', timeout=5)\" 2>/dev/null"] interval: 30s timeout: 10s start_period: 10s From ce89fe2728183ac5086c425bead84c81f0415a12 Mon Sep 17 00:00:00 2001 From: Alexandre LEPETIT <10155402+aelttil@users.noreply.github.com> Date: Thu, 14 May 2026 18:19:17 +0200 Subject: [PATCH 4/4] ci: restrict :X.Y and :latest aliases to stable semver tags only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tags SemVer sans tiret (vX.Y.Z) → :X.Y.Z + :X.Y + :latest Tags pré-release avec tiret (vX.Y.Z-rc.1, vX.Y.Z-dev, …) → :X.Y.Z-* uniquement Un step 'Detect stable tag' inspecte le ref Git via regex ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ pour activer les alias. Conforme à la convention SemVer : tout tag avec suffixe = pré-release. --- .github/workflows/build.yml | 44 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d74e81a..a6f11cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,21 @@ name: CI — Build & Push # Déclencheurs : # - push sur n'importe quelle branche → image taguée avec le nom de branche -# - push d'un tag v* → image taguée vX.Y.Z + vX.Y + latest +# - push d'un tag v* → tags selon la convention ci-dessous +# +# Convention de tags Docker : +# ┌─────────────────────┬─────────────────────────────────────────────────────┐ +# │ Git ref │ Tags Docker produits │ +# ├─────────────────────┼─────────────────────────────────────────────────────┤ +# │ branch: main │ :main │ +# │ branch: dev │ :dev │ +# │ branch: feature/foo │ :feature-foo │ +# │ tag: v2.1.1 │ :2.1.1 :2.1 :latest (release stable) │ +# │ tag: v2.1.1-rc.1 │ :2.1.1-rc.1 (pre-release, pas d'alias) │ +# │ tag: v2.1.1-dev │ :2.1.1-dev (pre-release, pas d'alias) │ +# └─────────────────────┴─────────────────────────────────────────────────────┘ +# Règle : un tag contenant un tiret (vX.Y.Z-*) est une pré-release → +# pas de :X.Y ni de :latest pour éviter de pointer une version instable. on: push: branches: @@ -19,12 +33,6 @@ env: jobs: # ───────────────────────────────────────────────────────────────────────── # Job — Build multi-arch + push GHCR - # - # Stratégie de tags automatique (via docker/metadata-action) : - # - branche main → ghcr.io/cloud-temple/graph-memory:main - # - branche dev → ghcr.io/cloud-temple/graph-memory:dev - # - branche feature/xx → ghcr.io/cloud-temple/graph-memory:feature-xx - # - tag v2.1.1 → ghcr.io/cloud-temple/graph-memory:2.1.1 :2.1 :latest # ───────────────────────────────────────────────────────────────────────── build: name: Build & Push Docker Image @@ -52,6 +60,18 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} # Génération automatique des tags et labels OCI + # IS_STABLE_TAG = true ssi le ref est un tag vX.Y.Z SANS tiret (release stable). + # Les pré-releases (v2.1.1-rc.1, v2.1.1-dev, …) ont un tiret → IS_STABLE_TAG=false. + - name: Detect stable tag + id: tag_type + run: | + REF="${{ github.ref }}" + if [[ "$REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "is_stable=true" >> "$GITHUB_OUTPUT" + else + echo "is_stable=false" >> "$GITHUB_OUTPUT" + fi + - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 @@ -60,12 +80,12 @@ jobs: tags: | # Branches → tag avec le nom de branche (main, dev, feature-xxx, …) type=ref,event=branch - # Tags semver → version complète (ex: v2.1.1 → 2.1.1) + # Tags semver → version complète (ex: v2.1.1 → 2.1.1 ; v2.1.1-rc.1 → 2.1.1-rc.1) type=semver,pattern={{version}} - # Tags semver → major.minor (ex: v2.1.1 → 2.1) - type=semver,pattern={{major}}.{{minor}} - # :latest uniquement sur les tags vX.Y.Z (pas sur les branches) - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + # :X.Y uniquement pour les releases stables (pas de tiret dans le tag) + type=semver,pattern={{major}}.{{minor}},enable=${{ steps.tag_type.outputs.is_stable }} + # :latest uniquement pour les releases stables (pas de tiret dans le tag) + type=raw,value=latest,enable=${{ steps.tag_type.outputs.is_stable }} - name: Build and Push uses: docker/build-push-action@v6