repowarden 4.7.0 → 4.7.2-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,29 +27,47 @@
27
27
 
28
28
  **Plus de 25 technologies** : Java (Maven, Gradle), JavaScript / TypeScript, Python, Go, Rust, PHP, Ruby, .NET, Dart / Flutter, Swift, Elixir, C / C++, Shell, Terraform, Packer, Ansible, Helm, Kubernetes, Docker, GitHub Actions — [détail](https://simdevwarden.github.io/repowarden/technologies/). Linux, macOS, Windows.
29
29
 
30
- ## Ce que tu fais à la main
31
-
32
- Le reste est automatique. Pas à pas et pièges : [où cliquer](https://simdevwarden.github.io/repowarden/validation/#ou-cliquer).
33
-
34
- | Geste | Où |
35
- |---|---|
36
- | Valider un ticket | étiquette `validé` (d'office si un mainteneur l'ouvre) |
37
- | Écrire la conception, avant le code | commentaire contenant « Conception retenue » sur le ticket |
38
- | Terminer une PR | **Ready for review** : elle se merge seule quand la CI est verte |
39
- | Livrer en production (flux `develop`) | *Actions → release → Run workflow* (ou la demande du vendredi), puis **Review deployments → Approve and deploy** sur le run |
40
- | Livrer un correctif urgent | étiquette `urgent` (à créer si absente) sur la PR `fix/…` |
41
- | Relire une PR, si exigé | *Files changed → Review changes → Approve* |
30
+ ## Le cycle d'une modification
31
+
32
+ De l'idée à la production, chaque changement suit le même chemin. Tu n'agis qu'aux étapes **en gras** ; le reste est automatique, et chaque étape est vérifiée par la CI.
33
+
34
+ ```mermaid
35
+ flowchart LR
36
+ A[Ticket] -->|validé| B[Conception]
37
+ B --> C[Branche du ticket]
38
+ C -->|commits + push| D[PR brouillon]
39
+ D -->|Ready for review| E{CI verte ?}
40
+ E -->|oui| F[develop]
41
+ E -->|non| C
42
+ F -->|vendredi, ou à la main| G[Demande de livraison]
43
+ G -->|Approve and deploy| H[main : release]
44
+ ```
42
45
 
43
- Une PR `develop` → `main` ouverte à la main ne reçoit jamais d'approbation : lancer le workflow `release`, qui ouvre la sienne.
46
+ | # | Étape | Qui | Ce qui se passe | Statut du ticket |
47
+ |---|---|---|---|---|
48
+ | 1 | Ouvrir un ticket (issue) | toi | rien ne se code sans ticket | à valider |
49
+ | 2 | **Valider le ticket** | toi | étiquette `validé` (posée d'office si un mainteneur ouvre le ticket) | backlog |
50
+ | 3 | **Écrire la conception** | toi | commentaire « Conception retenue » sur le ticket : ce qui change, fichiers touchés, hors périmètre. Sans elle, la vérification `ticket` de la PR échoue | backlog |
51
+ | 4 | **Prendre le ticket** | toi | `repowarden ticket 12` : assigné, branche `fix/12-sujet` créée depuis `develop` | en cours |
52
+ | 5 | Coder et committer | toi | `git cc` ; les hooks vérifient message, branche, secrets, formatage, puis les tests au push | en cours |
53
+ | 6 | Premier push | auto | PR **brouillon** vers `develop` ouverte, titre et description tirés du ticket | en cours |
54
+ | 7 | **Sortir du brouillon** | toi | bouton *Ready for review* (`gh pr ready`) | en relecture |
55
+ | 8 | Vérifier et intégrer | auto | CI complète ; verte → **merge automatique** (squash) dans `develop`. Rouge → corriger et pousser, retour en 5 | préprod |
56
+ | 9 | Préversion | auto | chaque soir si `develop` a changé : `vX.Y.Z-next.N` (paquet `repowarden@next`), pour tester avant la prod | préprod |
57
+ | 10 | Demande de livraison | auto | le vendredi matin, ou à la main (*Actions → release → Run workflow*, ou étiquette `urgent` sur une PR `fix/…`) : instantané `livraison/vX.Y.Z` et sa PR vers `main` | préprod |
58
+ | 11 | **Approuver la livraison** | toi | sur la page du run : **Review deployments → Approve and deploy** (pas « Approve » sur la PR, qui est une simple relecture) | préprod |
59
+ | 12 | Publier | auto | la GitHub App merge vers `main` : tag `vX.Y.Z`, release, paquet npm, documentation | done, ticket fermé |
60
+
61
+ Seul `develop` entre dans `main`, et jamais sans l'approbation de l'étape 11. Une PR `develop` → `main` ouverte à la main est fermée automatiquement, avec la marche à suivre. Pas à pas, commandes et pièges : [où cliquer](https://simdevwarden.github.io/repowarden/validation/#ou-cliquer).
44
62
 
45
63
  ## Démarrage
46
64
 
47
65
  ### 1. Sur ton poste, en une minute (tous tes dépôts)
48
66
 
49
67
  ```bash
50
- npm install -g repowarden # 1. le paquet (yarn global add, pnpm add -g : idem)
51
- repowarden install --global # 2. active les hooks : demande la langue, installe git cc
52
- repowarden # 3. vérifie : état du poste et étape suivante
68
+ npm install -g repowarden # 1. le paquet (yarn global add, pnpm add -g : idem)
69
+ repowarden install --global # 2. active les hooks : demande la langue, installe git cc
70
+ repowarden # 3. vérifie : état du poste et étape suivante
53
71
  ```
54
72
 
55
73
  L'étape 2 est indispensable : par sécurité, le paquet n'exécute rien à l'installation (aucun script `postinstall`, que npm 12 bloque d'ailleurs). Ensuite, dans **n'importe quel dépôt**, les hooks s'appliquent dès le prochain commit. Rien n'est ajouté aux projets ni à leur `package.json`.
@@ -59,7 +77,7 @@ cd mon-projet
59
77
  git switch -c feat/panier # nom de branche vérifié
60
78
  git add . && git cc # assistant : type, scope, description… puis commit
61
79
  git push -u origin feat/panier # tests des projets touchés avant l'envoi
62
- repowarden code-mort # nouveau code mort de ta branche
80
+ repowarden code-mort # nouveau code mort de ta branche
63
81
  ```
64
82
 
65
83
  | Action | Commande |
@@ -67,25 +85,17 @@ repowarden code-mort # nouveau code mort de ta branche
67
85
  | État du poste, étape suivante | `repowarden` (ou `repowarden statut`) |
68
86
  | Toutes les commandes | `repowarden --help` (`cc`, `code-mort`, `proteger`, `npm-publication`…) |
69
87
  | Mettre à jour | `npm update -g repowarden` (les hooks suivent) |
88
+ | Tester la prochaine version | `npm install -g repowarden@next` (retour : `repowarden@latest`) ; en CI : `uses: simdevWarden/repowarden@develop` |
70
89
  | Désinstaller | `repowarden uninstall --global`, **puis** `npm uninstall -g repowarden` (dans cet ordre : npm n'exécute rien à la désinstallation, les hooks resteraient branchés sur un dossier supprimé) |
71
90
 
72
91
  Sans Node : `git clone https://github.com/simdevWarden/repowarden.git ~/repowarden && ~/repowarden/install.sh --global` (mise à jour : `git -C ~/repowarden pull`).
73
92
 
74
- ### 4. GitHub App pour l'automatisation (merge auto, tickets, releases)
93
+ **Nouvelle machine** : ces trois commandes suffisent. Les règles du dépôt, l'environnement de livraison, la GitHub App, ses secrets et la publication npm vivent sur GitHub et npm : rien à reconfigurer. La clé privée de l'App ne doit jamais se trouver sur un poste.
75
94
 
76
- Pour que les workflows déclenchent les autres workflows (merge auto, tickets, releases), il faut une GitHub App au lieu du `GITHUB_TOKEN` :
77
-
78
- ```bash
79
- repowarden app init
80
- ```
81
-
82
- 1. Ouvre la page locale → clique "Create GitHub App" sur GitHub
83
- 2. Clique "Install" sur ton repo/org
84
- 3. Les secrets `REPOWARDEN_APP_CLIENT_ID` / `REPOWARDEN_APP_KEY` sont configurés automatiquement
85
-
86
- Sans cette étape, le `GITHUB_TOKEN` des Actions est utilisé : **ce qu'il fait ne déclenche pas les autres workflows** (merge auto, tickets, préversions).
87
-
88
- **Tester la prochaine version** (avant sa sortie en production) : `npm install -g repowarden@next`, ou `uses: simdevWarden/repowarden@develop` en CI. Retour à la version stable : `npm install -g repowarden@latest`. Les problèmes se signalent en [issue](https://github.com/simdevWarden/repowarden/issues).
95
+ > [!NOTE]
96
+ > **GitHub CLI (`gh`) : facultatif.**
97
+ > Sans `gh` : tous les hooks (commits, branches, secrets, formatage, tests), `git cc`, `repowarden code-mort`, `repowarden statut`, et la CI (GitHub ou GitLab), qui n'utilise jamais le `gh` du poste.
98
+ > Avec `gh` connecté (`gh auth login`), en plus : les tickets (`repowarden ticket`, `tickets`, branches de tes tickets créées en local) et la gestion du dépôt (`proteger`, `app init`, `npm-publication`, `pr-*`, `run-*`). Utile au mainteneur du dépôt GitHub ; chaque commande le signale s'il manque.
89
99
 
90
100
  ### 2. Dans un projet d'équipe : les mêmes règles pour tous
91
101
 
@@ -107,22 +117,44 @@ L'essentiel des deux fichiers :
107
117
  ```ini
108
118
  # .repowarden.conf : un poste en retard est prévenu, avec la commande de mise à jour
109
119
  [repowarden]
110
- version = 3
120
+ version = 4
111
121
  ```
112
122
 
113
- Puis rendre la CI obligatoire pour merger : `repowarden proteger` (protection de `main`, check `repowarden` exigé).
123
+ Puis rendre la CI obligatoire pour merger (`gh` connecté, admin du dépôt) :
124
+
125
+ ```bash
126
+ repowarden proteger --checks "repowarden,tests" # toutes les vérifications exigées, séparées par des virgules
127
+ repowarden proteger --checks "repowarden,tests" --verifier # plus tard : règles en ligne à jour ? (lecture seule)
128
+ ```
129
+
130
+ Toujours passer **toutes** les vérifications à `--checks` : sans lui, seule `repowarden` reste exigée. Avec un flux `develop` → `main`, `proteger` crée aussi l'étiquette `urgent` et exige l'approbation de chaque livraison : utiliser le [modèle de release du flux develop](https://simdevwarden.github.io/repowarden/releases/), sinon `main` attendrait un déploiement que rien ne crée (`proteger` le signale).
114
131
 
115
132
  Exemple complet : [repowarden-demo](https://github.com/simdevWarden/repowarden-demo) · Releases automatiques : [documentation](https://simdevwarden.github.io/repowarden/releases/).
116
133
 
117
- ### 3. Contribuer à repowarden
134
+ ### 3. GitHub App pour l'automatisation (merge auto, tickets, releases)
135
+
136
+ Pour que les workflows déclenchent les autres workflows (merge auto, tickets, releases), il faut une GitHub App au lieu du `GITHUB_TOKEN` :
137
+
138
+ ```bash
139
+ repowarden app init
140
+ ```
141
+
142
+ 1. Ouvre la page locale → clique « Create GitHub App » sur GitHub
143
+ 2. Clique « Install » sur ton dépôt ou ton organisation
144
+ 3. Les secrets `REPOWARDEN_APP_CLIENT_ID` / `REPOWARDEN_APP_KEY` sont configurés automatiquement
145
+
146
+ Sans cette étape, le `GITHUB_TOKEN` des Actions est utilisé : **ce qu'il fait ne déclenche pas les autres workflows** (merge auto, tickets, préversions).
147
+
148
+ ### 4. Contribuer à repowarden
118
149
 
119
150
  ```bash
120
151
  git clone https://github.com/simdevWarden/repowarden.git && cd repowarden
121
152
  ./install.sh # repowarden se vérifie lui-même (hooks de ce dépôt)
122
- bats test/ # tests (bats-core), aussi lancés au push
153
+ brew install shellcheck bats-core actionlint gitleaks # outils des vérifications (ou équivalent Linux)
154
+ bats test/ # tests, aussi lancés au push
123
155
  ```
124
156
 
125
- Les PR visent `develop` ; `main` ne reçoit que les livraisons, qui déclenchent les releases. Voir [CONTRIBUTING.md](CONTRIBUTING.md).
157
+ Les PR visent `develop` ; `main` ne reçoit que les livraisons, qui déclenchent les releases. Tout part d'un ticket : voir [AGENTS.md](AGENTS.md) et [CONTRIBUTING.md](CONTRIBUTING.md). Les problèmes se signalent en [issue](https://github.com/simdevWarden/repowarden/issues).
126
158
 
127
159
  **Dans VS Code** : l'extension [repowarden-vscode](https://github.com/simdevWarden/repowarden-vscode) guide la rédaction des commits, signale une branche mal nommée et des hooks inactifs. En attendant sa publication sur le Marketplace : `.vsix` dans ses [releases](https://github.com/simdevWarden/repowarden-vscode/releases), puis `code --install-extension repowarden-X.Y.Z.vsix`.
128
160
 
package/bin/commit CHANGED
@@ -6,7 +6,7 @@
6
6
  # sans cela, la ligne suivante serait lue
7
7
  # comme une nouvelle réponse)
8
8
  # git cc -m "description" [--type T] [--scope S] [--breaking NOTE]
9
- # [--body TEXTE] [--refs "Closes #12"]
9
+ # [--body TEXTE] [--refs "Ticket : #12"]
10
10
  # sans question (scripts, agents) : mêmes
11
11
  # contrôles, toute réponse refusée arrête
12
12
  # tout ; type et scope déduits de la
@@ -173,14 +173,19 @@ while :; do
173
173
  type="$answer"
174
174
  break
175
175
  fi
176
- t cc.type_invalid "$answer" "${#TYPES[@]}"
176
+ type_proche_r "$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')"
177
+ if [ -n "$REPLY" ]; then
178
+ t cc.type_invalid_suggest "$answer" "$REPLY" "${#TYPES[@]}"
179
+ else
180
+ t cc.type_invalid "$answer" "${#TYPES[@]}"
181
+ fi
177
182
  refuse "$REPLY"
178
183
  done
179
184
 
180
185
  # 2. Scope (optionnel)
181
186
  t cc.step2
182
187
  etape 2 "$REPLY"
183
- t cc.scope_hint
188
+ if [ -n "$default_scope" ]; then t cc.scope_hint_default "$default_scope"; else t cc.scope_hint; fi
184
189
  hint "$REPLY"
185
190
  while :; do
186
191
  if [ -n "$default_scope" ]; then
@@ -352,7 +357,8 @@ fi
352
357
  if [ -n "$ticket" ]; then
353
358
  t cc.refs_prompt_ticket "$ticket"
354
359
  else
355
- t cc.refs_prompt "$FORGE_REFS"
360
+ cfg_r integrationBranch ""
361
+ if [ -n "$REPLY" ]; then t cc.refs_prompt "Ticket : #12"; else t cc.refs_prompt "$FORGE_REFS"; fi
356
362
  fi
357
363
  ANSWER="$OPT_REFS"
358
364
  ask "$REPLY"
@@ -24,11 +24,13 @@ source "$ROOT/hooks/lib/common.sh"
24
24
  REPO=""
25
25
  WORKFLOW="release.yml"
26
26
  ENVIRONMENT=""
27
+ DRY_RUN=false
27
28
  while [ $# -gt 0 ]; do
28
29
  case "$1" in
29
30
  --repo) shift; REPO="${1:?--repo attend propriétaire/nom}" ;;
30
31
  --workflow) shift; WORKFLOW="${1:?--workflow attend un fichier}" ;;
31
32
  --environnement | --environment) shift; ENVIRONMENT="${1:?--environnement attend un nom}" ;;
33
+ --dry-run) DRY_RUN=true ;;
32
34
  -h | --help) sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
33
35
  *) err_t install.unknown_arg "$1"; exit 1 ;;
34
36
  esac
@@ -50,6 +52,67 @@ name="$(node -p 'require("./package.json").name')"
50
52
  # les doublons, y compris celui d'une tentative interrompue) ; tous révoqués à la fin
51
53
  TOKEN_PREFIX="repowarden-ci-${REPO##*/}"
52
54
 
55
+ # Vérifie si une relation de confiance correspond aux paramètres attendus.
56
+ # Affiche l'écart si --dry-run ou pour confirmation.
57
+ # Retourne : 0 = match, 1 = pas de relation, 2 = relation différente
58
+ verifier_trust() {
59
+ local json attendu_repo="$REPO" attendu_file="$WORKFLOW" attendu_env="$ENVIRONMENT"
60
+ json="$(npm trust list "$name" --json 2>/dev/null || echo '[]')"
61
+ if [ "$json" = "[]" ] || [ -z "$json" ]; then
62
+ return 1
63
+ fi
64
+ local match=0 id repo file env allow_publish
65
+ while IFS=$'\t' read -r id repo file env allow_publish; do
66
+ [ -n "$id" ] || continue
67
+ if [ "$repo" = "$attendu_repo" ] && [ "$file" = "$attendu_file" ] && [ "$env" = "$attendu_env" ] && [ "$allow_publish" = "true" ]; then
68
+ match=1
69
+ break
70
+ fi
71
+ done < <(node -e "
72
+ const data = $json;
73
+ for (const t of data) {
74
+ const id = t.id || t.key;
75
+ const repo = t.repository || t.repo || '';
76
+ const file = t.file || t.workflow || '';
77
+ const env = t.environment || t.env || '';
78
+ const allowPublish = t.allowPublish === true || t['allow-publish'] === true;
79
+ console.log(id + '\t' + repo + '\t' + file + '\t' + env + '\t' + allowPublish);
80
+ }
81
+ ")
82
+ if [ "$match" = 1 ]; then
83
+ return 0
84
+ fi
85
+ return 2
86
+ }
87
+
88
+ # Affiche les détails d'une relation de confiance pour diagnostic
89
+ afficher_trust() {
90
+ local json
91
+ json="$(npm trust list "$name" --json 2>/dev/null || echo '[]')"
92
+ if [ "$json" = "[]" ] || [ -z "$json" ]; then
93
+ echo " (aucune relation de confiance)"
94
+ return
95
+ fi
96
+ node -e "
97
+ const data = $json;
98
+ for (const t of data) {
99
+ const id = t.id || t.key;
100
+ const repo = t.repository || t.repo || '(inconnu)';
101
+ const file = t.file || t.workflow || '(inconnu)';
102
+ const env = t.environment || t.env || '(aucun)';
103
+ const allowPublish = t.allowPublish === true || t['allow-publish'] === true;
104
+ const allowStagePublish = t.allowStagePublish === true || t['allow-stage-publish'] === true;
105
+ console.log(' ID: ' + id);
106
+ console.log(' Dépôt: ' + repo);
107
+ console.log(' Workflow: ' + file);
108
+ console.log(' Environnement: ' + env);
109
+ console.log(' npm publish: ' + (allowPublish ? 'oui' : 'non'));
110
+ console.log(' npm stage publish: ' + (allowStagePublish ? 'oui' : 'non'));
111
+ console.log('');
112
+ }
113
+ "
114
+ }
115
+
53
116
  brand "npm · $name · $REPO"
54
117
 
55
118
  # 1. Connexion npm : entre toi et npm (navigateur ou mot de passe)
@@ -127,16 +190,82 @@ fi
127
190
 
128
191
  # 3. Publication de confiance, puis plus aucun jeton
129
192
  step_t npm.step_trust
130
- if grep -q '"id"' <<<"$(npm trust list "$name" --json 2>/dev/null || true)"; then
193
+ verifier_trust
194
+ cas=$?
195
+ if [ "$cas" = 0 ]; then
131
196
  ok_t npm.trust_exists
197
+ elif [ "$cas" = 1 ]; then
198
+ # Aucune relation : créer
199
+ if [ "$DRY_RUN" = true ]; then
200
+ info_t npm.trust_would_create "$REPO" "$WORKFLOW" "${ENVIRONMENT:-(aucun)}"
201
+ else
202
+ npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
203
+ ok_t npm.trust_set "$REPO" "$WORKFLOW"
204
+ fi
132
205
  else
133
- npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
134
- ok_t npm.trust_set "$REPO" "$WORKFLOW"
206
+ # Relation différente : afficher l'écart, demander confirmation, corriger sans perte
207
+ attention_t npm.trust_mismatch
208
+ afficher_trust
209
+ echo " Attendu :"
210
+ echo " Dépôt: $REPO"
211
+ echo " Workflow: $WORKFLOW"
212
+ echo " Environnement: ${ENVIRONMENT:-(aucun)}"
213
+ echo " npm publish: oui"
214
+ echo ""
215
+ if [ "$DRY_RUN" = true ]; then
216
+ info_t npm.trust_would_fix
217
+ else
218
+ # Confirmation avant changement
219
+ if [ -t 0 ]; then
220
+ t npm.trust_confirm
221
+ read -r -p " $REPLY" rep
222
+ echo
223
+ [[ "$rep" =~ ^[OoYy]$ ]] || { err_t npm.trust_cancelled; exit 1; }
224
+ fi
225
+ # Créer la nouvelle relation AVANT de révoquer l'ancienne (pas de perte)
226
+ npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
227
+ ok_t npm.trust_set "$REPO" "$WORKFLOW"
228
+ # Révoquer l'ancienne relation
229
+ json="$(npm trust list "$name" --json 2>/dev/null || echo '[]')"
230
+ old_id="$(node -e "
231
+ const data = $json;
232
+ for (const t of data) {
233
+ const id = t.id || t.key;
234
+ const repo = t.repository || t.repo || '';
235
+ const file = t.file || t.workflow || '';
236
+ const env = t.environment || t.env || '';
237
+ const allowPublish = t.allowPublish === true || t['allow-publish'] === true;
238
+ if (repo !== '$REPO' || file !== '$WORKFLOW' || env !== '$ENVIRONMENT' || allowPublish !== true) {
239
+ console.log(id);
240
+ }
241
+ }
242
+ ")"
243
+ for id in $old_id; do
244
+ [ -n "$id" ] && npm trust revoke "$name" --id "$id" --yes && ok_t npm.trust_revoked "$id"
245
+ done
246
+ fi
135
247
  fi
136
248
  gh variable set REPOWARDEN_NPM --repo "$REPO" --body true
137
249
  if grep -q '^NPM_TOKEN' <<<"$(gh secret list --repo "$REPO")"; then
138
- gh secret delete NPM_TOKEN --repo "$REPO"
139
- ok_t npm.token_removed
250
+ if [ "$cas" = 0 ] || [ "$cas" = 2 ]; then
251
+ # Relation de confiance OK (ou corrigée) : signaler le token et proposer suppression
252
+ attention_t npm.token_present
253
+ if [ "$DRY_RUN" = true ]; then
254
+ info_t npm.token_would_remove
255
+ elif [ -t 0 ]; then
256
+ t npm.token_remove_confirm
257
+ read -r -p " $REPLY" rep
258
+ echo
259
+ [[ "$rep" =~ ^[OoYy]$ ]] && gh secret delete NPM_TOKEN --repo "$REPO" && ok_t npm.token_removed
260
+ else
261
+ gh secret delete NPM_TOKEN --repo "$REPO"
262
+ ok_t npm.token_removed
263
+ fi
264
+ else
265
+ # Pas de relation de confiance : supprimer silencieusement (comportement existant)
266
+ gh secret delete NPM_TOKEN --repo "$REPO"
267
+ ok_t npm.token_removed
268
+ fi
140
269
  fi
141
270
  # Jetons temporaires révoqués sur npm (retrouvés par leur préfixe). npm peut
142
271
  # demander un code 2FA : la commande garde le terminal (questions visibles)
package/bin/proteger CHANGED
@@ -308,6 +308,60 @@ sig_attendue() {
308
308
  "$(sed -n 's/.*"require_last_push_approval": \([a-z]*\).*/\1/p' <<<"$r")" \
309
309
  "$(sed -n 's/.*"require_code_owner_review": \([a-z]*\).*/\1/p' <<<"$r")" "$deps" "$checks"
310
310
  }
311
+ # Anciens rulesets : rulesets de branche du dépôt, sous un autre nom que ceux
312
+ # de repowarden, qui exigent encore la vérification « repogarde » (retirée en
313
+ # v4) ; posés à la main ou par une ancienne version. Laissés à côté, ils
314
+ # bloqueraient les branches pour toujours. Une ligne par ruleset dans ANCIENS :
315
+ # id, nom, branches couvertes (~DEFAULT_BRANCH résolu : la branche par défaut
316
+ # change ensuite dans le flux develop), séparés par des tabulations.
317
+ # shellcheck disable=SC2016 # expressions jq (gh --jq), pas du shell
318
+ anciens_rulesets() {
319
+ local ids id ligne defaut=""
320
+ ANCIENS=""
321
+ ids="$(gh api "repos/$REPO/rulesets" --jq '.[] | select(.target == "branch"
322
+ and (.source_type // "Repository") == "Repository"
323
+ and (.name | test("^(repowarden|repogarde)( \\(.*\\))?$") | not)) | .id')" || return 0
324
+ for id in $ids; do
325
+ ligne="$(gh api "repos/$REPO/rulesets/$id" --jq 'select([.rules[]? | select(.type == "required_status_checks")
326
+ | .parameters.required_status_checks[].context] | index("repogarde"))
327
+ | "\(.id)\t\(.name)\t\(.conditions.ref_name.include | join(" "))"')" || continue
328
+ [ -n "$ligne" ] || continue
329
+ if [[ "$ligne" == *"~DEFAULT_BRANCH"* ]]; then
330
+ [ -n "$defaut" ] || defaut="$(gh api "repos/$REPO" --jq .default_branch)"
331
+ ligne="${ligne//\~DEFAULT_BRANCH/refs/heads/$defaut}"
332
+ fi
333
+ ANCIENS="$ANCIENS$ligne"$'\n'
334
+ done
335
+ }
336
+
337
+ # Ancien ruleset (ligne de ANCIENS) couvrant une des branches du ruleset $2 (JSON)
338
+ couvre() {
339
+ local motifs m b
340
+ read -ra motifs <<<"$(cut -f3 <<<"$1")"
341
+ for m in ${motifs[@]+"${motifs[@]}"}; do
342
+ [ "$m" != "~ALL" ] || return 0
343
+ for b in $(grep -o '"refs/heads/[^"]*"' <<<"$2" | tr -d '"'); do
344
+ # shellcheck disable=SC2053 # motif de GitHub (refs/heads/release/*)
345
+ [[ "$b" != $m ]] || return 0
346
+ done
347
+ done
348
+ return 1
349
+ }
350
+
351
+ # Réponse d'erreur de GitHub (JSON) → « message : erreurs », lisible
352
+ erreur_github() {
353
+ local message="" erreurs="" re_message='"message": *"([^"]*)"' re_erreurs='"errors": *\[([^]]*)\]'
354
+ [[ ! "$1" =~ $re_message ]] || message="${BASH_REMATCH[1]}"
355
+ [[ ! "$1" =~ $re_erreurs ]] || erreurs="${BASH_REMATCH[1]//\"/}"
356
+ if [ -n "$message" ]; then
357
+ REPLY="$message${erreurs:+ : $erreurs}"
358
+ else
359
+ REPLY="$(tr '\n' ' ' <<<"$1")"
360
+ fi
361
+ }
362
+
363
+ anciens_rulesets
364
+
311
365
  # shellcheck disable=SC2016 # expression jq (gh --jq), pas du shell
312
366
  SIG_EN_LIGNE='(.rules | map(select(.type == "pull_request"))[0].parameters) as $p
313
367
  | "approbations=\($p.required_approving_review_count) dernier_push=\($p.require_last_push_approval) codeowners=\($p.require_code_owner_review) deploiement=\([.rules[] | select(.type == "required_deployments") | .parameters.required_deployment_environments[]] | join(",")) verifications=\([.rules[] | select(.type == "required_status_checks") | .parameters.required_status_checks[].context] | sort | join(","))"'
@@ -335,6 +389,11 @@ if [ -n "$VERIFY" ]; then
335
389
  ecarts=$((ecarts + 1))
336
390
  fi
337
391
  done
392
+ while IFS=$'\t' read -r id name _; do
393
+ [ -n "$id" ] || continue
394
+ err_t ci.proteger.verify_legacy "$name" "$id"
395
+ ecarts=$((ecarts + 1))
396
+ done <<<"$ANCIENS"
338
397
  [ "$ecarts" = 0 ] || { err_t ci.proteger.verify_fix; exit 1; }
339
398
  ok_t ci.proteger.verify_ok
340
399
  exit 0
@@ -361,6 +420,7 @@ gh api -X PATCH "repos/$REPO" --silent "${settings[@]}"
361
420
  gh api -X PUT "repos/$REPO/actions/permissions/workflow" --silent \
362
421
  -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true
363
422
 
423
+ echecs=0
364
424
  for ruleset in "${rulesets[@]}"; do
365
425
  name="$(sed -n 's/^ "name": "\(.*\)",$/\1/p' <<<"$ruleset")"
366
426
  # Ruleset posé sous l'ancien nom (repogarde, v3) : repris et renommé, sinon
@@ -368,14 +428,45 @@ for ruleset in "${rulesets[@]}"; do
368
428
  old="${name/repowarden/repogarde}"
369
429
  id="$(gh api "repos/$REPO/rulesets" \
370
430
  --jq "([.[] | select(.name == \"$name\")] + [.[] | select(.name == \"$old\")]) | .[0].id // empty")"
431
+ # Sinon, un ancien ruleset sous un autre nom couvrant ses branches : repris
432
+ # (mis à jour en place, jamais un instant sans protection)
433
+ repris=""
434
+ if [ -z "$id" ] && grep -q '"target": "branch"' <<<"$ruleset"; then
435
+ while IFS= read -r ligne; do
436
+ { [ -n "$ligne" ] && couvre "$ligne" "$ruleset"; } || continue
437
+ id="${ligne%%$'\t'*}"
438
+ repris="$(cut -f2 <<<"$ligne")"
439
+ ANCIENS="$(grep -v "^$id"$'\t' <<<"$ANCIENS" || true)"
440
+ break
441
+ done <<<"$ANCIENS"
442
+ fi
371
443
  if [ -n "$id" ]; then
372
- gh api -X PUT "repos/$REPO/rulesets/$id" --input - <<<"$ruleset" >/dev/null
373
- ok_t ci.proteger.10 "$name"
444
+ methode=PUT chemin="repos/$REPO/rulesets/$id"
445
+ else
446
+ methode=POST chemin="repos/$REPO/rulesets"
447
+ fi
448
+ # Réponse gardée : en cas de refus, le détail de GitHub est affiché
449
+ if reponse="$(gh api -X "$methode" "$chemin" --input - <<<"$ruleset" 2>&1)"; then
450
+ if [ -n "$repris" ]; then
451
+ ok_t ci.proteger.legacy_taken "$name" "$repris"
452
+ elif [ -n "$id" ]; then
453
+ ok_t ci.proteger.10 "$name"
454
+ else
455
+ ok_t ci.proteger.11 "$name"
456
+ fi
374
457
  else
375
- gh api -X POST "repos/$REPO/rulesets" --input - <<<"$ruleset" >/dev/null
376
- ok_t ci.proteger.11 "$name"
458
+ erreur_github "$reponse"
459
+ err_t ci.proteger.api_error "$name" "$REPLY"
460
+ echecs=$((echecs + 1))
377
461
  fi
378
462
  done
463
+ # Anciens rulesets non repris (plusieurs sur les mêmes branches) : jamais
464
+ # supprimés ici, la marche à suivre est donnée
465
+ while IFS=$'\t' read -r id name _; do
466
+ [ -n "$id" ] || continue
467
+ err_t ci.proteger.legacy_left "$name" "$id" "$REPO" "$id"
468
+ echecs=$((echecs + 1))
469
+ done <<<"$ANCIENS"
379
470
  if [ -n "$env_json" ]; then
380
471
  gh api -X PUT "repos/$REPO/environments/$ENVIRONMENT" --input - --silent <<<"$env_json"
381
472
  policies="$(gh api "repos/$REPO/environments/$ENVIRONMENT/deployment-branch-policies" \
@@ -389,4 +480,5 @@ if [ -n "$env_json" ]; then
389
480
  fi
390
481
  [ -z "$urgent" ] || gh label create urgent -R "$REPO" --color B60205 \
391
482
  --description "Livraison immédiate après merge" >/dev/null
483
+ [ "$echecs" = 0 ] || { err_t ci.proteger.incomplete; exit 1; }
392
484
  ok_t ci.proteger.13
package/hooks/commit-msg CHANGED
@@ -39,6 +39,30 @@ if [[ "$SUBJECT" =~ ^(Merge|Revert|fixup!|squash!) ]]; then
39
39
  exit 0
40
40
  fi
41
41
 
42
+ # En-tête tenté mais refusé : type inconnu (ou en majuscules), description vide
43
+ if ! [[ "$SUBJECT" =~ $CC_PATTERN ]] &&
44
+ [[ "$SUBJECT" =~ ^([A-Za-z]+)(\([^\)]*\))?(!?):[[:space:]]*(.*)$ ]]; then
45
+ typ="${BASH_REMATCH[1]}" portee="${BASH_REMATCH[2]}" bang="${BASH_REMATCH[3]}" desc="${BASH_REMATCH[4]}"
46
+ typ_min="$(printf '%s' "$typ" | tr '[:upper:]' '[:lower:]')"
47
+ propose="$typ_min"
48
+ [[ "$typ_min" =~ ^($CC_TYPES)$ ]] || { type_proche_r "$typ_min"; propose="$REPLY"; }
49
+ if [ -z "$desc" ]; then
50
+ err_t hook.commit_msg.desc_empty "${propose:-$typ}$portee$bang:"
51
+ echo_t hook.commit_msg.kept >&2
52
+ exit 1
53
+ fi
54
+ if [ "$propose" != "$typ" ]; then
55
+ if [ -n "$propose" ]; then
56
+ err_t hook.commit_msg.type_unknown "$typ" "$propose$portee$bang: $desc"
57
+ else
58
+ err_t hook.commit_msg.type_none "$typ"
59
+ types_help
60
+ fi
61
+ echo_t hook.commit_msg.kept >&2
62
+ exit 1
63
+ fi
64
+ fi
65
+
42
66
  if ! [[ "$SUBJECT" =~ $CC_PATTERN ]]; then
43
67
  if [ "${REPOWARDEN_LANG:-}" = en ]; then
44
68
  cat >&2 <<EOF
@@ -80,6 +104,15 @@ EOF
80
104
  exit 1
81
105
  fi
82
106
 
107
+ # Point final retiré, comme dans git cc (pas des points de suspension) ; seule
108
+ # la première ligne est réécrite, le corps reste intact
109
+ if [[ "$SUBJECT" == *. && "$SUBJECT" != *.. ]]; then
110
+ SUBJECT="${SUBJECT%.}"
111
+ contenu="$(awk -v s="$SUBJECT" '!fait && !/^#/ { print s; fait = 1; next } { print }' "$MSG_FILE")"
112
+ printf '%s\n' "$contenu" >"$MSG_FILE"
113
+ info_t hook.commit_msg.dot >&2
114
+ fi
115
+
83
116
  authored_length "$SUBJECT"
84
117
  if [ "$REPLY" -gt 72 ]; then
85
118
  err_t hook.commit_msg.1 "$REPLY" >&2
@@ -84,6 +84,23 @@ cfg() {
84
84
  CC_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
85
85
  CC_PATTERN="^($CC_TYPES)(\([a-z0-9._-]+\))?!?: .+"
86
86
 
87
+ # Type le plus proche d'un type inconnu : alias courants, puis préfixe commun
88
+ # (doc → docs, feature → feat, Feat → feat) ; vide si aucun
89
+ type_proche_r() {
90
+ local t="$1" c
91
+ case "$t" in
92
+ feature | features) REPLY="feat"; return ;;
93
+ doc | documentation) REPLY="docs"; return ;;
94
+ fixe | fixed | fixes | bugfix | hotfix) REPLY="fix"; return ;;
95
+ tests) REPLY="test"; return ;;
96
+ refacto | refactoring) REPLY="refactor"; return ;;
97
+ esac
98
+ REPLY=""
99
+ for c in ${CC_TYPES//|/ }; do
100
+ if [[ "$c" == "$t"* || "$t" == "$c"* ]]; then REPLY="$c"; return; fi
101
+ done
102
+ }
103
+
87
104
  # REPLY = longueur de la première ligne écrite par l'auteur : le suffixe
88
105
  # « (#123) » ajouté par GitHub lors d'un merge squash n'est pas compté.
89
106
  authored_length() {
@@ -25,8 +25,10 @@ _msg_en() {
25
25
  cc.option_value) _M="Option %s expects a value." ;;
26
26
  cc.batch_message_required) _M="Without questions, the description is required: git cc -m \"<description>\" [--type T] [--scope S] [--body …] [--refs …] [--breaking …]." ;;
27
27
  cc.type_invalid) _M="\"%s\": pick a number (1 to %s) or a name from the list." ;;
28
+ cc.type_invalid_suggest) _M="\"%s\": unknown type. Did you mean \"%s\"? Number (1 to %s) or a name from the list." ;;
28
29
  cc.step2) _M="Scope (optional)" ;;
29
30
  cc.scope_hint) _M="Part of the project affected, e.g. api, auth, chart; Enter for none." ;;
31
+ cc.scope_hint_default) _M="Part of the project, e.g. api, auth, chart; Enter keeps \"%s\", \"-\" for none." ;;
30
32
  cc.scope_prompt_default) _M="Scope (\"-\" for none) [%s]: " ;;
31
33
  cc.scope_prompt) _M="Scope: " ;;
32
34
  cc.scope_fixed) _M="Scope: lowercase letters, digits, . _ - only. Suggested: %s (Enter to accept)." ;;
@@ -40,7 +42,7 @@ _msg_en() {
40
42
  cc.breaking_note_prompt) _M="What changes: " ;;
41
43
  cc.breaking_note_invalid) _M="Describe the change (at least 10 characters); otherwise answer no to the previous question." ;;
42
44
  cc.step4) _M="Description" ;;
43
- cc.desc_hint) _M="Imperative mood, no initial capital or final period; %s characters max." ;;
45
+ cc.desc_hint) _M="Imperative mood, no final period, lowercase start except acronyms or proper nouns; %s characters max." ;;
44
46
  cc.desc_required) _M="The description is required." ;;
45
47
  cc.header_too_long) _M="Header too long: %s characters (72 max). Shorten the description." ;;
46
48
  cc.step5) _M="Details (optional)" ;;
@@ -48,7 +50,8 @@ _msg_en() {
48
50
  cc.refs_prompt_ticket) _M="References (\"-\" for none) [%s]: " ;;
49
51
  cc.refs_prompt) _M="References (e.g. %s; Enter for none): " ;;
50
52
  tk.cli.no_ticket) _M="Branch %s has no ticket: this work will not be linked to any ticket (not blocking)." ;;
51
- tk.cli.propose) _M="Start it from a ticket: repowarden ticket nouveau \"<title>\" (or answer yes in git cc)." ;;
53
+ tk.cli.propose) _M="Start it from a ticket: repowarden ticket nouveau \"<title>\"." ;;
54
+ tk.cli.propose_cc) _M="Start it from a ticket: repowarden ticket nouveau \"<title>\" (or answer yes in git cc)." ;;
52
55
  tk.cli.silence) _M="Project without ticket tracking? git config repowarden.skip tickets to stop seeing this." ;;
53
56
  tk.cli.fetch) _M="Branch of ticket #%s…" ;;
54
57
  tk.cli.switched) _M="On branch %s (ticket #%s)." ;;
@@ -111,6 +114,11 @@ _msg_en() {
111
114
  hook.pre_commit.6) _M="gitleaks not installed, secret detection skipped (install: %s)." ;;
112
115
  hook.pre_commit.7) _M="Pre-commit OK" ;;
113
116
  hook.commit_msg.1) _M="First line too long (%s > 72 characters)." ;;
117
+ hook.commit_msg.type_unknown) _M="Unknown type \"%s\". Did you mean: %s" ;;
118
+ hook.commit_msg.type_none) _M="Unknown type \"%s\". Possible types:" ;;
119
+ hook.commit_msg.desc_empty) _M="Missing description after \"%s\": describe the change in the imperative mood." ;;
120
+ hook.commit_msg.kept) _M=" Your message is not lost: git commit -e -F .git/COMMIT_EDITMSG" ;;
121
+ hook.commit_msg.dot) _M="Final period removed from the description (same rule as git cc)." ;;
114
122
  hook.pre_push.1) _M="Push refused: branch name does not follow the convention." ;;
115
123
  hook.pre_push.2) _M="Direct push to '%s' refused: it only changes through pull requests." ;;
116
124
  hook.pre_push.3) _M=" Create a branch: git switch -c feat/my-feature, then open a PR." ;;
@@ -293,6 +301,11 @@ _msg_en() {
293
301
  ci.proteger.tags_none) _M="not protected (--sans-tags)" ;;
294
302
  ci.proteger.unknown_team) _M="Unknown or inaccessible team: %s (format @organisation/team)" ;;
295
303
  ci.proteger.unknown_user) _M="Unknown user: %s" ;;
304
+ ci.proteger.legacy_taken) _M="Ruleset \"%s\": old ruleset \"%s\" taken over (repogarde check removed)." ;;
305
+ ci.proteger.legacy_left) _M="Old ruleset \"%s\" (id %s): still requires the repogarde check, removed in v4, and blocks its branches. Delete it: Settings → Rules → Rulesets, or gh api -X DELETE repos/%s/rulesets/%s." ;;
306
+ ci.proteger.verify_legacy) _M="%s (id %s): old ruleset, still requires the repogarde check (removed in v4)" ;;
307
+ ci.proteger.api_error) _M="Ruleset \"%s\" rejected by GitHub: %s" ;;
308
+ ci.proteger.incomplete) _M="Incomplete protection: see the errors above." ;;
296
309
  ci.version.usage) _M="Usage: ci/version.sh next|notes [ref]" ;;
297
310
  ci.version.breaking) _M="Breaking changes" ;;
298
311
  ci.version.features) _M="Features" ;;
@@ -352,7 +365,16 @@ _msg_en() {
352
365
  npm.first_published) _M="%s@%s published by the pipeline." ;;
353
366
  npm.step_trust) _M="3/3 Tokenless publishing (trusted publishing)" ;;
354
367
  npm.trust_exists) _M="Trusted publishing already configured." ;;
368
+ npm.trust_mismatch) _M="Existing trust relationship does not match expected parameters:" ;;
369
+ npm.trust_confirm) _M="Confirm fix (new relationship created before revoking old)? [y/N] " ;;
370
+ npm.trust_cancelled) _M="Fix cancelled." ;;
371
+ npm.trust_would_create) _M="[dry-run] Trust relationship would be created: %s, workflow %s, env %s" ;;
372
+ npm.trust_would_fix) _M="[dry-run] Trust relationship would be fixed (new created, old revoked)" ;;
373
+ npm.trust_revoked) _M="Old trust relationship revoked: %s" ;;
355
374
  npm.trust_set) _M="Trusted publishing: %s, workflow %s." ;;
375
+ npm.token_present) _M="NPM_TOKEN secret still present while trusted publishing works." ;;
376
+ npm.token_remove_confirm) _M="Remove NPM_TOKEN secret? [y/N] " ;;
377
+ npm.token_would_remove) _M="[dry-run] NPM_TOKEN secret would be removed" ;;
356
378
  npm.token_removed) _M="NPM_TOKEN secret removed from the repository." ;;
357
379
  npm.token_revoked) _M="Temporary token revoked on npm." ;;
358
380
  npm.done) _M="Done: every release publishes %s to npm, without a token." ;;
@@ -25,8 +25,10 @@ _msg_fr() {
25
25
  cc.option_value) _M="L'option %s attend une valeur." ;;
26
26
  cc.batch_message_required) _M="Sans question, la description est obligatoire : git cc -m \"<description>\" [--type T] [--scope S] [--body …] [--refs …] [--breaking …]." ;;
27
27
  cc.type_invalid) _M="« %s » : choisis un numéro (1 à %s) ou un nom de la liste." ;;
28
+ cc.type_invalid_suggest) _M="« %s » : type inconnu. Voulais-tu « %s » ? Numéro (1 à %s) ou nom de la liste." ;;
28
29
  cc.step2) _M="Scope (optionnel)" ;;
29
30
  cc.scope_hint) _M="Partie du projet concernée, ex. api, auth, chart ; Entrée pour aucun." ;;
31
+ cc.scope_hint_default) _M="Partie du projet concernée, ex. api, auth, chart ; Entrée garde « %s », « - » pour aucun." ;;
30
32
  cc.scope_prompt_default) _M="Scope (« - » pour aucun) [%s] : " ;;
31
33
  cc.scope_prompt) _M="Scope : " ;;
32
34
  cc.scope_fixed) _M="Scope : minuscules, chiffres, . _ - uniquement. Proposé : %s (Entrée pour l'accepter)." ;;
@@ -40,7 +42,7 @@ _msg_fr() {
40
42
  cc.breaking_note_prompt) _M="Ce qui change : " ;;
41
43
  cc.breaking_note_invalid) _M="Décris le changement (10 caractères au moins) ; sinon, réponds non à la question précédente." ;;
42
44
  cc.step4) _M="Description" ;;
43
- cc.desc_hint) _M="À l'impératif, sans majuscule initiale ni point final ; %s caractères au plus." ;;
45
+ cc.desc_hint) _M="À l'impératif, sans point final, minuscule initiale sauf sigle ou nom propre ; %s caractères au plus." ;;
44
46
  cc.desc_required) _M="La description est obligatoire." ;;
45
47
  cc.header_too_long) _M="En-tête trop long : %s caractères (72 max). Raccourcis la description." ;;
46
48
  cc.step5) _M="Détails (optionnels)" ;;
@@ -48,7 +50,8 @@ _msg_fr() {
48
50
  cc.refs_prompt_ticket) _M="Références (« - » pour aucune) [%s] : " ;;
49
51
  cc.refs_prompt) _M="Références (ex. %s ; Entrée pour aucune) : " ;;
50
52
  tk.cli.no_ticket) _M="Branche %s sans ticket : ce travail ne sera relié à aucun ticket (non bloquant)." ;;
51
- tk.cli.propose) _M="Le partir d'un ticket : repowarden ticket nouveau \"<titre>\" (ou réponds oui à git cc)." ;;
53
+ tk.cli.propose) _M="Partir d'un ticket : repowarden ticket nouveau \"<titre>\"." ;;
54
+ tk.cli.propose_cc) _M="Partir d'un ticket : repowarden ticket nouveau \"<titre>\" (ou réponds oui à git cc)." ;;
52
55
  tk.cli.silence) _M="Projet sans suivi de tickets ? git config repowarden.skip tickets pour ne plus le voir." ;;
53
56
  tk.cli.fetch) _M="Branche du ticket #%s…" ;;
54
57
  tk.cli.switched) _M="Sur la branche %s (ticket #%s)." ;;
@@ -109,6 +112,11 @@ _msg_fr() {
109
112
  hook.pre_commit.6) _M="gitleaks absent, détection de secrets ignorée (installation : %s)." ;;
110
113
  hook.pre_commit.7) _M="Pre-commit OK" ;;
111
114
  hook.commit_msg.1) _M="Première ligne trop longue (%s > 72 caractères)." ;;
115
+ hook.commit_msg.type_unknown) _M="Type « %s » inconnu. Voulais-tu : %s" ;;
116
+ hook.commit_msg.type_none) _M="Type « %s » inconnu. Types possibles :" ;;
117
+ hook.commit_msg.desc_empty) _M="Description manquante après « %s » : décris le changement à l'impératif." ;;
118
+ hook.commit_msg.kept) _M=" Ton message n'est pas perdu : git commit -e -F .git/COMMIT_EDITMSG" ;;
119
+ hook.commit_msg.dot) _M="Point final retiré de la description (même règle que git cc)." ;;
112
120
  hook.pre_push.1) _M="Push refusé : nom de branche non conforme." ;;
113
121
  hook.pre_push.2) _M="Push direct refusé sur '%s' : elle n'évolue que par Pull Request." ;;
114
122
  hook.pre_push.3) _M=" Crée une branche : git switch -c feat/ma-feature, puis ouvre une PR." ;;
@@ -293,6 +301,11 @@ _msg_fr() {
293
301
  ci.proteger.tags_none) _M="non protégés (--sans-tags)" ;;
294
302
  ci.proteger.unknown_team) _M="Équipe inconnue ou inaccessible : %s (format @organisation/equipe)" ;;
295
303
  ci.proteger.unknown_user) _M="Utilisateur inconnu : %s" ;;
304
+ ci.proteger.legacy_taken) _M="Ruleset « %s » : ancien ruleset « %s » repris (vérification repogarde retirée)." ;;
305
+ ci.proteger.legacy_left) _M="Ancien ruleset « %s » (id %s) : exige encore la vérification repogarde, retirée en v4, et bloque ses branches. Le supprimer : Settings → Rules → Rulesets, ou gh api -X DELETE repos/%s/rulesets/%s." ;;
306
+ ci.proteger.verify_legacy) _M="%s (id %s) : ancien ruleset, exige encore la vérification repogarde (retirée en v4)" ;;
307
+ ci.proteger.api_error) _M="Ruleset « %s » refusé par GitHub : %s" ;;
308
+ ci.proteger.incomplete) _M="Protection incomplète : voir les erreurs ci-dessus." ;;
296
309
  ci.version.usage) _M="Usage : ci/version.sh next|notes [ref]" ;;
297
310
  ci.version.breaking) _M="Changements incompatibles" ;;
298
311
  ci.version.features) _M="Fonctionnalités" ;;
@@ -352,7 +365,16 @@ _msg_fr() {
352
365
  npm.first_published) _M="%s@%s publié par la pipeline." ;;
353
366
  npm.step_trust) _M="3/3 Publication sans jeton (publication de confiance)" ;;
354
367
  npm.trust_exists) _M="Publication de confiance déjà configurée." ;;
368
+ npm.trust_mismatch) _M="Relation de confiance existante mais ne correspond pas aux paramètres attendus :" ;;
369
+ npm.trust_confirm) _M="Confirmer la correction (nouvelle relation créée avant révocation de l'ancienne) ? [o/N] " ;;
370
+ npm.trust_cancelled) _M="Correction annulée." ;;
371
+ npm.trust_would_create) _M="[dry-run] Relation de confiance serait créée : %s, workflow %s, env %s" ;;
372
+ npm.trust_would_fix) _M="[dry-run] Relation de confiance serait corrigée (nouvelle créée, ancienne révoquée)" ;;
373
+ npm.trust_revoked) _M="Ancienne relation de confiance révoquée : %s" ;;
355
374
  npm.trust_set) _M="Publication de confiance : %s, workflow %s." ;;
375
+ npm.token_present) _M="Secret NPM_TOKEN encore présent alors que la publication de confiance fonctionne." ;;
376
+ npm.token_remove_confirm) _M="Supprimer le secret NPM_TOKEN ? [o/N] " ;;
377
+ npm.token_would_remove) _M="[dry-run] Secret NPM_TOKEN serait supprimé" ;;
356
378
  npm.token_removed) _M="Secret NPM_TOKEN supprimé du dépôt." ;;
357
379
  npm.token_revoked) _M="Jeton temporaire révoqué sur npm." ;;
358
380
  npm.done) _M="Terminé : chaque release publie %s sur npm, sans jeton." ;;
@@ -168,7 +168,8 @@ ticket_hint() {
168
168
  [ -z "$REPLY" ] || return 0
169
169
  attention_t tk.cli.no_ticket "$branch"
170
170
  if tickets_managed && has gh; then
171
- dim_t tk.cli.propose
171
+ # git cc ne propose de créer le ticket qu'avec ticketAutoCreate
172
+ if ticket_auto_create; then dim_t tk.cli.propose_cc; else dim_t tk.cli.propose; fi
172
173
  else
173
174
  dim_t tk.cli.silence
174
175
  fi
package/hooks/lib/ui.sh CHANGED
@@ -31,7 +31,27 @@ repowarden_version_r() {
31
31
  local tag=""
32
32
  REPLY="$(sed -n 's/^ "version": *"\([^"]*\)".*/\1/p' "$1/package.json" 2>/dev/null | sed -n 1p)"
33
33
  if [ -e "$1/.git" ]; then
34
- tag="$(git -C "$1" describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || true)"
34
+ # Sous-shell sans les variables que git exporte aux hooks (worktree) :
35
+ # elles l'emportent sur -C, ce seraient les tags du projet. Tag le plus
36
+ # proche de HEAD (git describe), remplacé par un tag de release dont le
37
+ # commit mergé est au moins aussi proche : la release est taguée sur le
38
+ # merge de la livraison dans main, invisible depuis develop pour
39
+ # describe ; à égalité, la version stable l'emporte sur la préversion
40
+ tag="$(
41
+ unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR
42
+ best="$(git -C "$1" describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || true)"
43
+ dist=""
44
+ [ -z "$best" ] || dist="$(git -C "$1" rev-list --count "$best..HEAD")"
45
+ while IFS= read -r t; do
46
+ git -C "$1" merge-base --is-ancestor "$t^2" HEAD 2>/dev/null || continue
47
+ d="$(git -C "$1" rev-list --count "$t^2..HEAD")"
48
+ if [ -z "$best" ] || [ "$d" -lt "$dist" ] ||
49
+ { [ "$d" -eq "$dist" ] && [[ "$t" != *-* && "$best" == *-* ]]; }; then
50
+ best="$t" dist="$d"
51
+ fi
52
+ done < <(git -C "$1" tag --no-merged HEAD --list 'v[0-9]*' 2>/dev/null)
53
+ echo "$best"
54
+ )"
35
55
  [ -z "$tag" ] || REPLY="${tag#v}"
36
56
  fi
37
57
  }
@@ -2,7 +2,7 @@
2
2
  # Hook prepare-commit-msg : déduit le préfixe Conventional Commits du nom de branche.
3
3
  # branche feat/bean : git commit → éditeur prérempli avec "feat(bean): "
4
4
  # git commit -m "ajoute X" → "feat(bean): ajoute X"
5
- # Un message déjà conforme n'est pas modifié.
5
+ # Un message déjà conforme, ou qui a la forme d'un en-tête, n'est pas modifié.
6
6
  set -euo pipefail
7
7
 
8
8
  source "$(dirname "$0")/lib/common.sh"
@@ -36,6 +36,9 @@ fi
36
36
 
37
37
  SUBJECT="$(grep -v '^#' "$MSG_FILE" | sed -n 1p || true)"
38
38
  [[ "$SUBJECT" =~ $CC_PATTERN ]] && exit 0
39
+ # En-tête déjà tenté (mot: ou mot(portée):), même raté : jamais enveloppé,
40
+ # commit-msg le juge (« doc: x » deviendrait « feat(y): doc: x », accepté)
41
+ [[ "$SUBJECT" =~ ^[A-Za-z]+(\([^\)]*\))?!?: ]] && exit 0
39
42
  [[ "$SUBJECT" =~ ^(Merge|Revert|fixup!|squash!) ]] && exit 0
40
43
 
41
44
  # Ajoute le préfixe en tête de la première ligne (vide ou non)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowarden",
3
- "version": "4.7.0",
3
+ "version": "4.7.2-next.6",
4
4
  "description": "Hooks git et CI multi-langages : messages de commit, nommage des branches, secrets, formatage, tests ciblés, code mort, releases.",
5
5
  "keywords": [
6
6
  "git-hooks",