Compare commits
20 Commits
5bedb1f8ea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22590d7250 | ||
|
|
5c8e833d8b | ||
|
|
cf88ba0ef5 | ||
|
|
8b4677a6f7 | ||
|
|
6ed666fb66 | ||
|
|
b15ba9eea8 | ||
|
|
6fa8990cae | ||
|
|
01f88a0eca | ||
|
|
e05578676f | ||
|
|
4aa648fa8c | ||
|
|
8fbdfcafd4 | ||
|
|
b52bc72ce8 | ||
|
|
4d66aea6ed | ||
|
|
18ce3b953e | ||
|
|
e757c35767 | ||
|
|
0bd64d64a9 | ||
|
|
de56585840 | ||
|
|
2eec10c61a | ||
|
|
4e72ddc32f | ||
|
|
11e5def11c |
90
.claude/workflow-progress.md
Normal file
90
.claude/workflow-progress.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Workflow — gitea-dashboard
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Champ | Valeur |
|
||||||
|
|-------|--------|
|
||||||
|
| Projet | gitea-dashboard |
|
||||||
|
| Chemin | /home/sylvain/nas/perso/sylvain/conserver/code/application_temp/gitea-dashboard |
|
||||||
|
| Date de creation | 2026-03-10 |
|
||||||
|
| Origine | gitea@192.168.0.106:admin/gitea-dashboard.git |
|
||||||
|
| Version courante | v1.0.0 |
|
||||||
|
| Track | major-initial |
|
||||||
|
| Phase courante | 5 — POST-RELEASE |
|
||||||
|
| Etape courante | 13 |
|
||||||
|
| workflow_version | v1.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — FRAMING
|
||||||
|
|
||||||
|
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|
||||||
|
|---|-------|--------|------|-------------|------------|-------|
|
||||||
|
| 1 | Discovery | done | 2026-03-10 | /forge --discovery | Auto (synthesis.md existe) | step_1: done, deliverable: docs/discovery/synthesis.md |
|
||||||
|
| 2 | Creation projet | done | 2026-03-10 | forge | Auto (fichiers existent) | step_2: done, files_created: 14 |
|
||||||
|
| 3 | Specs | done | 2026-03-10 | - | Auto (descriptif.md existe avec contexte, objectifs, perimetre, contraintes) | step_3: done |
|
||||||
|
| 4 | Recherche | done | 2026-03-10 | researcher | Auto (research.md existe) | step_4: done, topics_researched: 6 (auth, endpoints, pagination, strategie, cas limites, decisions) |
|
||||||
|
| 5 | Roadmap | done | 2026-03-10 | - | Auto (Gitea milestones + issues created) | step_5: done, milestone: v1.0.0 (id:29), issues: #1-#4, labels: feature/bug/improvement/backlog |
|
||||||
|
|
||||||
|
## Phase 2 — DEV (v1.0.0)
|
||||||
|
|
||||||
|
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|
||||||
|
|---|-------|--------|------|-------------|------------|-------|
|
||||||
|
| 6 | Plan de version | done | 2026-03-10 | architect | Auto (plan avec phases, budget scope, inclusions/exclusions) | step_6: done, plan: docs/plans/v1.0.0-plan.md, phases: 2+1, gitea_milestone: exists (id:29) |
|
||||||
|
| 7 | Developpement | done | 2026-03-10 | builder | Auto (tests passent) | step_7: done, commits: 4, files_created: 7, files_modified: 1, tests: 35 passed |
|
||||||
|
| 8 | Audit + corrections | done | 2026-03-10 | reviewer + guardian + fixer | Auto (score 100, floor 50) | step_8: done, audit_initial: 81, audit_final: 97, rounds: 2, corrections: 5, remaining_findings: 1 (minor contextuel) |
|
||||||
|
|
||||||
|
## Phase 3 — PRE-RELEASE
|
||||||
|
|
||||||
|
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|
||||||
|
|---|-------|--------|------|-------------|------------|-------|
|
||||||
|
| 9 | Smoke test | done | 2026-03-10 | tester + checklist | Auto (tests E2E) + Manuel (checklist) | step_9: done, mode: cli, rounds: 1, fix: __main__.py manquant |
|
||||||
|
| 10 | Documentation | done | 2026-03-10 | documenter | Auto (README.md et CHANGELOG.md a jour) | step_10: done |
|
||||||
|
|
||||||
|
## Phase 4 — PUBLICATION
|
||||||
|
|
||||||
|
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|
||||||
|
|---|-------|--------|------|-------------|------------|-------|
|
||||||
|
| 11 | Release | done | 2026-03-10 | /release | Auto (release creee) | step_11: done, tag: v1.0.0 |
|
||||||
|
| 12 | Deploy (optionnel) | skipped | 2026-03-10 | script | Auto (health check OK) | Outil CLI local, pas de deploiement serveur |
|
||||||
|
|
||||||
|
## Phase 5 — POST-RELEASE
|
||||||
|
|
||||||
|
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|
||||||
|
|---|-------|--------|------|-------------|------------|-------|
|
||||||
|
| 13 | Retrospective | done | 2026-03-10 | - | Auto (metriques et MEMORY.md ecrits) | step_13: done, metrics_written: true, analysis_written: true, gitea_milestone: closed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version interrompue (hotfix)
|
||||||
|
- Version interrompue :
|
||||||
|
- Etape :
|
||||||
|
- Phase dev :
|
||||||
|
- Branche :
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions de workflow
|
||||||
|
|
||||||
|
| Date | Action | Raison |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 2026-03-10 | init v1.0.0 major-initial | Nouveau projet, workflow complet |
|
||||||
|
| 2026-03-10 | step 1 done | Discovery synthesis produite via /forge |
|
||||||
|
| 2026-03-10 | step 2 done | Structure projet creee via /forge |
|
||||||
|
| 2026-03-10 | step 3 done | descriptif.md complet (contexte, objectifs, perimetre, contraintes) |
|
||||||
|
| 2026-03-10 | step 4 done | Recherche API Gitea : endpoints, pagination, auth, cas limites |
|
||||||
|
| 2026-03-10 | step 5 done | Milestone v1.0.0 + 4 issues + 4 labels crees sur Gitea |
|
||||||
|
| 2026-03-10 | step 6 done | Plan v1.0.0 (2 phases dev + 1 finalisation), architecture, ADR-002/003 |
|
||||||
|
| 2026-03-10 | step 7 done | 4 commits, 35 tests passent, issues #1-#4 fermees |
|
||||||
|
| 2026-03-10 | step 8 done | Audit: reviewer 81→100, guardian 91→97, 5 corrections, score final 97 |
|
||||||
|
| 2026-03-10 | step 9 done | Smoke test CLI reel, 13 repos affiches, fix __main__.py, milestone dupliquee nettoyee |
|
||||||
|
| 2026-03-10 | step 10 done | README complet, CHANGELOG v1.0.0, version bump pyproject.toml |
|
||||||
|
| 2026-03-10 | step 11 done | Tag v1.0.0, release Gitea creee, push origin |
|
||||||
|
| 2026-03-10 | step 12 skipped | CLI local, pas de deploy |
|
||||||
|
| 2026-03-10 | step 11 done | Tag v1.0.0, release Gitea, push origin |
|
||||||
|
| 2026-03-10 | step 13 done | Retrospective, metriques, analyse, MEMORY.md, milestone fermee |
|
||||||
|
|
||||||
|
## Versions completees
|
||||||
|
|
||||||
|
| Version | Date debut | Date fin | Notes |
|
||||||
|
|---------|-----------|----------|-------|
|
||||||
|
| v1.0.0 | 2026-03-10 | 2026-03-10 | major-initial, 12/13 steps, audit 97, 37 tests |
|
||||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.eggs/
|
||||||
|
*.egg
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
19
CHANGELOG.md
Normal file
19
CHANGELOG.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-03-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Client API Gitea avec authentification par token et pagination automatique
|
||||||
|
- Collecteur de données avec dataclass `RepoData`
|
||||||
|
- Affichage Rich du dashboard avec tableau repos et section milestones
|
||||||
|
- Point d'entrée CLI `gitea-dashboard` avec configuration par variables d'environnement
|
||||||
|
- Indicateurs visuels pour les repos forks, archives et miroirs
|
||||||
|
- Gestion des erreurs réseau (connexion refusée, timeout, erreurs API)
|
||||||
|
- Masquage du token dans les messages d'erreur
|
||||||
49
CLAUDE.md
Normal file
49
CLAUDE.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# gitea-dashboard
|
||||||
|
|
||||||
|
Dashboard CLI Python affichant l'etat des repos Gitea (issues, releases, milestones).
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Langage** : Python 3.x
|
||||||
|
- **Dependances** : requests, rich
|
||||||
|
- **API** : Gitea REST v1
|
||||||
|
- **Instance** : http://192.168.0.106:3000
|
||||||
|
|
||||||
|
## Principes
|
||||||
|
|
||||||
|
1. **Lecture seule** — le dashboard ne modifie jamais de donnees Gitea
|
||||||
|
2. **Configuration externalisee** — token et URL en variables d'environnement, jamais dans le code
|
||||||
|
3. **Separation des responsabilites** — client API / formatage / point d'entree distincts
|
||||||
|
4. **Gestion gracieuse** — un repo sans release ou milestone ne casse pas l'affichage
|
||||||
|
|
||||||
|
## Points d'attention
|
||||||
|
|
||||||
|
- Ne jamais committer de token ou secret
|
||||||
|
- Gerer la pagination API Gitea (reponses potentiellement tronquees)
|
||||||
|
- Tester avec des repos sans release et sans milestone
|
||||||
|
- L'API Gitea peut varier selon la version — tester sur l'instance reelle
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installation
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Execution
|
||||||
|
gitea-dashboard
|
||||||
|
# ou
|
||||||
|
python -m gitea_dashboard
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
ruff check src/ tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables d'environnement
|
||||||
|
|
||||||
|
| Variable | Description | Defaut |
|
||||||
|
|----------|-------------|--------|
|
||||||
|
| `GITEA_URL` | URL de l'instance Gitea | http://192.168.0.106:3000 |
|
||||||
|
| `GITEA_TOKEN` | Token API Gitea | (requis) |
|
||||||
71
README.md
71
README.md
@@ -1,3 +1,72 @@
|
|||||||
# gitea-dashboard
|
# gitea-dashboard
|
||||||
|
|
||||||
CLI Python dashboard for Gitea repos status (issues, releases, milestones)
|
Dashboard CLI affichant en une commande l'état de tous les repos d'une instance Gitea : issues ouvertes, dernières releases et progression des milestones.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Python >= 3.10
|
||||||
|
- Accès à une instance Gitea avec un token API
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Le dashboard se configure via deux variables d'environnement :
|
||||||
|
|
||||||
|
| Variable | Description | Défaut |
|
||||||
|
|----------|-------------|--------|
|
||||||
|
| `GITEA_URL` | URL de l'instance Gitea | `http://192.168.0.106:3000` |
|
||||||
|
| `GITEA_TOKEN` | Token API Gitea (requis) | — |
|
||||||
|
|
||||||
|
Pour créer un token : Gitea > Settings > Applications > Generate Token.
|
||||||
|
|
||||||
|
Exemple de configuration dans votre shell :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITEA_URL=https://gitea.tsmse.fr
|
||||||
|
# Définir GITEA_TOKEN avec la valeur obtenue depuis Gitea > Settings > Applications
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitea-dashboard
|
||||||
|
# ou
|
||||||
|
python -m gitea_dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple de sortie
|
||||||
|
|
||||||
|
```
|
||||||
|
Gitea Dashboard
|
||||||
|
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Repo ┃ Issues ┃ Release ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ mon-projet │ 3 │ v1.2.0 (il y a 2j) │
|
||||||
|
│ autre-repo │ 0 │ — │
|
||||||
|
└─────────────────┴────────┴──────────────────────┘
|
||||||
|
|
||||||
|
Milestones
|
||||||
|
mon-projet / v2.0 : 3/5 (60%)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Développement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installer avec les dépendances de développement
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Lancer les tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Vérifier le style
|
||||||
|
ruff check src/ tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Usage personnel.
|
||||||
|
|||||||
48
docs/README.md
Normal file
48
docs/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!-- Type: reference (Diataxis). Style: index de navigation, structure de la doc. -->
|
||||||
|
|
||||||
|
# Documentation — gitea-dashboard
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── project/ # Pilotage projet
|
||||||
|
│ ├── descriptif.md # Brief initial (etape 3)
|
||||||
|
│ └── demandes.md # Inbox features (/workflow demande)
|
||||||
|
├── technical/ # Architecture et decisions
|
||||||
|
│ ├── ARCHITECTURE.md # Architecture technique (etape 6, architect)
|
||||||
|
│ ├── decisions.md # ADR (Architecture Decision Records)
|
||||||
|
│ └── research.md # Resultats de recherche (etape 4, researcher)
|
||||||
|
├── discovery/ # Livrables discovery (etape 1, majeur uniquement)
|
||||||
|
│ └── synthesis.md # Synthese d'interview
|
||||||
|
├── plans/ # Plans de version (etape 6)
|
||||||
|
│ └── vX.Y.Z-plan.md # Un plan par version (architect)
|
||||||
|
├── guides/ # Documentation utilisateur (optionnel)
|
||||||
|
│ └── [sujet].md # deployment.md, api-usage.md, getting-started.md
|
||||||
|
├── dev/ # Notes de developpement (optionnel)
|
||||||
|
│ └── [sujet].md # setup.md, conventions.md, troubleshooting.md
|
||||||
|
└── README.md # Ce fichier
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **MAJUSCULES.md** : documents officiels et de pilotage (ARCHITECTURE, CHANGELOG)
|
||||||
|
- **minuscules.md** : documents de travail et notes (descriptif, demandes, research, decisions)
|
||||||
|
- **Nommage fichiers** : minuscules, tirets pour separer les mots (deployment.md, api-usage.md)
|
||||||
|
- Les dossiers `guides/` et `dev/` sont crees quand le besoin apparait
|
||||||
|
- Chaque document a un emplacement unique — pas de doc ailleurs que dans cette arborescence
|
||||||
|
- **Roadmap** : Gitea milestones (pas de ROADMAP.md local)
|
||||||
|
- **Backlog** : Gitea issues avec label `backlog` (pas de BACKLOG.md local)
|
||||||
|
|
||||||
|
## Ou mettre ma doc ?
|
||||||
|
|
||||||
|
| Type de doc | Ou |
|
||||||
|
|-------------|-----|
|
||||||
|
| Guide utilisateur (install, usage, config) | `guides/` |
|
||||||
|
| Notes dev (setup, conventions, debug) | `dev/` |
|
||||||
|
| Architecture, ADR, recherche | `technical/` |
|
||||||
|
| Brief, demandes | `project/` |
|
||||||
|
| Plan de version | `plans/` |
|
||||||
|
| Synthese discovery | `discovery/` |
|
||||||
|
| Roadmap, jalons | Gitea milestones |
|
||||||
|
| Backlog, idees, dette technique | Gitea issues (label `backlog`) |
|
||||||
64
docs/analyse/gitea-dashboard-v1.0.0-2026-03-10.md
Normal file
64
docs/analyse/gitea-dashboard-v1.0.0-2026-03-10.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!-- Type: explanation (Diataxis). Style: discursif, retour d'experience, redige par documenter a l'etape 13. -->
|
||||||
|
|
||||||
|
# Analyse workflow — gitea-dashboard v1.0.0
|
||||||
|
|
||||||
|
**Projet** : gitea-dashboard
|
||||||
|
**Version** : v1.0.0
|
||||||
|
**Track** : major-initial
|
||||||
|
**Date** : 2026-03-10
|
||||||
|
**Duree** : 1 session
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metriques
|
||||||
|
|
||||||
|
| Metrique | Valeur |
|
||||||
|
|----------|--------|
|
||||||
|
| Fichiers source | 11 (5 modules + 6 tests) |
|
||||||
|
| Lignes de code | 958 |
|
||||||
|
| Tests | 37 |
|
||||||
|
| Couverture | non mesuree (pas de coverage configure) |
|
||||||
|
| Score audit initial | 81/100 (reviewer), 91/100 (guardian) |
|
||||||
|
| Score audit final | 100/100 (reviewer), 97/100 (guardian) |
|
||||||
|
| Rounds audit | 2 |
|
||||||
|
| Findings corriges | 5 |
|
||||||
|
| Findings restants | 1 (minor contextuel : pagination sans borne max) |
|
||||||
|
| Commits | 19 |
|
||||||
|
| Etapes effectuees | 12/13 |
|
||||||
|
| Etapes skippees | 12 (deploy — CLI local) |
|
||||||
|
| Agents utilises | researcher, architect, builder (x2), reviewer (x2), guardian (x2), fixer, documenter |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui a bien fonctionne
|
||||||
|
|
||||||
|
- **Pipeline complet en 1 session** : de la discovery a la release en une seule conversation, workflow fluide
|
||||||
|
- **Separation des agents efficace** : chaque agent a produit un livrable propre sans chevauchement
|
||||||
|
- **Audit adversarial productif** : 5 findings reels corriges (raise_for_status, timeout, exit codes, tests), score 81 → 100/97
|
||||||
|
- **Smoke test revelateur** : a detecte le __main__.py manquant et la milestone dupliquee
|
||||||
|
- **Integration Gitea MCP** : creation de milestone, issues, labels, release sans quitter le workflow
|
||||||
|
|
||||||
|
## Ce qui a mal fonctionne
|
||||||
|
|
||||||
|
- **Milestone creee en double** : l'architect a cree une 2e milestone v1.0.0 (id 30) alors qu'il en existait deja une (id 29). Nettoyage manuel necessaire.
|
||||||
|
- **Token API pour smoke test** : le premier token fourni etait invalide (401), necessite un 2e essai. L'URL par defaut (192.168.0.106) ne correspondait pas a l'URL publique (gitea.tsmse.fr).
|
||||||
|
- **__main__.py oublie** : le builder n'a pas cree le fichier necessaire pour `python -m gitea_dashboard`. Detecte au smoke test.
|
||||||
|
|
||||||
|
## Friction workflow
|
||||||
|
|
||||||
|
- **Steps 1-3 rapides mais ceremoniels** : pour un projet simple et bien defini, les etapes discovery/creation/specs pourraient etre fusionnees.
|
||||||
|
- **Pas de coverage** : pytest-cov n'est pas dans les deps dev, metriques de couverture absentes.
|
||||||
|
- **Duplication reviewer/guardian** : les 2 agents ont trouve les memes findings (raise_for_status, timeout). La deduplication est manuelle.
|
||||||
|
|
||||||
|
## Suggestions d'amelioration
|
||||||
|
|
||||||
|
- [projet] Ajouter pytest-cov dans les deps dev et configurer la couverture
|
||||||
|
- [projet] Mettre a jour l'URL par defaut vers https://gitea.tsmse.fr dans cli.py
|
||||||
|
- [generique] Le builder devrait verifier la presence de __main__.py quand pyproject.toml definit un entry point CLI
|
||||||
|
- [generique] Deduplication automatique des findings entre reviewer et guardian avant de les passer au fixer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexte projet
|
||||||
|
|
||||||
|
Projet Python simple (4 modules, 1 dataclass, API REST). Stack classique sans complexite particuliere. Le workflow major-initial est lourd pour ce type de projet mais a permis de structurer proprement la documentation et les decisions architecturales des le depart.
|
||||||
566
docs/analyse/workflow-analysis-v1.0.0.md
Normal file
566
docs/analyse/workflow-analysis-v1.0.0.md
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
# Analyse complete du workflow — gitea-dashboard v1.0.0
|
||||||
|
|
||||||
|
**Objectif** : documenter chaque etape du workflow macro major-initial tel qu'il s'est deroule, avec les agents, outils et decisions pris a chaque moment, pour pouvoir analyser et ameliorer le workflow.
|
||||||
|
|
||||||
|
**Projet** : gitea-dashboard (dashboard CLI Python pour Gitea)
|
||||||
|
**Track** : major-initial (1 → 13)
|
||||||
|
**Date** : 2026-03-10
|
||||||
|
**Duree totale** : ~1h17 (18h14 → 19h31)
|
||||||
|
**Resultat** : v1.0.0 released, 37 tests, audit 97/100
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table des matieres
|
||||||
|
|
||||||
|
1. [Vue chronologique](#1-vue-chronologique)
|
||||||
|
2. [Detail par etape](#2-detail-par-etape)
|
||||||
|
3. [Agents utilises](#3-agents-utilises)
|
||||||
|
4. [Outils MCP et integrations](#4-outils-mcp-et-integrations)
|
||||||
|
5. [Metriques finales](#5-metriques-finales)
|
||||||
|
6. [Analyse critique](#6-analyse-critique)
|
||||||
|
7. [Recommandations](#7-recommandations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vue chronologique
|
||||||
|
|
||||||
|
```
|
||||||
|
18h14 ████ Init + Steps 1-3 (framing) 8 min
|
||||||
|
18h22 ████████ Step 4 (recherche API) 17 min
|
||||||
|
18h39 ██ Steps 5-6 (roadmap + plan) 5 min
|
||||||
|
18h44 █████ Step 7 (developpement 4 modules) 11 min
|
||||||
|
18h55 ████ Step 8 (audit + corrections) 8 min
|
||||||
|
19h03 ██ Step 9 (smoke test) 5 min
|
||||||
|
19h08 ██████████ Step 10 (documentation) 21 min
|
||||||
|
19h29 █ Steps 11+13 (release + retro) 2 min
|
||||||
|
19h31 DONE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observation** : la documentation (step 10, 21 min) a pris plus de temps que le developpement (step 7, 11 min). Le framing (steps 1-3) et la release (steps 11-13) sont les phases les plus rapides.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Detail par etape
|
||||||
|
|
||||||
|
### Phase 1 — FRAMING (steps 1-5)
|
||||||
|
|
||||||
|
#### Step 1 — Discovery | 18h14-18h19
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `/forge` (mode new project, inclut discovery) |
|
||||||
|
| **Modele** | opus (orchestrateur principal) |
|
||||||
|
| **Action** | Interview reduite (contexte deja fourni par l'utilisateur), challenge de 2 hypotheses (requests vs MCP, filtre repos) |
|
||||||
|
| **Outils** | `AskUserQuestion` (1 question sur le filtre repos) |
|
||||||
|
| **Livrable** | `docs/discovery/synthesis.md` |
|
||||||
|
| **Decision** | Tout afficher sans filtre en v1 |
|
||||||
|
|
||||||
|
**Deroulement** : L'utilisateur avait deja defini clairement le besoin (stack, scope, API). La discovery a ete acceleree : 1 seule question posee au lieu des 5-8 prevues. 2 hypotheses challengees : (1) requests vs client MCP → requests confirme pour autonomie du CLI, (2) filtre par owner → differe.
|
||||||
|
|
||||||
|
**Analyse** : Pour un projet simple et bien defini, la discovery formelle est surdimensionnee. Le temps de redaction du synthesis.md ne se justifie pas quand l'utilisateur a deja tout explicite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2 — Creation projet | 18h19-18h21
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `/forge` (mode new project, suite) |
|
||||||
|
| **Modele** | opus (orchestrateur principal) |
|
||||||
|
| **Action** | Generation de la structure complete du projet |
|
||||||
|
| **Outils** | `Write` (14 fichiers), `Read` (7 templates), `Bash` (mkdir, git) |
|
||||||
|
| **Livrable** | Structure projet complete |
|
||||||
|
|
||||||
|
**Fichiers crees** :
|
||||||
|
- `CLAUDE.md`, `.gitignore`, `README.md`, `pyproject.toml`
|
||||||
|
- `src/gitea_dashboard/__init__.py`, `src/gitea_dashboard/cli.py` (squelette)
|
||||||
|
- `tests/__init__.py`
|
||||||
|
- `docs/` (7 fichiers : README, synthesis, descriptif, demandes, ARCHITECTURE, decisions, research)
|
||||||
|
|
||||||
|
**Incident** : Le hook `secret-scanner` a bloque l'ecriture du README.md car il contenait le mot "token" dans un exemple. Reformulation sans valeur d'exemple → OK.
|
||||||
|
|
||||||
|
**Analyse** : La generation depuis des templates est efficace et coherente. Le hook de securite est un bon garde-fou mais genere un faux positif ici.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 3 — Specs | 18h21
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | Aucun (validation automatique) |
|
||||||
|
| **Modele** | — |
|
||||||
|
| **Action** | Verification que `descriptif.md` contient les 4 sections requises |
|
||||||
|
| **Livrable** | `docs/project/descriptif.md` (deja cree au step 2) |
|
||||||
|
|
||||||
|
**Analyse** : Step valide automatiquement car le descriptif avait ete cree complet au step 2. Pas de travail supplementaire. Cette etape sert de checkpoint de qualite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4 — Recherche technique | 18h22-18h28
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `researcher` |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Investigation API Gitea REST v1 |
|
||||||
|
| **Outils** | `WebFetch` (swagger.json de l'instance), `Read`, `Write` |
|
||||||
|
| **Livrable** | `docs/technical/research.md` (164 lignes) |
|
||||||
|
| **Duree agent** | ~4.5 min (274s) |
|
||||||
|
|
||||||
|
**Sujets investigues** (6) :
|
||||||
|
1. Authentification (format header, methodes deprecated)
|
||||||
|
2. Endpoints necessaires (4 : /user/repos, /releases/latest, /milestones, /issues)
|
||||||
|
3. Pagination (page/limit, headers X-Total-Count et Link)
|
||||||
|
4. Strategie d'appels (cout en requetes : ceil(N/50) + 2N)
|
||||||
|
5. Cas limites (404 releases, tableaux vides, forks, archives, open_issues inclut PRs)
|
||||||
|
6. Decisions recommandees (endpoint repos, comptage issues, affichage forks)
|
||||||
|
|
||||||
|
**Analyse** : Etape tres productive. Le researcher a produit un document exhaustif qui a guide tout le developpement. La decouverte que `open_issues_count` inclut les PRs (avec `open_pr_counter` en champ separe) a evite un bug en production. Le swagger de l'instance reelle a ete utilise comme source primaire → donnees fiables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 5 — Roadmap | 18h28-18h39
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | Orchestrateur principal (pas d'agent dedie) |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Creation milestone + issues + labels sur Gitea |
|
||||||
|
| **Outils MCP** | `mcp__gitea__milestone_write`, `mcp__gitea__issue_write` (x4), `mcp__gitea__label_write` (x4), `mcp__gitea__label_read`, `mcp__gitea__issue_write.add_labels` (x4) |
|
||||||
|
| **Livrable** | Milestone v1.0.0 (id:29) + issues #1-#4 + 4 labels |
|
||||||
|
|
||||||
|
**Labels crees** :
|
||||||
|
|
||||||
|
| Label | ID | Couleur |
|
||||||
|
|-------|----|---------|
|
||||||
|
| feature | 58 | #0075ca |
|
||||||
|
| bug | 59 | #d73a4a |
|
||||||
|
| improvement | 60 | #a2eeef |
|
||||||
|
| backlog | 61 | #e4e669 |
|
||||||
|
|
||||||
|
**Issues crees** :
|
||||||
|
|
||||||
|
| # | Titre | Phase |
|
||||||
|
|---|-------|-------|
|
||||||
|
| #1 | Client API Gitea avec authentification et pagination | Phase 1 |
|
||||||
|
| #2 | Collecte des donnees : repos, issues, releases, milestones | Phase 1 |
|
||||||
|
| #3 | Affichage dashboard avec Rich (tableaux, couleurs) | Phase 2 |
|
||||||
|
| #4 | Point d'entree CLI et configuration | Phase 2 |
|
||||||
|
|
||||||
|
**Particularite MCP** : Le `issue_write(method="create")` de Gitea MCP ignore le parametre `labels`. Il faut un appel separe `issue_write(method="add_labels")` apres creation. Le workflow documente cette contrainte.
|
||||||
|
|
||||||
|
**Analyse** : L'integration MCP Gitea est fluide pour les operations CRUD simples. 13 appels MCP necessaires (1 milestone + 4 labels + 4 issues + 4 add_labels) pour une etape qui pourrait etre plus compacte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2 — DEV (steps 6-8)
|
||||||
|
|
||||||
|
#### Step 6 — Plan de version | 18h39-18h44
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `architect` |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Production du plan detaille avec phases, interfaces, budget de scope |
|
||||||
|
| **Outils** | `Read` (5 fichiers contexte), `Write` (3 fichiers) |
|
||||||
|
| **Livrable** | `docs/plans/v1.0.0-plan.md`, `docs/technical/ARCHITECTURE.md`, `docs/technical/decisions.md` (ADR-002, ADR-003) |
|
||||||
|
| **Duree agent** | ~3.2 min (194s) |
|
||||||
|
|
||||||
|
**Decisions architecturales** :
|
||||||
|
- **ADR-002** : 4 modules max (client / collector / display / cli)
|
||||||
|
- **ADR-003** : Pas de parallelisation en v1 (sequentiel, < 20 repos acceptable)
|
||||||
|
|
||||||
|
**Plan structure** :
|
||||||
|
- **Phase 1** : client.py + collector.py + 2 fichiers tests (fixes #1, #2)
|
||||||
|
- **Phase 2** : display.py + cli.py (modif) + 2 fichiers tests (fixes #3, #4)
|
||||||
|
- **Budget** : 8 fichiers max (4 modules + 4 tests)
|
||||||
|
|
||||||
|
**Interfaces definies** : signatures de `GiteaClient`, `RepoData` dataclass, `collect_all()`, `render_dashboard()` — le builder n'a pas eu a decider de l'API.
|
||||||
|
|
||||||
|
**Incident** : L'architect a cree une 2e milestone v1.0.0 (id:30) sur Gitea alors qu'il en existait deja une (id:29). Cause : pas de verification d'existence avant creation. Nettoyage fait au step 9.
|
||||||
|
|
||||||
|
**Analyse** : Le plan est tres detaille (295 lignes) et inclut meme les risques d'audit anticipes. Les interfaces pre-definies accelerent le travail du builder. La section "risques d'audit" s'est revelee presciente (3 des 4 risques identifies se sont retrouves dans les findings reels).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 7 — Developpement | 18h44-18h55
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `builder` (x2, une invocation par phase) |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Implementation TDD des 4 modules |
|
||||||
|
| **Outils** | `Read`, `Write`, `Edit`, `Bash` (pytest, pip install, ruff) |
|
||||||
|
| **Livrable** | 4 modules + 4 fichiers tests, 35 tests passent |
|
||||||
|
| **Duree agents** | Phase 1: ~4 min (239s) + Phase 2: ~3 min (181s) = ~7 min |
|
||||||
|
|
||||||
|
**Phase 1 — Client + Collecteur** (builder invocation 1) :
|
||||||
|
- `src/gitea_dashboard/client.py` (81 lignes) : GiteaClient, auth, pagination, get_repos, get_latest_release (None sur 404), get_milestones
|
||||||
|
- `src/gitea_dashboard/collector.py` (52 lignes) : dataclass RepoData, collect_all()
|
||||||
|
- `tests/test_client.py` (160 lignes) : 9 tests
|
||||||
|
- `tests/test_collector.py` (130 lignes) : 6 tests
|
||||||
|
- 2 commits : `feat(client): ... (fixes #1)` + `feat(collector): ... (fixes #2)`
|
||||||
|
- **15 tests passent**
|
||||||
|
|
||||||
|
**Correction pyproject.toml** : Le builder a detecte et corrige le build-backend (`_legacy:_Backend` → `build_meta`). Bug present depuis la creation au step 2.
|
||||||
|
|
||||||
|
**Phase 2 — Display + CLI** (builder invocation 2) :
|
||||||
|
- `src/gitea_dashboard/display.py` (128 lignes) : render_dashboard(), tableau Rich, milestones, indicateurs [F]/[A]/[M], dates relatives
|
||||||
|
- `src/gitea_dashboard/cli.py` (58 lignes) : main(), lecture env vars, pipeline, gestion erreurs, masquage secret
|
||||||
|
- `tests/test_display.py` (216 lignes) : 12 tests
|
||||||
|
- `tests/test_cli.py` (127 lignes) : 8 tests
|
||||||
|
- 2 commits : `feat(display): ... (fixes #3)` + `feat(cli): ... (fixes #4)`
|
||||||
|
- **35 tests passent** (15 + 20)
|
||||||
|
|
||||||
|
**Ratio tests/code** : 633 lignes de tests / 325 lignes de code = 1.95x.
|
||||||
|
|
||||||
|
**Analyse** : Le decoupage en 2 invocations du builder (1 par phase du plan) a bien fonctionne. Chaque invocation a produit du code fonctionnel avec tests. Le builder a suivi les interfaces definies par l'architect sans deviation. Temps effectif de codage : 7 min pour 958 lignes — tres rapide grace aux interfaces pre-definies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 8 — Audit + corrections | 18h55-19h03
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `reviewer` (x2), `guardian` (x2), `fixer` (x1) |
|
||||||
|
| **Modele** | reviewer/guardian: opus, fixer: sonnet |
|
||||||
|
| **Action** | Audit qualite 5 axes + securite OWASP, corrections, re-audit |
|
||||||
|
| **Outils** | `Read` (fichiers source + tests), `Edit`, `Bash` (pytest, pip-audit) |
|
||||||
|
| **Livrable** | 5 findings corriges, score 81 → 97 |
|
||||||
|
| **Duree agents** | Round 1: ~2.5 min (parallele) + fixer: ~3 min + Round 2: ~1.5 min (parallele) |
|
||||||
|
|
||||||
|
**Round 1 — Audit initial** (reviewer + guardian en parallele) :
|
||||||
|
|
||||||
|
*Reviewer (81/100)* — 7 findings :
|
||||||
|
|
||||||
|
| ID | Severite | Fichier | Probleme |
|
||||||
|
|----|----------|---------|----------|
|
||||||
|
| FINDING-001 | major (-10) | client.py:62 | raise_for_status() manquant dans get_latest_release |
|
||||||
|
| FINDING-002 | minor (-3) | cli.py:42-49 | return au lieu de sys.exit(1) sur erreurs |
|
||||||
|
| FINDING-003 | minor (-3) | client.py:17 | Session jamais fermee |
|
||||||
|
| FINDING-004 | minor (-3) | client.py:17 | Pas de timeout (except Timeout = code mort) |
|
||||||
|
| FINDING-005 | minor | pyproject.toml:7 | Version 0.0.0 vs v1.0.0 |
|
||||||
|
| FINDING-006 | minor | test_cli.py:55 | Test duplique sans assertion message |
|
||||||
|
| FINDING-007 | minor | test_cli.py:101 | Test masquage ne teste pas le mecanisme reel |
|
||||||
|
|
||||||
|
*Guardian (91/100)* — 3 findings :
|
||||||
|
|
||||||
|
| ID | Severite | Categorie | Probleme |
|
||||||
|
|----|----------|-----------|----------|
|
||||||
|
| SEC-001 | minor (-3) | OWASP-A02 | HTTP en clair (reseau local) |
|
||||||
|
| SEC-002 | minor (-3) | OWASP-A07 | Timeout manquant |
|
||||||
|
| SEC-003 | minor (-3) | OWASP-A07 | raise_for_status manquant |
|
||||||
|
|
||||||
|
**Convergence findings** : SEC-002 = FINDING-004, SEC-003 = FINDING-001. Les 2 agents ont trouve les memes problemes independamment.
|
||||||
|
|
||||||
|
**Fixer** — 5 corrections appliquees :
|
||||||
|
|
||||||
|
| Finding | Correction |
|
||||||
|
|---------|-----------|
|
||||||
|
| FINDING-001/SEC-003 | `resp.raise_for_status()` ajoute + test server error |
|
||||||
|
| FINDING-004/SEC-002 | `timeout: int = 30` dans constructeur + propagation + 2 tests |
|
||||||
|
| FINDING-002 | `sys.exit(1)` dans les 3 blocs except |
|
||||||
|
| FINDING-006 | Test duplique supprime, assertion message ajoutee |
|
||||||
|
| FINDING-007 | Test renforce avec exception contenant la valeur sensible |
|
||||||
|
|
||||||
|
**Non corriges (acceptes)** : FINDING-003 (session.close — negligeable pour CLI), FINDING-005 (version — bump au step 10), SEC-001 (HTTP local — reseau prive).
|
||||||
|
|
||||||
|
**Round 2 — Re-audit** (reviewer + guardian en parallele) :
|
||||||
|
- Reviewer : **100/100**, 0 findings, APPROVED
|
||||||
|
- Guardian : **97/100**, 1 finding minor (boucle pagination sans borne max — contextuel), APPROVED
|
||||||
|
- Score de reference : min(100, 97) = **97/100**
|
||||||
|
- Delta potentiel round 3 : <= 0 → boucle adversariale terminee
|
||||||
|
|
||||||
|
**Analyse** : L'audit adversarial est la phase la plus productive du workflow en termes de qualite. Le finding major (raise_for_status) aurait cause des erreurs silencieuses en production. Le timeout manquant rendait le catch Timeout inutile. La parallelisation reviewer/guardian est efficace (meme temps que 1 seul agent). Le fixer (sonnet) a corrige 5 findings en 3 min sans erreur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3 — PRE-RELEASE (steps 9-10)
|
||||||
|
|
||||||
|
#### Step 9 — Smoke test | 19h03-19h08
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | Orchestrateur principal (test CLI reel) |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Execution du CLI contre l'instance Gitea reelle |
|
||||||
|
| **Outils** | `Bash` (python3 -m gitea_dashboard), `AskUserQuestion`, `Write` (__main__.py), `mcp__gitea__milestone_read`, `mcp__gitea__milestone_write` (delete) |
|
||||||
|
| **Livrable** | Dashboard affiche avec 13 repos reels |
|
||||||
|
|
||||||
|
**Deroulement** :
|
||||||
|
|
||||||
|
1. Variable d'environnement `GITEA_TOKEN` non definie → demande a l'utilisateur
|
||||||
|
2. Premier essai : **401 Unauthorized** (valeur invalide)
|
||||||
|
3. Test curl : confirme que la valeur est invalide
|
||||||
|
4. Deuxieme essai : **erreur** `No module named gitea_dashboard.__main__`
|
||||||
|
5. **Bug decouvert** : `__main__.py` manquant → creation du fichier
|
||||||
|
6. Re-execution : **succes**, 13 repos affiches
|
||||||
|
7. **Bug donnees** : milestone v1.0.0 dupliquee (id 29 vide + id 30 avec issues) → suppression id 29
|
||||||
|
8. Re-execution : dashboard propre
|
||||||
|
|
||||||
|
**Checklist validee** :
|
||||||
|
- [x] 13 repos affiches
|
||||||
|
- [x] Issues comptees correctement (iot: 28, gitea-dashboard: 4)
|
||||||
|
- [x] Releases avec dates relatives, "—" si aucune
|
||||||
|
- [x] Milestones avec progression
|
||||||
|
- [x] Erreurs gerees
|
||||||
|
|
||||||
|
**Analyse** : Le smoke test a decouvert 2 vrais problemes : (1) `__main__.py` manquant — le builder ne l'a pas cree car ce n'etait pas dans les interfaces du plan, et (2) la milestone dupliquee creee par l'architect. Sans smoke test, la v1.0.0 aurait ete inutilisable via `python -m`. C'est l'etape qui justifie le plus son existence dans le workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 10 — Documentation | 19h08-19h29
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | `documenter` |
|
||||||
|
| **Modele** | sonnet |
|
||||||
|
| **Action** | Mise a jour README.md complet + creation CHANGELOG.md + version bump |
|
||||||
|
| **Outils** | `Read`, `Write`, `Edit` |
|
||||||
|
| **Livrable** | README.md (72 lignes), CHANGELOG.md (19 lignes), pyproject.toml (version 1.0.0) |
|
||||||
|
| **Duree agent** | ~1.6 min (95s) |
|
||||||
|
|
||||||
|
**README.md** : description, prerequis, installation, configuration (variables d'env avec tableau), usage, exemple de sortie, section developpement.
|
||||||
|
|
||||||
|
**CHANGELOG.md** : format Keep a Changelog, 7 entrees "Added" pour v1.0.0.
|
||||||
|
|
||||||
|
**Incident** : Le hook secret-scanner a necessite une reformulation de la section configuration (eviter d'ecrire une valeur d'exemple pour la variable sensible).
|
||||||
|
|
||||||
|
**Analyse** : 21 min pour la documentation semble long, mais c'est principalement du temps d'attente MCP/hooks. Le documenter (sonnet) a produit le contenu en 95s.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4 — PUBLICATION (steps 11-12)
|
||||||
|
|
||||||
|
#### Step 11 — Release | 19h29-19h31
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | Orchestrateur principal |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Push, tag git, release Gitea |
|
||||||
|
| **Outils** | `Bash` (git push, git tag, git push tag), `mcp__gitea__create_release` |
|
||||||
|
| **Livrable** | Tag v1.0.0, release Gitea avec notes |
|
||||||
|
|
||||||
|
**Sequence** :
|
||||||
|
1. `git push origin main` (18 commits)
|
||||||
|
2. `git tag -a v1.0.0` + `git push origin v1.0.0`
|
||||||
|
3. `mcp__gitea__create_release` avec body detaille (Added + Details)
|
||||||
|
|
||||||
|
**Analyse** : Etape rapide et sans friction. L'integration MCP pour la release Gitea evite de passer par l'interface web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 12 — Deploy | skipped
|
||||||
|
|
||||||
|
Raison : outil CLI local, pas de deploiement serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5 — POST-RELEASE (step 13)
|
||||||
|
|
||||||
|
#### Step 13 — Retrospective | 19h31
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Skill/Agent** | Orchestrateur principal |
|
||||||
|
| **Modele** | opus |
|
||||||
|
| **Action** | Collecte metriques, analyse, MEMORY.md, fermeture milestone |
|
||||||
|
| **Outils** | `Bash` (find, wc, pytest, git log), `Write` (analyse + MEMORY.md), `mcp__gitea__milestone_write` (close), `Edit` (workflow-progress.md) |
|
||||||
|
| **Livrable** | `docs/analyse/gitea-dashboard-v1.0.0-2026-03-10.md`, MEMORY.md |
|
||||||
|
|
||||||
|
**Analyse** : La retrospective est principalement un travail de synthese. La fermeture automatique du milestone Gitea via MCP est un bon point d'integration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Agents utilises
|
||||||
|
|
||||||
|
### Tableau recapitulatif
|
||||||
|
|
||||||
|
| Agent | Modele | Invocations | Role | Duree totale | Read-only |
|
||||||
|
|-------|--------|-------------|------|-------------|-----------|
|
||||||
|
| **researcher** | opus | 1 | Investigation API Gitea | ~4.5 min | oui |
|
||||||
|
| **architect** | opus | 1 | Plan v1.0.0, architecture, ADR | ~3.2 min | non |
|
||||||
|
| **builder** | opus | 2 | Implementation phase 1 + phase 2 | ~7 min | non |
|
||||||
|
| **reviewer** | opus | 2 | Audit qualite round 1 + 2 | ~2 min | oui |
|
||||||
|
| **guardian** | opus | 2 | Audit securite round 1 + 2 | ~2.5 min | oui |
|
||||||
|
| **fixer** | sonnet | 1 | Corrections findings | ~3 min | non |
|
||||||
|
| **documenter** | sonnet | 1 | README + CHANGELOG | ~1.6 min | non |
|
||||||
|
| **scout** | sonnet | 1 | Exploration pour cette analyse | ~1.3 min | oui |
|
||||||
|
|
||||||
|
**Total** : 11 invocations d'agents, ~25 min de temps agent cumule.
|
||||||
|
|
||||||
|
### Agents non utilises
|
||||||
|
|
||||||
|
| Agent | Raison |
|
||||||
|
|-------|--------|
|
||||||
|
| **orchestrator** | < 6 fichiers, pas necessaire |
|
||||||
|
| **tester** | Smoke test fait manuellement par l'orchestrateur principal |
|
||||||
|
|
||||||
|
### Repartition par modele
|
||||||
|
|
||||||
|
| Modele | Agents | Invocations | Usage |
|
||||||
|
|--------|--------|-------------|-------|
|
||||||
|
| **opus** | researcher, architect, builder, reviewer, guardian | 8 | Taches complexes (recherche, planification, implementation, audit) |
|
||||||
|
| **sonnet** | fixer, documenter, scout | 3 | Taches plus simples (corrections, docs, exploration) |
|
||||||
|
| **haiku** | — | 0 | Jamais utilise (interdit par les regles) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Outils MCP et integrations
|
||||||
|
|
||||||
|
### Outils Gitea MCP
|
||||||
|
|
||||||
|
| Outil | Appels | Etape | Usage |
|
||||||
|
|-------|--------|-------|-------|
|
||||||
|
| `mcp__gitea__milestone_write` (create) | 1 (+1 par architect) | 5 | Creation milestone v1.0.0 |
|
||||||
|
| `mcp__gitea__milestone_write` (delete) | 1 | 9 | Suppression milestone dupliquee |
|
||||||
|
| `mcp__gitea__milestone_write` (edit) | 1 | 13 | Fermeture milestone |
|
||||||
|
| `mcp__gitea__milestone_read` (list) | 1 | 9 | Verification doublons |
|
||||||
|
| `mcp__gitea__issue_write` (create) | 4 | 5 | Creation issues #1-#4 |
|
||||||
|
| `mcp__gitea__issue_write` (add_labels) | 4 | 5 | Ajout labels aux issues |
|
||||||
|
| `mcp__gitea__label_write` (create) | 4 | 5 | Creation labels |
|
||||||
|
| `mcp__gitea__label_read` (list) | 1 | 5 | Verification labels existants |
|
||||||
|
| `mcp__gitea__create_release` | 1 | 11 | Creation release v1.0.0 |
|
||||||
|
| `mcp__gitea__get_me` | 1 | 9 | Verification connectivite |
|
||||||
|
| **Total** | **19 appels MCP** | | |
|
||||||
|
|
||||||
|
### Outils Claude Code
|
||||||
|
|
||||||
|
| Outil | Usage principal |
|
||||||
|
|-------|----------------|
|
||||||
|
| `Read` | Lecture fichiers (templates, code, config) — utilise massivement |
|
||||||
|
| `Write` | Creation de nouveaux fichiers (docs, source, tests) |
|
||||||
|
| `Edit` | Modifications ciblees (workflow-progress.md surtout) |
|
||||||
|
| `Bash` | Git operations, pytest, pip, curl, find/wc |
|
||||||
|
| `Glob` | Recherche de fichiers par pattern |
|
||||||
|
| `AskUserQuestion` | 3 questions posees (filtre repos, smoke test mode, valeur sensible) |
|
||||||
|
| `Agent` | 11 invocations (voir section 3) |
|
||||||
|
| `Skill` | 2 invocations (/workflow, /forge) |
|
||||||
|
| `ToolSearch` | Chargement dynamique des outils MCP et AskUserQuestion |
|
||||||
|
|
||||||
|
### Hooks actifs
|
||||||
|
|
||||||
|
| Hook | Declenchements | Effet |
|
||||||
|
|------|---------------|-------|
|
||||||
|
| `secret-scanner` | 2 | Blocage ecriture README (faux positif), reformulation necessaire |
|
||||||
|
| `UserPromptSubmit` | Chaque message | Validation OK |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Metriques finales
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
| Metrique | Valeur |
|
||||||
|
|----------|--------|
|
||||||
|
| Fichiers source Python | 6 (5 modules + __main__.py) |
|
||||||
|
| Fichiers tests Python | 5 (4 test_*.py + __init__.py) |
|
||||||
|
| Lignes code source | 325 |
|
||||||
|
| Lignes tests | 633 |
|
||||||
|
| Ratio tests/code | 1.95x |
|
||||||
|
| Tests | 37 |
|
||||||
|
| Couverture | Non mesuree |
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
| Metrique | Valeur |
|
||||||
|
|----------|--------|
|
||||||
|
| Commits | 20 |
|
||||||
|
| Etapes effectuees | 12/13 |
|
||||||
|
| Etapes skippees | 1 (deploy) |
|
||||||
|
| Agents invoques | 11 |
|
||||||
|
| Appels MCP Gitea | 19 |
|
||||||
|
| Questions posees a l'utilisateur | 3 |
|
||||||
|
| Incidents/bugs | 3 (milestone doublon, __main__.py, valeur invalide) |
|
||||||
|
|
||||||
|
### Audit
|
||||||
|
|
||||||
|
| Metrique | Round 1 | Round 2 |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| Reviewer | 81/100 | 100/100 |
|
||||||
|
| Guardian | 91/100 | 97/100 |
|
||||||
|
| Findings totaux | 10 (1 major, 9 minor) | 1 (minor contextuel) |
|
||||||
|
| Findings corriges | 5 | — |
|
||||||
|
| Findings acceptes | 3 | 1 |
|
||||||
|
| Findings differes | 1 (version pyproject) | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Analyse critique
|
||||||
|
|
||||||
|
### Forces du workflow
|
||||||
|
|
||||||
|
**1. Pipeline structure et tracable**
|
||||||
|
Chaque etape produit un livrable identifie, commite, et logue dans workflow-progress.md. La tracabilite est excellente : on peut suivre chaque decision, chaque agent, chaque commit.
|
||||||
|
|
||||||
|
**2. Audit adversarial efficace**
|
||||||
|
Le round 1 a trouve un vrai bug major (raise_for_status manquant) et un code mort (timeout). Le mecanisme reviewer + guardian en parallele puis fixer est productif. La convergence des findings entre les 2 agents valide la pertinence.
|
||||||
|
|
||||||
|
**3. Smoke test revelateur**
|
||||||
|
A decouvert 2 bugs que ni le builder ni l'audit n'avaient trouves : __main__.py manquant et milestone dupliquee. Justifie pleinement son existence.
|
||||||
|
|
||||||
|
**4. Integration Gitea MCP fluide**
|
||||||
|
Issues, milestones, labels, releases — tout cree depuis le workflow sans quitter le terminal. 19 appels MCP sans erreur technique.
|
||||||
|
|
||||||
|
**5. Separation des responsabilites agents**
|
||||||
|
Chaque agent a un perimetre clair. Le builder n'a pas eu a decider de l'architecture (c'est l'architect), le fixer n'a pas eu a trouver les bugs (c'est le reviewer/guardian).
|
||||||
|
|
||||||
|
### Faiblesses du workflow
|
||||||
|
|
||||||
|
**1. Ceremonie excessive pour un petit projet**
|
||||||
|
13 etapes pour 325 lignes de code. Les steps 1-3 (framing) produisent 7 fichiers de documentation pour un projet dont le besoin tenait en 3 phrases. Le ratio documentation/code est disproportionne.
|
||||||
|
|
||||||
|
**2. Duplication reviewer/guardian**
|
||||||
|
Les 2 agents ont trouve les memes findings (raise_for_status, timeout). Pas de deduplication avant le fixer. 2 rapports a lire et comparer manuellement.
|
||||||
|
|
||||||
|
**3. L'architect cree des doublons**
|
||||||
|
La milestone v1.0.0 a ete creee 2 fois (orchestrateur au step 5 + architect au step 6). Pas de verification d'existence avant creation.
|
||||||
|
|
||||||
|
**4. __main__.py oublie**
|
||||||
|
Ni le plan de l'architect ni le builder n'ont prevu `__main__.py`. Le plan definissait un entry point dans pyproject.toml mais pas le support de `python -m`. Decouvert au smoke test.
|
||||||
|
|
||||||
|
**5. Pas de couverture de code**
|
||||||
|
37 tests mais aucune mesure de couverture. `pytest-cov` absent des deps dev. Impossible de savoir si les tests couvrent tout le code.
|
||||||
|
|
||||||
|
**6. URL par defaut incorrecte**
|
||||||
|
Le code utilise `http://192.168.0.106:3000` mais l'instance repond sur `https://gitea.tsmse.fr`. Le smoke test a necessite un override manuel. Non corrige.
|
||||||
|
|
||||||
|
### Temps par categorie
|
||||||
|
|
||||||
|
| Categorie | Duree | % du total |
|
||||||
|
|-----------|-------|------------|
|
||||||
|
| Framing (steps 1-3) | 8 min | 10% |
|
||||||
|
| Recherche (step 4) | 17 min | 22% |
|
||||||
|
| Planification (steps 5-6) | 5 min | 6% |
|
||||||
|
| Developpement (step 7) | 11 min | 14% |
|
||||||
|
| Audit (step 8) | 8 min | 10% |
|
||||||
|
| Test (step 9) | 5 min | 6% |
|
||||||
|
| Documentation (step 10) | 21 min | 27% |
|
||||||
|
| Release + retro (steps 11-13) | 2 min | 3% |
|
||||||
|
|
||||||
|
**Observation** : la documentation (27%) et la recherche (22%) consomment presque la moitie du temps. Le developpement proprement dit ne represente que 14%.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Recommandations
|
||||||
|
|
||||||
|
### Pour le workflow (generique)
|
||||||
|
|
||||||
|
| # | Recommandation | Impact | Effort |
|
||||||
|
|---|---------------|--------|--------|
|
||||||
|
| 1 | **Deduplication findings** : fusionner les rapports reviewer/guardian avant le fixer | Evite le travail en double | Moyen |
|
||||||
|
| 2 | **Fusion steps 1-3** pour les projets simples (< 10 fichiers estimes) | Reduit la ceremonie | Faible |
|
||||||
|
| 3 | **Checklist builder** : verifier __main__.py si pyproject.toml a un entry point CLI | Evite un bug recurrent | Faible |
|
||||||
|
| 4 | **Verification existence** : avant de creer un milestone/issue Gitea, verifier s'il existe deja | Evite les doublons | Faible |
|
||||||
|
| 5 | **Coverage obligatoire** : ajouter pytest-cov au template pyproject.toml des projets Python | Metriques manquantes | Faible |
|
||||||
|
| 6 | **Mode "light"** : pour les projets < 500 lignes, proposer un workflow reduit (6-7-8-11-13) | Adapte la ceremonie au projet | Moyen |
|
||||||
|
|
||||||
|
### Pour ce projet (gitea-dashboard)
|
||||||
|
|
||||||
|
| # | Recommandation | Priorite |
|
||||||
|
|---|---------------|----------|
|
||||||
|
| 1 | Ajouter pytest-cov et mesurer la couverture | Haute |
|
||||||
|
| 2 | Changer l'URL par defaut vers `https://gitea.tsmse.fr` | Moyenne |
|
||||||
|
| 3 | Ajouter une limite max de pages dans la pagination | Basse |
|
||||||
|
| 4 | Parallelisation des appels API (ThreadPoolExecutor) pour v1.1 | Basse |
|
||||||
51
docs/discovery/synthesis.md
Normal file
51
docs/discovery/synthesis.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!-- Type: explanation (Diataxis). Style: discursif, synthese des besoins et decisions, produit par /forge --discovery. -->
|
||||||
|
|
||||||
|
# Discovery Synthesis — gitea-dashboard
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Besoin d'un outil de supervision rapide pour l'instance Gitea personnelle.
|
||||||
|
Actuellement, il faut naviguer dans l'interface web repo par repo pour voir
|
||||||
|
l'etat des issues, releases et milestones. Un dashboard CLI permet d'avoir
|
||||||
|
une vue d'ensemble en une commande.
|
||||||
|
|
||||||
|
## Utilisateurs cibles
|
||||||
|
|
||||||
|
Utilisateur unique (admin de l'instance Gitea). Usage en terminal, execution
|
||||||
|
ponctuelle pour avoir un snapshot de l'etat des repos.
|
||||||
|
|
||||||
|
## Besoins identifies
|
||||||
|
|
||||||
|
### Fonctionnels
|
||||||
|
- Lister tous les repos de l'utilisateur
|
||||||
|
- Afficher le nombre d'issues ouvertes par repo
|
||||||
|
- Afficher la derniere release par repo (tag, date)
|
||||||
|
- Afficher l'etat des milestones (ouvertes, progression)
|
||||||
|
- Formatage terminal lisible et structure (tableaux, couleurs)
|
||||||
|
|
||||||
|
### Non-fonctionnels
|
||||||
|
- Temps de reponse acceptable (< 10s pour une dizaine de repos)
|
||||||
|
- Code maintenable, teste, structure proprement
|
||||||
|
- Configuration externalisee (URL, token)
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- API Gitea REST a http://192.168.0.106:3000
|
||||||
|
- Authentification par token API (variable d'environnement)
|
||||||
|
- Python 3.x avec requests et rich
|
||||||
|
- Affichage unique (pas de mode watch/refresh)
|
||||||
|
- Pas de filtre par owner/org en v1 — tous les repos
|
||||||
|
|
||||||
|
## Decisions prises
|
||||||
|
|
||||||
|
| Decision | Justification |
|
||||||
|
|----------|---------------|
|
||||||
|
| Python + requests | Stack simple, maitrisee, suffisante pour des appels REST |
|
||||||
|
| rich pour le formatage | Tableaux, couleurs, mise en page terminal de qualite |
|
||||||
|
| Token en variable d'env | Securite : pas de secret dans le code ou les fichiers |
|
||||||
|
| Pas de filtre en v1 | Nombre de repos limite, simplifier le scope initial |
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
- Pagination API : verifier si l'API Gitea pagine les resultats (a traiter en recherche step 4)
|
||||||
|
- Gestion des repos sans release ou sans milestone (affichage gracieux)
|
||||||
294
docs/plans/v1.0.0-plan.md
Normal file
294
docs/plans/v1.0.0-plan.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<!-- Type: reference (Diataxis). Style: factuel, structure par phases, actionnable par le builder. -->
|
||||||
|
|
||||||
|
# Plan de version v1.0.0 — gitea-dashboard
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Livrer un dashboard CLI fonctionnel qui affiche en une commande l'etat de tous les repos Gitea : issues ouvertes, derniere release, milestones. Premiere version utilisable.
|
||||||
|
|
||||||
|
## Track
|
||||||
|
|
||||||
|
**Major initial** (v1.0.0, nouveau projet) : 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> (12) -> 13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Budget de scope
|
||||||
|
|
||||||
|
| Critere | Valeur |
|
||||||
|
|---------|--------|
|
||||||
|
| Max fichiers par phase | 4 |
|
||||||
|
| Total fichiers estimes | 8 (4 modules + 4 tests) |
|
||||||
|
|
||||||
|
### Inclus
|
||||||
|
|
||||||
|
- Client API Gitea avec auth token et pagination
|
||||||
|
- Collecte des donnees : repos, issues (hors PRs), derniere release, milestones ouvertes
|
||||||
|
- Affichage Rich : tableau repos + section milestones + indicateurs visuels
|
||||||
|
- Point d'entree CLI avec configuration par variables d'environnement
|
||||||
|
- Gestion des cas limites (repos sans release, sans milestone, forks, archives)
|
||||||
|
|
||||||
|
### Exclus
|
||||||
|
|
||||||
|
- Interface web ou GUI
|
||||||
|
- Mode watch / rafraichissement automatique
|
||||||
|
- Filtrage par owner/organisation
|
||||||
|
- Modification de donnees Gitea (lecture seule)
|
||||||
|
- Notifications ou alertes
|
||||||
|
- Framework CLI (argparse, click, typer)
|
||||||
|
|
||||||
|
### Differe (v1.1+)
|
||||||
|
|
||||||
|
- Parallelisation des appels API (ThreadPoolExecutor)
|
||||||
|
- Filtrage/tri des repos (par nom, activite, owner)
|
||||||
|
- Cache des reponses API
|
||||||
|
- Options CLI (--format, --filter, --sort)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Etapes skippees
|
||||||
|
|
||||||
|
| Etape | Nom | Raison |
|
||||||
|
|-------|-----|--------|
|
||||||
|
| 12 | Deploy | Outil CLI local, pas de deploiement serveur |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 : Client API et collecte de donnees
|
||||||
|
|
||||||
|
**Goal** : Disposer d'un client API Gitea fonctionnel et d'un collecteur qui agrege les donnees de tous les repos.
|
||||||
|
|
||||||
|
**Issues Gitea** : fixes #1, fixes #2
|
||||||
|
|
||||||
|
### Fichiers
|
||||||
|
|
||||||
|
| Action | Fichier | Modifications | Cross-references |
|
||||||
|
|--------|---------|---------------|------------------|
|
||||||
|
| Create | `src/gitea_dashboard/client.py` | Client API Gitea : auth, requetes GET, pagination | `collector.py` (consommateur) |
|
||||||
|
| Create | `src/gitea_dashboard/collector.py` | Collecte et agregation des donnees repos | `client.py` (dependance), `display.py` (producteur de donnees) |
|
||||||
|
| Create | `tests/test_client.py` | Tests unitaires du client API avec mocks | `client.py` |
|
||||||
|
| Create | `tests/test_collector.py` | Tests unitaires du collecteur avec mocks | `collector.py` |
|
||||||
|
|
||||||
|
### Interfaces
|
||||||
|
|
||||||
|
#### client.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GiteaClient:
|
||||||
|
"""Client API Gitea en lecture seule."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, token: str) -> None:
|
||||||
|
"""Initialise le client avec l'URL de base et le token API."""
|
||||||
|
|
||||||
|
def get_repos(self) -> list[dict]:
|
||||||
|
"""Retourne tous les repos de l'utilisateur (pagination automatique).
|
||||||
|
Endpoint: GET /api/v1/user/repos
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_latest_release(self, owner: str, repo: str) -> dict | None:
|
||||||
|
"""Retourne la derniere release du repo, ou None si aucune.
|
||||||
|
Endpoint: GET /api/v1/repos/{owner}/{repo}/releases/latest
|
||||||
|
Gere HTTP 404 en retournant None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_milestones(self, owner: str, repo: str) -> list[dict]:
|
||||||
|
"""Retourne les milestones ouvertes du repo.
|
||||||
|
Endpoint: GET /api/v1/repos/{owner}/{repo}/milestones?state=open
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Methode privee interne pour la pagination :
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_paginated(self, endpoint: str, params: dict | None = None) -> list[dict]:
|
||||||
|
"""Requete GET avec pagination automatique.
|
||||||
|
Boucle tant que len(page) == limit (50).
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### collector.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class RepoData:
|
||||||
|
"""Donnees agregees d'un repo."""
|
||||||
|
name: str
|
||||||
|
full_name: str
|
||||||
|
description: str
|
||||||
|
open_issues: int # open_issues_count - open_pr_counter
|
||||||
|
is_fork: bool
|
||||||
|
is_archived: bool
|
||||||
|
is_mirror: bool
|
||||||
|
latest_release: dict | None # {tag_name, published_at} ou None
|
||||||
|
milestones: list[dict] # [{title, open_issues, closed_issues, due_on}]
|
||||||
|
|
||||||
|
def collect_all(client: GiteaClient) -> list[RepoData]:
|
||||||
|
"""Collecte les donnees de tous les repos.
|
||||||
|
Pour chaque repo : enrichit avec release et milestones.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comportement attendu
|
||||||
|
|
||||||
|
1. `GiteaClient("http://192.168.0.106:3000", "token123")` cree un client
|
||||||
|
2. `client.get_repos()` retourne la liste complete (pagination transparente si > 50 repos)
|
||||||
|
3. `client.get_latest_release("admin", "mon-repo")` retourne `{"tag_name": "v1.0", "published_at": "..."}` ou `None`
|
||||||
|
4. `collect_all(client)` retourne une liste de `RepoData` pour chaque repo, meme ceux sans release ou milestone
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `test_client.py` : mock de `requests.Session` pour tester auth header, pagination (1 page, 2 pages), 404 sur release
|
||||||
|
- `test_collector.py` : mock de `GiteaClient` pour tester l'agregation, les cas limites (repo sans release, sans milestone, fork, archive)
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
Le client peut interroger l'API Gitea et le collecteur produit une liste de `RepoData` prete pour l'affichage. Les tests passent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 : Affichage Rich et point d'entree CLI
|
||||||
|
|
||||||
|
**Goal** : Afficher les donnees collectees dans un dashboard terminal lisible et fournir le point d'entree executable.
|
||||||
|
|
||||||
|
**Issues Gitea** : fixes #3, fixes #4
|
||||||
|
|
||||||
|
### Fichiers
|
||||||
|
|
||||||
|
| Action | Fichier | Modifications | Cross-references |
|
||||||
|
|--------|---------|---------------|------------------|
|
||||||
|
| Create | `src/gitea_dashboard/display.py` | Formatage et affichage Rich | `collector.py` (consomme `RepoData`) |
|
||||||
|
| Modify | `src/gitea_dashboard/cli.py` | Orchestration : config -> client -> collecte -> affichage | `client.py`, `collector.py`, `display.py` |
|
||||||
|
| Create | `tests/test_display.py` | Tests du formatage (capture console Rich) | `display.py` |
|
||||||
|
| Create | `tests/test_cli.py` | Tests d'integration du point d'entree | `cli.py` |
|
||||||
|
|
||||||
|
### Interfaces
|
||||||
|
|
||||||
|
#### display.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
def render_dashboard(repos: list[RepoData], console: Console | None = None) -> None:
|
||||||
|
"""Affiche le dashboard complet dans le terminal.
|
||||||
|
|
||||||
|
- Tableau principal : nom repo, indicateurs (fork/archive/mirror),
|
||||||
|
issues ouvertes, derniere release (tag + date relative)
|
||||||
|
- Section milestones : par repo ayant des milestones,
|
||||||
|
nom, progression (closed/total), date echeance
|
||||||
|
|
||||||
|
Le parametre console permet l'injection pour les tests.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cli.py (modification)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Point d'entree principal.
|
||||||
|
|
||||||
|
1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
|
||||||
|
2. Cree le GiteaClient
|
||||||
|
3. Collecte les donnees via collect_all()
|
||||||
|
4. Affiche via render_dashboard()
|
||||||
|
5. Gere les erreurs : config manquante, connexion refusee, timeout
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comportement attendu
|
||||||
|
|
||||||
|
Execution nominale :
|
||||||
|
```
|
||||||
|
$ export GITEA_TOKEN=abc123
|
||||||
|
$ gitea-dashboard
|
||||||
|
|
||||||
|
Gitea Dashboard
|
||||||
|
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Repo ┃ Issues ┃ Release ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ mon-projet │ 3 │ v1.2.0 (il y a 2j) │
|
||||||
|
│ autre-repo [F] │ 0 │ — │
|
||||||
|
│ archive [A] │ 1 │ v0.1.0 (il y a 6m) │
|
||||||
|
└──────────────────┴────────┴─────────────────────┘
|
||||||
|
|
||||||
|
Milestones
|
||||||
|
mon-projet / v2.0 : 3/5 (60%) — echeance 2026-04-01
|
||||||
|
```
|
||||||
|
|
||||||
|
Erreur de configuration :
|
||||||
|
```
|
||||||
|
$ gitea-dashboard
|
||||||
|
Erreur : GITEA_TOKEN non defini. Exportez la variable d'environnement.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `test_display.py` : capture de la sortie Rich via `Console(file=StringIO())`, verification du contenu (noms de repos, indicateurs, gestion des valeurs None)
|
||||||
|
- `test_cli.py` : mock des variables d'environnement, mock du client API, verification des messages d'erreur
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
`gitea-dashboard` est executable, affiche un dashboard lisible, et gere proprement les erreurs. Tous les tests passent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3 : Smoke test et documentation
|
||||||
|
|
||||||
|
**Goal** : Valider le fonctionnement reel sur l'instance Gitea et documenter le projet.
|
||||||
|
|
||||||
|
**Dependances** : phases 1 et 2 terminees, acces a l'instance Gitea (192.168.0.106:3000)
|
||||||
|
|
||||||
|
**Composants cles** :
|
||||||
|
- Test E2E manuel sur l'instance reelle
|
||||||
|
- Checklist de validation (repos avec/sans release, avec/sans milestone, forks)
|
||||||
|
- README.md a jour (installation, usage, configuration)
|
||||||
|
- CHANGELOG.md pour la v1.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture des modules
|
||||||
|
|
||||||
|
| Module | Responsabilite | Dependances |
|
||||||
|
|--------|---------------|-------------|
|
||||||
|
| `client.py` | Communication API Gitea (auth, requetes, pagination) | `requests` |
|
||||||
|
| `collector.py` | Agregation des donnees de tous les repos | `client.py` |
|
||||||
|
| `display.py` | Formatage et rendu terminal | `rich`, `collector.py` (types) |
|
||||||
|
| `cli.py` | Point d'entree, configuration, orchestration | tous les modules |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risques d'audit
|
||||||
|
|
||||||
|
| Zone | Risque | Severite estimee |
|
||||||
|
|------|--------|-----------------|
|
||||||
|
| `client.py` — pagination | Boucle infinie si l'API repond toujours `limit` elements | major |
|
||||||
|
| `client.py` — gestion erreurs | Erreurs HTTP non-404 non gerees (500, timeout, connexion) | major |
|
||||||
|
| `cli.py` — token en clair | Affichage accidentel du token dans les logs/erreurs | critical |
|
||||||
|
| `collector.py` — types | Champs API manquants ou renommes entre versions Gitea | minor |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues Gitea rattachees
|
||||||
|
|
||||||
|
| Issue | Titre | Phase |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| [#1](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/1) | Client API Gitea avec authentification et pagination | Phase 1 |
|
||||||
|
| [#2](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/2) | Collecte des donnees : repos, issues, releases, milestones | Phase 1 |
|
||||||
|
| [#3](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/3) | Affichage dashboard avec Rich (tableaux, couleurs) | Phase 2 |
|
||||||
|
| [#4](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/4) | Point d'entree CLI et configuration | Phase 2 |
|
||||||
|
|
||||||
|
**Milestone** : [v1.0.0](https://gitea.tsmse.fr/admin/gitea-dashboard/milestone/30)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependances
|
||||||
|
|
||||||
|
| Dependance | Type | Version |
|
||||||
|
|------------|------|---------|
|
||||||
|
| Python | Runtime | >= 3.10 |
|
||||||
|
| requests | Librairie | >= 2.31 |
|
||||||
|
| rich | Librairie | >= 13.0 |
|
||||||
|
| pytest | Dev | >= 7.0 |
|
||||||
|
| ruff | Dev | >= 0.4 |
|
||||||
|
| Instance Gitea | Service externe | 1.25.1 a 192.168.0.106:3000 |
|
||||||
23
docs/project/demandes.md
Normal file
23
docs/project/demandes.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- Type: reference (Diataxis). Style: inbox format libre, traite par /workflow demande. -->
|
||||||
|
|
||||||
|
# Demandes — gitea-dashboard
|
||||||
|
|
||||||
|
> **Note** : ce projet utilise Gitea, les issues Gitea sont la source de verite.
|
||||||
|
> Ce fichier sert de fallback.
|
||||||
|
> Utiliser `/workflow demande` pour traiter les demandes.
|
||||||
|
|
||||||
|
## En attente de classification
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Classifiees — en attente
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Terminees
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Demandes ecosystem
|
||||||
|
|
||||||
|
-
|
||||||
50
docs/project/descriptif.md
Normal file
50
docs/project/descriptif.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!-- Type: reference (Diataxis). Style: factuel, fige apres creation. Capture l'intention originale. -->
|
||||||
|
|
||||||
|
# Descriptif — gitea-dashboard
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Supervision de l'instance Gitea personnelle (192.168.0.106:3000).
|
||||||
|
Pas de vue d'ensemble disponible sans naviguer repo par repo dans l'interface web.
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
- Afficher en une commande l'etat de tous les repos Gitea
|
||||||
|
- Visualiser les issues ouvertes, dernieres releases et milestones
|
||||||
|
- Fournir un output terminal lisible et structure
|
||||||
|
|
||||||
|
## Perimetre
|
||||||
|
|
||||||
|
### Inclus
|
||||||
|
|
||||||
|
- Connexion API Gitea avec authentification token
|
||||||
|
- Liste de tous les repos de l'utilisateur
|
||||||
|
- Nombre d'issues ouvertes par repo
|
||||||
|
- Derniere release par repo (tag + date)
|
||||||
|
- Etat des milestones (nom, progression open/closed)
|
||||||
|
- Formatage rich (tableaux, couleurs)
|
||||||
|
|
||||||
|
### Exclus
|
||||||
|
|
||||||
|
- Interface web ou GUI
|
||||||
|
- Mode watch / rafraichissement automatique
|
||||||
|
- Filtrage par owner/organisation
|
||||||
|
- Modification de donnees (lecture seule)
|
||||||
|
- Notifications ou alertes
|
||||||
|
|
||||||
|
## Utilisateurs cibles
|
||||||
|
|
||||||
|
Administrateur unique de l'instance Gitea. Usage terminal.
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- Dependances : requests, rich
|
||||||
|
- API Gitea REST v1
|
||||||
|
- Token en variable d'environnement (GITEA_TOKEN)
|
||||||
|
- Instance locale : http://192.168.0.106:3000
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- API Gitea : https://gitea.io/en-us/ (documentation Swagger disponible sur l'instance)
|
||||||
|
- Rich : https://rich.readthedocs.io/
|
||||||
145
docs/technical/ARCHITECTURE.md
Normal file
145
docs/technical/ARCHITECTURE.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<!-- Type: reference (Diataxis). Style: factuel, exhaustif, structure par le code. Pas de tutoriel ici. -->
|
||||||
|
|
||||||
|
# Architecture — gitea-dashboard
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
gitea-dashboard
|
||||||
|
==============
|
||||||
|
|
||||||
|
Terminal Application Gitea API
|
||||||
|
-------- ----------- ---------
|
||||||
|
|
||||||
|
+------------------+
|
||||||
|
$ gitea-dashboard | cli.py |
|
||||||
|
------------------->| - lit env vars |
|
||||||
|
| - configure |
|
||||||
|
| - gere erreurs |
|
||||||
|
+--------+---------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+------------------+
|
||||||
|
| collector.py |
|
||||||
|
| - orchestre la |
|
||||||
|
| collecte | +------------------+
|
||||||
|
| - agrege en |---->| client.py |
|
||||||
|
| RepoData | | - auth token |-----> GET /api/v1/user/repos
|
||||||
|
+--------+---------+ | - pagination |-----> GET /repos/{o}/{r}/releases/latest
|
||||||
|
| | - erreurs HTTP |-----> GET /repos/{o}/{r}/milestones
|
||||||
|
v +------------------+
|
||||||
|
+------------------+
|
||||||
|
| display.py |
|
||||||
|
| - tableau repos |
|
||||||
|
<-------------------| - milestones |
|
||||||
|
Output Rich | - indicateurs |
|
||||||
|
(tableaux, +------------------+
|
||||||
|
couleurs)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composants
|
||||||
|
|
||||||
|
### cli.py — Point d'entree
|
||||||
|
|
||||||
|
Responsabilite : orchestration du flux principal.
|
||||||
|
|
||||||
|
- Lit les variables d'environnement `GITEA_URL` (defaut: `http://192.168.0.106:3000`) et `GITEA_TOKEN` (requis)
|
||||||
|
- Valide la configuration, affiche un message d'erreur clair si manquante
|
||||||
|
- Cree le `GiteaClient`, lance la collecte, passe les resultats a l'affichage
|
||||||
|
- Gere les erreurs globales (connexion refusee, timeout, erreurs API)
|
||||||
|
- Enregistre comme entry point : `gitea-dashboard = "gitea_dashboard.cli:main"`
|
||||||
|
|
||||||
|
### client.py — Client API Gitea
|
||||||
|
|
||||||
|
Responsabilite : communication avec l'API REST Gitea.
|
||||||
|
|
||||||
|
- Authentification via header `Authorization: token <GITEA_TOKEN>`
|
||||||
|
- Methode interne `_get_paginated()` pour la pagination transparente (limit=50, boucle tant que page pleine)
|
||||||
|
- Methodes publiques : `get_repos()`, `get_latest_release(owner, repo)`, `get_milestones(owner, repo)`
|
||||||
|
- Gestion du 404 sur `/releases/latest` (retourne `None`)
|
||||||
|
- Utilise `requests.Session` pour reutiliser les connexions HTTP
|
||||||
|
|
||||||
|
### collector.py — Collecteur de donnees
|
||||||
|
|
||||||
|
Responsabilite : agregation des donnees de tous les repos.
|
||||||
|
|
||||||
|
- Definit le dataclass `RepoData` qui normalise les donnees d'un repo
|
||||||
|
- Calcul des issues ouvertes reelles : `open_issues_count - open_pr_counter`
|
||||||
|
- Pour chaque repo : enrichit avec la derniere release et les milestones ouvertes
|
||||||
|
- Fonction principale `collect_all(client) -> list[RepoData]`
|
||||||
|
|
||||||
|
### display.py — Affichage Rich
|
||||||
|
|
||||||
|
Responsabilite : formatage et rendu terminal.
|
||||||
|
|
||||||
|
- Tableau principal : nom du repo, indicateurs visuels ([F]ork, [A]rchive, [M]irror), issues ouvertes, derniere release (tag + date)
|
||||||
|
- Section milestones : affichee uniquement pour les repos ayant des milestones ouvertes, avec progression (closed/total)
|
||||||
|
- Accepte un parametre `Console` optionnel pour l'injection dans les tests
|
||||||
|
- Fonction principale `render_dashboard(repos, console=None)`
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
gitea-dashboard/
|
||||||
|
CLAUDE.md # Instructions projet
|
||||||
|
pyproject.toml # Config build, deps, entry point
|
||||||
|
README.md # Documentation utilisateur
|
||||||
|
src/
|
||||||
|
gitea_dashboard/
|
||||||
|
__init__.py # Docstring du package
|
||||||
|
cli.py # Point d'entree, config, orchestration
|
||||||
|
client.py # Client API Gitea (auth, pagination)
|
||||||
|
collector.py # Agregation des donnees repos
|
||||||
|
display.py # Formatage Rich (tableaux, couleurs)
|
||||||
|
tests/
|
||||||
|
__init__.py
|
||||||
|
test_client.py # Tests client API (mocks requests)
|
||||||
|
test_collector.py # Tests collecteur (mocks client)
|
||||||
|
test_display.py # Tests affichage (capture console)
|
||||||
|
test_cli.py # Tests integration CLI (mocks env)
|
||||||
|
docs/
|
||||||
|
plans/
|
||||||
|
v1.0.0-plan.md # Plan de version
|
||||||
|
technical/
|
||||||
|
ARCHITECTURE.md # Ce fichier
|
||||||
|
decisions.md # ADR
|
||||||
|
research.md # Recherche technique API
|
||||||
|
discovery/
|
||||||
|
synthesis.md # Synthese discovery
|
||||||
|
project/
|
||||||
|
descriptif.md # Descriptif du projet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flux de donnees principal
|
||||||
|
|
||||||
|
```
|
||||||
|
1. cli.main()
|
||||||
|
|
|
||||||
|
+-- Lit os.environ["GITEA_TOKEN"] et os.environ.get("GITEA_URL", default)
|
||||||
|
|
|
||||||
|
+-- GiteaClient(url, token)
|
||||||
|
|
|
||||||
|
+-- collect_all(client)
|
||||||
|
| |
|
||||||
|
| +-- client.get_repos() -> list[dict] (pagine)
|
||||||
|
| |
|
||||||
|
| +-- Pour chaque repo:
|
||||||
|
| +-- client.get_latest_release(owner, name) -> dict | None
|
||||||
|
| +-- client.get_milestones(owner, name) -> list[dict]
|
||||||
|
| +-- RepoData(...)
|
||||||
|
|
|
||||||
|
+-- render_dashboard(repos)
|
||||||
|
|
|
||||||
|
+-- Rich Table (repos)
|
||||||
|
+-- Rich Table (milestones)
|
||||||
|
+-- Console.print()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Decisions architecturales
|
||||||
|
|
||||||
|
Les decisions sont tracees dans `docs/technical/decisions.md` (format ADR).
|
||||||
|
|
||||||
|
Decisions cles pour v1.0.0 :
|
||||||
|
- **ADR-001** : Stack Python + requests + rich
|
||||||
|
- **ADR-002** : 4 modules maximum (client, collector, display, cli)
|
||||||
|
- **ADR-003** : Pas de parallelisation en v1 (sequentiel, plus simple a deboguer)
|
||||||
48
docs/technical/decisions.md
Normal file
48
docs/technical/decisions.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!-- Type: reference (Diataxis). Style: factuel, format ADR Nygard (Contexte/Decision/Consequences). Jamais supprimer un ADR. -->
|
||||||
|
|
||||||
|
# Architecture Decision Records — gitea-dashboard
|
||||||
|
|
||||||
|
## ADR-001 : Stack Python + requests + rich
|
||||||
|
|
||||||
|
**Date** : 2026-03-10
|
||||||
|
**Statut** : accepte
|
||||||
|
|
||||||
|
**Contexte** : Besoin d'un outil CLI de dashboard pour Gitea. Choix du langage et des librairies.
|
||||||
|
|
||||||
|
**Decision** : Python avec requests pour les appels API et rich pour le formatage terminal.
|
||||||
|
|
||||||
|
**Consequences** :
|
||||||
|
- Stack simple et maitrisee par l'utilisateur
|
||||||
|
- Pas de framework CLI lourd (argparse suffit si besoin)
|
||||||
|
- rich offre des tableaux et couleurs sans configuration complexe
|
||||||
|
- Dependance a requests (pas de client async, acceptable pour un affichage unique)
|
||||||
|
|
||||||
|
## ADR-002 : 4 modules maximum (client, collector, display, cli)
|
||||||
|
|
||||||
|
**Date** : 2026-03-10
|
||||||
|
**Statut** : accepte
|
||||||
|
|
||||||
|
**Contexte** : Definir la granularite des modules Python pour un projet simple (dashboard CLI).
|
||||||
|
|
||||||
|
**Decision** : Limiter a 4 modules avec des responsabilites claires : `client.py` (API), `collector.py` (agregation), `display.py` (formatage), `cli.py` (entree).
|
||||||
|
|
||||||
|
**Consequences** :
|
||||||
|
- Separation des responsabilites sans over-engineering
|
||||||
|
- Chaque module est testable independamment
|
||||||
|
- Un fichier = une responsabilite = un jeu de tests
|
||||||
|
- Si le projet grandit, chaque module peut evoluer independamment
|
||||||
|
|
||||||
|
## ADR-003 : Pas de parallelisation en v1
|
||||||
|
|
||||||
|
**Date** : 2026-03-10
|
||||||
|
**Statut** : accepte
|
||||||
|
|
||||||
|
**Contexte** : La collecte de donnees necessite N appels par repo (release + milestones). La parallelisation (ThreadPoolExecutor) est possible mais ajoute de la complexite.
|
||||||
|
|
||||||
|
**Decision** : V1 en sequentiel. La parallelisation est differee a v1.1+.
|
||||||
|
|
||||||
|
**Consequences** :
|
||||||
|
- Code plus simple a ecrire, deboguer et tester
|
||||||
|
- Temps de reponse acceptable pour < 20 repos (estimee < 10s)
|
||||||
|
- Pas de problemes de concurrence
|
||||||
|
- Facile a ajouter plus tard sans changer les interfaces (le collecteur est le seul point d'appel)
|
||||||
164
docs/technical/research.md
Normal file
164
docs/technical/research.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<!-- Type: explanation (Diataxis). Style: discursif, comparaisons argumentees, sources citees. -->
|
||||||
|
|
||||||
|
# Recherche technique — gitea-dashboard
|
||||||
|
|
||||||
|
Date : 2026-03-10 | Gitea 1.25.1 | Source : swagger.v1.json + docs.gitea.com
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Le dashboard CLI doit afficher pour chaque repo Gitea : le nombre d'issues ouvertes, la derniere release, et l'etat des milestones. Cette recherche identifie les endpoints API necessaires, la strategie de pagination, le format d'authentification, et les cas limites.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Authentification
|
||||||
|
|
||||||
|
### Format recommande
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: token <GITEA_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header `Authorization` avec le prefixe `token ` (suivi d'un espace) est la methode recommandee. Les anciennes methodes (query parameter `?token=` ou `?access_token=`) sont **deprecated depuis Gitea 1.23** et seront supprimees.
|
||||||
|
|
||||||
|
**Source** : schema `securityDefinitions` du swagger + https://docs.gitea.com/development/api-usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Endpoints API necessaires
|
||||||
|
|
||||||
|
### 2.1 Liste des repos de l'utilisateur
|
||||||
|
|
||||||
|
**Endpoint** : `GET /api/v1/user/repos`
|
||||||
|
|
||||||
|
Retourne les repos dont l'utilisateur authentifie est proprietaire.
|
||||||
|
|
||||||
|
| Parametre | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `page` | integer | Numero de page (defaut : 1) |
|
||||||
|
| `limit` | integer | Elements par page (defaut : 50, max : 50) |
|
||||||
|
|
||||||
|
**Reponse** : Array de `Repository`
|
||||||
|
|
||||||
|
**Champs cles** :
|
||||||
|
|
||||||
|
| Champ | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `id` | int64 | Identifiant unique |
|
||||||
|
| `name` | string | Nom du repo |
|
||||||
|
| `full_name` | string | `owner/name` |
|
||||||
|
| `description` | string | Description |
|
||||||
|
| `fork` | boolean | Est un fork |
|
||||||
|
| `archived` | boolean | Est archive |
|
||||||
|
| `mirror` | boolean | Est un miroir |
|
||||||
|
| `open_issues_count` | int64 | Issues ouvertes (inclut les PRs) |
|
||||||
|
| `open_pr_counter` | int64 | PRs ouvertes (champ separe) |
|
||||||
|
| `release_counter` | int64 | Nombre total de releases |
|
||||||
|
| `owner.login` | string | Login du proprietaire |
|
||||||
|
| `updated_at` | datetime | Derniere mise a jour |
|
||||||
|
|
||||||
|
**Alternative** : `GET /api/v1/repos/search` retourne tous les repos visibles (pas seulement ceux de l'utilisateur). Reponse enveloppee dans `{"ok": true, "data": [...]}`.
|
||||||
|
|
||||||
|
### 2.2 Derniere release par repo
|
||||||
|
|
||||||
|
**Endpoint** : `GET /api/v1/repos/{owner}/{repo}/releases/latest`
|
||||||
|
|
||||||
|
Retourne la release la plus recente (hors draft et pre-release). Un seul objet, pas de pagination.
|
||||||
|
|
||||||
|
**Champs cles** :
|
||||||
|
|
||||||
|
| Champ | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `tag_name` | string | Nom du tag git |
|
||||||
|
| `name` | string | Titre de la release |
|
||||||
|
| `published_at` | datetime | Date de publication |
|
||||||
|
| `prerelease` | boolean | Est une pre-release |
|
||||||
|
|
||||||
|
**Retourne HTTP 404** si le repo n'a aucune release.
|
||||||
|
|
||||||
|
### 2.3 Milestones par repo
|
||||||
|
|
||||||
|
**Endpoint** : `GET /api/v1/repos/{owner}/{repo}/milestones`
|
||||||
|
|
||||||
|
| Parametre | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `state` | string | `open`, `closed`, `all` |
|
||||||
|
| `page` | integer | Numero de page |
|
||||||
|
| `limit` | integer | Elements par page |
|
||||||
|
|
||||||
|
**Champs cles** :
|
||||||
|
|
||||||
|
| Champ | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `title` | string | Nom du milestone |
|
||||||
|
| `state` | StateType | `open` ou `closed` |
|
||||||
|
| `open_issues` | int64 | Issues ouvertes dans le milestone |
|
||||||
|
| `closed_issues` | int64 | Issues fermees dans le milestone |
|
||||||
|
| `due_on` | datetime | Date d'echeance |
|
||||||
|
|
||||||
|
Le modele contient deja `open_issues` et `closed_issues` — progression calculable sans appel supplementaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Pagination
|
||||||
|
|
||||||
|
| Parametre | Defaut | Maximum |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| `page` | 1 | - |
|
||||||
|
| `limit` | 50 | 50 |
|
||||||
|
|
||||||
|
### Headers de reponse
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `X-Total-Count` | Nombre total d'elements |
|
||||||
|
| `Link` | URLs de navigation (`rel="next"`, `rel="last"`) |
|
||||||
|
|
||||||
|
### Strategie recommandee
|
||||||
|
|
||||||
|
1. Premiere requete avec `?limit=50&page=1`
|
||||||
|
2. Lire `X-Total-Count` dans les headers
|
||||||
|
3. Si `total > limit` : calculer le nombre de pages, lancer les requetes suivantes
|
||||||
|
4. Alternative simple : boucler tant que `len(results) == limit`
|
||||||
|
|
||||||
|
**Source** : https://docs.gitea.com/development/api-usage (section Pagination)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Strategie d'appels
|
||||||
|
|
||||||
|
Pour N repos :
|
||||||
|
|
||||||
|
| Donnee | Appels | Endpoint |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| Liste des repos | ceil(N/50) | `GET /user/repos` |
|
||||||
|
| Derniere release | N | `GET /repos/{o}/{r}/releases/latest` |
|
||||||
|
| Milestones | N | `GET /repos/{o}/{r}/milestones` |
|
||||||
|
| **Total** | **ceil(N/50) + 2N** | |
|
||||||
|
|
||||||
|
Issues ouvertes : derivees de `open_issues_count - open_pr_counter` du modele Repository (zero appel supplementaire).
|
||||||
|
|
||||||
|
**Parallelisation** : `concurrent.futures.ThreadPoolExecutor` avec 5-10 workers. La lib `requests` est thread-safe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cas limites
|
||||||
|
|
||||||
|
| Cas | Comportement API | Gestion |
|
||||||
|
|-----|-----------------|---------|
|
||||||
|
| Repo sans release | `/releases/latest` retourne 404 | Capturer, afficher "—" |
|
||||||
|
| Repo sans milestone | `/milestones` retourne `[]` | Tableau vide, rien a afficher |
|
||||||
|
| Repo sans issues | `open_issues_count` = 0 | Comportement standard |
|
||||||
|
| Repo fork | `fork` = true | Afficher avec indicateur visuel |
|
||||||
|
| Repo archive | `archived` = true | Afficher avec indicateur visuel |
|
||||||
|
| `open_issues_count` inclut PRs | Herite de GitHub | Calculer `open_issues_count - open_pr_counter` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Decisions recommandees
|
||||||
|
|
||||||
|
| # | Decision | Recommandation | Justification |
|
||||||
|
|---|----------|---------------|---------------|
|
||||||
|
| 1 | Endpoint repos | `GET /user/repos` | Plus simple, reponse directe, suffisant pour v1 |
|
||||||
|
| 2 | Comptage issues | `open_issues_count - open_pr_counter` | Zero appel supplementaire |
|
||||||
|
| 3 | Forks/archives | Tout afficher avec indicateurs | Exhaustivite, pas de filtre en v1 |
|
||||||
|
| 4 | Parallelisation | ThreadPoolExecutor (5 workers) | Acceptable pour < 200 repos |
|
||||||
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "gitea-dashboard"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "CLI dashboard for Gitea repos status"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.31",
|
||||||
|
"rich>=13.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"ruff>=0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
gitea-dashboard = "gitea_dashboard.cli:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
src = ["src", "tests"]
|
||||||
|
line-length = 100
|
||||||
1
src/gitea_dashboard/__init__.py
Normal file
1
src/gitea_dashboard/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Gitea Dashboard — CLI dashboard for Gitea repos status."""
|
||||||
5
src/gitea_dashboard/__main__.py
Normal file
5
src/gitea_dashboard/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Allow running with python -m gitea_dashboard."""
|
||||||
|
|
||||||
|
from gitea_dashboard.cli import main
|
||||||
|
|
||||||
|
main()
|
||||||
58
src/gitea_dashboard/cli.py
Normal file
58
src/gitea_dashboard/cli.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Point d'entree pour le CLI gitea-dashboard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from gitea_dashboard.client import GiteaClient
|
||||||
|
from gitea_dashboard.collector import collect_all
|
||||||
|
from gitea_dashboard.display import render_dashboard
|
||||||
|
|
||||||
|
_DEFAULT_URL = "http://192.168.0.106:3000"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Point d'entree principal.
|
||||||
|
|
||||||
|
1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
|
||||||
|
2. Cree le GiteaClient
|
||||||
|
3. Collecte les donnees via collect_all()
|
||||||
|
4. Affiche via render_dashboard()
|
||||||
|
5. Gere les erreurs : config manquante, connexion refusee, timeout
|
||||||
|
"""
|
||||||
|
console = Console(stderr=True)
|
||||||
|
|
||||||
|
token = os.environ.get("GITEA_TOKEN")
|
||||||
|
if not token:
|
||||||
|
console.print(
|
||||||
|
"[red]Erreur : GITEA_TOKEN non defini. Exportez la variable d'environnement.[/red]"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
url = os.environ.get("GITEA_URL", _DEFAULT_URL)
|
||||||
|
|
||||||
|
client = GiteaClient(url, token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
repos = collect_all(client)
|
||||||
|
except requests.ConnectionError:
|
||||||
|
console.print("[red]Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
except requests.Timeout:
|
||||||
|
console.print(
|
||||||
|
"[red]Erreur : delai d'attente depasse. Le serveur Gitea ne repond pas.[/red]"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
# Ne jamais afficher le token dans les messages d'erreur
|
||||||
|
msg = str(exc)
|
||||||
|
if token in msg:
|
||||||
|
msg = msg.replace(token, "***")
|
||||||
|
console.print(f"[red]Erreur API : {msg}[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
render_dashboard(repos)
|
||||||
81
src/gitea_dashboard/client.py
Normal file
81
src/gitea_dashboard/client.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"""Client API Gitea en lecture seule."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaClient:
|
||||||
|
"""Client API Gitea en lecture seule.
|
||||||
|
|
||||||
|
Utilise requests.Session pour reutiliser les connexions HTTP.
|
||||||
|
Authentification via header Authorization: token <TOKEN>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_PAGE_LIMIT = 50
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, token: str, timeout: int = 30) -> None:
|
||||||
|
"""Initialise le client avec l'URL de base et le token API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: URL de base de l'instance Gitea.
|
||||||
|
token: Token API pour l'authentification.
|
||||||
|
timeout: Delai maximum en secondes pour chaque requete (defaut: 30).
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.timeout = timeout
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers["Authorization"] = f"token {token}"
|
||||||
|
|
||||||
|
def _get_paginated(self, endpoint: str, params: dict | None = None) -> list[dict]:
|
||||||
|
"""Requete GET avec pagination automatique.
|
||||||
|
|
||||||
|
Boucle tant que len(page) == limit (50).
|
||||||
|
"""
|
||||||
|
all_items: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
merged_params = dict(params) if params else {}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
merged_params["limit"] = self._PAGE_LIMIT
|
||||||
|
merged_params["page"] = page
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
resp = self.session.get(url, params=merged_params, timeout=self.timeout)
|
||||||
|
resp.raise_for_status()
|
||||||
|
items = resp.json()
|
||||||
|
all_items.extend(items)
|
||||||
|
if len(items) < self._PAGE_LIMIT:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return all_items
|
||||||
|
|
||||||
|
def get_repos(self) -> list[dict]:
|
||||||
|
"""Retourne tous les repos de l'utilisateur (pagination automatique).
|
||||||
|
|
||||||
|
Endpoint: GET /api/v1/user/repos
|
||||||
|
"""
|
||||||
|
return self._get_paginated("/api/v1/user/repos")
|
||||||
|
|
||||||
|
def get_latest_release(self, owner: str, repo: str) -> dict | None:
|
||||||
|
"""Retourne la derniere release du repo, ou None si aucune.
|
||||||
|
|
||||||
|
Endpoint: GET /api/v1/repos/{owner}/{repo}/releases/latest
|
||||||
|
Gere HTTP 404 en retournant None.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/releases/latest"
|
||||||
|
resp = self.session.get(url, timeout=self.timeout)
|
||||||
|
if resp.status_code == 404:
|
||||||
|
return None
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def get_milestones(self, owner: str, repo: str) -> list[dict]:
|
||||||
|
"""Retourne les milestones ouvertes du repo.
|
||||||
|
|
||||||
|
Endpoint: GET /api/v1/repos/{owner}/{repo}/milestones?state=open
|
||||||
|
"""
|
||||||
|
return self._get_paginated(
|
||||||
|
f"/api/v1/repos/{owner}/{repo}/milestones",
|
||||||
|
params={"state": "open"},
|
||||||
|
)
|
||||||
52
src/gitea_dashboard/collector.py
Normal file
52
src/gitea_dashboard/collector.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Collecte et agregation des donnees repos Gitea."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from gitea_dashboard.client import GiteaClient
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RepoData:
|
||||||
|
"""Donnees agregees d'un repo."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
full_name: str
|
||||||
|
description: str
|
||||||
|
open_issues: int # open_issues_count - open_pr_counter
|
||||||
|
is_fork: bool
|
||||||
|
is_archived: bool
|
||||||
|
is_mirror: bool
|
||||||
|
latest_release: dict | None # {tag_name, published_at} ou None
|
||||||
|
milestones: list[dict] # [{title, open_issues, closed_issues, due_on}]
|
||||||
|
|
||||||
|
|
||||||
|
def collect_all(client: GiteaClient) -> list[RepoData]:
|
||||||
|
"""Collecte les donnees de tous les repos.
|
||||||
|
|
||||||
|
Pour chaque repo : enrichit avec release et milestones.
|
||||||
|
Calcule open_issues = open_issues_count - open_pr_counter.
|
||||||
|
"""
|
||||||
|
repos = client.get_repos()
|
||||||
|
result: list[RepoData] = []
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
owner = repo["owner"]["login"]
|
||||||
|
name = repo["name"]
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
RepoData(
|
||||||
|
name=name,
|
||||||
|
full_name=repo["full_name"],
|
||||||
|
description=repo.get("description", "") or "",
|
||||||
|
open_issues=repo["open_issues_count"] - repo["open_pr_counter"],
|
||||||
|
is_fork=repo["fork"],
|
||||||
|
is_archived=repo["archived"],
|
||||||
|
is_mirror=repo["mirror"],
|
||||||
|
latest_release=client.get_latest_release(owner, name),
|
||||||
|
milestones=client.get_milestones(owner, name),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
128
src/gitea_dashboard/display.py
Normal file
128
src/gitea_dashboard/display.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
"""Formatage et affichage Rich du dashboard Gitea."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from gitea_dashboard.collector import RepoData
|
||||||
|
|
||||||
|
|
||||||
|
def _format_repo_name(repo: RepoData) -> str:
|
||||||
|
"""Formate le nom du repo avec les indicateurs visuels."""
|
||||||
|
indicators = []
|
||||||
|
if repo.is_fork:
|
||||||
|
indicators.append("[F]")
|
||||||
|
if repo.is_archived:
|
||||||
|
indicators.append("[A]")
|
||||||
|
if repo.is_mirror:
|
||||||
|
indicators.append("[M]")
|
||||||
|
|
||||||
|
if indicators:
|
||||||
|
return f"{repo.name} {' '.join(indicators)}"
|
||||||
|
return repo.name
|
||||||
|
|
||||||
|
|
||||||
|
def _format_relative_date(iso_date: str) -> str:
|
||||||
|
"""Convertit une date ISO en date relative lisible."""
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
delta = now - dt
|
||||||
|
|
||||||
|
days = delta.days
|
||||||
|
if days < 0:
|
||||||
|
return "dans le futur"
|
||||||
|
if days == 0:
|
||||||
|
return "aujourd'hui"
|
||||||
|
if days == 1:
|
||||||
|
return "il y a 1j"
|
||||||
|
if days < 30:
|
||||||
|
return f"il y a {days}j"
|
||||||
|
months = days // 30
|
||||||
|
if months < 12:
|
||||||
|
return f"il y a {months}m"
|
||||||
|
years = days // 365
|
||||||
|
return f"il y a {years}a"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_release(release: dict | None) -> str:
|
||||||
|
"""Formate la release pour l'affichage."""
|
||||||
|
if release is None:
|
||||||
|
return "\u2014"
|
||||||
|
|
||||||
|
tag = release.get("tag_name", "")
|
||||||
|
published = release.get("published_at", "")
|
||||||
|
|
||||||
|
if published:
|
||||||
|
relative = _format_relative_date(published)
|
||||||
|
if relative:
|
||||||
|
return f"{tag} ({relative})"
|
||||||
|
|
||||||
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
def render_dashboard(repos: list[RepoData], console: Console | None = None) -> None:
|
||||||
|
"""Affiche le dashboard complet dans le terminal.
|
||||||
|
|
||||||
|
- Tableau principal : nom repo, indicateurs (fork/archive/mirror),
|
||||||
|
issues ouvertes, derniere release (tag + date relative)
|
||||||
|
- Section milestones : par repo ayant des milestones,
|
||||||
|
nom, progression (closed/total), date echeance
|
||||||
|
|
||||||
|
Le parametre console permet l'injection pour les tests.
|
||||||
|
"""
|
||||||
|
if console is None:
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
if not repos:
|
||||||
|
console.print("Aucun repo trouve.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Tableau principal
|
||||||
|
table = Table(title="Gitea Dashboard")
|
||||||
|
table.add_column("Repo", style="bold")
|
||||||
|
table.add_column("Issues", justify="right")
|
||||||
|
table.add_column("Release")
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
name = _format_repo_name(repo)
|
||||||
|
issues_str = str(repo.open_issues)
|
||||||
|
issues_style = "red" if repo.open_issues > 0 else "green"
|
||||||
|
release_str = _format_release(repo.latest_release)
|
||||||
|
|
||||||
|
table.add_row(name, f"[{issues_style}]{issues_str}[/{issues_style}]", release_str)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
# Section milestones — uniquement si au moins un repo en a
|
||||||
|
repos_with_milestones = [r for r in repos if r.milestones]
|
||||||
|
|
||||||
|
if repos_with_milestones:
|
||||||
|
console.print()
|
||||||
|
console.print("[bold]Milestones[/bold]")
|
||||||
|
|
||||||
|
for repo in repos_with_milestones:
|
||||||
|
for ms in repo.milestones:
|
||||||
|
title = ms["title"]
|
||||||
|
closed = ms["closed_issues"]
|
||||||
|
total = ms["open_issues"] + ms["closed_issues"]
|
||||||
|
pct = round(closed / total * 100) if total > 0 else 0
|
||||||
|
|
||||||
|
line = f" {repo.name} / {title} : {closed}/{total} ({pct}%)"
|
||||||
|
|
||||||
|
due_on = ms.get("due_on")
|
||||||
|
if due_on:
|
||||||
|
# Extraire juste la date (YYYY-MM-DD)
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(due_on.replace("Z", "+00:00"))
|
||||||
|
line += f" \u2014 echeance {dt.strftime('%Y-%m-%d')}"
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
console.print(line)
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
127
tests/test_cli.py
Normal file
127
tests/test_cli.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""Tests for CLI entry point."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from gitea_dashboard.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
class TestMainNominal:
|
||||||
|
"""Test main() happy path."""
|
||||||
|
|
||||||
|
@patch("gitea_dashboard.cli.render_dashboard")
|
||||||
|
@patch("gitea_dashboard.cli.collect_all")
|
||||||
|
@patch("gitea_dashboard.cli.GiteaClient")
|
||||||
|
def test_main_runs_full_pipeline(self, mock_client_cls, mock_collect, mock_render):
|
||||||
|
"""main() creates client, collects, and renders."""
|
||||||
|
env = {"GITEA_TOKEN": "test-token-123", "GITEA_URL": "http://localhost:3000"}
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
mock_collect.return_value = []
|
||||||
|
|
||||||
|
with patch.dict("os.environ", env, clear=False):
|
||||||
|
main()
|
||||||
|
|
||||||
|
mock_client_cls.assert_called_once_with("http://localhost:3000", "test-token-123")
|
||||||
|
mock_collect.assert_called_once_with(mock_client)
|
||||||
|
mock_render.assert_called_once_with(mock_collect.return_value)
|
||||||
|
|
||||||
|
@patch("gitea_dashboard.cli.render_dashboard")
|
||||||
|
@patch("gitea_dashboard.cli.collect_all")
|
||||||
|
@patch("gitea_dashboard.cli.GiteaClient")
|
||||||
|
def test_main_uses_default_url(self, mock_client_cls, mock_collect, mock_render):
|
||||||
|
"""main() uses default URL when GITEA_URL is not set."""
|
||||||
|
env = {"GITEA_TOKEN": "my-token"}
|
||||||
|
mock_client_cls.return_value = MagicMock()
|
||||||
|
mock_collect.return_value = []
|
||||||
|
|
||||||
|
with patch.dict("os.environ", env, clear=True):
|
||||||
|
main()
|
||||||
|
|
||||||
|
mock_client_cls.assert_called_once_with("http://192.168.0.106:3000", "my-token")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMainMissingToken:
|
||||||
|
"""Test main() when GITEA_TOKEN is not set."""
|
||||||
|
|
||||||
|
def test_error_message_when_token_missing(self, capsys):
|
||||||
|
"""main() exits with code 1 and prints message mentioning GITEA_TOKEN."""
|
||||||
|
with patch.dict("os.environ", {}, clear=True):
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "GITEA_TOKEN" in captured.err
|
||||||
|
|
||||||
|
|
||||||
|
class TestMainConnectionErrors:
|
||||||
|
"""Test main() error handling for network issues."""
|
||||||
|
|
||||||
|
@patch("gitea_dashboard.cli.collect_all")
|
||||||
|
@patch("gitea_dashboard.cli.GiteaClient")
|
||||||
|
def test_connection_error_handled(self, mock_client_cls, mock_collect):
|
||||||
|
"""ConnectionError is caught and exits with code 1."""
|
||||||
|
env = {"GITEA_TOKEN": "test-token"}
|
||||||
|
mock_client_cls.return_value = MagicMock()
|
||||||
|
mock_collect.side_effect = requests.ConnectionError("Connection refused")
|
||||||
|
|
||||||
|
with patch.dict("os.environ", env, clear=True):
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
|
||||||
|
@patch("gitea_dashboard.cli.collect_all")
|
||||||
|
@patch("gitea_dashboard.cli.GiteaClient")
|
||||||
|
def test_timeout_error_handled(self, mock_client_cls, mock_collect):
|
||||||
|
"""Timeout is caught and exits with code 1."""
|
||||||
|
env = {"GITEA_TOKEN": "test-token"}
|
||||||
|
mock_client_cls.return_value = MagicMock()
|
||||||
|
mock_collect.side_effect = requests.Timeout("Request timed out")
|
||||||
|
|
||||||
|
with patch.dict("os.environ", env, clear=True):
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
|
||||||
|
@patch("gitea_dashboard.cli.collect_all")
|
||||||
|
@patch("gitea_dashboard.cli.GiteaClient")
|
||||||
|
def test_request_exception_handled(self, mock_client_cls, mock_collect):
|
||||||
|
"""Generic RequestException is caught and exits with code 1."""
|
||||||
|
env = {"GITEA_TOKEN": "test-token"}
|
||||||
|
mock_client_cls.return_value = MagicMock()
|
||||||
|
mock_collect.side_effect = requests.RequestException("Something went wrong")
|
||||||
|
|
||||||
|
with patch.dict("os.environ", env, clear=True):
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
|
||||||
|
@patch("gitea_dashboard.cli.collect_all")
|
||||||
|
@patch("gitea_dashboard.cli.GiteaClient")
|
||||||
|
def test_token_not_in_error_output(self, mock_client_cls, mock_collect, capsys):
|
||||||
|
"""Token must never appear in error messages, even when present in the exception."""
|
||||||
|
env = {"GITEA_TOKEN": "super-secret-token-xyz"}
|
||||||
|
mock_client_cls.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Build exception message that embeds the token value from env
|
||||||
|
# to simulate a real-world leak scenario
|
||||||
|
def make_exc(environ):
|
||||||
|
leaked = environ["GITEA_TOKEN"]
|
||||||
|
return requests.RequestException(f"HTTP Error: Authorization token {leaked} rejected")
|
||||||
|
|
||||||
|
import os as _os
|
||||||
|
|
||||||
|
with patch.dict("os.environ", env, clear=True):
|
||||||
|
mock_collect.side_effect = make_exc(_os.environ)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main()
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert env["GITEA_TOKEN"] not in captured.out
|
||||||
|
assert env["GITEA_TOKEN"] not in captured.err
|
||||||
160
tests/test_client.py
Normal file
160
tests/test_client.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""Tests for GiteaClient API client."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
from gitea_dashboard.client import GiteaClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestGiteaClientInit:
|
||||||
|
"""Test client initialization and auth."""
|
||||||
|
|
||||||
|
def test_auth_header_is_set(self):
|
||||||
|
"""Session must carry Authorization header with token prefix."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "my-secret-token")
|
||||||
|
assert client.session.headers["Authorization"] == "token my-secret-token"
|
||||||
|
|
||||||
|
def test_base_url_stored(self):
|
||||||
|
"""Base URL is stored without trailing slash."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000/", "tok")
|
||||||
|
assert client.base_url == "http://gitea.local:3000"
|
||||||
|
|
||||||
|
def test_default_timeout_is_30(self):
|
||||||
|
"""Default timeout is 30 seconds."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
assert client.timeout == 30
|
||||||
|
|
||||||
|
def test_custom_timeout_is_stored(self):
|
||||||
|
"""Custom timeout is stored and used."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok", timeout=10)
|
||||||
|
assert client.timeout == 10
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetPaginated:
|
||||||
|
"""Test internal pagination logic."""
|
||||||
|
|
||||||
|
def _make_client(self):
|
||||||
|
return GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
|
||||||
|
def test_single_page(self):
|
||||||
|
"""When response has fewer items than limit, stop after one request."""
|
||||||
|
client = self._make_client()
|
||||||
|
# Return 3 items (< 50 limit) -> single page
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
mock_resp.json.return_value = [{"id": 1}, {"id": 2}, {"id": 3}]
|
||||||
|
|
||||||
|
with patch.object(client.session, "get", return_value=mock_resp) as mock_get:
|
||||||
|
result = client._get_paginated("/api/v1/user/repos")
|
||||||
|
|
||||||
|
assert result == [{"id": 1}, {"id": 2}, {"id": 3}]
|
||||||
|
# Called exactly once (single page)
|
||||||
|
mock_get.assert_called_once()
|
||||||
|
|
||||||
|
def test_two_pages(self):
|
||||||
|
"""When first page is full (limit items), fetch second page."""
|
||||||
|
client = self._make_client()
|
||||||
|
|
||||||
|
page1 = [{"id": i} for i in range(50)] # Exactly limit=50
|
||||||
|
page2 = [{"id": i} for i in range(50, 60)] # 10 items -> last page
|
||||||
|
|
||||||
|
mock_resp1 = MagicMock()
|
||||||
|
mock_resp1.raise_for_status = MagicMock()
|
||||||
|
mock_resp1.json.return_value = page1
|
||||||
|
|
||||||
|
mock_resp2 = MagicMock()
|
||||||
|
mock_resp2.raise_for_status = MagicMock()
|
||||||
|
mock_resp2.json.return_value = page2
|
||||||
|
|
||||||
|
with patch.object(client.session, "get", side_effect=[mock_resp1, mock_resp2]):
|
||||||
|
result = client._get_paginated("/api/v1/user/repos")
|
||||||
|
|
||||||
|
assert len(result) == 60
|
||||||
|
assert result == page1 + page2
|
||||||
|
|
||||||
|
def test_pagination_params_forwarded(self):
|
||||||
|
"""Extra params are merged with pagination params."""
|
||||||
|
client = self._make_client()
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.raise_for_status = MagicMock()
|
||||||
|
mock_resp.json.return_value = []
|
||||||
|
|
||||||
|
with patch.object(client.session, "get", return_value=mock_resp) as mock_get:
|
||||||
|
client._get_paginated("/api/v1/repos/o/r/milestones", params={"state": "open"})
|
||||||
|
|
||||||
|
call_params = mock_get.call_args[1]["params"]
|
||||||
|
assert call_params["state"] == "open"
|
||||||
|
assert call_params["limit"] == 50
|
||||||
|
assert call_params["page"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetRepos:
|
||||||
|
"""Test get_repos method."""
|
||||||
|
|
||||||
|
def test_get_repos_calls_paginated(self):
|
||||||
|
"""get_repos delegates to _get_paginated with correct endpoint."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
with patch.object(client, "_get_paginated", return_value=[{"id": 1}]) as mock_pag:
|
||||||
|
result = client.get_repos()
|
||||||
|
|
||||||
|
mock_pag.assert_called_once_with("/api/v1/user/repos")
|
||||||
|
assert result == [{"id": 1}]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLatestRelease:
|
||||||
|
"""Test get_latest_release method."""
|
||||||
|
|
||||||
|
def test_returns_release_on_success(self):
|
||||||
|
"""Returns release dict when repo has a release."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.status_code = 200
|
||||||
|
mock_resp.json.return_value = {"tag_name": "v1.0", "published_at": "2026-01-01"}
|
||||||
|
|
||||||
|
with patch.object(client.session, "get", return_value=mock_resp):
|
||||||
|
result = client.get_latest_release("admin", "my-repo")
|
||||||
|
|
||||||
|
assert result == {"tag_name": "v1.0", "published_at": "2026-01-01"}
|
||||||
|
|
||||||
|
def test_returns_none_on_404(self):
|
||||||
|
"""Returns None when repo has no release (404)."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.status_code = 404
|
||||||
|
|
||||||
|
with patch.object(client.session, "get", return_value=mock_resp):
|
||||||
|
result = client.get_latest_release("admin", "no-release-repo")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_raises_on_server_error(self):
|
||||||
|
"""HTTP 500 raises an exception instead of silently returning bad data."""
|
||||||
|
import pytest
|
||||||
|
import requests as req
|
||||||
|
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.status_code = 500
|
||||||
|
mock_resp.raise_for_status.side_effect = req.HTTPError("500 Server Error")
|
||||||
|
|
||||||
|
with patch.object(client.session, "get", return_value=mock_resp):
|
||||||
|
with pytest.raises(req.HTTPError):
|
||||||
|
client.get_latest_release("admin", "my-repo")
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetMilestones:
|
||||||
|
"""Test get_milestones method."""
|
||||||
|
|
||||||
|
def test_get_milestones_calls_paginated_with_state_open(self):
|
||||||
|
"""get_milestones delegates to _get_paginated with state=open."""
|
||||||
|
client = GiteaClient("http://gitea.local:3000", "tok")
|
||||||
|
milestones = [{"title": "v2.0", "open_issues": 3, "closed_issues": 2}]
|
||||||
|
|
||||||
|
with patch.object(client, "_get_paginated", return_value=milestones) as mock_pag:
|
||||||
|
result = client.get_milestones("admin", "my-repo")
|
||||||
|
|
||||||
|
mock_pag.assert_called_once_with(
|
||||||
|
"/api/v1/repos/admin/my-repo/milestones",
|
||||||
|
params={"state": "open"},
|
||||||
|
)
|
||||||
|
assert result == milestones
|
||||||
130
tests/test_collector.py
Normal file
130
tests/test_collector.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""Tests for data collector."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from gitea_dashboard.collector import RepoData, collect_all
|
||||||
|
|
||||||
|
|
||||||
|
def _make_repo(
|
||||||
|
name="my-repo",
|
||||||
|
full_name="admin/my-repo",
|
||||||
|
description="A repo",
|
||||||
|
open_issues_count=5,
|
||||||
|
open_pr_counter=2,
|
||||||
|
fork=False,
|
||||||
|
archived=False,
|
||||||
|
mirror=False,
|
||||||
|
owner_login="admin",
|
||||||
|
):
|
||||||
|
"""Build a fake repo dict as returned by the Gitea API."""
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"full_name": full_name,
|
||||||
|
"description": description,
|
||||||
|
"open_issues_count": open_issues_count,
|
||||||
|
"open_pr_counter": open_pr_counter,
|
||||||
|
"fork": fork,
|
||||||
|
"archived": archived,
|
||||||
|
"mirror": mirror,
|
||||||
|
"owner": {"login": owner_login},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCollectAll:
|
||||||
|
"""Test collect_all function."""
|
||||||
|
|
||||||
|
def test_basic_repo(self):
|
||||||
|
"""Collects repo data with release and milestones."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_repos.return_value = [_make_repo()]
|
||||||
|
client.get_latest_release.return_value = {
|
||||||
|
"tag_name": "v1.0",
|
||||||
|
"published_at": "2026-01-01",
|
||||||
|
}
|
||||||
|
client.get_milestones.return_value = [
|
||||||
|
{"title": "v2.0", "open_issues": 3, "closed_issues": 2, "due_on": None},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = collect_all(client)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
repo = result[0]
|
||||||
|
assert isinstance(repo, RepoData)
|
||||||
|
assert repo.name == "my-repo"
|
||||||
|
assert repo.full_name == "admin/my-repo"
|
||||||
|
assert repo.description == "A repo"
|
||||||
|
assert repo.open_issues == 3 # 5 - 2
|
||||||
|
assert repo.is_fork is False
|
||||||
|
assert repo.is_archived is False
|
||||||
|
assert repo.is_mirror is False
|
||||||
|
assert repo.latest_release == {"tag_name": "v1.0", "published_at": "2026-01-01"}
|
||||||
|
assert len(repo.milestones) == 1
|
||||||
|
|
||||||
|
def test_repo_without_release(self):
|
||||||
|
"""Repo with no release gets None for latest_release."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_repos.return_value = [_make_repo()]
|
||||||
|
client.get_latest_release.return_value = None
|
||||||
|
client.get_milestones.return_value = []
|
||||||
|
|
||||||
|
result = collect_all(client)
|
||||||
|
|
||||||
|
assert result[0].latest_release is None
|
||||||
|
|
||||||
|
def test_repo_without_milestones(self):
|
||||||
|
"""Repo with no milestones gets empty list."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_repos.return_value = [_make_repo()]
|
||||||
|
client.get_latest_release.return_value = None
|
||||||
|
client.get_milestones.return_value = []
|
||||||
|
|
||||||
|
result = collect_all(client)
|
||||||
|
|
||||||
|
assert result[0].milestones == []
|
||||||
|
|
||||||
|
def test_open_issues_subtracts_prs(self):
|
||||||
|
"""open_issues = open_issues_count - open_pr_counter."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_repos.return_value = [
|
||||||
|
_make_repo(open_issues_count=10, open_pr_counter=4),
|
||||||
|
]
|
||||||
|
client.get_latest_release.return_value = None
|
||||||
|
client.get_milestones.return_value = []
|
||||||
|
|
||||||
|
result = collect_all(client)
|
||||||
|
|
||||||
|
assert result[0].open_issues == 6
|
||||||
|
|
||||||
|
def test_multiple_repos(self):
|
||||||
|
"""Collects data for multiple repos."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_repos.return_value = [
|
||||||
|
_make_repo(name="repo-a", full_name="admin/repo-a"),
|
||||||
|
_make_repo(name="repo-b", full_name="org/repo-b", owner_login="org"),
|
||||||
|
]
|
||||||
|
client.get_latest_release.return_value = None
|
||||||
|
client.get_milestones.return_value = []
|
||||||
|
|
||||||
|
result = collect_all(client)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result[0].name == "repo-a"
|
||||||
|
assert result[1].name == "repo-b"
|
||||||
|
# Verify correct owner/repo passed to enrichment calls
|
||||||
|
client.get_latest_release.assert_any_call("admin", "repo-a")
|
||||||
|
client.get_latest_release.assert_any_call("org", "repo-b")
|
||||||
|
|
||||||
|
def test_fork_and_archived_flags(self):
|
||||||
|
"""Fork and archived flags are propagated."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_repos.return_value = [
|
||||||
|
_make_repo(fork=True, archived=True, mirror=True),
|
||||||
|
]
|
||||||
|
client.get_latest_release.return_value = None
|
||||||
|
client.get_milestones.return_value = []
|
||||||
|
|
||||||
|
result = collect_all(client)
|
||||||
|
|
||||||
|
assert result[0].is_fork is True
|
||||||
|
assert result[0].is_archived is True
|
||||||
|
assert result[0].is_mirror is True
|
||||||
216
tests/test_display.py
Normal file
216
tests/test_display.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
"""Tests for Rich dashboard display."""
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from gitea_dashboard.collector import RepoData
|
||||||
|
from gitea_dashboard.display import render_dashboard
|
||||||
|
|
||||||
|
|
||||||
|
def _make_console():
|
||||||
|
"""Create a console that captures output for testing.
|
||||||
|
|
||||||
|
highlight=False desactive le highlighting automatique de Rich
|
||||||
|
qui fragmente les nombres et noms avec des codes ANSI.
|
||||||
|
"""
|
||||||
|
buf = StringIO()
|
||||||
|
return Console(file=buf, force_terminal=True, width=120, highlight=False), buf
|
||||||
|
|
||||||
|
|
||||||
|
def _make_repo(
|
||||||
|
name="my-repo",
|
||||||
|
full_name="admin/my-repo",
|
||||||
|
description="A repo",
|
||||||
|
open_issues=3,
|
||||||
|
is_fork=False,
|
||||||
|
is_archived=False,
|
||||||
|
is_mirror=False,
|
||||||
|
latest_release=None,
|
||||||
|
milestones=None,
|
||||||
|
):
|
||||||
|
"""Build a RepoData for testing."""
|
||||||
|
return RepoData(
|
||||||
|
name=name,
|
||||||
|
full_name=full_name,
|
||||||
|
description=description,
|
||||||
|
open_issues=open_issues,
|
||||||
|
is_fork=is_fork,
|
||||||
|
is_archived=is_archived,
|
||||||
|
is_mirror=is_mirror,
|
||||||
|
latest_release=latest_release,
|
||||||
|
milestones=milestones if milestones is not None else [],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderDashboardTable:
|
||||||
|
"""Test the main repos table rendering."""
|
||||||
|
|
||||||
|
def test_basic_repo_displayed(self):
|
||||||
|
"""Repo name and issues count appear in output."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [
|
||||||
|
_make_repo(
|
||||||
|
name="mon-projet",
|
||||||
|
open_issues=3,
|
||||||
|
latest_release={"tag_name": "v1.2.0", "published_at": "2026-03-08T10:00:00Z"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "mon-projet" in output
|
||||||
|
assert "v1.2.0" in output
|
||||||
|
|
||||||
|
def test_repo_without_release_shows_dash(self):
|
||||||
|
"""Repo with no release shows a dash character."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="no-release", latest_release=None)]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "no-release" in output
|
||||||
|
# The dash character for "no release"
|
||||||
|
assert "\u2014" in output or "—" in output
|
||||||
|
|
||||||
|
def test_fork_indicator(self):
|
||||||
|
"""Fork repos show [F] indicator."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="forked-repo", is_fork=True)]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "forked-repo" in output
|
||||||
|
assert "[F]" in output
|
||||||
|
|
||||||
|
def test_archive_indicator(self):
|
||||||
|
"""Archived repos show [A] indicator."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="old-repo", is_archived=True)]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "[A]" in output
|
||||||
|
|
||||||
|
def test_mirror_indicator(self):
|
||||||
|
"""Mirror repos show [M] indicator."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="mirror-repo", is_mirror=True)]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "[M]" in output
|
||||||
|
|
||||||
|
def test_multiple_indicators(self):
|
||||||
|
"""Repo with multiple flags shows all indicators."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="special", is_fork=True, is_archived=True)]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "[F]" in output
|
||||||
|
assert "[A]" in output
|
||||||
|
|
||||||
|
def test_issues_zero(self):
|
||||||
|
"""Repos with 0 issues display 0."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="clean-repo", open_issues=0)]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "0" in output
|
||||||
|
|
||||||
|
def test_multiple_repos(self):
|
||||||
|
"""Multiple repos all appear in the output."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [
|
||||||
|
_make_repo(name="repo-alpha", open_issues=1),
|
||||||
|
_make_repo(name="repo-beta", open_issues=5),
|
||||||
|
]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "repo-alpha" in output
|
||||||
|
assert "repo-beta" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderDashboardMilestones:
|
||||||
|
"""Test the milestones section rendering."""
|
||||||
|
|
||||||
|
def test_milestones_displayed(self):
|
||||||
|
"""Repos with milestones show milestone info."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [
|
||||||
|
_make_repo(
|
||||||
|
name="mon-projet",
|
||||||
|
milestones=[
|
||||||
|
{
|
||||||
|
"title": "v2.0",
|
||||||
|
"open_issues": 2,
|
||||||
|
"closed_issues": 3,
|
||||||
|
"due_on": "2026-04-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "v2.0" in output
|
||||||
|
assert "3/5" in output # closed/total
|
||||||
|
assert "60%" in output
|
||||||
|
|
||||||
|
def test_milestone_without_due_date(self):
|
||||||
|
"""Milestone without due_on omits the deadline."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [
|
||||||
|
_make_repo(
|
||||||
|
name="projet",
|
||||||
|
milestones=[
|
||||||
|
{
|
||||||
|
"title": "backlog",
|
||||||
|
"open_issues": 5,
|
||||||
|
"closed_issues": 1,
|
||||||
|
"due_on": None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "backlog" in output
|
||||||
|
assert "1/6" in output
|
||||||
|
|
||||||
|
def test_no_milestones_section_when_none(self):
|
||||||
|
"""When no repos have milestones, milestone section is absent."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
repos = [_make_repo(name="simple", milestones=[])]
|
||||||
|
|
||||||
|
render_dashboard(repos, console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "Milestones" not in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderDashboardEmpty:
|
||||||
|
"""Test empty repo list handling."""
|
||||||
|
|
||||||
|
def test_empty_list_shows_message(self):
|
||||||
|
"""Empty repo list shows informative message."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
|
||||||
|
render_dashboard([], console=console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
|
||||||
|
assert "Aucun repo" in output
|
||||||
Reference in New Issue
Block a user