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/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. +    diff --git a/gitpull.py b/gitpull.py index 2aadef0..f8a480e 100644 --- a/gitpull.py +++ b/gitpull.py @@ -3,22 +3,23 @@ import hashlib import hmac 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.3.0", -) - BASE_DIR = pathlib.Path(__file__).parent CONFIG_PATH = BASE_DIR / 'config' / 'config.json' +DB_PATH = BASE_DIR / 'data' / 'deployments.db' def _load_config() -> dict: @@ -40,11 +41,141 @@ 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 +_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 .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, +.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 .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, +.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 """ @@ -153,6 +284,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 { @@ -196,6 +332,7 @@ async def home():
Déploiement CI/CD automatique via git pull au push sur main.
${repo}${repo}