From 3caa2fc5ca8c1a4f9b4c3612dc888a82ab03f002 Mon Sep 17 00:00:00 2001 From: plenoir Date: Tue, 19 May 2026 20:31:27 +0200 Subject: [PATCH 1/7] #38 Footer : lien GitHub sur les pages / et /webhookdemo Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 43 +++++++++++++++++++++++++++++++++++++++---- gitpull.py | 2 ++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8493fd4..0425dea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,12 @@ pip install -r requirements.txt python gitpull.py ``` +### Run in background (production-style) +```shell +./run.sh +``` +Uses `nohup python gitpull.py &`; logs go to `nohup.out`. + ### Lint ```shell pylint gitpull.py @@ -46,7 +52,7 @@ The entire application is in a single file: `gitpull.py`. - **`/`** — Dark-themed HTML home page. Lists configured repos (loaded via `GET /config/repos`) with Add / Edit / Delete buttons. All CRUD actions use JS `fetch` — no page reload. - **`/beats`** — Health check; returns `{"result": true}`. - **`/webhook`** (POST) — Receives GitHub push events. Verifies the HMAC-SHA256 signature if `webhook_secret` is set in config. Only processes pushes to `refs/heads/main`. Calls `update_webhook()`. -- **`/webhookdemo`** — Returns an HTML terminal-style page simulating `git reset` and `git pull` output. No real git commands are run. +- **`/webhookdemo`** — Returns an HTML terminal-style page simulating `git reset` and `git pull` output. Reads `demo/demo.json` for repo metadata. No real git commands are run. - **`/docs`** — Swagger UI (built into FastAPI). - **`GET /config/repos`** — Lists configured repos (excludes `ip` and `webhook_secret` keys). - **`POST /config/repos`** — Adds a repo `{"repo": "owner/repo", "path": "/abs/path"}`. Returns 409 if already exists. @@ -82,11 +88,40 @@ The entire application is in a single file: `gitpull.py`. ## Tests -`test_gitpull.py` — 23 tests, 98% coverage. Uses `fastapi.testclient.TestClient` (synchronous). The `patch_config` autouse fixture swaps `gitpull.config_github`, `gitpull.BASE_DIR`, and `gitpull.CONFIG_PATH` in-process for each test, then restores them — no real config files are read or written during tests (except in `TestLoadConfig` which explicitly tests file creation). +`test_gitpull.py` — 23 tests, 98% coverage. Uses `fastapi.testclient.TestClient` (synchronous). + +**`patch_config` autouse fixture** swaps three module-level globals for each test, then restores them: +- `gitpull.config_github` — replaced with `CONFIG_FIXTURE` dict +- `gitpull.BASE_DIR` — replaced with `tmp_path` +- `gitpull.CONFIG_PATH` — replaced with `tmp_path/config/config.json` + +It also creates `tmp_path/demo/demo.json` so `/webhookdemo` can read it. No real config files are read or written during tests (except in `TestLoadConfig` which explicitly tests file creation). + +## Demo fixture + +`demo/demo.json` is a minimal GitHub push-event payload used by `/webhookdemo` and the test fixture. It contains `ref` and `repository.full_name`. `demo/demo_full.json` is a full GitHub payload sample (reference only, not used at runtime). ## CI -`.github/workflows/build.yml` runs three jobs on push to `main` and on PRs: +`.github/workflows/build.yml` triggers on push to `main` or `develop`, and on pull requests. Runs three jobs: - **test** — `pytest --cov` across Python 3.11, 3.12, 3.13; uploads `coverage.xml` as artifact. -- **pylint** — linting across the same Python matrix. +- **pylint** — linting (`pylint $(git ls-files '*.py')`) across the same Python matrix. - **sonarqube** — runs after `test`, regenerates `coverage.xml` and sends it to SonarCloud (requires `SONAR_TOKEN` secret). + +When bumping the app version (`app = FastAPI(version=...)`) also update `sonar-project.properties` → `sonar.projectVersion`. + +## Milestone 1.4.0 — features in progress + +Issues open in the GitHub project (all in Backlog): +- **#29** — README badge → version 1.4.0 +- **#30** — `POST /deploy/{owner}/{repo}` endpoint + Deploy button on home page +- **#31** — `POST /reload` endpoint + Reload button on home page (uses `os.execv`; page polls `/beats` and auto-redirects) +- **#32** — SQLite `data/deployments.db`, table `deployment_log`, function `log_action()` +- **#33** — Call `log_action()` for every significant action: `startup`, `webhook`, `git_reset`, `git_pull`, `deploy`, `reload` +- **#34** — `GET /history` HTML page + `GET /api/history` JSON (paginated, filterable by repo/status) +- **#35** — Error badge on home page rows when last deployment failed (enriches `GET /config/repos` response) +- **#36** — Epic grouping #32–#35 +- **#37** — Swagger UI in dark mode +- **#38** — Footer link to `https://github.com/lenoirpatrick/githubwebhook` + +Dependency order: **#32 → #33 → #34 and #35**. Issues #30, #31, #37, #38 are independent. diff --git a/gitpull.py b/gitpull.py index 2aadef0..60fa1c9 100644 --- a/gitpull.py +++ b/gitpull.py @@ -221,6 +221,7 @@ async def home(): @@ -463,6 +464,7 @@ def webhookdemo(): ← Accueil Health check API docs + GitHub """ From e0866d99181f64f562962dd68312b827849147d2 Mon Sep 17 00:00:00 2001 From: plenoir Date: Tue, 19 May 2026 21:06:31 +0200 Subject: [PATCH 2/7] =?UTF-8?q?#37=20Swagger=20UI=20en=20dark=20mode=20via?= =?UTF-8?q?=20endpoint=20/docs=20personnalis=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- gitpull.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/gitpull.py b/gitpull.py index 60fa1c9..96f140c 100644 --- a/gitpull.py +++ b/gitpull.py @@ -8,6 +8,7 @@ import uvicorn from fastapi import FastAPI, HTTPException, Request +from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import HTMLResponse from pydantic import BaseModel @@ -15,6 +16,7 @@ title="GitHub Webhook Server", description="Serveur de webhook GitHub pour déploiement CI/CD automatique via `git pull`.", version="1.3.0", + docs_url=None, ) BASE_DIR = pathlib.Path(__file__).parent @@ -45,6 +47,62 @@ class RepoConfig(BaseModel): path: str +_SWAGGER_DARK_CSS = """ +body { background-color: #0d1117 !important; } +.swagger-ui { background-color: #0d1117; color: #c9d1d9; } +.swagger-ui .info .title, +.swagger-ui .info p, +.swagger-ui .info li, +.swagger-ui .info a { color: #c9d1d9; } +.swagger-ui .info a { color: #58a6ff; } +.swagger-ui .scheme-container { background: #161b22; box-shadow: none; border-bottom: 1px solid #30363d; } +.swagger-ui .opblock-tag { color: #c9d1d9; border-bottom: 1px solid #30363d; } +.swagger-ui .opblock-tag:hover { background: rgba(255,255,255,0.04); } +.swagger-ui .opblock { border-color: #30363d !important; background: rgba(255,255,255,0.02) !important; } +.swagger-ui .opblock .opblock-summary-description { color: #8b949e; } +.swagger-ui .opblock .opblock-summary-path { color: #c9d1d9; } +.swagger-ui .opblock.opblock-get .opblock-summary-method { background: #1f6feb; } +.swagger-ui .opblock.opblock-post .opblock-summary-method { background: #238636; } +.swagger-ui .opblock.opblock-put .opblock-summary-method { background: #9e6a03; } +.swagger-ui .opblock.opblock-delete .opblock-summary-method { background: #b91c1c; } +.swagger-ui .opblock-body pre.microlight, +.swagger-ui .microlight { background: #161b22 !important; color: #c9d1d9 !important; } +.swagger-ui .opblock-description-wrapper p, +.swagger-ui .opblock-external-docs-wrapper p, +.swagger-ui .opblock-title_normal p { color: #c9d1d9; } +.swagger-ui table thead tr td, +.swagger-ui table thead tr th { color: #8b949e; border-bottom: 1px solid #30363d; } +.swagger-ui .response-col_status { color: #c9d1d9; } +.swagger-ui .response-col_description p { color: #c9d1d9; } +.swagger-ui .response-col_links { color: #8b949e; } +.swagger-ui .responses-inner h4, +.swagger-ui .responses-inner h5 { color: #c9d1d9; } +.swagger-ui .model-box, +.swagger-ui .model { background: #161b22; color: #c9d1d9; } +.swagger-ui section.models { background: #161b22; border-color: #30363d; } +.swagger-ui section.models h4 { color: #c9d1d9; } +.swagger-ui .model-title { color: #c9d1d9; } +.swagger-ui .prop-type { color: #58a6ff; } +.swagger-ui input[type=text], +.swagger-ui input[type=password], +.swagger-ui textarea, +.swagger-ui select { background: #0d1117; color: #c9d1d9; border-color: #30363d; } +.swagger-ui .btn { color: #c9d1d9; border-color: #30363d; background: transparent; } +.swagger-ui .btn.execute { background: #238636; border-color: #238636; color: #fff; } +.swagger-ui .btn.authorize { color: #58a6ff; border-color: #58a6ff; } +.swagger-ui .btn.cancel { color: #f85149; border-color: #f85149; } +.swagger-ui .topbar { display: none; } +.swagger-ui .arrow { filter: invert(1); } +""" + + +@app.get("/docs", include_in_schema=False) +async def custom_docs(): + html = get_swagger_ui_html(openapi_url="/openapi.json", title=f"{app.title} — Swagger UI") + content = html.body.decode().replace("", f"") + return HTMLResponse(content=content) + + @app.get("/", response_class=HTMLResponse) async def home(): """ Page index """ From 7904dc073ddb72cf5c937f5dc4c65acbe33eae80 Mon Sep 17 00:00:00 2001 From: plenoir Date: Tue, 19 May 2026 21:08:01 +0200 Subject: [PATCH 3/7] =?UTF-8?q?#37=20Swagger=20dark=20mode=20:=20correctio?= =?UTF-8?q?n=20CSS=20sch=C3=A9mas/mod=C3=A8les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- gitpull.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/gitpull.py b/gitpull.py index 96f140c..2a526fe 100644 --- a/gitpull.py +++ b/gitpull.py @@ -80,9 +80,27 @@ class RepoConfig(BaseModel): .swagger-ui .model-box, .swagger-ui .model { background: #161b22; color: #c9d1d9; } .swagger-ui section.models { background: #161b22; border-color: #30363d; } -.swagger-ui section.models h4 { color: #c9d1d9; } +.swagger-ui section.models .model-container { background: #0d1117; border-color: #30363d; } +.swagger-ui section.models .model-container:hover { background: #0d1117; } +.swagger-ui section.models h4 { color: #c9d1d9; border-color: #30363d; } .swagger-ui .model-title { color: #c9d1d9; } +.swagger-ui .model-toggle:after { filter: invert(1); } .swagger-ui .prop-type { color: #58a6ff; } +.swagger-ui .prop-format { color: #8b949e; } +.swagger-ui table.model tr.property-row td { color: #c9d1d9; border-color: #30363d; } +.swagger-ui .model span, +.swagger-ui .model .property { color: #c9d1d9; } +.swagger-ui .model .property.primitive { color: #3fb950; } +.swagger-ui .model-hint { background: #30363d; color: #c9d1d9; } +.swagger-ui span.model-title__text { color: #c9d1d9; } +.swagger-ui .models-control { background: none; color: #c9d1d9; } +.swagger-ui .opblock-body .model-example { background: #161b22; } +.swagger-ui .tab li { color: #8b949e; } +.swagger-ui .tab li.active { color: #c9d1d9; } +.swagger-ui .highlight-code > .microlight { background: #161b22 !important; color: #c9d1d9 !important; } +.swagger-ui .responses-wrapper { background: #0d1117; } +.swagger-ui .response-control-media-type__accept-message { color: #8b949e; } +.swagger-ui .response-control-media-type select { background: #0d1117; color: #c9d1d9; border-color: #30363d; } .swagger-ui input[type=text], .swagger-ui input[type=password], .swagger-ui textarea, From 2f953cf1e71d3a2497fdd908f1176c8c027f5382 Mon Sep 17 00:00:00 2001 From: plenoir Date: Tue, 19 May 2026 21:10:23 +0200 Subject: [PATCH 4/7] =?UTF-8?q?#37=20Swagger=20dark=20mode=20:=20titres=20?= =?UTF-8?q?sch=C3=A9mas=20et=20boutons=20Collapse=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- gitpull.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/gitpull.py b/gitpull.py index 2a526fe..46f9672 100644 --- a/gitpull.py +++ b/gitpull.py @@ -82,18 +82,35 @@ class RepoConfig(BaseModel): .swagger-ui section.models { background: #161b22; border-color: #30363d; } .swagger-ui section.models .model-container { background: #0d1117; border-color: #30363d; } .swagger-ui section.models .model-container:hover { background: #0d1117; } +/* Titres de schémas : h4 + tous ses enfants inline (span, button, small, svg) */ .swagger-ui section.models h4 { color: #c9d1d9; border-color: #30363d; } +.swagger-ui section.models h4 span, +.swagger-ui section.models h4 small, +.swagger-ui section.models h4 button { color: #c9d1d9; background: none; border: none; cursor: pointer; } +/* Bouton "Collapse all" / flèche inline dans le titre de chaque schéma */ +.swagger-ui .model-box-control, +.swagger-ui .model-box-control:focus { background: none; border: none; color: #c9d1d9; } +.swagger-ui .model-box-control svg, +.swagger-ui section.models h4 svg { fill: #c9d1d9; } .swagger-ui .model-title { color: #c9d1d9; } +.swagger-ui span.model-title__text { color: #c9d1d9; } +.swagger-ui .models-control { background: none; color: #c9d1d9; } +.swagger-ui .models-control svg { fill: #c9d1d9; } +/* Flèches toggle dans l'arbre des propriétés */ .swagger-ui .model-toggle:after { filter: invert(1); } +.swagger-ui .model-toggle { background: none; } +/* Propriétés */ .swagger-ui .prop-type { color: #58a6ff; } .swagger-ui .prop-format { color: #8b949e; } .swagger-ui table.model tr.property-row td { color: #c9d1d9; border-color: #30363d; } .swagger-ui .model span, -.swagger-ui .model .property { color: #c9d1d9; } +.swagger-ui .model .property, +.swagger-ui .model span.prop-name { color: #c9d1d9; } .swagger-ui .model .property.primitive { color: #3fb950; } +/* "Any of", "One of", "All of" wrappers */ +.swagger-ui .model .inner-object { background: #161b22; } +.swagger-ui .model span.model { background: #161b22; color: #c9d1d9; } .swagger-ui .model-hint { background: #30363d; color: #c9d1d9; } -.swagger-ui span.model-title__text { color: #c9d1d9; } -.swagger-ui .models-control { background: none; color: #c9d1d9; } .swagger-ui .opblock-body .model-example { background: #161b22; } .swagger-ui .tab li { color: #8b949e; } .swagger-ui .tab li.active { color: #c9d1d9; } From cc2d415d4d7af9bbdaa0822faf68009c7e72e725 Mon Sep 17 00:00:00 2001 From: plenoir Date: Tue, 19 May 2026 21:14:16 +0200 Subject: [PATCH 5/7] #29 #30 #31 Version 1.4.0, endpoint deploy, endpoint reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #29: badge version 1.4.0 dans README et sonar-project.properties - #30: POST /deploy/{owner}/{repo} + bouton Deploy par dépôt - #31: POST /reload (os.execv) + bouton Recharger + polling /beats côté client Co-Authored-By: Claude Sonnet 4.6 --- README.md | 1 + gitpull.py | 53 +++++++++++++++++++++++++++++++++++++++- sonar-project.properties | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2980efd..fcf5fb9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # githubwebhook Gestion du webhook Github pour déploiement CI/CD sur vos environnements. +![version](https://img.shields.io/badge/version-1.4.0-blue.svg?style=flat) ![Python 3.11](https://img.shields.io/badge/python-3.11-green.svg?style=flat&logo=python&logoColor=white) ![Python 3.14](https://img.shields.io/badge/python-3.14-green.svg?style=flat&logo=python&logoColor=white) ![FastAPI](https://img.shields.io/badge/FastAPI-0.135-green.svg?style=flat&logo=flask&logoColor=white) diff --git a/gitpull.py b/gitpull.py index 46f9672..9c5e851 100644 --- a/gitpull.py +++ b/gitpull.py @@ -3,8 +3,11 @@ import hashlib import hmac import json +import os import pathlib import subprocess +import sys +import threading import uvicorn from fastapi import FastAPI, HTTPException, Request @@ -15,7 +18,7 @@ app = FastAPI( title="GitHub Webhook Server", description="Serveur de webhook GitHub pour déploiement CI/CD automatique via `git pull`.", - version="1.3.0", + version="1.4.0", docs_url=None, ) @@ -246,6 +249,11 @@ async def home(): .btn-edit:hover { background: #30363d; } .btn-delete { background: transparent; color: #f85149; border-color: #f8514944; margin-left: 0.4rem; } .btn-delete:hover { background: #f8514911; } + .btn-deploy { background: transparent; color: #3fb950; border-color: #3fb95044; margin-left: 0.4rem; } + .btn-deploy:hover { background: #3fb95011; } + .btn-reload { background: #9e6a03; color: #fff; border: 1px solid #bb8009; margin-left: 0.75rem; } + .btn-reload:hover { opacity: 0.85; } + .btn-reload:disabled { opacity: 0.5; cursor: not-allowed; } /* Modal */ .modal-overlay { @@ -289,6 +297,7 @@ async def home():

Déploiement CI/CD automatique via git pull au push sur main.

En ligne

▶ Lancer la démo + @@ -348,6 +357,7 @@ async def home(): ${repo} ${cfg.path} + @@ -398,6 +408,27 @@ async def home(): loadRepos(); } + async function deployRepo(repo) { + if (!confirm(`Lancer le deploy de ${repo} ?`)) return; + const [owner, repoName] = repo.split('/'); + const res = await fetch(`/deploy/${owner}/${repoName}`, {method: 'POST'}); + const data = await res.json(); + alert(data.result ? `✓ ${data.message}` : `✗ ${data.message}`); + } + + async function reloadServer() { + if (!confirm('Redémarrer le serveur ?')) return; + const btn = document.getElementById('reload-btn'); + btn.disabled = true; + btn.textContent = '↺ Redémarrage…'; + try { await fetch('/reload', {method: 'POST'}); } catch {} + await new Promise(r => setTimeout(r, 1500)); + (function poll() { + fetch('/beats').then(r => { if (r.ok) location.reload(); else setTimeout(poll, 500); }) + .catch(() => setTimeout(poll, 500)); + })(); + } + window.onload = loadRepos; @@ -564,6 +595,26 @@ def webhookdemo(): return HTMLResponse(content=html, status_code=200) +@app.post('/deploy/{owner}/{repo_name}') +def deploy(owner: str, repo_name: str): + """ Déclenche un git pull sur le dépôt configuré """ + key = f"{owner}/{repo_name}" + if key not in config_github: + raise HTTPException(status_code=404, detail=f"Repo {key} non configuré") + return update_webhook({"repository": {"full_name": key}}) + + +@app.post('/reload') +def reload_server(): + """ Redémarre le processus serveur via os.execv """ + def _restart(): + import time + time.sleep(0.5) + os.execv(sys.executable, [sys.executable] + sys.argv) + threading.Thread(target=_restart, daemon=True).start() + return {"result": True, "message": "Redémarrage en cours…"} + + def update_webhook(webhook_github): """ Fonction commune de mise à jour du dépot """ repo = webhook_github['repository']['full_name'] diff --git a/sonar-project.properties b/sonar-project.properties index 90db189..b54b070 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,7 +4,7 @@ sonar.organization=lenoirpatrick # This is the name and version displayed in the SonarCloud UI. sonar.projectName=githubwebhook -sonar.projectVersion=1.1.0 +sonar.projectVersion=1.4.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. #sonar.sources=. From 610d0d3709661ef67e083a9c26f09d4470a2e73a Mon Sep 17 00:00:00 2001 From: plenoir Date: Tue, 19 May 2026 21:25:26 +0200 Subject: [PATCH 6/7] #32 #33 #34 #35 #36 SQLite deployment_log, log_action, /history, last_status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #32: SQLite data/deployments.db, table deployment_log, _init_db(), log_action() - #33: log_action() appelé sur startup, webhook, git_reset, git_pull, deploy, reload - #34: GET /api/history (paginé, filtrable repo/status) + GET /history page HTML - #35: GET /config/repos enrichi avec last_status et last_timestamp par dépôt - Tests: patch DB_PATH dans fixture + 13 nouveaux tests (36 total) Co-Authored-By: Claude Sonnet 4.6 --- gitpull.py | 275 +++++++++++++++++++++++++++++++++++++++++++++--- test_gitpull.py | 126 +++++++++++++++++++++- 2 files changed, 385 insertions(+), 16 deletions(-) diff --git a/gitpull.py b/gitpull.py index 9c5e851..d71786e 100644 --- a/gitpull.py +++ b/gitpull.py @@ -5,25 +5,21 @@ import json import os import pathlib +import sqlite3 import subprocess import sys import threading +from contextlib import asynccontextmanager import uvicorn -from fastapi import FastAPI, HTTPException, Request +from fastapi import FastAPI, HTTPException, Query, Request from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import HTMLResponse from pydantic import BaseModel -app = FastAPI( - title="GitHub Webhook Server", - description="Serveur de webhook GitHub pour déploiement CI/CD automatique via `git pull`.", - version="1.4.0", - docs_url=None, -) - BASE_DIR = pathlib.Path(__file__).parent CONFIG_PATH = BASE_DIR / 'config' / 'config.json' +DB_PATH = BASE_DIR / 'data' / 'deployments.db' def _load_config() -> dict: @@ -45,6 +41,45 @@ def _save_config() -> None: config_github = _load_config() +def _init_db() -> None: + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + with sqlite3.connect(DB_PATH) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS deployment_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + action TEXT NOT NULL, + repo TEXT, + status TEXT NOT NULL DEFAULT 'ok', + message TEXT + ) + """) + + +def log_action(action: str, repo: str = None, status: str = 'ok', message: str = '') -> None: + _init_db() + with sqlite3.connect(DB_PATH) as conn: + conn.execute( + "INSERT INTO deployment_log (action, repo, status, message) VALUES (?, ?, ?, ?)", + (action, repo, status, message), + ) + + +@asynccontextmanager +async def _lifespan(_app: FastAPI): + log_action('startup') + yield + + +app = FastAPI( + title="GitHub Webhook Server", + description="Serveur de webhook GitHub pour déploiement CI/CD automatique via `git pull`.", + version="1.4.0", + docs_url=None, + lifespan=_lifespan, +) + + class RepoConfig(BaseModel): repo: str path: str @@ -321,6 +356,7 @@ async def home():