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 +36 -4
- package/bin/run-list +9 -8
- package/bin/ticket +13 -3
- package/ci/tickets/commun.sh +27 -2
- package/ci/tickets/issue.sh +6 -4
- package/ci/tickets/pr.sh +1 -1
- package/ci/tickets/push.sh +117 -4
- package/ci/tickets/registre.sh +12 -20
- package/ci/tickets/release.sh +3 -2
- package/ci/tickets.sh +2 -2
- package/hooks/lib/commentaires.sh +123 -0
- package/hooks/lib/i18n/en.sh +22 -1
- package/hooks/lib/i18n/fr.sh +22 -1
- package/hooks/lib/lang.sh +3 -0
- package/hooks/lib/tickets.sh +50 -0
- package/hooks/pre-commit +8 -1
- package/package.json +1 -1
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
local existante="$REPLY"
|
|
68
|
+
switch_to "$existante"
|
|
66
69
|
prendre "$n"
|
|
67
|
-
ok_t tk.cli.switched "$
|
|
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"
|
package/ci/tickets/commun.sh
CHANGED
|
@@ -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" =~
|
|
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
|
-
|
|
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
|
|
package/ci/tickets/issue.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
commenter "$ISSUE" "$_T"
|
|
121
123
|
notice "$_T"
|
|
122
124
|
return 0
|
|
123
125
|
fi
|
package/ci/tickets/pr.sh
CHANGED
package/ci/tickets/push.sh
CHANGED
|
@@ -90,15 +90,128 @@ criteres_coches_r() {
|
|
|
90
90
|
REPLY="${sortie%$'\n'}"
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
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
|
package/ci/tickets/registre.sh
CHANGED
|
@@ -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
|
|
8
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
#
|
|
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
|
-
[ -
|
|
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
|
-
|
|
92
|
+
commenter "$n" "$_T"$'\n\n'"$(repere "$branche")"
|
|
101
93
|
notice_t tk.branch_notice "$branche" "$n"
|
|
102
94
|
}
|
package/ci/tickets/release.sh
CHANGED
|
@@ -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
|
-
|
|
24
|
+
commenter "$n" "$msg"
|
|
25
25
|
else
|
|
26
26
|
statut "$n" "$S_DONE"
|
|
27
|
-
|
|
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
|
|
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
|
+
}'
|
package/hooks/lib/i18n/en.sh
CHANGED
|
@@ -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
|
|
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)." ;;
|
package/hooks/lib/i18n/fr.sh
CHANGED
|
@@ -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
|
|
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 ---------------------------------------------------------------
|
package/hooks/lib/tickets.sh
CHANGED
|
@@ -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
|
-
#
|
|
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.
|
|
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",
|