repowarden 4.10.2 → 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/bot CHANGED
@@ -115,7 +115,24 @@ TRAVAIL="$(mktemp -d)"
115
115
  trap 'rm -rf "$TRAVAIL"' EXIT
116
116
  # Identité des commits du bot (workflow : <slug>[bot]), crédits de gh
117
117
  NOM_BOT="${REPOWARDEN_BOT_NAME:-repowarden-bot}"
118
- COURRIEL_BOT="${REPOWARDEN_BOT_EMAIL:-repowarden-bot@users.noreply.github.com}"
118
+
119
+ # Adresse <id>+<slug>[bot]@users.noreply.github.com, comme bot.yml : seule
120
+ # forme que GitHub rattache à l'App. Slug tiré du nom, car un jeton
121
+ # d'installation ne peut pas lire /app ; repli signalé si l'id est introuvable.
122
+ courriel_bot_r() {
123
+ REPLY="${REPOWARDEN_BOT_EMAIL:-}"
124
+ [ -z "$REPLY" ] || return 0
125
+ local slug="${NOM_BOT%\[bot\]}" id
126
+ id="$(gh api "users/${slug}[bot]" --jq .id 2>/dev/null || true)"
127
+ case "$id" in
128
+ "" | *[!0-9]*)
129
+ REPLY="repowarden-bot@users.noreply.github.com"
130
+ attention_t bot.email_fallback "${slug}[bot]" "$REPLY" ;;
131
+ *) REPLY="${id}+${slug}[bot]@users.noreply.github.com" ;;
132
+ esac
133
+ }
134
+ courriel_bot_r
135
+ COURRIEL_BOT="$REPLY"
119
136
 
120
137
  # Dernière ligne d'erreur d'une sortie (✖ …), pour le rapport
121
138
  derniere_erreur_r() {
package/bin/bots CHANGED
@@ -431,18 +431,19 @@ YAML
431
431
  YAML
432
432
  fi
433
433
  fi
434
- # Tickets livrés : statut done et fermeture à la release, préprod à la
435
- # préversion ; seulement avec le suivi des tickets (sinon le job échoue)
434
+ # Tickets livrés : statut done et fermeture à la release (ou à une
435
+ # livraison sans nouvelle version), préprod à la préversion ; seulement avec le suivi des tickets (sinon le job échoue)
436
436
  if choisi tickets || [ -e .github/workflows/tickets.yml ]; then
437
437
  cat <<'YAML'
438
438
 
439
439
  tickets:
440
440
  needs: release
441
- if: needs.release.outputs.release_created == 'true' || needs.release.outputs.prerelease_created == 'true'
441
+ if: needs.release.outputs.release_created == 'true' || needs.release.outputs.prerelease_created == 'true' || needs.release.outputs.delivered_without_version != ''
442
442
  uses: simdevWarden/repowarden/.github/workflows/tickets.yml@v4
443
443
  permissions: { contents: write, issues: write, pull-requests: write, statuses: write }
444
444
  with:
445
445
  tag: ${{ needs.release.outputs.tag_name || needs.release.outputs.prerelease_tag }}
446
+ delivered-sha: ${{ needs.release.outputs.delivered_without_version }}
446
447
  YAML
447
448
  if [ -n "$AVEC_APP" ]; then
448
449
  cat <<'YAML'
package/bin/garde CHANGED
@@ -100,6 +100,16 @@ verifier_commit() {
100
100
  fi
101
101
  }
102
102
 
103
+ # Vrai si les mots $@ demandent une lecture seule (aide, aperçu) : rien n'est
104
+ # commité, rien à refuser
105
+ lecture_seule() {
106
+ local mot
107
+ for mot in "$@"; do
108
+ case "$mot" in -h | --help | --dry-run) return 0 ;; esac
109
+ done
110
+ return 1
111
+ }
112
+
103
113
  # repowarden cc / commit (ou git cc), lancé depuis le dossier $dir
104
114
  verifier_cc() {
105
115
  dossier_r "$dir"
@@ -169,6 +179,7 @@ verifier_git() {
169
179
  done
170
180
  case "$sous" in
171
181
  commit)
182
+ lecture_seule ${args_git[@]+"${args_git[@]}"} && exit 0
172
183
  amend="" noedit=""
173
184
  for mot in ${args_git[@]+"${args_git[@]}"}; do
174
185
  case "$mot" in
@@ -183,7 +194,7 @@ verifier_git() {
183
194
  refuser garde.commit "$ligne"
184
195
  ;;
185
196
  cc)
186
- verifier_commit "$ligne"
197
+ lecture_seule ${args_git[@]+"${args_git[@]}"} || verifier_commit "$ligne"
187
198
  ;;
188
199
  add)
189
200
  for mot in ${args_git[@]+"${args_git[@]}"}; do
@@ -227,7 +238,7 @@ verifier_git() {
227
238
  # Ligne de commande shell : chaque commande (&&, ||, ;, |, retour à la ligne),
228
239
  # « cd » suivi, variables d'environnement en tête ignorées
229
240
  verifier_shell() {
230
- local commande="$1" segment mots m nl=$'\n' lignes
241
+ local commande="$1" segment mots m cmd_cc nl=$'\n' lignes
231
242
  dir="$cwd"
232
243
  # Une commande par ligne. Découpage hors de <( … ) : bash 3.2 (macOS) y
233
244
  # lit mal ces motifs (« bad substitution ») et ne vérifierait plus rien
@@ -264,7 +275,13 @@ verifier_shell() {
264
275
  verifier_git "${@//[\"\']/}"
265
276
  ;;
266
277
  repowarden | */repowarden)
267
- case "${2//[\"\']/}" in cc | commit) verifier_cc "repowarden ${2//[\"\']/}" ;; esac
278
+ case "${2//[\"\']/}" in
279
+ cc | commit)
280
+ cmd_cc="repowarden ${2//[\"\']/}"
281
+ shift 2
282
+ lecture_seule "${@//[\"\']/}" || verifier_cc "$cmd_cc"
283
+ ;;
284
+ esac
268
285
  ;;
269
286
  esac
270
287
  done <<<"$lignes"
package/bin/migrer CHANGED
@@ -29,24 +29,10 @@ done
29
29
  [ -n "$GIT_TOPLEVEL" ] || { err_t install.not_repo; exit 1; }
30
30
  cd "$GIT_TOPLEVEL"
31
31
 
32
- # Version majeure dont le tag flottant (vN) est publié : celle de repowarden ;
33
- # préversion d'une nouvelle majeure (X.0.0-next) : X-1, vX n'existant pas encore
34
- majeure_r() {
35
- local v
36
- repowarden_version_r "$ROOT"
37
- v="$REPLY"
38
- # 0.0.0-dev : version inconnue (clone sans tags), rien à déduire
39
- if [ "$v" = 0.0.0-dev ]; then REPLY=""; return; fi
40
- REPLY="${v%%.*}"
41
- REPLY="${REPLY%%-*}"
42
- [[ "$v" != *.0.0-* ]] || REPLY=$((REPLY - 1))
43
- }
44
- if [ -n "${REPOWARDEN_MAJEURE:-}" ]; then
45
- MAJEURE="$REPOWARDEN_MAJEURE"
46
- else
47
- majeure_r
48
- MAJEURE="$REPLY"
49
- fi
32
+ # Version majeure dont le tag flottant (vN) est publié
33
+ repowarden_version_r "$ROOT"
34
+ repowarden_majeure_r "$REPLY"
35
+ MAJEURE="$REPLY"
50
36
  [ -n "$MAJEURE" ] || { err_t mig.unknown_version "$ROOT"; exit 1; }
51
37
  [[ "$MAJEURE" =~ ^[1-9][0-9]*$ ]] || { err_t mig.no_major "$MAJEURE"; exit 1; }
52
38
  BRANCHE="chore/migration-repowarden-v$MAJEURE"
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/statut CHANGED
@@ -9,6 +9,8 @@ set -euo pipefail
9
9
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
10
10
  # shellcheck source=../hooks/lib/common.sh
11
11
  source "$ROOT/hooks/lib/common.sh"
12
+ # shellcheck source=../hooks/lib/secrets.sh
13
+ source "$ROOT/hooks/lib/secrets.sh"
12
14
  exec 2>&1 # une seule sortie, dans l'ordre (avertissements compris)
13
15
  # shellcheck disable=SC2034 # lu par check_version (common.sh)
14
16
  HOOKS_DIR="$ROOT/hooks"
@@ -78,6 +80,42 @@ if [ -n "$GIT_TOPLEVEL" ]; then
78
80
  attention_t st.repo_migrate
79
81
  suivant="${suivant:-repowarden migrer}"
80
82
  fi
83
+ # Workflows qui appellent repowarden : dépôt officiel et tag vN de la
84
+ # majeure installée. Autre dépôt (ancien propriétaire, fork), branche
85
+ # (cassé quand elle disparaît) ou majeure dépassée : la ligne à corriger
86
+ repowarden_majeure_r "$version"
87
+ majeure="$REPLY"
88
+ re_uses='^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*["'"'"']?([A-Za-z0-9_.-]+)/repowarden(/[^@[:space:]"'"'"']*)?@([^[:space:]"'"'"'#]+)'
89
+ re_tag='^v([0-9]+)(\.[0-9]+)*(-[0-9A-Za-z.]+)?$'
90
+ re_sha='^[0-9a-f]{40}$'
91
+ for wf in "$GIT_TOPLEVEL"/.github/workflows/*.yml "$GIT_TOPLEVEL"/.github/workflows/*.yaml; do
92
+ [ -f "$wf" ] || continue
93
+ n=0
94
+ while IFS= read -r ligne || [ -n "$ligne" ]; do
95
+ n=$((n + 1))
96
+ [[ "$ligne" =~ $re_uses ]] || continue
97
+ proprio="${BASH_REMATCH[2]}" chemin="${BASH_REMATCH[3]}" ref="${BASH_REMATCH[4]}"
98
+ attendu="simdevWarden/repowarden$chemin@v${majeure:-N}"
99
+ shopt -s nocasematch
100
+ officiel=""
101
+ [[ "$proprio" != simdevwarden ]] || officiel=1
102
+ shopt -u nocasematch
103
+ cle=""
104
+ if [ -z "$officiel" ]; then
105
+ cle=st.wf_owner arg="$proprio/repowarden"
106
+ elif [[ "$ref" =~ $re_tag ]]; then
107
+ if [ -n "$majeure" ] && [ "${BASH_REMATCH[1]}" -lt "$majeure" ]; then
108
+ cle=st.wf_old arg="$ref"
109
+ fi
110
+ elif ! [[ "$ref" =~ $re_sha ]]; then
111
+ cle=st.wf_branch arg="$ref"
112
+ fi
113
+ [ -n "$cle" ] || continue
114
+ attention_t "$cle" "${wf#"$GIT_TOPLEVEL"/}" "$n" "$arg" "$attendu"
115
+ t st.wf_fix
116
+ suivant="${suivant:-$REPLY}"
117
+ done <"$wf"
118
+ done
81
119
  # Branche develop présente, flux non déclaré : traité comme « main seule »
82
120
  cfg_r integrationBranch ""
83
121
  if [ -z "$REPLY" ] && git rev-parse -q --verify refs/remotes/origin/develop >/dev/null; then
@@ -93,6 +131,11 @@ if [ -n "$GIT_TOPLEVEL" ]; then
93
131
  ok_t st.repo_version_ok "$attendue"
94
132
  fi
95
133
  fi
134
+ # Fichier sensible ni suivi ni ignoré (même avertissement qu'au pre-commit)
135
+ if ! skipped secrets && ! skipped sensitive-files; then
136
+ sensitive_files_r
137
+ [ -z "$REPLY" ] || sensitive_files_warn "$REPLY"
138
+ fi
96
139
  info_t st.repo_forge "$FORGE"
97
140
  [ "$FORGE" != github ] || dim_t st.repo_rules
98
141
  fi
@@ -103,6 +146,11 @@ for outil in gitleaks ruff vulture staticcheck pmd; do
103
146
  if has "$outil"; then presents="$presents $outil"; else absents="$absents $outil"; fi
104
147
  done
105
148
  info_t st.tools "${presents# }" "${absents# }"
149
+ # Sans gitleaks, les secrets ne sont vérifiés que par la CI
150
+ if ! has gitleaks && ! skipped secrets; then
151
+ gitleaks_install_r
152
+ suivant="${suivant:-$REPLY}"
153
+ fi
106
154
 
107
155
  echo
108
156
  if [ -n "$suivant" ]; then
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,12 +104,18 @@ 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
103
111
  shift
104
112
  done
105
113
  [ -n "$titre" ] || { err_t tk.cli.title_required; exit 1; }
114
+ # Étiquette « type: … » inexistante : gh refuserait de créer le ticket
115
+ if [ -n "$type" ] && tickets_managed && [[ " $TYPES_TICKET " != *" $type "* ]]; then
116
+ err_t tk.cli.bad_type "$type" "${TYPES_TICKET// /, }"
117
+ exit 1
118
+ fi
106
119
  gh_ready || exit 1
107
120
  if tickets_managed; then
108
121
  etiquettes+=(--label "$S_AVALIDER")
@@ -146,6 +159,8 @@ case "${1:-}" in
146
159
  ;;
147
160
  *)
148
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
149
164
  cmd_ticket "${1#\#}"
150
165
  else
151
166
  err_t install.unknown_arg "$1"
package/bin/tickets CHANGED
@@ -3,7 +3,7 @@
3
3
  # repowarden tickets init [--repo propriétaire/nom] [--sans-validation-auto]
4
4
  # repowarden tickets sync branches locales des tickets qui me sont assignés
5
5
  # (automatique si réglage ticketSync = auto ; défaut manual)
6
- # - étiquettes : « validé », les statuts (à valider, backlog, en cours,
6
+ # - étiquettes : « validé », « à décider » (suspend la validation d'office), les statuts (à valider, backlog, en cours,
7
7
  # en relecture, préprod, done) et les types (type: feat, type: fix…), qui
8
8
  # donnent le préfixe de la branche du ticket ;
9
9
  # - modèle de ticket .github/ISSUE_TEMPLATE/ticket.yml (créé « à valider ») ;
@@ -60,6 +60,7 @@ while IFS='|' read -r nom couleur description; do
60
60
  done <<'ETIQUETTES'
61
61
  validé|0e8a16|Ticket accepté par un mainteneur (débloque ses PR)
62
62
  statut: à valider|fbca04|Ticket créé, en attente de validation
63
+ à décider|d4c5f9|Proposé : pas de validation d'office, un mainteneur décide
63
64
  statut: backlog|c5def5|Validé, pas encore commencé
64
65
  statut: en cours|1d76db|Travail commencé (PR en brouillon)
65
66
  statut: en relecture|5319e7|PR prête, en relecture
@@ -6,7 +6,7 @@
6
6
 
7
7
  # Mise en route sur un dépôt existant (tickets init, relançable) : tickets
8
8
  # ouverts sans statut rangés (validé ou ouvert par un mainteneur → backlog,
9
- # sinon à valider) ; branches des PR ouvertes et mergées inscrites sur les
9
+ # sinon, ou « à décider », à valider) ; branches des PR ouvertes et mergées inscrites sur les
10
10
  # tickets qu'elles citent. Aucune branche créée en masse : elle naît quand le
11
11
  # ticket est pris (repowarden ticket N).
12
12
  cmd_adopter() {
@@ -17,7 +17,8 @@ cmd_adopter() {
17
17
  ! grep -q '^statut: ' <<<"$labels" || continue
18
18
  if grep -qxF "$VALIDE" <<<"$labels"; then
19
19
  statut "$n" "$S_BACKLOG"
20
- elif [ "${AUTO_VALIDATE:-maintainers}" = maintainers ] && est_mainteneur "$assoc"; then
20
+ elif [ "${AUTO_VALIDATE:-maintainers}" = maintainers ] && est_mainteneur "$assoc" \
21
+ && ! grep -qxF "$A_DECIDER" <<<"$labels"; then
21
22
  gh issue edit "$n" --add-label "$VALIDE" >/dev/null
22
23
  statut "$n" "$S_BACKLOG"
23
24
  else
@@ -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
 
@@ -7,17 +7,24 @@
7
7
  # Ticket $1 validé : backlog s'il attendait la validation, sa branche, et
8
8
  # vérification relancée sur les PR ouvertes qui l'attendaient
9
9
  accepter() {
10
- local n="$1" pr head sha body base brouillon
11
- if grep -qxF "$S_AVALIDER" <<<"$(etiquettes "$n")"; then
10
+ local n="$1" pr head sha body base brouillon labels
11
+ labels="$(etiquettes "$n")"
12
+ if grep -qxF "$S_AVALIDER" <<<"$labels"; then
12
13
  statut "$n" "$S_BACKLOG"
13
14
  fi
15
+ # Décision prise : « à décider » n'a plus lieu d'être
16
+ if grep -qxF "$A_DECIDER" <<<"$labels"; then
17
+ gh issue edit "$n" --remove-label "$A_DECIDER" >/dev/null
18
+ fi
14
19
  # La branche naît quand le ticket est pris (assigné) : un ticket validé
15
- # pour plus tard n'a pas de branche qui vieillit en attendant
16
- 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
17
24
  creer_branche "$n"
18
25
  else
19
26
  _tr tk.validated_waiting "$n"
20
- gh issue comment "$n" --body "$_T" >/dev/null
27
+ commenter "$n" "$_T"
21
28
  fi
22
29
  while IFS=$'\t' read -r pr head sha base brouillon; do
23
30
  [ -n "$pr" ] || continue
@@ -36,7 +43,8 @@ accepter() {
36
43
  # GitHub ne garde qu'un run en attente par groupe de concurrence, le run
37
44
  # « opened » peut être annulé par ceux des étiquettes posées à la création.
38
45
  # Non si déjà validé, ou (hors ouverture) rangé au-delà de « à valider »
39
- # (« validé » retiré à la main : décision respectée). Test seul : appelé dans un « if », où set -e
46
+ # (« validé » retiré à la main : décision respectée), ou proposé à décider
47
+ # (étiquette « à décider » : le mainteneur pose « validé »). Test seul : appelé dans un « if », où set -e
40
48
  # ne s'applique pas.
41
49
  a_valider_d_office() {
42
50
  local labels l
@@ -45,6 +53,7 @@ a_valider_d_office() {
45
53
  [ "${ISSUE_STATE:-open}" = open ] || return 1
46
54
  labels="$(etiquettes "$ISSUE")"
47
55
  ! grep -qxF "$VALIDE" <<<"$labels" || return 1
56
+ ! grep -qxF "$A_DECIDER" <<<"$labels" || return 1
48
57
  [ "$ACTION" != opened ] || return 0
49
58
  while IFS= read -r l; do
50
59
  [[ "$l" == "statut: "* && "$l" != "$S_AVALIDER" ]] && return 1
@@ -110,7 +119,7 @@ cmd_issue() {
110
119
  gh issue reopen "$ISSUE" >/dev/null
111
120
  statut "$ISSUE" "$S_PREPROD"
112
121
  _tr tk.reopened "$fpr" "$fbase"
113
- gh issue comment "$ISSUE" --body "$_T" >/dev/null
122
+ commenter "$ISSUE" "$_T"
114
123
  notice "$_T"
115
124
  return 0
116
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