diff --git a/CHANGELOG.md b/CHANGELOG.md index b47b64b..e461d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 0.32.1 — 2026-10-25 + +### Corrigé — URL sœurs de la barre dérivées de l'environnement de l'agent + +Sur la dev, le menu inter-apps pointait vers les noms **prod** +des apps sœurs : seul `AGENT_EVENEMENTS_URL` y est posé (hôte +`agent-dev.dracodev.net`), et chaque URL sœur retombait sur son +défaut officiel faute de variable explicite — il aurait fallu +déclarer quatre variables de plus dans le `.env` dev. + +- **Résolution des URL sœurs** dans `app/config.py` (les + gabarits et le JS de la barre consomment `draco_urls` + inchangé), dans l'ordre : 1) la variable explicite — + `DRACO_SSO_URL` (ObsiGate), `DRACO_FLOWDECK_URL`, + `DRACO_TIKTAK_URL`, `KANBANTE_PUBLIC_URL` (Kanbante + elle-même) — prioritaire, contrat inchangé ; 2) **dérivation + d'environnement** depuis l'origine de l'agent telle que + l'app la connaît déjà (`AGENT_EVENEMENTS_URL`) : si son + hôte est de la forme `agent.dracodev.net` + (suffixe vide ou, typiquement, `-dev`), l'URL sœur devient + `https://.dracodev.net` (slugs : `og`, + `flowdeck`, `tiktak`, `kanbante`) ; hôte hors motif (autre + domaine, LAN, IP) = **aucune dérivation**, jamais de lien + deviné ; 3) le défaut officiel (prod), comme avant. +- L'URL Gitea ne change pas. +- Tests : avec le seul `AGENT_EVENEMENTS_URL` en `-dev`, les + quatre URL sœurs servies sont en `-dev` ; une URL explicite + gagne sur la dérivation ; hôte d'agent hors motif et aucun + environnement posé restent aux défauts officiels + (non-régression). + + ## 0.32.0 — 2026-10-25 ### Refactor — noms publics dracodev par configuration (dev/prod) diff --git a/app/config.py b/app/config.py index 242cef6..0643392 100644 --- a/app/config.py +++ b/app/config.py @@ -107,6 +107,10 @@ Environment variables: receiving this app's signed events. Default: derived from AGENT_URL (``/api/v1/evenements``). + Son hôte sert aussi de source à la + dérivation d'environnement des URL + sœurs ci-dessous (voir + ``_url_soeur_draco``). KANBANTE_PUBLIC_URL This app's own public base URL (barre commune self-link, SSO ``return_to``). Default (official) @@ -119,6 +123,20 @@ Environment variables: https://tiktak.dracodev.net. ObsiGate's bar link reuses DRACO_SSO_URL (the SSO emitter IS ObsiGate). + Résolution d'une URL sœur, dans + l'ordre : 1) la variable explicite + ci-dessus (prioritaire, contrat + inchangé) ; 2) dérivation depuis + l'hôte de l'agent connu par + AGENT_EVENEMENTS_URL — si cet hôte + est ``agent.dracodev.net`` + (p. ex. ``agent-dev``), l'URL sœur + devient ``https://. + dracodev.net`` (slugs : og, flowdeck, + tiktak, kanbante) ; hôte hors motif + (autre domaine, LAN, IP) = aucune + dérivation, jamais de lien deviné ; + 3) le défaut officiel (prod). No public dracodev name is hard-coded outside these defaults: dev/prod differ only by environment, never by code. @@ -130,12 +148,14 @@ Environment variables: """ import os +import re from dataclasses import dataclass from pathlib import Path +from urllib.parse import urlparse BASE_DIR = Path(__file__).resolve().parent.parent -APP_VERSION = "0.32.0" +APP_VERSION = "0.32.1" def _env_bool(name: str, default: bool) -> bool: @@ -171,6 +191,48 @@ def _env_wip_limits(name: str) -> dict[str, int]: return limits +# ── Dérivation d'environnement des URL sœurs dracodev (0.32.1) ── +# Sur la dev, seul AGENT_EVENEMENTS_URL est posé (hôte +# agent-dev.dracodev.net) : les URL sœurs retombaient donc sur leurs +# défauts prod dans la barre commune. La résolution ci-dessous +# évite de devoir déclarer quatre variables de plus. +_MOTIF_HOTE_AGENT = re.compile(r"agent([a-z0-9-]*)\.dracodev\.net") + + +def _suffixe_environnement_agent(agent_evenements_url: str) -> str | None: + """Suffixe d'environnement lu sur l'hôte de l'agent. + + Retourne le suffixe (``""`` pour le nom officiel, ``"-dev"`` + pour la dev) si l'hôte de ``agent_evenements_url`` est de la + forme ``agent.dracodev.net`` ; ``None`` hors motif + (autre domaine, LAN, IP…) — dans ce cas, aucune dérivation. + """ + try: + hote = urlparse(agent_evenements_url).hostname or "" + except ValueError: + return None + motif = _MOTIF_HOTE_AGENT.fullmatch(hote) + return motif.group(1) if motif else None + + +def _url_soeur_draco( + nom_env: str, slug: str, defaut: str, suffixe: str | None +) -> str: + """Résout l'URL publique d'une app sœur de la barre commune. + + 1) ``nom_env`` explicite — prioritaire, contrat inchangé ; + 2) dérivation d'environnement : ``https://. + dracodev.net`` quand l'hôte de l'agent suit le motif ; + 3) défaut officiel (prod), comme avant. + """ + explicite = os.getenv(nom_env) + if explicite: + return explicite.rstrip("/") + if suffixe is not None: + return f"https://{slug}{suffixe}.dracodev.net" + return defaut + + @dataclass(frozen=True) class Settings: gitea_url: str = "https://git.dracodev.net" @@ -234,6 +296,14 @@ def _env_settings() -> Settings: agent_url = os.getenv( "AGENT_URL", "https://agent.dracodev.net" ).rstrip("/") + agent_evenements_url = ( + os.getenv("AGENT_EVENEMENTS_URL") + or f"{agent_url}/api/v1/evenements" + ).rstrip("/") + # Origine de l'agent telle que l'app la connaît déjà : c'est + # elle qui porte le suffixe d'environnement (-dev…) repris + # par les URL sœurs dépourvues de variable explicite. + suffixe_agent = _suffixe_environnement_agent(agent_evenements_url) return Settings( gitea_url=os.getenv("GITEA_URL", "https://git.dracodev.net").rstrip("/"), gitea_token=os.getenv("GITEA_TOKEN", ""), @@ -276,22 +346,30 @@ def _env_settings() -> Settings: draco_secret=os.getenv("DRACO_SESSION_SECRET", ""), draco_secret_prev=os.getenv("DRACO_SESSION_SECRET_PREV", ""), sso_machine_key=os.getenv("DRACO_SSO_MACHINE_KEY", ""), - sso_url=os.getenv("DRACO_SSO_URL", "https://og.dracodev.net").rstrip("/"), - public_url=os.getenv( - "KANBANTE_PUBLIC_URL", "https://kanbante.dracodev.net" - ).rstrip("/"), - flowdeck_url=os.getenv( - "DRACO_FLOWDECK_URL", "https://flowdeck.dracodev.net" - ).rstrip("/"), - tiktak_url=os.getenv( - "DRACO_TIKTAK_URL", "https://tiktak.dracodev.net" - ).rstrip("/"), + sso_url=_url_soeur_draco( + "DRACO_SSO_URL", "og", "https://og.dracodev.net", suffixe_agent + ), + public_url=_url_soeur_draco( + "KANBANTE_PUBLIC_URL", + "kanbante", + "https://kanbante.dracodev.net", + suffixe_agent, + ), + flowdeck_url=_url_soeur_draco( + "DRACO_FLOWDECK_URL", + "flowdeck", + "https://flowdeck.dracodev.net", + suffixe_agent, + ), + tiktak_url=_url_soeur_draco( + "DRACO_TIKTAK_URL", + "tiktak", + "https://tiktak.dracodev.net", + suffixe_agent, + ), agent_url=agent_url, archive_suggest_days=_env_int("KANBANTE_ARCHIVE_SUGGEST_DAYS", 180), - agent_evenements_url=( - os.getenv("AGENT_EVENEMENTS_URL") - or f"{agent_url}/api/v1/evenements" - ).rstrip("/"), + agent_evenements_url=agent_evenements_url, agent_evenements_cle=os.getenv("AGENT_EVENEMENTS_CLE", ""), ) diff --git a/pyproject.toml b/pyproject.toml index ceaf7e6..1b0b852 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kanbante" -version = "0.32.0" +version = "0.32.1" description = "Kanban project manager integrated with Gitea" requires-python = ">=3.11" dependencies = [ diff --git a/tests/test_draco_barre.py b/tests/test_draco_barre.py index 1fe1f10..a8b3735 100644 --- a/tests/test_draco_barre.py +++ b/tests/test_draco_barre.py @@ -71,6 +71,117 @@ def test_config_agent_evenements_derive_de_agent_url(monkeypatch): ) +# ── Dérivation d'environnement des URL sœurs (0.32.1) ── +# Sur la dev, seul AGENT_EVENEMENTS_URL est posé : les URL sœurs +# doivent suivre son suffixe (-dev) sans quatre variables de plus. +_ENV_URLS_SOEURS = ( + "DRACO_SSO_URL", # ObsiGate (émettrice SSO) + "DRACO_FLOWDECK_URL", + "DRACO_TIKTAK_URL", + "KANBANTE_PUBLIC_URL", # Kanbante elle-même + "AGENT_URL", +) + +_DEFAUTS_OFFICIELS = ( + "https://og.dracodev.net", + "https://flowdeck.dracodev.net", + "https://tiktak.dracodev.net", + "https://kanbante.dracodev.net", +) + +_URLS_DEV = ( + "https://og-dev.dracodev.net", + "https://flowdeck-dev.dracodev.net", + "https://tiktak-dev.dracodev.net", + "https://kanbante-dev.dracodev.net", +) + + +def _nettoyer_env_soeurs(monkeypatch): + for nom in _ENV_URLS_SOEURS: + monkeypatch.delenv(nom, raising=False) + + +def _urls_soeurs(settings): + return ( + settings.sso_url, + settings.flowdeck_url, + settings.tiktak_url, + settings.public_url, + ) + + +def test_config_urls_soeurs_derivees_du_seul_agent_evenements(monkeypatch): + from app.config import _env_settings + + _nettoyer_env_soeurs(monkeypatch) + monkeypatch.setenv( + "AGENT_EVENEMENTS_URL", + "https://agent-dev.dracodev.net/api/v1/evenements", + ) + assert _urls_soeurs(_env_settings()) == _URLS_DEV + + +def test_barre_urls_soeurs_derivees_du_seul_agent_evenements( + monkeypatch, fake_gitea +): + from fastapi.testclient import TestClient + + import app.main as main + + _nettoyer_env_soeurs(monkeypatch) + monkeypatch.setenv( + "AGENT_EVENEMENTS_URL", + "https://agent-dev.dracodev.net/api/v1/evenements", + ) + with TestClient(main.app) as client: + r = client.get("/") + assert r.status_code == 200 + for url in _URLS_DEV: + assert f"{url}/" in r.text + + +def test_config_url_soeur_explicite_gagne_sur_la_derivation(monkeypatch): + from app.config import _env_settings + + _nettoyer_env_soeurs(monkeypatch) + monkeypatch.setenv( + "AGENT_EVENEMENTS_URL", + "https://agent-dev.dracodev.net/api/v1/evenements", + ) + monkeypatch.setenv( + "DRACO_FLOWDECK_URL", "https://flowdeck.example.test" + ) + s = _env_settings() + assert s.flowdeck_url == "https://flowdeck.example.test" + # Les autres sœurs restent dérivées de l'environnement. + assert s.sso_url == "https://og-dev.dracodev.net" + assert s.tiktak_url == "https://tiktak-dev.dracodev.net" + assert s.public_url == "https://kanbante-dev.dracodev.net" + + +def test_config_hote_agent_hors_motif_reste_aux_defauts(monkeypatch): + from app.config import _env_settings + + _nettoyer_env_soeurs(monkeypatch) + for url_agent in ( + "https://agent.example.com/api/v1/evenements", + "https://agent-dev.autre-domaine.net/api/v1/evenements", + "http://192.168.30.101:8790/api/v1/evenements", + "http://127.0.0.1:8790/api/v1/evenements", + ): + monkeypatch.setenv("AGENT_EVENEMENTS_URL", url_agent) + assert _urls_soeurs(_env_settings()) == _DEFAUTS_OFFICIELS + + +def test_config_aucun_env_reste_aux_defauts_officiels(monkeypatch): + from app.config import _env_settings + + _nettoyer_env_soeurs(monkeypatch) + monkeypatch.delenv("AGENT_EVENEMENTS_URL", raising=False) + assert _urls_soeurs(_env_settings()) == _DEFAUTS_OFFICIELS + + def test_statiques_de_la_barre_servis(client): assert client.get("/static/css/draco-barre.css").status_code == 200 assert client.get("/static/js/draco-barre.js").status_code == 200