Initial commit
This commit is contained in:
15
.claude-plugin/plugin.json
Normal file
15
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "git",
|
||||||
|
"description": "Workflow Git complet : branches, commits, conflits, PR avec automation QA",
|
||||||
|
"version": "1.4.10",
|
||||||
|
"author": {
|
||||||
|
"name": "Aurélien Tournayre",
|
||||||
|
"email": "aurelien.tournayre@gmail.com"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# git
|
||||||
|
|
||||||
|
Workflow Git complet : branches, commits, conflits, PR avec automation QA
|
||||||
152
commands/branch.md
Normal file
152
commands/branch.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
model: claude-sonnet-4-5-20250929
|
||||||
|
allowed-tools: Bash
|
||||||
|
argument-hint: <source-branch> [issue-number-or-text]
|
||||||
|
description: Création de branche Git avec workflow structuré
|
||||||
|
---
|
||||||
|
|
||||||
|
# Création de branche Git
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Créer une nouvelle branche Git de manière structurée avec support des issues GitHub.
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
SOURCE_BRANCH: $1
|
||||||
|
ISSUE_OR_TEXT: $2
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
- Utilise les outils Bash pour les opérations Git
|
||||||
|
- Valide que la branche source existe
|
||||||
|
- Génère un nom de branche basé sur l'issue si fournie
|
||||||
|
- Applique les conventions de nommage du projet
|
||||||
|
|
||||||
|
## Relevant Files
|
||||||
|
- @.git/config
|
||||||
|
- @.gitignore
|
||||||
|
- @docs/README.md
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
**🚨 ÉTAPE CRITIQUE : CHECKOUT VERS SOURCE D'ABORD 🚨**
|
||||||
|
|
||||||
|
1. **Vérifier SOURCE_BRANCH obligatoire**
|
||||||
|
- Si `SOURCE_BRANCH` n'est pas fourni → ARRÊTER et demander à l'utilisateur
|
||||||
|
|
||||||
|
2. **Valider SOURCE_BRANCH existe localement**
|
||||||
|
- `git branch --list "$SOURCE_BRANCH"`
|
||||||
|
- Si n'existe pas → ARRÊTER avec erreur
|
||||||
|
|
||||||
|
3. **🔴 CHECKOUT VERS SOURCE_BRANCH AVANT TOUT 🔴**
|
||||||
|
- `git checkout $SOURCE_BRANCH`
|
||||||
|
- Vérifier qu'on est bien dessus : `git branch --show-current`
|
||||||
|
- **CRITIQUE** : Cette étape garantit qu'on crée depuis un point propre
|
||||||
|
|
||||||
|
4. **🔴 PULL POUR METTRE À JOUR SOURCE_BRANCH 🔴**
|
||||||
|
- `git pull origin $SOURCE_BRANCH`
|
||||||
|
- Garantit qu'on part du dernier commit de origin
|
||||||
|
- **CRITIQUE** : Évite de créer depuis un point obsolète
|
||||||
|
|
||||||
|
5. **Générer nom de la nouvelle branche**
|
||||||
|
- Si `ISSUE_OR_TEXT` est fourni :
|
||||||
|
- Détecte si c'est un numéro (entier) ou du texte
|
||||||
|
- Si c'est un numéro :
|
||||||
|
- Récupère les informations de l'issue via GitHub CLI (`gh issue view ${ISSUE_OR_TEXT}`)
|
||||||
|
- Génère un nom de branche : `issue/${ISSUE_OR_TEXT}-{titre-simplifie}`
|
||||||
|
- Le titre est nettoyé (espaces -> tirets, caractères spéciaux supprimés, minuscules)
|
||||||
|
- Si c'est du texte :
|
||||||
|
- Génère un nom de branche : `feature/${ISSUE_OR_TEXT-simplifie}`
|
||||||
|
- Le texte est nettoyé (espaces -> tirets, caractères spéciaux supprimés, minuscules)
|
||||||
|
- Si pas de `ISSUE_OR_TEXT`, demande le nom de branche à l'utilisateur
|
||||||
|
|
||||||
|
6. **Vérifier que la nouvelle branche n'existe pas déjà**
|
||||||
|
- `git branch --list "$NEW_BRANCH"`
|
||||||
|
- Si existe déjà → ARRÊTER avec erreur
|
||||||
|
|
||||||
|
7. **Créer et checkout la nouvelle branche**
|
||||||
|
- `git checkout -b $NEW_BRANCH`
|
||||||
|
- La branche est créée depuis SOURCE_BRANCH (car on est dessus)
|
||||||
|
|
||||||
|
8. **NE PAS configurer de tracking automatiquement**
|
||||||
|
- ❌ **INTERDIT** : `git branch --set-upstream-to=origin/$SOURCE_BRANCH $NEW_BRANCH`
|
||||||
|
- ✅ Le tracking sera configuré automatiquement lors du premier push avec `-u`
|
||||||
|
- ✅ Lors du push : `git push -u origin $NEW_BRANCH`
|
||||||
|
- **RAISON** : Configurer le tracking vers SOURCE_BRANCH pousse les commits sur la branche parente au lieu de créer une nouvelle branche distante
|
||||||
|
|
||||||
|
## Expertise
|
||||||
|
Conventions de nommage des branches :
|
||||||
|
- `feature/nom-descriptif` : Nouvelles fonctionnalités
|
||||||
|
- `fix/nom-bug` : Corrections de bugs
|
||||||
|
- `issue/123-nom-descriptif` : Basé sur une issue GitHub
|
||||||
|
- Utilise des tirets, pas d'espaces ni caractères spéciaux
|
||||||
|
|
||||||
|
## Template
|
||||||
|
```bash
|
||||||
|
# Exemple d'usage avec numéro d'issue :
|
||||||
|
/git:branch main 42
|
||||||
|
|
||||||
|
# Résultat attendu :
|
||||||
|
# - Récupère l'issue #42
|
||||||
|
# - Titre: "Add user authentication system"
|
||||||
|
# - Crée la branche: issue/42-add-user-authentication-system
|
||||||
|
# - Checkout vers cette branche
|
||||||
|
|
||||||
|
# Exemple d'usage avec texte :
|
||||||
|
/git:branch main "Add login form"
|
||||||
|
|
||||||
|
# Résultat attendu :
|
||||||
|
# - Crée la branche: feature/add-login-form
|
||||||
|
# - Checkout vers cette branche
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
```bash
|
||||||
|
# Créer une branche depuis main avec issue GitHub
|
||||||
|
/git:branch main 123
|
||||||
|
|
||||||
|
# Créer une branche depuis main avec texte descriptif
|
||||||
|
/git:branch main "user authentication"
|
||||||
|
|
||||||
|
# Créer une branche depuis develop sans argument supplémentaire
|
||||||
|
/git:branch develop
|
||||||
|
|
||||||
|
# Créer une branche depuis une branche existante avec issue
|
||||||
|
/git:branch feature/api-base 456
|
||||||
|
|
||||||
|
# Créer une branche fix depuis main avec texte
|
||||||
|
/git:branch main "fix login bug"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report
|
||||||
|
- Nom de la branche créée
|
||||||
|
- Branche source utilisée
|
||||||
|
- Issue associée (si applicable)
|
||||||
|
- Statut du checkout
|
||||||
|
- Note : Le tracking remote sera configuré lors du premier push avec `git push -u origin $NEW_BRANCH`
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- ✅ `SOURCE_BRANCH` doit exister localement
|
||||||
|
- ✅ `SOURCE_BRANCH` est obligatoire
|
||||||
|
- ✅ **CHECKOUT vers SOURCE_BRANCH AVANT création** (CRITIQUE)
|
||||||
|
- ✅ **PULL pour mettre à jour SOURCE_BRANCH** (CRITIQUE)
|
||||||
|
- ✅ La nouvelle branche ne doit pas déjà exister
|
||||||
|
- ✅ Si `ISSUE_OR_TEXT` est un numéro, l'issue doit exister sur GitHub
|
||||||
|
- ✅ Le nom généré respecte les conventions de nommage
|
||||||
|
- ✅ Détection automatique entre numéro d'issue et texte descriptif
|
||||||
|
|
||||||
|
## Pourquoi checkout + pull vers SOURCE_BRANCH d'abord ?
|
||||||
|
|
||||||
|
**Problème 1 évité** :
|
||||||
|
- Si on est sur `feature/A` et on crée `feature/B` depuis `main`
|
||||||
|
- Sans checkout vers `main` d'abord, la branche est créée depuis `feature/A`
|
||||||
|
- Les commits de `feature/A` se retrouvent sur `feature/B`
|
||||||
|
- Résultat : impossible de créer une PR propre
|
||||||
|
|
||||||
|
**Problème 2 évité** :
|
||||||
|
- Si `main` locale est en retard sur `origin/main`
|
||||||
|
- Sans pull, on crée depuis un point obsolète
|
||||||
|
- Résultat : commits manquants, conflits, PR avec historique incorrect
|
||||||
|
|
||||||
|
**Solution** :
|
||||||
|
1. TOUJOURS faire `git checkout $SOURCE_BRANCH`
|
||||||
|
2. TOUJOURS faire `git pull origin $SOURCE_BRANCH`
|
||||||
|
3. PUIS créer avec `git checkout -b $NEW_BRANCH`
|
||||||
161
commands/commit.md
Normal file
161
commands/commit.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
model: claude-sonnet-4-5-20250929
|
||||||
|
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*), Bash(git diff:*), Bash(git log:*), Bash(git push:*)
|
||||||
|
argument-hint: [message] | --no-verify | --push
|
||||||
|
description: Créer des commits bien formatés avec format conventional et emoji
|
||||||
|
---
|
||||||
|
|
||||||
|
# Commit Git Intelligent
|
||||||
|
|
||||||
|
Créer un commit bien formaté : $ARGUMENTS
|
||||||
|
|
||||||
|
## Ce Que Fait Cette Commande
|
||||||
|
|
||||||
|
1. Sauf si spécifié avec --no-verify, exécute automatiquement les vérifications pre-commit :
|
||||||
|
- make qa pour assurer la qualité du code
|
||||||
|
2. Vérifie quels fichiers sont stagés avec git status
|
||||||
|
3. Si 0 fichiers sont stagés, ajoute automatiquement tous les fichiers modifiés et nouveaux avec git add
|
||||||
|
4. Effectue un git diff pour comprendre les changements à commiter
|
||||||
|
5. Analyse le diff pour déterminer si plusieurs changements logiques distincts sont présents
|
||||||
|
6. Si plusieurs changements distincts sont détectés, suggère de diviser le commit en plusieurs commits plus petits
|
||||||
|
7. Pour chaque commit (ou le commit unique si pas de division), crée un message de commit utilisant le format conventional avec emoji
|
||||||
|
8. Si l'option --push est spécifiée, pousse automatiquement le(s) commit(s) vers le remote avec git push
|
||||||
|
|
||||||
|
## Bonnes Pratiques pour les Commits
|
||||||
|
|
||||||
|
- **Vérifier avant de commiter** : S'assurer que le code est linté, se build correctement, et que la documentation est à jour
|
||||||
|
- **Commits atomiques** : Chaque commit doit contenir des changements liés qui servent un seul objectif
|
||||||
|
- **Diviser les gros changements** : Si les changements touchent plusieurs préoccupations, les diviser en commits séparés
|
||||||
|
- **Format conventional commit** : Utiliser le format <type>: <description> où type est un de :
|
||||||
|
- feat: Une nouvelle fonctionnalité
|
||||||
|
- fix: Une correction de bug
|
||||||
|
- docs: Changements de documentation
|
||||||
|
- style: Changements de style de code (formatage, etc)
|
||||||
|
- refactor: Changements de code qui ne corrigent pas de bugs ni n'ajoutent de fonctionnalités
|
||||||
|
- perf: Améliorations de performance
|
||||||
|
- test: Ajout ou correction de tests
|
||||||
|
- chore: Changements du processus de build, outils, etc.
|
||||||
|
- **Présent, mode impératif** : Écrire les messages de commit comme des commandes (ex. "ajouter fonctionnalité" pas "ajouté fonctionnalité")
|
||||||
|
- **Première ligne concise** : Garder la première ligne sous 72 caractères
|
||||||
|
- **Emoji** : Chaque type de commit est associé à un emoji approprié :
|
||||||
|
- ✨ feat: Nouvelle fonctionnalité
|
||||||
|
- 🐛 fix: Correction de bug
|
||||||
|
- 📝 docs: Documentation
|
||||||
|
- 💄 style: Formatage/style
|
||||||
|
- ♻️ refactor: Refactorisation de code
|
||||||
|
- ⚡️ perf: Améliorations de performance
|
||||||
|
- ✅ test: Tests
|
||||||
|
- 🔧 chore: Outils, configuration
|
||||||
|
- 🚀 ci: Améliorations CI/CD
|
||||||
|
- 🗑️ revert: Annulation de changements
|
||||||
|
- 🧪 test: Ajouter un test qui échoue
|
||||||
|
- 🚨 fix: Corriger les warnings compilateur/linter
|
||||||
|
- 🔒️ fix: Corriger les problèmes de sécurité
|
||||||
|
- 👥 chore: Ajouter ou mettre à jour les contributeurs
|
||||||
|
- 🚚 refactor: Déplacer ou renommer des ressources
|
||||||
|
- 🏗️ refactor: Faire des changements architecturaux
|
||||||
|
- 🔀 chore: Fusionner des branches
|
||||||
|
- 📦️ chore: Ajouter ou mettre à jour les fichiers compilés ou packages
|
||||||
|
- ➕ chore: Ajouter une dépendance
|
||||||
|
- ➖ chore: Supprimer une dépendance
|
||||||
|
- 🌱 chore: Ajouter ou mettre à jour les fichiers de seed
|
||||||
|
- 🧑💻 chore: Améliorer l'expérience développeur
|
||||||
|
- 🧵 feat: Ajouter ou mettre à jour le code lié au multithreading ou à la concurrence
|
||||||
|
- 🔍️ feat: Améliorer le SEO
|
||||||
|
- 🏷️ feat: Ajouter ou mettre à jour les types
|
||||||
|
- 💬 feat: Ajouter ou mettre à jour le texte et les littéraux
|
||||||
|
- 🌐 feat: Internationalisation et localisation
|
||||||
|
- 👔 feat: Ajouter ou mettre à jour la logique métier
|
||||||
|
- 📱 feat: Travailler sur le design responsive
|
||||||
|
- 🚸 feat: Améliorer l'expérience utilisateur / utilisabilité
|
||||||
|
- 🩹 fix: Correction simple pour un problème non-critique
|
||||||
|
- 🥅 fix: Intercepter les erreurs
|
||||||
|
- 👽️ fix: Mettre à jour le code suite aux changements d'API externe
|
||||||
|
- 🔥 fix: Supprimer du code ou des fichiers
|
||||||
|
- 🎨 style: Améliorer la structure/format du code
|
||||||
|
- 🚑️ fix: Hotfix critique
|
||||||
|
- 🎉 chore: Commencer un projet
|
||||||
|
- 🔖 chore: Tags de release/version
|
||||||
|
- 🚧 wip: Travail en cours
|
||||||
|
- 💚 fix: Corriger le build CI
|
||||||
|
- 📌 chore: Épingler les dépendances à des versions spécifiques
|
||||||
|
- 👷 ci: Ajouter ou mettre à jour le système de build CI
|
||||||
|
- 📈 feat: Ajouter ou mettre à jour le code d'analytics ou de tracking
|
||||||
|
- ✏️ fix: Corriger les fautes de frappe
|
||||||
|
- ⏪️ revert: Annuler les changements
|
||||||
|
- 📄 chore: Ajouter ou mettre à jour la licence
|
||||||
|
- 💥 feat: Introduire des changements cassants
|
||||||
|
- 🍱 assets: Ajouter ou mettre à jour les assets
|
||||||
|
- ♿️ feat: Améliorer l'accessibilité
|
||||||
|
- 💡 docs: Ajouter ou mettre à jour les commentaires dans le code source
|
||||||
|
- 🗃️ db: Effectuer des changements liés à la base de données
|
||||||
|
- 🔊 feat: Ajouter ou mettre à jour les logs
|
||||||
|
- 🔇 fix: Supprimer les logs
|
||||||
|
- 🤡 test: Mocker des choses
|
||||||
|
- 🥚 feat: Ajouter ou mettre à jour un easter egg
|
||||||
|
- 🙈 chore: Ajouter ou mettre à jour le fichier .gitignore
|
||||||
|
- 📸 test: Ajouter ou mettre à jour les snapshots
|
||||||
|
- ⚗️ experiment: Effectuer des expériences
|
||||||
|
- 🚩 feat: Ajouter, mettre à jour, ou supprimer les feature flags
|
||||||
|
- 💫 ui: Ajouter ou mettre à jour les animations et transitions
|
||||||
|
- ⚰️ refactor: Supprimer le code mort
|
||||||
|
- 🦺 feat: Ajouter ou mettre à jour le code lié à la validation
|
||||||
|
- ✈️ feat: Améliorer le support hors ligne
|
||||||
|
|
||||||
|
## Directives pour Diviser les Commits
|
||||||
|
|
||||||
|
Lors de l'analyse du diff, considérer diviser les commits selon ces critères :
|
||||||
|
|
||||||
|
1. **Préoccupations différentes** : Changements dans des parties non-liées du codebase
|
||||||
|
2. **Types de changements différents** : Mélange de fonctionnalités, corrections, refactorisation, etc.
|
||||||
|
3. **Patterns de fichiers** : Changements dans différents types de fichiers (ex. code source vs documentation)
|
||||||
|
4. **Groupement logique** : Changements qui seraient plus faciles à comprendre ou réviser séparément
|
||||||
|
5. **Taille** : Changements très larges qui seraient plus clairs s'ils étaient décomposés
|
||||||
|
|
||||||
|
## Exemples
|
||||||
|
|
||||||
|
Bons messages de commit :
|
||||||
|
- ✨ feat: ajouter système d'authentification utilisateur
|
||||||
|
- 🐛 fix: résoudre fuite mémoire dans le processus de rendu
|
||||||
|
- 📝 docs: mettre à jour documentation API avec nouveaux endpoints
|
||||||
|
- ♻️ refactor: simplifier la logique de gestion d'erreurs dans le parser
|
||||||
|
- 🚨 fix: résoudre warnings linter dans les fichiers de composants
|
||||||
|
- 🧑💻 chore: améliorer processus de setup des outils développeur
|
||||||
|
- 👔 feat: implémenter logique métier pour validation de transaction
|
||||||
|
- 🩹 fix: corriger incohérence de style mineure dans le header
|
||||||
|
- 🚑️ fix: patcher vulnérabilité de sécurité critique dans le flux d'auth
|
||||||
|
- 🎨 style: réorganiser structure des composants pour meilleure lisibilité
|
||||||
|
- 🔥 fix: supprimer code legacy déprécié
|
||||||
|
- 🦺 feat: ajouter validation d'entrée pour formulaire d'inscription utilisateur
|
||||||
|
- 💚 fix: résoudre tests CI pipeline qui échouent
|
||||||
|
- 📈 feat: implémenter tracking analytics pour engagement utilisateur
|
||||||
|
- 🔒️ fix: renforcer exigences de mot de passe d'authentification
|
||||||
|
- ♿️ feat: améliorer accessibilité des formulaires pour lecteurs d'écran
|
||||||
|
|
||||||
|
Exemple de division de commits :
|
||||||
|
- Premier commit : ✨ feat: ajouter définitions de types pour nouvelle version solc
|
||||||
|
- Deuxième commit : 📝 docs: mettre à jour documentation pour nouvelles versions solc
|
||||||
|
- Troisième commit : 🔧 chore: mettre à jour dépendances package.json
|
||||||
|
- Quatrième commit : 🏷️ feat: ajouter définitions de types pour nouveaux endpoints API
|
||||||
|
- Cinquième commit : 🧵 feat: améliorer gestion de concurrence dans worker threads
|
||||||
|
- Sixième commit : 🚨 fix: résoudre problèmes de linting dans nouveau code
|
||||||
|
- Septième commit : ✅ test: ajouter tests unitaires pour fonctionnalités nouvelle version solc
|
||||||
|
- Huitième commit : 🔒️ fix: mettre à jour dépendances avec vulnérabilités de sécurité
|
||||||
|
|
||||||
|
## Options de Commande
|
||||||
|
|
||||||
|
- --no-verify: Ignorer l'exécution des vérifications pre-commit (qa)
|
||||||
|
- --push: Pousser automatiquement le(s) commit(s) vers le remote après création
|
||||||
|
|
||||||
|
## Notes Importantes
|
||||||
|
|
||||||
|
- Par défaut, les vérifications pre-commit (qa) s'exécuteront pour assurer la qualité du code
|
||||||
|
- Si ces vérifications échouent, il vous sera demandé si vous voulez procéder au commit quand même ou corriger les problèmes d'abord
|
||||||
|
- Si des fichiers spécifiques sont déjà stagés, la commande ne commitera que ces fichiers
|
||||||
|
- Si aucun fichier n'est stagé, elle stagera automatiquement tous les fichiers modifiés et nouveaux
|
||||||
|
- Le message de commit sera construit basé sur les changements détectés
|
||||||
|
- Avant de commiter, la commande révisera le diff pour identifier si plusieurs commits seraient plus appropriés
|
||||||
|
- Si elle suggère plusieurs commits, elle vous aidera à stager et commiter les changements séparément
|
||||||
|
- Révise toujours le diff du commit pour s'assurer que le message correspond aux changements
|
||||||
|
- Avec --push, le commit sera automatiquement poussé vers le remote après création
|
||||||
|
- Les options peuvent être combinées : /git:commit --no-verify --push
|
||||||
303
commands/conflit.md
Normal file
303
commands/conflit.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
---
|
||||||
|
model: claude-sonnet-4-5-20250929
|
||||||
|
allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git merge:*), Bash(git rebase:*), Bash(git checkout:*), Bash(git add:*), Read, Edit
|
||||||
|
argument-hint: <branche-destination>
|
||||||
|
description: Analyse les conflits git et propose à l'utilisateur une résolution pas à pas avec validation de chaque étape.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Résolution Interactive de Conflits Git
|
||||||
|
|
||||||
|
Résoudre les conflits git de manière interactive : $ARGUMENTS
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Analyser les conflits git et guider l'utilisateur dans une résolution pas à pas, fichier par fichier, avec validation à chaque étape.
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
- DESTINATION_BRANCH: $1 (branche de destination pour le merge/rebase)
|
||||||
|
- CURRENT_BRANCH: !`git branch --show-current`
|
||||||
|
- CONFLICTED_FILES: Liste des fichiers en conflit
|
||||||
|
- RESOLUTION_MODE: merge ou rebase (détecté automatiquement)
|
||||||
|
|
||||||
|
## État Actuel du Repository
|
||||||
|
|
||||||
|
- Branche actuelle : !`git branch --show-current`
|
||||||
|
- Status Git : !`git status --porcelain`
|
||||||
|
- Conflits détectés : !`git diff --name-only --diff-filter=U`
|
||||||
|
- Commits divergents : !`git log --oneline HEAD...$DESTINATION_BRANCH --max-count=5`
|
||||||
|
|
||||||
|
## Ce Que Fait Cette Commande
|
||||||
|
|
||||||
|
1. Détecte s'il y a un merge/rebase en cours ou si on doit l'initier
|
||||||
|
2. Identifie tous les fichiers en conflit
|
||||||
|
3. Pour chaque fichier en conflit :
|
||||||
|
- Affiche le contexte du conflit
|
||||||
|
- Montre les différences entre les versions
|
||||||
|
- Propose 3 stratégies de résolution
|
||||||
|
- Demande validation avant d'appliquer
|
||||||
|
4. Vérifie que tous les conflits sont résolus
|
||||||
|
5. Finalise le merge/rebase
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Validation initiale
|
||||||
|
|
||||||
|
**Vérifier DESTINATION_BRANCH obligatoire :**
|
||||||
|
- Si `DESTINATION_BRANCH` n'est pas fourni → ARRÊTER et demander à l'utilisateur
|
||||||
|
|
||||||
|
**Vérifier que DESTINATION_BRANCH existe :**
|
||||||
|
- `git branch --list "$DESTINATION_BRANCH"` (locale)
|
||||||
|
- `git branch -r --list "origin/$DESTINATION_BRANCH"` (remote)
|
||||||
|
- Si n'existe pas → ARRÊTER avec erreur
|
||||||
|
|
||||||
|
**Vérifier l'état du repository :**
|
||||||
|
- `git status` pour détecter :
|
||||||
|
- Merge en cours (fichiers "both modified")
|
||||||
|
- Rebase en cours (`.git/rebase-merge/` ou `.git/rebase-apply/`)
|
||||||
|
- Conflits existants
|
||||||
|
|
||||||
|
### 2. Initier l'opération si nécessaire
|
||||||
|
|
||||||
|
**Si aucun merge/rebase en cours :**
|
||||||
|
- Demander à l'utilisateur : "Voulez-vous merger ou rebaser $CURRENT_BRANCH sur $DESTINATION_BRANCH ?"
|
||||||
|
- Options :
|
||||||
|
1. Merge : `git merge $DESTINATION_BRANCH`
|
||||||
|
2. Rebase : `git rebase $DESTINATION_BRANCH`
|
||||||
|
3. Annuler
|
||||||
|
- Exécuter l'opération choisie
|
||||||
|
|
||||||
|
**Si l'opération échoue avec conflits :**
|
||||||
|
- Continuer avec l'analyse des conflits
|
||||||
|
|
||||||
|
### 3. Analyse des conflits
|
||||||
|
|
||||||
|
**Lister tous les fichiers en conflit :**
|
||||||
|
```bash
|
||||||
|
git diff --name-only --diff-filter=U
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pour chaque fichier, collecter :**
|
||||||
|
- Chemin complet du fichier
|
||||||
|
- Nombre de sections en conflit
|
||||||
|
- Lignes concernées
|
||||||
|
- Contexte (fonction/classe/module)
|
||||||
|
|
||||||
|
### 4. Résolution interactive par fichier
|
||||||
|
|
||||||
|
**Pour chaque fichier en conflit :**
|
||||||
|
|
||||||
|
**Étape A : Afficher le contexte**
|
||||||
|
- Nom du fichier et chemin
|
||||||
|
- Nombre de conflits dans ce fichier
|
||||||
|
- `git diff $FICHIER` pour voir les marqueurs de conflit
|
||||||
|
|
||||||
|
**Étape B : Analyser les versions**
|
||||||
|
- Lire le fichier avec Read pour voir les marqueurs :
|
||||||
|
- `<<<<<<< HEAD` : version actuelle
|
||||||
|
- `=======` : séparateur
|
||||||
|
- `>>>>>>> $DESTINATION_BRANCH` : version à merger
|
||||||
|
- Afficher les différences de manière claire
|
||||||
|
|
||||||
|
**Étape C : Proposer 3 stratégies**
|
||||||
|
|
||||||
|
1. **Garder la version actuelle (ours)**
|
||||||
|
- `git checkout --ours $FICHIER`
|
||||||
|
- Quand : notre version est correcte, l'autre est obsolète
|
||||||
|
|
||||||
|
2. **Garder la version entrante (theirs)**
|
||||||
|
- `git checkout --theirs $FICHIER`
|
||||||
|
- Quand : leur version est correcte, la nôtre est obsolète
|
||||||
|
|
||||||
|
3. **Résolution manuelle**
|
||||||
|
- Utiliser Edit pour fusionner manuellement
|
||||||
|
- Supprimer les marqueurs `<<<<<<<`, `=======`, `>>>>>>>`
|
||||||
|
- Combiner les changements pertinents des deux versions
|
||||||
|
|
||||||
|
**Étape D : Demander confirmation**
|
||||||
|
- Afficher un résumé de la stratégie choisie
|
||||||
|
- Demander : "Voulez-vous appliquer cette résolution ? (oui/non/voir le diff)"
|
||||||
|
- Si "voir le diff" : montrer le résultat avec `git diff --cached $FICHIER`
|
||||||
|
|
||||||
|
**Étape E : Appliquer la résolution**
|
||||||
|
- Exécuter la stratégie choisie
|
||||||
|
- Marquer le fichier comme résolu : `git add $FICHIER`
|
||||||
|
- Confirmer : "✅ Conflit résolu dans $FICHIER"
|
||||||
|
|
||||||
|
### 5. Vérification finale
|
||||||
|
|
||||||
|
**Après avoir résolu tous les fichiers :**
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
- Vérifier qu'il n'y a plus de fichiers "both modified"
|
||||||
|
- Vérifier que tous les fichiers conflictuels sont stagés
|
||||||
|
|
||||||
|
**Demander confirmation finale :**
|
||||||
|
- "Tous les conflits sont résolus. Voulez-vous finaliser ?"
|
||||||
|
- Options :
|
||||||
|
1. Oui, finaliser
|
||||||
|
2. Non, réviser les changements
|
||||||
|
3. Annuler tout (abort)
|
||||||
|
|
||||||
|
### 6. Finalisation
|
||||||
|
|
||||||
|
**Si merge :**
|
||||||
|
```bash
|
||||||
|
git commit --no-edit
|
||||||
|
# ou si l'utilisateur veut personnaliser :
|
||||||
|
git commit -m "Merge branch '$DESTINATION_BRANCH' into $CURRENT_BRANCH"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Si rebase :**
|
||||||
|
```bash
|
||||||
|
git rebase --continue
|
||||||
|
```
|
||||||
|
|
||||||
|
**Si annulation demandée :**
|
||||||
|
```bash
|
||||||
|
git merge --abort # ou
|
||||||
|
git rebase --abort
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stratégies de Résolution Détaillées
|
||||||
|
|
||||||
|
### Stratégie 1 : Garder la version actuelle (ours)
|
||||||
|
- Utilisation : Quand notre implémentation est plus récente/correcte
|
||||||
|
- Commande : `git checkout --ours $FICHIER && git add $FICHIER`
|
||||||
|
- Attention : Perte des changements de l'autre branche
|
||||||
|
|
||||||
|
### Stratégie 2 : Garder la version entrante (theirs)
|
||||||
|
- Utilisation : Quand la version à merger est plus récente/correcte
|
||||||
|
- Commande : `git checkout --theirs $FICHIER && git add $FICHIER`
|
||||||
|
- Attention : Perte de nos changements
|
||||||
|
|
||||||
|
### Stratégie 3 : Résolution manuelle intelligente
|
||||||
|
- Utilisation : Quand les deux versions contiennent des changements valides
|
||||||
|
- Processus :
|
||||||
|
1. Lire le fichier avec Read
|
||||||
|
2. Identifier les sections en conflit
|
||||||
|
3. Analyser la logique de chaque version
|
||||||
|
4. Utiliser Edit pour fusionner :
|
||||||
|
- Garder les imports/dépendances des deux côtés
|
||||||
|
- Fusionner la logique métier intelligemment
|
||||||
|
- Supprimer tous les marqueurs de conflit
|
||||||
|
5. Vérifier la syntaxe du résultat
|
||||||
|
6. `git add $FICHIER`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Exemple 1 : Merge avec conflits
|
||||||
|
```bash
|
||||||
|
# Situation : on est sur feature/auth, on veut merger main
|
||||||
|
/git:conflit main
|
||||||
|
|
||||||
|
# Claude détecte : pas de merge en cours
|
||||||
|
# Claude demande : "Voulez-vous merger main dans feature/auth ?"
|
||||||
|
# Utilisateur : "oui, merge"
|
||||||
|
# Claude exécute : git merge main
|
||||||
|
# Conflits détectés dans : src/auth.php, config/app.php
|
||||||
|
# Claude guide la résolution fichier par fichier
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple 2 : Rebase en cours avec conflits
|
||||||
|
```bash
|
||||||
|
# Situation : rebase en cours, 3 fichiers en conflit
|
||||||
|
/git:conflit develop
|
||||||
|
|
||||||
|
# Claude détecte : rebase en cours sur develop
|
||||||
|
# Claude liste : file1.php, file2.js, file3.md
|
||||||
|
# Claude résout interactivement chaque fichier
|
||||||
|
# Claude finalise : git rebase --continue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple 3 : Résolution manuelle complexe
|
||||||
|
```bash
|
||||||
|
/git:conflit main
|
||||||
|
|
||||||
|
# Conflit dans src/payment.php :
|
||||||
|
# HEAD : ajout méthode processRefund()
|
||||||
|
# main : ajout méthode processChargeback()
|
||||||
|
# Stratégie : Résolution manuelle
|
||||||
|
# Claude fusionne les deux méthodes
|
||||||
|
# Validation : utilisateur confirme
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Rapport de Résolution de Conflits
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- Branche actuelle : $CURRENT_BRANCH
|
||||||
|
- Branche destination : $DESTINATION_BRANCH
|
||||||
|
- Type d'opération : merge/rebase
|
||||||
|
|
||||||
|
## Conflits Détectés
|
||||||
|
- Nombre total de fichiers : X
|
||||||
|
- Fichiers résolus : Y
|
||||||
|
- Fichiers restants : Z
|
||||||
|
|
||||||
|
## Résolutions Appliquées
|
||||||
|
|
||||||
|
### Fichier : src/auth.php
|
||||||
|
- Stratégie : Résolution manuelle
|
||||||
|
- Raison : Fusion de deux implémentations valides
|
||||||
|
- Lignes modifiées : 42-58
|
||||||
|
|
||||||
|
### Fichier : config/app.php
|
||||||
|
- Stratégie : Garder version actuelle (ours)
|
||||||
|
- Raison : Configuration locale spécifique
|
||||||
|
|
||||||
|
## Statut Final
|
||||||
|
✅ Tous les conflits résolus
|
||||||
|
✅ Merge/rebase finalisé avec succès
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Avant de commencer
|
||||||
|
- ✅ S'assurer que le working directory est propre
|
||||||
|
- ✅ Avoir une sauvegarde (commit ou stash)
|
||||||
|
- ✅ Comprendre les changements des deux branches
|
||||||
|
|
||||||
|
### Pendant la résolution
|
||||||
|
- ✅ Résoudre un fichier à la fois
|
||||||
|
- ✅ Tester la syntaxe après chaque résolution manuelle
|
||||||
|
- ✅ Ne jamais garder les marqueurs de conflit (<<<<, ====, >>>>)
|
||||||
|
- ✅ Valider que la logique est cohérente
|
||||||
|
- ✅ En cas de doute, demander à l'utilisateur
|
||||||
|
|
||||||
|
### Après la résolution
|
||||||
|
- ✅ Vérifier que le code compile/s'exécute
|
||||||
|
- ✅ Lancer les tests si disponibles
|
||||||
|
- ✅ Réviser le diff final avant commit
|
||||||
|
|
||||||
|
## Messages d'Erreur et Solutions
|
||||||
|
|
||||||
|
### "error: you need to resolve your current index first"
|
||||||
|
- Cause : Conflits non résolus
|
||||||
|
- Solution : Continuer la résolution ou faire `git merge --abort`
|
||||||
|
|
||||||
|
### "no changes added to commit"
|
||||||
|
- Cause : Fichiers résolus mais non stagés
|
||||||
|
- Solution : `git add $FICHIER` après chaque résolution
|
||||||
|
|
||||||
|
### "conflict (content): Merge conflict in X"
|
||||||
|
- Cause : Changements incompatibles dans le même fichier
|
||||||
|
- Solution : Résoudre avec une des 3 stratégies
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- ✅ DESTINATION_BRANCH doit exister (locale ou remote)
|
||||||
|
- ✅ Tous les fichiers en conflit doivent être traités
|
||||||
|
- ✅ Aucun marqueur de conflit ne doit rester dans les fichiers
|
||||||
|
- ✅ Tous les fichiers résolus doivent être stagés
|
||||||
|
- ✅ L'utilisateur doit valider avant chaque résolution
|
||||||
|
- ✅ L'utilisateur doit confirmer avant la finalisation
|
||||||
|
|
||||||
|
## Notes Importantes
|
||||||
|
|
||||||
|
- La commande est 100% interactive : chaque action nécessite validation
|
||||||
|
- L'utilisateur garde le contrôle total du processus
|
||||||
|
- Possibilité d'annuler à tout moment avec merge/rebase --abort
|
||||||
|
- Les résolutions manuelles utilisent Edit pour garantir la qualité
|
||||||
|
- Un rapport détaillé est généré à la fin
|
||||||
6
commands/pr.md
Normal file
6
commands/pr.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
description: Crée une Pull Request optimisée avec workflow structuré
|
||||||
|
argument-hint: [branch-base, milestone, project, --delete, --no-review]
|
||||||
|
---
|
||||||
|
|
||||||
|
You must use the Skill tool to invoke the "git-pr" skill with the following arguments.
|
||||||
6
commands/release-notes.md
Normal file
6
commands/release-notes.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
description: Génère des notes de release HTML orientées utilisateurs finaux
|
||||||
|
argument-hint: "<branche-source> <branche-cible> [nom-release]"
|
||||||
|
---
|
||||||
|
|
||||||
|
You must use the Skill tool to invoke the "git:release-notes" skill.
|
||||||
270
commands/release-report.md
Normal file
270
commands/release-report.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
---
|
||||||
|
name: git:release-report
|
||||||
|
description: Génère un rapport HTML d'analyse d'impact entre deux branches
|
||||||
|
argument-hint: [branch-source, branche-cible, nom-release]
|
||||||
|
arguments:
|
||||||
|
- name: branche-source
|
||||||
|
description: Branche source à analyser (ex release/v27.0.0)
|
||||||
|
required: true
|
||||||
|
- name: branche-cible
|
||||||
|
description: Branche de référence (ex main ou develop)
|
||||||
|
required: true
|
||||||
|
- name: nom-release
|
||||||
|
description: Nom de la release pour le fichier (optionnel)
|
||||||
|
required: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Générer rapport d'analyse de release
|
||||||
|
|
||||||
|
Génère un rapport HTML détaillé comparant deux branches pour analyser l'impact d'une release.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/git:release-report <branche-source> <branche-cible> [nom-release]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exemples :**
|
||||||
|
- `/git:release-report release/v27.0.0 main`
|
||||||
|
- `/git:release-report release/v27.0.0 develop v27.0.0`
|
||||||
|
- `/git:release-report feature/new-module main "Module XYZ"`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Cette commande génère un rapport HTML orienté Product Owner qui analyse :
|
||||||
|
|
||||||
|
1. **Statistiques globales** : fichiers modifiés, lignes ajoutées/supprimées, commits
|
||||||
|
2. **Répartition par type de fichier** : PHP, Twig, JS, etc.
|
||||||
|
3. **Fonctionnalités principales** : extraction depuis les commits
|
||||||
|
4. **Impact métier** : par domaine fonctionnel
|
||||||
|
5. **Qualité & maintenabilité** : évolution du code
|
||||||
|
|
||||||
|
Le rapport est généré dans `REPORT_PATH/impact_<nom-release>.html`
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
REPORT_PATH: `.claude/reports`
|
||||||
|
|
||||||
|
Variables à extraire des arguments :
|
||||||
|
|
||||||
|
- `$BRANCH_SOURCE` : Branche source à analyser (ex: release/v27.0.0)
|
||||||
|
- `$BRANCH_TARGET` : Branche de référence (ex: main ou develop)
|
||||||
|
- `$RELEASE_NAME` : Nom de la release pour le fichier (ex: v27.0.0)
|
||||||
|
|
||||||
|
Si `$RELEASE_NAME` n'est pas fourni, utiliser le nom de `$BRANCH_SOURCE` en retirant le préfixe "release/"
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 0. Vérification des arguments obligatoires
|
||||||
|
|
||||||
|
**AVANT TOUTE EXÉCUTION**, vérifier que les arguments obligatoires sont fournis :
|
||||||
|
|
||||||
|
1. Si `$BRANCH_SOURCE` est manquant :
|
||||||
|
- 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 :
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### 1. Validation des paramètres
|
||||||
|
|
||||||
|
```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 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Collecte des statistiques git
|
||||||
|
|
||||||
|
Exécuter les commandes suivantes en parallèle :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Statistiques globales
|
||||||
|
git diff --stat $BRANCH_TARGET..$BRANCH_SOURCE | tail -1
|
||||||
|
|
||||||
|
# Nombre total de fichiers modifiés et détails lignes
|
||||||
|
git diff --numstat $BRANCH_TARGET..$BRANCH_SOURCE | \
|
||||||
|
awk '{files++; added+=$1; deleted+=$2} END {print files, added, deleted}'
|
||||||
|
|
||||||
|
# Répartition par type de fichier
|
||||||
|
git diff --numstat $BRANCH_TARGET..$BRANCH_SOURCE | python3 -c "
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
stats = defaultdict(lambda: {'count': 0, 'added': 0, 'deleted': 0})
|
||||||
|
|
||||||
|
for line in sys.stdin:
|
||||||
|
parts = line.strip().split('\t')
|
||||||
|
if len(parts) < 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
added = int(parts[0]) if parts[0] != '-' else 0
|
||||||
|
deleted = int(parts[1]) if parts[1] != '-' else 0
|
||||||
|
path = parts[2]
|
||||||
|
|
||||||
|
ext = 'autre'
|
||||||
|
if path.endswith('.php'):
|
||||||
|
ext = 'php'
|
||||||
|
elif path.endswith('.twig'):
|
||||||
|
ext = 'twig'
|
||||||
|
elif path.endswith(('.yml', '.yaml')):
|
||||||
|
ext = 'yaml'
|
||||||
|
elif path.endswith('.js'):
|
||||||
|
ext = 'js'
|
||||||
|
elif path.endswith('.scss'):
|
||||||
|
ext = 'scss'
|
||||||
|
elif path.endswith('.md'):
|
||||||
|
ext = 'md'
|
||||||
|
elif path.endswith('.json'):
|
||||||
|
ext = 'json'
|
||||||
|
elif path.endswith('.sh'):
|
||||||
|
ext = 'sh'
|
||||||
|
|
||||||
|
stats[ext]['count'] += 1
|
||||||
|
stats[ext]['added'] += added
|
||||||
|
stats[ext]['deleted'] += deleted
|
||||||
|
stats[ext]['total'] = stats[ext]['added'] + stats[ext]['deleted']
|
||||||
|
|
||||||
|
for ext in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
|
||||||
|
name = ext[0]
|
||||||
|
data = ext[1]
|
||||||
|
print(f'{name}|{data[\"count\"]}|{data[\"added\"]}|{data[\"deleted\"]}|{data[\"total\"]}')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Types de modifications (A/M/D/R)
|
||||||
|
git diff --name-status $BRANCH_TARGET..$BRANCH_SOURCE | cut -f1 | sort | uniq -c
|
||||||
|
|
||||||
|
# Nombre de commits
|
||||||
|
git rev-list --count $BRANCH_TARGET..$BRANCH_SOURCE
|
||||||
|
|
||||||
|
# Total fichiers dans la branche cible
|
||||||
|
git ls-tree -r $BRANCH_TARGET --name-only | wc -l
|
||||||
|
|
||||||
|
# Top commits avec features/fixes
|
||||||
|
git log $BRANCH_TARGET..$BRANCH_SOURCE --oneline --no-merges | \
|
||||||
|
grep -E "(feat|feature|✨|🚀|📝|🐛|fix|♻️|refactor)" | head -50
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Analyse des domaines fonctionnels
|
||||||
|
|
||||||
|
Analyser les commits pour identifier les domaines principaux impactés :
|
||||||
|
|
||||||
|
- Grouper par préfixe de commit (AVENANT, DOSSIER, APF, etc.)
|
||||||
|
- Identifier les patterns récurrents
|
||||||
|
- Extraire les fonctionnalités majeures avec leur impact
|
||||||
|
|
||||||
|
### 4. Génération du rapport HTML
|
||||||
|
|
||||||
|
Utiliser le template suivant et remplir avec les données collectées :
|
||||||
|
|
||||||
|
**Structure du rapport :**
|
||||||
|
|
||||||
|
1. **Header** avec executive summary
|
||||||
|
- Nom de la release
|
||||||
|
- Résumé en 2-3 phrases
|
||||||
|
- Chiffres clés (% code modifié, commits, variation nette)
|
||||||
|
|
||||||
|
2. **KPI Grid** (4 indicateurs visuels)
|
||||||
|
- % du code modifié
|
||||||
|
- Nombre de commits
|
||||||
|
- Variation nette (simplification/ajout)
|
||||||
|
- Domaine principal (ex: "70% focus AVENANTS")
|
||||||
|
|
||||||
|
3. **Fonctionnalités principales** (cards avec impact)
|
||||||
|
- Impact Très élevé (rouge)
|
||||||
|
- Impact Élevé (orange)
|
||||||
|
- Impact Moyen (jaune)
|
||||||
|
- Impact Faible (vert)
|
||||||
|
|
||||||
|
4. **Corrections majeures**
|
||||||
|
- Liste des bugs corrigés
|
||||||
|
|
||||||
|
5. **Qualité & Maintenabilité**
|
||||||
|
- Code simplifié
|
||||||
|
- Documentation
|
||||||
|
- Refactoring
|
||||||
|
- Interface
|
||||||
|
|
||||||
|
6. **Vue d'ensemble technique**
|
||||||
|
- Chart bars par type de fichier
|
||||||
|
- Métrique périmètre impacté
|
||||||
|
|
||||||
|
7. **Impact business** (4 axes)
|
||||||
|
- Gestion administrative
|
||||||
|
- Communication
|
||||||
|
- Performance
|
||||||
|
- Sécurité
|
||||||
|
|
||||||
|
**Style CSS :**
|
||||||
|
- Gradient violet (#667eea → #764ba2)
|
||||||
|
- Cards avec border-left coloré selon impact
|
||||||
|
- Progress bars animées
|
||||||
|
- KPI boxes avec fond dégradé
|
||||||
|
- Design responsive
|
||||||
|
|
||||||
|
### 5. Sauvegarde du fichier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OUTPUT_FILE="REPORT_PATH/impact_${RELEASE_NAME}.html"
|
||||||
|
mkdir -p REPORT_PATH
|
||||||
|
# Écrire le contenu HTML généré
|
||||||
|
echo "Rapport généré : $OUTPUT_FILE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format de sortie
|
||||||
|
|
||||||
|
Le rapport doit être :
|
||||||
|
|
||||||
|
- **Orienté Product Owner** : focus sur l'impact métier, pas les détails techniques
|
||||||
|
- **Visuel** : KPI, charts, couleurs par niveau d'impact
|
||||||
|
- **Actionnable** : lister les features, bugs corrigés, améliorations
|
||||||
|
- **Comparable** : même structure pour toutes les releases
|
||||||
|
|
||||||
|
## Règles importantes
|
||||||
|
|
||||||
|
1. **NE PAS** utiliser de termes techniques obscurs
|
||||||
|
2. **TOUJOURS** expliquer l'impact utilisateur
|
||||||
|
3. **GROUPER** les changements par domaine fonctionnel
|
||||||
|
4. **QUANTIFIER** l'impact (%, nombre, ratio)
|
||||||
|
5. **PRIORISER** par impact métier (Très élevé → Faible)
|
||||||
|
|
||||||
|
## Exemple d'exécution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ /git:release-report release/v27.0.0 main
|
||||||
|
|
||||||
|
Analyse de release/v27.0.0 vs main...
|
||||||
|
|
||||||
|
✓ Collecte statistiques git
|
||||||
|
✓ Analyse types de fichiers
|
||||||
|
✓ Extraction fonctionnalités
|
||||||
|
✓ Génération rapport HTML
|
||||||
|
|
||||||
|
Rapport généré : REPORT_PATH/impact_v27.0.0.html
|
||||||
|
|
||||||
|
Résumé :
|
||||||
|
- 1 250 fichiers modifiés (17.5% du codebase)
|
||||||
|
- 45 320 lignes ajoutées, 38 100 lignes supprimées
|
||||||
|
- +7 220 lignes nettes (+10.2%)
|
||||||
|
- 780 commits
|
||||||
|
- Focus principal : NOTIFICATIONS (65%)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Le rapport est auto-suffisant (HTML avec CSS inline)
|
||||||
|
- Compatible tous navigateurs modernes
|
||||||
|
- Peut être imprimé ou converti en PDF
|
||||||
|
- Les couleurs suivent le design system du projet
|
||||||
157
plugin.lock.json
Normal file
157
plugin.lock.json
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:atournayre/claude-marketplace:git",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "98e9eebc3d592a577e2edf3a276683691261d8bd",
|
||||||
|
"treeHash": "c89759d4291f7378025eeb4d894e6fd1ea49f0e580cba2ee04698b9cff3fad10",
|
||||||
|
"generatedAt": "2025-11-28T10:13:59.784186Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "git",
|
||||||
|
"description": "Workflow Git complet : branches, commits, conflits, PR avec automation QA",
|
||||||
|
"version": "1.4.10"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "ea1a63e12bf87382ed6840e7545257dcaec49109e919689dab31f59782928ba6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "c7d8a77a31f2709c837ecae7513f5e7a221b6ba637651e916daaa581d6364745"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/release-notes.md",
|
||||||
|
"sha256": "69f0d84ffcffdf029d7dc6361af07a36d73632b04110969a441ef70e09661f8b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/release-report.md",
|
||||||
|
"sha256": "ed9d4bbee99a67fb01f41d7678708673aaaabfc2e917238cf49b189777f87373"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/conflit.md",
|
||||||
|
"sha256": "23d90e64267685580e1b55eabf23744f3f56fcfc0520a16e652172b6adff9d72"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/pr.md",
|
||||||
|
"sha256": "aea873052c8d1d74bbc94f3d1e5901aea80a149e10ac8f3cac7d995ca8d14196"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/branch.md",
|
||||||
|
"sha256": "1831249b22338a0f1b1865026e2b1f67eada8f58c807e53fe55e7936cacb7253"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/commit.md",
|
||||||
|
"sha256": "377fa014bddc3c89b8398e58d71e452031a709f3b676786aad501978785366bc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/release-notes/SKILL.md",
|
||||||
|
"sha256": "f5ad25a5beae3378cc5ed2f134f65c15f768cae8ddbf5525279c018e87955156"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/README.md",
|
||||||
|
"sha256": "d29792c0d324d5a992b84e9670ae04a6f801e1f139c7027207fd85b38e447017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/SKILL.md",
|
||||||
|
"sha256": "a694f05ca0205d5313fdb011f30e3757c073d68b1595d3094398a07ad51bf248"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/.scope-consistency.md",
|
||||||
|
"sha256": "17403400ef95e0632f5d2e698afad4b4fad7107a6814d0c3f13f5c6d7b75d090"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/tests/test_project_cache.py",
|
||||||
|
"sha256": "9aef79bc287b2b0333845542f0e8726ab71127f73732579728b27452bc0ee20f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/tests/.gitignore",
|
||||||
|
"sha256": "8967b41828f2d5926dfb060c41059c2ddbb56714788b74c5950776787e56cfd2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/tests/run_tests.sh",
|
||||||
|
"sha256": "3a030d068fe8e12fb355eff97364398bd70b60c8e935e6ffd46268d89fd94d5a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/tests/test_milestone_cache.py",
|
||||||
|
"sha256": "b0f39908d51702290298a4f0fa106b7431cdcedec0767830e263ea45d9d1a51f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/milestone_cache.py",
|
||||||
|
"sha256": "f743ad0dc9243823f598dfc5d09de2dc75617191244dfee56ed73f31a5f31996"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/cleanup_branch.sh",
|
||||||
|
"sha256": "53b23ef46c9d6de8959a1d51100cf26644aea285001892a6e35e1b858aec1a51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/final_report.sh",
|
||||||
|
"sha256": "8af34c388538d7e3e80a965336c75943d4d6e1b8a804d98f56d16c487a177549"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/assign_project.py",
|
||||||
|
"sha256": "f6a6fd248f1babd5d4dc1c92420fbce0112e65b3b5d817052408a2d26fee3d89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/confirm_base_branch.py",
|
||||||
|
"sha256": "9bab042d1dd39f1ed560db87c3cca47533b8e9a1c4a4ba470a2019772a6cb2f7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/verify_pr_template.sh",
|
||||||
|
"sha256": "750092b980b4861d2ee2d167ae2f317679b884fb735bc28aa922e5e9dab0620c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/safe_push_pr.sh",
|
||||||
|
"sha256": "3375a7d44c9a197298db4549aaab78882a34dc700e7010398422060cecfc305f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/project_cache.py",
|
||||||
|
"sha256": "e61da785c9f834567047ae01792a6fc3219b5221a3705cd293b0562a243b8c0d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/analyze_changes.sh",
|
||||||
|
"sha256": "106648f6781b4f934fb24ba2cd5720fa28772f2191227b8e05bb9d673876fabf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/smart_qa.sh",
|
||||||
|
"sha256": "c6a813f05c4e484caef8533f12bd9043540ba60bbe2ea0ea11603cc8a89440aa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/assign_milestone.py",
|
||||||
|
"sha256": "d20cf3b2904fbc0f502cf490a7e913c616b40a233a6854e126c1851a4debea66"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/create_pr.sh",
|
||||||
|
"sha256": "0363bd2aec85de0492273d270537f0420033d9383980fe674f6a22deddf8778f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/check_scopes.sh",
|
||||||
|
"sha256": "57d14de8af8992985ca9460d6ccedd41d92fe910f5be037eb2c22d9a107ee015"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/auto_review.sh",
|
||||||
|
"sha256": "05dc55caf354d71acdd7693b86a585f77ca062e1ad1dae927e34967437d4b666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/git-pr/scripts/gh_auth_setup.sh",
|
||||||
|
"sha256": "74031c0569eee3f3431e47832f969c318d5c1400101faff5cc8711760b4b4115"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "c89759d4291f7378025eeb4d894e6fd1ea49f0e580cba2ee04698b9cff3fad10"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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