repowarden 4.4.0 → 4.7.0

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.
@@ -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
  }
@@ -118,6 +118,8 @@ _msg_en() {
118
118
  hook.pre_push.5) _M="%s: no changed file, nothing to test." ;;
119
119
  hook.pre_push.6) _M="Nothing to test (branch deletion only)." ;;
120
120
  hook.pre_push.7) _M="Pre-push OK" ;;
121
+ hook.pre_push.lint.missing) _M="%s not installed, %s not checked (install: %s)." ;;
122
+ hook.pre_push.lint.run) _M="%s: %s" ;;
121
123
  hook.branch_hint.1) _M="Branch name \"%s\" does not follow the convention: commits and pushes will be refused. Rename:" ;;
122
124
  hook.branch_hint.2) _M=" Format <type>/<topic> (e.g. feat/signup); full help on the next commit." ;;
123
125
  hook.post_merge.1) _M="🧹 Local branch deleted (merged and deleted on the server): %s" ;;
@@ -262,6 +264,7 @@ _msg_en() {
262
264
  ci.proteger.5) _M=" v* tags: %s" ;;
263
265
  ci.proteger.6) _M=" default branch: %s" ;;
264
266
  ci.proteger.7) _M=" required approvals: %s; CODEOWNERS review: %s; approval of unattributed changes: %s" ;;
267
+ ci.proteger.solo) _M=" solo repository (a single person with write access): no approval by another person required" ;;
265
268
  ci.proteger.8) _M=" (admins keep an explicit bypass, recorded in the PR history)" ;;
266
269
  ci.proteger.9) _M=" \"%s\" environment: approval by %s; protected branches and v* tags" ;;
267
270
  ci.proteger.10) _M="Ruleset \"%s\" updated." ;;
@@ -276,6 +279,13 @@ _msg_en() {
276
279
  ci.proteger.unattributed_invalid) _M="unattributedApproval: true or false." ;;
277
280
  ci.proteger.no_branch) _M="None of the branches \"%s\" exists on %s." ;;
278
281
  ci.proteger.no_cleanup) _M="Automatic branch deletion disabled (it would delete develop): add a workflow calling nettoyage-branches.yml, otherwise merged branches remain (see In an organisation)." ;;
282
+ ci.proteger.no_delivery_env) _M="main will require a successful \"%s\" deployment, but no workflow creates that deployment: a deployment nothing creates would block main forever. Call release-auto.yml with delivery-environment (see docs/releases.md, develop flow), or set deliveryApproval = review in .repowarden.conf." ;;
283
+ ci.proteger.stale_concurrency) _M="%s: a pending delivery request (manual or scheduled) would silently block the next ones. Set cancel-in-progress: \${{ github.event_name != 'push' }} (a new request replaces the pending one) and one group per trigger: release-\${{ github.ref_name }}-\${{ github.event.schedule || github.event_name }}." ;;
284
+ ci.proteger.urgent) _M=" \"urgent\" label created (its merge triggers the delivery request)" ;;
285
+ ci.proteger.verify_missing) _M="%s: ruleset missing online" ;;
286
+ ci.proteger.verify_diff) _M="%s: online rules differ\n online: %s\n expected: %s" ;;
287
+ ci.proteger.verify_fix) _M="Outdated rules: run repowarden proteger again to update them." ;;
288
+ ci.proteger.verify_ok) _M="Online rules up to date." ;;
279
289
  ci.proteger.delivery) _M=" delivery into main: %s approval(s) required (moving to a new version); auto-merge of work PRs into develop: %s" ;;
280
290
  ci.proteger.methods_flow) _M="main: merge commit; %s: squash or merge commit" ;;
281
291
  ci.proteger.tags_org) _M="reserved for workflows" ;;
@@ -363,6 +373,7 @@ _msg_en() {
363
373
  st.repo_version_old) _M="The project requires repowarden %s; installed: %s" ;;
364
374
  st.repo_version_ok) _M="Version required by the project (%s): met" ;;
365
375
  st.repo_forge) _M="Platform: %s" ;;
376
+ st.repo_rules) _M="GitHub rules up to date? repowarden proteger --verifier (read-only)" ;;
366
377
  st.tools) _M="Tools present: %s · missing: %s (depending on the projects' languages)" ;;
367
378
  st.next) _M="Next step:" ;;
368
379
  st.ready) _M="All set: git cc to commit, repowarden code-mort on a branch." ;;
@@ -403,13 +414,16 @@ _msg_en() {
403
414
  tk.validated_waiting) _M="Ticket validated, waiting to be taken: its branch will be created when it is assigned, or by taking it yourself with \`repowarden ticket %s\`." ;;
404
415
  tk.pr_inverted) _M="PR closed automatically: a PR from \`%s\` to \`%s\` is pointless. A persistent branch only goes out for a delivery (develop → main) or a back-merge (main → develop). To update a work branch: on that branch, \`git merge origin/%s\`, then \`git push\`." ;;
405
416
  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." ;;
417
+ 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." ;;
406
418
  tk.branch_registered) _M="Ticket branch: \`%s\`." ;;
407
419
  tk.branch_second) _M="Second branch for this ticket: \`%s\` (already: %s). One ticket, one branch: group the work, or open a separate ticket." ;;
408
420
  tk.adopted) _M="Existing tickets: %s sorted, %s branch(es) registered." ;;
409
421
  tk.init_adopt) _M="Existing tickets and branches" ;;
410
422
  tk.abandoned) _M="Ticket #%s abandoned: PR closed, branch deleted." ;;
411
423
  tk.branch_deleted) _M="Branch %s deleted (ticket #%s abandoned)" ;;
424
+ 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." ;;
412
425
  tk.draft_opened) _M="Draft PR #%s opened for ticket #%s" ;;
426
+ tk.draft_updated) _M="PR #%s (ticket #%s): title and description taken from the ticket" ;;
413
427
  tk.created_body) _M="Ticket created automatically for PR #%s, which cited none. To be validated by a maintainer (\"validé\" label)." ;;
414
428
  tk.created) _M="Ticket #%s created for PR #%s and linked in its description: to be validated by a maintainer." ;;
415
429
  tk.init_labels) _M="Labels (statuses and validation)" ;;
@@ -417,9 +431,36 @@ _msg_en() {
417
431
  tk.init_exists) _M="%s already exists: left as is." ;;
418
432
  tk.init_written) _M="%s created." ;;
419
433
  tk.init_done) _M="Ticket tracking ready." ;;
434
+ tk.init_auto_create_on) _M="Auto ticket creation: ENABLED (ticketAutoCreate = true)" ;;
435
+ tk.init_auto_create_off) _M="Auto ticket creation: DISABLED (default). To enable: git config repowarden.ticketAutoCreate true" ;;
420
436
  tk.init_next) _M="Next: commit these files; require the \"ticket\" check (repowarden proteger --checks \"…,ticket\"); in release.yml, wire the tickets job (see the Tickets docs)." ;;
421
437
  tk.prerelease) _M="Available in pre-release **%s**: to be tested before production." ;;
422
438
  tk.released) _M="Released in **%s**." ;;
439
+ # Ticket comment
440
+ tk.comment.adding) _M="Adding comment to ticket #%s..." ;;
441
+ tk.comment.added) _M="Comment added to ticket #%s." ;;
442
+ # Ticket label
443
+ tk.label.adding) _M="Adding label \"%s\" to ticket #%s..." ;;
444
+ tk.label.added) _M="Label \"%s\" added to ticket #%s." ;;
445
+ tk.label.removing) _M="Removing label \"%s\" from ticket #%s..." ;;
446
+ tk.label.removed) _M="Label \"%s\" removed from ticket #%s." ;;
447
+ # PR label
448
+ pr.label.adding) _M="Adding label \"%s\" to PR #%s..." ;;
449
+ pr.label.added) _M="Label \"%s\" added to PR #%s." ;;
450
+ pr.label.removing) _M="Removing label \"%s\" from PR #%s..." ;;
451
+ pr.label.removed) _M="Label \"%s\" removed from PR #%s." ;;
452
+ # Run rerun
453
+ run.rerun.failed) _M="Rerunning last failed run..." ;;
454
+ run.rerun.workflow) _M="Rerunning last run of workflow \"%s\"..." ;;
455
+ run.rerun.running) _M="Rerunning run #%s..." ;;
456
+ run.rerun.done) _M="Run #%s rerun." ;;
457
+ run.rerun.none_failed) _M="No failed run found." ;;
458
+ run.rerun.none_workflow) _M="No run found for workflow \"%s\"." ;;
459
+ # Run list
460
+ run.listing.all) _M="Listing recent runs..." ;;
461
+ run.listing.workflow) _M="Listing runs for workflow \"%s\"..." ;;
462
+ # PR checks
463
+ pr.checks.checking) _M="Checking checks for PR #%s..." ;;
423
464
  *) _M="" ;;
424
465
  esac
425
466
  }
@@ -116,6 +116,8 @@ _msg_fr() {
116
116
  hook.pre_push.5) _M="%s : aucun fichier modifié, rien à tester." ;;
117
117
  hook.pre_push.6) _M="Rien à tester (suppression de branche uniquement)." ;;
118
118
  hook.pre_push.7) _M="Pre-push OK" ;;
119
+ hook.pre_push.lint.missing) _M="%s absent, %s non vérifié (installation : %s)." ;;
120
+ hook.pre_push.lint.run) _M="%s : %s" ;;
119
121
  hook.branch_hint.1) _M="Nom de branche « %s » non conforme : commits et push seront refusés. Renommer :" ;;
120
122
  hook.branch_hint.2) _M=" Format <type>/<sujet> (ex. feat/inscription) ; aide complète au prochain commit." ;;
121
123
  hook.post_merge.1) _M="🧹 Branche locale supprimée (mergée et supprimée sur le serveur) : %s" ;;
@@ -262,6 +264,7 @@ _msg_fr() {
262
264
  ci.proteger.5) _M=" tags v* : %s" ;;
263
265
  ci.proteger.6) _M=" branche par défaut : %s" ;;
264
266
  ci.proteger.7) _M=" approbations exigées : %s ; revue des CODEOWNERS : %s ; approbation des changements non attribués : %s" ;;
267
+ ci.proteger.solo) _M=" dépôt solo (une seule personne avec l'écriture) : aucune approbation par une autre personne exigée" ;;
265
268
  ci.proteger.8) _M=" (les admins gardent un contournement explicite, tracé dans l'historique de la PR)" ;;
266
269
  ci.proteger.9) _M=" environnement « %s » : approbation de %s ; branches protégées et tags v*" ;;
267
270
  ci.proteger.10) _M="Ruleset « %s » mis à jour." ;;
@@ -276,6 +279,13 @@ _msg_fr() {
276
279
  ci.proteger.unattributed_invalid) _M="unattributedApproval : true ou false." ;;
277
280
  ci.proteger.no_branch) _M="Aucune des branches « %s » n'existe sur %s." ;;
278
281
  ci.proteger.no_cleanup) _M="Suppression automatique des branches désactivée (elle supprimerait develop) : ajouter un workflow qui appelle nettoyage-branches.yml, sinon les branches mergées restent (voir En organisation)." ;;
282
+ ci.proteger.no_delivery_env) _M="main exigera un déploiement « %s » réussi, mais aucun workflow ne crée ce déploiement : un déploiement que rien ne crée bloquerait main pour toujours. Appeler release-auto.yml avec delivery-environment (voir docs/releases.md, flux develop), ou deliveryApproval = review dans .repowarden.conf." ;;
283
+ ci.proteger.stale_concurrency) _M="%s : une demande de livraison (manuelle ou planifiée) en attente bloquerait en silence les suivantes. Mettre cancel-in-progress: \${{ github.event_name != 'push' }} (une nouvelle demande remplace celle en attente) et un groupe par déclencheur : release-\${{ github.ref_name }}-\${{ github.event.schedule || github.event_name }}." ;;
284
+ ci.proteger.urgent) _M=" étiquette « urgent » créée (son merge lance la demande de livraison)" ;;
285
+ ci.proteger.verify_missing) _M="%s : ruleset absent en ligne" ;;
286
+ ci.proteger.verify_diff) _M="%s : règles en ligne différentes\n en ligne : %s\n attendu : %s" ;;
287
+ ci.proteger.verify_fix) _M="Règles périmées : relancer repowarden proteger pour les mettre à jour." ;;
288
+ ci.proteger.verify_ok) _M="Règles en ligne à jour." ;;
279
289
  ci.proteger.delivery) _M=" livraison vers main : %s approbation(s) exigée(s) (passage à une nouvelle version) ; merge automatique des PR de travail vers develop : %s" ;;
280
290
  ci.proteger.methods_flow) _M="main : merge commit ; %s : squash ou merge commit" ;;
281
291
  ci.proteger.tags_org) _M="réservés aux workflows" ;;
@@ -363,6 +373,7 @@ _msg_fr() {
363
373
  st.repo_version_old) _M="Le projet demande repowarden %s ; installé : %s" ;;
364
374
  st.repo_version_ok) _M="Version demandée par le projet (%s) : respectée" ;;
365
375
  st.repo_forge) _M="Plateforme : %s" ;;
376
+ st.repo_rules) _M="Règles GitHub à jour ? repowarden proteger --verifier (lecture seule)" ;;
366
377
  st.tools) _M="Outils présents : %s · absents : %s (selon les langages des projets)" ;;
367
378
  st.next) _M="Étape suivante :" ;;
368
379
  st.ready) _M="Tout est prêt : git cc pour commiter, repowarden code-mort sur une branche." ;;
@@ -403,13 +414,16 @@ _msg_fr() {
403
414
  tk.validated_waiting) _M="Ticket validé, en attente d'être pris : sa branche sera créée à l'assignation, ou en le prenant soi-même avec \`repowarden ticket %s\`." ;;
404
415
  tk.pr_inverted) _M="PR fermée automatiquement : une PR de \`%s\` vers \`%s\` est sans objet. Une branche persistante ne part que pour une livraison (develop → main) ou un retour (main → develop). Pour mettre une branche de travail à jour : sur cette branche, \`git merge origin/%s\`, puis \`git push\`." ;;
405
416
  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." ;;
417
+ 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." ;;
406
418
  tk.branch_registered) _M="Branche du ticket : \`%s\`." ;;
407
419
  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é." ;;
408
420
  tk.adopted) _M="Tickets existants : %s rangé(s), %s branche(s) inscrite(s)." ;;
409
421
  tk.init_adopt) _M="Tickets et branches existants" ;;
410
422
  tk.abandoned) _M="Ticket #%s abandonné : PR fermée, branche supprimée." ;;
411
423
  tk.branch_deleted) _M="Branche %s supprimée (ticket #%s abandonné)" ;;
424
+ 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." ;;
412
425
  tk.draft_opened) _M="PR #%s ouverte en brouillon pour le ticket #%s" ;;
426
+ tk.draft_updated) _M="PR #%s (ticket #%s) : titre et description repris du ticket" ;;
413
427
  tk.created_body) _M="Ticket créé automatiquement pour la PR #%s, qui n'en citait aucun. À valider par un mainteneur (étiquette « validé »)." ;;
414
428
  tk.created) _M="Ticket #%s créé pour la PR #%s et relié à sa description : à valider par un mainteneur." ;;
415
429
  tk.init_labels) _M="Étiquettes (statuts et validation)" ;;
@@ -417,9 +431,36 @@ _msg_fr() {
417
431
  tk.init_exists) _M="%s existe déjà : laissé tel quel." ;;
418
432
  tk.init_written) _M="%s créé." ;;
419
433
  tk.init_done) _M="Suivi des tickets prêt." ;;
434
+ tk.init_auto_create_on) _M="Création auto de ticket : ACTIVÉE (ticketAutoCreate = true)" ;;
435
+ tk.init_auto_create_off) _M="Création auto de ticket : DÉSACTIVÉE (défaut). Pour l'activer : git config repowarden.ticketAutoCreate true" ;;
420
436
  tk.init_next) _M="Ensuite : committer ces fichiers ; exiger la vérification « ticket » (repowarden proteger --checks \"…,ticket\") ; dans release.yml, brancher le job tickets (voir la doc Tickets)." ;;
421
437
  tk.prerelease) _M="Disponible en préversion **%s** : à tester avant la production." ;;
422
438
  tk.released) _M="Publié dans **%s**." ;;
439
+ # Ticket comment
440
+ tk.comment.adding) _M="Ajout d'un commentaire sur le ticket #%s..." ;;
441
+ tk.comment.added) _M="Commentaire ajouté au ticket #%s." ;;
442
+ # Ticket label
443
+ tk.label.adding) _M="Ajout du label « %s » sur le ticket #%s..." ;;
444
+ tk.label.added) _M="Label « %s » ajouté au ticket #%s." ;;
445
+ tk.label.removing) _M="Retrait du label « %s » du ticket #%s..." ;;
446
+ tk.label.removed) _M="Label « %s » retiré du ticket #%s." ;;
447
+ # PR label
448
+ pr.label.adding) _M="Ajout du label « %s » sur la PR #%s..." ;;
449
+ pr.label.added) _M="Label « %s » ajouté à la PR #%s." ;;
450
+ pr.label.removing) _M="Retrait du label « %s » de la PR #%s..." ;;
451
+ pr.label.removed) _M="Label « %s » retiré de la PR #%s." ;;
452
+ # Run rerun
453
+ run.rerun.failed) _M="Relance du dernier run échoué..." ;;
454
+ run.rerun.workflow) _M="Relance du dernier run du workflow « %s »..." ;;
455
+ run.rerun.running) _M="Relance du run #%s..." ;;
456
+ run.rerun.done) _M="Run #%s relancé." ;;
457
+ run.rerun.none_failed) _M="Aucun run échoué trouvé." ;;
458
+ run.rerun.none_workflow) _M="Aucun run trouvé pour le workflow « %s »." ;;
459
+ # Run list
460
+ run.listing.all) _M="Liste des runs récents..." ;;
461
+ run.listing.workflow) _M="Liste des runs du workflow « %s »..." ;;
462
+ # PR checks
463
+ pr.checks.checking) _M="Vérification des checks de la PR #%s..." ;;
423
464
  *) _M="" ;;
424
465
  esac
425
466
  }
@@ -24,21 +24,73 @@ branch_ticket_r() {
24
24
  if [[ "$1" =~ ^[a-z]+/([0-9]+)(-|$) ]]; then REPLY="${BASH_REMATCH[1]}"; fi
25
25
  }
26
26
 
27
+ # $1 → sans accents (un à un : les classes [àâ] sont fausses en locale C) ;
28
+ # les autres caractères sont conservés, le nettoyage reste à l'appelant
29
+ sans_accents() {
30
+ printf '%s' "$1" | sed -e 's/à/a/g' -e 's/â/a/g' -e 's/ä/a/g' -e 's/é/e/g' -e 's/è/e/g' -e 's/ê/e/g' -e 's/ë/e/g' -e 's/î/i/g' -e 's/ï/i/g' -e 's/ô/o/g' -e 's/ö/o/g' -e 's/ù/u/g' -e 's/û/u/g' -e 's/ü/u/g' -e 's/ç/c/g' -e 's/œ/oe/g' -e 's/æ/ae/g' -e 's/À/a/g' -e 's/Â/a/g' -e 's/Ä/a/g' -e 's/É/e/g' -e 's/È/e/g' -e 's/Ê/e/g' -e 's/Ë/e/g' -e 's/Î/i/g' -e 's/Ï/i/g' -e 's/Ô/o/g' -e 's/Ö/o/g' -e 's/Ù/u/g' -e 's/Û/u/g' -e 's/Ü/u/g' -e 's/Ç/c/g' -e 's/Œ/oe/g' -e 's/Æ/ae/g'
31
+ }
32
+
33
+ # Mots vides qu'un nom de branche (ou une portée) ne garde pas en fin de coupe :
34
+ # « …-garde-la-trace-de » → « …-garde-la-trace »
35
+ MOTS_VIDES="a au aux de des du en et l la le les of on ou par pour sur the to un une"
36
+
37
+ # $1 (minuscules, tirets) → sans ses mots vides finaux
38
+ sans_mots_vides() {
39
+ local slug="$1" mot
40
+ while [[ "$slug" == *-* ]]; do
41
+ mot="${slug##*-}"
42
+ [[ " $MOTS_VIDES " == *" $mot "* ]] || break
43
+ slug="${slug%-*}"
44
+ done
45
+ printf '%s' "$slug"
46
+ }
47
+
27
48
  # REPLY = nom de branche d'un ticket : $1 numéro, $2 titre, $3 type (feat
28
49
  # par défaut) → feat/12-ajoute-le-panier (minuscules, sans accents, 40
29
- # caractères de titre au plus, coupé sur un tiret)
50
+ # caractères au plus, coupé sur un tiret puis sur un mot entier)
30
51
  ticket_branch_r() {
31
52
  local n="$1" titre="$2" type="${3:-feat}" slug
32
- # Accents retirés un à un (pas de classe [àâ] : fausse en locale C)
33
- slug="$(printf '%s' "$titre" | sed -e 's/à/a/g' -e 's/â/a/g' -e 's/ä/a/g' -e 's/é/e/g' -e 's/è/e/g' -e 's/ê/e/g' -e 's/ë/e/g' -e 's/î/i/g' -e 's/ï/i/g' -e 's/ô/o/g' -e 's/ö/o/g' -e 's/ù/u/g' -e 's/û/u/g' -e 's/ü/u/g' -e 's/ç/c/g' -e 's/œ/oe/g' -e 's/æ/ae/g' -e 's/À/a/g' -e 's/Â/a/g' -e 's/Ä/a/g' -e 's/É/e/g' -e 's/È/e/g' -e 's/Ê/e/g' -e 's/Ë/e/g' -e 's/Î/i/g' -e 's/Ï/i/g' -e 's/Ô/o/g' -e 's/Ö/o/g' -e 's/Ù/u/g' -e 's/Û/u/g' -e 's/Ü/u/g' -e 's/Ç/c/g' -e 's/Œ/oe/g' -e 's/Æ/ae/g' |
34
- LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed -e 's/[^a-z0-9]\{1,\}/-/g' -e 's/^-//' -e 's/-$//')"
53
+ slug="$(sans_accents "$titre" | LC_ALL=C tr '[:upper:]' '[:lower:]' |
54
+ LC_ALL=C sed -e 's/[^a-z0-9]\{1,\}/-/g' -e 's/^-//' -e 's/-$//')"
35
55
  if [ "${#slug}" -gt 40 ]; then
36
56
  slug="${slug:0:40}"
37
57
  [[ "$slug" == *-* ]] && slug="${slug%-*}"
38
58
  fi
59
+ slug="$(sans_mots_vides "$slug")"
39
60
  REPLY="$type/$n${slug:+-$slug}"
40
61
  }
41
62
 
63
+ # REPLY = titre de PR d'un ticket : $1 type (« feat » par défaut), $2 titre du
64
+ # ticket → « feat(portée): sujet ». Portée = préfixe du titre avant le premier
65
+ # « : » (« Tickets : … » → « tickets »), 20 caractères au plus, omise si vide ;
66
+ # sujet = le reste, première lettre abaissée. 72 caractères au plus, coupé sur
67
+ # un mot entier. Vide si le titre ne donne rien (l'appelant décide du repli).
68
+ ticket_pr_title_r() {
69
+ local type="${1:-feat}" titre="$2" portee sujet tete res
70
+ sujet="$titre"
71
+ portee=""
72
+ if [[ "$titre" =~ ^([^:]{1,30})[[:space:]]*:[[:space:]]+(.+)$ ]]; then
73
+ portee="$(sans_accents "${BASH_REMATCH[1]}" | LC_ALL=C tr '[:upper:]' '[:lower:]' |
74
+ LC_ALL=C sed -e 's/[^a-z0-9._-]\{1,\}/-/g' -e 's/^-//' -e 's/-$//')"
75
+ [ "${#portee}" -le 20 ] || portee=""
76
+ sujet="${BASH_REMATCH[2]}"
77
+ fi
78
+ sujet="$(printf '%s' "${sujet:0:1}" | LC_ALL=C tr '[:upper:]' '[:lower:]')${sujet:1}"
79
+ tete="$type${portee:+($portee)}: "
80
+ REPLY=""
81
+ [ -n "$sujet" ] || return 0
82
+ res="$tete$sujet"
83
+ # 72 caractères au plus, coupé sur un mot entier (authored_length → REPLY)
84
+ authored_length "$res"
85
+ while [ "$REPLY" -gt 72 ] && [ -n "$sujet" ]; do
86
+ if [[ "$sujet" == *" "* ]]; then sujet="${sujet% *}"; else sujet="${sujet%?}"; fi
87
+ res="$tete$sujet"
88
+ authored_length "$res"
89
+ done
90
+ [ -n "$sujet" ] || res=""
91
+ REPLY="$res"
92
+ }
93
+
42
94
  # REPLY = type de branche d'après les étiquettes d'un ticket (une par ligne) :
43
95
  # « type: fix », « bug » → fix ; feat par défaut
44
96
  ticket_type_r() {
@@ -90,6 +142,21 @@ tickets_managed() {
90
142
  [ "$TICKETS_MANAGED" = oui ]
91
143
  }
92
144
 
145
+ # Vrai si la création automatique de ticket est activée : réglage
146
+ # ticketAutoCreate = true (défaut false)
147
+ TICKET_AUTO_CREATE=""
148
+ ticket_auto_create() {
149
+ if [ -z "$TICKET_AUTO_CREATE" ]; then
150
+ TICKET_AUTO_CREATE=non
151
+ cfg_r ticketAutoCreate ""
152
+ case "$REPLY" in
153
+ true | oui | yes) TICKET_AUTO_CREATE=oui ;;
154
+ false | non | no | "") ;;
155
+ esac
156
+ fi
157
+ [ "$TICKET_AUTO_CREATE" = oui ]
158
+ }
159
+
93
160
  # Branche de travail sans ticket : avertissement, jamais bloquant. Création
94
161
  # proposée seulement si repowarden gère la vie des tickets du projet (et que
95
162
  # gh est là pour la faire) ; sinon, comment faire taire l'avertissement.