Initial commit
This commit is contained in:
127
skills/git-pr/.scope-consistency.md
Normal file
127
skills/git-pr/.scope-consistency.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Consistance des Scopes GitHub
|
||||
|
||||
## ⚠️ IMPORTANT: Source de Vérité Unique
|
||||
|
||||
Pour garantir la cohérence des scopes GitHub demandés, **UNE SEULE source de vérité existe**:
|
||||
|
||||
### 📍 Fichier de Référence
|
||||
|
||||
```
|
||||
scripts/gh_auth_setup.sh
|
||||
```
|
||||
|
||||
**Array `REQUIRED_SCOPES`** ligne ~4-10:
|
||||
|
||||
```bash
|
||||
REQUIRED_SCOPES=(
|
||||
"repo" # Accès complet aux repos (PRs, commits, etc.)
|
||||
"read:org" # Lecture infos organisation
|
||||
"read:project" # Lecture projets GitHub
|
||||
"project" # Écriture/assignation aux projets
|
||||
"gist" # Gestion des gists
|
||||
)
|
||||
```
|
||||
|
||||
## 🔒 Règles Strictes
|
||||
|
||||
### DO ✅
|
||||
|
||||
1. **Modifier uniquement `gh_auth_setup.sh`** pour ajouter/retirer des scopes
|
||||
2. **Utiliser systématiquement** `bash gh_auth_setup.sh` pour renouveler l'auth
|
||||
3. **Vérifier les scopes** à chaque début de workflow (Étape 1.5 dans SKILL.md)
|
||||
4. **Documenter** dans ce fichier toute modification de scopes
|
||||
|
||||
### DON'T ❌
|
||||
|
||||
1. ❌ **NE JAMAIS** utiliser `gh auth refresh -s <scope>` manuellement sans passer par le script
|
||||
2. ❌ **NE JAMAIS** hard-coder les scopes dans SKILL.md ou ailleurs
|
||||
3. ❌ **NE JAMAIS** supposer qu'un scope est présent sans vérification
|
||||
|
||||
## 📋 Checklist Modification Scopes
|
||||
|
||||
Si ajout/retrait de scope nécessaire:
|
||||
|
||||
- [ ] Modifier `REQUIRED_SCOPES` dans `scripts/gh_auth_setup.sh`
|
||||
- [ ] Mettre à jour section "Scopes Requis" dans `SKILL.md`
|
||||
- [ ] Mettre à jour tableau dans `README.md`
|
||||
- [ ] Documenter dans ce fichier (section Historique)
|
||||
- [ ] Tester avec `bash scripts/gh_auth_setup.sh`
|
||||
- [ ] Vérifier avec `gh auth status`
|
||||
- [ ] Tester le workflow complet
|
||||
|
||||
## 📜 Historique des Modifications
|
||||
|
||||
### 2025-11-05 - Initialisation
|
||||
|
||||
**Scopes définis**: `repo`, `read:org`, `read:project`, `project`, `gist`
|
||||
|
||||
**Contexte**: Création du système de consistance suite à oubli intermittent de scopes lors des renouvellements d'authentification.
|
||||
|
||||
**Changements**:
|
||||
- Création `scripts/gh_auth_setup.sh` (script centralisant TOUS les scopes)
|
||||
- Ajout Étape 1.5 dans `SKILL.md` (vérification automatique scopes)
|
||||
- Création `README.md` (documentation complète)
|
||||
- Création `.scope-consistency.md` (ce fichier - règles de cohérence)
|
||||
|
||||
**Impact**: Garantit qu'aucun scope ne sera oublié lors des futurs renouvellements.
|
||||
|
||||
---
|
||||
|
||||
### Template Modification Future
|
||||
|
||||
```markdown
|
||||
### YYYY-MM-DD - [Titre Modification]
|
||||
|
||||
**Scopes modifiés**: `scope1`, `scope2`
|
||||
|
||||
**Contexte**: [Raison de la modification]
|
||||
|
||||
**Changements**:
|
||||
- [Détail changement 1]
|
||||
- [Détail changement 2]
|
||||
|
||||
**Impact**: [Impact sur le workflow]
|
||||
```
|
||||
|
||||
## 🔍 Débogage
|
||||
|
||||
### Symptôme: "Scopes manquants" malgré renouvellement récent
|
||||
|
||||
**Diagnostic**:
|
||||
```bash
|
||||
# 1. Vérifier scopes actuels
|
||||
gh auth status | grep "Token scopes"
|
||||
|
||||
# 2. Comparer avec REQUIRED_SCOPES
|
||||
cat scripts/gh_auth_setup.sh | grep -A 10 "REQUIRED_SCOPES"
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Re-run le script de setup
|
||||
bash scripts/gh_auth_setup.sh
|
||||
```
|
||||
|
||||
### Symptôme: Script fonctionne mais assignation projet échoue
|
||||
|
||||
**Diagnostic**:
|
||||
```bash
|
||||
# Vérifier que 'project' ET pas seulement 'read:project'
|
||||
gh auth status | grep -o "project"
|
||||
```
|
||||
|
||||
**Cause Probable**: Token ancien avec seulement `read:project`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Logout complet puis re-setup
|
||||
gh auth logout
|
||||
bash scripts/gh_auth_setup.sh
|
||||
```
|
||||
|
||||
## 📚 Références
|
||||
|
||||
- Script source: `scripts/gh_auth_setup.sh`
|
||||
- Workflow: `SKILL.md` (Étape 1.5)
|
||||
- Documentation: `README.md`
|
||||
- GitHub Scopes Doc: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
|
||||
211
skills/git-pr/README.md
Normal file
211
skills/git-pr/README.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Git PR Skill - Documentation
|
||||
|
||||
## Configuration Initiale
|
||||
|
||||
### Authentification GitHub
|
||||
|
||||
Ce skill nécessite une authentification GitHub avec des scopes spécifiques.
|
||||
|
||||
#### Scopes Requis
|
||||
|
||||
| Scope | Description | Usage |
|
||||
|-------|-------------|-------|
|
||||
| `repo` | Accès complet aux repos | Création PR, lecture commits, gestion branches |
|
||||
| `read:org` | Lecture infos organisation | Récupération infos repository organisation |
|
||||
| `read:project` | Lecture projets GitHub | Liste et lecture des projets |
|
||||
| `project` | Écriture aux projets | Assignation PR aux projets |
|
||||
| `gist` | Gestion gists | Partage extraits de code si nécessaire |
|
||||
|
||||
#### Configuration Automatique (Recommandé)
|
||||
|
||||
```bash
|
||||
bash scripts/gh_auth_setup.sh
|
||||
```
|
||||
|
||||
Ce script configure automatiquement TOUS les scopes requis.
|
||||
|
||||
#### Configuration Manuelle
|
||||
|
||||
```bash
|
||||
gh auth refresh --hostname github.com \
|
||||
-s repo \
|
||||
-s read:org \
|
||||
-s read:project \
|
||||
-s project \
|
||||
-s gist
|
||||
```
|
||||
|
||||
#### Vérification
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
|
||||
Sortie attendue:
|
||||
```
|
||||
Token scopes: 'gist', 'project', 'read:org', 'repo'
|
||||
```
|
||||
|
||||
⚠️ **Note**: `read:project` n'apparaît pas toujours explicitement mais est inclus avec `project`.
|
||||
|
||||
## Protection Contre les Oublis
|
||||
|
||||
Le skill vérifie automatiquement les scopes au démarrage (Étape 1.5).
|
||||
|
||||
Si un scope manque :
|
||||
- ❌ Arrêt immédiat avec message d'erreur
|
||||
- 📋 Liste des scopes manquants affichée
|
||||
- 🔄 Commande de renouvellement suggérée
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Via Slash Command
|
||||
|
||||
```bash
|
||||
/git:pr <branche-base> [milestone] [projet] [--delete] [--no-review]
|
||||
```
|
||||
|
||||
### Via Skill Direct
|
||||
|
||||
```bash
|
||||
ARGUMENTS="develop 1.0.0 TMA --delete" skill dev:git-pr
|
||||
```
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Erreur: "Scopes manquants"
|
||||
|
||||
```bash
|
||||
❌ Scopes GitHub manquants: read:project project
|
||||
```
|
||||
|
||||
**Solution**: Relancer `gh_auth_setup.sh`
|
||||
|
||||
### Erreur: "Impossible de récupérer les projets"
|
||||
|
||||
Vérifier authentification:
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
|
||||
Si scopes corrects mais erreur persiste:
|
||||
```bash
|
||||
# Forcer renouvellement
|
||||
gh auth logout
|
||||
bash scripts/gh_auth_setup.sh
|
||||
```
|
||||
|
||||
### Erreur: "your authentication token is missing required scopes"
|
||||
|
||||
**Cause**: Token obsolète ou scopes révoqués
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
bash scripts/gh_auth_setup.sh
|
||||
```
|
||||
|
||||
## Cache Persistant
|
||||
|
||||
Le skill utilise un système de cache pour optimiser les performances.
|
||||
|
||||
### Cache Milestones
|
||||
|
||||
**Fichier**: `.claude/cache/git-milestones.json`
|
||||
|
||||
**Fonctionnalités**:
|
||||
- Stockage des milestones GitHub
|
||||
- Recherche par titre exact ou alias
|
||||
- Normalisation semver automatique (`26` → `26.0.0`)
|
||||
- Génération d'aliases depuis titres (`26.0.0 (Hotfix)` → alias `26.0.0`)
|
||||
|
||||
**Refresh**: Automatique si milestone introuvable
|
||||
|
||||
### Cache Projets
|
||||
|
||||
**Fichier**: `.claude/cache/git-projects.json`
|
||||
|
||||
**Fonctionnalités**:
|
||||
- Stockage des projets GitHub
|
||||
- Recherche case-insensitive par titre ou alias
|
||||
- Génération d'aliases depuis mots-clés (`Bug Tracking` → `["bug", "tracking"]`)
|
||||
|
||||
**Refresh**: Automatique si projet introuvable
|
||||
|
||||
### Commandes Utiles
|
||||
|
||||
```bash
|
||||
# Vider cache milestones
|
||||
rm .claude/cache/git-milestones.json
|
||||
|
||||
# Vider cache projets
|
||||
rm .claude/cache/git-projects.json
|
||||
|
||||
# Vider tout le cache
|
||||
rm -rf .claude/cache/
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
git-pr/
|
||||
├── SKILL.md # Définition du skill et workflow
|
||||
├── README.md # Cette documentation
|
||||
├── scripts/
|
||||
│ ├── gh_auth_setup.sh # Configuration automatique auth
|
||||
│ ├── verify_pr_template.sh
|
||||
│ ├── smart_qa.sh
|
||||
│ ├── analyze_changes.sh
|
||||
│ ├── confirm_base_branch.py
|
||||
│ ├── safe_push_pr.sh
|
||||
│ ├── assign_milestone.py # Assignation milestone avec cache
|
||||
│ ├── milestone_cache.py # Module cache milestones
|
||||
│ ├── assign_project.py # Assignation projet avec cache
|
||||
│ ├── project_cache.py # Module cache projets
|
||||
│ └── cleanup_branch.sh
|
||||
└── tests/
|
||||
├── run_tests.sh # Lance tous les tests
|
||||
├── test_milestone_cache.py # Tests unitaires milestones
|
||||
└── test_project_cache.py # Tests unitaires projets
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Ajout d'un Nouveau Scope
|
||||
|
||||
1. Modifier `gh_auth_setup.sh`:
|
||||
```bash
|
||||
REQUIRED_SCOPES=(
|
||||
# ... scopes existants
|
||||
"nouveau_scope" # Description
|
||||
)
|
||||
```
|
||||
|
||||
2. Modifier `SKILL.md` section "Scopes Requis"
|
||||
|
||||
3. Mettre à jour ce README
|
||||
|
||||
### Tests Unitaires
|
||||
|
||||
```bash
|
||||
# Lancer tous les tests
|
||||
bash tests/run_tests.sh
|
||||
|
||||
# Lancer un test spécifique
|
||||
cd tests
|
||||
python3 test_milestone_cache.py -v
|
||||
python3 test_project_cache.py -v
|
||||
```
|
||||
|
||||
### Test de Consistance
|
||||
|
||||
Après modification:
|
||||
```bash
|
||||
# Tester le script
|
||||
bash scripts/gh_auth_setup.sh
|
||||
|
||||
# Vérifier
|
||||
gh auth status
|
||||
|
||||
# Tester assignation projet
|
||||
python3 scripts/assign_project.py <pr-number> --project <nom>
|
||||
```
|
||||
326
skills/git-pr/SKILL.md
Normal file
326
skills/git-pr/SKILL.md
Normal file
@@ -0,0 +1,326 @@
|
||||
---
|
||||
name: git-pr
|
||||
description: >
|
||||
Automatise la création de Pull Requests GitHub avec workflow complet incluant:
|
||||
QA intelligente (PHP), commits structurés, assignation milestone et projet,
|
||||
code review automatique. Utilisable via /git:pr ou invocation automatique
|
||||
quand l'utilisateur demande à créer/ouvrir/faire une PR.
|
||||
allowed-tools: [Bash, Read, Write, TodoWrite, AskUserQuestion]
|
||||
model: claude-sonnet-4-5-20250929
|
||||
---
|
||||
|
||||
# Git PR Skill - Checklist d'exécution
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
SCRIPTS_DIR="/home/atournayre/.claude/plugins/marketplaces/atournayre-claude-plugin-marketplace/git/skills/git-pr/scripts"
|
||||
PR_TEMPLATE_PATH=".github/pull_request_template.md"
|
||||
```
|
||||
|
||||
## Checklist d'exécution
|
||||
|
||||
### 1. Initialisation
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
START_TIME=$(date +%s)
|
||||
```
|
||||
|
||||
EXÉCUTER TodoWrite avec ces todos exacts :
|
||||
```yaml
|
||||
todos:
|
||||
- content: "Vérifier template PR"
|
||||
status: "pending"
|
||||
activeForm: "Vérification du template PR"
|
||||
- content: "Lancer QA intelligente"
|
||||
status: "pending"
|
||||
activeForm: "Lancement de la QA intelligente"
|
||||
- content: "Analyser changements git"
|
||||
status: "pending"
|
||||
activeForm: "Analyse des changements git"
|
||||
- content: "Confirmer branche de base"
|
||||
status: "pending"
|
||||
activeForm: "Confirmation de la branche de base"
|
||||
- content: "Générer description PR intelligente"
|
||||
status: "pending"
|
||||
activeForm: "Génération de la description PR intelligente"
|
||||
- content: "Push et création PR"
|
||||
status: "pending"
|
||||
activeForm: "Push et création de la PR"
|
||||
- content: "Assigner milestone"
|
||||
status: "pending"
|
||||
activeForm: "Assignation du milestone"
|
||||
- content: "Assigner projet GitHub"
|
||||
status: "pending"
|
||||
activeForm: "Assignation du projet GitHub"
|
||||
- content: "Code review automatique"
|
||||
status: "pending"
|
||||
activeForm: "Code review automatique"
|
||||
- content: "Nettoyage branche locale"
|
||||
status: "pending"
|
||||
activeForm: "Nettoyage de la branche locale"
|
||||
```
|
||||
|
||||
### 2. Parsing arguments
|
||||
|
||||
EXÉCUTER ce bloc pour parser $ARGUMENTS :
|
||||
```bash
|
||||
ARGS=($ARGUMENTS)
|
||||
BRANCH_BASE=""
|
||||
MILESTONE=""
|
||||
PROJECT_NAME=""
|
||||
DELETE_FLAG=""
|
||||
NO_REVIEW_FLAG=""
|
||||
|
||||
for arg in "${ARGS[@]}"; do
|
||||
case "$arg" in
|
||||
--delete) DELETE_FLAG="--delete" ;;
|
||||
--no-review) NO_REVIEW_FLAG="--no-review" ;;
|
||||
*)
|
||||
if [ -z "$BRANCH_BASE" ]; then
|
||||
BRANCH_BASE="$arg"
|
||||
elif [ -z "$MILESTONE" ]; then
|
||||
MILESTONE="$arg"
|
||||
elif [ -z "$PROJECT_NAME" ]; then
|
||||
PROJECT_NAME="$arg"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
```
|
||||
|
||||
### 3. Vérification scopes GitHub
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
bash $SCRIPTS_DIR/check_scopes.sh
|
||||
```
|
||||
|
||||
- Exit 0 → continuer
|
||||
- Exit 1 → ARRÊT, afficher message du script
|
||||
|
||||
### 4. Template PR
|
||||
|
||||
- Marquer todo #1 in_progress
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
bash $SCRIPTS_DIR/verify_pr_template.sh "$PR_TEMPLATE_PATH"
|
||||
```
|
||||
|
||||
- Exit 0 → marquer todo #1 completed
|
||||
- Exit 1 → ARRÊT
|
||||
|
||||
### 5. QA (si pas --no-review)
|
||||
|
||||
- Marquer todo #2 in_progress
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
bash $SCRIPTS_DIR/smart_qa.sh
|
||||
```
|
||||
|
||||
- Exit 0 → marquer todo #2 completed
|
||||
- Exit 1 → ARRÊT
|
||||
|
||||
### 6. Analyse changements
|
||||
|
||||
- Marquer todo #3 in_progress
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
bash $SCRIPTS_DIR/analyze_changes.sh
|
||||
```
|
||||
|
||||
- Stocker sortie JSON
|
||||
- Marquer todo #3 completed
|
||||
|
||||
### 7. Branche de base
|
||||
|
||||
- Marquer todo #4 in_progress
|
||||
|
||||
Si BRANCH_BASE fourni :
|
||||
```bash
|
||||
python3 $SCRIPTS_DIR/confirm_base_branch.py --branch "$BRANCH_BASE"
|
||||
```
|
||||
|
||||
Sinon :
|
||||
```bash
|
||||
python3 $SCRIPTS_DIR/confirm_base_branch.py
|
||||
```
|
||||
|
||||
Si needs_user_input: true → utiliser AskUserQuestion :
|
||||
```yaml
|
||||
questions:
|
||||
- question: "Quelle branche de base pour la PR ?"
|
||||
header: "Branche"
|
||||
multiSelect: false
|
||||
options:
|
||||
- label: "develop"
|
||||
description: "Branche développement"
|
||||
- label: "main"
|
||||
description: "Branche production"
|
||||
```
|
||||
|
||||
- Marquer todo #4 completed
|
||||
|
||||
### 8. Génération description intelligente
|
||||
|
||||
- Marquer todo "Générer description PR intelligente" in_progress
|
||||
|
||||
EXÉCUTER pour récupérer les informations :
|
||||
```bash
|
||||
BRANCH_NAME=$(git branch --show-current)
|
||||
echo "=== COMMITS ==="
|
||||
git log $BRANCH_BASE..$BRANCH_NAME --oneline
|
||||
echo ""
|
||||
echo "=== DIFF STAT ==="
|
||||
git diff $BRANCH_BASE..$BRANCH_NAME --stat
|
||||
echo ""
|
||||
echo "=== FICHIERS MODIFIÉS ==="
|
||||
git diff $BRANCH_BASE..$BRANCH_NAME --name-only
|
||||
```
|
||||
|
||||
LIRE le template PR avec Read tool : `$PR_TEMPLATE_PATH`
|
||||
|
||||
**GÉNÉRER LA DESCRIPTION** en tant que Claude :
|
||||
1. Analyser les commits et le diff
|
||||
2. Remplir intelligemment chaque section du template :
|
||||
- **Bug fix** : supprimer si pas de fix, sinon lier l'issue
|
||||
- **Description** : résumer les changements basé sur les commits
|
||||
- **Type de changement** : cocher (✔️) les types appropriés basé sur les commits
|
||||
- **Tests** : indiquer si tests ajoutés/modifiés
|
||||
- **Checklist** : cocher ce qui s'applique
|
||||
- **Actions** : cocher ce qui est nécessaire
|
||||
3. Sauvegarder dans `/tmp/pr_body_generated.md` avec Write tool
|
||||
|
||||
### 9. Création PR
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
PR_NUMBER=$(bash $SCRIPTS_DIR/create_pr.sh "$BRANCH_BASE" "/tmp/pr_body_generated.md")
|
||||
```
|
||||
|
||||
- Exit 0 → stocker PR_NUMBER, marquer todo "Générer description PR intelligente" completed, puis marquer todo "Push et création PR" completed
|
||||
- Exit 1 → ARRÊT
|
||||
|
||||
### 10. Milestone
|
||||
|
||||
- Marquer todo "Assigner milestone" in_progress
|
||||
|
||||
Si MILESTONE fourni :
|
||||
```bash
|
||||
python3 $SCRIPTS_DIR/assign_milestone.py $PR_NUMBER --milestone "$MILESTONE"
|
||||
```
|
||||
|
||||
Sinon :
|
||||
```bash
|
||||
python3 $SCRIPTS_DIR/assign_milestone.py $PR_NUMBER
|
||||
```
|
||||
|
||||
Si needs_user_input: true → utiliser AskUserQuestion avec milestones disponibles
|
||||
|
||||
- Marquer todo "Assigner milestone" completed (même si échec, non bloquant)
|
||||
|
||||
### 11. Projet
|
||||
|
||||
- Marquer todo "Assigner projet GitHub" in_progress
|
||||
|
||||
Si PROJECT_NAME fourni :
|
||||
```bash
|
||||
python3 $SCRIPTS_DIR/assign_project.py $PR_NUMBER --project "$PROJECT_NAME"
|
||||
```
|
||||
|
||||
Sinon :
|
||||
```bash
|
||||
python3 $SCRIPTS_DIR/assign_project.py $PR_NUMBER
|
||||
```
|
||||
|
||||
Si needs_user_input: true → utiliser AskUserQuestion avec projets disponibles
|
||||
|
||||
- Marquer todo "Assigner projet GitHub" completed (même si échec, non bloquant)
|
||||
|
||||
### 12. Review intelligente (si pas --no-review)
|
||||
|
||||
- Marquer todo "Code review automatique" in_progress
|
||||
|
||||
EXÉCUTER pour récupérer les données :
|
||||
```bash
|
||||
PR_DATA=$(bash $SCRIPTS_DIR/auto_review.sh $PR_NUMBER)
|
||||
```
|
||||
|
||||
**ANALYSER EN TANT QUE CLAUDE** les données JSON retournées et générer une review intelligente :
|
||||
|
||||
1. **Conformité template PR** :
|
||||
- Vérifier que toutes les sections requises sont remplies
|
||||
- Signaler les sections manquantes ou incomplètes
|
||||
|
||||
2. **Qualité du code** :
|
||||
- Patterns suspects (code dupliqué, fonctions trop longues)
|
||||
- Problèmes de sécurité potentiels (injections, données sensibles)
|
||||
- Respect des conventions du projet
|
||||
|
||||
3. **Tests** :
|
||||
- Tests manquants pour les nouvelles fonctionnalités
|
||||
- Couverture des cas limites
|
||||
|
||||
4. **Documentation** :
|
||||
- Commentaires nécessaires absents
|
||||
- Mise à jour README si API modifiée
|
||||
|
||||
5. **Suggestions d'amélioration** :
|
||||
- Refactorisation possible
|
||||
- Performance
|
||||
- Lisibilité
|
||||
|
||||
**GÉNÉRER le commentaire de review** avec structure :
|
||||
```markdown
|
||||
## 🔍 Code Review Automatique
|
||||
|
||||
### ✅ Points positifs
|
||||
- [ce qui est bien fait]
|
||||
|
||||
### ⚠️ Points d'attention
|
||||
- [problèmes potentiels à vérifier]
|
||||
|
||||
### 💡 Suggestions
|
||||
- [améliorations possibles]
|
||||
|
||||
### 📋 Checklist conformité
|
||||
- [ ] Template PR complet
|
||||
- [ ] Tests présents
|
||||
- [ ] Documentation à jour
|
||||
|
||||
---
|
||||
*Review générée par git-pr skill*
|
||||
```
|
||||
|
||||
EXÉCUTER pour poster le commentaire :
|
||||
```bash
|
||||
gh pr comment $PR_NUMBER --body "$REVIEW_COMMENT"
|
||||
```
|
||||
|
||||
- Marquer todo "Code review automatique" completed
|
||||
|
||||
### 13. Nettoyage
|
||||
|
||||
- Marquer todo "Nettoyage branche locale" in_progress
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
bash $SCRIPTS_DIR/cleanup_branch.sh "$BRANCH_BASE" "$BRANCH_NAME" $DELETE_FLAG
|
||||
```
|
||||
|
||||
Si needs_user_input: true → utiliser AskUserQuestion pour confirmer suppression
|
||||
|
||||
- Marquer todo "Nettoyage branche locale" completed
|
||||
|
||||
### 14. Rapport final
|
||||
|
||||
EXÉCUTER :
|
||||
```bash
|
||||
bash $SCRIPTS_DIR/final_report.sh $PR_NUMBER $START_TIME
|
||||
```
|
||||
|
||||
Afficher le rapport YAML généré.
|
||||
31
skills/git-pr/scripts/analyze_changes.sh
Executable file
31
skills/git-pr/scripts/analyze_changes.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Collecte les statistiques git en une fois
|
||||
# Usage: analyze_changes.sh
|
||||
# Output: JSON avec stats des changements
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Collecter stats
|
||||
FILES_CHANGED=$(git diff --cached --numstat | wc -l)
|
||||
ADDITIONS=$(git diff --cached --numstat | awk '{sum+=$1} END {print sum+0}')
|
||||
DELETIONS=$(git diff --cached --numstat | awk '{sum+=$2} END {print sum+0}')
|
||||
|
||||
# Lister fichiers modifiés
|
||||
MODIFIED_FILES=$(git diff --cached --name-only | jq -R . | jq -s .)
|
||||
|
||||
# Détecter fichiers PHP
|
||||
HAS_PHP_FILES=false
|
||||
if git diff --cached --name-only | grep -q '\.php$'; then
|
||||
HAS_PHP_FILES=true
|
||||
fi
|
||||
|
||||
# Générer JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"files_changed": $FILES_CHANGED,
|
||||
"additions": $ADDITIONS,
|
||||
"deletions": $DELETIONS,
|
||||
"modified_files": $MODIFIED_FILES,
|
||||
"has_php_files": $HAS_PHP_FILES
|
||||
}
|
||||
EOF
|
||||
146
skills/git-pr/scripts/assign_milestone.py
Executable file
146
skills/git-pr/scripts/assign_milestone.py
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lister milestones et gérer assignation
|
||||
Usage: assign_milestone.py <pr_number> [--milestone <name>]
|
||||
Output: Milestone assigné ou "ignored" (stdout)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
from milestone_cache import MilestoneCache
|
||||
|
||||
|
||||
def get_repo_info():
|
||||
"""Récupère owner/repo depuis git remote"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
url = result.stdout.strip()
|
||||
# Extraire owner/repo depuis URL GitHub
|
||||
match = re.search(r'github\.com[:/](.+/.+?)(?:\.git)?$', url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
raise ValueError(f"Format URL invalide: {url}")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur récupération repo: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_open_milestones(repo):
|
||||
"""Récupère les milestones ouverts via gh API"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", "api", f"repos/{repo}/milestones", "--jq",
|
||||
"[.[] | select(.state == \"open\") | {number, title}]"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Erreur récupération milestones: {e.stderr}", file=sys.stderr)
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Erreur parsing JSON milestones: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def get_open_milestones_cached(repo):
|
||||
"""Récupère milestones avec cache"""
|
||||
cache = MilestoneCache()
|
||||
if not cache.cache.get("milestones"):
|
||||
milestones = get_open_milestones(repo)
|
||||
cache.refresh_from_api(milestones)
|
||||
return milestones
|
||||
return cache.cache["milestones"]
|
||||
|
||||
|
||||
def find_milestone(repo, query):
|
||||
"""Cherche milestone par query (exact ou partiel)"""
|
||||
cache = MilestoneCache()
|
||||
result = cache.find(query)
|
||||
if result:
|
||||
return result
|
||||
normalized = cache.normalize_semver(query)
|
||||
result = cache.find(normalized)
|
||||
if result:
|
||||
return result
|
||||
milestones = get_open_milestones(repo)
|
||||
cache.refresh_from_api(milestones)
|
||||
result = cache.find(normalized)
|
||||
if result:
|
||||
return result
|
||||
return cache.create(normalized)
|
||||
|
||||
|
||||
def assign_milestone(pr_number, milestone_title):
|
||||
"""Assigne un milestone à la PR"""
|
||||
try:
|
||||
subprocess.run(
|
||||
["gh", "pr", "edit", str(pr_number), "--milestone", milestone_title],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Erreur assignation milestone: {e.stderr}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Assigne un milestone à une PR")
|
||||
parser.add_argument("pr_number", type=int, help="Numéro de la PR")
|
||||
parser.add_argument("--milestone", help="Nom du milestone à assigner")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo = get_repo_info()
|
||||
milestones = get_open_milestones_cached(repo)
|
||||
|
||||
if not milestones:
|
||||
print("ℹ️ Aucun milestone ouvert - ignoré")
|
||||
print("ignored")
|
||||
sys.exit(0)
|
||||
|
||||
# Si --milestone fourni, utiliser find_milestone pour recherche intelligente
|
||||
if args.milestone:
|
||||
try:
|
||||
milestone = find_milestone(repo, args.milestone)
|
||||
if assign_milestone(args.pr_number, milestone['title']):
|
||||
print(f"✅ Milestone '{milestone['title']}' assigné")
|
||||
print(milestone['title'])
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Sinon, retourner JSON pour AskUserQuestion
|
||||
# Suggérer le premier milestone (généralement le plus récent)
|
||||
milestones_with_suggestion = [
|
||||
{
|
||||
"number": m["number"],
|
||||
"title": m["title"],
|
||||
"is_suggested": i == 0
|
||||
}
|
||||
for i, m in enumerate(milestones)
|
||||
]
|
||||
|
||||
output = {
|
||||
"milestones": milestones_with_suggestion,
|
||||
"needs_user_input": True
|
||||
}
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
182
skills/git-pr/scripts/assign_project.py
Executable file
182
skills/git-pr/scripts/assign_project.py
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Assigner PR à projet GitHub
|
||||
Usage: assign_project.py <pr_number> [--project <name>]
|
||||
Output: Projet assigné ou "ignored" (stdout)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
from project_cache import ProjectCache
|
||||
|
||||
|
||||
def get_repo_info():
|
||||
"""Récupère owner/repo depuis git remote"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
url = result.stdout.strip()
|
||||
match = re.search(r'github\.com[:/](.+/.+?)(?:\.git)?$', url)
|
||||
if match:
|
||||
repo_full = match.group(1)
|
||||
owner, repo = repo_full.split('/')
|
||||
return owner, repo
|
||||
raise ValueError(f"Format URL invalide: {url}")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur récupération repo: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_projects_list(owner):
|
||||
"""Récupère les projets via gh project list"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", "project", "list", "--owner", owner, "--format", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
projects_data = json.loads(result.stdout)
|
||||
projects = []
|
||||
for project in projects_data.get('projects', []):
|
||||
projects.append({
|
||||
'id': project.get('id'),
|
||||
'title': project.get('title'),
|
||||
'number': project.get('number')
|
||||
})
|
||||
return projects
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"⚠️ Impossible de récupérer les projets: {e.stderr}", file=sys.stderr)
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Erreur parsing JSON projets: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def get_projects_cached(owner):
|
||||
"""Récupère projets avec cache"""
|
||||
cache = ProjectCache()
|
||||
if not cache.cache.get("projects"):
|
||||
projects = get_projects_list(owner)
|
||||
cache.refresh_from_api(projects)
|
||||
return projects
|
||||
return cache.cache["projects"]
|
||||
|
||||
|
||||
def find_project(owner, query):
|
||||
"""Cherche projet par query (exact ou alias)"""
|
||||
cache = ProjectCache()
|
||||
result = cache.find(query)
|
||||
if result:
|
||||
return result
|
||||
projects = get_projects_list(owner)
|
||||
cache.refresh_from_api(projects)
|
||||
return cache.find(query)
|
||||
|
||||
|
||||
|
||||
|
||||
def assign_pr_to_project(pr_number, project_id, owner, repo):
|
||||
"""Assigne la PR au projet via GraphQL"""
|
||||
# Récupérer l'ID de la PR
|
||||
try:
|
||||
pr_result = subprocess.run(
|
||||
["gh", "pr", "view", str(pr_number), "--json", "id"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
pr_data = json.loads(pr_result.stdout)
|
||||
pr_id = pr_data['id']
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur récupération ID PR: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Ajouter la PR au projet
|
||||
mutation = """
|
||||
mutation($projectId: ID!, $contentId: ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["gh", "api", "graphql",
|
||||
"-f", f"query={mutation}",
|
||||
"-f", f"projectId={project_id}",
|
||||
"-f", f"contentId={pr_id}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Erreur assignation projet: {e.stderr}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Assigne une PR à un projet GitHub")
|
||||
parser.add_argument("pr_number", type=int, help="Numéro de la PR")
|
||||
parser.add_argument("--project", help="Nom du projet à assigner")
|
||||
args = parser.parse_args()
|
||||
|
||||
owner, repo = get_repo_info()
|
||||
projects = get_projects_cached(owner)
|
||||
|
||||
if not projects:
|
||||
print("ℹ️ Aucun projet trouvé - ignoré")
|
||||
print("ignored")
|
||||
sys.exit(0)
|
||||
|
||||
# Si --project fourni, utiliser find_project pour recherche intelligente
|
||||
if args.project:
|
||||
try:
|
||||
project = find_project(owner, args.project)
|
||||
if not project:
|
||||
print(f"❌ Projet '{args.project}' non trouvé", file=sys.stderr)
|
||||
available = [p['title'] for p in projects]
|
||||
print(f"Projets disponibles: {', '.join(available)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if assign_pr_to_project(args.pr_number, project['id'], owner, repo):
|
||||
print(f"✅ Projet '{project['title']}' assigné")
|
||||
print(project['title'])
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Sinon, retourner JSON pour AskUserQuestion
|
||||
projects_list = [
|
||||
{
|
||||
"id": p["id"],
|
||||
"title": p["title"],
|
||||
"number": p.get("number")
|
||||
}
|
||||
for p in projects
|
||||
]
|
||||
|
||||
output = {
|
||||
"projects": projects_list,
|
||||
"needs_user_input": True
|
||||
}
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
41
skills/git-pr/scripts/auto_review.sh
Executable file
41
skills/git-pr/scripts/auto_review.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Récupère les données de la PR pour analyse intelligente par Claude
|
||||
# Usage: auto_review.sh <pr_number>
|
||||
# Sortie: JSON avec toutes les informations nécessaires pour la review
|
||||
# Exit 0 si OK, Exit 1 si échec
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PR_NUMBER="$1"
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "❌ PR_NUMBER requis" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Récupérer les informations de la PR
|
||||
PR_INFO=$(gh pr view "$PR_NUMBER" --json title,body,files,additions,deletions,commits,baseRefName,headRefName 2>/dev/null)
|
||||
|
||||
if [ -z "$PR_INFO" ]; then
|
||||
echo "❌ Impossible de récupérer les informations de la PR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Récupérer le diff complet
|
||||
DIFF=$(gh pr diff "$PR_NUMBER" 2>/dev/null || echo "")
|
||||
|
||||
# Récupérer le template PR s'il existe
|
||||
PR_TEMPLATE=""
|
||||
if [ -f ".github/pull_request_template.md" ]; then
|
||||
PR_TEMPLATE=$(cat .github/pull_request_template.md)
|
||||
fi
|
||||
|
||||
# Générer sortie JSON pour Claude
|
||||
cat <<EOF
|
||||
{
|
||||
"pr_number": $PR_NUMBER,
|
||||
"pr_info": $PR_INFO,
|
||||
"diff": $(echo "$DIFF" | jq -Rs .),
|
||||
"template": $(echo "$PR_TEMPLATE" | jq -Rs .)
|
||||
}
|
||||
EOF
|
||||
36
skills/git-pr/scripts/check_scopes.sh
Executable file
36
skills/git-pr/scripts/check_scopes.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Vérifie que tous les scopes GitHub requis sont présents
|
||||
# Exit 0 si OK, Exit 1 si scopes manquants
|
||||
|
||||
set -e
|
||||
|
||||
REQUIRED_SCOPES=(repo read:org project gist)
|
||||
|
||||
# Récupérer scopes actuels
|
||||
CURRENT_SCOPES=$(gh auth status 2>&1 | grep "Token scopes" | sed 's/.*Token scopes: //' | tr -d "'" | tr ',' ' ')
|
||||
|
||||
if [ -z "$CURRENT_SCOPES" ]; then
|
||||
echo "❌ Impossible de récupérer les scopes GitHub"
|
||||
echo " Vérifier l'authentification avec: gh auth status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier chaque scope requis
|
||||
MISSING_SCOPES=()
|
||||
for scope in "${REQUIRED_SCOPES[@]}"; do
|
||||
if ! echo "$CURRENT_SCOPES" | grep -qw "$scope"; then
|
||||
MISSING_SCOPES+=("$scope")
|
||||
fi
|
||||
done
|
||||
|
||||
# Rapport
|
||||
if [ ${#MISSING_SCOPES[@]} -gt 0 ]; then
|
||||
echo "❌ Scopes GitHub manquants: ${MISSING_SCOPES[*]}"
|
||||
echo ""
|
||||
echo "🔄 Pour renouveler l'authentification:"
|
||||
echo " bash $(dirname "$0")/gh_auth_setup.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Scopes GitHub valides"
|
||||
exit 0
|
||||
28
skills/git-pr/scripts/cleanup_branch.sh
Executable file
28
skills/git-pr/scripts/cleanup_branch.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Proposer/exécuter suppression branche locale
|
||||
# Usage: cleanup_branch.sh <branch_base> <branch_name> [--delete]
|
||||
# Exit: 0=supprimée, 1=conservée
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH_BASE="$1"
|
||||
BRANCH_NAME="$2"
|
||||
DELETE_FLAG="${3:-}"
|
||||
|
||||
# Si --delete fourni, suppression auto
|
||||
if [ "$DELETE_FLAG" = "--delete" ]; then
|
||||
echo "🗑️ Suppression automatique de la branche $BRANCH_NAME..."
|
||||
git checkout "$BRANCH_BASE"
|
||||
git branch -D "$BRANCH_NAME"
|
||||
echo "✅ Branche $BRANCH_NAME supprimée"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Sinon, retourner JSON pour AskUserQuestion
|
||||
cat <<EOF
|
||||
{
|
||||
"branch_name": "$BRANCH_NAME",
|
||||
"branch_base": "$BRANCH_BASE",
|
||||
"needs_user_input": true
|
||||
}
|
||||
EOF
|
||||
84
skills/git-pr/scripts/confirm_base_branch.py
Executable file
84
skills/git-pr/scripts/confirm_base_branch.py
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Détecte les branches disponibles et gère la confirmation
|
||||
Usage: confirm_base_branch.py [--branch <name>]
|
||||
Output: Nom branche validé (stdout) ou JSON options
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
|
||||
|
||||
def get_remote_branches():
|
||||
"""Récupère les branches remote et filtre develop/main/master/release/*"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "branch", "-r"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
branches = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
branch = line.strip()
|
||||
# Retirer origin/
|
||||
branch = re.sub(r'^origin/', '', branch)
|
||||
# Ignorer HEAD
|
||||
if 'HEAD' in branch:
|
||||
continue
|
||||
# Filtrer develop/main/master/release/*/hotfix/*
|
||||
if branch in ['develop', 'main', 'master'] or branch.startswith('release/') or branch.startswith('hotfix/'):
|
||||
branches.append(branch)
|
||||
|
||||
return sorted(set(branches))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Erreur lors de la récupération des branches: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def validate_branch(branch_name, available_branches):
|
||||
"""Valide qu'une branche existe"""
|
||||
if branch_name in available_branches:
|
||||
return True
|
||||
print(f"❌ Branche '{branch_name}' non trouvée", file=sys.stderr)
|
||||
print(f"Branches disponibles: {', '.join(available_branches)}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Confirme la branche de base pour la PR")
|
||||
parser.add_argument("--branch", help="Nom de la branche de base")
|
||||
args = parser.parse_args()
|
||||
|
||||
branches = get_remote_branches()
|
||||
|
||||
if not branches:
|
||||
print("❌ Aucune branche develop/main/master/release/hotfix trouvée", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Si --branch fourni, valider et retourner
|
||||
if args.branch:
|
||||
if validate_branch(args.branch, branches):
|
||||
print(args.branch)
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
# Sinon, retourner JSON pour AskUserQuestion
|
||||
suggested = "develop" if "develop" in branches else branches[0]
|
||||
|
||||
output = {
|
||||
"branches": branches,
|
||||
"suggested": suggested,
|
||||
"needs_user_input": True
|
||||
}
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
61
skills/git-pr/scripts/create_pr.sh
Executable file
61
skills/git-pr/scripts/create_pr.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# Création complète de PR: lit template, génère titre, crée PR
|
||||
# Usage: create_pr.sh <branch_base> <pr_template_path>
|
||||
# Output: PR_NUMBER (stdout) ou exit 1
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH_BASE="$1"
|
||||
PR_TEMPLATE_PATH="$2"
|
||||
SCRIPTS_DIR="$(dirname "$0")"
|
||||
|
||||
# Récupérer branche courante
|
||||
BRANCH_NAME=$(git branch --show-current)
|
||||
|
||||
# Lire template PR
|
||||
if [ ! -f "$PR_TEMPLATE_PATH" ]; then
|
||||
echo "❌ Template PR absent: $PR_TEMPLATE_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
PR_TEMPLATE=$(cat "$PR_TEMPLATE_PATH")
|
||||
|
||||
# Détecter issue depuis nom de branche (ex: feat/123-description, fix/456-bug)
|
||||
ISSUE_NUMBER=$(echo "$BRANCH_NAME" | grep -oE '[0-9]+' | head -1 || echo "")
|
||||
|
||||
# Générer titre PR
|
||||
if [ -n "$ISSUE_NUMBER" ]; then
|
||||
# Vérifier que l'issue existe et récupérer son titre
|
||||
ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --json title -q '.title' 2>/dev/null || echo "")
|
||||
if [ -n "$ISSUE_TITLE" ]; then
|
||||
PR_TITLE="$ISSUE_TITLE / Issue #$ISSUE_NUMBER"
|
||||
echo "✅ Titre PR basé sur issue #$ISSUE_NUMBER" >&2
|
||||
else
|
||||
# Issue non trouvée, utiliser nom de branche
|
||||
echo "⚠️ Issue #$ISSUE_NUMBER non trouvée" >&2
|
||||
# Nettoyer le nom de branche pour créer un titre
|
||||
PR_TITLE=$(echo "$BRANCH_NAME" | sed 's|^[^/]*/||' | sed 's/-/ /g' | sed 's/[0-9]* *//')
|
||||
PR_TITLE="${PR_TITLE^}" # Capitalize first letter
|
||||
fi
|
||||
else
|
||||
# Pas de numéro d'issue, utiliser nom de branche
|
||||
echo "ℹ️ Pas d'issue détectée dans '$BRANCH_NAME'" >&2
|
||||
PR_TITLE=$(echo "$BRANCH_NAME" | sed 's|^[^/]*/||' | sed 's/-/ /g')
|
||||
PR_TITLE="${PR_TITLE^}"
|
||||
fi
|
||||
|
||||
# Créer fichier temporaire avec le body
|
||||
PR_BODY_FILE="/tmp/pr_body_$(date +%s).md"
|
||||
echo "$PR_TEMPLATE" > "$PR_BODY_FILE"
|
||||
|
||||
# Appeler le script de push sécurisé
|
||||
PR_NUMBER=$(bash "$SCRIPTS_DIR/safe_push_pr.sh" "$BRANCH_BASE" "$BRANCH_NAME" "$PR_TITLE" "$PR_BODY_FILE")
|
||||
EXIT_CODE=$?
|
||||
|
||||
# Nettoyer
|
||||
rm -f "$PR_BODY_FILE"
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$PR_NUMBER"
|
||||
70
skills/git-pr/scripts/final_report.sh
Executable file
70
skills/git-pr/scripts/final_report.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Génère le rapport final YAML de la PR
|
||||
# Usage: final_report.sh <pr_number> <start_time>
|
||||
# Output: Rapport YAML formaté
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PR_NUMBER="$1"
|
||||
START_TIME="$2"
|
||||
|
||||
if [ -z "$PR_NUMBER" ] || [ -z "$START_TIME" ]; then
|
||||
echo "❌ Usage: final_report.sh <pr_number> <start_time>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Récupérer infos de la PR
|
||||
PR_INFO=$(gh pr view "$PR_NUMBER" --json title,url,baseRefName,headRefName,additions,deletions,changedFiles 2>/dev/null || echo "{}")
|
||||
|
||||
PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // "N/A"')
|
||||
PR_URL=$(echo "$PR_INFO" | jq -r '.url // "N/A"')
|
||||
BRANCH_BASE=$(echo "$PR_INFO" | jq -r '.baseRefName // "N/A"')
|
||||
BRANCH_NAME=$(echo "$PR_INFO" | jq -r '.headRefName // "N/A"')
|
||||
ADDITIONS=$(echo "$PR_INFO" | jq -r '.additions // 0')
|
||||
DELETIONS=$(echo "$PR_INFO" | jq -r '.deletions // 0')
|
||||
FILES_CHANGED=$(echo "$PR_INFO" | jq -r '.changedFiles // 0')
|
||||
|
||||
# Calculer durée
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
|
||||
# Formater durée
|
||||
if [ $DURATION -lt 60 ]; then
|
||||
DURATION_STR="${DURATION}s"
|
||||
elif [ $DURATION -lt 3600 ]; then
|
||||
MINUTES=$((DURATION / 60))
|
||||
SECONDS=$((DURATION % 60))
|
||||
DURATION_STR="${MINUTES}m ${SECONDS}s"
|
||||
else
|
||||
HOURS=$((DURATION / 3600))
|
||||
MINUTES=$(((DURATION % 3600) / 60))
|
||||
SECONDS=$((DURATION % 60))
|
||||
DURATION_STR="${HOURS}h ${MINUTES}m ${SECONDS}s"
|
||||
fi
|
||||
|
||||
# Formater timestamps
|
||||
START_DATE=$(date -d "@$START_TIME" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$START_TIME" '+%Y-%m-%d %H:%M:%S')
|
||||
END_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Générer rapport YAML
|
||||
cat <<EOF
|
||||
task: "Pull Request créée avec succès"
|
||||
status: "completed"
|
||||
|
||||
details:
|
||||
pr_number: $PR_NUMBER
|
||||
pr_title: "$PR_TITLE"
|
||||
pr_url: "$PR_URL"
|
||||
branch_source: "$BRANCH_NAME"
|
||||
branch_base: "$BRANCH_BASE"
|
||||
|
||||
stats:
|
||||
files_changed: $FILES_CHANGED
|
||||
additions: $ADDITIONS
|
||||
deletions: $DELETIONS
|
||||
|
||||
timing:
|
||||
start: "$START_DATE"
|
||||
end: "$END_DATE"
|
||||
duration: "$DURATION_STR"
|
||||
EOF
|
||||
38
skills/git-pr/scripts/gh_auth_setup.sh
Executable file
38
skills/git-pr/scripts/gh_auth_setup.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configuration authentification GitHub avec tous les scopes requis
|
||||
# Usage: bash gh_auth_setup.sh
|
||||
|
||||
set -e
|
||||
|
||||
REQUIRED_SCOPES=(
|
||||
"repo" # Accès complet aux repos (PRs, commits, etc.)
|
||||
"read:org" # Lecture infos organisation
|
||||
"read:project" # Lecture projets GitHub
|
||||
"project" # Écriture/assignation aux projets
|
||||
"gist" # Gestion des gists
|
||||
)
|
||||
|
||||
echo "🔐 Configuration authentification GitHub"
|
||||
echo ""
|
||||
echo "Scopes requis:"
|
||||
for scope in "${REQUIRED_SCOPES[@]}"; do
|
||||
echo " - $scope"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Construire la commande avec tous les scopes
|
||||
CMD="gh auth refresh --hostname github.com"
|
||||
for scope in "${REQUIRED_SCOPES[@]}"; do
|
||||
CMD="$CMD -s $scope"
|
||||
done
|
||||
|
||||
echo "🔄 Exécution: $CMD"
|
||||
echo ""
|
||||
|
||||
eval "$CMD"
|
||||
|
||||
echo ""
|
||||
echo "✅ Authentification configurée avec succès"
|
||||
echo ""
|
||||
echo "Vérification des scopes:"
|
||||
gh auth status
|
||||
156
skills/git-pr/scripts/milestone_cache.py
Normal file
156
skills/git-pr/scripts/milestone_cache.py
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Module de cache pour milestones GitHub"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CACHE_DIR = Path(".claude/cache")
|
||||
CACHE_FILE = CACHE_DIR / "git-milestones.json"
|
||||
|
||||
|
||||
class MilestoneCache:
|
||||
"""Gestion cache milestones"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = self.load()
|
||||
|
||||
def load(self) -> dict:
|
||||
"""Charge cache depuis fichier"""
|
||||
if not CACHE_FILE.exists():
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return {"updated_at": None, "milestones": []}
|
||||
try:
|
||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return {"updated_at": None, "milestones": []}
|
||||
|
||||
def save(self):
|
||||
"""Sauvegarde cache vers fichier"""
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
self.cache["updated_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.cache, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def find(self, query: str) -> Optional[dict]:
|
||||
"""Cherche milestone par titre exact ou alias"""
|
||||
for milestone in self.cache.get("milestones", []):
|
||||
if milestone["title"] == query:
|
||||
return milestone
|
||||
if query in milestone.get("aliases", []):
|
||||
return milestone
|
||||
return None
|
||||
|
||||
def add(self, milestone: dict):
|
||||
"""Ajoute milestone au cache avec génération aliases"""
|
||||
aliases = self.generate_aliases(milestone["title"])
|
||||
milestone["aliases"] = aliases
|
||||
existing = [m for m in self.cache.get("milestones", []) if m["number"] == milestone["number"]]
|
||||
if not existing:
|
||||
if "milestones" not in self.cache:
|
||||
self.cache["milestones"] = []
|
||||
self.cache["milestones"].append(milestone)
|
||||
self.save()
|
||||
|
||||
def generate_aliases(self, title: str) -> list[str]:
|
||||
"""Génère aliases depuis titre (version exacte + formes courtes)
|
||||
|
||||
Exemples:
|
||||
- "26.1.1 (Hotfix)" → ["26.1.1", "26.1", "26"]
|
||||
- "26.1.0" → ["26.1.0", "26.1", "26"]
|
||||
- "26.0.0 (Avenant)" → ["26.0.0", "26.0", "26"]
|
||||
|
||||
Logique: extraire version semver et générer formes courtes
|
||||
"""
|
||||
match = re.match(r'^(\d+)\.(\d+)\.(\d+)', title)
|
||||
if match:
|
||||
major, minor, patch = match.groups()
|
||||
aliases = []
|
||||
full_version = f"{major}.{minor}.{patch}"
|
||||
if full_version != title:
|
||||
aliases.append(full_version)
|
||||
aliases.append(f"{major}.{minor}")
|
||||
aliases.append(major)
|
||||
return aliases
|
||||
return []
|
||||
|
||||
def refresh_from_api(self, milestones: list[dict]):
|
||||
"""Remplace cache entièrement avec données API"""
|
||||
enriched = []
|
||||
for milestone in milestones:
|
||||
aliases = self.generate_aliases(milestone["title"])
|
||||
enriched.append({
|
||||
"number": milestone["number"],
|
||||
"title": milestone["title"],
|
||||
"aliases": aliases
|
||||
})
|
||||
self.cache["milestones"] = enriched
|
||||
self.save()
|
||||
|
||||
def create(self, title: str) -> dict:
|
||||
"""Crée milestone sur GitHub via API et l'ajoute au cache
|
||||
|
||||
Récupère repo via get_repo_info()
|
||||
gh api repos/{repo}/milestones -f title="99.0.0" -f state="open"
|
||||
Retourne: {"number": 43, "title": "99.0.0", ...}
|
||||
"""
|
||||
repo = self.get_repo_info()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", "api", f"repos/{repo}/milestones", "-f", f"title={title}", "-f", "state=open"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
milestone = json.loads(result.stdout)
|
||||
self.add(milestone)
|
||||
return milestone
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Erreur création milestone: {e.stderr}", file=sys.stderr)
|
||||
raise
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Erreur parsing JSON: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
def get_repo_info(self) -> str:
|
||||
"""Récupère owner/repo depuis git remote"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
url = result.stdout.strip()
|
||||
match = re.search(r'github\.com[:/](.+/.+?)(?:\.git)?$', url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
raise ValueError(f"Format URL invalide: {url}")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur récupération repo: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
def normalize_semver(self, version: str) -> str:
|
||||
"""Normalise version en semver complet
|
||||
|
||||
Exemples:
|
||||
- "26" → "26.0.0"
|
||||
- "26.1" → "26.1.0"
|
||||
- "26.1.1" → "26.1.1"
|
||||
- "26.0.0 (Avenant)" → "26.0.0 (Avenant)" (conserve suffixe)
|
||||
"""
|
||||
match = re.match(r'^(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)?$', version)
|
||||
if not match:
|
||||
return version
|
||||
major = match.group(1)
|
||||
minor = match.group(2) or "0"
|
||||
patch = match.group(3) or "0"
|
||||
suffix = match.group(4) or ""
|
||||
return f"{major}.{minor}.{patch}{suffix}"
|
||||
111
skills/git-pr/scripts/project_cache.py
Normal file
111
skills/git-pr/scripts/project_cache.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Module de cache pour projets GitHub"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CACHE_DIR = Path(".claude/cache")
|
||||
CACHE_FILE = CACHE_DIR / "git-projects.json"
|
||||
|
||||
|
||||
class ProjectCache:
|
||||
"""Gestion cache projets"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = self.load()
|
||||
|
||||
def load(self) -> dict:
|
||||
"""Charge cache depuis fichier"""
|
||||
if not CACHE_FILE.exists():
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return {"updated_at": None, "projects": []}
|
||||
try:
|
||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return {"updated_at": None, "projects": []}
|
||||
|
||||
def save(self):
|
||||
"""Sauvegarde cache vers fichier"""
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
self.cache["updated_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.cache, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def find(self, query: str) -> Optional[dict]:
|
||||
"""Cherche projet par titre exact ou alias (case-insensitive)"""
|
||||
query_lower = query.lower()
|
||||
for project in self.cache.get("projects", []):
|
||||
if project["title"].lower() == query_lower:
|
||||
return project
|
||||
if query_lower in [alias.lower() for alias in project.get("aliases", [])]:
|
||||
return project
|
||||
return None
|
||||
|
||||
def add(self, project: dict):
|
||||
"""Ajoute projet au cache avec génération aliases"""
|
||||
aliases = self.generate_aliases(project["title"])
|
||||
project["aliases"] = aliases
|
||||
existing = [p for p in self.cache.get("projects", []) if p["id"] == project["id"]]
|
||||
if not existing:
|
||||
if "projects" not in self.cache:
|
||||
self.cache["projects"] = []
|
||||
self.cache["projects"].append(project)
|
||||
self.save()
|
||||
|
||||
def generate_aliases(self, title: str) -> list[str]:
|
||||
"""Génère aliases depuis titre
|
||||
|
||||
Exemples:
|
||||
- "Project Alpha" → ["project", "alpha"]
|
||||
- "Sprint 2024-Q1" → ["sprint", "2024", "q1"]
|
||||
- "Bug Tracking" → ["bug", "tracking"]
|
||||
|
||||
Logique: extraire mots-clés significatifs
|
||||
"""
|
||||
aliases = []
|
||||
words = re.findall(r'[\w]+', title.lower())
|
||||
for word in words:
|
||||
if len(word) >= 2 and word not in ['the', 'and', 'for']:
|
||||
aliases.append(word)
|
||||
return list(set(aliases))
|
||||
|
||||
def refresh_from_api(self, projects: list[dict]):
|
||||
"""Remplace cache entièrement avec données API"""
|
||||
enriched = []
|
||||
for project in projects:
|
||||
aliases = self.generate_aliases(project["title"])
|
||||
enriched.append({
|
||||
"id": project["id"],
|
||||
"title": project["title"],
|
||||
"number": project.get("number"),
|
||||
"aliases": aliases
|
||||
})
|
||||
self.cache["projects"] = enriched
|
||||
self.save()
|
||||
|
||||
def get_repo_info(self) -> str:
|
||||
"""Récupère owner/repo depuis git remote"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
url = result.stdout.strip()
|
||||
match = re.search(r'github\.com[:/](.+/.+?)(?:\.git)?$', url)
|
||||
if match:
|
||||
repo_full = match.group(1)
|
||||
owner, _ = repo_full.split('/')
|
||||
return owner
|
||||
raise ValueError(f"Format URL invalide: {url}")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur récupération repo: {e}", file=sys.stderr)
|
||||
raise
|
||||
73
skills/git-pr/scripts/safe_push_pr.sh
Executable file
73
skills/git-pr/scripts/safe_push_pr.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Vérifications sécurité + push + création PR
|
||||
# Usage: safe_push_pr.sh <branch_base> <branch_name> <pr_title> <pr_body_file>
|
||||
# Output: PR_NUMBER (stdout) ou exit 1
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH_BASE="$1"
|
||||
BRANCH_NAME="$2"
|
||||
PR_TITLE="$3"
|
||||
PR_BODY_FILE="$4"
|
||||
|
||||
# Vérification 1: Branche courante
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
if [ "$CURRENT_BRANCH" != "$BRANCH_NAME" ]; then
|
||||
echo "❌ Branche courante ($CURRENT_BRANCH) != branche attendue ($BRANCH_NAME)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérification 2: Commits à pousser
|
||||
COMMITS_TO_PUSH=$(git log --oneline "origin/$BRANCH_BASE..$BRANCH_NAME" 2>/dev/null | wc -l || echo "0")
|
||||
|
||||
if [ "$COMMITS_TO_PUSH" -eq 0 ]; then
|
||||
echo "❌ Aucun commit à pousser vers $BRANCH_BASE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérification 3: Afficher commits
|
||||
echo "📋 Commits à pousser ($COMMITS_TO_PUSH):"
|
||||
git log --oneline "origin/$BRANCH_BASE..$BRANCH_NAME" | sed 's/^/ /'
|
||||
|
||||
# Vérification 4: Vérifier que le fichier body existe
|
||||
if [ ! -f "$PR_BODY_FILE" ]; then
|
||||
echo "❌ Fichier PR body absent: $PR_BODY_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push vers origin
|
||||
echo "🚀 Push vers origin/$BRANCH_NAME..."
|
||||
if ! git push -u origin "$BRANCH_NAME"; then
|
||||
echo "❌ Échec du push" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Création PR
|
||||
echo "📝 Création de la Pull Request..."
|
||||
PR_URL=$(gh pr create \
|
||||
--base "$BRANCH_BASE" \
|
||||
--title "$PR_TITLE" \
|
||||
--body-file "$PR_BODY_FILE" \
|
||||
2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Échec création PR: $PR_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extraire le numéro de PR depuis l'URL
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -oP '/pull/\K\d+' || echo "")
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
# Fallback: extraire depuis gh pr view
|
||||
PR_NUMBER=$(gh pr view --json number -q .number 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "❌ Impossible d'extraire le numéro de PR" >&2
|
||||
echo "URL: $PR_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ PR #$PR_NUMBER créée: $PR_URL"
|
||||
echo "$PR_NUMBER"
|
||||
31
skills/git-pr/scripts/smart_qa.sh
Executable file
31
skills/git-pr/scripts/smart_qa.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Détecte les fichiers PHP modifiés et lance make qa si nécessaire
|
||||
# Usage: smart_qa.sh
|
||||
# Exit: 0=ok/ignoré, 1=échec QA
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Détecter fichiers PHP modifiés
|
||||
PHP_FILES=$(git diff --name-only --cached | grep '\.php$' || true)
|
||||
|
||||
if [ -z "$PHP_FILES" ]; then
|
||||
echo "ℹ️ Aucun fichier PHP modifié - QA ignorée"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Fichiers PHP détectés - Lancement de make qa..."
|
||||
echo "$PHP_FILES" | sed 's/^/ - /'
|
||||
|
||||
# Lancer QA avec timeout
|
||||
if timeout 600 make qa; then
|
||||
echo "✅ QA passée avec succès"
|
||||
exit 0
|
||||
else
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 124 ]; then
|
||||
echo "❌ QA timeout (>600s)" >&2
|
||||
else
|
||||
echo "❌ QA échouée" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
16
skills/git-pr/scripts/verify_pr_template.sh
Executable file
16
skills/git-pr/scripts/verify_pr_template.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Vérifie l'existence du template PR
|
||||
# Usage: verify_pr_template.sh <chemin_template>
|
||||
# Exit: 0=ok, 1=absent
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TEMPLATE_PATH="${1:-.github/pull_request_template.md}"
|
||||
|
||||
if [ ! -f "$TEMPLATE_PATH" ]; then
|
||||
echo "❌ Template PR absent: $TEMPLATE_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Template PR trouvé: $TEMPLATE_PATH"
|
||||
exit 0
|
||||
15
skills/git-pr/tests/.gitignore
vendored
Normal file
15
skills/git-pr/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Cache test local
|
||||
.claude/
|
||||
|
||||
# Éditeurs
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
18
skills/git-pr/tests/run_tests.sh
Executable file
18
skills/git-pr/tests/run_tests.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Lance tous les tests unitaires du skill git-pr
|
||||
|
||||
set -e
|
||||
|
||||
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
echo "🧪 Lancement tests git-pr..."
|
||||
echo ""
|
||||
|
||||
python3 test_milestone_cache.py -v
|
||||
echo ""
|
||||
|
||||
python3 test_project_cache.py -v
|
||||
echo ""
|
||||
|
||||
echo "✅ Tous les tests passés"
|
||||
210
skills/git-pr/tests/test_milestone_cache.py
Normal file
210
skills/git-pr/tests/test_milestone_cache.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests unitaires pour milestone_cache"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
from milestone_cache import MilestoneCache, CACHE_DIR, CACHE_FILE
|
||||
|
||||
|
||||
class TestNormalizeSemver(unittest.TestCase):
|
||||
"""Test normalisation semver"""
|
||||
|
||||
def test_normalise_un_chiffre_ajoute_zero_zero(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.normalize_semver("26"), "26.0.0")
|
||||
|
||||
def test_normalise_deux_chiffres_ajoute_zero(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.normalize_semver("26.1"), "26.1.0")
|
||||
|
||||
def test_normalise_trois_chiffres_reste_identique(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.normalize_semver("26.1.1"), "26.1.1")
|
||||
|
||||
def test_normalise_conserve_suffixe(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.normalize_semver("26.0.0 (Avenant)"), "26.0.0 (Avenant)")
|
||||
|
||||
|
||||
class TestGenerateAliases(unittest.TestCase):
|
||||
"""Test génération aliases stricte"""
|
||||
|
||||
def test_genere_alias_pour_titre_avec_parentheses(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.generate_aliases("26.1.1 (Hotfix)"), ["26.1.1"])
|
||||
|
||||
def test_genere_alias_pour_titre_avec_avenant(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.generate_aliases("26.0.0 (Avenant)"), ["26.0.0"])
|
||||
|
||||
def test_genere_pas_alias_pour_titre_sans_parentheses(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.generate_aliases("26.1.0"), [])
|
||||
|
||||
def test_genere_pas_alias_pour_titre_non_semver(self):
|
||||
cache = MilestoneCache()
|
||||
self.assertEqual(cache.generate_aliases("Release Candidate"), [])
|
||||
|
||||
|
||||
class TestFindExactMatch(unittest.TestCase):
|
||||
"""Test recherche exacte"""
|
||||
|
||||
def test_trouve_par_titre_exact(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("26.0.0 (Avenant)")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["number"], 42)
|
||||
|
||||
def test_ne_trouve_pas_si_titre_different(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("27.0.0")
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestFindByAlias(unittest.TestCase):
|
||||
"""Test recherche par alias"""
|
||||
|
||||
def test_query_trouve_via_alias(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 43, "title": "26.1.1 (Hotfix)", "aliases": ["26.1.1"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("26.1.1")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["title"], "26.1.1 (Hotfix)")
|
||||
|
||||
def test_query_trouve_milestone_sans_parentheses(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 44, "title": "26.1.0", "aliases": []}
|
||||
]
|
||||
}
|
||||
result = cache.find("26.1.0")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["number"], 44)
|
||||
|
||||
|
||||
class TestNoCollisionMatch(unittest.TestCase):
|
||||
"""Test pas de collision entre versions"""
|
||||
|
||||
def test_query_partielle_ne_trouve_pas_version_complete(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 43, "title": "26.1.1 (Hotfix)", "aliases": ["26.1.1"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("26.1")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_query_majeure_ne_trouve_pas_version_mineure(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("26")
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestFindWithNormalization(unittest.TestCase):
|
||||
"""Test recherche avec normalisation"""
|
||||
|
||||
def test_query_normalisee_trouve_milestone(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 44, "title": "26.1.0", "aliases": []}
|
||||
]
|
||||
}
|
||||
normalized = cache.normalize_semver("26.1")
|
||||
result = cache.find(normalized)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["title"], "26.1.0")
|
||||
|
||||
def test_query_majeure_normalisee_trouve_avenant(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {
|
||||
"milestones": [
|
||||
{"number": 42, "title": "26.0.0 (Avenant)", "aliases": ["26.0.0"]}
|
||||
]
|
||||
}
|
||||
normalized = cache.normalize_semver("26")
|
||||
result = cache.find(normalized)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["title"], "26.0.0 (Avenant)")
|
||||
|
||||
|
||||
class TestCachePersistence(unittest.TestCase):
|
||||
"""Test sauvegarde/chargement"""
|
||||
|
||||
def test_charge_cache_vide_si_fichier_inexistant(self):
|
||||
cache = MilestoneCache()
|
||||
cache.cache = {"milestones": []}
|
||||
self.assertEqual(cache.cache["milestones"], [])
|
||||
|
||||
|
||||
class TestCreateMilestone(unittest.TestCase):
|
||||
"""Test création milestone inexistant"""
|
||||
|
||||
@patch('milestone_cache.subprocess.run')
|
||||
def test_cree_milestone_via_api(self, mock_run):
|
||||
mock_run.side_effect = [
|
||||
MagicMock(stdout="git@github.com:owner/repo.git\n"),
|
||||
MagicMock(stdout='{"number": 50, "title": "99.0.0"}')
|
||||
]
|
||||
cache = MilestoneCache()
|
||||
result = cache.create("99.0.0")
|
||||
self.assertEqual(result["number"], 50)
|
||||
self.assertEqual(result["title"], "99.0.0")
|
||||
|
||||
|
||||
class TestCreateWithNormalization(unittest.TestCase):
|
||||
"""Test création avec normalisation semver"""
|
||||
|
||||
@patch('milestone_cache.subprocess.run')
|
||||
def test_cree_milestone_normalise_depuis_majeure(self, mock_run):
|
||||
mock_run.side_effect = [
|
||||
MagicMock(stdout="git@github.com:owner/repo.git\n"),
|
||||
MagicMock(stdout='{"number": 51, "title": "26.0.0"}')
|
||||
]
|
||||
cache = MilestoneCache()
|
||||
normalized = cache.normalize_semver("26")
|
||||
result = cache.create(normalized)
|
||||
self.assertEqual(result["title"], "26.0.0")
|
||||
|
||||
@patch('milestone_cache.subprocess.run')
|
||||
def test_cree_milestone_normalise_depuis_mineure(self, mock_run):
|
||||
mock_run.side_effect = [
|
||||
MagicMock(stdout="git@github.com:owner/repo.git\n"),
|
||||
MagicMock(stdout='{"number": 52, "title": "26.1.0"}')
|
||||
]
|
||||
cache = MilestoneCache()
|
||||
normalized = cache.normalize_semver("26.1")
|
||||
result = cache.create(normalized)
|
||||
self.assertEqual(result["title"], "26.1.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
153
skills/git-pr/tests/test_project_cache.py
Normal file
153
skills/git-pr/tests/test_project_cache.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests unitaires pour project_cache"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
from project_cache import ProjectCache, CACHE_DIR, CACHE_FILE
|
||||
|
||||
|
||||
class TestGenerateAliases(unittest.TestCase):
|
||||
"""Test génération aliases"""
|
||||
|
||||
def test_genere_alias_pour_titre_simple(self):
|
||||
cache = ProjectCache()
|
||||
aliases = cache.generate_aliases("Project Alpha")
|
||||
self.assertIn("project", aliases)
|
||||
self.assertIn("alpha", aliases)
|
||||
|
||||
def test_genere_alias_pour_titre_avec_tirets(self):
|
||||
cache = ProjectCache()
|
||||
aliases = cache.generate_aliases("Bug Tracking")
|
||||
self.assertIn("bug", aliases)
|
||||
self.assertIn("tracking", aliases)
|
||||
|
||||
def test_genere_alias_pour_titre_avec_chiffres(self):
|
||||
cache = ProjectCache()
|
||||
aliases = cache.generate_aliases("Sprint 2024-Q1")
|
||||
self.assertIn("sprint", aliases)
|
||||
self.assertIn("2024", aliases)
|
||||
self.assertIn("q1", aliases)
|
||||
|
||||
def test_genere_pas_alias_pour_mots_courts(self):
|
||||
cache = ProjectCache()
|
||||
aliases = cache.generate_aliases("The Big Project")
|
||||
self.assertNotIn("the", aliases)
|
||||
self.assertIn("big", aliases)
|
||||
self.assertIn("project", aliases)
|
||||
|
||||
|
||||
class TestFindExactMatch(unittest.TestCase):
|
||||
"""Test recherche exacte"""
|
||||
|
||||
def test_trouve_par_titre_exact(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {
|
||||
"projects": [
|
||||
{"id": "PVT_123", "title": "Project Alpha", "number": 1, "aliases": ["project", "alpha"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("Project Alpha")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["id"], "PVT_123")
|
||||
|
||||
def test_trouve_par_titre_exact_case_insensitive(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {
|
||||
"projects": [
|
||||
{"id": "PVT_123", "title": "Project Alpha", "number": 1, "aliases": ["project", "alpha"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("project alpha")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["id"], "PVT_123")
|
||||
|
||||
def test_ne_trouve_pas_si_titre_different(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {
|
||||
"projects": [
|
||||
{"id": "PVT_123", "title": "Project Alpha", "number": 1, "aliases": ["project", "alpha"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("Project Beta")
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestFindByAlias(unittest.TestCase):
|
||||
"""Test recherche par alias"""
|
||||
|
||||
def test_query_trouve_via_alias(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {
|
||||
"projects": [
|
||||
{"id": "PVT_456", "title": "Bug Tracking", "number": 2, "aliases": ["bug", "tracking"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("bug")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["title"], "Bug Tracking")
|
||||
|
||||
def test_query_trouve_via_alias_case_insensitive(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {
|
||||
"projects": [
|
||||
{"id": "PVT_456", "title": "Bug Tracking", "number": 2, "aliases": ["bug", "tracking"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("BUG")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["title"], "Bug Tracking")
|
||||
|
||||
def test_query_trouve_projet_sans_alias(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {
|
||||
"projects": [
|
||||
{"id": "PVT_789", "title": "Main", "number": 3, "aliases": ["main"]}
|
||||
]
|
||||
}
|
||||
result = cache.find("Main")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["number"], 3)
|
||||
|
||||
|
||||
class TestCachePersistence(unittest.TestCase):
|
||||
"""Test sauvegarde/chargement"""
|
||||
|
||||
def test_charge_cache_vide_si_fichier_inexistant(self):
|
||||
cache = ProjectCache()
|
||||
cache.cache = {"projects": []}
|
||||
self.assertEqual(cache.cache["projects"], [])
|
||||
|
||||
|
||||
class TestRefreshFromApi(unittest.TestCase):
|
||||
"""Test refresh depuis API"""
|
||||
|
||||
def test_refresh_remplace_cache_complet(self):
|
||||
cache = ProjectCache()
|
||||
projects = [
|
||||
{"id": "PVT_111", "title": "Project A", "number": 1},
|
||||
{"id": "PVT_222", "title": "Project B", "number": 2}
|
||||
]
|
||||
cache.refresh_from_api(projects)
|
||||
self.assertEqual(len(cache.cache["projects"]), 2)
|
||||
self.assertEqual(cache.cache["projects"][0]["id"], "PVT_111")
|
||||
self.assertEqual(cache.cache["projects"][1]["id"], "PVT_222")
|
||||
|
||||
def test_refresh_genere_aliases(self):
|
||||
cache = ProjectCache()
|
||||
projects = [
|
||||
{"id": "PVT_333", "title": "Sprint Planning", "number": 3}
|
||||
]
|
||||
cache.refresh_from_api(projects)
|
||||
self.assertIn("sprint", cache.cache["projects"][0]["aliases"])
|
||||
self.assertIn("planning", cache.cache["projects"][0]["aliases"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
302
skills/release-notes/SKILL.md
Normal file
302
skills/release-notes/SKILL.md
Normal file
@@ -0,0 +1,302 @@
|
||||
---
|
||||
name: release-notes
|
||||
description: >
|
||||
Génère des notes de release HTML orientées utilisateurs finaux.
|
||||
Transforme les commits techniques en descriptions accessibles sans jargon.
|
||||
allowed-tools: [Bash, Read, Write, Grep, Glob, AskUserQuestion]
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Release Notes Skill
|
||||
|
||||
Génère un document HTML orienté utilisateurs finaux, sans jargon technique.
|
||||
|
||||
## Variables
|
||||
|
||||
```bash
|
||||
REPORT_PATH=".claude/reports"
|
||||
TARGET="$ARGUMENTS" # Format: "branche-source branche-cible [nom-release]"
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Étape 0: Extraction et validation des arguments
|
||||
|
||||
```bash
|
||||
# Parser les arguments
|
||||
BRANCH_SOURCE=$(echo "$TARGET" | awk '{print $1}')
|
||||
BRANCH_TARGET=$(echo "$TARGET" | awk '{print $2}')
|
||||
RELEASE_NAME=$(echo "$TARGET" | awk '{print $3}')
|
||||
```
|
||||
|
||||
**AVANT TOUTE EXÉCUTION**, vérifier que les arguments obligatoires sont fournis :
|
||||
|
||||
1. Si `$BRANCH_SOURCE` est manquant ou vide :
|
||||
- Lister les branches disponibles : `git branch -r --list 'origin/release/*' --list 'origin/feature/*' | head -10`
|
||||
- Utiliser `AskUserQuestion` pour demander la branche source
|
||||
- Question : "Quelle est la branche source à analyser ?"
|
||||
- Proposer les branches récentes de type `release/*` ou `feature/*`
|
||||
|
||||
2. Si `$BRANCH_TARGET` est manquant ou vide :
|
||||
- Utiliser `AskUserQuestion` pour demander la branche cible
|
||||
- Question : "Quelle est la branche de référence ?"
|
||||
- Options suggérées : `main`, `develop`, `master`
|
||||
|
||||
**Ne pas continuer** tant que les deux arguments obligatoires ne sont pas fournis.
|
||||
|
||||
Si `$RELEASE_NAME` n'est pas fourni, utiliser le nom de `$BRANCH_SOURCE` en retirant le préfixe "release/" ou "feature/".
|
||||
|
||||
### Étape 1: Validation des branches
|
||||
|
||||
```bash
|
||||
# Vérifier que les branches existent
|
||||
git rev-parse --verify $BRANCH_SOURCE
|
||||
git rev-parse --verify $BRANCH_TARGET
|
||||
|
||||
# Vérifier qu'il y a des différences
|
||||
DIFF_COUNT=$(git rev-list --count $BRANCH_TARGET..$BRANCH_SOURCE)
|
||||
if [ $DIFF_COUNT -eq 0 ]; then
|
||||
echo "Aucune différence entre les branches"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Commits à analyser: $DIFF_COUNT"
|
||||
```
|
||||
|
||||
### Étape 2: Collecte des commits
|
||||
|
||||
```bash
|
||||
# Récupérer tous les commits avec leur message complet
|
||||
git log $BRANCH_TARGET..$BRANCH_SOURCE --oneline --no-merges
|
||||
|
||||
# Pour plus de contexte si nécessaire
|
||||
git log $BRANCH_TARGET..$BRANCH_SOURCE --pretty=format:"%h|%s|%b" --no-merges
|
||||
```
|
||||
|
||||
### Étape 3: Analyse et catégorisation
|
||||
|
||||
Analyser chaque commit et le catégoriser selon son **impact utilisateur** :
|
||||
|
||||
#### Catégories
|
||||
|
||||
1. **Nouveautés** (icône étoile)
|
||||
- Nouvelles fonctionnalités visibles par l'utilisateur
|
||||
- Nouveaux écrans, boutons, options
|
||||
- Mots-clés : feat, feature, ✨, 🚀, nouveau, ajout
|
||||
|
||||
2. **Améliorations** (icône flèche vers le haut)
|
||||
- Améliorations de fonctionnalités existantes
|
||||
- Meilleure ergonomie, rapidité
|
||||
- Mots-clés : improve, enhance, ♻️, ⚡, amélioration, optimisation, perf
|
||||
|
||||
3. **Corrections** (icône coche)
|
||||
- Bugs corrigés impactant l'utilisateur
|
||||
- Problèmes résolus
|
||||
- Mots-clés : fix, 🐛, correction, résolution, bug
|
||||
|
||||
4. **Sécurité** (icône bouclier) - si applicable
|
||||
- Améliorations de sécurité
|
||||
- Mots-clés : security, 🔒, sécurité
|
||||
|
||||
#### Commits à IGNORER (ne pas inclure dans les notes)
|
||||
|
||||
- `refactor:` - Refactorisation interne
|
||||
- `test:` / `✅` - Tests
|
||||
- `chore:` / `🔧` - Maintenance technique
|
||||
- `ci:` / `👷` - CI/CD
|
||||
- `docs:` / `📝` - Documentation technique (sauf si user-facing)
|
||||
- `style:` / `💄` - Formatage code
|
||||
- Commits de merge
|
||||
- Mises à jour de dépendances
|
||||
|
||||
### Étape 4: Rédaction des notes
|
||||
|
||||
Pour chaque commit retenu, rédiger une description **orientée utilisateur** :
|
||||
|
||||
**Règles de rédaction STRICTES :**
|
||||
|
||||
1. **ZÉRO jargon technique**
|
||||
- ❌ "Refactoring du composant UserController"
|
||||
- ✅ "Amélioration de la gestion de votre compte"
|
||||
- ❌ "Ajout d'un endpoint API REST"
|
||||
- ✅ "Nouvelle fonctionnalité disponible"
|
||||
|
||||
2. **Bénéfice utilisateur en premier**
|
||||
- ❌ "Ajout d'un cache sur les requêtes API"
|
||||
- ✅ "L'application est maintenant plus rapide"
|
||||
- ❌ "Optimisation des requêtes SQL"
|
||||
- ✅ "Les pages se chargent plus rapidement"
|
||||
|
||||
3. **Verbes d'action simples**
|
||||
- Vous pouvez maintenant...
|
||||
- Il est désormais possible de...
|
||||
- Nous avons corrigé...
|
||||
- Nous avons amélioré...
|
||||
|
||||
4. **Phrases courtes et claires**
|
||||
- Max 1-2 phrases par élément
|
||||
- Pas d'acronymes sans explication (pas de API, SQL, REST, etc.)
|
||||
|
||||
5. **Ton positif et professionnel**
|
||||
- Éviter les formulations négatives
|
||||
- Focus sur ce qui est possible/amélioré
|
||||
|
||||
### Étape 5: Génération du HTML
|
||||
|
||||
Utiliser le template suivant avec CSS inline :
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notes de version - [RELEASE_NAME]</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
color: white;
|
||||
}
|
||||
header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||
.version { font-size: 1.2rem; opacity: 0.9; }
|
||||
.date { font-size: 0.9rem; opacity: 0.7; }
|
||||
main { display: flex; flex-direction: column; gap: 2rem; }
|
||||
.category {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.category h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
.category.new h2 { border-color: #ffd700; }
|
||||
.category.improved h2 { border-color: #4caf50; }
|
||||
.category.fixed h2 { border-color: #2196f3; }
|
||||
.category.security h2 { border-color: #ff5722; }
|
||||
.category ul { list-style: none; }
|
||||
.category li {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.category li:last-child { border-bottom: none; }
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
padding: 1rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 1rem; }
|
||||
header h1 { font-size: 1.8rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Quoi de neuf ?</h1>
|
||||
<p class="version">Version [RELEASE_NAME]</p>
|
||||
<p class="date">[DATE du jour]</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Section Nouveautés (si non vide) -->
|
||||
<section class="category new">
|
||||
<h2>⭐ Nouveautés</h2>
|
||||
<ul>
|
||||
<li>Description orientée utilisateur...</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Section Améliorations (si non vide) -->
|
||||
<section class="category improved">
|
||||
<h2>📈 Améliorations</h2>
|
||||
<ul>
|
||||
<li>Description orientée utilisateur...</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Section Corrections (si non vide) -->
|
||||
<section class="category fixed">
|
||||
<h2>✅ Corrections</h2>
|
||||
<ul>
|
||||
<li>Description orientée utilisateur...</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Section Sécurité (si applicable et non vide) -->
|
||||
<section class="category security">
|
||||
<h2>🔒 Sécurité</h2>
|
||||
<ul>
|
||||
<li>Description orientée utilisateur...</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Merci d'utiliser notre application !</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Notes :**
|
||||
- Ne pas inclure les sections vides
|
||||
- Remplacer [RELEASE_NAME] par le nom de la release
|
||||
- Remplacer [DATE] par la date du jour au format "26 novembre 2025"
|
||||
|
||||
### Étape 6: Sauvegarde du fichier
|
||||
|
||||
```bash
|
||||
OUTPUT_FILE="$REPORT_PATH/release_notes_${RELEASE_NAME}.html"
|
||||
mkdir -p "$REPORT_PATH"
|
||||
```
|
||||
|
||||
Utiliser le tool `Write` pour écrire le contenu HTML dans `$OUTPUT_FILE`.
|
||||
|
||||
Afficher le résumé :
|
||||
```
|
||||
Notes de release générées : $OUTPUT_FILE
|
||||
|
||||
Résumé :
|
||||
- X nouveautés
|
||||
- Y améliorations
|
||||
- Z corrections
|
||||
```
|
||||
|
||||
## Exemples de transformation
|
||||
|
||||
| Commit technique | Note utilisateur |
|
||||
|------------------|------------------|
|
||||
| `✨ feat: implémenter cache Redis sur endpoint /api/users` | L'affichage de la liste des utilisateurs est maintenant plus rapide |
|
||||
| `🐛 fix: corriger validation email dans le formulaire d'inscription` | Nous avons corrigé un problème qui empêchait certaines adresses email d'être acceptées lors de l'inscription |
|
||||
| `⚡ perf: optimiser requêtes N+1 sur la page dashboard` | Le tableau de bord se charge maintenant plus rapidement |
|
||||
| `✨ feat: ajouter export CSV des factures` | Vous pouvez maintenant exporter vos factures au format Excel |
|
||||
| `🐛 fix: résoudre crash sur iOS 16 lors de l'upload` | Nous avons corrigé un problème qui pouvait faire fermer l'application lors de l'envoi de fichiers |
|
||||
|
||||
## Règles importantes
|
||||
|
||||
1. **ZÉRO jargon technique** - L'utilisateur final ne doit pas voir de termes comme "API", "refactoring", "backend", "cache", "endpoint", "requête"
|
||||
2. **Bénéfice utilisateur** - Chaque item doit répondre à "Qu'est-ce que ça change pour moi ?"
|
||||
3. **Ignorer l'invisible** - Ne pas mentionner les changements internes (tests, CI, deps, refactoring)
|
||||
4. **Ton positif** - Utiliser un ton accueillant et positif
|
||||
5. **Accessibilité** - HTML sémantique, contrastes suffisants, responsive
|
||||
6. **Concision** - Max 1-2 phrases par item, pas de détails superflus
|
||||
Reference in New Issue
Block a user