repowarden 4.7.1 → 4.7.2-next.6

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.
@@ -24,11 +24,13 @@ source "$ROOT/hooks/lib/common.sh"
24
24
  REPO=""
25
25
  WORKFLOW="release.yml"
26
26
  ENVIRONMENT=""
27
+ DRY_RUN=false
27
28
  while [ $# -gt 0 ]; do
28
29
  case "$1" in
29
30
  --repo) shift; REPO="${1:?--repo attend propriétaire/nom}" ;;
30
31
  --workflow) shift; WORKFLOW="${1:?--workflow attend un fichier}" ;;
31
32
  --environnement | --environment) shift; ENVIRONMENT="${1:?--environnement attend un nom}" ;;
33
+ --dry-run) DRY_RUN=true ;;
32
34
  -h | --help) sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
33
35
  *) err_t install.unknown_arg "$1"; exit 1 ;;
34
36
  esac
@@ -50,6 +52,67 @@ name="$(node -p 'require("./package.json").name')"
50
52
  # les doublons, y compris celui d'une tentative interrompue) ; tous révoqués à la fin
51
53
  TOKEN_PREFIX="repowarden-ci-${REPO##*/}"
52
54
 
55
+ # Vérifie si une relation de confiance correspond aux paramètres attendus.
56
+ # Affiche l'écart si --dry-run ou pour confirmation.
57
+ # Retourne : 0 = match, 1 = pas de relation, 2 = relation différente
58
+ verifier_trust() {
59
+ local json attendu_repo="$REPO" attendu_file="$WORKFLOW" attendu_env="$ENVIRONMENT"
60
+ json="$(npm trust list "$name" --json 2>/dev/null || echo '[]')"
61
+ if [ "$json" = "[]" ] || [ -z "$json" ]; then
62
+ return 1
63
+ fi
64
+ local match=0 id repo file env allow_publish
65
+ while IFS=$'\t' read -r id repo file env allow_publish; do
66
+ [ -n "$id" ] || continue
67
+ if [ "$repo" = "$attendu_repo" ] && [ "$file" = "$attendu_file" ] && [ "$env" = "$attendu_env" ] && [ "$allow_publish" = "true" ]; then
68
+ match=1
69
+ break
70
+ fi
71
+ done < <(node -e "
72
+ const data = $json;
73
+ for (const t of data) {
74
+ const id = t.id || t.key;
75
+ const repo = t.repository || t.repo || '';
76
+ const file = t.file || t.workflow || '';
77
+ const env = t.environment || t.env || '';
78
+ const allowPublish = t.allowPublish === true || t['allow-publish'] === true;
79
+ console.log(id + '\t' + repo + '\t' + file + '\t' + env + '\t' + allowPublish);
80
+ }
81
+ ")
82
+ if [ "$match" = 1 ]; then
83
+ return 0
84
+ fi
85
+ return 2
86
+ }
87
+
88
+ # Affiche les détails d'une relation de confiance pour diagnostic
89
+ afficher_trust() {
90
+ local json
91
+ json="$(npm trust list "$name" --json 2>/dev/null || echo '[]')"
92
+ if [ "$json" = "[]" ] || [ -z "$json" ]; then
93
+ echo " (aucune relation de confiance)"
94
+ return
95
+ fi
96
+ node -e "
97
+ const data = $json;
98
+ for (const t of data) {
99
+ const id = t.id || t.key;
100
+ const repo = t.repository || t.repo || '(inconnu)';
101
+ const file = t.file || t.workflow || '(inconnu)';
102
+ const env = t.environment || t.env || '(aucun)';
103
+ const allowPublish = t.allowPublish === true || t['allow-publish'] === true;
104
+ const allowStagePublish = t.allowStagePublish === true || t['allow-stage-publish'] === true;
105
+ console.log(' ID: ' + id);
106
+ console.log(' Dépôt: ' + repo);
107
+ console.log(' Workflow: ' + file);
108
+ console.log(' Environnement: ' + env);
109
+ console.log(' npm publish: ' + (allowPublish ? 'oui' : 'non'));
110
+ console.log(' npm stage publish: ' + (allowStagePublish ? 'oui' : 'non'));
111
+ console.log('');
112
+ }
113
+ "
114
+ }
115
+
53
116
  brand "npm · $name · $REPO"
54
117
 
55
118
  # 1. Connexion npm : entre toi et npm (navigateur ou mot de passe)
@@ -127,16 +190,82 @@ fi
127
190
 
128
191
  # 3. Publication de confiance, puis plus aucun jeton
129
192
  step_t npm.step_trust
130
- if grep -q '"id"' <<<"$(npm trust list "$name" --json 2>/dev/null || true)"; then
193
+ verifier_trust
194
+ cas=$?
195
+ if [ "$cas" = 0 ]; then
131
196
  ok_t npm.trust_exists
197
+ elif [ "$cas" = 1 ]; then
198
+ # Aucune relation : créer
199
+ if [ "$DRY_RUN" = true ]; then
200
+ info_t npm.trust_would_create "$REPO" "$WORKFLOW" "${ENVIRONMENT:-(aucun)}"
201
+ else
202
+ npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
203
+ ok_t npm.trust_set "$REPO" "$WORKFLOW"
204
+ fi
132
205
  else
133
- npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
134
- ok_t npm.trust_set "$REPO" "$WORKFLOW"
206
+ # Relation différente : afficher l'écart, demander confirmation, corriger sans perte
207
+ attention_t npm.trust_mismatch
208
+ afficher_trust
209
+ echo " Attendu :"
210
+ echo " Dépôt: $REPO"
211
+ echo " Workflow: $WORKFLOW"
212
+ echo " Environnement: ${ENVIRONMENT:-(aucun)}"
213
+ echo " npm publish: oui"
214
+ echo ""
215
+ if [ "$DRY_RUN" = true ]; then
216
+ info_t npm.trust_would_fix
217
+ else
218
+ # Confirmation avant changement
219
+ if [ -t 0 ]; then
220
+ t npm.trust_confirm
221
+ read -r -p " $REPLY" rep
222
+ echo
223
+ [[ "$rep" =~ ^[OoYy]$ ]] || { err_t npm.trust_cancelled; exit 1; }
224
+ fi
225
+ # Créer la nouvelle relation AVANT de révoquer l'ancienne (pas de perte)
226
+ npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
227
+ ok_t npm.trust_set "$REPO" "$WORKFLOW"
228
+ # Révoquer l'ancienne relation
229
+ json="$(npm trust list "$name" --json 2>/dev/null || echo '[]')"
230
+ old_id="$(node -e "
231
+ const data = $json;
232
+ for (const t of data) {
233
+ const id = t.id || t.key;
234
+ const repo = t.repository || t.repo || '';
235
+ const file = t.file || t.workflow || '';
236
+ const env = t.environment || t.env || '';
237
+ const allowPublish = t.allowPublish === true || t['allow-publish'] === true;
238
+ if (repo !== '$REPO' || file !== '$WORKFLOW' || env !== '$ENVIRONMENT' || allowPublish !== true) {
239
+ console.log(id);
240
+ }
241
+ }
242
+ ")"
243
+ for id in $old_id; do
244
+ [ -n "$id" ] && npm trust revoke "$name" --id "$id" --yes && ok_t npm.trust_revoked "$id"
245
+ done
246
+ fi
135
247
  fi
136
248
  gh variable set REPOWARDEN_NPM --repo "$REPO" --body true
137
249
  if grep -q '^NPM_TOKEN' <<<"$(gh secret list --repo "$REPO")"; then
138
- gh secret delete NPM_TOKEN --repo "$REPO"
139
- ok_t npm.token_removed
250
+ if [ "$cas" = 0 ] || [ "$cas" = 2 ]; then
251
+ # Relation de confiance OK (ou corrigée) : signaler le token et proposer suppression
252
+ attention_t npm.token_present
253
+ if [ "$DRY_RUN" = true ]; then
254
+ info_t npm.token_would_remove
255
+ elif [ -t 0 ]; then
256
+ t npm.token_remove_confirm
257
+ read -r -p " $REPLY" rep
258
+ echo
259
+ [[ "$rep" =~ ^[OoYy]$ ]] && gh secret delete NPM_TOKEN --repo "$REPO" && ok_t npm.token_removed
260
+ else
261
+ gh secret delete NPM_TOKEN --repo "$REPO"
262
+ ok_t npm.token_removed
263
+ fi
264
+ else
265
+ # Pas de relation de confiance : supprimer silencieusement (comportement existant)
266
+ gh secret delete NPM_TOKEN --repo "$REPO"
267
+ ok_t npm.token_removed
268
+ fi
140
269
  fi
141
270
  # Jetons temporaires révoqués sur npm (retrouvés par leur préfixe). npm peut
142
271
  # demander un code 2FA : la commande garde le terminal (questions visibles)
package/bin/proteger CHANGED
@@ -308,6 +308,60 @@ sig_attendue() {
308
308
  "$(sed -n 's/.*"require_last_push_approval": \([a-z]*\).*/\1/p' <<<"$r")" \
309
309
  "$(sed -n 's/.*"require_code_owner_review": \([a-z]*\).*/\1/p' <<<"$r")" "$deps" "$checks"
310
310
  }
311
+ # Anciens rulesets : rulesets de branche du dépôt, sous un autre nom que ceux
312
+ # de repowarden, qui exigent encore la vérification « repogarde » (retirée en
313
+ # v4) ; posés à la main ou par une ancienne version. Laissés à côté, ils
314
+ # bloqueraient les branches pour toujours. Une ligne par ruleset dans ANCIENS :
315
+ # id, nom, branches couvertes (~DEFAULT_BRANCH résolu : la branche par défaut
316
+ # change ensuite dans le flux develop), séparés par des tabulations.
317
+ # shellcheck disable=SC2016 # expressions jq (gh --jq), pas du shell
318
+ anciens_rulesets() {
319
+ local ids id ligne defaut=""
320
+ ANCIENS=""
321
+ ids="$(gh api "repos/$REPO/rulesets" --jq '.[] | select(.target == "branch"
322
+ and (.source_type // "Repository") == "Repository"
323
+ and (.name | test("^(repowarden|repogarde)( \\(.*\\))?$") | not)) | .id')" || return 0
324
+ for id in $ids; do
325
+ ligne="$(gh api "repos/$REPO/rulesets/$id" --jq 'select([.rules[]? | select(.type == "required_status_checks")
326
+ | .parameters.required_status_checks[].context] | index("repogarde"))
327
+ | "\(.id)\t\(.name)\t\(.conditions.ref_name.include | join(" "))"')" || continue
328
+ [ -n "$ligne" ] || continue
329
+ if [[ "$ligne" == *"~DEFAULT_BRANCH"* ]]; then
330
+ [ -n "$defaut" ] || defaut="$(gh api "repos/$REPO" --jq .default_branch)"
331
+ ligne="${ligne//\~DEFAULT_BRANCH/refs/heads/$defaut}"
332
+ fi
333
+ ANCIENS="$ANCIENS$ligne"$'\n'
334
+ done
335
+ }
336
+
337
+ # Ancien ruleset (ligne de ANCIENS) couvrant une des branches du ruleset $2 (JSON)
338
+ couvre() {
339
+ local motifs m b
340
+ read -ra motifs <<<"$(cut -f3 <<<"$1")"
341
+ for m in ${motifs[@]+"${motifs[@]}"}; do
342
+ [ "$m" != "~ALL" ] || return 0
343
+ for b in $(grep -o '"refs/heads/[^"]*"' <<<"$2" | tr -d '"'); do
344
+ # shellcheck disable=SC2053 # motif de GitHub (refs/heads/release/*)
345
+ [[ "$b" != $m ]] || return 0
346
+ done
347
+ done
348
+ return 1
349
+ }
350
+
351
+ # Réponse d'erreur de GitHub (JSON) → « message : erreurs », lisible
352
+ erreur_github() {
353
+ local message="" erreurs="" re_message='"message": *"([^"]*)"' re_erreurs='"errors": *\[([^]]*)\]'
354
+ [[ ! "$1" =~ $re_message ]] || message="${BASH_REMATCH[1]}"
355
+ [[ ! "$1" =~ $re_erreurs ]] || erreurs="${BASH_REMATCH[1]//\"/}"
356
+ if [ -n "$message" ]; then
357
+ REPLY="$message${erreurs:+ : $erreurs}"
358
+ else
359
+ REPLY="$(tr '\n' ' ' <<<"$1")"
360
+ fi
361
+ }
362
+
363
+ anciens_rulesets
364
+
311
365
  # shellcheck disable=SC2016 # expression jq (gh --jq), pas du shell
312
366
  SIG_EN_LIGNE='(.rules | map(select(.type == "pull_request"))[0].parameters) as $p
313
367
  | "approbations=\($p.required_approving_review_count) dernier_push=\($p.require_last_push_approval) codeowners=\($p.require_code_owner_review) deploiement=\([.rules[] | select(.type == "required_deployments") | .parameters.required_deployment_environments[]] | join(",")) verifications=\([.rules[] | select(.type == "required_status_checks") | .parameters.required_status_checks[].context] | sort | join(","))"'
@@ -335,6 +389,11 @@ if [ -n "$VERIFY" ]; then
335
389
  ecarts=$((ecarts + 1))
336
390
  fi
337
391
  done
392
+ while IFS=$'\t' read -r id name _; do
393
+ [ -n "$id" ] || continue
394
+ err_t ci.proteger.verify_legacy "$name" "$id"
395
+ ecarts=$((ecarts + 1))
396
+ done <<<"$ANCIENS"
338
397
  [ "$ecarts" = 0 ] || { err_t ci.proteger.verify_fix; exit 1; }
339
398
  ok_t ci.proteger.verify_ok
340
399
  exit 0
@@ -361,6 +420,7 @@ gh api -X PATCH "repos/$REPO" --silent "${settings[@]}"
361
420
  gh api -X PUT "repos/$REPO/actions/permissions/workflow" --silent \
362
421
  -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true
363
422
 
423
+ echecs=0
364
424
  for ruleset in "${rulesets[@]}"; do
365
425
  name="$(sed -n 's/^ "name": "\(.*\)",$/\1/p' <<<"$ruleset")"
366
426
  # Ruleset posé sous l'ancien nom (repogarde, v3) : repris et renommé, sinon
@@ -368,14 +428,45 @@ for ruleset in "${rulesets[@]}"; do
368
428
  old="${name/repowarden/repogarde}"
369
429
  id="$(gh api "repos/$REPO/rulesets" \
370
430
  --jq "([.[] | select(.name == \"$name\")] + [.[] | select(.name == \"$old\")]) | .[0].id // empty")"
431
+ # Sinon, un ancien ruleset sous un autre nom couvrant ses branches : repris
432
+ # (mis à jour en place, jamais un instant sans protection)
433
+ repris=""
434
+ if [ -z "$id" ] && grep -q '"target": "branch"' <<<"$ruleset"; then
435
+ while IFS= read -r ligne; do
436
+ { [ -n "$ligne" ] && couvre "$ligne" "$ruleset"; } || continue
437
+ id="${ligne%%$'\t'*}"
438
+ repris="$(cut -f2 <<<"$ligne")"
439
+ ANCIENS="$(grep -v "^$id"$'\t' <<<"$ANCIENS" || true)"
440
+ break
441
+ done <<<"$ANCIENS"
442
+ fi
371
443
  if [ -n "$id" ]; then
372
- gh api -X PUT "repos/$REPO/rulesets/$id" --input - <<<"$ruleset" >/dev/null
373
- ok_t ci.proteger.10 "$name"
444
+ methode=PUT chemin="repos/$REPO/rulesets/$id"
445
+ else
446
+ methode=POST chemin="repos/$REPO/rulesets"
447
+ fi
448
+ # Réponse gardée : en cas de refus, le détail de GitHub est affiché
449
+ if reponse="$(gh api -X "$methode" "$chemin" --input - <<<"$ruleset" 2>&1)"; then
450
+ if [ -n "$repris" ]; then
451
+ ok_t ci.proteger.legacy_taken "$name" "$repris"
452
+ elif [ -n "$id" ]; then
453
+ ok_t ci.proteger.10 "$name"
454
+ else
455
+ ok_t ci.proteger.11 "$name"
456
+ fi
374
457
  else
375
- gh api -X POST "repos/$REPO/rulesets" --input - <<<"$ruleset" >/dev/null
376
- ok_t ci.proteger.11 "$name"
458
+ erreur_github "$reponse"
459
+ err_t ci.proteger.api_error "$name" "$REPLY"
460
+ echecs=$((echecs + 1))
377
461
  fi
378
462
  done
463
+ # Anciens rulesets non repris (plusieurs sur les mêmes branches) : jamais
464
+ # supprimés ici, la marche à suivre est donnée
465
+ while IFS=$'\t' read -r id name _; do
466
+ [ -n "$id" ] || continue
467
+ err_t ci.proteger.legacy_left "$name" "$id" "$REPO" "$id"
468
+ echecs=$((echecs + 1))
469
+ done <<<"$ANCIENS"
379
470
  if [ -n "$env_json" ]; then
380
471
  gh api -X PUT "repos/$REPO/environments/$ENVIRONMENT" --input - --silent <<<"$env_json"
381
472
  policies="$(gh api "repos/$REPO/environments/$ENVIRONMENT/deployment-branch-policies" \
@@ -389,4 +480,5 @@ if [ -n "$env_json" ]; then
389
480
  fi
390
481
  [ -z "$urgent" ] || gh label create urgent -R "$REPO" --color B60205 \
391
482
  --description "Livraison immédiate après merge" >/dev/null
483
+ [ "$echecs" = 0 ] || { err_t ci.proteger.incomplete; exit 1; }
392
484
  ok_t ci.proteger.13
@@ -301,6 +301,11 @@ _msg_en() {
301
301
  ci.proteger.tags_none) _M="not protected (--sans-tags)" ;;
302
302
  ci.proteger.unknown_team) _M="Unknown or inaccessible team: %s (format @organisation/team)" ;;
303
303
  ci.proteger.unknown_user) _M="Unknown user: %s" ;;
304
+ ci.proteger.legacy_taken) _M="Ruleset \"%s\": old ruleset \"%s\" taken over (repogarde check removed)." ;;
305
+ ci.proteger.legacy_left) _M="Old ruleset \"%s\" (id %s): still requires the repogarde check, removed in v4, and blocks its branches. Delete it: Settings → Rules → Rulesets, or gh api -X DELETE repos/%s/rulesets/%s." ;;
306
+ ci.proteger.verify_legacy) _M="%s (id %s): old ruleset, still requires the repogarde check (removed in v4)" ;;
307
+ ci.proteger.api_error) _M="Ruleset \"%s\" rejected by GitHub: %s" ;;
308
+ ci.proteger.incomplete) _M="Incomplete protection: see the errors above." ;;
304
309
  ci.version.usage) _M="Usage: ci/version.sh next|notes [ref]" ;;
305
310
  ci.version.breaking) _M="Breaking changes" ;;
306
311
  ci.version.features) _M="Features" ;;
@@ -360,7 +365,16 @@ _msg_en() {
360
365
  npm.first_published) _M="%s@%s published by the pipeline." ;;
361
366
  npm.step_trust) _M="3/3 Tokenless publishing (trusted publishing)" ;;
362
367
  npm.trust_exists) _M="Trusted publishing already configured." ;;
368
+ npm.trust_mismatch) _M="Existing trust relationship does not match expected parameters:" ;;
369
+ npm.trust_confirm) _M="Confirm fix (new relationship created before revoking old)? [y/N] " ;;
370
+ npm.trust_cancelled) _M="Fix cancelled." ;;
371
+ npm.trust_would_create) _M="[dry-run] Trust relationship would be created: %s, workflow %s, env %s" ;;
372
+ npm.trust_would_fix) _M="[dry-run] Trust relationship would be fixed (new created, old revoked)" ;;
373
+ npm.trust_revoked) _M="Old trust relationship revoked: %s" ;;
363
374
  npm.trust_set) _M="Trusted publishing: %s, workflow %s." ;;
375
+ npm.token_present) _M="NPM_TOKEN secret still present while trusted publishing works." ;;
376
+ npm.token_remove_confirm) _M="Remove NPM_TOKEN secret? [y/N] " ;;
377
+ npm.token_would_remove) _M="[dry-run] NPM_TOKEN secret would be removed" ;;
364
378
  npm.token_removed) _M="NPM_TOKEN secret removed from the repository." ;;
365
379
  npm.token_revoked) _M="Temporary token revoked on npm." ;;
366
380
  npm.done) _M="Done: every release publishes %s to npm, without a token." ;;
@@ -301,6 +301,11 @@ _msg_fr() {
301
301
  ci.proteger.tags_none) _M="non protégés (--sans-tags)" ;;
302
302
  ci.proteger.unknown_team) _M="Équipe inconnue ou inaccessible : %s (format @organisation/equipe)" ;;
303
303
  ci.proteger.unknown_user) _M="Utilisateur inconnu : %s" ;;
304
+ ci.proteger.legacy_taken) _M="Ruleset « %s » : ancien ruleset « %s » repris (vérification repogarde retirée)." ;;
305
+ ci.proteger.legacy_left) _M="Ancien ruleset « %s » (id %s) : exige encore la vérification repogarde, retirée en v4, et bloque ses branches. Le supprimer : Settings → Rules → Rulesets, ou gh api -X DELETE repos/%s/rulesets/%s." ;;
306
+ ci.proteger.verify_legacy) _M="%s (id %s) : ancien ruleset, exige encore la vérification repogarde (retirée en v4)" ;;
307
+ ci.proteger.api_error) _M="Ruleset « %s » refusé par GitHub : %s" ;;
308
+ ci.proteger.incomplete) _M="Protection incomplète : voir les erreurs ci-dessus." ;;
304
309
  ci.version.usage) _M="Usage : ci/version.sh next|notes [ref]" ;;
305
310
  ci.version.breaking) _M="Changements incompatibles" ;;
306
311
  ci.version.features) _M="Fonctionnalités" ;;
@@ -360,7 +365,16 @@ _msg_fr() {
360
365
  npm.first_published) _M="%s@%s publié par la pipeline." ;;
361
366
  npm.step_trust) _M="3/3 Publication sans jeton (publication de confiance)" ;;
362
367
  npm.trust_exists) _M="Publication de confiance déjà configurée." ;;
368
+ npm.trust_mismatch) _M="Relation de confiance existante mais ne correspond pas aux paramètres attendus :" ;;
369
+ npm.trust_confirm) _M="Confirmer la correction (nouvelle relation créée avant révocation de l'ancienne) ? [o/N] " ;;
370
+ npm.trust_cancelled) _M="Correction annulée." ;;
371
+ npm.trust_would_create) _M="[dry-run] Relation de confiance serait créée : %s, workflow %s, env %s" ;;
372
+ npm.trust_would_fix) _M="[dry-run] Relation de confiance serait corrigée (nouvelle créée, ancienne révoquée)" ;;
373
+ npm.trust_revoked) _M="Ancienne relation de confiance révoquée : %s" ;;
363
374
  npm.trust_set) _M="Publication de confiance : %s, workflow %s." ;;
375
+ npm.token_present) _M="Secret NPM_TOKEN encore présent alors que la publication de confiance fonctionne." ;;
376
+ npm.token_remove_confirm) _M="Supprimer le secret NPM_TOKEN ? [o/N] " ;;
377
+ npm.token_would_remove) _M="[dry-run] Secret NPM_TOKEN serait supprimé" ;;
364
378
  npm.token_removed) _M="Secret NPM_TOKEN supprimé du dépôt." ;;
365
379
  npm.token_revoked) _M="Jeton temporaire révoqué sur npm." ;;
366
380
  npm.done) _M="Terminé : chaque release publie %s sur npm, sans jeton." ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowarden",
3
- "version": "4.7.1",
3
+ "version": "4.7.2-next.6",
4
4
  "description": "Hooks git et CI multi-langages : messages de commit, nommage des branches, secrets, formatage, tests ciblés, code mort, releases.",
5
5
  "keywords": [
6
6
  "git-hooks",