repowarden 4.7.2-next.6 → 4.8.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.
- package/README.md +5 -1
- package/bin/app +24 -10
- package/bin/bot +231 -0
- package/bin/bots +370 -0
- package/bin/migrer +205 -0
- package/bin/proteger +31 -2
- package/bin/repowarden +7 -1
- package/bin/statut +12 -0
- package/bin/tickets +5 -7
- package/hooks/lib/i18n/en.sh +120 -2
- package/hooks/lib/i18n/fr.sh +120 -2
- package/install.sh +19 -1
- package/package.json +1 -1
package/bin/bots
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Bots du dépôt GitHub courant, configurés avec l'accord de l'utilisateur :
|
|
3
|
+
# repowarden bots questions, aperçu, puis confirmation
|
|
4
|
+
# repowarden install --bots (idem, après l'installation des hooks)
|
|
5
|
+
# repowarden install --sans-bots
|
|
6
|
+
# ne plus jamais le proposer (repowarden.bots = off)
|
|
7
|
+
# Rien n'est fait sans accord : une question globale (non par défaut), le
|
|
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.
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
|
16
|
+
# shellcheck source=../hooks/lib/common.sh
|
|
17
|
+
source "$ROOT/hooks/lib/common.sh"
|
|
18
|
+
|
|
19
|
+
aide() { sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; }
|
|
20
|
+
|
|
21
|
+
# Commandes enchaînées (remplaçables dans les tests)
|
|
22
|
+
BIN="${REPOWARDEN_BIN:-$ROOT/bin}"
|
|
23
|
+
DEPUIS_INSTALL=""
|
|
24
|
+
while [ $# -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--depuis-install) DEPUIS_INSTALL=1 ;;
|
|
27
|
+
-h | --help) aide; exit 0 ;;
|
|
28
|
+
*) err_t install.unknown_arg "$1"; exit 1 ;;
|
|
29
|
+
esac
|
|
30
|
+
shift
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
# Rien n'est proposé : une ligne dit pourquoi, et comment le faire plus tard
|
|
34
|
+
pas_maintenant() { # $1 clé de la raison
|
|
35
|
+
t "$1"
|
|
36
|
+
info_t bots.skip "$REPLY"
|
|
37
|
+
exit 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Refus permanent (install --sans-bots) : silence depuis l'installation
|
|
41
|
+
if [ -n "$DEPUIS_INSTALL" ] && [ "$(git config --get repowarden.bots 2>/dev/null || true)" = off ]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
[ -n "$GIT_TOPLEVEL" ] || pas_maintenant bots.why_repo
|
|
45
|
+
cd "$GIT_TOPLEVEL"
|
|
46
|
+
[ "$FORGE" = github ] || pas_maintenant bots.why_github
|
|
47
|
+
command -v gh >/dev/null || pas_maintenant bots.why_gh
|
|
48
|
+
gh auth status >/dev/null 2>&1 || pas_maintenant bots.why_gh_login
|
|
49
|
+
REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)"
|
|
50
|
+
[ -n "$REPO" ] || pas_maintenant bots.why_github
|
|
51
|
+
[ "$(gh api "repos/$REPO" --jq .permissions.admin 2>/dev/null || true)" = true ] || pas_maintenant bots.why_admin
|
|
52
|
+
# Terminal (ou réponses fournies, tests) : sinon, jamais de question
|
|
53
|
+
[ -t 0 ] || [ -n "${REPOWARDEN_INTERACTIF:-}" ] || pas_maintenant bots.why_tty
|
|
54
|
+
|
|
55
|
+
# Question fermée : $1 clé, $2 défaut (o ou n) ; vrai si oui
|
|
56
|
+
demander() {
|
|
57
|
+
local reponse choix="o/N"
|
|
58
|
+
[ "$2" = n ] || choix="O/n"
|
|
59
|
+
t "$1"
|
|
60
|
+
printf '%s [%s] ' "$REPLY" "$choix" >&2
|
|
61
|
+
IFS= read -r reponse || reponse=""
|
|
62
|
+
case "$reponse" in
|
|
63
|
+
"") [ "$2" = o ] ;;
|
|
64
|
+
[oOyY]*) return 0 ;;
|
|
65
|
+
*) return 1 ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
brand "bots · $REPO"
|
|
70
|
+
demander bots.ask_global n || { info_t bots.later; exit 0; }
|
|
71
|
+
|
|
72
|
+
# --- Flux ------------------------------------------------------------------
|
|
73
|
+
cfg_r integrationBranch ""
|
|
74
|
+
INTEGRATION="$REPLY"
|
|
75
|
+
DEFAUT="$(gh api "repos/$REPO" --jq .default_branch)"
|
|
76
|
+
PRINCIPALE=main
|
|
77
|
+
[ "$DEFAUT" = master ] && PRINCIPALE=master
|
|
78
|
+
echo >&2
|
|
79
|
+
t bots.flow_question
|
|
80
|
+
printf '%s\n' "$REPLY" >&2
|
|
81
|
+
t bots.flow_main "$PRINCIPALE"
|
|
82
|
+
printf ' 1) %s\n' "$REPLY" >&2
|
|
83
|
+
t bots.flow_develop "$PRINCIPALE"
|
|
84
|
+
printf ' 2) %s\n' "$REPLY" >&2
|
|
85
|
+
# develop → main par défaut : seul develop entre dans main
|
|
86
|
+
choix_flux=2
|
|
87
|
+
read -r -p "[$choix_flux] : " reponse || reponse=""
|
|
88
|
+
case "${reponse:-$choix_flux}" in
|
|
89
|
+
2*) FLUX=develop; INTEGRATION="${INTEGRATION:-develop}" ;;
|
|
90
|
+
*) FLUX=main; INTEGRATION="" ;;
|
|
91
|
+
esac
|
|
92
|
+
CREER_DEVELOP=""
|
|
93
|
+
if [ "$FLUX" = develop ] &&
|
|
94
|
+
[ -z "$(gh api "repos/$REPO/branches/$INTEGRATION" --jq .name 2>/dev/null || true)" ]; then
|
|
95
|
+
if demander bots.ask_create_develop o; then
|
|
96
|
+
CREER_DEVELOP=1
|
|
97
|
+
else
|
|
98
|
+
FLUX=main INTEGRATION=""
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# --- Un accord par bot -----------------------------------------------------
|
|
103
|
+
echo >&2
|
|
104
|
+
info_t bots.one_by_one
|
|
105
|
+
APP_PRESENTE=""
|
|
106
|
+
[ -z "$(gh variable get REPOWARDEN_APP_CLIENT_ID --repo "$REPO" 2>/dev/null || true)" ] || APP_PRESENTE=1
|
|
107
|
+
NPM_POSSIBLE=""
|
|
108
|
+
[ ! -f package.json ] || grep -qE '"private": *true' package.json || NPM_POSSIBLE=1
|
|
109
|
+
BOTS=()
|
|
110
|
+
for bot in ci release nettoyage tickets app proteger npm; do
|
|
111
|
+
case "$bot" in
|
|
112
|
+
nettoyage) [ "$FLUX" = develop ] || continue ;;
|
|
113
|
+
app) [ -z "$APP_PRESENTE" ] || { info_t bots.app_present; continue; } ;;
|
|
114
|
+
npm) [ -n "$NPM_POSSIBLE" ] || continue ;;
|
|
115
|
+
esac
|
|
116
|
+
dim_t "bots.desc_$bot"
|
|
117
|
+
demander "bots.ask_$bot" o && BOTS+=("$bot")
|
|
118
|
+
done
|
|
119
|
+
[ "${#BOTS[@]}" -gt 0 ] || [ -n "$CREER_DEVELOP" ] || { info_t bots.later; exit 0; }
|
|
120
|
+
choisi() { [[ " ${BOTS[*]+"${BOTS[*]}"} " == *" $1 "* ]]; }
|
|
121
|
+
AVEC_APP=""
|
|
122
|
+
{ [ -n "$APP_PRESENTE" ] || choisi app; } && AVEC_APP=1
|
|
123
|
+
|
|
124
|
+
# --- Modèles des workflows -------------------------------------------------
|
|
125
|
+
modele_ci() {
|
|
126
|
+
cat <<'YAML'
|
|
127
|
+
# Vérifications repowarden en CI (obligatoires pour merger : repowarden proteger)
|
|
128
|
+
name: repowarden
|
|
129
|
+
|
|
130
|
+
on:
|
|
131
|
+
pull_request:
|
|
132
|
+
# edited : revérifie quand le titre de la PR change (message du commit en squash)
|
|
133
|
+
types: [opened, synchronize, reopened, edited]
|
|
134
|
+
push:
|
|
135
|
+
YAML
|
|
136
|
+
if [ "$FLUX" = develop ]; then
|
|
137
|
+
echo " branches: [$PRINCIPALE, $INTEGRATION]"
|
|
138
|
+
else
|
|
139
|
+
echo " branches: [$PRINCIPALE]"
|
|
140
|
+
fi
|
|
141
|
+
cat <<'YAML'
|
|
142
|
+
# Lancé par le workflow de release sur sa PR (release-auto)
|
|
143
|
+
workflow_dispatch:
|
|
144
|
+
|
|
145
|
+
permissions:
|
|
146
|
+
contents: read
|
|
147
|
+
YAML
|
|
148
|
+
[ "$FLUX" != develop ] || echo " pull-requests: write # fix-pr : PR mal visée reciblée"
|
|
149
|
+
cat <<'YAML'
|
|
150
|
+
|
|
151
|
+
jobs:
|
|
152
|
+
repowarden:
|
|
153
|
+
runs-on: ubuntu-latest
|
|
154
|
+
steps:
|
|
155
|
+
- uses: actions/checkout@v7
|
|
156
|
+
with:
|
|
157
|
+
fetch-depth: 0 # historique complet : messages de commit, diff depuis la base
|
|
158
|
+
# Outils du projet nécessaires au formatage et aux tests, par exemple :
|
|
159
|
+
# - uses: actions/setup-node@v7
|
|
160
|
+
# with: { node-version: 24, cache: npm }
|
|
161
|
+
# - run: npm ci
|
|
162
|
+
- uses: simdevWarden/repowarden@v4
|
|
163
|
+
YAML
|
|
164
|
+
if [ "$FLUX" = develop ]; then
|
|
165
|
+
cat <<'YAML'
|
|
166
|
+
with:
|
|
167
|
+
# strict: true # outil absent bloquant, une fois l'outillage installé ci-dessus
|
|
168
|
+
# Seule la branche d'intégration entre dans la principale : une PR
|
|
169
|
+
# de travail mal visée est reciblée avant les vérifications
|
|
170
|
+
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
|
+
YAML
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
modele_release() {
|
|
181
|
+
if [ "$FLUX" = develop ]; then
|
|
182
|
+
cat <<YAML
|
|
183
|
+
# Releases, flux $INTEGRATION → $PRINCIPALE (mode tag) : préversion vX.Y.Z-next.N le
|
|
184
|
+
# soir si $INTEGRATION a changé ; le vendredi matin (ou à la main), une demande
|
|
185
|
+
# de livraison $INTEGRATION → $PRINCIPALE, qui publie le tag vX.Y.Z et la release.
|
|
186
|
+
name: release
|
|
187
|
+
|
|
188
|
+
on:
|
|
189
|
+
push:
|
|
190
|
+
branches: [$PRINCIPALE, $INTEGRATION]
|
|
191
|
+
schedule:
|
|
192
|
+
- cron: "0 19 * * *" # préversion du soir, si $INTEGRATION a changé
|
|
193
|
+
- cron: "0 7 * * 5" # fenêtre de livraison : le vendredi matin
|
|
194
|
+
workflow_dispatch: # à la main (et urgence) : préversion et demande de livraison
|
|
195
|
+
|
|
196
|
+
# Une file par branche et par déclencheur ; une demande manuelle ou planifiée
|
|
197
|
+
# remplace celle en attente ; un push n'annule jamais un run en cours
|
|
198
|
+
concurrency:
|
|
199
|
+
group: release-\${{ github.ref_name }}-\${{ github.event.schedule || github.event_name }}
|
|
200
|
+
cancel-in-progress: \${{ github.event_name != 'push' }}
|
|
201
|
+
|
|
202
|
+
permissions: {}
|
|
203
|
+
|
|
204
|
+
jobs:
|
|
205
|
+
release:
|
|
206
|
+
uses: simdevWarden/repowarden/.github/workflows/release-auto.yml@v4
|
|
207
|
+
permissions: { contents: write, pull-requests: write, actions: write, checks: read, statuses: write }
|
|
208
|
+
with:
|
|
209
|
+
mode: tag
|
|
210
|
+
integration-branch: $INTEGRATION
|
|
211
|
+
main-branch: $PRINCIPALE
|
|
212
|
+
delivery-schedule: "0 7 * * 5" # même cron que la fenêtre ci-dessus
|
|
213
|
+
YAML
|
|
214
|
+
if [ -n "$AVEC_APP" ]; then
|
|
215
|
+
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
|
|
218
|
+
secrets:
|
|
219
|
+
app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
|
|
220
|
+
YAML
|
|
221
|
+
fi
|
|
222
|
+
else
|
|
223
|
+
cat <<'YAML'
|
|
224
|
+
# Releases automatiques : à chaque merge sur la branche principale, la version
|
|
225
|
+
# (semver) est calculée depuis les Conventional Commits ; une PR « release
|
|
226
|
+
# x.y.z » est validée par la CI puis mergée, et le tag et la release créés.
|
|
227
|
+
name: release
|
|
228
|
+
|
|
229
|
+
on:
|
|
230
|
+
push:
|
|
231
|
+
YAML
|
|
232
|
+
echo " branches: [$PRINCIPALE]"
|
|
233
|
+
cat <<'YAML'
|
|
234
|
+
workflow_dispatch:
|
|
235
|
+
|
|
236
|
+
# Une release à la fois
|
|
237
|
+
concurrency:
|
|
238
|
+
group: release
|
|
239
|
+
cancel-in-progress: false
|
|
240
|
+
|
|
241
|
+
permissions: {}
|
|
242
|
+
|
|
243
|
+
jobs:
|
|
244
|
+
release:
|
|
245
|
+
uses: simdevWarden/repowarden/.github/workflows/release-auto.yml@v4
|
|
246
|
+
permissions: { contents: write, pull-requests: write, actions: write, checks: read, statuses: write }
|
|
247
|
+
YAML
|
|
248
|
+
if [ -n "$AVEC_APP" ]; then
|
|
249
|
+
cat <<'YAML'
|
|
250
|
+
with:
|
|
251
|
+
app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }}
|
|
252
|
+
secrets:
|
|
253
|
+
app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
|
|
254
|
+
YAML
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
modele_nettoyage() {
|
|
260
|
+
cat <<'YAML'
|
|
261
|
+
# Branches de PR supprimées après merge (la suppression automatique de GitHub
|
|
262
|
+
# est désactivée dans le flux develop : elle supprimerait la branche
|
|
263
|
+
# d'intégration au merge de la livraison). Les branches longues et toute
|
|
264
|
+
# branche encore source d'une PR ouverte sont conservées.
|
|
265
|
+
name: nettoyage
|
|
266
|
+
|
|
267
|
+
on:
|
|
268
|
+
pull_request:
|
|
269
|
+
types: [closed]
|
|
270
|
+
|
|
271
|
+
permissions: {}
|
|
272
|
+
|
|
273
|
+
jobs:
|
|
274
|
+
nettoyage:
|
|
275
|
+
uses: simdevWarden/repowarden/.github/workflows/nettoyage-branches.yml@v4
|
|
276
|
+
permissions:
|
|
277
|
+
contents: write
|
|
278
|
+
pull-requests: read
|
|
279
|
+
YAML
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fichier_du() { # $1 bot → REPLY : chemin de son workflow (vide : aucun)
|
|
283
|
+
case "$1" in
|
|
284
|
+
ci) REPLY=.github/workflows/repowarden.yml ;;
|
|
285
|
+
release) REPLY=.github/workflows/release.yml ;;
|
|
286
|
+
nettoyage) REPLY=.github/workflows/nettoyage.yml ;;
|
|
287
|
+
*) REPLY="" ;;
|
|
288
|
+
esac
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# --- Aperçu, puis confirmation ---------------------------------------------
|
|
292
|
+
echo >&2
|
|
293
|
+
step_t bots.preview
|
|
294
|
+
[ -z "$CREER_DEVELOP" ] || info_t bots.prev_develop "$INTEGRATION" "$PRINCIPALE"
|
|
295
|
+
if [ "$FLUX" = develop ]; then
|
|
296
|
+
info_t bots.prev_conf "$INTEGRATION"
|
|
297
|
+
[ -n "$AVEC_APP" ] || info_t bots.prev_review
|
|
298
|
+
fi
|
|
299
|
+
for bot in ${BOTS[@]+"${BOTS[@]}"}; do
|
|
300
|
+
fichier_du "$bot"
|
|
301
|
+
f="$REPLY"
|
|
302
|
+
if [ -n "$f" ]; then
|
|
303
|
+
if [ -e "$f" ]; then
|
|
304
|
+
ecart="$(diff <("modele_$bot") "$f" | grep -c '^[<>]' || true)"
|
|
305
|
+
info_t bots.prev_kept "$f" "$ecart"
|
|
306
|
+
else
|
|
307
|
+
info_t bots.prev_file "$f"
|
|
308
|
+
fi
|
|
309
|
+
else
|
|
310
|
+
info_t "bots.prev_$bot"
|
|
311
|
+
fi
|
|
312
|
+
done
|
|
313
|
+
demander bots.ask_confirm n || { info_t bots.later; exit 0; }
|
|
314
|
+
|
|
315
|
+
# --- Application -----------------------------------------------------------
|
|
316
|
+
echecs=0
|
|
317
|
+
lancer() { # $1 bot, puis la commande : une erreur n'arrête pas les autres
|
|
318
|
+
local bot="$1"
|
|
319
|
+
shift
|
|
320
|
+
echo >&2
|
|
321
|
+
step_t "bots.run_$bot"
|
|
322
|
+
if ! "$@"; then
|
|
323
|
+
err_t bots.failed_one "$bot"
|
|
324
|
+
echecs=$((echecs + 1))
|
|
325
|
+
fi
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if [ -n "$CREER_DEVELOP" ]; then
|
|
329
|
+
sha="$(gh api "repos/$REPO/git/ref/heads/$DEFAUT" --jq .object.sha)"
|
|
330
|
+
gh api -X POST "repos/$REPO/git/refs" -f "ref=refs/heads/$INTEGRATION" -f "sha=$sha" --silent
|
|
331
|
+
ok_t bots.develop_created "$INTEGRATION" "$DEFAUT"
|
|
332
|
+
fi
|
|
333
|
+
if [ "$FLUX" = develop ]; then
|
|
334
|
+
conf="$GIT_TOPLEVEL/.repowarden.conf"
|
|
335
|
+
git config -f "$conf" repowarden.integrationBranch "$INTEGRATION"
|
|
336
|
+
git config -f "$conf" --get repowarden.protectedBranches >/dev/null ||
|
|
337
|
+
git config -f "$conf" repowarden.protectedBranches "$PRINCIPALE $INTEGRATION"
|
|
338
|
+
if [ -z "$AVEC_APP" ]; then
|
|
339
|
+
git config -f "$conf" --get repowarden.deliveryApproval >/dev/null ||
|
|
340
|
+
git config -f "$conf" repowarden.deliveryApproval review
|
|
341
|
+
fi
|
|
342
|
+
ok_t bots.conf_written .repowarden.conf
|
|
343
|
+
fi
|
|
344
|
+
for bot in ${BOTS[@]+"${BOTS[@]}"}; do
|
|
345
|
+
fichier_du "$bot"
|
|
346
|
+
f="$REPLY"
|
|
347
|
+
if [ -n "$f" ]; then
|
|
348
|
+
if [ -e "$f" ]; then
|
|
349
|
+
info_t bots.kept "$f"
|
|
350
|
+
else
|
|
351
|
+
mkdir -p "${f%/*}"
|
|
352
|
+
"modele_$bot" >"$f"
|
|
353
|
+
ok_t bots.written "$f"
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
done
|
|
357
|
+
choisi tickets && lancer tickets bash "$BIN/tickets" init --repo "$REPO"
|
|
358
|
+
choisi app && lancer app bash "$BIN/app" init --repo "$REPO"
|
|
359
|
+
choisi proteger && lancer proteger bash "$BIN/proteger" --repo "$REPO"
|
|
360
|
+
choisi npm && lancer npm bash "$BIN/npm-publication" --repo "$REPO"
|
|
361
|
+
|
|
362
|
+
echo >&2
|
|
363
|
+
if [ "$echecs" = 0 ]; then
|
|
364
|
+
ok_t bots.done
|
|
365
|
+
else
|
|
366
|
+
attention_t bots.done_errors "$echecs"
|
|
367
|
+
fi
|
|
368
|
+
[ -z "$(git status --porcelain -- .github .repowarden.conf 2>/dev/null)" ] || info_t bots.commit_files
|
|
369
|
+
dim_t bots.verify
|
|
370
|
+
[ "$echecs" = 0 ]
|
package/bin/migrer
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Passage de repogarde à repowarden dans le projet courant (docs/migration-v4.md) :
|
|
3
|
+
# repowarden migrer applique la migration dans l'arbre de travail
|
|
4
|
+
# repowarden migrer --pr sur une branche chore/migration-repowarden-vN :
|
|
5
|
+
# commit, push et PR (rien si une PR est ouverte)
|
|
6
|
+
# repowarden migrer --hooks déplace aussi les hooks lefthook résiduels de
|
|
7
|
+
# .git/hooks dans .git/hooks-lefthook/
|
|
8
|
+
# Configuration (.repowarden.conf, .repowarden/), workflows (uses: vers le
|
|
9
|
+
# dépôt repowarden et sa version majeure, anciens noms, permissions), puis la
|
|
10
|
+
# liste de ce qui reste à faire à la main. Relançable : rien n'est fait deux fois.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
|
14
|
+
# shellcheck source=../hooks/lib/common.sh
|
|
15
|
+
source "$ROOT/hooks/lib/common.sh"
|
|
16
|
+
|
|
17
|
+
aide() { sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; }
|
|
18
|
+
|
|
19
|
+
PR="" HOOKS_LEFTHOOK=""
|
|
20
|
+
while [ $# -gt 0 ]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--pr) PR=1 ;;
|
|
23
|
+
--hooks) HOOKS_LEFTHOOK=1 ;;
|
|
24
|
+
-h | --help) aide; exit 0 ;;
|
|
25
|
+
*) err_t install.unknown_arg "$1"; exit 1 ;;
|
|
26
|
+
esac
|
|
27
|
+
shift
|
|
28
|
+
done
|
|
29
|
+
[ -n "$GIT_TOPLEVEL" ] || { err_t install.not_repo; exit 1; }
|
|
30
|
+
cd "$GIT_TOPLEVEL"
|
|
31
|
+
|
|
32
|
+
# Version majeure dont le tag flottant (vN) est publié : celle de repowarden ;
|
|
33
|
+
# préversion d'une nouvelle majeure (X.0.0-next) : X-1, vX n'existant pas encore
|
|
34
|
+
majeure_r() {
|
|
35
|
+
local v
|
|
36
|
+
repowarden_version_r "$ROOT"
|
|
37
|
+
v="$REPLY"
|
|
38
|
+
REPLY="${v%%.*}"
|
|
39
|
+
REPLY="${REPLY%%-*}"
|
|
40
|
+
[[ "$v" != *.0.0-* ]] || REPLY=$((REPLY - 1))
|
|
41
|
+
}
|
|
42
|
+
if [ -n "${REPOWARDEN_MAJEURE:-}" ]; then
|
|
43
|
+
MAJEURE="$REPOWARDEN_MAJEURE"
|
|
44
|
+
else
|
|
45
|
+
majeure_r
|
|
46
|
+
MAJEURE="$REPLY"
|
|
47
|
+
fi
|
|
48
|
+
[[ "$MAJEURE" =~ ^[1-9][0-9]*$ ]] || { err_t mig.no_major "$MAJEURE"; exit 1; }
|
|
49
|
+
BRANCHE="chore/migration-repowarden-v$MAJEURE"
|
|
50
|
+
|
|
51
|
+
if [ -n "$PR" ]; then
|
|
52
|
+
command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
|
|
53
|
+
[ -z "$(git status --porcelain)" ] || { err_t mig.dirty; exit 1; }
|
|
54
|
+
if [ -n "$(gh pr list --head "$BRANCHE" --state open --json number --jq '.[].number' 2>/dev/null)" ]; then
|
|
55
|
+
info_t mig.pr_open "$BRANCHE"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
git switch -q -c "$BRANCHE"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
FAITS=0
|
|
62
|
+
fait() { FAITS=$((FAITS + 1)); ok_t "$@"; }
|
|
63
|
+
|
|
64
|
+
# Fichier réécrit par sed (-E), sans -i (BSD et GNU diffèrent)
|
|
65
|
+
reecrire() { # $1 fichier, $2… expressions sed
|
|
66
|
+
local f="$1" tmp
|
|
67
|
+
shift
|
|
68
|
+
tmp="$(mktemp)"
|
|
69
|
+
sed -E "$@" "$f" >"$tmp"
|
|
70
|
+
if cmp -s "$f" "$tmp"; then
|
|
71
|
+
rm -f "$tmp"
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
cat "$tmp" >"$f"
|
|
75
|
+
rm -f "$tmp"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 1. Configuration du projet
|
|
79
|
+
if [ -f .repogarde.conf ]; then
|
|
80
|
+
if [ -e .repowarden.conf ]; then
|
|
81
|
+
attention_t mig.both .repogarde.conf .repowarden.conf
|
|
82
|
+
else
|
|
83
|
+
git mv .repogarde.conf .repowarden.conf 2>/dev/null || mv .repogarde.conf .repowarden.conf
|
|
84
|
+
reecrire .repowarden.conf -e 's/repogarde/repowarden/g' \
|
|
85
|
+
-e "s/^([[:space:]]*version[[:space:]]*=[[:space:]]*).*/\\1$MAJEURE/" || true
|
|
86
|
+
fait mig.conf
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
if [ -d .repogarde ]; then
|
|
90
|
+
if [ -e .repowarden ]; then
|
|
91
|
+
attention_t mig.both .repogarde/ .repowarden/
|
|
92
|
+
else
|
|
93
|
+
git mv .repogarde .repowarden 2>/dev/null || mv .repogarde .repowarden
|
|
94
|
+
fait mig.dir
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# 2. Workflows
|
|
99
|
+
secrets=""
|
|
100
|
+
for wf in .github/workflows/*.yml .github/workflows/*.yaml; do
|
|
101
|
+
[ -f "$wf" ] || continue
|
|
102
|
+
grep -qi repogarde "$wf" || continue
|
|
103
|
+
# shellcheck disable=SC2013 # noms sans espace
|
|
104
|
+
for nom in $(grep -oE '(secrets|vars)\.REPOGARDE_[A-Z0-9_]+' "$wf" | sort -u); do
|
|
105
|
+
secrets="$secrets ${nom/REPOGARDE_/REPOWARDEN_}"
|
|
106
|
+
done
|
|
107
|
+
# uses: vers l'ancien dépôt, toute référence (tag, branche d'essai) ;
|
|
108
|
+
# entrée retirée (version désormais fixée par uses:) ; anciens noms ;
|
|
109
|
+
# tickets.yml de la v4 : création de branches (contents: write)
|
|
110
|
+
change=""
|
|
111
|
+
reecrire "$wf" \
|
|
112
|
+
-e "s#(uses:[[:space:]]*)[A-Za-z0-9_.-]+/repogarde((/[^@[:space:]]+)?)@[^[:space:]]+#\\1simdevWarden/repowarden\\2@v$MAJEURE#" \
|
|
113
|
+
-e '/^[[:space:]]*repogarde-ref:/d' \
|
|
114
|
+
-e 's#SimBienvenueHoulBoumi/repogarde#simdevWarden/repowarden#g' \
|
|
115
|
+
-e 's/repogarde/repowarden/g' -e 's/REPOGARDE_/REPOWARDEN_/g' && change=1
|
|
116
|
+
# permissions du job (pas celles du workflow, souvent « {} »)
|
|
117
|
+
if grep -q 'repowarden/.github/workflows/tickets.yml' "$wf"; then
|
|
118
|
+
reecrire "$wf" -e '/^[[:space:]]*permissions:[[:space:]]*\{[^}]*issues:/{/contents:/!s/\{[[:space:]]*/{ contents: write, /;}' && change=1
|
|
119
|
+
fi
|
|
120
|
+
[ -z "$change" ] || fait mig.workflow "$wf"
|
|
121
|
+
done
|
|
122
|
+
if [ -f .github/workflows/repogarde.yml ] && [ ! -e .github/workflows/repowarden.yml ]; then
|
|
123
|
+
git mv .github/workflows/repogarde.yml .github/workflows/repowarden.yml 2>/dev/null ||
|
|
124
|
+
mv .github/workflows/repogarde.yml .github/workflows/repowarden.yml
|
|
125
|
+
fait mig.renamed .github/workflows/repogarde.yml .github/workflows/repowarden.yml
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Suivi des tickets d'une version antérieure : sans l'événement push, le
|
|
129
|
+
# premier push d'une branche de ticket n'ouvre pas la PR brouillon préremplie
|
|
130
|
+
# (ni revue, assignation, fermeture : statuts figés). Section on: remplacée
|
|
131
|
+
# par celle du modèle (repowarden tickets init), le reste gardé.
|
|
132
|
+
for wf in .github/workflows/*.yml .github/workflows/*.yaml; do
|
|
133
|
+
[ -f "$wf" ] || continue
|
|
134
|
+
grep -q 'repowarden/.github/workflows/tickets.yml' "$wf" || continue
|
|
135
|
+
grep -qE '^ push:' "$wf" && continue
|
|
136
|
+
tmp="$(mktemp)"
|
|
137
|
+
awk '
|
|
138
|
+
/^on:/ {
|
|
139
|
+
print "on:"
|
|
140
|
+
print " pull_request_target:"
|
|
141
|
+
print " types: [opened, reopened, edited, synchronize, ready_for_review, converted_to_draft, closed]"
|
|
142
|
+
print " pull_request_review:"
|
|
143
|
+
print " types: [submitted]"
|
|
144
|
+
print " issues:"
|
|
145
|
+
print " types: [opened, labeled, assigned, closed]"
|
|
146
|
+
print " push:"
|
|
147
|
+
print " branches-ignore: [main, master, develop]"
|
|
148
|
+
dans = 1
|
|
149
|
+
next
|
|
150
|
+
}
|
|
151
|
+
dans && /^[^ #]/ { dans = 0 }
|
|
152
|
+
!dans { print }
|
|
153
|
+
' "$wf" >"$tmp"
|
|
154
|
+
cat "$tmp" >"$wf"
|
|
155
|
+
rm -f "$tmp"
|
|
156
|
+
fait mig.tickets_events "$wf"
|
|
157
|
+
info_t mig.tickets_labels
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
# 3. Hooks lefthook résiduels (lefthook n'est plus pris en charge)
|
|
161
|
+
hooks_dir="$(git rev-parse --git-common-dir)/hooks"
|
|
162
|
+
residuels=""
|
|
163
|
+
for h in "$hooks_dir"/*; do
|
|
164
|
+
[ -f "$h" ] && [[ "$h" != *.sample ]] && grep -qs lefthook "$h" && residuels="$residuels ${h##*/}"
|
|
165
|
+
done
|
|
166
|
+
if [ -n "$residuels" ]; then
|
|
167
|
+
if [ -n "$HOOKS_LEFTHOOK" ]; then
|
|
168
|
+
mkdir -p "$hooks_dir-lefthook"
|
|
169
|
+
for h in $residuels; do mv "$hooks_dir/$h" "$hooks_dir-lefthook/$h"; done
|
|
170
|
+
ok_t mig.hooks_moved "$residuels" "$hooks_dir-lefthook/"
|
|
171
|
+
else
|
|
172
|
+
attention_t mig.hooks_left "$residuels"
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
for f in lefthook.yml lefthook.yaml .lefthook.yml .lefthook.yaml; do
|
|
176
|
+
[ -f "$f" ] && attention_t mig.lefthook_conf "$f"
|
|
177
|
+
done
|
|
178
|
+
|
|
179
|
+
# 4. À faire à la main
|
|
180
|
+
[ -z "$secrets" ] || attention_t mig.secrets "$secrets"
|
|
181
|
+
restants="$(git grep -il repogarde -- . ':!CHANGELOG.md' 2>/dev/null | tr '\n' ' ' || true)"
|
|
182
|
+
[ -z "$restants" ] || info_t mig.remaining "$restants"
|
|
183
|
+
|
|
184
|
+
if [ "$FAITS" = 0 ]; then
|
|
185
|
+
ok_t mig.nothing
|
|
186
|
+
if [ -n "$PR" ]; then
|
|
187
|
+
git switch -q -
|
|
188
|
+
git branch -q -D "$BRANCHE"
|
|
189
|
+
fi
|
|
190
|
+
exit 0
|
|
191
|
+
fi
|
|
192
|
+
info_t mig.next
|
|
193
|
+
|
|
194
|
+
[ -n "$PR" ] || exit 0
|
|
195
|
+
git add -A
|
|
196
|
+
t mig.commit_subject "$MAJEURE"
|
|
197
|
+
sujet="$REPLY"
|
|
198
|
+
t mig.commit_body
|
|
199
|
+
bash "$ROOT/bin/commit" -m "$sujet" --type chore --scope migration --body "$REPLY"
|
|
200
|
+
git push -q -u origin "$BRANCHE"
|
|
201
|
+
t mig.pr_body
|
|
202
|
+
corps="$REPLY"
|
|
203
|
+
t mig.pr_after
|
|
204
|
+
gh pr create --head "$BRANCHE" --title "chore(migration): $sujet" --body "$corps"$'\n\n'"$REPLY" >/dev/null
|
|
205
|
+
ok_t mig.pr_created "$BRANCHE"
|
package/bin/proteger
CHANGED
|
@@ -193,7 +193,15 @@ if [ -n "$integration" ]; then
|
|
|
193
193
|
# develop ne l'a pas reçu (retour main → develop vide après chaque
|
|
194
194
|
# release). Une livraison n'apporte que develop, vérifiée par la CI ; le
|
|
195
195
|
# retour reste fait quand main a un contenu propre (correctif direct).
|
|
196
|
+
# Garde-fou de livraison (statut posé par release-auto) : la livraison ne
|
|
197
|
+
# se merge que si elle peut aboutir (préversion publiée, protection en place)
|
|
198
|
+
contexts_branches="$contexts"
|
|
199
|
+
if grep -qs "release-auto.yml" "$GIT_TOPLEVEL"/.github/workflows/*.y*ml; then
|
|
200
|
+
contexts="${contexts:+$contexts,} {\"context\": \"livraison\", \"integration_id\": 15368}"
|
|
201
|
+
livraison_exigee=1
|
|
202
|
+
fi
|
|
196
203
|
[ -z "$others" ] || rulesets+=("$(branch_ruleset repowarden "$others" '"merge"' false "$livraison" "$last_push" "$deploiement")")
|
|
204
|
+
contexts="$contexts_branches"
|
|
197
205
|
# Branche d'intégration : squash seul (un commit par PR de travail, titre =
|
|
198
206
|
# message : les commits intermédiaires n'entrent jamais) ; plus de retour
|
|
199
207
|
# main → develop à merger, seuls develop et ses instantanés livraison/*
|
|
@@ -254,6 +262,7 @@ fi
|
|
|
254
262
|
echo_t ci.proteger.1 "$REPO"
|
|
255
263
|
echo_t ci.proteger.2 "$existing"
|
|
256
264
|
echo_t ci.proteger.3 "$CHECKS"
|
|
265
|
+
[ -z "${livraison_exigee:-}" ] || echo_t ci.proteger.livraison_check
|
|
257
266
|
echo_t ci.proteger.4 "$methods" "$delete_on_merge"
|
|
258
267
|
if [ -n "$integration" ] && [ "$DELIVERY" = deployment ]; then
|
|
259
268
|
echo_t ci.proteger.delivery_deploy "$ENVIRONMENT" "$auto_merge"
|
|
@@ -360,12 +369,24 @@ erreur_github() {
|
|
|
360
369
|
fi
|
|
361
370
|
}
|
|
362
371
|
|
|
363
|
-
|
|
372
|
+
# Rulesets indisponibles (dépôt privé d'un compte gratuit : GitHub les refuse
|
|
373
|
+
# en 403) : les réglages du dépôt restent appliqués, les rulesets sautés avec
|
|
374
|
+
# un message clair au lieu d'un arrêt sans explication
|
|
375
|
+
RULESETS_OK=1
|
|
376
|
+
if ! reponse="$(gh api "repos/$REPO/rulesets" 2>&1)" && [[ "$reponse" == *"403"* ]]; then
|
|
377
|
+
RULESETS_OK=""
|
|
378
|
+
fi
|
|
379
|
+
ANCIENS=""
|
|
380
|
+
[ -z "$RULESETS_OK" ] || anciens_rulesets
|
|
364
381
|
|
|
365
382
|
# shellcheck disable=SC2016 # expression jq (gh --jq), pas du shell
|
|
366
383
|
SIG_EN_LIGNE='(.rules | map(select(.type == "pull_request"))[0].parameters) as $p
|
|
367
384
|
| "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(","))"'
|
|
368
385
|
|
|
386
|
+
if [ -n "$VERIFY" ] && [ -z "$RULESETS_OK" ]; then
|
|
387
|
+
err_t ci.proteger.rulesets_unavailable "$REPO"
|
|
388
|
+
exit 1
|
|
389
|
+
fi
|
|
369
390
|
if [ -n "$VERIFY" ]; then
|
|
370
391
|
ecarts=0
|
|
371
392
|
for ruleset in "${rulesets[@]}"; do
|
|
@@ -407,6 +428,7 @@ if [ -n "$integration" ] && ! gh api "repos/$REPO/labels/urgent" --silent 2>/dev
|
|
|
407
428
|
fi
|
|
408
429
|
|
|
409
430
|
if [ -n "$DRY_RUN" ]; then
|
|
431
|
+
[ -n "$RULESETS_OK" ] || attention_t ci.proteger.rulesets_unavailable "$REPO"
|
|
410
432
|
printf '%s\n' "${rulesets[@]}"
|
|
411
433
|
[ -z "$env_json" ] || printf '%s\n' "$env_json"
|
|
412
434
|
exit 0
|
|
@@ -421,7 +443,8 @@ gh api -X PUT "repos/$REPO/actions/permissions/workflow" --silent \
|
|
|
421
443
|
-f default_workflow_permissions=read -F can_approve_pull_request_reviews=true
|
|
422
444
|
|
|
423
445
|
echecs=0
|
|
424
|
-
|
|
446
|
+
[ -n "$RULESETS_OK" ] || { attention_t ci.proteger.rulesets_unavailable "$REPO"; rulesets=(); }
|
|
447
|
+
for ruleset in ${rulesets[@]+"${rulesets[@]}"}; do
|
|
425
448
|
name="$(sed -n 's/^ "name": "\(.*\)",$/\1/p' <<<"$ruleset")"
|
|
426
449
|
# Ruleset posé sous l'ancien nom (repogarde, v3) : repris et renommé, sinon
|
|
427
450
|
# il resterait à côté et exigerait une vérification « repogarde » disparue
|
|
@@ -454,6 +477,12 @@ for ruleset in "${rulesets[@]}"; do
|
|
|
454
477
|
else
|
|
455
478
|
ok_t ci.proteger.11 "$name"
|
|
456
479
|
fi
|
|
480
|
+
elif [ "$name" = "repowarden (tags)" ] && [ "$owner_type" = Organization ] &&
|
|
481
|
+
[[ "$reponse" == *"ruleset source or owner organization"* ]] &&
|
|
482
|
+
gh api -X "$methode" "$chemin" --input - <<<"$(tags_ruleset User)" >/dev/null 2>&1; then
|
|
483
|
+
# Exception GitHub Actions refusée (App non installée dans l'organisation) :
|
|
484
|
+
# règle des comptes personnels, tags ni déplacés ni supprimés
|
|
485
|
+
attention_t ci.proteger.tags_fallback
|
|
457
486
|
else
|
|
458
487
|
erreur_github "$reponse"
|
|
459
488
|
err_t ci.proteger.api_error "$name" "$REPLY"
|