repowarden 4.11.0-next.13 → 4.11.0-next.21

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/bin/pr-checks CHANGED
@@ -21,9 +21,41 @@ if [[ "$1" == "--help" || "$1" == "-h" ]]; then
21
21
  fi
22
22
  command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
23
23
 
24
- n="${1}"
24
+ n="${1#\#}"
25
25
 
26
26
  step_t pr.checks.checking "$n"
27
- gh api "repos/{{owner}}/{{repo}}/pulls/$n/check-runs" --jq '.check_runs[] | {name: .name, conclusion: .conclusion, status: .status, url: .html_url}' 2>/dev/null | while IFS= read -r line; do
28
- echo "$line"
29
- done
27
+ # gh pr checks résout le commit de tête de la PR : les check-runs y sont
28
+ # rattachés, pas à la PR. Le verdict se calcule sur les lignes, pas sur son
29
+ # code ; sans ligne, un code non nul est une vraie erreur (PR introuvable,
30
+ # aucun check), déjà expliquée par gh
31
+ rc=0
32
+ lignes="$(gh pr checks "$n" --json name,bucket,link \
33
+ -q '.[] | [.bucket, .name, .link] | @tsv')" || rc=$?
34
+ if [ -z "$lignes" ]; then
35
+ [ "$rc" -ne 0 ] || info_t pr.checks.none "$n"
36
+ exit "$rc"
37
+ fi
38
+
39
+ reussis=0 echecs=0 attentes=0
40
+ while IFS=$'\t' read -r etat nom lien; do
41
+ case "$etat" in
42
+ pass | skipping) reussis=$((reussis + 1)) ;;
43
+ fail | cancel)
44
+ echecs=$((echecs + 1))
45
+ err_t pr.checks.failed "$nom" "$lien"
46
+ ;;
47
+ pending)
48
+ attentes=$((attentes + 1))
49
+ info_t pr.checks.pending "$nom"
50
+ ;;
51
+ esac
52
+ done <<<"$lignes"
53
+
54
+ if [ "$echecs" -gt 0 ]; then
55
+ err_t pr.checks.verdict "$n" "$reussis" "$echecs" "$attentes"
56
+ exit 1
57
+ elif [ "$attentes" -gt 0 ]; then
58
+ info_t pr.checks.verdict "$n" "$reussis" "$echecs" "$attentes"
59
+ else
60
+ ok_t pr.checks.green "$n"
61
+ fi
package/bin/run-list CHANGED
@@ -14,7 +14,7 @@ aide() {
14
14
  printf '%b\n' "$_T"
15
15
  }
16
16
 
17
- if [[ "$1" == "--help" || "$1" == "-h" ]]; then
17
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
18
18
  aide
19
19
  exit 0
20
20
  fi
@@ -34,14 +34,15 @@ done
34
34
 
35
35
  repo="${GH_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}"
36
36
 
37
+ # --jq plutôt que --template : le gabarit Go écrit les identifiants en
38
+ # notation scientifique (3.96e+10), inutilisables par run rerun
39
+ filtre=()
37
40
  if [ -n "$workflow" ]; then
38
41
  step_t run.listing.workflow "$workflow"
39
- gh run list --repo "$repo" --workflow "$workflow" --limit "$limit" \
40
- --json databaseId,name,conclusion,createdAt,headBranch \
41
- --template '{{range .}}{{.databaseId}} {{.name}} {{.conclusion}} {{.createdAt}} {{.headBranch}}{{"\n"}}{{end}}'
42
+ filtre=(--workflow "$workflow")
42
43
  else
43
44
  step_t run.listing.all
44
- gh run list --repo "$repo" --limit "$limit" \
45
- --json databaseId,name,conclusion,createdAt,headBranch \
46
- --template '{{range .}}{{.databaseId}} {{.name}} {{.conclusion}} {{.createdAt}} {{.headBranch}}{{"\n"}}{{end}}'
47
- fi
45
+ fi
46
+ gh run list --repo "$repo" ${filtre[@]+"${filtre[@]}"} --limit "$limit" \
47
+ --json databaseId,name,conclusion,createdAt,headBranch \
48
+ --jq '.[] | [.databaseId, .name, .conclusion, .createdAt, .headBranch] | @tsv'
package/bin/ticket CHANGED
@@ -18,7 +18,9 @@ source "$ROOT/hooks/lib/common.sh"
18
18
  # shellcheck source=../hooks/lib/tickets.sh
19
19
  source "$ROOT/hooks/lib/tickets.sh"
20
20
 
21
- aide() { _tr help.ticket; printf '%b\n' "$_T" >&2; }
21
+ # Aide sur la sortie standard quand elle est demandée (code 0) ; les cas
22
+ # d'erreur la redirigent eux-mêmes vers stderr
23
+ aide() { _tr help.ticket; printf '%b\n' "$_T"; }
22
24
 
23
25
  [ -n "$GIT_TOPLEVEL" ] || { err_t install.not_repo; exit 1; }
24
26
  cd "$GIT_TOPLEVEL"
@@ -62,9 +64,14 @@ cmd_ticket() {
62
64
  git fetch -q origin --prune
63
65
  ticket_remote_branch_r "$n"
64
66
  if [ -n "$REPLY" ]; then
65
- switch_to "$REPLY"
67
+ local existante="$REPLY"
68
+ switch_to "$existante"
66
69
  prendre "$n"
67
- ok_t tk.cli.switched "$REPLY" "$n"
70
+ ok_t tk.cli.switched "$existante" "$n"
71
+ # Faite avant la validation : jamais recréée, son lien va sur le ticket
72
+ if ticket_note_unlinked_branch "$n" "$existante"; then
73
+ attention_t tk.cli.unlinked "$existante" "$n"
74
+ fi
68
75
  return 0
69
76
  fi
70
77
  json="$(gh issue view "$n" --json state,title,labels \
@@ -97,6 +104,7 @@ cmd_nouveau() {
97
104
  --type) shift; type="${1:?--type attend un type}" ;;
98
105
  --description) shift; description="${1:?--description attend un texte}" ;;
99
106
  --porcelain) porcelain=1 ;;
107
+ -h | --help) aide; exit 0 ;;
100
108
  -*) err_t install.unknown_arg "$1"; exit 1 ;;
101
109
  *) titre="$1" ;;
102
110
  esac
@@ -151,6 +159,8 @@ case "${1:-}" in
151
159
  ;;
152
160
  *)
153
161
  if [[ "$1" =~ ^#?[0-9]+$ ]]; then
162
+ # ticket <n> --help : l'aide, sans prendre le ticket
163
+ case "${2:-}" in -h | --help) aide; exit 0 ;; esac
154
164
  cmd_ticket "${1#\#}"
155
165
  else
156
166
  err_t install.unknown_arg "$1"
@@ -5,12 +5,16 @@
5
5
  # Chargé par ci/tickets.sh (après hooks/lib/common.sh et hooks/lib/tickets.sh).
6
6
  # shellcheck disable=SC2154 # statuts, réglages et compteurs définis dans les autres modules
7
7
 
8
+ # Ligne de description lue comme référence de ticket (« Ticket : #12 », mot-clé
9
+ # de fermeture) : partagée avec la PR brouillon, qui cite ces lignes reprises
10
+ REF_TICKET_RE='^[[:space:]]*(Tickets?|Refs?|Closes|Fixes|Resolves)[[:space:]]*:?[[:space:]]*(.*)$'
11
+
8
12
  # REPLY = numéros de tickets (uniques) cités par la description $1 (« Ticket : #12 »,
9
13
  # « Tickets : #3, #4 », ou un mot-clé de fermeture) et la branche $2 (type/12-sujet)
10
14
  pr_tickets_r() {
11
15
  local body="$1" branch="$2" found="" n line
12
16
  while IFS= read -r line; do
13
- if [[ "$line" =~ ^[[:space:]]*(Tickets?|Refs?|Closes|Fixes|Resolves)[[:space:]]*:?[[:space:]]*(.*)$ ]]; then
17
+ if [[ "$line" =~ $REF_TICKET_RE ]]; then
14
18
  for n in $(grep -oE '#[0-9]+' <<<"${BASH_REMATCH[2]}" | tr -d '#'); do
15
19
  [[ " $found " == *" $n "* ]] || found="$found $n"
16
20
  done
@@ -53,10 +57,31 @@ statut() {
53
57
  gh issue edit "$n" --add-label "$s" ${retirer[@]+"${retirer[@]}"} >/dev/null
54
58
  }
55
59
 
60
+ # Pied des commentaires du bot : l'avatar d'une App mène à une page 404 sur
61
+ # GitHub (pas de profil), le pied dit d'où vient le bot. Repère invisible : un
62
+ # texte déjà signé (repris, mis à jour) n'en reçoit pas un second.
63
+ PIED="<!-- repowarden:pied -->"
64
+
65
+ # REPLY = texte $1 terminé par le pied (page de l'App APP_SLUG, doc) ; sans
66
+ # slug (jeton des Actions, github-actions[bot]) ou déjà signé, texte inchangé
67
+ pied_r() {
68
+ REPLY="$1"
69
+ [ -n "${APP_SLUG:-}" ] && [[ "$REPLY" != *"$PIED"* ]] || return 0
70
+ _tr tk.footer "$APP_SLUG" "$APP_SLUG"
71
+ REPLY="$REPLY"$'\n\n'"$PIED<sub>$_T</sub>"
72
+ }
73
+
74
+ # Commentaire $2 du bot sur le ticket $1, avec son pied
75
+ commenter() {
76
+ pied_r "$2"
77
+ gh issue comment "$1" --body "$REPLY" >/dev/null
78
+ }
79
+
56
80
  # Ferme la PR $1 avec le commentaire $2 ; déjà fermée ou mergée entre-temps
57
81
  # (merge pendant le run) : rien à faire, pas un échec
58
82
  fermer_pr() {
59
- gh pr close "$1" --comment "$2" >/dev/null 2>&1 && return 0
83
+ pied_r "$2"
84
+ gh pr close "$1" --comment "$REPLY" >/dev/null 2>&1 && return 0
60
85
  [ "$(gh pr view "$1" --json state -q .state 2>/dev/null)" != OPEN ]
61
86
  }
62
87
 
@@ -17,12 +17,14 @@ accepter() {
17
17
  gh issue edit "$n" --remove-label "$A_DECIDER" >/dev/null
18
18
  fi
19
19
  # La branche naît quand le ticket est pris (assigné) : un ticket validé
20
- # pour plus tard n'a pas de branche qui vieillit en attendant
21
- if [ -n "$(assignes "$n")" ]; then
20
+ # pour plus tard n'a pas de branche qui vieillit en attendant. Une branche
21
+ # déjà faite à son nom (avant la validation) est reprise, jamais recréée.
22
+ branche_du_ticket_r "$n"
23
+ if [ -n "$REPLY" ] || [ -n "$(assignes "$n")" ]; then
22
24
  creer_branche "$n"
23
25
  else
24
26
  _tr tk.validated_waiting "$n"
25
- gh issue comment "$n" --body "$_T" >/dev/null
27
+ commenter "$n" "$_T"
26
28
  fi
27
29
  while IFS=$'\t' read -r pr head sha base brouillon; do
28
30
  [ -n "$pr" ] || continue
@@ -117,7 +119,7 @@ cmd_issue() {
117
119
  gh issue reopen "$ISSUE" >/dev/null
118
120
  statut "$ISSUE" "$S_PREPROD"
119
121
  _tr tk.reopened "$fpr" "$fbase"
120
- gh issue comment "$ISSUE" --body "$_T" >/dev/null
122
+ commenter "$ISSUE" "$_T"
121
123
  notice "$_T"
122
124
  return 0
123
125
  fi
package/ci/tickets/pr.sh CHANGED
@@ -50,7 +50,7 @@ cmd_pr() {
50
50
  for n in $tickets; do
51
51
  statut "$n" "$S_PREPROD"
52
52
  _tr tk.merged "$PR" "$BASE"
53
- gh issue comment "$n" --body "$_T" >/dev/null
53
+ commenter "$n" "$_T"
54
54
  done
55
55
  return 0
56
56
  fi
@@ -90,15 +90,128 @@ criteres_coches_r() {
90
90
  REPLY="${sortie%$'\n'}"
91
91
  }
92
92
 
93
- # REPLY = description de la PR brouillon du ticket $1 (branche $2) : le ticket
94
- # (critères en cases à cocher), puis la liste des commits, puis « Ticket : #N »
93
+ # Fin de chaque commentaire lu d'un bloc (séparateur ASCII) : un texte écrit à
94
+ # la main ne le contient pas, les commentaires se découpent sans ambiguïté
95
+ SEP_COMMENTAIRE=$'\x1e'
96
+
97
+ # REPLY = commentaires de mainteneur du ticket $1, chacun suivi d'une ligne
98
+ # SEP_COMMENTAIRE : comme la conception, seul un mainteneur alimente la PR
99
+ commentaires_mainteneur_r() {
100
+ REPLY="$(gh issue view "$1" --json comments -q '.comments[]
101
+ | select(.authorAssociation == "OWNER" or .authorAssociation == "MEMBER" or .authorAssociation == "COLLABORATOR")
102
+ | .body + "\n\u001e"' 2>/dev/null || true)"
103
+ }
104
+
105
+ # $1 = ligne d'un commentaire → REPLY : titre décalé d'un niveau (rangé sous la
106
+ # section du bot), ligne citée si pr_tickets_r la lirait : seul le pied compte
107
+ ligne_reprise_r() {
108
+ REPLY="$1"
109
+ [[ ! "$REPLY" =~ ^#{1,5}[[:space:]] ]] || REPLY="#$REPLY"
110
+ [[ ! "$REPLY" =~ $REF_TICKET_RE ]] || REPLY="> $REPLY"
111
+ }
112
+
113
+ # $1 = texte → REPLY : sans ses lignes vides finales
114
+ sans_fin_vide_r() {
115
+ REPLY="$1"
116
+ while [[ "$REPLY" == *$'\n' ]]; do REPLY="${REPLY%$'\n'}"; done
117
+ }
118
+
119
+ # Première ligne d'un commentaire d'ajout : titre ou gras « Ajout… », « Constat lié… »
120
+ AJOUT_RE='^(#{1,6}[[:space:]]+|\*\*)?([Aa]jouts?|[Cc]onstats?[[:space:]]+li(é|e)s?)([[:space:]:*(.,-]|$)'
121
+
122
+ # $1 = commentaires (commentaires_mainteneur_r) → REPLY : ceux dont la première
123
+ # ligne est un titre ou un gras « Ajout » / « Constat lié », dans l'ordre :
124
+ # les ajouts faits au ticket après coup, que le corps du ticket n'a pas
125
+ ajouts_ticket_r() {
126
+ local ligne bloc="" premiere="" sortie=""
127
+ while IFS= read -r ligne; do
128
+ ligne="${ligne%$'\r'}"
129
+ if [ "$ligne" = "$SEP_COMMENTAIRE" ]; then
130
+ if [[ "$premiere" =~ $AJOUT_RE ]]; then
131
+ sans_fin_vide_r "$bloc"
132
+ sortie="$sortie${sortie:+$'\n\n'}$REPLY"
133
+ fi
134
+ bloc="" premiere=""
135
+ continue
136
+ fi
137
+ if [ -z "$premiere" ]; then
138
+ [ -n "${ligne//[[:space:]]/}" ] || continue
139
+ premiere="$ligne"
140
+ fi
141
+ ligne_reprise_r "$ligne"
142
+ bloc="$bloc$REPLY"$'\n'
143
+ done <<<"$1"$'\n'"$SEP_COMMENTAIRE"
144
+ REPLY="$sortie"
145
+ }
146
+
147
+ # $1 = commentaires → REPLY : du dernier « Conception retenue », les blocs « Ce
148
+ # qui change » et « Hors périmètre » (titre ou gras, jusqu'au libellé suivant) :
149
+ # le périmètre sans les détails (fichiers, tests) que montre déjà le diff
150
+ resume_conception_r() {
151
+ local ligne bloc="" dernier="" garder=0 conception=0
152
+ while IFS= read -r ligne; do
153
+ ligne="${ligne%$'\r'}"
154
+ if [ "$ligne" = "$SEP_COMMENTAIRE" ]; then
155
+ [ "$conception" = 0 ] || dernier="$bloc"
156
+ bloc="" garder=0 conception=0
157
+ continue
158
+ fi
159
+ if [[ "$ligne" =~ ^(#{1,6}[[:space:]]|\*\*) ]]; then
160
+ garder=0
161
+ case "$ligne" in
162
+ *[Cc]onception\ retenue*) conception=1; continue ;;
163
+ *[Cc]e\ qui\ change* | *[Hh]ors\ p*) garder=1 ;;
164
+ esac
165
+ fi
166
+ [ "$garder" = 1 ] || continue
167
+ ligne_reprise_r "$ligne"
168
+ bloc="$bloc$REPLY"$'\n'
169
+ done <<<"$1"$'\n'"$SEP_COMMENTAIRE"
170
+ sans_fin_vide_r "$dernier"
171
+ }
172
+
173
+ # $1 = description actuelle de la PR → REPLY : sa dernière section « ## Réalisé »
174
+ # (jusqu'au titre suivant ou au pied), écrite par l'auteur : reprise telle quelle
175
+ section_realise_r() {
176
+ local ligne dedans=0 section=""
177
+ while IFS= read -r ligne; do
178
+ ligne="${ligne%$'\r'}"
179
+ if [[ "$ligne" =~ ^##[[:space:]]+R(é|e)alis(é|e) ]]; then
180
+ dedans=1 section="$ligne"$'\n'
181
+ elif [ "$dedans" = 1 ]; then
182
+ if [[ "$ligne" =~ ^#{1,2}[[:space:]] || "$ligne" == "Ticket : #"* ]]; then
183
+ dedans=0
184
+ else
185
+ section="$section$ligne"$'\n'
186
+ fi
187
+ fi
188
+ done <<<"$1"
189
+ sans_fin_vide_r "$section"
190
+ }
191
+
192
+ # REPLY = description de la PR brouillon du ticket $1 (branche $2, PR $3 si
193
+ # elle existe) : le ticket (critères en cases à cocher), ses ajouts en
194
+ # commentaire, le résumé de sa conception, les commits, la section « Réalisé »
195
+ # de l'auteur gardée, puis « Ticket : #N ». Le bot ne régénère que ses sections.
95
196
  description_pr_brouillon_r() {
96
- local n="$1" branche="$2" corps commits
197
+ local n="$1" branche="$2" pr="${3:-}" corps commits commentaires
97
198
  criteres_coches_r "$(gh issue view "$n" --json body -q .body)"
98
199
  corps="$REPLY"
200
+ commentaires_mainteneur_r "$n"
201
+ commentaires="$REPLY"
202
+ ajouts_ticket_r "$commentaires"
203
+ _tr tk.pr_additions
204
+ [ -z "$REPLY" ] || corps="$corps"$'\n\n## '"$_T"$'\n'"$REPLY"
205
+ resume_conception_r "$commentaires"
206
+ _tr tk.pr_design
207
+ [ -z "$REPLY" ] || corps="$corps"$'\n\n## '"$_T"$'\n'"$REPLY"
99
208
  commits="$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$branche" \
100
209
  -q '.commits[] | "- \(.sha[0:7]) \(.commit.message | split("\n")[0])"' 2>/dev/null || true)"
101
210
  [ -z "$commits" ] || corps="$corps"$'\n\n## Commits\n'"$commits"
211
+ if [ -n "$pr" ]; then
212
+ section_realise_r "$(gh pr view "$pr" --json body -q .body 2>/dev/null || true)"
213
+ [ -z "$REPLY" ] || corps="$corps"$'\n\n'"$REPLY"
214
+ fi
102
215
  REPLY="$corps"$'\n\nTicket : #'"$n"
103
216
  }
104
217
 
@@ -124,7 +237,7 @@ cmd_push() {
124
237
  [ "$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$PUSH_BRANCH" -q .ahead_by)" != 0 ] || return 0
125
238
  titre_pr_brouillon_r "$n" "$PUSH_BRANCH"
126
239
  titre="$REPLY"
127
- description_pr_brouillon_r "$n" "$PUSH_BRANCH"
240
+ description_pr_brouillon_r "$n" "$PUSH_BRANCH" "$pr"
128
241
  corps="$REPLY"
129
242
  if [ -n "$pr" ]; then
130
243
  gh pr edit "$pr" --title "$titre" --body "$corps" >/dev/null
@@ -4,20 +4,8 @@
4
4
  # Chargé par ci/tickets.sh (après hooks/lib/common.sh et hooks/lib/tickets.sh).
5
5
  # shellcheck disable=SC2154 # statuts, réglages et compteurs définis dans les autres modules
6
6
 
7
- # Registre des branches d'un ticket : repère invisible dans les commentaires
8
- # du bot. GitHub ne lie une branche au ticket qu'à sa création, et une branche
9
- # liée supprimée y perd son nom : le registre garde le nom de chaque branche.
10
- REPERE="repowarden:branche"
11
-
12
- repere() { printf '<!-- %s %s -->' "$REPERE" "$1"; }
13
-
14
- # REPLY = branches inscrites sur le ticket $1 (séparées par des espaces)
15
- branches_inscrites_r() {
16
- local corps
17
- corps="$(gh issue view "$1" --json comments -q '.comments[].body' 2>/dev/null || true)"
18
- REPLY="$(grep -oE "<!-- $REPERE [^ ]+ -->" <<<"$corps" | awk '{ printf "%s ", $3 }' || true)"
19
- REPLY="${REPLY% }"
20
- }
7
+ # Registre (REPERE, repere, branches_inscrites_r) : hooks/lib/tickets.sh,
8
+ # partagé avec repowarden ticket.
21
9
 
22
10
  # Inscrit la branche $2 sur le ticket $1, une seule fois ($3 : origine, ex. PR #7).
23
11
  # Une seconde branche pour le même ticket est inscrite et signalée.
@@ -34,7 +22,7 @@ inscrire_branche() {
34
22
  _tr tk.branch_registered "$b"
35
23
  fi
36
24
  [ -z "$origine" ] || _T="$_T ($origine)"
37
- gh issue comment "$n" --body "$_T"$'\n\n'"$(repere "$b")" >/dev/null
25
+ commenter "$n" "$_T"$'\n\n'"$(repere "$b")"
38
26
  INSCRITES=$((INSCRITES + 1))
39
27
  }
40
28
 
@@ -65,19 +53,23 @@ nettoyer_branche() {
65
53
  notice_t tk.branch_deleted "$branche" "$n"
66
54
  else
67
55
  _tr tk.branch_kept "$branche" "${avance:-?}" "${prs:--}"
68
- gh issue comment "$n" --body "$_T" >/dev/null
56
+ commenter "$n" "$_T"
69
57
  fi
70
58
  }
71
59
 
72
60
  # Ticket $1 validé : sa branche naît de la branche d'intégration, liée au
73
- # ticket (panneau « Development »). Rien si elle existe déjà, ou si une PR
74
- # ouverte cite déjà le ticket (travail commencé hors de sa branche).
61
+ # ticket (panneau « Development »). Rien si une PR ouverte cite déjà le ticket
62
+ # (travail commencé hors de sa branche) ; si elle existe déjà, jamais recréée :
63
+ # faite avant la validation, elle n'est pas liée et son lien est inscrit.
75
64
  creer_branche() {
76
65
  local n="$1" titre labels branche pr head body
77
66
  # Travail déjà mergé ou en production : plus de branche à (re)créer
78
67
  travail_termine "$(etiquettes "$n")" && return 0
79
68
  branche_du_ticket_r "$n"
80
- [ -z "$REPLY" ] || return 0
69
+ if [ -n "$REPLY" ]; then
70
+ ticket_note_unlinked_branch "$n" "$REPLY" || true
71
+ return 0
72
+ fi
81
73
  while IFS=$'\t' read -r pr head; do
82
74
  [ -n "$pr" ] || continue
83
75
  body="$(gh pr view "$pr" --json body -q .body)"
@@ -97,6 +89,6 @@ creer_branche() {
97
89
  -f oid="$(gh api "repos/$GH_REPO/git/ref/heads/$INTEGRATION" -q .object.sha)" \
98
90
  -f name="$branche" >/dev/null
99
91
  _tr tk.branch_created "$branche" "$INTEGRATION" "$n" "$branche"
100
- gh issue comment "$n" --body "$_T"$'\n\n'"$(repere "$branche")" >/dev/null
92
+ commenter "$n" "$_T"$'\n\n'"$(repere "$branche")"
101
93
  notice_t tk.branch_notice "$branche" "$n"
102
94
  }
@@ -21,10 +21,11 @@ cmd_release() {
21
21
  while IFS= read -r n; do
22
22
  [ -n "$n" ] || continue
23
23
  if [[ "$tag" == *-* ]]; then
24
- gh issue comment "$n" --body "$msg" >/dev/null
24
+ commenter "$n" "$msg"
25
25
  else
26
26
  statut "$n" "$S_DONE"
27
- gh issue close "$n" --comment "$msg" >/dev/null
27
+ pied_r "$msg"
28
+ gh issue close "$n" --comment "$REPLY" >/dev/null
28
29
  nettoyer_branche "$n"
29
30
  fi
30
31
  done < <(gh issue list --state open --label "$S_PREPROD" --limit 500 --json number -q '.[].number')
package/ci/tickets.sh CHANGED
@@ -25,8 +25,8 @@
25
25
  # feat/12-sujet. « Closes #12 » fermerait le ticket dès le merge dans develop :
26
26
  # c'est la release qui le ferme.
27
27
  #
28
- # Variables : GH_TOKEN, GH_REPO, INTEGRATION ; pr : PR, ACTION, MERGED, DRAFT,
29
- # HEAD, BASE, HEAD_SHA, AUTHOR ; issue : ISSUE, ACTION, LABEL, STATE_REASON ;
28
+ # Variables : GH_TOKEN, GH_REPO, INTEGRATION, APP_SLUG (pied des commentaires) ;
29
+ # pr : PR, ACTION, MERGED, DRAFT, HEAD, BASE, HEAD_SHA, AUTHOR ; issue : ISSUE, ACTION, LABEL, STATE_REASON ;
30
30
  # push : PUSH_BRANCH, DELETED, PUSHER, SHA ; revue : PR, HEAD, REVIEW_STATE ;
31
31
  # release : TAG, ou DELIVERED_SHA et MAIN (livraison sans nouvelle version)
32
32
  set -euo pipefail
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # Fonctions ajoutées sans commentaire : chaque nouvelle fonction doit dire,
3
+ # juste au-dessus, ce qu'elle fait et pourquoi (docstring acceptée en Python).
4
+ # Comme pour le code mort, seul le code ajouté compte : une fonction dont le
5
+ # nom existait déjà dans le fichier n'est jamais signalée, même retouchée.
6
+ # Avertissement seulement (pre-commit et CI), désactivable par skip = comments.
7
+ # Langages : Shell (.sh, .bash, ou shebang sh/bash), Python, JavaScript/TypeScript.
8
+
9
+ # Une ligne « fichier:ligne<TAB>nom » par fonction ajoutée sans commentaire
10
+ # entre $1 (commit de base) et $2 (commit, ou vide pour le contenu stagé).
11
+ comments_missing() {
12
+ local base="$1" new="$2" st a b f old lang added tmp
13
+ tmp="$(mktemp -d)"
14
+ while IFS=$'\t' read -r st a b; do
15
+ [ -n "$a" ] || continue
16
+ # Renommage ou copie : l'ancien chemin donne les fonctions existantes
17
+ if [ -n "$b" ]; then old="$a" f="$b"; else old="$a" f="$a"; fi
18
+ is_excluded "$f" && continue
19
+ case "${f##*/}" in
20
+ *.sh | *.bash) lang=shell ;;
21
+ *.py) lang=py ;;
22
+ *.js | *.jsx | *.mjs | *.cjs | *.ts | *.tsx) lang=js ;;
23
+ *.*) continue ;;
24
+ *) lang=shebang ;;
25
+ esac
26
+ git show "$new:$f" >"$tmp/new" 2>/dev/null || continue
27
+ if [ "$lang" = shebang ]; then
28
+ # Script sans extension (bin/…) : seul un shebang sh ou bash compte
29
+ IFS= read -r st <"$tmp/new" || true
30
+ [[ "$st" =~ ^#!.*[/\ ](ba)?sh([[:space:]]|$) ]] || continue
31
+ lang=shell
32
+ fi
33
+ git show "$base:$old" >"$tmp/base" 2>/dev/null || : >"$tmp/base"
34
+ if [ -n "$new" ]; then
35
+ git diff -U0 --no-color --no-ext-diff "$base" "$new" -- "$f"
36
+ else
37
+ git diff --cached -U0 --no-color --no-ext-diff "$base" -- "$f"
38
+ fi 2>/dev/null >"$tmp/diff"
39
+ # Numéros des lignes ajoutées ou modifiées, d'après les en-têtes @@
40
+ added="$(awk '/^@@/ {
41
+ split($3, a, ","); s = substr(a[1], 2); n = (a[2] == "" ? 1 : a[2])
42
+ for (i = 0; i < n; i++) printf " %d", s + i
43
+ }' "$tmp/diff") "
44
+ [ "$added" != " " ] || continue
45
+ awk -v lang="$lang" -v added="$added" -v f="$f" -v q="'" "$COMMENTS_AWK" "$tmp/base" "$tmp/new"
46
+ done < <(
47
+ if [ -n "$new" ]; then
48
+ git diff --name-status -M --diff-filter=ACMR "$base" "$new"
49
+ else
50
+ git diff --cached --name-status -M --diff-filter=ACMR "$base"
51
+ fi 2>/dev/null
52
+ )
53
+ rm -rf "$tmp"
54
+ }
55
+
56
+ # Avertit des fonctions ajoutées sans commentaire, sans jamais bloquer : le
57
+ # commentaire aide la relecture, son absence n'est pas une faute. REPLY = nombre.
58
+ comments_check() {
59
+ local loc name n=0
60
+ while IFS=$'\t' read -r loc name; do
61
+ [ -n "$loc" ] || continue
62
+ n=$((n + 1))
63
+ attention_t cm.missing "$loc" "$name"
64
+ done < <(comments_missing "$1" "$2")
65
+ [ "$n" = 0 ] || info_t cm.summary "$n"
66
+ REPLY="$n"
67
+ }
68
+
69
+ # Programme awk de comments_missing : lit le fichier de base (noms des
70
+ # fonctions existantes) puis le nouveau, et signale les fonctions nouvelles
71
+ # dont aucune ligne de commentaire ne précède la déclaration
72
+ # shellcheck disable=SC2016 # programme awk, rien à développer
73
+ COMMENTS_AWK='
74
+ # Nom de la fonction déclarée par la ligne l (vide sinon), selon le langage
75
+ function nom(l) {
76
+ if (lang == "shell") {
77
+ if (l ~ /^[ \t]*function[ \t]+[A-Za-z_]/) {
78
+ sub(/^[ \t]*function[ \t]+/, "", l); sub(/[ \t(){}].*$/, "", l); return l
79
+ }
80
+ if (l ~ /^[ \t]*[A-Za-z_][A-Za-z0-9_:.@-]*[ \t]*\(\)/) {
81
+ sub(/^[ \t]*/, "", l); sub(/[ \t]*\(\).*$/, "", l); return l
82
+ }
83
+ } else if (lang == "py") {
84
+ if (l ~ /^[ \t]*(async[ \t]+)?def[ \t]+[A-Za-z_]/) {
85
+ sub(/^[ \t]*(async[ \t]+)?def[ \t]+/, "", l); sub(/[^A-Za-z0-9_].*$/, "", l); return l
86
+ }
87
+ } else {
88
+ if (l ~ /^[ \t]*(export[ \t]+(default[ \t]+)?)?(async[ \t]+)?function[ \t]*\*?[ \t]*[A-Za-z_$]/) {
89
+ sub(/^[ \t]*(export[ \t]+(default[ \t]+)?)?(async[ \t]+)?function[ \t]*\*?[ \t]*/, "", l)
90
+ sub(/[^A-Za-z0-9_$].*$/, "", l); return l
91
+ }
92
+ if (l ~ /^[ \t]*(export[ \t]+)?(const|let|var)[ \t]+[A-Za-z_$][A-Za-z0-9_$]*[ \t]*(:[^=]*)?=[ \t]*(async[ \t]+)?(function[^A-Za-z0-9_$]|function$|\([^)]*\)[^=]*=>|[A-Za-z_$][A-Za-z0-9_$]*[ \t]*=>)/) {
93
+ sub(/^[ \t]*(export[ \t]+)?(const|let|var)[ \t]+/, "", l); sub(/[^A-Za-z0-9_$].*$/, "", l); return l
94
+ }
95
+ }
96
+ return ""
97
+ }
98
+ # Directive d outil (shellcheck, eslint, noqa…) : n explique rien, on regarde au-dessus
99
+ function directive(l) {
100
+ return l ~ /^[ \t]*(#|\/\/)[ \t]*(shellcheck|noqa|type:|pylint:|eslint|@ts-|prettier-)/
101
+ }
102
+ # Commentaire juste au-dessus de la ligne i (décorateurs et directives sautés),
103
+ # ou docstring en tête du corps pour Python
104
+ function commentee(i, j, s) {
105
+ for (j = i - 1; j > 0 && (L[j] ~ /^[ \t]*@/ || directive(L[j])); j--) ;
106
+ if (j > 0 && L[j] !~ /^#!/ && L[j] ~ /^[ \t]*(#|\/\/|\/\*|\*)/) return 1
107
+ if (lang != "py") return 0
108
+ # Fin de la signature (deux-points en fin de ligne), puis première ligne non vide
109
+ for (j = i; j <= N && j < i + 20 && L[j] !~ /:[ \t]*(#.*)?$/; j++) ;
110
+ for (j++; j <= N && L[j] ~ /^[ \t]*$/; j++) ;
111
+ s = "^[ \t]*[rRuUbBfF]*[\"" q "]"
112
+ return j <= N && L[j] ~ s
113
+ }
114
+ FILENAME == ARGV[1] { n = nom($0); if (n != "") ancien[n] = 1; next }
115
+ { L[FNR] = $0; N = FNR }
116
+ END {
117
+ for (i = 1; i <= N; i++) {
118
+ if (index(added, " " i " ") == 0) continue
119
+ n = nom(L[i])
120
+ if (n == "" || (n in ancien) || commentee(i)) continue
121
+ print f ":" i "\t" n
122
+ }
123
+ }'
@@ -56,6 +56,7 @@ _msg_en() {
56
56
  tk.cli.silence) _M="Project without ticket tracking? git config repowarden.skip tickets to stop seeing this." ;;
57
57
  tk.cli.fetch) _M="Branch of ticket #%s…" ;;
58
58
  tk.cli.switched) _M="On branch %s (ticket #%s)." ;;
59
+ tk.cli.unlinked) _M="Branch %s was created before validation: GitHub cannot link it to ticket #%s afterwards, its link is written on the ticket." ;;
59
60
  tk.cli.created_branch) _M="Branch %s created from %s, linked to ticket #%s." ;;
60
61
  tk.cli.not_validated) _M="Ticket #%s not validated yet: its branch will be created automatically on validation (\"%s\" label)." ;;
61
62
  tk.cli.closed) _M="Ticket #%s is closed: no branch to create." ;;
@@ -121,7 +122,7 @@ _msg_en() {
121
122
  help.code-mort) _M="Usage: repowarden code-mort [base]\n\nDead code introduced by your branch (like CI): only added/changed lines\nsince base (commits, staged/uncommitted, new files). Default: merge-base\nwith origin/main. See docs/code-mort.md (proven vs candidates)." ;;
122
123
  help.ticket) _M="Usage: repowarden ticket <n> branch for ticket (created, linked if needed)\n repowarden ticket ticket of current branch\n repowarden ticket nouveau \"<title>\" [--type fix] [--description \"…\"]\n repowarden ticket comment <n> \"<text>\" add comment\n repowarden ticket label <n> <label> [--remove] manage labels\n\nTicket first: taking <n> assigns to you and switches to its branch\n(created from develop if needed). \"nouveau\" creates a ticket (\"à valider\"\nif tracking is managed). Needs gh connected (https://cli.github.com)." ;;
123
124
  help.tickets) _M="Usage: repowarden tickets init [--repo owner/name] [--sans-validation-auto]\n repowarden tickets sync\n\nSets up ticket tracking: labels, issue template, workflow; classifies\nexisting tickets; records PR branches. sync checks out local branches for\ntickets assigned to you (auto with ticketSync=auto, manual by default)." ;;
124
- help.pr) _M="Usage: repowarden pr label <n> <label> [--remove]\n repowarden pr checks <n>\n\nlabel adds or removes a label from PR #<n>; checks shows its checks\n(name, conclusion, status, link).\nExamples:\n repowarden pr label 123 \"ready-for-review\"\n repowarden pr label 123 \"needs-changes\" --remove\n repowarden pr checks 123" ;;
125
+ help.pr) _M="Usage: repowarden pr label <n> <label> [--remove]\n repowarden pr checks <n>\n\nlabel adds or removes a label from PR #<n>; checks lists its failed\njobs (with their link) and pending ones, then the verdict; exit 1 if a job fails.\nExamples:\n repowarden pr label 123 \"ready-for-review\"\n repowarden pr label 123 \"needs-changes\" --remove\n repowarden pr checks 123" ;;
125
126
  help.run) _M="Usage: repowarden run rerun <run-id|--failed|--workflow <name>>\n repowarden run list [--workflow <name>] [--limit N]\n\nrerun restarts a GitHub Actions workflow. list shows recent runs\n(default limit 20).\nExamples:\n repowarden run rerun --failed\n repowarden run list --workflow \"CI\" --limit 10" ;;
126
127
  help.proteger) _M="Usage: repowarden proteger [--repo owner/name] [--checks \"repowarden,tests (ubuntu-latest)\"]\n [--relecteurs N] [--codeowners] [--environnement NAME] [--approbateurs \"login …\"]\n [--sans-tags] [--dry-run] [--verifier]\n\nProtects GitHub repo per .repowarden.conf: required PR, required checks,\nno force push/deletion, ruleset, squash merges, v* tags reserved to\nworkflows, optional human approvals. --verifier checks online rules\nwithout changes. Needs gh and admin rights." ;;
127
128
  help.migrer) _M="Usage: repowarden migrer [--pr] [--hooks]\n\nMigrates from repogarde to repowarden (docs/migration-v4.md): configs,\nworkflows, renames, lists manual steps left. Safe to rerun.\n --pr on chore/migration-repowarden-vN: commit, push and PR\n --hooks also move leftover lefthook hooks to .git/hooks-lefthook/" ;;
@@ -293,6 +294,11 @@ _msg_en() {
293
294
  ci.fix_pr.2) _M="PR #%s retargeted: %s → %s (%s flow)" ;;
294
295
  ci.fix_pr.3) _M="Title follows the convention: %s" ;;
295
296
  ci.fix_pr.4) _M="Title \"%s\" replaced with \"%s\"" ;;
297
+ ci.verrous_pip.compile) _M="Lock file %s: re-running the command from its header" ;;
298
+ ci.verrous_pip.refused) _M="Lock file %s skipped: the output of its header command is not this file" ;;
299
+ ci.verrous_pip.compliant) _M="pip lock files match their header: no commit" ;;
300
+ ci.verrous_pip.committed) _M="Lock files regenerated and committed: %s" ;;
301
+ ci.verrous_pip.pushed) _M="Commit pushed to %s" ;;
296
302
  ci.version.1) _M="version announced by the delivery PR" ;;
297
303
  ci.version.2) _M="No feat, fix, perf or breaking change since %s: no version." ;;
298
304
  ci.version.3) _M="first version" ;;
@@ -426,6 +432,12 @@ _msg_en() {
426
432
  dc.knip_file) _M="file never imported" ;;
427
433
  dc.knip_export) _M="export never imported:" ;;
428
434
  dc.knip_dep) _M="dependency never imported:" ;;
435
+ # Functions added without a comment
436
+ cm.section) _M="Functions without a comment (new code only)" ;;
437
+ cm.no_base) _M="No base: comment check skipped." ;;
438
+ cm.missing) _M="%s: function %s has no comment right above it" ;;
439
+ cm.summary) _M="%s function(s) added without a comment: one or two lines right above, saying what it does and why (warning, not blocking)." ;;
440
+ cm.none) _M="Every added function has a comment." ;;
429
441
  ci.section.commits) _M="Commit messages (Conventional Commits)" ;;
430
442
  ci.section.branch) _M="Branch name" ;;
431
443
  ci.section.secrets) _M="Secrets (gitleaks)" ;;
@@ -644,6 +656,7 @@ _msg_en() {
644
656
  tk.pr_backmerge_empty) _M="PR closed automatically: \`%s\` brings no content to \`%s\`. The back-merge is done automatically after a release, only when there is something to bring back." ;;
645
657
  tk.pr_delivery_manual) _M="PR closed automatically: the \`%s\` → \`%s\` delivery is opened by the release workflow; a PR opened by hand would never get a deployment approval. Run *Actions → release → Run workflow* on \`%s\` (or \`gh workflow run release.yml --ref %s\`), then **Approve and deploy** on the run." ;;
646
658
  tk.branch_registered) _M="Ticket branch: \`%s\`." ;;
659
+ tk.branch_unlinked) _M="Ticket branch: [\`%s\`](%s). Created before the ticket was validated: GitHub does not link an existing branch, so it does not show under *Development*; this comment keeps its link." ;;
647
660
  tk.branch_second) _M="Second branch for this ticket: \`%s\` (already: %s). One ticket, one branch: group the work, or open a separate ticket." ;;
648
661
  tk.adopted) _M="Existing tickets: %s sorted, %s branch(es) registered." ;;
649
662
  tk.init_adopt) _M="Existing tickets and branches" ;;
@@ -652,6 +665,8 @@ _msg_en() {
652
665
  tk.branch_kept) _M="Branch \`%s\` kept: %s commit(s) missing from the integration branch, open PR(s): %s. Delete it by hand if the work lives elsewhere." ;;
653
666
  tk.draft_opened) _M="Draft PR #%s opened for ticket #%s" ;;
654
667
  tk.draft_updated) _M="PR #%s (ticket #%s): title and description taken from the ticket" ;;
668
+ tk.pr_additions) _M="Ticket additions" ;;
669
+ tk.pr_design) _M="Design (summary)" ;;
655
670
  tk.created_body) _M="Ticket created automatically for PR #%s, which cited none. To be validated by a maintainer (\"validé\" label)." ;;
656
671
  tk.created) _M="Ticket #%s created for PR #%s and linked in its description: to be validated by a maintainer." ;;
657
672
  tk.init_labels) _M="Labels (statuses and validation)" ;;
@@ -665,6 +680,7 @@ _msg_en() {
665
680
  tk.init_next_wired) _M="Next: commit these files; require the \"ticket\" check (repowarden proteger --checks \"…,ticket\"). The tickets job of release.yml is already wired." ;;
666
681
  tk.prerelease) _M="Available in pre-release **%s**: to be tested before production." ;;
667
682
  tk.released) _M="Released in **%s**." ;;
683
+ tk.footer) _M="Posted by this repository's GitHub App ([repowarden docs](https://simdevwarden.github.io/repowarden/en/industrialisation/#github-app-for-workflows)): [%s](https://github.com/apps/%s)" ;;
668
684
  tk.delivered) _M="Delivered to **%s**, without a new version (commit %s)." ;;
669
685
  # Ticket comment
670
686
  tk.comment.adding) _M="Adding comment to ticket #%s..." ;;
@@ -691,6 +707,11 @@ _msg_en() {
691
707
  run.listing.workflow) _M="Listing runs for workflow \"%s\"..." ;;
692
708
  # PR checks
693
709
  pr.checks.checking) _M="Checking checks for PR #%s..." ;;
710
+ pr.checks.failed) _M="Failed: %s %s" ;;
711
+ pr.checks.pending) _M="Pending: %s" ;;
712
+ pr.checks.verdict) _M="PR #%s: %s passed, %s failed, %s pending." ;;
713
+ pr.checks.green) _M="All checks of PR #%s are green." ;;
714
+ pr.checks.none) _M="No check on PR #%s." ;;
694
715
  # AI agent guard (repowarden garde, repowarden agents)
695
716
  garde.format) _M="Unknown format \"%s\": claude or opencode." ;;
696
717
  garde.no_jq) _M="repowarden garde: jq missing, action not checked (install jq: https://jqlang.org)." ;;
@@ -56,6 +56,7 @@ _msg_fr() {
56
56
  tk.cli.silence) _M="Projet sans suivi de tickets ? git config repowarden.skip tickets pour ne plus le voir." ;;
57
57
  tk.cli.fetch) _M="Branche du ticket #%s…" ;;
58
58
  tk.cli.switched) _M="Sur la branche %s (ticket #%s)." ;;
59
+ tk.cli.unlinked) _M="Branche %s créée avant la validation : GitHub ne la lie pas au ticket #%s après coup, son lien est inscrit sur le ticket." ;;
59
60
  tk.cli.created_branch) _M="Branche %s créée depuis %s, liée au ticket #%s." ;;
60
61
  tk.cli.not_validated) _M="Ticket #%s pas encore validé : sa branche sera créée automatiquement à la validation (étiquette « %s »)." ;;
61
62
  tk.cli.closed) _M="Ticket #%s fermé : pas de branche à créer." ;;
@@ -244,7 +245,7 @@ _msg_fr() {
244
245
  help.code-mort) _M="Usage : repowarden code-mort [base]\n\nCode mort introduit par ta branche, comme en CI : seules les lignes ajoutées\nou modifiées depuis la base comptent (commits, modifications en cours,\nnouveaux fichiers). Défaut : point de départ de la branche depuis origin/main.\nVoir docs/code-mort.md : ce qui est prouvé (bloquant) et ce qui est candidat." ;;
245
246
  help.ticket) _M="Usage : repowarden ticket <n> branche du ticket (créée, liée, si besoin)\n repowarden ticket ticket de la branche courante\n repowarden ticket nouveau \"<titre>\" [--type fix] [--description \"…\"]\n repowarden ticket comment <n> \"<texte>\" commentaire sur le ticket\n repowarden ticket label <n> <label> [--remove] labels du ticket\n\nLe ticket d'abord : prendre <n> l'assigne à toi et place sur sa branche\n(créée depuis develop au besoin) ; nouveau crée le ticket (« à valider » si\nle suivi est géré par repowarden). Demande gh connecté (https://cli.github.com)." ;;
246
247
  help.tickets) _M="Usage : repowarden tickets init [--repo propriétaire/nom] [--sans-validation-auto]\n repowarden tickets sync\n\nPrépare le suivi des tickets d'un dépôt GitHub (relançable sans risque) :\nétiquettes (« validé », statuts et types, préfixe des branches), modèle de\nticket, workflow ; tickets existants rangés ; branches des PR inscrites sur\nleurs tickets. sync : branches locales des tickets qui me sont assignés\n(automatique avec ticketSync = auto, défaut manual)." ;;
247
- help.pr) _M="Usage : repowarden pr label <n> <label> [--remove]\n repowarden pr checks <n>\n\nlabel ajoute ou retire un label sur la PR #<n> ; checks affiche l'état de\nses checks (nom, conclusion, statut, lien).\nExemples :\n repowarden pr label 123 \"ready-for-review\"\n repowarden pr label 123 \"needs-changes\" --remove\n repowarden pr checks 123" ;;
248
+ help.pr) _M="Usage : repowarden pr label <n> <label> [--remove]\n repowarden pr checks <n>\n\nlabel ajoute ou retire un label sur la PR #<n> ; checks liste ses jobs en\néchec (avec leur lien) et en attente, puis le verdict ; code 1 si un job échoue.\nExemples :\n repowarden pr label 123 \"ready-for-review\"\n repowarden pr label 123 \"needs-changes\" --remove\n repowarden pr checks 123" ;;
248
249
  help.run) _M="Usage : repowarden run rerun <run-id|--failed|--workflow <nom>>\n repowarden run list [--workflow <nom>] [--limit N]\n\nrerun relance un workflow GitHub Actions : un run par son id, le dernier\néchoué (--failed) ou le dernier d'un workflow (--workflow) ; list affiche\nles runs récents (--limit, défaut 20).\nExemples :\n repowarden run rerun --failed\n repowarden run list --workflow \"CI\" --limit 10" ;;
249
250
  help.proteger) _M="Usage : repowarden proteger [--repo propriétaire/nom] [--checks \"repowarden,tests (ubuntu-latest)\"]\n [--relecteurs N] [--codeowners] [--environnement NOM] [--approbateurs \"login …\"]\n [--sans-tags] [--dry-run] [--verifier]\n\nProtège le dépôt GitHub courant d'après .repowarden.conf (relançable sans\nrisque) : PR obligatoire, vérifications exigées, ni suppression ni push forcé\n(ruleset « repowarden »), merge squash, tags v* réservés aux workflows,\nvalidation humaine optionnelle. --verifier compare les règles en ligne sans\nrien modifier. Réglages équivalents dans .repowarden.conf. Demande gh et les\ndroits d'administration du dépôt." ;;
250
251
  help.migrer) _M="Usage : repowarden migrer [--pr] [--hooks]\n\nPassage de repogarde à repowarden dans le projet courant\n(docs/migration-v4.md) : configuration (.repowarden.conf, .repowarden/),\nworkflows (uses: vers le dépôt repowarden et sa version majeure, anciens\nnoms), puis la liste de ce qui reste à faire à la main. Relançable : rien\nn'est fait deux fois.\n --pr sur la branche chore/migration-repowarden-vN : commit, push et PR\n --hooks déplace aussi les hooks lefthook résiduels dans .git/hooks-lefthook/" ;;
@@ -294,6 +295,11 @@ _msg_fr() {
294
295
  ci.fix_pr.2) _M="PR #%s reciblée : %s → %s (flux %s)" ;;
295
296
  ci.fix_pr.3) _M="Titre conforme : %s" ;;
296
297
  ci.fix_pr.4) _M="Titre « %s » remplacé par « %s »" ;;
298
+ ci.verrous_pip.compile) _M="Verrou %s : commande de son en-tête relancée" ;;
299
+ ci.verrous_pip.refused) _M="Verrou %s ignoré : la sortie de la commande de son en-tête n'est pas ce fichier" ;;
300
+ ci.verrous_pip.compliant) _M="Verrous pip conformes à leur en-tête : aucun commit" ;;
301
+ ci.verrous_pip.committed) _M="Verrous régénérés et commités : %s" ;;
302
+ ci.verrous_pip.pushed) _M="Commit poussé sur %s" ;;
297
303
  ci.version.1) _M="version annoncée par la PR de livraison" ;;
298
304
  ci.version.2) _M="Aucun feat, fix, perf ni changement incompatible depuis %s : pas de version." ;;
299
305
  ci.version.3) _M="première version" ;;
@@ -427,6 +433,12 @@ _msg_fr() {
427
433
  dc.knip_file) _M="fichier jamais importé" ;;
428
434
  dc.knip_export) _M="export jamais importé :" ;;
429
435
  dc.knip_dep) _M="dépendance jamais importée :" ;;
436
+ # Fonctions ajoutées sans commentaire
437
+ cm.section) _M="Fonctions sans commentaire (nouveau code uniquement)" ;;
438
+ cm.no_base) _M="Pas de base : contrôle des commentaires ignoré." ;;
439
+ cm.missing) _M="%s : fonction %s sans commentaire juste au-dessus" ;;
440
+ cm.summary) _M="%s fonction(s) ajoutée(s) sans commentaire : une à deux lignes juste au-dessus, qui disent ce qu'elle fait et pourquoi (avertissement, non bloquant)." ;;
441
+ cm.none) _M="Chaque fonction ajoutée est commentée." ;;
430
442
  ci.section.commits) _M="Messages de commit (Conventional Commits)" ;;
431
443
  ci.section.branch) _M="Nom de branche" ;;
432
444
  ci.section.secrets) _M="Secrets (gitleaks)" ;;
@@ -645,6 +657,7 @@ _msg_fr() {
645
657
  tk.pr_backmerge_empty) _M="PR fermée automatiquement : \`%s\` n'apporte aucun contenu à \`%s\`. Le retour est fait automatiquement après une release, seulement quand il y a quelque chose à rapporter." ;;
646
658
  tk.pr_delivery_manual) _M="PR fermée automatiquement : la livraison \`%s\` → \`%s\` est ouverte par le workflow de release ; une PR ouverte à la main n'aurait jamais d'approbation de déploiement. Lancer *Actions → release → Run workflow* sur \`%s\` (ou \`gh workflow run release.yml --ref %s\`), puis **Approve and deploy** sur le run." ;;
647
659
  tk.branch_registered) _M="Branche du ticket : \`%s\`." ;;
660
+ tk.branch_unlinked) _M="Branche du ticket : [\`%s\`](%s). Créée avant la validation du ticket : GitHub ne lie pas une branche existante, elle n'apparaît donc pas dans « Development » ; ce commentaire en garde le lien." ;;
648
661
  tk.branch_second) _M="Seconde branche pour ce ticket : \`%s\` (déjà : %s). Un ticket, une branche : regrouper le travail, ou ouvrir un ticket séparé." ;;
649
662
  tk.adopted) _M="Tickets existants : %s rangé(s), %s branche(s) inscrite(s)." ;;
650
663
  tk.init_adopt) _M="Tickets et branches existants" ;;
@@ -653,6 +666,8 @@ _msg_fr() {
653
666
  tk.branch_kept) _M="Branche \`%s\` gardée : %s commit(s) absent(s) de la branche d'intégration, PR ouverte(s) : %s. À supprimer à la main si le travail est ailleurs." ;;
654
667
  tk.draft_opened) _M="PR #%s ouverte en brouillon pour le ticket #%s" ;;
655
668
  tk.draft_updated) _M="PR #%s (ticket #%s) : titre et description repris du ticket" ;;
669
+ tk.pr_additions) _M="Ajouts du ticket" ;;
670
+ tk.pr_design) _M="Conception (résumé)" ;;
656
671
  tk.created_body) _M="Ticket créé automatiquement pour la PR #%s, qui n'en citait aucun. À valider par un mainteneur (étiquette « validé »)." ;;
657
672
  tk.created) _M="Ticket #%s créé pour la PR #%s et relié à sa description : à valider par un mainteneur." ;;
658
673
  tk.init_labels) _M="Étiquettes (statuts et validation)" ;;
@@ -666,6 +681,7 @@ _msg_fr() {
666
681
  tk.init_next_wired) _M="Ensuite : committer ces fichiers ; exiger la vérification « ticket » (repowarden proteger --checks \"…,ticket\"). Le job tickets de release.yml est déjà branché." ;;
667
682
  tk.prerelease) _M="Disponible en préversion **%s** : à tester avant la production." ;;
668
683
  tk.released) _M="Publié dans **%s**." ;;
684
+ tk.footer) _M="Message de l'App GitHub de ce dépôt ([doc repowarden](https://simdevwarden.github.io/repowarden/industrialisation/#github-app-des-workflows)) : [%s](https://github.com/apps/%s)" ;;
669
685
  tk.delivered) _M="Livré sur **%s**, sans nouvelle version (commit %s)." ;;
670
686
  # Ticket comment
671
687
  tk.comment.adding) _M="Ajout d'un commentaire sur le ticket #%s..." ;;
@@ -692,6 +708,11 @@ _msg_fr() {
692
708
  run.listing.workflow) _M="Liste des runs du workflow « %s »..." ;;
693
709
  # PR checks
694
710
  pr.checks.checking) _M="Vérification des checks de la PR #%s..." ;;
711
+ pr.checks.failed) _M="En échec : %s %s" ;;
712
+ pr.checks.pending) _M="En attente : %s" ;;
713
+ pr.checks.verdict) _M="PR #%s : %s réussi(s), %s en échec, %s en attente." ;;
714
+ pr.checks.green) _M="Tous les checks de la PR #%s sont verts." ;;
715
+ pr.checks.none) _M="Aucun check sur la PR #%s." ;;
695
716
  # Garde-fou des agents IA (repowarden garde, repowarden agents)
696
717
  garde.format) _M="Format inconnu « %s » : claude ou opencode." ;;
697
718
  garde.no_jq) _M="repowarden garde : jq absent, action non vérifiée (installer jq : https://jqlang.org)." ;;
package/hooks/lib/lang.sh CHANGED
@@ -54,6 +54,9 @@ done
54
54
  # Détection de code mort (fonctions <plugin>_deadcode des plugins ci-dessus)
55
55
  # shellcheck source=deadcode.sh
56
56
  source "$(dirname "${BASH_SOURCE[0]}")/deadcode.sh"
57
+ # Fonctions ajoutées sans commentaire (pre-commit et CI)
58
+ # shellcheck source=commentaires.sh
59
+ source "$(dirname "${BASH_SOURCE[0]}")/commentaires.sh"
57
60
  unset _plugin
58
61
 
59
62
  # --- Détection ---------------------------------------------------------------
@@ -112,6 +112,49 @@ ticket_type_r() {
112
112
  done <<<"$1"
113
113
  }
114
114
 
115
+ # Registre des branches d'un ticket : repère invisible dans les commentaires
116
+ # du bot. GitHub ne lie une branche au ticket qu'à sa création, et une branche
117
+ # liée supprimée y perd son nom : le registre garde le nom de chaque branche.
118
+ REPERE="repowarden:branche"
119
+
120
+ repere() { printf '<!-- %s %s -->' "$REPERE" "$1"; }
121
+
122
+ # REPLY = branches inscrites sur le ticket $1 (séparées par des espaces)
123
+ branches_inscrites_r() {
124
+ local corps
125
+ corps="$(gh issue view "$1" --json comments -q '.comments[].body' 2>/dev/null || true)"
126
+ REPLY="$(grep -oE "<!-- $REPERE [^ ]+ -->" <<<"$corps" | awk '{ printf "%s ", $3 }' || true)"
127
+ REPLY="${REPLY% }"
128
+ }
129
+
130
+ # REPLY = adresse de la branche $2 si elle n'est pas liée au ticket $1, vide si
131
+ # elle l'est ou si GitHub ne répond pas (dans le doute, on ne signale rien)
132
+ ticket_unlinked_branch_url_r() {
133
+ local lignes url
134
+ REPLY=""
135
+ # shellcheck disable=SC2016 # variables GraphQL, pas du shell
136
+ lignes="$(gh api graphql -F owner='{owner}' -F repo='{repo}' -F n="$1" -f query='query($owner: String!, $repo: String!, $n: Int!) {
137
+ repository(owner: $owner, name: $repo) { url issue(number: $n) { linkedBranches(first: 50) { nodes { ref { name } } } } }
138
+ }' -q '.data.repository.url, .data.repository.issue.linkedBranches.nodes[].ref.name' 2>/dev/null)" || return 0
139
+ url="${lignes%%$'\n'*}"
140
+ [ -n "$url" ] || return 0
141
+ grep -qxF "$2" <<<"$lignes" || REPLY="$url/tree/$2"
142
+ }
143
+
144
+ # Branche $2 du ticket $1 créée avant sa validation : GitHub ne lie pas une
145
+ # branche existante (createLinkedBranch répond null), son lien est donc inscrit
146
+ # sur le ticket, une seule fois. Vrai si le commentaire a été posté.
147
+ ticket_note_unlinked_branch() {
148
+ local n="$1" b="$2" url
149
+ ticket_unlinked_branch_url_r "$n" "$b"
150
+ url="$REPLY"
151
+ [ -n "$url" ] || return 1
152
+ branches_inscrites_r "$n"
153
+ [[ " $REPLY " != *" $b "* ]] || return 1
154
+ _tr tk.branch_unlinked "$b" "$url"
155
+ gh issue comment "$n" --body "$_T"$'\n\n'"$(repere "$b")" >/dev/null
156
+ }
157
+
115
158
  # Vrai si la branche $1 est une branche de travail : ni protégée, ni une
116
159
  # exception (main, develop, branches des bots…)
117
160
  work_branch() {
@@ -168,12 +211,19 @@ ticket_auto_create() {
168
211
  # Branche de travail sans ticket : avertissement, jamais bloquant. Création
169
212
  # proposée seulement si repowarden gère la vie des tickets du projet (et que
170
213
  # gh est là pour la faire) ; sinon, comment faire taire l'avertissement.
214
+ # Projet sans suivi géré : une fois par branche seulement (le répéter à chaque
215
+ # commit n'apprend rien), mémorisé dans la config locale de la branche, que git
216
+ # efface avec elle.
171
217
  ticket_hint() {
172
218
  local branch="$1"
173
219
  skipped tickets && return 0
174
220
  work_branch "$branch" || return 0
175
221
  branch_ticket_r "$branch"
176
222
  [ -z "$REPLY" ] || return 0
223
+ if ! tickets_managed; then
224
+ [ "$(git config --get "branch.$branch.repowardenTicketWarned" 2>/dev/null)" != true ] || return 0
225
+ git config "branch.$branch.repowardenTicketWarned" true 2>/dev/null || true
226
+ fi
177
227
  attention_t tk.cli.no_ticket "$branch"
178
228
  dim_t tk.cli.explain
179
229
  if tickets_managed && has gh; then
package/hooks/pre-commit CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Hook pre-commit : bloque les commits directs sur main, détecte les secrets,
3
- # puis formate les fichiers stagés selon le(s) langage(s) détecté(s).
3
+ # formate les fichiers stagés selon le(s) langage(s) détecté(s), puis signale
4
+ # les fonctions ajoutées sans commentaire.
4
5
  set -euo pipefail
5
6
 
6
7
  source "$(dirname "$0")/lib/common.sh"
@@ -74,4 +75,10 @@ if ! skipped format; then
74
75
  restore_unstaged
75
76
  fi
76
77
 
78
+ # 4. Fonctions ajoutées sans commentaire juste au-dessus (avertissement seulement,
79
+ # après le formatage : les lignes signalées sont celles du commit)
80
+ if ! skipped comments && git rev-parse -q --verify HEAD >/dev/null; then
81
+ comments_check HEAD ""
82
+ fi
83
+
77
84
  ok_t hook.pre_commit.7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowarden",
3
- "version": "4.11.0-next.13",
3
+ "version": "4.11.0-next.21",
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",