repowarden 0.0.0-stage → 4.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -2
  3. package/bin/code-mort +33 -0
  4. package/bin/commit +412 -0
  5. package/bin/npm-publication +151 -0
  6. package/bin/proteger +283 -0
  7. package/bin/repogarde +20 -0
  8. package/bin/repowarden +63 -0
  9. package/bin/statut +100 -0
  10. package/bin/ticket +129 -0
  11. package/bin/tickets +135 -0
  12. package/hooks/commit-msg +64 -0
  13. package/hooks/lang/actions.sh +9 -0
  14. package/hooks/lang/ansible.sh +18 -0
  15. package/hooks/lang/cpp.sh +22 -0
  16. package/hooks/lang/dart.sh +20 -0
  17. package/hooks/lang/deno.sh +15 -0
  18. package/hooks/lang/docker.sh +26 -0
  19. package/hooks/lang/dotnet.sh +15 -0
  20. package/hooks/lang/elixir.sh +13 -0
  21. package/hooks/lang/go.sh +22 -0
  22. package/hooks/lang/gradle.sh +28 -0
  23. package/hooks/lang/helm.sh +28 -0
  24. package/hooks/lang/kubernetes.sh +29 -0
  25. package/hooks/lang/maven.sh +27 -0
  26. package/hooks/lang/node.sh +75 -0
  27. package/hooks/lang/packer.sh +16 -0
  28. package/hooks/lang/php.sh +20 -0
  29. package/hooks/lang/python.sh +79 -0
  30. package/hooks/lang/ruby.sh +24 -0
  31. package/hooks/lang/rust.sh +17 -0
  32. package/hooks/lang/shell.sh +9 -0
  33. package/hooks/lang/swift.sh +20 -0
  34. package/hooks/lang/tasks.sh +16 -0
  35. package/hooks/lang/terraform.sh +29 -0
  36. package/hooks/lib/common.sh +393 -0
  37. package/hooks/lib/compat.sh +24 -0
  38. package/hooks/lib/deadcode.sh +153 -0
  39. package/hooks/lib/forge.sh +39 -0
  40. package/hooks/lib/i18n/en.sh +393 -0
  41. package/hooks/lib/i18n/fr.sh +393 -0
  42. package/hooks/lib/i18n.sh +81 -0
  43. package/hooks/lib/lang.sh +338 -0
  44. package/hooks/lib/safety.sh +108 -0
  45. package/hooks/lib/tickets.sh +184 -0
  46. package/hooks/lib/ui.sh +40 -0
  47. package/hooks/post-checkout +26 -0
  48. package/hooks/post-merge +47 -0
  49. package/hooks/pre-commit +58 -0
  50. package/hooks/pre-push +91 -0
  51. package/hooks/prepare-commit-msg +48 -0
  52. package/install.sh +245 -0
  53. package/package.json +31 -4
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # Publication npm automatique d'un dépôt, mise en place en une commande :
3
+ # chaque release publie le paquet (workflow npm-publish.yml), sans jeton : npm
4
+ # vérifie que la publication vient de ce dépôt et de release.yml (publication
5
+ # de confiance, OIDC) et joint la provenance au paquet.
6
+ #
7
+ # bin/npm-publication [--repo propriétaire/nom] [--workflow release.yml]
8
+ # [--environnement NOM]
9
+ #
10
+ # 1. connexion npm (npm login si besoin) ;
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.
18
+ set -euo pipefail
19
+
20
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
21
+ # shellcheck source=../hooks/lib/common.sh
22
+ source "$ROOT/hooks/lib/common.sh"
23
+
24
+ REPO=""
25
+ WORKFLOW="release.yml"
26
+ ENVIRONMENT=""
27
+ while [ $# -gt 0 ]; do
28
+ case "$1" in
29
+ --repo) shift; REPO="${1:?--repo attend propriétaire/nom}" ;;
30
+ --workflow) shift; WORKFLOW="${1:?--workflow attend un fichier}" ;;
31
+ --environnement | --environment) shift; ENVIRONMENT="${1:?--environnement attend un nom}" ;;
32
+ -h | --help) sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
33
+ *) err_t install.unknown_arg "$1"; exit 1 ;;
34
+ esac
35
+ shift
36
+ done
37
+
38
+ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
39
+ command -v npm >/dev/null || { err_t npm.no_npm; exit 1; }
40
+ # npm trust : npm 11.15 au minimum
41
+ if ! node -e 'const [a,b]=process.argv[1].split(".").map(Number);process.exit(a>11||(a==11&&b>=15)?0:1)' "$(npm --version)"; then
42
+ err_t npm.old_npm "$(npm --version)"
43
+ exit 1
44
+ fi
45
+ cd "$GIT_TOPLEVEL"
46
+ [ -f package.json ] || { err_t npm.no_package; exit 1; }
47
+ [ -n "$REPO" ] || REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
48
+ name="$(node -p 'require("./package.json").name')"
49
+ # Préfixe des jetons temporaires : nom unique à chaque création (npm refuse
50
+ # les doublons, y compris celui d'une tentative interrompue) ; tous révoqués à la fin
51
+ TOKEN_PREFIX="repowarden-ci-${REPO##*/}"
52
+
53
+ brand "npm · $name · $REPO"
54
+
55
+ # 1. Connexion npm : entre toi et npm (navigateur ou mot de passe)
56
+ step_t npm.step_login
57
+ npm whoami >/dev/null 2>&1 || npm login
58
+ ok_t npm.logged "$(npm whoami)"
59
+ # Créer un jeton et configurer la publication de confiance exigent la 2FA du
60
+ # compte (les codes envoyés par e-mail ne sont qu'une vérification de connexion)
61
+ 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
62
+ err_t npm.no_2fa
63
+ dim_t npm.hint_2fa "$(npm whoami)"
64
+ exit 1
65
+ fi
66
+
67
+ publie() { [ -n "$(npm view "$name" name 2>/dev/null || true)" ]; }
68
+
69
+ # 2. Première publication, faite par la pipeline avec un jeton temporaire
70
+ if ! publie; then
71
+ step_t npm.step_token
72
+ if grep -q '^NPM_TOKEN' <<<"$(gh secret list --repo "$REPO")"; then
73
+ ok_t npm.token_exists
74
+ else
75
+ # npm écrit ses questions et le jeton au même endroit : les questions
76
+ # sont posées ici (saisie masquée) et le jeton va droit au secret
77
+ if [[ "$name" == @*/* ]]; then portee=(--scopes "${name%%/*}"); else portee=(--packages-all); fi
78
+ t npm.ask_password
79
+ read -r -s -p " $REPLY" mdp
80
+ echo
81
+ otp="" sortie=""
82
+ for essai in 1 2 3 4; do
83
+ # Sortie texte, pas --json : npm y masque le jeton (« npm_*** »)
84
+ if sortie="$(npm_config_password="$mdp" npm_config_otp="$otp" npm token create \
85
+ --name "$TOKEN_PREFIX-$(date +%Y%m%d%H%M%S)" --expires 7 "${portee[@]}" \
86
+ --packages-and-scopes-permission read-write --bypass-2fa </dev/null 2>&1)"; then
87
+ break
88
+ fi
89
+ # Code à usage unique (application ou e-mail) demandé : on le pose
90
+ if [ "$essai" -lt 4 ] && grep -qiE 'otp|one-time|two-factor' <<<"$sortie"; then
91
+ t npm.ask_otp
92
+ read -r -p " $REPLY" otp
93
+ continue
94
+ fi
95
+ sed -E 's/npm_[A-Za-z0-9]+/npm_***/g' <<<"$sortie" >&2
96
+ mdp=""
97
+ err_t npm.token_failed
98
+ exit 1
99
+ done
100
+ mdp=""
101
+ jeton="$(grep -oE 'npm_[A-Za-z0-9]+' <<<"$sortie" | sed -n 1p || true)"
102
+ sortie=""
103
+ [ -n "$jeton" ] || { err_t npm.token_failed; exit 1; }
104
+ printf '%s' "$jeton" | gh secret set NPM_TOKEN --repo "$REPO"
105
+ jeton=""
106
+ ok_t npm.token_stored
107
+ fi
108
+ gh variable set REPOWARDEN_NPM --repo "$REPO" --body true
109
+
110
+ # Release déjà publiée : la pipeline est lancée tout de suite (elle publie
111
+ # la dernière release absente de npm) ; sinon, la prochaine release publie
112
+ if [ -n "$(git ls-remote --tags origin 'v[0-9]*')" ]; then
113
+ branche="$(gh repo view "$REPO" --json defaultBranchRef -q .defaultBranchRef.name)"
114
+ gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$branche"
115
+ info_t npm.dispatched "$WORKFLOW"
116
+ else
117
+ info_t npm.waiting "$name"
118
+ fi
119
+ for i in $(seq 1 360); do
120
+ publie && break
121
+ [ $((i % 10)) -ne 0 ] || dim_t npm.still_waiting "$((i / 2))"
122
+ sleep 30
123
+ done
124
+ publie || { attention_t npm.not_yet; exit 0; }
125
+ ok_t npm.first_published "$name" "$(npm view "$name" version)"
126
+ fi
127
+
128
+ # 3. Publication de confiance, puis plus aucun jeton
129
+ step_t npm.step_trust
130
+ if grep -q '"id"' <<<"$(npm trust list "$name" --json 2>/dev/null || true)"; then
131
+ ok_t npm.trust_exists
132
+ else
133
+ npm trust github "$name" --file "$WORKFLOW" --repo "$REPO" ${ENVIRONMENT:+--env "$ENVIRONMENT"} --allow-publish --yes
134
+ ok_t npm.trust_set "$REPO" "$WORKFLOW"
135
+ fi
136
+ gh variable set REPOWARDEN_NPM --repo "$REPO" --body true
137
+ 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
140
+ fi
141
+ # Jetons temporaires révoqués sur npm (retrouvés par leur préfixe). npm peut
142
+ # demander un code 2FA : la commande garde le terminal (questions visibles)
143
+ ids="$({ npm token list --json 2>/dev/null || true; } |
144
+ TOKEN_PREFIX="$TOKEN_PREFIX" node -e '
145
+ let l = []; try { l = JSON.parse(require("fs").readFileSync(0, "utf8")); } catch {}
146
+ const p = process.env.TOKEN_PREFIX;
147
+ for (const t of [].concat(l)) if (t && typeof t.name === "string" && (t.name === p || t.name.startsWith(p + "-"))) console.log(t.id || t.key);')"
148
+ for id in $ids; do
149
+ npm token revoke "$id" && ok_t npm.token_revoked
150
+ done
151
+ ok_t npm.done "$name"
package/bin/proteger ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env bash
2
+ # Protège le dépôt GitHub courant d'après .repowarden.conf (relançable sans risque) :
3
+ # - branches protectedBranches : PR obligatoire, vérifications exigées à jour,
4
+ # ni suppression ni push forcé (ruleset « repowarden ») ;
5
+ # - merges : squash ; avec integrationBranch (flux develop), merge commit sur
6
+ # main et branche par défaut develop ; titre de PR = message du commit ;
7
+ # - tags v* : créés, déplacés ou supprimés uniquement par les workflows ;
8
+ # - GitHub Actions autorisé à créer des PR (releases automatiques) ;
9
+ # - validation humaine (optionnelle) : approbations obligatoires, revue des
10
+ # CODEOWNERS, environnement de déploiement approuvé par des personnes.
11
+ #
12
+ # bin/proteger [--repo propriétaire/nom] [--checks "repowarden,tests (ubuntu-latest)"] [--relecteurs N]
13
+ # [--codeowners] [--environnement NOM] [--approbateurs "login …"]
14
+ # [--sans-tags] [--dry-run]
15
+ # --sans-tags : dépôt qui déplace lui-même un tag (tag majeur flottant v2…).
16
+ # Réglages équivalents dans .repowarden.conf (requiredReviews, codeOwnerReview,
17
+ # environment, environmentReviewers). Demande un gh connecté, admin du dépôt.
18
+ set -euo pipefail
19
+
20
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
21
+ # shellcheck source=../hooks/lib/common.sh
22
+ source "$ROOT/hooks/lib/common.sh"
23
+
24
+ cfg_r requiredReviews 0
25
+ REVIEWS="$REPLY"
26
+ cfg_r codeOwnerReview false
27
+ CODEOWNERS="$REPLY"
28
+ cfg_r environment ""
29
+ ENVIRONMENT="$REPLY"
30
+ cfg_r environmentReviewers ""
31
+ APPROVERS="$REPLY"
32
+ REPO=""
33
+ CHECKS="repowarden"
34
+ DRY_RUN=""
35
+ NO_TAGS=""
36
+ while [ $# -gt 0 ]; do
37
+ case "$1" in
38
+ --repo) shift; REPO="${1:?--repo attend propriétaire/nom}" ;;
39
+ --checks) shift; CHECKS="${1:?--checks attend une liste}" ;;
40
+ --relecteurs) shift; REVIEWS="${1:?--relecteurs attend un nombre}" ;;
41
+ --codeowners) CODEOWNERS=true ;;
42
+ --environnement) shift; ENVIRONMENT="${1:?--environnement attend un nom}" ;;
43
+ --approbateurs) shift; APPROVERS="${1:?--approbateurs attend des logins}" ;;
44
+ --sans-tags) NO_TAGS=1 ;;
45
+ --dry-run) DRY_RUN=1 ;;
46
+ -h | --help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
47
+ *) err_t install.unknown_arg "$1"; exit 1 ;;
48
+ esac
49
+ shift
50
+ done
51
+ [ -n "$REPO" ] || REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
52
+ [[ "$REVIEWS" =~ ^[0-9]$ ]] || { err_t ci.proteger.reviews_invalid; exit 1; }
53
+ [[ "$CODEOWNERS" =~ ^(true|false)$ ]] || { err_t ci.proteger.codeowners_invalid; exit 1; }
54
+ # Avec relecture : le dernier à pousser ne peut pas approuver son propre push
55
+ last_push=false
56
+ [ "$REVIEWS" = 0 ] || last_push=true
57
+
58
+ cfg_r protectedBranches "main master"
59
+ protected="$REPLY"
60
+ cfg_r integrationBranch ""
61
+ integration="$REPLY"
62
+
63
+ # Branches protégées existant sur GitHub (main ou master selon le dépôt)
64
+ existing=""
65
+ for b in $protected; do
66
+ # Nom renvoyé comparé : GitHub redirige une branche renommée (master → main)
67
+ [ "$(gh api "repos/$REPO/branches/$b" --jq .name 2>/dev/null)" = "$b" ] && existing="$existing $b"
68
+ done
69
+ [ -n "$existing" ] || { err_t ci.proteger.no_branch "$protected" "$REPO"; exit 1; }
70
+
71
+ # Vérifications séparées par des virgules : un nom de job peut contenir des
72
+ # espaces (« tests (ubuntu-latest) »)
73
+ contexts=""
74
+ IFS=, read -ra checks <<<"$CHECKS"
75
+ for c in "${checks[@]}"; do
76
+ c="${c#"${c%%[![:space:]]*}"}"
77
+ c="${c%"${c##*[![:space:]]}"}"
78
+ [ -n "$c" ] && contexts="$contexts {\"context\": \"$c\", \"integration_id\": 15368},"
79
+ done
80
+ contexts="${contexts%,}"
81
+
82
+ refs_json() { # branches → "refs/heads/a", "refs/heads/b"
83
+ local b out=""
84
+ for b in "$@"; do out="$out \"refs/heads/$b\","; done
85
+ echo "${out%,}"
86
+ }
87
+
88
+ # Ruleset de branches : $1 nom, $2 branches (liste), $3 méthodes de merge (JSON),
89
+ # $4 PR à jour exigée (défaut true), $5 approbations (défaut REVIEWS),
90
+ # $6 dernier push approuvé par un autre (défaut selon REVIEWS)
91
+ branch_ruleset() {
92
+ # shellcheck disable=SC2086 # liste de branches volontairement découpée
93
+ cat <<JSON
94
+ {
95
+ "name": "$1",
96
+ "target": "branch",
97
+ "enforcement": "active",
98
+ "bypass_actors": [],
99
+ "conditions": {"ref_name": {"include": [$(refs_json $2)], "exclude": []}},
100
+ "rules": [
101
+ {"type": "deletion"},
102
+ {"type": "non_fast_forward"},
103
+ {"type": "pull_request", "parameters": {
104
+ "allowed_merge_methods": [$3],
105
+ "dismiss_stale_reviews_on_push": true,
106
+ "require_code_owner_review": $CODEOWNERS,
107
+ "require_last_push_approval": ${6:-$last_push},
108
+ "required_approving_review_count": ${5:-$REVIEWS},
109
+ "required_review_thread_resolution": true}},
110
+ {"type": "required_status_checks", "parameters": {
111
+ "strict_required_status_checks_policy": ${4:-true},
112
+ "do_not_enforce_on_create": false,
113
+ "required_status_checks": [$contexts]}}
114
+ ]
115
+ }
116
+ JSON
117
+ }
118
+
119
+ # Tags de version v*. Organisation : posés uniquement par les workflows
120
+ # (GitHub Actions en exception). Compte personnel : GitHub n'accepte pas
121
+ # cette exception ; un tag publié ne peut alors être ni déplacé ni supprimé
122
+ # (exception : administrateurs, tracée), sa création restant libre.
123
+ tags_ruleset() { # $1 : Organization ou User
124
+ local bypass rules
125
+ if [ "$1" = Organization ]; then
126
+ bypass='{"actor_id": 15368, "actor_type": "Integration", "bypass_mode": "always"}'
127
+ rules='{"type": "creation"}, {"type": "update"}, {"type": "deletion"}'
128
+ else
129
+ bypass='{"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always"}'
130
+ rules='{"type": "update"}, {"type": "deletion"}'
131
+ fi
132
+ cat <<JSON
133
+ {
134
+ "name": "repowarden (tags)",
135
+ "target": "tag",
136
+ "enforcement": "active",
137
+ "bypass_actors": [$bypass],
138
+ "conditions": {"ref_name": {"include": ["refs/tags/v*"], "exclude": []}},
139
+ "rules": [$rules]
140
+ }
141
+ JSON
142
+ }
143
+
144
+ rulesets=()
145
+ if [ -n "$integration" ]; then
146
+ # main : livraisons (develop, release/…, hotfix/…) en merge commit, pour
147
+ # garder le détail des commits dans l'historique et les notes de version ;
148
+ # develop : travail en squash, merge commit pour release/… et hotfix/…
149
+ others=""
150
+ for b in $existing; do [ "$b" = "$integration" ] || others="$others $b"; done
151
+ # Livraison vers main = passage à une nouvelle version : approbation
152
+ # humaine obligatoire (la PR de livraison est ouverte par le workflow,
153
+ # le mainteneur peut donc l'approuver), sans exiger un autre approbateur
154
+ # que l'auteur du dernier push sauf si des relectures sont configurées
155
+ livraison=$((REVIEWS > 0 ? REVIEWS : 1))
156
+ # PR à jour non exigée non plus : le merge commit d'une livraison n'existe
157
+ # que sur main ; strict, la livraison suivante serait bloquée tant que
158
+ # develop ne l'a pas reçu (retour main → develop vide après chaque
159
+ # release). Une livraison n'apporte que develop, vérifiée par la CI ; le
160
+ # retour reste fait quand main a un contenu propre (correctif direct).
161
+ [ -z "$others" ] || rulesets+=("$(branch_ruleset repowarden "$others" '"merge"' false "$livraison" "$last_push")")
162
+ # Branche d'intégration : PR à jour non exigée. Le retour de main (après
163
+ # une release) serait sinon presque toujours en retard, et « mettre à jour »
164
+ # fusionnerait develop dans main en contournant la livraison ; la CI
165
+ # vérifie de toute façon chaque push sur develop.
166
+ [[ " $existing " != *" $integration "* ]] ||
167
+ rulesets+=("$(branch_ruleset "repowarden ($integration)" "$integration" '"squash", "merge"' false)")
168
+ merge_commit=true
169
+ # Nettoyage par le workflow nettoyage-branches (garde une branche encore
170
+ # source d'une autre PR ouverte)
171
+ delete_on_merge=false
172
+ # Merge automatique désactivé : aucune livraison programmée sans décision
173
+ auto_merge=false
174
+ # Sans ce workflow, les branches mergées ne seraient plus supprimées
175
+ if ! grep -qs "nettoyage-branches.yml" "$GIT_TOPLEVEL"/.github/workflows/*.y*ml; then
176
+ attention_t ci.proteger.no_cleanup
177
+ fi
178
+ t ci.proteger.methods_flow "$integration"
179
+ methods="$REPLY"
180
+ else
181
+ rulesets+=("$(branch_ruleset repowarden "$existing" '"squash"')")
182
+ merge_commit=false
183
+ auto_merge=true
184
+ delete_on_merge=true
185
+ methods="squash"
186
+ fi
187
+ owner_type="$(gh api "repos/$REPO" --jq .owner.type)"
188
+ if [ -z "$NO_TAGS" ]; then
189
+ rulesets+=("$(tags_ruleset "$owner_type")")
190
+ if [ "$owner_type" = Organization ]; then
191
+ t ci.proteger.tags_org
192
+ tags_msg="$REPLY"
193
+ else
194
+ t ci.proteger.tags_user
195
+ tags_msg="$REPLY"
196
+ fi
197
+ else
198
+ t ci.proteger.tags_none
199
+ tags_msg="$REPLY"
200
+ fi
201
+
202
+ echo_t ci.proteger.1 "$REPO"
203
+ echo_t ci.proteger.2 "$existing"
204
+ echo_t ci.proteger.3 "$CHECKS"
205
+ echo_t ci.proteger.4 "$methods" "$delete_on_merge"
206
+ [ -z "$integration" ] || echo_t ci.proteger.delivery "$livraison"
207
+ echo_t ci.proteger.5 "$tags_msg"
208
+ [ -z "$integration" ] || echo_t ci.proteger.6 "$integration"
209
+ echo_t ci.proteger.7 "$REVIEWS" "$CODEOWNERS"
210
+ if [ "$REVIEWS" != 0 ] || [ "$CODEOWNERS" = true ]; then
211
+ echo_t ci.proteger.8
212
+ fi
213
+
214
+ # Environnement de déploiement : chaque job « environment: NOM » attend
215
+ # l'approbation d'une des personnes désignées ; déploiement seulement depuis
216
+ # les branches protégées et les tags v*
217
+ env_json=""
218
+ if [ -n "$ENVIRONMENT" ]; then
219
+ [ -n "$APPROVERS" ] || APPROVERS="$(gh api user --jq .login)"
220
+ reviewers=""
221
+ # Personnes (login) ou équipes (@organisation/equipe : membres gérés dans GitHub)
222
+ for login in $APPROVERS; do
223
+ case "$login" in
224
+ */*)
225
+ team="${login#@}"
226
+ id="$(gh api "orgs/${team%%/*}/teams/${team#*/}" --jq .id)" ||
227
+ { err_t ci.proteger.unknown_team "$login"; exit 1; }
228
+ reviewers="$reviewers {\"type\": \"Team\", \"id\": $id},"
229
+ ;;
230
+ *)
231
+ id="$(gh api "users/${login#@}" --jq .id)" || { err_t ci.proteger.unknown_user "$login"; exit 1; }
232
+ reviewers="$reviewers {\"type\": \"User\", \"id\": $id},"
233
+ ;;
234
+ esac
235
+ done
236
+ # Seul approbateur = soi-même (projet solo) : l'auto-approbation reste permise
237
+ self_review=true
238
+ [ "$(wc -w <<<"$APPROVERS")" -gt 1 ] || self_review=false
239
+ env_json="{\"reviewers\": [${reviewers%,}], \"prevent_self_review\": $self_review,
240
+ \"deployment_branch_policy\": {\"protected_branches\": false, \"custom_branch_policies\": true}}"
241
+ echo_t ci.proteger.9 "$ENVIRONMENT" "$APPROVERS"
242
+ fi
243
+ if [ -n "$DRY_RUN" ]; then
244
+ printf '%s\n' "${rulesets[@]}"
245
+ [ -z "$env_json" ] || printf '%s\n' "$env_json"
246
+ exit 0
247
+ fi
248
+
249
+ settings=(-F allow_squash_merge=true -F allow_merge_commit="$merge_commit" -F allow_rebase_merge=false
250
+ -F delete_branch_on_merge="$delete_on_merge" -F allow_auto_merge="$auto_merge"
251
+ -f squash_merge_commit_title=PR_TITLE -f squash_merge_commit_message=PR_BODY)
252
+ [ -z "$integration" ] || settings+=(-f default_branch="$integration")
253
+ gh api -X PATCH "repos/$REPO" --silent "${settings[@]}"
254
+ gh api -X PUT "repos/$REPO/actions/permissions/workflow" --silent \
255
+ -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true
256
+
257
+ for ruleset in "${rulesets[@]}"; do
258
+ name="$(sed -n 's/^ "name": "\(.*\)",$/\1/p' <<<"$ruleset")"
259
+ # Ruleset posé sous l'ancien nom (repogarde, v3) : repris et renommé, sinon
260
+ # il resterait à côté et exigerait une vérification « repogarde » disparue
261
+ old="${name/repowarden/repogarde}"
262
+ id="$(gh api "repos/$REPO/rulesets" \
263
+ --jq "([.[] | select(.name == \"$name\")] + [.[] | select(.name == \"$old\")]) | .[0].id // empty")"
264
+ if [ -n "$id" ]; then
265
+ gh api -X PUT "repos/$REPO/rulesets/$id" --input - <<<"$ruleset" >/dev/null
266
+ ok_t ci.proteger.10 "$name"
267
+ else
268
+ gh api -X POST "repos/$REPO/rulesets" --input - <<<"$ruleset" >/dev/null
269
+ ok_t ci.proteger.11 "$name"
270
+ fi
271
+ done
272
+ if [ -n "$env_json" ]; then
273
+ gh api -X PUT "repos/$REPO/environments/$ENVIRONMENT" --input - --silent <<<"$env_json"
274
+ policies="$(gh api "repos/$REPO/environments/$ENVIRONMENT/deployment-branch-policies" \
275
+ --jq '.branch_policies[] | "\(.type):\(.name)"')"
276
+ for policy in $(for b in $existing; do echo "branch:$b"; done) "tag:v*"; do
277
+ grep -qxF "$policy" <<<"$policies" && continue
278
+ gh api -X POST "repos/$REPO/environments/$ENVIRONMENT/deployment-branch-policies" --silent \
279
+ -f type="${policy%%:*}" -f name="${policy#*:}"
280
+ done
281
+ ok_t ci.proteger.12 "$ENVIRONMENT"
282
+ fi
283
+ ok_t ci.proteger.13
package/bin/repogarde ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Ancien nom de la commande (repogarde, jusqu'à la v3) : relaie vers repowarden
3
+ # avec un avertissement. Retiré en v5.
4
+ set -euo pipefail
5
+
6
+ # Dossier réel du paquet : npm appelle ce script via un lien symbolique
7
+ src="${BASH_SOURCE[0]}"
8
+ while [ -L "$src" ]; do
9
+ dir="$(cd "$(dirname "$src")" && pwd -P)"
10
+ src="$(readlink "$src")"
11
+ case "$src" in /*) ;; *) src="$dir/$src" ;; esac
12
+ done
13
+ ROOT="$(cd "$(dirname "$src")/.." && pwd -P)"
14
+ # shellcheck source=../hooks/lib/ui.sh
15
+ source "$ROOT/hooks/lib/ui.sh"
16
+ # shellcheck source=../hooks/lib/i18n.sh
17
+ source "$ROOT/hooks/lib/i18n.sh"
18
+
19
+ attention_t compat.old_command
20
+ exec bash "$ROOT/bin/repowarden" "$@"
package/bin/repowarden ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # Point d'entrée unique de repowarden (paquet npm, ou clone du dépôt) :
3
+ # repowarden état du poste et étape suivante
4
+ # repowarden statut (idem)
5
+ # repowarden install [--global] [--lang fr|en] active les hooks (install.sh)
6
+ # repowarden uninstall [--global] [--purge] les retire (avant npm uninstall -g)
7
+ # repowarden cc assistant de commit
8
+ # repowarden code-mort nouveau code mort de la branche
9
+ # repowarden ticket <n> branche du ticket (créée, liée, si besoin)
10
+ # repowarden ticket nouveau "<titre>" nouveau ticket
11
+ # repowarden tickets init suivi des tickets (GitHub)
12
+ # repowarden proteger [options] protection des branches (GitHub)
13
+ # repowarden npm-publication publication npm automatique
14
+ # repowarden --version
15
+ set -euo pipefail
16
+
17
+ # Dossier réel du paquet : npm appelle ce script via un lien symbolique
18
+ # (node_modules/.bin, préfixe global) ; readlink -f absent de macOS
19
+ src="${BASH_SOURCE[0]}"
20
+ while [ -L "$src" ]; do
21
+ dir="$(cd "$(dirname "$src")" && pwd -P)"
22
+ src="$(readlink "$src")"
23
+ case "$src" in /*) ;; *) src="$dir/$src" ;; esac
24
+ done
25
+ ROOT="$(cd "$(dirname "$src")/.." && pwd -P)"
26
+ # shellcheck source=../hooks/lib/ui.sh
27
+ source "$ROOT/hooks/lib/ui.sh"
28
+ # shellcheck source=../hooks/lib/i18n.sh
29
+ source "$ROOT/hooks/lib/i18n.sh"
30
+
31
+ aide() { sed -n '2,14p' "$ROOT/bin/repowarden" | sed 's/^# \{0,1\}//'; }
32
+
33
+ commande="${1:-}"
34
+ [ $# -eq 0 ] || shift
35
+ case "$commande" in
36
+ install)
37
+ # npx : paquet dans un cache temporaire, effaçable à tout moment ;
38
+ # des hooks globaux y pointeraient vers un dossier disparu
39
+ if [[ " $* " == *" --global "* && "$ROOT" == */_npx/* ]]; then
40
+ err_t npm.npx_global
41
+ exit 1
42
+ fi
43
+ exec bash "$ROOT/install.sh" "$@"
44
+ ;;
45
+ uninstall) exec bash "$ROOT/install.sh" --uninstall "$@" ;;
46
+ cc | commit) exec bash "$ROOT/bin/commit" "$@" ;;
47
+ code-mort | deadcode) exec bash "$ROOT/bin/code-mort" "$@" ;;
48
+ tickets) exec bash "$ROOT/bin/tickets" "$@" ;;
49
+ ticket) exec bash "$ROOT/bin/ticket" "$@" ;;
50
+ proteger | protect) exec bash "$ROOT/bin/proteger" "$@" ;;
51
+ npm-publication | npm-publish) exec bash "$ROOT/bin/npm-publication" "$@" ;;
52
+ -v | --version | version)
53
+ repowarden_version_r "$ROOT"
54
+ echo "$REPLY"
55
+ ;;
56
+ "" | statut | status) exec bash "$ROOT/bin/statut" "$@" ;;
57
+ -h | --help | help) aide ;;
58
+ *)
59
+ err_t install.unknown_arg "$commande"
60
+ aide >&2
61
+ exit 1
62
+ ;;
63
+ esac
package/bin/statut ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ # État de repowarden sur ce poste et dans le dépôt courant, puis la commande
3
+ # suivante à lancer (l'équivalent de « gh auth status » ou « brew doctor ») :
4
+ # repowarden statut (ou repowarden, sans argument)
5
+ # Lecture seule : rien n'est modifié.
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
9
+ # shellcheck source=../hooks/lib/common.sh
10
+ source "$ROOT/hooks/lib/common.sh"
11
+ exec 2>&1 # une seule sortie, dans l'ordre (avertissements compris)
12
+ # shellcheck disable=SC2034 # lu par check_version (common.sh)
13
+ HOOKS_DIR="$ROOT/hooks"
14
+
15
+ repowarden_version_r "$ROOT"
16
+ version="$REPLY"
17
+ case "$ROOT" in
18
+ */node_modules/*) mode=npm ;;
19
+ *) mode=clone ;;
20
+ esac
21
+ suivant="" # première action à faire, affichée à la fin
22
+
23
+ reel() { [ -d "$1" ] && (cd "$1" && pwd -P); }
24
+
25
+ brand "${version:-?} · $mode · $ROOT"
26
+
27
+ # Hooks globaux : ceux de cette installation, d'une autre, ou d'un autre outil
28
+ global="$(git config --global --get core.hooksPath 2>/dev/null || true)"
29
+ local_hooks="$(git config --local --get core.hooksPath 2>/dev/null || true)"
30
+ if [ -z "$global" ] && [ -n "$local_hooks" ] && [ "$(reel "$local_hooks")" = "$ROOT/hooks" ]; then
31
+ # Installation locale (install.sh sans --global) : active dans ce dépôt
32
+ ok_t st.hooks_repo
33
+ elif [ -z "$global" ]; then
34
+ err_t st.hooks_off
35
+ suivant="${suivant:-repowarden install --global}"
36
+ elif [ "$(reel "$global")" = "$ROOT/hooks" ]; then
37
+ ok_t st.hooks_on
38
+ elif [ -f "$global/lib/common.sh" ] && grep -qsE "repowarden|repogarde" "$global/lib/common.sh"; then
39
+ attention_t st.hooks_other "$global"
40
+ suivant="${suivant:-repowarden install --global}"
41
+ else
42
+ info_t st.hooks_foreign "$global"
43
+ fi
44
+
45
+ # Assistant de commit (git cc) : valeur effective, installation locale comprise
46
+ alias_cc="$(git config --get alias.cc 2>/dev/null || true)"
47
+ if [[ "$alias_cc" == *"$ROOT/bin/commit"* ]]; then
48
+ ok_t st.cc_on
49
+ elif [[ "$alias_cc" == *"/bin/commit"* ]]; then
50
+ attention_t st.cc_other
51
+ suivant="${suivant:-repowarden install --global}"
52
+ elif [ -n "$alias_cc" ]; then
53
+ info_t st.cc_foreign "$alias_cc"
54
+ else
55
+ err_t st.cc_off
56
+ suivant="${suivant:-repowarden install --global}"
57
+ fi
58
+
59
+ info_t st.lang "$REPOWARDEN_LANG"
60
+
61
+ # Dépôt courant
62
+ if [ -n "$GIT_TOPLEVEL" ]; then
63
+ step_t st.repo "$GIT_TOPLEVEL"
64
+ local_path="$(git config --local --get core.hooksPath 2>/dev/null || true)"
65
+ if [ -n "$local_path" ]; then
66
+ info_t st.repo_local "$local_path"
67
+ fi
68
+ for f in lefthook.yml lefthook.yaml .lefthook.yml .lefthook.yaml; do
69
+ [ -f "$GIT_TOPLEVEL/$f" ] || continue
70
+ attention_t st.repo_lefthook "$f"
71
+ break
72
+ done
73
+ cfg_r version ""
74
+ if [ -n "$REPLY" ]; then
75
+ attendue="${REPLY#v}"
76
+ if [ -n "$(check_version 2>&1)" ]; then
77
+ attention_t st.repo_version_old "$attendue" "${version:-?}"
78
+ [ "$mode" = npm ] && suivant="${suivant:-npm update -g repowarden}"
79
+ else
80
+ ok_t st.repo_version_ok "$attendue"
81
+ fi
82
+ fi
83
+ info_t st.repo_forge "$FORGE"
84
+ fi
85
+
86
+ # Outils : secrets, code mort (selon les langages)
87
+ presents="" absents=""
88
+ for outil in gitleaks ruff vulture staticcheck pmd; do
89
+ if has "$outil"; then presents="$presents $outil"; else absents="$absents $outil"; fi
90
+ done
91
+ info_t st.tools "${presents# }" "${absents# }"
92
+
93
+ echo
94
+ if [ -n "$suivant" ]; then
95
+ t st.next
96
+ printf '%s %s%s%s\n' "$REPLY" "$UI_B" "$suivant" "$UI_N"
97
+ else
98
+ ok_t st.ready
99
+ fi
100
+ dim_t st.help