repowarden 4.4.0 → 4.7.1

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/run-rerun ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # Relancer un workflow GitHub Actions échoué
3
+ # repowarden run rerun <run-id|--failed|--workflow>
4
+ set -euo pipefail
5
+
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
7
+ # shellcheck source=../hooks/lib/common.sh
8
+ source "$ROOT/hooks/lib/common.sh"
9
+ # shellcheck source=../hooks/lib/i18n.sh
10
+ source "$ROOT/hooks/lib/i18n.sh"
11
+
12
+ aide() {
13
+ cat <<EOF
14
+ Usage: repowarden run rerun <run-id|--failed|--workflow <nom>>
15
+
16
+ Relance un workflow GitHub Actions.
17
+
18
+ Options:
19
+ <run-id> ID du run à relancer
20
+ --failed Relance le dernier run échoué du repo
21
+ --workflow <nom> Relance le dernier run du workflow spécifié
22
+
23
+ Exemples:
24
+ repowarden run rerun 123456789
25
+ repowarden run rerun --failed
26
+ repowarden run rerun --workflow "CI"
27
+ EOF
28
+ }
29
+
30
+ [ $# -ge 1 ] || { aide >&2; exit 1; }
31
+ if [[ "$1" == "--help" || "$1" == "-h" ]]; then
32
+ aide
33
+ exit 0
34
+ fi
35
+ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
36
+
37
+ arg="${1}"
38
+ run_id=""
39
+
40
+ case "$arg" in
41
+ --failed)
42
+ step_t run.rerun.failed
43
+ run_id=$(gh run list --repo "${GH_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" --limit 1 --json databaseId,conclusion --jq '.[] | select(.conclusion=="failure") | .databaseId')
44
+ [ -n "$run_id" ] || { err_t run.rerun.none_failed; exit 1; }
45
+ ;;
46
+ --workflow)
47
+ shift
48
+ workflow="${1:?--workflow attend un nom}"
49
+ step_t run.rerun.workflow "$workflow"
50
+ run_id=$(gh run list --repo "${GH_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" --workflow "$workflow" --limit 1 --json databaseId --jq '.[0].databaseId')
51
+ [ -n "$run_id" ] || { err_t run.rerun.none_workflow "$workflow"; exit 1; }
52
+ ;;
53
+ *)
54
+ if [[ "$arg" =~ ^[0-9]+$ ]]; then
55
+ run_id="$arg"
56
+ else
57
+ err_t install.unknown_arg "$arg"
58
+ exit 1
59
+ fi
60
+ ;;
61
+ esac
62
+
63
+ step_t run.rerun.running "$run_id"
64
+ gh run rerun "$run_id"
65
+ ok_t run.rerun.done "$run_id"
package/bin/statut CHANGED
@@ -81,6 +81,7 @@ if [ -n "$GIT_TOPLEVEL" ]; then
81
81
  fi
82
82
  fi
83
83
  info_t st.repo_forge "$FORGE"
84
+ [ "$FORGE" != github ] || dim_t st.repo_rules
84
85
  fi
85
86
 
86
87
  # Outils : secrets, code mort (selon les langages)
package/bin/ticket CHANGED
@@ -5,6 +5,9 @@
5
5
  # repowarden ticket nouveau "<titre>" [--type fix] [--description "…"]
6
6
  # crée le ticket (« à valider » si le suivi
7
7
  # est géré par repowarden, sinon avec sa branche)
8
+ # repowarden ticket comment <n> "<texte>" ajoute un commentaire sur le ticket
9
+ # repowarden ticket label <n> <label> [--remove]
10
+ # gère les labels du ticket
8
11
  # repowarden ticket ticket de la branche courante
9
12
  # Demande un gh connecté (https://cli.github.com).
10
13
  set -euo pipefail
@@ -108,6 +111,8 @@ cmd_nouveau() {
108
111
  case "${1:-}" in
109
112
  -h | --help | help) aide ;;
110
113
  nouveau | new) shift; cmd_nouveau "$@" ;;
114
+ comment) shift; exec bash "$ROOT/bin/ticket-comment" "$@" ;;
115
+ label) shift; exec bash "$ROOT/bin/ticket-label" "$@" ;;
111
116
  "")
112
117
  branche="$(git symbolic-ref --short HEAD 2>/dev/null || echo "")"
113
118
  branch_ticket_r "$branche"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Ajouter un commentaire sur un ticket GitHub
3
+ # repowarden ticket comment <n> "<texte>"
4
+ set -euo pipefail
5
+
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
7
+ # shellcheck source=../hooks/lib/common.sh
8
+ source "$ROOT/hooks/lib/common.sh"
9
+ # shellcheck source=../hooks/lib/i18n.sh
10
+ source "$ROOT/hooks/lib/i18n.sh"
11
+
12
+ aide() {
13
+ cat <<EOF
14
+ Usage: repowarden ticket comment <n> "<texte>"
15
+
16
+ Ajoute un commentaire sur le ticket #<n>.
17
+
18
+ Exemples:
19
+ repowarden ticket comment 123 "Conception retenue : ..."
20
+ repowarden ticket comment 456 "PR prête pour review"
21
+ EOF
22
+ }
23
+
24
+ [ $# -ge 2 ] || { aide >&2; exit 1; }
25
+ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
26
+
27
+ n="${1}"
28
+ texte="${2}"
29
+
30
+ step_t tk.comment.adding "$n"
31
+ gh issue comment "$n" --body "$texte"
32
+ ok_t tk.comment.added "$n"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Gérer les labels d'un ticket GitHub
3
+ # repowarden ticket label <n> <label> [--remove]
4
+ set -euo pipefail
5
+
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
7
+ # shellcheck source=../hooks/lib/common.sh
8
+ source "$ROOT/hooks/lib/common.sh"
9
+ # shellcheck source=../hooks/lib/i18n.sh
10
+ source "$ROOT/hooks/lib/i18n.sh"
11
+
12
+ aide() {
13
+ cat <<EOF
14
+ Usage: repowarden ticket label <n> <label> [--remove]
15
+
16
+ Ajoute ou retire un label sur le ticket #<n>.
17
+
18
+ Options:
19
+ --remove Retire le label au lieu de l'ajouter
20
+
21
+ Exemples:
22
+ repowarden ticket label 123 "validé"
23
+ repowarden ticket label 123 "statut: backlog"
24
+ repowarden ticket label 123 "wip" --remove
25
+ EOF
26
+ }
27
+
28
+ [ $# -ge 2 ] || { aide >&2; exit 1; }
29
+ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
30
+
31
+ n="${1}"
32
+ label="${2}"
33
+ remove=false
34
+
35
+ shift 2
36
+ while [ $# -gt 0 ]; do
37
+ case "$1" in
38
+ --remove) remove=true ;;
39
+ *) err_t install.unknown_arg "$1"; exit 1 ;;
40
+ esac
41
+ shift
42
+ done
43
+
44
+ if [ "$remove" = true ]; then
45
+ step_t tk.label.removing "$label" "$n"
46
+ gh issue edit "$n" --remove-label "$label"
47
+ ok_t tk.label.removed "$label" "$n"
48
+ else
49
+ step_t tk.label.adding "$label" "$n"
50
+ gh issue edit "$n" --add-label "$label"
51
+ ok_t tk.label.added "$label" "$n"
52
+ fi
package/bin/tickets CHANGED
@@ -123,11 +123,14 @@ permissions: {}
123
123
 
124
124
  jobs:
125
125
  tickets:
126
- uses: SimBienvenueHoulBoumi/repowarden/.github/workflows/tickets.yml@v4
126
+ uses: simdevWarden/repowarden/.github/workflows/tickets.yml@v4
127
127
  permissions: { contents: write, issues: write, pull-requests: write, statuses: write }
128
128
  # GitHub App (repowarden app init) : ses actions déclenchent les autres workflows
129
+ # auto-create : true = crée un ticket « à valider » pour PR sans ticket,
130
+ # false = échec de la vérification avec la marche à suivre (défaut false)
129
131
  with:
130
132
  app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }}
133
+ auto-create: false
131
134
  secrets:
132
135
  app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
133
136
  YAML
@@ -136,6 +139,16 @@ YAML
136
139
  step_t tk.init_adopt
137
140
  GH_REPO="$REPO" AUTO_VALIDATE="${AUTO_VALIDATE:-maintainers}" "$ROOT/ci/tickets.sh" adopter | sed 's/^/ /'
138
141
 
142
+ # Afficher l'état de ticketAutoCreate
143
+ echo
144
+ if git config --get repowarden.ticketAutoCreate | grep -qE '^(true|oui|yes)$'; then
145
+ t tk.init_auto_create_on
146
+ hint "$_T"
147
+ else
148
+ t tk.init_auto_create_off
149
+ hint "$_T"
150
+ fi
151
+
139
152
  echo
140
153
  ok_t tk.init_done
141
154
  dim_t tk.init_next
@@ -52,10 +52,22 @@ cmd_issue() {
52
52
  accepter "$ISSUE"
53
53
  ;;
54
54
  assigned)
55
- # Pris en charge : sa branche (s'il est validé), en cours s'il
56
- # attendait dans le backlog
57
- est_valide "$ISSUE" && creer_branche "$ISSUE"
58
- grep -qxF "$S_BACKLOG" <<<"$(etiquettes "$ISSUE")" && statut "$ISSUE" "$S_ENCOURS"
55
+ # Pris en charge : sa branche (s'il est validé), en cours s'il est
56
+ # validé ET assigné (quelle que soit la présence du label backlog,
57
+ # pour gérer la race validation+assignation quasi simultanée).
58
+ # Ne passe en cours que s'il n'est pas déjà plus loin (relecture, préprod, done).
59
+ if est_valide "$ISSUE"; then
60
+ creer_branche "$ISSUE"
61
+ local labels
62
+ labels="$(etiquettes "$ISSUE")"
63
+ if grep -qxF "$S_AVALIDER" <<<"$labels" || grep -qxF "$S_BACKLOG" <<<"$labels"; then
64
+ # Pas de label plus avancé (relecture, préprod, done) ?
65
+ grep -qxF "$S_RELECTURE" <<<"$labels" && return 0
66
+ grep -qxF "$S_PREPROD" <<<"$labels" && return 0
67
+ grep -qxF "$S_DONE" <<<"$labels" && return 0
68
+ statut "$ISSUE" "$S_ENCOURS"
69
+ fi
70
+ fi
59
71
  return 0
60
72
  ;;
61
73
  closed)
@@ -71,7 +83,10 @@ cmd_issue() {
71
83
  _tr tk.reopened "$fpr" "$fbase"
72
84
  gh issue comment "$ISSUE" --body "$_T" >/dev/null
73
85
  notice "$_T"
86
+ return 0
74
87
  fi
88
+ # Terminé à la main ou en production : branche restante nettoyée
89
+ nettoyer_branche "$ISSUE"
75
90
  return 0
76
91
  fi
77
92
  # Abandonné : PR fermées, branche supprimée (un ticket terminé est
package/ci/tickets/pr.sh CHANGED
@@ -22,6 +22,14 @@ cmd_pr() {
22
22
  _tr tk.pr_backmerge_empty "$HEAD" "$BASE"
23
23
  gh pr close "$PR" --comment "$_T" >/dev/null
24
24
  notice "$_T"
25
+ # Livraison ouverte par le workflow de release : celle d'une personne
26
+ # n'aurait jamais d'approbation de déploiement, elle resterait bloquée
27
+ elif [ "$HEAD" = "${INTEGRATION:-develop}" ] && [ "${DELIVERY_WORKFLOW:-false}" = true ] &&
28
+ [[ "${AUTHOR:-}" != *"[bot]" ]]; then
29
+ _tr tk.pr_delivery_manual "$HEAD" "$BASE" "$HEAD" "$HEAD"
30
+ gh pr close "$PR" --comment "$_T" >/dev/null
31
+ notice "$_T"
32
+ return 0
25
33
  fi
26
34
  fi
27
35
  # Bots de dépendances, PR de release, livraisons et retours : pas de ticket
@@ -47,14 +55,22 @@ cmd_pr() {
47
55
  return 0
48
56
  fi
49
57
 
50
- # Aucun ticket : créé à partir de la PR, à valider, et relié à la PR
58
+ # Aucun ticket : si auto-create activé, créé à partir de la PR, à valider,
59
+ # et relié à la PR ; sinon, échec de la vérification avec la marche à suivre.
51
60
  if [ -z "$tickets" ]; then
52
- _tr tk.created_body "$PR"
53
- url="$(gh issue create --title "$(gh pr view "$PR" --json title -q .title)" \
54
- --body "$_T" --label "$S_AVALIDER")"
55
- tickets="${url##*/}"
56
- gh pr edit "$PR" --body "$(printf '%s\n\nTicket : #%s\n' "$body" "$tickets")" >/dev/null
57
- notice_t tk.created "$tickets" "$PR"
61
+ if [ "${AUTO_CREATE:-false}" = true ]; then
62
+ _tr tk.created_body "$PR"
63
+ url="$(gh issue create --title "$(gh pr view "$PR" --json title -q .title)" \
64
+ --body "$_T" --label "$S_AVALIDER")"
65
+ tickets="${url##*/}"
66
+ gh pr edit "$PR" --body "$(printf '%s\n\nTicket : #%s\n' "$body" "$tickets")" >/dev/null
67
+ notice_t tk.created "$tickets" "$PR"
68
+ else
69
+ _tr tk.created_body "$PR"
70
+ gh api "repos/$GH_REPO/statuses/$HEAD_SHA" -f state=failure -f context=ticket \
71
+ -f description="PR sans ticket : créez-en un avec 'repowarden ticket nouveau \"<titre>\"' ou citez 'Ticket : #N' dans la description" >/dev/null
72
+ return 0
73
+ fi
58
74
  fi
59
75
 
60
76
  # Statut du travail (seulement pour un ticket validé)
@@ -4,12 +4,91 @@
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
+ # REPLY = rang d'impact du sujet de commit $1 : 0 rupture, 1 feat, 2 fix/perf,
8
+ # 3 autre type conforme
9
+ rang_impact() {
10
+ case "$1" in
11
+ *"!: "*) REPLY=0 ;;
12
+ feat*:*) REPLY=1 ;;
13
+ fix*:* | perf*:*) REPLY=2 ;;
14
+ *) REPLY=3 ;;
15
+ esac
16
+ }
17
+
18
+ # REPLY = sujet du commit conforme de plus fort impact de la branche $1, vide
19
+ # si aucun commit conforme (repli du titre de PR quand le ticket n'en donne pas)
20
+ commit_impact_r() {
21
+ local s rang meilleur="" meilleur_rang=""
22
+ REPLY=""
23
+ while IFS= read -r s; do
24
+ [ -n "$s" ] || continue
25
+ [[ "$s" =~ $CC_PATTERN ]] || continue
26
+ rang_impact "$s"
27
+ rang="$REPLY"
28
+ if [ -z "$meilleur" ] || [ "$rang" -lt "$meilleur_rang" ]; then
29
+ meilleur="$s"
30
+ meilleur_rang="$rang"
31
+ fi
32
+ done < <(gh api "repos/$GH_REPO/compare/$INTEGRATION...$1" \
33
+ -q '.commits[].commit.message | split("\n")[0]' 2>/dev/null || true)
34
+ REPLY="$meilleur"
35
+ }
36
+
37
+ # REPLY = titre de la PR brouillon du ticket $1 (branche $2) : « type(portée):
38
+ # sujet » depuis le ticket, sinon le commit conforme de plus fort impact de la
39
+ # branche, sinon le titre déduit de la branche
40
+ titre_pr_brouillon_r() {
41
+ local n="$1" branche="$2" type
42
+ ticket_type_r "$(etiquettes "$n")"
43
+ type="$REPLY"
44
+ ticket_pr_title_r "$type" "$(gh issue view "$n" --json title -q .title)"
45
+ [ -z "$REPLY" ] || return 0
46
+ commit_impact_r "$branche"
47
+ [ -z "$REPLY" ] || return 0
48
+ suggest_pr_title_r "$branche"
49
+ }
50
+
51
+ # $1 = corps d'un ticket → REPLY : les puces de ses critères d'acceptation
52
+ # (« Terminé quand… », « Critères d'acceptation ») en cases à cocher « - [ ] »,
53
+ # le reste inchangé
54
+ criteres_coches_r() {
55
+ local corps="$1" ligne dedans=0 sortie=""
56
+ while IFS= read -r ligne; do
57
+ if [[ "$ligne" =~ ^#{1,6}[[:space:]] ]]; then
58
+ dedans=0
59
+ case "$ligne" in
60
+ *[Tt]ermin* | *[Cc]rit* | *[Aa]cceptance* | *[Dd]one*) dedans=1 ;;
61
+ esac
62
+ elif [ "$dedans" = 1 ]; then
63
+ # « - x » → « - [ ] x » ; une case déjà là n'est pas dupliquée
64
+ ligne="$(printf '%s' "$ligne" |
65
+ sed -E 's/^([[:space:]]*[-*][[:space:]]+)(\[[xX ]\][[:space:]]+)?/\1[ ] /')"
66
+ fi
67
+ sortie="$sortie$ligne"$'\n'
68
+ done <<<"$corps"
69
+ REPLY="${sortie%$'\n'}"
70
+ }
71
+
72
+ # REPLY = description de la PR brouillon du ticket $1 (branche $2) : le ticket
73
+ # (critères en cases à cocher), puis la liste des commits, puis « Ticket : #N »
74
+ description_pr_brouillon_r() {
75
+ local n="$1" branche="$2" corps commits
76
+ criteres_coches_r "$(gh issue view "$n" --json body -q .body)"
77
+ corps="$REPLY"
78
+ commits="$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$branche" \
79
+ -q '.commits[] | "- \(.sha[0:7]) \(.commit.message | split("\n")[0])"' 2>/dev/null || true)"
80
+ [ -z "$commits" ] || corps="$corps"$'\n\n## Commits\n'"$commits"
81
+ REPLY="$corps"$'\n\nTicket : #'"$n"
82
+ }
83
+
7
84
  # Premier push sur la branche d'un ticket : PR brouillon vers la branche
8
- # d'intégration, citant le ticket, assignée à l'auteur du push ; ticket en
9
- # cours. Ouverte par le jeton des Actions, elle ne lance pas la CI : elle
10
- # tourne au push suivant, ou au passage « prête pour revue ».
85
+ # d'intégration, au titre et à la description repris du ticket, citant le
86
+ # ticket, assignée à l'auteur du push ; ticket en cours. Ouverte par le jeton
87
+ # des Actions, elle ne lance pas la CI : elle tourne au push suivant, ou au
88
+ # passage « prête pour revue ». Tant qu'elle est en brouillon, chaque push met
89
+ # à jour son titre et sa description ; une PR prête n'est plus touchée.
11
90
  cmd_push() {
12
- local n pr titre type url
91
+ local n pr titre corps url
13
92
  : "${PUSH_BRANCH:?}" "${SHA:?}"
14
93
  [ "${DELETED:-false}" != true ] || return 0
15
94
  branch_ticket_r "$PUSH_BRANCH"
@@ -18,17 +97,20 @@ cmd_push() {
18
97
  # Branche faite hors du ticket (à la main) : inscrite sur le ticket
19
98
  inscrire_branche "$n" "$PUSH_BRANCH"
20
99
  pr="$(gh pr list --state open --head "$PUSH_BRANCH" --json number -q '.[0].number // empty')"
21
- [ -z "$pr" ] || return 0
100
+ # PR existante : rien à faire si elle n'est plus en brouillon
101
+ [ -z "$pr" ] || [ "$(gh pr view "$pr" --json isDraft -q .isDraft)" = true ] || return 0
22
102
  [ "$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$PUSH_BRANCH" -q .ahead_by)" != 0 ] || return 0
23
- titre="$(gh issue view "$n" --json title -q .title)"
24
- type="${PUSH_BRANCH%%/*}"
25
- case "$type" in feature) type=feat ;; bugfix | hotfix) type=fix ;; esac
26
- # Titre conventionnel (message du commit en squash) : type: titre du ticket
27
- titre="$type: $(tr '[:upper:]' '[:lower:]' <<<"${titre:0:1}")${titre:1}"
28
- authored_length "$titre"
29
- [ "$REPLY" -le 72 ] || titre="${titre:0:72}"
103
+ titre_pr_brouillon_r "$n" "$PUSH_BRANCH"
104
+ titre="$REPLY"
105
+ description_pr_brouillon_r "$n" "$PUSH_BRANCH"
106
+ corps="$REPLY"
107
+ if [ -n "$pr" ]; then
108
+ gh pr edit "$pr" --title "$titre" --body "$corps" >/dev/null
109
+ notice_t tk.draft_updated "$pr" "$n"
110
+ return 0
111
+ fi
30
112
  url="$(gh pr create --draft --base "$INTEGRATION" --head "$PUSH_BRANCH" --title "$titre" \
31
- --body "Ticket : #$n" ${PUSHER:+--assignee "$PUSHER"})"
113
+ --body "$corps" ${PUSHER:+--assignee "$PUSHER"})"
32
114
  [ -z "${PUSHER:-}" ] || gh issue edit "$n" --add-assignee "$PUSHER" >/dev/null || true
33
115
  est_valide "$n" && statut "$n" "$S_ENCOURS"
34
116
  notice_t tk.draft_opened "${url##*/}" "$n"
@@ -50,6 +50,25 @@ branche_du_ticket_r() {
50
50
  done < <(gh api "repos/$GH_REPO/git/matching-refs/heads/" --paginate -q '.[].ref' 2>/dev/null || true)
51
51
  }
52
52
 
53
+ # Ticket $1 terminé : sa branche, si elle existe encore (travail passé par une
54
+ # autre branche), est supprimée quand elle n'apporte rien à la branche
55
+ # d'intégration et qu'aucune PR ouverte n'en part ; sinon gardée et signalée
56
+ nettoyer_branche() {
57
+ local n="$1" branche avance prs
58
+ branche_du_ticket_r "$n"
59
+ branche="$REPLY"
60
+ [ -n "$branche" ] || return 0
61
+ prs="$(gh pr list --state open --head "$branche" --json number -q 'map("#\(.number)") | join(" ")')"
62
+ avance="$(gh api "repos/$GH_REPO/compare/${INTEGRATION:-develop}...$branche" -q .ahead_by 2>/dev/null || true)"
63
+ if [ -z "$prs" ] && [ "$avance" = 0 ]; then
64
+ gh api -X DELETE "repos/$GH_REPO/git/refs/heads/$branche" >/dev/null
65
+ notice_t tk.branch_deleted "$branche" "$n"
66
+ else
67
+ _tr tk.branch_kept "$branche" "${avance:-?}" "${prs:--}"
68
+ gh issue comment "$n" --body "$_T" >/dev/null
69
+ fi
70
+ }
71
+
53
72
  # Ticket $1 validé : sa branche naît de la branche d'intégration, liée au
54
73
  # ticket (panneau « Development »). Rien si elle existe déjà, ou si une PR
55
74
  # ouverte cite déjà le ticket (travail commencé hors de sa branche).
@@ -15,6 +15,7 @@ cmd_release() {
15
15
  _tr tk.released "$TAG"
16
16
  statut "$n" "$S_DONE"
17
17
  gh issue close "$n" --comment "$_T" >/dev/null
18
+ nettoyer_branche "$n"
18
19
  fi
19
20
  done < <(gh issue list --state open --label "$S_PREPROD" --limit 500 --json number -q '.[].number')
20
21
  }
package/hooks/commit-msg CHANGED
@@ -39,6 +39,30 @@ if [[ "$SUBJECT" =~ ^(Merge|Revert|fixup!|squash!) ]]; then
39
39
  exit 0
40
40
  fi
41
41
 
42
+ # En-tête tenté mais refusé : type inconnu (ou en majuscules), description vide
43
+ if ! [[ "$SUBJECT" =~ $CC_PATTERN ]] &&
44
+ [[ "$SUBJECT" =~ ^([A-Za-z]+)(\([^\)]*\))?(!?):[[:space:]]*(.*)$ ]]; then
45
+ typ="${BASH_REMATCH[1]}" portee="${BASH_REMATCH[2]}" bang="${BASH_REMATCH[3]}" desc="${BASH_REMATCH[4]}"
46
+ typ_min="$(printf '%s' "$typ" | tr '[:upper:]' '[:lower:]')"
47
+ propose="$typ_min"
48
+ [[ "$typ_min" =~ ^($CC_TYPES)$ ]] || { type_proche_r "$typ_min"; propose="$REPLY"; }
49
+ if [ -z "$desc" ]; then
50
+ err_t hook.commit_msg.desc_empty "${propose:-$typ}$portee$bang:"
51
+ echo_t hook.commit_msg.kept >&2
52
+ exit 1
53
+ fi
54
+ if [ "$propose" != "$typ" ]; then
55
+ if [ -n "$propose" ]; then
56
+ err_t hook.commit_msg.type_unknown "$typ" "$propose$portee$bang: $desc"
57
+ else
58
+ err_t hook.commit_msg.type_none "$typ"
59
+ types_help
60
+ fi
61
+ echo_t hook.commit_msg.kept >&2
62
+ exit 1
63
+ fi
64
+ fi
65
+
42
66
  if ! [[ "$SUBJECT" =~ $CC_PATTERN ]]; then
43
67
  if [ "${REPOWARDEN_LANG:-}" = en ]; then
44
68
  cat >&2 <<EOF
@@ -80,6 +104,15 @@ EOF
80
104
  exit 1
81
105
  fi
82
106
 
107
+ # Point final retiré, comme dans git cc (pas des points de suspension) ; seule
108
+ # la première ligne est réécrite, le corps reste intact
109
+ if [[ "$SUBJECT" == *. && "$SUBJECT" != *.. ]]; then
110
+ SUBJECT="${SUBJECT%.}"
111
+ contenu="$(awk -v s="$SUBJECT" '!fait && !/^#/ { print s; fait = 1; next } { print }' "$MSG_FILE")"
112
+ printf '%s\n' "$contenu" >"$MSG_FILE"
113
+ info_t hook.commit_msg.dot >&2
114
+ fi
115
+
83
116
  authored_length "$SUBJECT"
84
117
  if [ "$REPLY" -gt 72 ]; then
85
118
  err_t hook.commit_msg.1 "$REPLY" >&2
@@ -84,6 +84,23 @@ cfg() {
84
84
  CC_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
85
85
  CC_PATTERN="^($CC_TYPES)(\([a-z0-9._-]+\))?!?: .+"
86
86
 
87
+ # Type le plus proche d'un type inconnu : alias courants, puis préfixe commun
88
+ # (doc → docs, feature → feat, Feat → feat) ; vide si aucun
89
+ type_proche_r() {
90
+ local t="$1" c
91
+ case "$t" in
92
+ feature | features) REPLY="feat"; return ;;
93
+ doc | documentation) REPLY="docs"; return ;;
94
+ fixe | fixed | fixes | bugfix | hotfix) REPLY="fix"; return ;;
95
+ tests) REPLY="test"; return ;;
96
+ refacto | refactoring) REPLY="refactor"; return ;;
97
+ esac
98
+ REPLY=""
99
+ for c in ${CC_TYPES//|/ }; do
100
+ if [[ "$c" == "$t"* || "$t" == "$c"* ]]; then REPLY="$c"; return; fi
101
+ done
102
+ }
103
+
87
104
  # REPLY = longueur de la première ligne écrite par l'auteur : le suffixe
88
105
  # « (#123) » ajouté par GitHub lors d'un merge squash n'est pas compté.
89
106
  authored_length() {