Compare commits

..

12 Commits

Author SHA1 Message Date
sylvain
03d09ac13b docs(v1.1.0): update README, freeze CHANGELOG, bump version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 04:46:38 +01:00
sylvain
79cbcd8e33 chore(workflow): complete step 9, merge step 10 into 11
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:45:04 +01:00
sylvain
f39158ed55 chore(workflow): complete step 8 (audit 100), start step 9
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:41:47 +01:00
sylvain
bb3bc761e3 test(collector): add filtering optimization and edge case tests
- test_filtered_repos_have_no_api_calls: prouve que get_latest_release et
  get_milestones ne sont pas appelés pour les repos exclus par le filtre include
- test_collect_all_include_empty_list: documente le contrat implicite où
  include=[] est équivalent à include=None (tous les repos inclus)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 04:40:40 +01:00
sylvain
1bc3b2fd36 chore(workflow): complete step 7, start step 8
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:36:56 +01:00
sylvain
844c9ccd08 chore(workflow): complete step 7, v1.1.0 dev done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:36:11 +01:00
sylvain
2232260821 feat(cli): add --repo and --exclude filtering (fixes #5)
Add argparse-based CLI parsing with repeatable --repo/-r (include) and
--exclude/-x (exclude) options. Filtering is case-insensitive substring
matching, applied post-fetch in collect_all() per ADR-005.

- parse_args() separated from main() for testability
- main(argv=None) accepts argv for test injection
- collect_all() gains optional include/exclude parameters
- 14 new tests (8 filtering + 6 CLI parsing/integration)
- All 51 tests pass, backward compatible (no args = v1.0.0 behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:35:42 +01:00
sylvain
0f8e34edf3 chore(workflow): complete step 6, start step 7
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:30:54 +01:00
sylvain
8e8271be9d docs(v1.1.0): version plan and ADR — repo filtering feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:30:07 +01:00
sylvain
85c3023b34 chore(workflow): init v1.1.0 (minor)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:18:41 +01:00
sylvain
22590d7250 docs(analyse): workflow analysis v1.0.0 — complete breakdown
13 steps, 11 agents, 19 MCP calls, chronology, metrics, recommendations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:42:13 +01:00
sylvain
5c8e833d8b chore(workflow): complete step 13 (retrospective), v1.0.0 done
Metrics, analysis, MEMORY.md written. Milestone v1.0.0 closed on Gitea.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:31:45 +01:00
13 changed files with 1237 additions and 28 deletions

View File

@@ -7,11 +7,11 @@
| Chemin | /home/sylvain/nas/perso/sylvain/conserver/code/application_temp/gitea-dashboard | | Chemin | /home/sylvain/nas/perso/sylvain/conserver/code/application_temp/gitea-dashboard |
| Date de creation | 2026-03-10 | | Date de creation | 2026-03-10 |
| Origine | gitea@192.168.0.106:admin/gitea-dashboard.git | | Origine | gitea@192.168.0.106:admin/gitea-dashboard.git |
| Version courante | v1.0.0 | | Version courante | v1.1.0 |
| Track | major-initial | | Track | minor |
| Phase courante | 4 — PUBLICATION | | Phase courante | 3 — PRE-RELEASE |
| Etape courante | 11 | | Etape courante | 9 (done) |
| workflow_version | v1.0 | | workflow_version | v1.1 |
--- ---
@@ -44,14 +44,27 @@
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes | | # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------| |---|-------|--------|------|-------------|------------|-------|
| 11 | Release | in_progress | 2026-03-10 | /release | Auto (release creee) | | | 11 | Release | done | 2026-03-10 | /release | Auto (release creee) | step_11: done, tag: v1.0.0 |
| 12 | Deploy (optionnel) | en_attente | | script | Auto (health check OK) | Optionnel | | 12 | Deploy (optionnel) | skipped | 2026-03-10 | script | Auto (health check OK) | Outil CLI local, pas de deploiement serveur |
## Phase 5 — POST-RELEASE ## Phase 5 — POST-RELEASE
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes | | # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------| |---|-------|--------|------|-------------|------------|-------|
| 13 | Retrospective | en_attente | | - | Auto (metriques et MEMORY.md ecrits) | | | 13 | Retrospective | done | 2026-03-10 | - | Auto (metriques et MEMORY.md ecrits) | step_13: done, metrics_written: true, analysis_written: true, gitea_milestone: closed |
## Phase 2 — DEV (v1.1.0)
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 6 | Plan de version | done | 2026-03-11 | architect | Auto (plan avec phases, budget scope) | step_6: done, plan: docs/plans/v1.1.0-plan.md, phases: 2, gitea_milestone: exists (id:32), ADR-004/005 |
| 7 | Developpement | done | 2026-03-11 | build / builder | Auto (tests passent) | step_7: done, commits: 1, files_modified: 5, tests: 51 passed (37 existing + 14 new) |
| 8 | Audit + corrections | done | 2026-03-11 | reviewer + guardian + fixer | Auto (score 100) | step_8: done, audit_initial: 94, audit_final: 100, rounds: 2, corrections: 2, remaining_findings: 0 |
| 9 | Smoke test | done | 2026-03-11 | tester + checklist | Auto (E2E pass + checklist) | step_9: done, mode: cli, rounds: 1, tests: 3/3 passed |
| 10 | Documentation | merged_with_11 | 2026-03-11 | - | - | step_10: merged_with_11, pas de docs/guides ni OpenAPI |
| 11 | Release | en_attente | | /release | Auto (release creee) | |
| 12 | Deploy (optionnel) | en_attente | | - | - | CLI local, probablement skip |
| 13 | Retrospective | en_attente | | documenter | Auto (metriques et analyse) | |
--- ---
@@ -78,8 +91,18 @@
| 2026-03-10 | step 8 done | Audit: reviewer 81→100, guardian 91→97, 5 corrections, score final 97 | | 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 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 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 |
| 2026-03-11 | Start v1.1.0 at step 6 | Minor track, feature: filtrage par repo (#5), test workflow v2.2.0 |
| 2026-03-11 | step 6 done | Plan v1.1.0 (2 phases, 6 fichiers, ADR-004/005), milestone exists (id:32) |
| 2026-03-11 | step 7 done | 1 commit, 5 fichiers modifies, 51 tests (14 nouveaux), fixes #5 |
| 2026-03-11 | step 8 done | Audit: reviewer 94→100, guardian 97 (HTTP pre-existant), 2 corrections testing, score final 100 |
| 2026-03-11 | step 9 done | Smoke test CLI reel, 3/3 tests, retrocompat OK, inclusion OK, exclusion OK |
## Versions completees ## Versions completees
| Version | Date debut | Date fin | Notes | | Version | Date debut | Date fin | Notes |
|---------|-----------|----------|-------| |---------|-----------|----------|-------|
| v1.0.0 | 2026-03-10 | 2026-03-10 | major-initial, 12/13 steps, audit 97, 37 tests |

View File

@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased] ## [Unreleased]
## [1.1.0] - 2026-03-11
### Added
- Options CLI `--repo`/`-r` et `--exclude`/`-x` pour filtrer les repos par nom (sous-chaine, insensible a la casse)
- Parsing CLI via argparse avec `parse_args()` separee pour testabilite
- Parametres `include`/`exclude` dans `collect_all()` pour filtrage post-fetch
## [1.0.0] - 2026-03-10 ## [1.0.0] - 2026-03-10
### Added ### Added
@@ -17,3 +25,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- Indicateurs visuels pour les repos forks, archives et miroirs - Indicateurs visuels pour les repos forks, archives et miroirs
- Gestion des erreurs réseau (connexion refusée, timeout, erreurs API) - Gestion des erreurs réseau (connexion refusée, timeout, erreurs API)
- Masquage du token dans les messages d'erreur - Masquage du token dans les messages d'erreur
[1.1.0]: https://gitea.tsmse.fr/admin/gitea-dashboard/compare/v1.0.0...v1.1.0

View File

@@ -39,6 +39,26 @@ gitea-dashboard
python -m gitea_dashboard python -m gitea_dashboard
``` ```
### Filtrage des repos
L'option `--repo`/`-r` filtre les repos à afficher (sous-chaîne, insensible à la casse).
L'option `--exclude`/`-x` exclut des repos de l'affichage.
Les deux options sont cumulables et répétables.
```bash
# Afficher uniquement les repos contenant "api"
gitea-dashboard --repo api
# Afficher les repos contenant "dashboard" ou "monitor"
gitea-dashboard -r dashboard -r monitor
# Exclure les repos contenant "archive" ou "test"
gitea-dashboard --exclude archive --exclude test
# Combiner inclusion et exclusion
gitea-dashboard --repo projet --exclude archive
```
### Exemple de sortie ### Exemple de sortie
``` ```

View 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.

View 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 |

259
docs/plans/v1.1.0-plan.md Normal file
View File

@@ -0,0 +1,259 @@
<!-- Type: reference (Diataxis). Style: factuel, structure par phases, actionnable par le builder. -->
# Plan de version v1.1.0 — gitea-dashboard
## Objectif
Permettre a l'utilisateur de filtrer l'affichage du dashboard par nom de repo (inclusion et exclusion), via des options CLI. Premiere evolution fonctionnelle du dashboard.
## Track
**Minor** : 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> (12) -> 13
---
## Budget de scope
| Critere | Valeur |
|---------|--------|
| Max fichiers par phase | 4 |
| Total fichiers estimes | 6 (3 modules modifies + 3 fichiers de tests modifies) |
### Inclus
- Option CLI `--repo` / `-r` pour filtrer par nom(s) de repo (inclusion)
- Option CLI `--exclude` / `-x` pour exclure des repos par nom
- Filtrage par sous-chaine (match partiel, insensible a la casse)
- Filtrage applicable aux deux options (cumulable : `--repo X --exclude Y`)
- Ajout d'argparse pour le parsing des options CLI
- Tests unitaires du filtrage et tests d'integration CLI
### Exclus
- Filtrage par owner/organisation (hors scope, pas de demande)
- Filtrage par regex (sous-chaine suffit pour v1.1.0)
- Filtrage par labels, activite ou date
- Tri des resultats (differe)
- Options `--format`, `--sort` (differees)
- Parallelisation des appels API (ADR-003, differee)
### Differe (v1.2+)
- Filtrage par owner/organisation
- Option `--sort` (par nom, issues, date de release)
- Cache des reponses API
- Option `--format` (json, csv)
---
## Etapes skippees
| Etape | Nom | Raison |
|-------|-----|--------|
| 1 | Discovery | Projet existant, discovery v1.0.0 suffisante |
| 2 | Project creation | Projet existant |
| 3 | Specs | Minor — specs couvertes par l'issue #5 et ce plan |
| 4 | Research | Pas de technologie nouvelle (argparse est stdlib) |
| 5 | Roadmap | Minor — roadmap existante, issue #5 deja creee |
| 12 | Deploy | Outil CLI local, pas de deploiement serveur |
---
## Phase 1 : Parsing CLI et logique de filtrage
**Goal** : Ajouter les options `--repo` et `--exclude` a la CLI et implementer la logique de filtrage dans le collecteur.
**Issues Gitea** : fixes #5
### Fichiers
| Action | Fichier | Modifications | Cross-references |
|--------|---------|---------------|------------------|
| Modify | `src/gitea_dashboard/cli.py` | Ajouter argparse, options `--repo`/`-r` et `--exclude`/`-x`, passer les filtres a `collect_all()` | `collector.py` (passe les filtres) |
| Modify | `src/gitea_dashboard/collector.py` | Ajouter parametres `include` et `exclude` a `collect_all()`, logique de filtrage | `cli.py` (appele avec filtres), `client.py` (inchange) |
| Modify | `tests/test_cli.py` | Tests argparse, passage des filtres, combinaison d'options | `cli.py` |
| Modify | `tests/test_collector.py` | Tests du filtrage : inclusion, exclusion, combinaison, casse, sous-chaine | `collector.py` |
### Interfaces
#### cli.py (modifications)
```python
import argparse
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse les arguments CLI.
Options:
--repo / -r : noms de repos a inclure (repeatable)
--exclude / -x : noms de repos a exclure (repeatable)
Returns:
Namespace avec .repo (list[str] | None) et .exclude (list[str] | None)
"""
def main(argv: list[str] | None = None) -> None:
"""Point d'entree principal.
Modification : accepte argv pour testabilite.
Appelle parse_args(), puis passe include/exclude a collect_all().
"""
```
Pourquoi `parse_args` separe de `main` : testabilite. On peut tester le parsing seul sans mocker l'environnement complet.
Pourquoi `argv` en parametre de `main` : permet aux tests d'injecter des arguments sans patcher `sys.argv`.
#### collector.py (modifications)
```python
def collect_all(
client: GiteaClient,
include: list[str] | None = None,
exclude: list[str] | None = None,
) -> list[RepoData]:
"""Collecte les donnees des repos, avec filtrage optionnel.
Args:
client: Client API Gitea.
include: Si fourni, ne garde que les repos dont le nom contient
au moins une des sous-chaines (insensible a la casse).
exclude: Si fourni, exclut les repos dont le nom contient
au moins une des sous-chaines (insensible a la casse).
Ordre d'application : include d'abord (si present), puis exclude.
Si include est None ou vide, tous les repos sont inclus avant l'etape exclude.
"""
```
Pourquoi le filtrage est dans `collector.py` et non `cli.py` : le collecteur est responsable de "quels repos collecter". Cela evite de polluer le CLI avec de la logique metier et garde la testabilite (on teste le filtrage sans mocker argparse).
Pourquoi le filtrage est post-fetch (apres `get_repos()`) et non pre-fetch : l'API Gitea `/user/repos` ne supporte pas de filtre par nom cote serveur. On doit recuperer tous les repos puis filtrer localement.
### Comportement attendu
1. Sans options, comportement identique a v1.0.0 :
```
$ gitea-dashboard
# Affiche tous les repos (aucun changement)
```
2. Filtrage par inclusion :
```
$ gitea-dashboard --repo dashboard --repo infra
# Affiche uniquement les repos dont le nom contient "dashboard" ou "infra"
```
3. Filtrage par exclusion :
```
$ gitea-dashboard --exclude fork --exclude test
# Affiche tous les repos sauf ceux dont le nom contient "fork" ou "test"
```
4. Combinaison inclusion + exclusion :
```
$ gitea-dashboard --repo projet -x old
# Inclut les repos contenant "projet", puis exclut ceux contenant "old"
```
5. Insensibilite a la casse :
```
$ gitea-dashboard --repo Dashboard
# Match "gitea-dashboard", "Dashboard-test", etc.
```
6. Aucun repo ne correspond :
```
$ gitea-dashboard --repo inexistant
# Affiche "Aucun repo trouve." (comportement existant de render_dashboard)
```
### Tests
#### test_cli.py (ajouts)
- `test_parse_args_no_options` : retourne `Namespace(repo=None, exclude=None)`
- `test_parse_args_single_repo` : `--repo foo` -> `Namespace(repo=["foo"], ...)`
- `test_parse_args_multiple_repo` : `--repo foo --repo bar` -> `Namespace(repo=["foo", "bar"], ...)`
- `test_parse_args_short_flags` : `-r foo -x bar` fonctionne comme les formes longues
- `test_main_passes_filters_to_collect_all` : verifie que `collect_all` est appele avec les bons `include`/`exclude`
- `test_main_no_filters_passes_none` : sans options, `collect_all(client, include=None, exclude=None)`
#### test_collector.py (ajouts)
- `test_collect_all_no_filter` : comportement identique a v1.0.0 (retrocompatibilite)
- `test_collect_all_include_single` : filtre par une sous-chaine
- `test_collect_all_include_multiple` : filtre par plusieurs sous-chaines (OR)
- `test_collect_all_exclude_single` : exclut par une sous-chaine
- `test_collect_all_include_and_exclude` : inclusion puis exclusion
- `test_collect_all_case_insensitive` : "Dashboard" matche "gitea-dashboard"
- `test_collect_all_no_match` : retourne une liste vide si aucun repo ne correspond
- `test_collect_all_exclude_all` : retourne une liste vide si tout est exclu
### Livrable
La commande `gitea-dashboard --repo X -x Y` filtre l'affichage. Sans options, le comportement est identique a v1.0.0. Tous les tests passent (existants + nouveaux).
---
## Phase 2 : Smoke test et documentation
**Goal** : Valider le filtrage sur l'instance reelle et mettre a jour la documentation.
**Dependances** : phase 1 terminee, acces a l'instance Gitea (192.168.0.106:3000)
**Composants cles** :
- Test E2E manuel : `gitea-dashboard --repo dashboard`, `gitea-dashboard -x fork`, combinaison
- Verification de la retrocompatibilite : `gitea-dashboard` sans options
- Mise a jour de README.md (section usage avec les nouvelles options)
- Mise a jour de CHANGELOG.md (section Added pour v1.1.0)
- Mise a jour de `--help` (automatique via argparse)
---
## Architecture des modules (impact)
Le changement est minimal et respecte l'architecture existante (ADR-002) :
| Module | Impact | Detail |
|--------|--------|--------|
| `cli.py` | Modifie | Ajout argparse + passage des filtres |
| `collector.py` | Modifie | Nouveaux parametres `include`/`exclude` dans `collect_all()` |
| `client.py` | Inchange | Aucun impact (le filtrage est local, pas API) |
| `display.py` | Inchange | Recoit toujours `list[RepoData]`, ne sait pas si c'est filtre |
Pas de nouveau module. La signature de `collect_all()` est modifiee avec des parametres optionnels : **retrocompatible** (les parametres ont des valeurs par defaut `None`).
---
## Risques d'audit
| Zone | Risque | Severite estimee |
|------|--------|-----------------|
| `cli.py` — argparse | Interaction entre `argv` et `sys.argv` : s'assurer que `parse_args(None)` delegue bien a `sys.argv` | minor |
| `collector.py` — filtrage | Match partiel trop agressif (ex: `--repo a` matche tous les repos contenant "a") | minor |
| `collector.py` — ordre include/exclude | L'ordre d'application doit etre documente et teste | minor |
| `cli.py` — retrocompatibilite | Entry point `main()` ne doit pas casser si appele sans arguments | major |
---
## Issues Gitea rattachees
| Issue | Titre | Phase |
|-------|-------|-------|
| [#5](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/5) | Ajouter le filtrage par repo | Phase 1 |
---
## Dependances
| Dependance | Type | Version |
|------------|------|---------|
| Python | Runtime | >= 3.10 |
| argparse | Stdlib | inclus dans Python |
| requests | Librairie | >= 2.31 (inchange) |
| rich | Librairie | >= 13.0 (inchange) |
| pytest | Dev | >= 7.0 (inchange) |
| ruff | Dev | >= 0.4 (inchange) |
| Instance Gitea | Service externe | 192.168.0.106:3000 |

View File

@@ -46,3 +46,35 @@
- Temps de reponse acceptable pour < 20 repos (estimee < 10s) - Temps de reponse acceptable pour < 20 repos (estimee < 10s)
- Pas de problemes de concurrence - Pas de problemes de concurrence
- Facile a ajouter plus tard sans changer les interfaces (le collecteur est le seul point d'appel) - Facile a ajouter plus tard sans changer les interfaces (le collecteur est le seul point d'appel)
## ADR-004 : Argparse pour le parsing CLI (v1.1.0)
**Date** : 2026-03-11
**Statut** : accepte
**Contexte** : La v1.1.0 introduit des options CLI (`--repo`, `--exclude`). Un parser d'arguments est necessaire. Trois options : argparse (stdlib), click, typer.
**Decision** : Utiliser argparse (stdlib Python). Pas de dependance externe pour le parsing CLI.
**Consequences** :
- Zero nouvelle dependance (argparse est dans la stdlib)
- Coherent avec ADR-001 (stack simple, pas de framework lourd)
- `--help` genere automatiquement
- Suffisant pour des options simples (repeatable flags)
- Si les options deviennent complexes (sous-commandes, autocompletion), migration vers click/typer sera possible
## ADR-005 : Filtrage par sous-chaine dans le collecteur (v1.1.0)
**Date** : 2026-03-11
**Statut** : accepte
**Contexte** : Le filtrage des repos peut etre fait dans le CLI (apres collecte) ou dans le collecteur (pendant la collecte). Le filtrage par regex est plus puissant mais plus complexe que la sous-chaine.
**Decision** : Filtrage par sous-chaine (insensible a la casse) dans `collect_all()`. Ordre : include d'abord, exclude ensuite.
**Consequences** :
- Le collecteur reste le seul responsable de "quels repos collecter"
- Le CLI reste un simple orchestrateur (ADR-002 respecte)
- Retrocompatible : les nouveaux parametres ont des valeurs par defaut None
- Sous-chaine est intuitive pour l'utilisateur (pas besoin de connaitre les regex)
- Le filtrage est post-fetch car l'API Gitea ne supporte pas le filtre par nom

View File

@@ -0,0 +1,6 @@
# Findings History
| Version | Severity | Category | File | Pattern | Resolution |
|---------|----------|----------|------|---------|------------|
| v1.1.0 | minor | testing | tests/test_collector.py | Pas de test prouvant que les repos filtrés n'entraînent pas d'appels API inutiles (get_latest_release / get_milestones) | Ajout de `test_filtered_repos_have_no_api_calls` : vérifie que `assert_called_once_with` cible uniquement le repo qui a passé le filtre |
| v1.1.0 | minor | testing | tests/test_collector.py | Pas de test pour `include=[]` (liste vide) — contrat implicite `if include:` traite `[]` comme `None` | Ajout de `test_collect_all_include_empty_list` : compare le résultat de `collect_all(client, include=[])` avec `collect_all(client)` |

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "gitea-dashboard" name = "gitea-dashboard"
version = "1.0.0" version = "1.1.0"
description = "CLI dashboard for Gitea repos status" description = "CLI dashboard for Gitea repos status"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import argparse
import os import os
import sys import sys
@@ -15,15 +16,50 @@ from gitea_dashboard.display import render_dashboard
_DEFAULT_URL = "http://192.168.0.106:3000" _DEFAULT_URL = "http://192.168.0.106:3000"
def main() -> None: def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse les arguments CLI.
Options:
--repo / -r : noms de repos a inclure (repeatable)
--exclude / -x : noms de repos a exclure (repeatable)
Returns:
Namespace avec .repo (list[str] | None) et .exclude (list[str] | None)
"""
parser = argparse.ArgumentParser(
description="Dashboard CLI affichant l'etat des repos Gitea.",
)
parser.add_argument(
"--repo",
"-r",
action="append",
default=None,
help="Filtrer par nom de repo (sous-chaine, insensible a la casse). Repeatable.",
)
parser.add_argument(
"--exclude",
"-x",
action="append",
default=None,
help="Exclure les repos par nom (sous-chaine, insensible a la casse). Repeatable.",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> None:
"""Point d'entree principal. """Point d'entree principal.
1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis) Args:
2. Cree le GiteaClient argv: Arguments CLI. Si None, utilise sys.argv (via argparse).
3. Collecte les donnees via collect_all()
4. Affiche via render_dashboard() 1. Parse les options CLI (--repo, --exclude)
5. Gere les erreurs : config manquante, connexion refusee, timeout 2. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
3. Cree le GiteaClient
4. Collecte les donnees via collect_all() avec filtres
5. Affiche via render_dashboard()
6. Gere les erreurs : config manquante, connexion refusee, timeout
""" """
args = parse_args(argv)
console = Console(stderr=True) console = Console(stderr=True)
token = os.environ.get("GITEA_TOKEN") token = os.environ.get("GITEA_TOKEN")
@@ -38,7 +74,7 @@ def main() -> None:
client = GiteaClient(url, token) client = GiteaClient(url, token)
try: try:
repos = collect_all(client) repos = collect_all(client, include=args.repo, exclude=args.exclude)
except requests.ConnectionError: except requests.ConnectionError:
console.print("[red]Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.[/red]") console.print("[red]Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.[/red]")
sys.exit(1) sys.exit(1)

View File

@@ -22,13 +22,37 @@ class RepoData:
milestones: list[dict] # [{title, open_issues, closed_issues, due_on}] milestones: list[dict] # [{title, open_issues, closed_issues, due_on}]
def collect_all(client: GiteaClient) -> list[RepoData]: def _matches_any(name: str, patterns: list[str]) -> bool:
"""Collecte les donnees de tous les repos. """Return True if name contains any of the patterns (case-insensitive)."""
name_lower = name.lower()
return any(p.lower() in name_lower for p in patterns)
Pour chaque repo : enrichit avec release et milestones.
Calcule open_issues = open_issues_count - open_pr_counter. def collect_all(
client: GiteaClient,
include: list[str] | None = None,
exclude: list[str] | None = None,
) -> list[RepoData]:
"""Collecte les donnees des repos, avec filtrage optionnel.
Args:
client: Client API Gitea.
include: Si fourni, ne garde que les repos dont le nom contient
au moins une des sous-chaines (insensible a la casse).
exclude: Si fourni, exclut les repos dont le nom contient
au moins une des sous-chaines (insensible a la casse).
Ordre d'application : include d'abord (si present), puis exclude.
Si include est None ou vide, tous les repos sont inclus avant l'etape exclude.
""" """
repos = client.get_repos() repos = client.get_repos()
# Filtrage post-fetch : l'API Gitea ne supporte pas le filtre par nom
if include:
repos = [r for r in repos if _matches_any(r["name"], include)]
if exclude:
repos = [r for r in repos if not _matches_any(r["name"], exclude)]
result: list[RepoData] = [] result: list[RepoData] = []
for repo in repos: for repo in repos:

View File

@@ -22,10 +22,10 @@ class TestMainNominal:
mock_collect.return_value = [] mock_collect.return_value = []
with patch.dict("os.environ", env, clear=False): with patch.dict("os.environ", env, clear=False):
main() main([])
mock_client_cls.assert_called_once_with("http://localhost:3000", "test-token-123") mock_client_cls.assert_called_once_with("http://localhost:3000", "test-token-123")
mock_collect.assert_called_once_with(mock_client) mock_collect.assert_called_once_with(mock_client, include=None, exclude=None)
mock_render.assert_called_once_with(mock_collect.return_value) mock_render.assert_called_once_with(mock_collect.return_value)
@patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.render_dashboard")
@@ -38,7 +38,7 @@ class TestMainNominal:
mock_collect.return_value = [] mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True): with patch.dict("os.environ", env, clear=True):
main() main([])
mock_client_cls.assert_called_once_with("http://192.168.0.106:3000", "my-token") mock_client_cls.assert_called_once_with("http://192.168.0.106:3000", "my-token")
@@ -50,7 +50,7 @@ class TestMainMissingToken:
"""main() exits with code 1 and prints message mentioning GITEA_TOKEN.""" """main() exits with code 1 and prints message mentioning GITEA_TOKEN."""
with patch.dict("os.environ", {}, clear=True): with patch.dict("os.environ", {}, clear=True):
with pytest.raises(SystemExit) as exc_info: with pytest.raises(SystemExit) as exc_info:
main() main([])
assert exc_info.value.code == 1 assert exc_info.value.code == 1
captured = capsys.readouterr() captured = capsys.readouterr()
@@ -70,7 +70,7 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True): with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info: with pytest.raises(SystemExit) as exc_info:
main() main([])
assert exc_info.value.code == 1 assert exc_info.value.code == 1
@@ -84,7 +84,7 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True): with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info: with pytest.raises(SystemExit) as exc_info:
main() main([])
assert exc_info.value.code == 1 assert exc_info.value.code == 1
@@ -98,7 +98,7 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True): with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info: with pytest.raises(SystemExit) as exc_info:
main() main([])
assert exc_info.value.code == 1 assert exc_info.value.code == 1
@@ -120,8 +120,76 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True): with patch.dict("os.environ", env, clear=True):
mock_collect.side_effect = make_exc(_os.environ) mock_collect.side_effect = make_exc(_os.environ)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main() main([])
captured = capsys.readouterr() captured = capsys.readouterr()
assert env["GITEA_TOKEN"] not in captured.out assert env["GITEA_TOKEN"] not in captured.out
assert env["GITEA_TOKEN"] not in captured.err assert env["GITEA_TOKEN"] not in captured.err
class TestParseArgs:
"""Test parse_args function."""
def test_no_options(self):
"""No arguments returns None for both repo and exclude."""
from gitea_dashboard.cli import parse_args
args = parse_args([])
assert args.repo is None
assert args.exclude is None
def test_single_repo(self):
"""--repo foo returns repo=["foo"]."""
from gitea_dashboard.cli import parse_args
args = parse_args(["--repo", "foo"])
assert args.repo == ["foo"]
def test_multiple_repo(self):
"""--repo foo --repo bar returns repo=["foo", "bar"]."""
from gitea_dashboard.cli import parse_args
args = parse_args(["--repo", "foo", "--repo", "bar"])
assert args.repo == ["foo", "bar"]
def test_short_flags(self):
"""-r foo -x bar works like long forms."""
from gitea_dashboard.cli import parse_args
args = parse_args(["-r", "foo", "-x", "bar"])
assert args.repo == ["foo"]
assert args.exclude == ["bar"]
class TestMainWithFilters:
"""Test main() passes filters to collect_all."""
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_main_passes_filters(self, mock_client_cls, mock_collect, mock_render):
"""main() passes include/exclude from CLI args to collect_all."""
env = {"GITEA_TOKEN": "test-token"}
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True):
main(["--repo", "dash", "--exclude", "old"])
mock_collect.assert_called_once_with(mock_client, include=["dash"], exclude=["old"])
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_main_no_filters_passes_none(self, mock_client_cls, mock_collect, mock_render):
"""Without options, collect_all is called with include=None, exclude=None."""
env = {"GITEA_TOKEN": "test-token"}
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True):
main([])
mock_collect.assert_called_once_with(mock_client, include=None, exclude=None)

View File

@@ -128,3 +128,104 @@ class TestCollectAll:
assert result[0].is_fork is True assert result[0].is_fork is True
assert result[0].is_archived is True assert result[0].is_archived is True
assert result[0].is_mirror is True assert result[0].is_mirror is True
class TestCollectAllFiltering:
"""Test collect_all filtering (include/exclude)."""
def _setup_client(self, repo_names: list[str]) -> MagicMock:
"""Create a mock client returning repos with the given names."""
client = MagicMock()
client.get_repos.return_value = [
_make_repo(name=n, full_name=f"admin/{n}") for n in repo_names
]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
return client
def test_no_filter_returns_all(self):
"""Without include/exclude, all repos are returned (backward compat)."""
client = self._setup_client(["alpha", "beta", "gamma"])
result = collect_all(client)
assert [r.name for r in result] == ["alpha", "beta", "gamma"]
def test_include_single(self):
"""Include filters repos by substring match."""
client = self._setup_client(["gitea-dashboard", "infra-core", "notes"])
result = collect_all(client, include=["dashboard"])
assert [r.name for r in result] == ["gitea-dashboard"]
def test_include_multiple(self):
"""Multiple include patterns are OR-combined."""
client = self._setup_client(["gitea-dashboard", "infra-core", "notes"])
result = collect_all(client, include=["dashboard", "infra"])
assert [r.name for r in result] == ["gitea-dashboard", "infra-core"]
def test_exclude_single(self):
"""Exclude removes repos matching the substring."""
client = self._setup_client(["gitea-dashboard", "old-fork", "notes"])
result = collect_all(client, exclude=["fork"])
assert [r.name for r in result] == ["gitea-dashboard", "notes"]
def test_include_and_exclude(self):
"""Include is applied first, then exclude."""
client = self._setup_client(["projet-web", "projet-old", "infra"])
result = collect_all(client, include=["projet"], exclude=["old"])
assert [r.name for r in result] == ["projet-web"]
def test_case_insensitive(self):
"""Filtering is case-insensitive."""
client = self._setup_client(["Gitea-Dashboard", "infra"])
result = collect_all(client, include=["dashboard"])
assert [r.name for r in result] == ["Gitea-Dashboard"]
def test_no_match_returns_empty(self):
"""Returns empty list when no repo matches include filter."""
client = self._setup_client(["alpha", "beta"])
result = collect_all(client, include=["inexistant"])
assert result == []
def test_exclude_all_returns_empty(self):
"""Returns empty list when all repos are excluded."""
client = self._setup_client(["alpha", "beta"])
result = collect_all(client, exclude=["alpha", "beta"])
assert result == []
def test_filtered_repos_have_no_api_calls(self):
"""Repos excluded by include filter must not trigger enrichment API calls."""
client = self._setup_client(["gitea-dashboard", "infra-core", "notes"])
collect_all(client, include=["dashboard"])
# Only gitea-dashboard passed the filter — enrichment calls must target it only
client.get_latest_release.assert_called_once_with("admin", "gitea-dashboard")
client.get_milestones.assert_called_once_with("admin", "gitea-dashboard")
def test_collect_all_include_empty_list(self):
"""include=[] behaves like include=None — all repos are returned.
The contract: an empty list is falsy, so `if include:` is False, meaning
no inclusion filter is applied and every repo is included before exclude.
"""
client = self._setup_client(["alpha", "beta", "gamma"])
result_none = collect_all(client)
result_empty = collect_all(client, include=[])
assert [r.name for r in result_empty] == [r.name for r in result_none]