repowarden 4.8.3 → 4.9.0-next.10

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm](https://img.shields.io/npm/v/repowarden)](https://www.npmjs.com/package/repowarden)
7
7
  [![Documentation](https://img.shields.io/badge/docs-site-indigo)](https://simdevwarden.github.io/repowarden/)
8
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/simdevWarden/repowarden/badge)](https://scorecard.dev/viewer/?uri=github.com/simdevWarden/repowarden)
9
- [![Licence MIT](https://img.shields.io/badge/licence-MIT-blue)](LICENSE)
9
+ [![Licence GPL-3.0-or-later](https://img.shields.io/badge/licence-GPL--3.0--or--later-blue)](LICENSE)
10
10
 
11
11
  **repowarden** garde l'entrée de vos dépôts Git : messages de commit, nommage des branches, secrets, formatage et tests, **quel que soit le langage** — sur les postes (hooks), en CI (GitHub, GitLab) et côté serveur.
12
12
 
@@ -166,4 +166,4 @@ Les PR visent `develop` ; `main` ne reçoit que les livraisons, qui déclenchent
166
166
 
167
167
  [Démarrage rapide](https://simdevwarden.github.io/repowarden/demarrage/) · [Hooks et règles](https://simdevwarden.github.io/repowarden/hooks/) · [Technologies](https://simdevwarden.github.io/repowarden/technologies/) · [Configuration](https://simdevwarden.github.io/repowarden/configuration/) · [CI](https://simdevwarden.github.io/repowarden/ci/) · [Versions et releases](https://simdevwarden.github.io/repowarden/releases/) · [Validation humaine](https://simdevwarden.github.io/repowarden/validation/) · [Code mort](https://simdevwarden.github.io/repowarden/code-mort/) · [En organisation](https://simdevwarden.github.io/repowarden/industrialisation/) · [Sources internes (Nexus, Artifactory)](https://simdevwarden.github.io/repowarden/industrialisation/#8-sources-internes-reseau-ferme-nexus-artifactory) · [Désinstallation](https://simdevwarden.github.io/repowarden/desinstallation/) · [Sécurité](SECURITY.md) · [Contribuer](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
168
168
 
169
- Licence [MIT](LICENSE).
169
+ Licence [GPL-3.0-or-later](LICENSE) : GNU General Public License version 3, ou (au choix) toute version ultérieure.
package/bin/app CHANGED
@@ -15,7 +15,7 @@
15
15
  # --administration, pour appliquer la configuration depuis la CI) ;
16
16
  # 2. identifiants rangés sans être affichés : variable REPOWARDEN_APP_CLIENT_ID,
17
17
  # secret REPOWARDEN_APP_KEY (clé privée) ;
18
- # 3. installation sur le dépôt : tu cliques sur « Install ».
18
+ # 3. installation sur le dépôt : « Install » (vérifiée avec la clé ; relance : « non vérifiée »).
19
19
  # Relançable : une App déjà configurée n'est pas recréée. Demande gh et node.
20
20
  set -euo pipefail
21
21
 
@@ -49,6 +49,7 @@ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
49
49
  command -v node >/dev/null || { err_t app.no_node; exit 1; }
50
50
  [ -n "$REPO" ] || REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
51
51
  owner="${REPO%%/*}"
52
+ NOM_DEMANDE="$NOM"
52
53
  # App du bot : identifiants à part (dépôt de pilotage), droits sur les workflows
53
54
  PREFIXE=REPOWARDEN_APP
54
55
  if [ -n "$BOT" ]; then
@@ -67,6 +68,25 @@ ouvrir() {
67
68
  fi
68
69
  }
69
70
 
71
+ # Conteneur dont le navigateur (sur l'hôte) ne joint pas 127.0.0.1 : la page
72
+ # locale et le retour de GitHub y seraient injoignables (App orpheline si
73
+ # « Create » est cliqué). VS Code Dev Containers et Codespaces redirigent les
74
+ # ports d'eux-mêmes. REPOWARDEN_CONTENEUR=0|1 force la réponse.
75
+ conteneur_isole() {
76
+ case "${REPOWARDEN_CONTENEUR:-}" in 0) return 1 ;; 1) return 0 ;; esac
77
+ [ -z "${REMOTE_CONTAINERS:-}${CODESPACES:-}${VSCODE_IPC_HOOK_CLI:-}" ] || return 1
78
+ [ -e /.dockerenv ] || [ -e /run/.containerenv ] ||
79
+ grep -qE 'docker|kubepods|containerd|lxc|podman' /proc/1/cgroup 2>/dev/null
80
+ }
81
+
82
+ # Marche à suivre quand la page locale n'est pas joignable : lancer sur l'hôte
83
+ marche_a_suivre() {
84
+ local cmd="npx repowarden app init --repo $REPO"
85
+ [ -z "$NOM_DEMANDE" ] || cmd="$cmd --nom $NOM_DEMANDE"
86
+ if [ -n "$BOT" ]; then cmd="$cmd --bot"; elif [ -n "$ADMIN" ]; then cmd="$cmd --administration"; fi
87
+ info_t app.howto "$cmd"
88
+ }
89
+
70
90
  # 1. Création, si l'App n'est pas déjà configurée pour ce dépôt
71
91
  step_t app.step_create
72
92
  client_id="$(gh variable get "${PREFIXE}_CLIENT_ID" --repo "$REPO" 2>/dev/null || true)"
@@ -75,18 +95,38 @@ if [ -n "$client_id" ] && grep -q "^${PREFIXE}_KEY" <<<"$(gh secret list --repo
75
95
  slug="$(gh variable get "${PREFIXE}_SLUG" --repo "$REPO" 2>/dev/null || true)"
76
96
  else
77
97
  if [ "$(gh api "users/$owner" -q .type)" = Organization ]; then
78
- cible="https://github.com/organizations/$owner/settings/apps/new"
98
+ apps="https://github.com/organizations/$owner/settings/apps"
79
99
  else
80
- cible="https://github.com/settings/apps/new"
100
+ apps="https://github.com/settings/apps"
101
+ fi
102
+ cible="$apps/new"
103
+ # Nom déjà pris (souvent une App orpheline d'un essai interrompu) : GitHub
104
+ # refuserait la création ; arrêt avec la marche à suivre. Slug : nom en
105
+ # minuscules, autres caractères en tirets ; une App privée d'un autre
106
+ # compte reste invisible (rappel à l'ouverture de la page)
107
+ slug_voulu="$(printf '%s' "$NOM" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')"
108
+ if [ "$(gh api "apps/$slug_voulu" -q .slug 2>/dev/null || true)" = "$slug_voulu" ]; then
109
+ err_t app.name_taken "$NOM"
110
+ info_t app.name_taken_fix "$apps"
111
+ exit 1
112
+ fi
113
+ if conteneur_isole; then
114
+ err_t app.container
115
+ marche_a_suivre
116
+ exit 1
81
117
  fi
82
118
  sortie="$(mktemp)"
83
119
  trap 'rm -f "$sortie"' EXIT
84
120
  # Page locale : envoie la description à GitHub (formulaire POST), puis
85
121
  # reçoit le code de retour ; le code est écrit sur la sortie standard
86
- NOM="$NOM" REPO="$REPO" ADMIN="$ADMIN" BOT="$BOT" CIBLE="$cible" node -e '
122
+ delai_page="${REPOWARDEN_APP_DELAI:-180}"
123
+ NOM="$NOM" REPO="$REPO" ADMIN="$ADMIN" BOT="$BOT" CIBLE="$cible" DELAI_PAGE="$delai_page" node -e '
87
124
  const http = require("http"), crypto = require("crypto");
88
125
  const etat = crypto.randomBytes(16).toString("hex");
126
+ // Page jamais consultée (navigateur qui ne joint pas 127.0.0.1) : arrêt
127
+ const attente = setTimeout(() => { console.log("DELAI_PAGE"); process.exit(1); }, Number(process.env.DELAI_PAGE) * 1000);
89
128
  const srv = http.createServer((req, res) => {
129
+ clearTimeout(attente);
90
130
  const u = new URL(req.url, "http://127.0.0.1");
91
131
  if (u.pathname === "/fin") {
92
132
  if (u.searchParams.get("state") !== etat) { res.writeHead(400); res.end("état invalide"); return; }
@@ -120,10 +160,22 @@ else
120
160
  port="$(sed -n 's/^PORT //p' "$sortie")"
121
161
  [ -n "$port" ] || { err_t app.server_failed; exit 1; }
122
162
  info_t app.open "http://127.0.0.1:$port/"
163
+ dim_t app.name_hint "$NOM" "$apps"
123
164
  ouvrir "http://127.0.0.1:$port/"
124
165
  wait "$serveur" || true
125
166
  code="$(sed -n 's/^CODE //p' "$sortie")"
126
- [ -n "$code" ] || { err_t app.no_code; exit 1; }
167
+ if grep -q '^DELAI_PAGE' "$sortie"; then
168
+ err_t app.page_timeout "$delai_page"
169
+ marche_a_suivre
170
+ exit 1
171
+ fi
172
+ if [ -z "$code" ]; then
173
+ err_t app.no_code
174
+ attention_t app.orphan
175
+ info_t app.name_taken_fix "$apps"
176
+ marche_a_suivre
177
+ exit 1
178
+ fi
127
179
  # Conversion du code : identifiants de l'App, rangés sans être affichés
128
180
  reponse="$(gh api -X POST "app-manifests/$code/conversions")"
129
181
  client_id="$(node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).client_id' <<<"$reponse")"
@@ -153,10 +205,10 @@ installee() {
153
205
  }
154
206
  cle="${cle:-}"
155
207
  if [ -z "$cle" ]; then
156
- # Relance (clé non disponible ici) : vérifiée au premier run du suivi des tickets
157
- info_t app.check_in_ci "https://github.com/apps/$slug/installations/new"
158
- echo
159
- ok_t app.done
208
+ # Relance : la clé privée n'est pas ici, et le jeton de gh ne peut pas
209
+ # lister les installations d'une App (user/installations : 403) ; état
210
+ # « non vérifié » plutôt qu'un succès non constaté
211
+ attention_t app.not_verified "${slug:-?}" "$REPO" "https://github.com/apps/${slug:-<slug>}/installations/new"
160
212
  exit 0
161
213
  fi
162
214
  if [ -n "$slug" ] && ! installee; then
package/bin/bots CHANGED
@@ -6,17 +6,21 @@
6
6
  # ne plus jamais le proposer (repowarden.bots = off)
7
7
  # Rien n'est fait sans accord : une question globale (non par défaut), le
8
8
  # flux (main seul, ou develop → main), un accord par bot (CI, releases,
9
- # nettoyage, tickets, App, protection, npm), l'aperçu de ce qui sera créé ou
10
- # modifié, puis une confirmation finale. Un fichier existant n'est jamais
11
- # écrasé. Relançable pour compléter. Demande un terminal, gh connecté et les
12
- # droits d'administration du dépôt.
9
+ # nettoyage, tickets, App, protection, npm), la livraison (flux develop avec
10
+ # releases : automatique ou approuvée, rythme, préversion obligatoire),
11
+ # l'aperçu de ce qui sera créé ou modifié, puis une confirmation finale. Un
12
+ # fichier existant n'est jamais écrasé. Relançable pour compléter. Demande un
13
+ # terminal, gh connecté et les droits d'administration du dépôt.
13
14
  set -euo pipefail
14
15
 
15
16
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
16
17
  # shellcheck source=../hooks/lib/common.sh
17
18
  source "$ROOT/hooks/lib/common.sh"
19
+ # Détection des langages des hooks (marqueurs des plugins) : outillage de la CI
20
+ # shellcheck source=../hooks/lib/lang.sh
21
+ source "$ROOT/hooks/lib/lang.sh"
18
22
 
19
- aide() { sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; }
23
+ aide() { sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; }
20
24
 
21
25
  # Commandes enchaînées (remplaçables dans les tests)
22
26
  BIN="${REPOWARDEN_BIN:-$ROOT/bin}"
@@ -80,7 +84,7 @@ t bots.flow_question
80
84
  printf '%s\n' "$REPLY" >&2
81
85
  t bots.flow_main "$PRINCIPALE"
82
86
  printf ' 1) %s\n' "$REPLY" >&2
83
- t bots.flow_develop "$PRINCIPALE"
87
+ t bots.flow_develop "$PRINCIPALE" "$PRINCIPALE"
84
88
  printf ' 2) %s\n' "$REPLY" >&2
85
89
  # develop → main par défaut : seul develop entre dans main
86
90
  choix_flux=2
@@ -90,8 +94,9 @@ case "${reponse:-$choix_flux}" in
90
94
  *) FLUX=main; INTEGRATION="" ;;
91
95
  esac
92
96
  CREER_DEVELOP=""
97
+ # Nom renvoyé comparé : sur une 404, gh écrit le corps de l'erreur sur stdout
93
98
  if [ "$FLUX" = develop ] &&
94
- [ -z "$(gh api "repos/$REPO/branches/$INTEGRATION" --jq .name 2>/dev/null || true)" ]; then
99
+ [ "$(gh api "repos/$REPO/branches/$INTEGRATION" --jq .name 2>/dev/null || true)" != "$INTEGRATION" ]; then
95
100
  if demander bots.ask_create_develop o; then
96
101
  CREER_DEVELOP=1
97
102
  else
@@ -121,6 +126,113 @@ choisi() { [[ " ${BOTS[*]+"${BOTS[*]}"} " == *" $1 "* ]]; }
121
126
  AVEC_APP=""
122
127
  { [ -n "$APP_PRESENTE" ] || choisi app; } && AVEC_APP=1
123
128
 
129
+ # --- Livraison (flux develop, releases) : mode, rythme, préversion -----------
130
+ # Défauts : le release.yml d'aujourd'hui (approbation, regroupé, préversion
131
+ # obligatoire) ; chaque réponse devient une entrée de release-auto.yml
132
+ LIVRAISON_AUTO="" A_CHAQUE_TICKET="" PREVERSION_OBLIGATOIRE=1
133
+ if [ "$FLUX" = develop ] && choisi release; then
134
+ echo >&2
135
+ dim_t bots.desc_delivery
136
+ if demander bots.ask_delivery_auto n; then
137
+ LIVRAISON_AUTO=1
138
+ # Le merge automatique passe par la GitHub App (jeton des Actions : pas de release)
139
+ [ -n "$AVEC_APP" ] || attention_t bots.auto_no_app
140
+ fi
141
+ dim_t bots.desc_rythme
142
+ demander bots.ask_each_ticket n && A_CHAQUE_TICKET=1
143
+ demander bots.ask_preversion o || PREVERSION_OBLIGATOIRE=""
144
+ fi
145
+
146
+ # --- Outillage de la CI : langages du projet --------------------------------
147
+ # Même détection que les hooks (marqueurs des plugins, repowarden.skip), à la
148
+ # racine. strict activé seulement si le formatage et les tests de chaque
149
+ # langage ont leur outil (SANS_STRICT : raison sinon).
150
+ CI_LANGS=""
151
+ SANS_STRICT=""
152
+ sans_strict() { SANS_STRICT="${SANS_STRICT:+$SANS_STRICT, }$1"; }
153
+ active_plugins_init
154
+ for p in python node maven gradle go; do
155
+ { [[ " $ACTIVE_PLUGINS " == *" $p "* ]] && has_marker . "$p"; } || continue
156
+ CI_LANGS="$CI_LANGS $p"
157
+ case "$p" in
158
+ node) grep -q '"prettier"' package.json || sans_strict "prettier absent de package.json" ;;
159
+ maven) grep -q spotless pom.xml || sans_strict "Spotless non configuré (pom.xml)" ;;
160
+ gradle)
161
+ find . -maxdepth 3 \( -name '*.gradle' -o -name '*.gradle.kts' \) -not -path '*/build/*' -print0 2>/dev/null |
162
+ xargs -0 grep -qs spotless || sans_strict "Spotless non configuré (Gradle)"
163
+ ;;
164
+ esac
165
+ done
166
+ CI_LANGS="${CI_LANGS# }"
167
+
168
+ # Étapes d'installation de l'outillage des langages détectés (actions figées
169
+ # par SHA, comme les workflows de repowarden)
170
+ outillage_ci() {
171
+ local p pm java=""
172
+ for p in $CI_LANGS; do
173
+ case "$p" in
174
+ python)
175
+ cat <<'YAML'
176
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
177
+ with:
178
+ python-version: "3.12"
179
+ YAML
180
+ [ ! -f requirements.txt ] || echo " - run: pip install -r requirements.txt"
181
+ cat <<'YAML'
182
+ # Formatage, code mort et tests (versions figées)
183
+ - run: pip install ruff==0.16.10 vulture==2.16 pytest==9.1.1
184
+ YAML
185
+ ;;
186
+ node)
187
+ pm="$(node_pm)"
188
+ cat <<'YAML'
189
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
190
+ with:
191
+ node-version: 24
192
+ YAML
193
+ case "$pm" in
194
+ pnpm) echo " - run: corepack enable && pnpm install --frozen-lockfile" ;;
195
+ yarn) echo " - run: corepack enable && yarn install" ;;
196
+ *)
197
+ if [ -f package-lock.json ]; then
198
+ echo " cache: npm"
199
+ echo " - run: npm ci"
200
+ else
201
+ echo " - run: npm install"
202
+ fi
203
+ ;;
204
+ esac
205
+ ;;
206
+ maven | gradle)
207
+ if [ -z "$java" ]; then
208
+ java=1
209
+ cat <<YAML
210
+ - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
211
+ with:
212
+ distribution: temurin
213
+ java-version: "21"
214
+ cache: $p
215
+ YAML
216
+ fi
217
+ if [ "$p" = gradle ] && [ ! -x gradlew ]; then
218
+ cat <<'YAML'
219
+ - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
220
+ with:
221
+ gradle-version: current
222
+ YAML
223
+ fi
224
+ ;;
225
+ go)
226
+ cat <<'YAML'
227
+ - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
228
+ with:
229
+ go-version-file: go.mod
230
+ YAML
231
+ ;;
232
+ esac
233
+ done
234
+ }
235
+
124
236
  # --- Modèles des workflows -------------------------------------------------
125
237
  modele_ci() {
126
238
  cat <<'YAML'
@@ -152,34 +264,66 @@ jobs:
152
264
  repowarden:
153
265
  runs-on: ubuntu-latest
154
266
  steps:
155
- - uses: actions/checkout@v7
267
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
156
268
  with:
157
269
  fetch-depth: 0 # historique complet : messages de commit, diff depuis la base
270
+ YAML
271
+ if [ -n "$CI_LANGS" ]; then
272
+ echo " # Outillage du projet ($CI_LANGS) : formatage et tests"
273
+ outillage_ci
274
+ else
275
+ cat <<'YAML'
158
276
  # Outils du projet nécessaires au formatage et aux tests, par exemple :
159
- # - uses: actions/setup-node@v7
277
+ # - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
160
278
  # with: { node-version: 24, cache: npm }
161
279
  # - run: npm ci
162
- - uses: simdevWarden/repowarden@v4
163
280
  YAML
281
+ fi
282
+ echo " - uses: simdevWarden/repowarden@v4"
283
+ # strict : actif si l'outillage de chaque langage est installé ci-dessus
284
+ local actif="" strict="strict: true # outil absent bloquant, une fois l'outillage installé ci-dessus"
285
+ if [ -n "$CI_LANGS" ]; then
286
+ if [ -z "$SANS_STRICT" ]; then
287
+ actif=1 strict="strict: true # outillage installé ci-dessus : un outil absent fait échouer"
288
+ else
289
+ strict="strict: true # $SANS_STRICT : à activer une fois l'outil en place"
290
+ fi
291
+ fi
292
+ if [ -n "$actif" ]; then
293
+ echo " with:"
294
+ echo " $strict"
295
+ elif [ "$FLUX" = develop ]; then
296
+ echo " with:"
297
+ echo " # $strict"
298
+ else
299
+ echo " # with:"
300
+ echo " # $strict"
301
+ fi
164
302
  if [ "$FLUX" = develop ]; then
165
303
  cat <<'YAML'
166
- with:
167
- # strict: true # outil absent bloquant, une fois l'outillage installé ci-dessus
168
304
  # Seule la branche d'intégration entre dans la principale : une PR
169
305
  # de travail mal visée est reciblée avant les vérifications
170
306
  fix-pr: "true"
171
- YAML
172
- else
173
- cat <<'YAML'
174
- # with:
175
- # strict: true # outil absent bloquant, une fois l'outillage installé ci-dessus
176
307
  YAML
177
308
  fi
178
309
  }
179
310
 
180
311
  modele_release() {
181
312
  if [ "$FLUX" = develop ]; then
182
- cat <<YAML
313
+ if [ -n "$A_CHAQUE_TICKET" ]; then
314
+ cat <<YAML
315
+ # Releases, flux $INTEGRATION → $PRINCIPALE (mode tag) : à chaque merge sur
316
+ # $INTEGRATION, préversion vX.Y.Z-next.N et demande de livraison
317
+ # $INTEGRATION → $PRINCIPALE, qui publie le tag vX.Y.Z et la release.
318
+ name: release
319
+
320
+ on:
321
+ push:
322
+ branches: [$PRINCIPALE, $INTEGRATION]
323
+ workflow_dispatch: # à la main (et urgence) : préversion et demande de livraison
324
+ YAML
325
+ else
326
+ cat <<YAML
183
327
  # Releases, flux $INTEGRATION → $PRINCIPALE (mode tag) : préversion vX.Y.Z-next.N le
184
328
  # soir si $INTEGRATION a changé ; le vendredi matin (ou à la main), une demande
185
329
  # de livraison $INTEGRATION → $PRINCIPALE, qui publie le tag vX.Y.Z et la release.
@@ -192,6 +336,9 @@ on:
192
336
  - cron: "0 19 * * *" # préversion du soir, si $INTEGRATION a changé
193
337
  - cron: "0 7 * * 5" # fenêtre de livraison : le vendredi matin
194
338
  workflow_dispatch: # à la main (et urgence) : préversion et demande de livraison
339
+ YAML
340
+ fi
341
+ cat <<YAML
195
342
 
196
343
  # Une file par branche et par déclencheur ; une demande manuelle ou planifiée
197
344
  # remplace celle en attente ; un push n'annule jamais un run en cours
@@ -209,14 +356,29 @@ jobs:
209
356
  mode: tag
210
357
  integration-branch: $INTEGRATION
211
358
  main-branch: $PRINCIPALE
212
- delivery-schedule: "0 7 * * 5" # même cron que la fenêtre ci-dessus
213
359
  YAML
214
- if [ -n "$AVEC_APP" ]; then
360
+ if [ -n "$A_CHAQUE_TICKET" ]; then
215
361
  cat <<'YAML'
216
- delivery-environment: production # « Approve and deploy » valide la livraison
217
- app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }} # la GitHub App merge la livraison approuvée
362
+ preversion-on-push: true # préversion à chaque merge
363
+ delivery-on-push: true # demande de livraison à chaque merge
364
+ YAML
365
+ else
366
+ echo ' delivery-schedule: "0 7 * * 5" # même cron que la fenêtre ci-dessus'
367
+ fi
368
+ [ -n "$PREVERSION_OBLIGATOIRE" ] ||
369
+ echo " preversion-obligatoire: false # version stable possible sans préversion"
370
+ if [ -n "$LIVRAISON_AUTO" ]; then
371
+ echo " delivery-auto: true # la GitHub App merge la livraison, vérifications vertes"
372
+ elif [ -n "$AVEC_APP" ]; then
373
+ echo " delivery-environment: production # « Approve and deploy » valide la livraison"
374
+ fi
375
+ if [ -n "$AVEC_APP" ]; then
376
+ local merge="la GitHub App merge la livraison approuvée"
377
+ [ -z "$LIVRAISON_AUTO" ] || merge="la GitHub App merge la livraison"
378
+ cat <<YAML
379
+ app-client-id: \${{ vars.REPOWARDEN_APP_CLIENT_ID }} # $merge
218
380
  secrets:
219
- app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
381
+ app-key: \${{ secrets.REPOWARDEN_APP_KEY }}
220
382
  YAML
221
383
  fi
222
384
  else
@@ -251,6 +413,27 @@ YAML
251
413
  app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }}
252
414
  secrets:
253
415
  app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
416
+ YAML
417
+ fi
418
+ fi
419
+ # Tickets livrés : statut done et fermeture à la release, préprod à la
420
+ # préversion ; seulement avec le suivi des tickets (sinon le job échoue)
421
+ if choisi tickets || [ -e .github/workflows/tickets.yml ]; then
422
+ cat <<'YAML'
423
+
424
+ tickets:
425
+ needs: release
426
+ if: needs.release.outputs.release_created == 'true' || needs.release.outputs.prerelease_created == 'true'
427
+ uses: simdevWarden/repowarden/.github/workflows/tickets.yml@v4
428
+ permissions: { contents: write, issues: write, pull-requests: write, statuses: write }
429
+ with:
430
+ tag: ${{ needs.release.outputs.tag_name || needs.release.outputs.prerelease_tag }}
431
+ YAML
432
+ if [ -n "$AVEC_APP" ]; then
433
+ cat <<'YAML'
434
+ app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }}
435
+ secrets:
436
+ app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
254
437
  YAML
255
438
  fi
256
439
  fi
@@ -294,7 +477,22 @@ step_t bots.preview
294
477
  [ -z "$CREER_DEVELOP" ] || info_t bots.prev_develop "$INTEGRATION" "$PRINCIPALE"
295
478
  if [ "$FLUX" = develop ]; then
296
479
  info_t bots.prev_conf "$INTEGRATION"
297
- [ -n "$AVEC_APP" ] || info_t bots.prev_review
480
+ if [ -n "$LIVRAISON_AUTO" ]; then
481
+ info_t bots.prev_conf_auto
482
+ elif [ -z "$AVEC_APP" ]; then
483
+ info_t bots.prev_review
484
+ fi
485
+ fi
486
+ # Choix de livraison écrits dans release.yml (questions posées)
487
+ if [ "$FLUX" = develop ] && choisi release; then
488
+ if [ -n "$LIVRAISON_AUTO" ]; then
489
+ info_t bots.prev_delivery_auto
490
+ [ -n "$AVEC_APP" ] || attention_t bots.auto_no_app
491
+ else
492
+ info_t bots.prev_delivery_approval
493
+ fi
494
+ if [ -n "$A_CHAQUE_TICKET" ]; then info_t bots.prev_each_ticket; else info_t bots.prev_grouped; fi
495
+ if [ -n "$PREVERSION_OBLIGATOIRE" ]; then info_t bots.prev_preversion_on; else info_t bots.prev_preversion_off; fi
298
496
  fi
299
497
  for bot in ${BOTS[@]+"${BOTS[@]}"}; do
300
498
  fichier_du "$bot"
@@ -305,6 +503,12 @@ for bot in ${BOTS[@]+"${BOTS[@]}"}; do
305
503
  info_t bots.prev_kept "$f" "$ecart"
306
504
  else
307
505
  info_t bots.prev_file "$f"
506
+ # Outillage écrit dans la CI, ou modèle générique faute de langage reconnu
507
+ if [ "$bot" = ci ] && [ -n "$CI_LANGS" ]; then
508
+ info_t bots.prev_ci_langs "$CI_LANGS"
509
+ elif [ "$bot" = ci ]; then
510
+ info_t bots.prev_ci_generic
511
+ fi
308
512
  fi
309
513
  else
310
514
  info_t "bots.prev_$bot"
@@ -332,10 +536,16 @@ if [ -n "$CREER_DEVELOP" ]; then
332
536
  fi
333
537
  if [ "$FLUX" = develop ]; then
334
538
  conf="$GIT_TOPLEVEL/.repowarden.conf"
335
- git config -f "$conf" repowarden.integrationBranch "$INTEGRATION"
539
+ # Valeur déjà en place : fichier laissé tel quel (git config le réindenterait)
540
+ [ "$(git config -f "$conf" --get repowarden.integrationBranch || true)" = "$INTEGRATION" ] ||
541
+ git config -f "$conf" repowarden.integrationBranch "$INTEGRATION"
336
542
  git config -f "$conf" --get repowarden.protectedBranches >/dev/null ||
337
543
  git config -f "$conf" repowarden.protectedBranches "$PRINCIPALE $INTEGRATION"
338
- if [ -z "$AVEC_APP" ]; then
544
+ if [ -n "$LIVRAISON_AUTO" ]; then
545
+ # Choix explicite : remplace une approbation déjà réglée
546
+ [ "$(git config -f "$conf" --get repowarden.deliveryApproval || true)" = auto ] ||
547
+ git config -f "$conf" repowarden.deliveryApproval auto
548
+ elif [ -z "$AVEC_APP" ]; then
339
549
  git config -f "$conf" --get repowarden.deliveryApproval >/dev/null ||
340
550
  git config -f "$conf" repowarden.deliveryApproval review
341
551
  fi
@@ -5,16 +5,16 @@
5
5
  # de confiance, OIDC) et joint la provenance au paquet.
6
6
  #
7
7
  # bin/npm-publication [--repo propriétaire/nom] [--workflow release.yml]
8
- # [--environnement NOM]
8
+ # [--environnement NOM] [--dry-run]
9
9
  #
10
10
  # 1. connexion npm (npm login si besoin) ;
11
11
  # 2. paquet absent de npm (la publication de confiance exige un paquet
12
- # existant) : jeton npm temporaire (7 jours, limité au scope du paquet)
13
- # créé et rangé directement en secret NPM_TOKEN, sans être affiché ;
14
- # variable REPOWARDEN_NPM=true ; la pipeline publie la dernière release
15
- # (lancée par la commande), ou la prochaine s'il n'y en a pas encore ;
16
- # 3. publication de confiance (npm trust github), puis jeton révoqué et
17
- # secret supprimé. Interrompue, la commande reprend où elle en était.
12
+ # existant) : la dernière release est publiée d'ici, par npm publish lancé
13
+ # dans le terminal (npm conduit sa validation : navigateur ou code) ;
14
+ # aucune release : rien n'est publié, relancer après la première ;
15
+ # 3. publication de confiance (npm trust github), variable REPOWARDEN_NPM ;
16
+ # un ancien secret NPM_TOKEN et d'anciens jetons temporaires sont retirés.
17
+ # Interrompue, la commande reprend où elle en était.
18
18
  set -euo pipefail
19
19
 
20
20
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
@@ -48,8 +48,7 @@ cd "$GIT_TOPLEVEL"
48
48
  [ -f package.json ] || { err_t npm.no_package; exit 1; }
49
49
  [ -n "$REPO" ] || REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
50
50
  name="$(node -p 'require("./package.json").name')"
51
- # Préfixe des jetons temporaires : nom unique à chaque création (npm refuse
52
- # les doublons, y compris celui d'une tentative interrompue) ; tous révoqués à la fin
51
+ # Préfixe des jetons temporaires de l'ancien parcours, révoqués à la fin
53
52
  TOKEN_PREFIX="repowarden-ci-${REPO##*/}"
54
53
 
55
54
  # Vérifie si une relation de confiance correspond aux paramètres attendus.
@@ -119,8 +118,8 @@ brand "npm · $name · $REPO"
119
118
  step_t npm.step_login
120
119
  npm whoami >/dev/null 2>&1 || npm login
121
120
  ok_t npm.logged "$(npm whoami)"
122
- # Créer un jeton et configurer la publication de confiance exigent la 2FA du
123
- # compte (les codes envoyés par e-mail ne sont qu'une vérification de connexion)
121
+ # Configurer la publication de confiance exige la 2FA du compte (les codes
122
+ # envoyés par e-mail ne sont qu'une vérification de connexion)
124
123
  if [ "$(npm profile get --json 2>/dev/null | node -e 'try{const p=JSON.parse(require("fs").readFileSync(0,"utf8"));process.stdout.write(p.tfa&&p.tfa.mode?"1":"")}catch{}')" != 1 ]; then
125
124
  err_t npm.no_2fa
126
125
  dim_t npm.hint_2fa "$(npm whoami)"
@@ -129,69 +128,40 @@ fi
129
128
 
130
129
  publie() { [ -n "$(npm view "$name" name 2>/dev/null || true)" ]; }
131
130
 
132
- # 2. Première publication, faite par la pipeline avec un jeton temporaire
131
+ # 2. Première publication, faite ici depuis la dernière release : npm conduit
132
+ # lui-même sa validation (navigateur ou code), dans le terminal. Aucun jeton
133
+ # créé ni stocké, aucun mot de passe saisi par repowarden
133
134
  if ! publie; then
134
- step_t npm.step_token
135
- if grep -q '^NPM_TOKEN' <<<"$(gh secret list --repo "$REPO")"; then
136
- ok_t npm.token_exists
137
- else
138
- # npm écrit ses questions et le jeton au même endroit : les questions
139
- # sont posées ici (saisie masquée) et le jeton va droit au secret
140
- if [[ "$name" == @*/* ]]; then portee=(--scopes "${name%%/*}"); else portee=(--packages-all); fi
141
- t npm.ask_password
142
- read -r -s -p " $REPLY" mdp
143
- echo
144
- otp="" sortie=""
145
- for essai in 1 2 3 4; do
146
- # Sortie texte, pas --json : npm y masque le jeton (« npm_*** »)
147
- if sortie="$(npm_config_password="$mdp" npm_config_otp="$otp" npm token create \
148
- --name "$TOKEN_PREFIX-$(date +%Y%m%d%H%M%S)" --expires 7 "${portee[@]}" \
149
- --packages-and-scopes-permission read-write --bypass-2fa </dev/null 2>&1)"; then
150
- break
151
- fi
152
- # Code à usage unique (application ou e-mail) demandé : on le pose
153
- if [ "$essai" -lt 4 ] && grep -qiE 'otp|one-time|two-factor' <<<"$sortie"; then
154
- t npm.ask_otp
155
- read -r -p " $REPLY" otp
156
- continue
157
- fi
158
- sed -E 's/npm_[A-Za-z0-9]+/npm_***/g' <<<"$sortie" >&2
159
- mdp=""
160
- err_t npm.token_failed
161
- exit 1
162
- done
163
- mdp=""
164
- jeton="$(grep -oE 'npm_[A-Za-z0-9]+' <<<"$sortie" | sed -n 1p || true)"
165
- sortie=""
166
- [ -n "$jeton" ] || { err_t npm.token_failed; exit 1; }
167
- printf '%s' "$jeton" | gh secret set NPM_TOKEN --repo "$REPO"
168
- jeton=""
169
- ok_t npm.token_stored
135
+ step_t npm.step_first
136
+ tag="$(gh release view --repo "$REPO" --json tagName -q .tagName 2>/dev/null || true)"
137
+ if [ -z "$tag" ]; then
138
+ attention_t npm.no_release "$name"
139
+ exit 0
170
140
  fi
171
- gh variable set REPOWARDEN_NPM --repo "$REPO" --body true
172
-
173
- # Release déjà publiée : la pipeline est lancée tout de suite (elle publie
174
- # la dernière release absente de npm) ; sinon, la prochaine release publie
175
- if [ -n "$(git ls-remote --tags origin 'v[0-9]*')" ]; then
176
- branche="$(gh repo view "$REPO" --json defaultBranchRef -q .defaultBranchRef.name)"
177
- gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$branche"
178
- info_t npm.dispatched "$WORKFLOW"
179
- else
180
- info_t npm.waiting "$name"
141
+ version="${tag#v}"
142
+ if [ "$DRY_RUN" = true ]; then
143
+ info_t npm.would_publish "$name" "$version" "$tag"
144
+ exit 0
145
+ fi
146
+ clone="$(mktemp -d "${TMPDIR:-/tmp}/repowarden-npm.XXXXXX")"
147
+ trap 'rm -rf "$clone"' EXIT
148
+ gh repo clone "$REPO" "$clone/src" -- -q --depth 1 --branch "$tag"
149
+ info_t npm.publishing "$name" "$version" "$tag"
150
+ # Sortie non capturée, entrée héritée : npm pose ses questions lui-même
151
+ if ! (cd "$clone/src" && npm pkg set "version=$version" && npm publish --access public); then
152
+ err_t npm.publish_failed "$name"
153
+ exit 1
181
154
  fi
182
- for i in $(seq 1 360); do
183
- publie && break
184
- [ $((i % 10)) -ne 0 ] || dim_t npm.still_waiting "$((i / 2))"
185
- sleep 30
186
- done
187
- publie || { attention_t npm.not_yet; exit 0; }
188
- ok_t npm.first_published "$name" "$(npm view "$name" version)"
155
+ rm -rf "$clone"
156
+ trap - EXIT
157
+ ok_t npm.first_published "$name" "$version"
189
158
  fi
190
159
 
191
160
  # 3. Publication de confiance, puis plus aucun jeton
192
161
  step_t npm.step_trust
193
- verifier_trust
194
- cas=$?
162
+ # Pas de relation (1) ou relation différente (2) : ne doit pas arrêter le script (set -e)
163
+ cas=0
164
+ verifier_trust || cas=$?
195
165
  if [ "$cas" = 0 ]; then
196
166
  ok_t npm.trust_exists
197
167
  elif [ "$cas" = 1 ]; then