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 CHANGED
@@ -99,7 +99,11 @@ Sans Node : `git clone https://github.com/simdevWarden/repowarden.git ~/repoward
99
99
 
100
100
  ### 2. Dans un projet d'équipe : les mêmes règles pour tous
101
101
 
102
- Chaque personne de l'équipe installe repowarden une fois sur son poste (partie 1). Le projet, lui, ajoute deux fichiers, à copier depuis [`templates/project/`](templates/project) :
102
+ Chaque personne de l'équipe installe repowarden une fois sur son poste (partie 1).
103
+
104
+ **Sur GitHub, en une commande** : dans le projet, `repowarden bots` (aussi proposé par `repowarden install`) configure les bots du dépôt, avec ton accord pour chacun : CI, releases, nettoyage des branches, tickets, GitHub App, protection, npm. Il demande le flux (`main` seule, ou `develop` → `main`), montre l'aperçu, puis attend ta confirmation ; un fichier existant n'est jamais écrasé.
105
+
106
+ **À la main**, le projet ajoute deux fichiers, à copier depuis [`templates/project/`](templates/project) :
103
107
 
104
108
  | Fichier | Rôle |
105
109
  |---|---|
package/bin/app CHANGED
@@ -4,6 +4,10 @@
4
4
  # sans CI, merge automatique sans préversion…) ; un jeton de l'App, si.
5
5
  #
6
6
  # repowarden app init [--repo propriétaire/nom] [--nom NOM] [--administration]
7
+ # repowarden app init --bot App du bot d'organisation (repowarden bot),
8
+ # dans le dépôt de pilotage : workflows et
9
+ # administration en plus ; identifiants
10
+ # REPOWARDEN_BOT_CLIENT_ID, REPOWARDEN_BOT_KEY
7
11
  #
8
12
  # 1. création (flux « manifest » de GitHub) : une page locale envoie la
9
13
  # description de l'App à GitHub, tu cliques sur « Create » ; permissions
@@ -19,16 +23,17 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
19
23
  # shellcheck source=../hooks/lib/common.sh
20
24
  source "$ROOT/hooks/lib/common.sh"
21
25
 
22
- aide() { sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; }
26
+ aide() { sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; }
23
27
 
24
28
  commande="${1:-}"
25
29
  [ $# -eq 0 ] || shift
26
- REPO="" NOM="" ADMIN=""
30
+ REPO="" NOM="" ADMIN="" BOT=""
27
31
  while [ $# -gt 0 ]; do
28
32
  case "$1" in
29
33
  --repo) shift; REPO="${1:?--repo attend propriétaire/nom}" ;;
30
34
  --nom) shift; NOM="${1:?--nom attend un nom}" ;;
31
35
  --administration) ADMIN=1 ;;
36
+ --bot) BOT=1 ADMIN=1 ;;
32
37
  -h | --help) aide; exit 0 ;;
33
38
  *) err_t install.unknown_arg "$1"; exit 1 ;;
34
39
  esac
@@ -44,6 +49,12 @@ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
44
49
  command -v node >/dev/null || { err_t app.no_node; exit 1; }
45
50
  [ -n "$REPO" ] || REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
46
51
  owner="${REPO%%/*}"
52
+ # App du bot : identifiants à part (dépôt de pilotage), droits sur les workflows
53
+ PREFIXE=REPOWARDEN_APP
54
+ if [ -n "$BOT" ]; then
55
+ PREFIXE=REPOWARDEN_BOT
56
+ NOM="${NOM:-repowarden-bot-$owner}"
57
+ fi
47
58
  NOM="${NOM:-repowarden-$owner}"
48
59
  brand "app · $REPO"
49
60
 
@@ -58,10 +69,10 @@ ouvrir() {
58
69
 
59
70
  # 1. Création, si l'App n'est pas déjà configurée pour ce dépôt
60
71
  step_t app.step_create
61
- client_id="$(gh variable get REPOWARDEN_APP_CLIENT_ID --repo "$REPO" 2>/dev/null || true)"
62
- if [ -n "$client_id" ] && grep -q '^REPOWARDEN_APP_KEY' <<<"$(gh secret list --repo "$REPO")"; then
72
+ client_id="$(gh variable get "${PREFIXE}_CLIENT_ID" --repo "$REPO" 2>/dev/null || true)"
73
+ if [ -n "$client_id" ] && grep -q "^${PREFIXE}_KEY" <<<"$(gh secret list --repo "$REPO")"; then
63
74
  ok_t app.exists "$client_id"
64
- slug="$(gh variable get REPOWARDEN_APP_SLUG --repo "$REPO" 2>/dev/null || true)"
75
+ slug="$(gh variable get "${PREFIXE}_SLUG" --repo "$REPO" 2>/dev/null || true)"
65
76
  else
66
77
  if [ "$(gh api "users/$owner" -q .type)" = Organization ]; then
67
78
  cible="https://github.com/organizations/$owner/settings/apps/new"
@@ -72,7 +83,7 @@ else
72
83
  trap 'rm -f "$sortie"' EXIT
73
84
  # Page locale : envoie la description à GitHub (formulaire POST), puis
74
85
  # reçoit le code de retour ; le code est écrit sur la sortie standard
75
- NOM="$NOM" REPO="$REPO" ADMIN="$ADMIN" CIBLE="$cible" node -e '
86
+ NOM="$NOM" REPO="$REPO" ADMIN="$ADMIN" BOT="$BOT" CIBLE="$cible" node -e '
76
87
  const http = require("http"), crypto = require("crypto");
77
88
  const etat = crypto.randomBytes(16).toString("hex");
78
89
  const srv = http.createServer((req, res) => {
@@ -88,6 +99,8 @@ else
88
99
  const port = srv.address().port;
89
100
  const perms = { contents: "write", pull_requests: "write", issues: "write", statuses: "write", metadata: "read" };
90
101
  if (process.env.ADMIN) perms.administration = "write";
102
+ // Bot : PR de migration qui modifient les workflows des projets
103
+ if (process.env.BOT) perms.workflows = "write";
91
104
  const manifeste = JSON.stringify({
92
105
  name: process.env.NOM, url: "https://github.com/" + process.env.REPO,
93
106
  // Webhook inactif : GitHub exige tout de même son adresse
@@ -117,11 +130,11 @@ else
117
130
  slug="$(node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).slug' <<<"$reponse")"
118
131
  # Clé gardée en mémoire le temps de vérifier l'installation, jamais affichée
119
132
  cle="$(node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(0, "utf8")).pem)' <<<"$reponse")"
120
- printf '%s' "$cle" | gh secret set REPOWARDEN_APP_KEY --repo "$REPO"
133
+ printf '%s' "$cle" | gh secret set "${PREFIXE}_KEY" --repo "$REPO"
121
134
  reponse=""
122
- gh variable set REPOWARDEN_APP_CLIENT_ID --repo "$REPO" --body "$client_id"
123
- gh variable set REPOWARDEN_APP_SLUG --repo "$REPO" --body "$slug"
124
- ok_t app.created "$slug"
135
+ gh variable set "${PREFIXE}_CLIENT_ID" --repo "$REPO" --body "$client_id"
136
+ gh variable set "${PREFIXE}_SLUG" --repo "$REPO" --body "$slug"
137
+ ok_t app.created "$slug" "${PREFIXE}_CLIENT_ID" "${PREFIXE}_KEY"
125
138
  fi
126
139
 
127
140
  # 2. Installation sur le dépôt (vérifiée depuis tes installations)
@@ -147,6 +160,7 @@ if [ -z "$cle" ]; then
147
160
  exit 0
148
161
  fi
149
162
  if [ -n "$slug" ] && ! installee; then
163
+ [ -z "$BOT" ] || info_t app.bot_all_repos
150
164
  info_t app.open "https://github.com/apps/$slug/installations/new"
151
165
  ouvrir "https://github.com/apps/$slug/installations/new"
152
166
  for _ in $(seq 1 120); do installee && break; sleep 5; done
package/bin/bot ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ # Bot d'organisation : tient à jour les projets où l'App du bot est installée.
3
+ # repowarden bot [--repo o/r]… [--dry-run] [--rapport FICHIER]
4
+ # passage : protection (proteger) et migration
5
+ # (migrer --pr) de chaque projet adopté
6
+ # repowarden bot rapport FICHIER
7
+ # rapport publié dans l'issue « Rapport du bot
8
+ # repowarden » du dépôt courant (créée ou mise à jour)
9
+ # repowarden bot init workflow du dépôt de pilotage (.github/workflows/bot.yml)
10
+ # Un projet est adopté s'il a un .repowarden.conf ; sa clé bot y décide :
11
+ # auto (défaut : protection appliquée, PR de migration), pr (PR seulement),
12
+ # off (ignoré). GH_TOKEN : jeton d'installation de l'App (repowarden app init --bot).
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
+ # Scripts appelés (remplaçables dans les tests) et origine des clones
22
+ BIN="${REPOWARDEN_BIN:-$ROOT/bin}"
23
+ GIT_URL="${REPOWARDEN_BOT_GIT:-https://github.com}"
24
+ TITRE_RAPPORT="Rapport du bot repowarden"
25
+
26
+ commande="${1:-}"
27
+ case "$commande" in
28
+ init | rapport) shift ;;
29
+ -h | --help) aide; exit 0 ;;
30
+ *) commande=passage ;;
31
+ esac
32
+
33
+ command -v gh >/dev/null || { err_t bot.no_gh; exit 1; }
34
+
35
+ # --- repowarden bot init : workflow du dépôt de pilotage -------------------
36
+ if [ "$commande" = init ]; then
37
+ [ -n "$GIT_TOPLEVEL" ] || { err_t install.not_repo; exit 1; }
38
+ f="$GIT_TOPLEVEL/.github/workflows/bot.yml"
39
+ if [ -e "$f" ]; then
40
+ attention_t bot.init_exists ".github/workflows/bot.yml"
41
+ exit 0
42
+ fi
43
+ mkdir -p "${f%/*}"
44
+ cat >"$f" <<'YAML'
45
+ # Bot repowarden : chaque nuit, tient à jour les projets où son App est
46
+ # installée (protection, PR de migration) ; rapport dans l'issue « Rapport du
47
+ # bot repowarden ». Identifiants : repowarden app init --bot.
48
+ name: bot
49
+
50
+ on:
51
+ schedule:
52
+ - cron: "17 3 * * *"
53
+ workflow_dispatch:
54
+ inputs:
55
+ dry-run:
56
+ description: Voir ce qui serait fait, sans rien modifier
57
+ type: boolean
58
+ default: false
59
+
60
+ # Un passage à la fois
61
+ concurrency:
62
+ group: bot
63
+ cancel-in-progress: false
64
+
65
+ permissions: {}
66
+
67
+ jobs:
68
+ bot:
69
+ uses: simdevWarden/repowarden/.github/workflows/bot.yml@v4
70
+ permissions: { contents: read, issues: write }
71
+ with:
72
+ app-client-id: ${{ vars.REPOWARDEN_BOT_CLIENT_ID }}
73
+ dry-run: ${{ inputs.dry-run || false }}
74
+ secrets:
75
+ app-key: ${{ secrets.REPOWARDEN_BOT_KEY }}
76
+ YAML
77
+ ok_t bot.init_done ".github/workflows/bot.yml"
78
+ dim_t bot.init_next
79
+ exit 0
80
+ fi
81
+
82
+ # --- repowarden bot rapport FICHIER : issue unique du dépôt de pilotage -----
83
+ if [ "$commande" = rapport ]; then
84
+ fichier="${1:?repowarden bot rapport FICHIER}"
85
+ [ -f "$fichier" ] || { err_t bot.no_report "$fichier"; exit 1; }
86
+ numero="$(gh issue list --state open --search "\"$TITRE_RAPPORT\" in:title" --json number,title \
87
+ --jq ".[] | select(.title == \"$TITRE_RAPPORT\") | .number" | sed -n 1p)"
88
+ if [ -n "$numero" ]; then
89
+ gh issue edit "$numero" --body-file "$fichier" >/dev/null
90
+ ok_t bot.report_updated "$numero"
91
+ else
92
+ gh issue create --title "$TITRE_RAPPORT" --body-file "$fichier" >/dev/null
93
+ ok_t bot.report_created
94
+ fi
95
+ exit 0
96
+ fi
97
+
98
+ # --- Passage ----------------------------------------------------------------
99
+ REPOS="" DRY_RUN="" RAPPORT=""
100
+ while [ $# -gt 0 ]; do
101
+ case "$1" in
102
+ --repo) shift; REPOS="$REPOS ${1:?--repo attend propriétaire/nom}" ;;
103
+ --dry-run) DRY_RUN=1 ;;
104
+ --rapport) shift; RAPPORT="${1:?--rapport attend un fichier}" ;;
105
+ *) err_t install.unknown_arg "$1"; exit 1 ;;
106
+ esac
107
+ shift
108
+ done
109
+ if [ -z "$REPOS" ]; then
110
+ REPOS="$(gh api installation/repositories --paginate \
111
+ --jq '.repositories[] | select(.archived | not) | .full_name')"
112
+ fi
113
+
114
+ TRAVAIL="$(mktemp -d)"
115
+ trap 'rm -rf "$TRAVAIL"' EXIT
116
+ # Identité des commits du bot (workflow : <slug>[bot]), crédits de gh
117
+ NOM_BOT="${REPOWARDEN_BOT_NAME:-repowarden-bot}"
118
+ COURRIEL_BOT="${REPOWARDEN_BOT_EMAIL:-repowarden-bot@users.noreply.github.com}"
119
+
120
+ # Dernière ligne d'erreur d'une sortie (✖ …), pour le rapport
121
+ derniere_erreur_r() {
122
+ REPLY="$(grep '✖' <<<"$1" | tail -n 1 | sed 's/^[^✖]*✖ *//')"
123
+ [ -n "$REPLY" ] || REPLY="$(tail -n 1 <<<"$1")"
124
+ REPLY="${REPLY//|/\\|}"
125
+ }
126
+
127
+ # Vérifications exigées : requiredChecks du projet, sinon celles du ruleset
128
+ # repowarden en ligne (jamais retirées), sinon repowarden
129
+ verifications_r() { # $1 dépôt, $2 configuration
130
+ REPLY="$(git config -f "$2" repowarden.requiredChecks 2>/dev/null || true)"
131
+ [ -z "$REPLY" ] || return 0
132
+ local id
133
+ id="$(gh api "repos/$1/rulesets" --jq '.[] | select(.name == "repowarden") | .id' 2>/dev/null || true)"
134
+ [ -z "$id" ] || REPLY="$(gh api "repos/$1/rulesets/$id" \
135
+ --jq '[.rules[] | select(.type == "required_status_checks") | .parameters.required_status_checks[].context] | join(",")' \
136
+ 2>/dev/null || true)"
137
+ REPLY="${REPLY:-repowarden}"
138
+ }
139
+
140
+ t bot.col_repo; c1="$REPLY"
141
+ t bot.col_mode; c2="$REPLY"
142
+ t bot.col_protection; c3="$REPLY"
143
+ t bot.col_migration; c4="$REPLY"
144
+ # Sorties de migrer reconnues : rien à migrer, PR déjà ouverte
145
+ t mig.nothing; MIG_RIEN="$REPLY"
146
+ t mig.pr_open ""; MIG_OUVERTE="${REPLY%%(*}"
147
+ lignes="| $c1 | $c2 | $c3 | $c4 |"$'\n'"|---|---|---|---|"
148
+ erreurs=0
149
+ ligne() { lignes="$lignes"$'\n'"| \`$1\` | $2 | $3 | $4 |"; }
150
+
151
+ for depot in $REPOS; do
152
+ step_t bot.repo "$depot"
153
+ conf="$TRAVAIL/${depot//\//_}.conf"
154
+ if ! gh api -H "Accept: application/vnd.github.raw" "repos/$depot/contents/.repowarden.conf" >"$conf" 2>/dev/null &&
155
+ ! gh api -H "Accept: application/vnd.github.raw" "repos/$depot/contents/.repogarde.conf" >"$conf" 2>/dev/null; then
156
+ t bot.not_adopted; info "$REPLY"
157
+ ligne "$depot" "—" "$REPLY" "—"
158
+ continue
159
+ fi
160
+ sed -i.bak 's/^\[repogarde\]/[repowarden]/' "$conf"
161
+ mode="$(git config -f "$conf" repowarden.bot 2>/dev/null || echo auto)"
162
+ case "$mode" in
163
+ off)
164
+ t bot.ignored; info "$REPLY"
165
+ ligne "$depot" off "$REPLY" "—"
166
+ continue ;;
167
+ auto | pr) ;;
168
+ *)
169
+ t bot.mode_invalid "$mode"; err "$REPLY"
170
+ ligne "$depot" "\`$mode\`" "✖ $REPLY" "—"
171
+ erreurs=$((erreurs + 1))
172
+ continue ;;
173
+ esac
174
+
175
+ clone="$TRAVAIL/${depot//\//_}"
176
+ if ! git clone -q --depth 1 -c credential.helper= -c 'credential.helper=!gh auth git-credential' \
177
+ "$GIT_URL/$depot.git" "$clone" 2>"$TRAVAIL/clone.err"; then
178
+ derniere_erreur_r "$(cat "$TRAVAIL/clone.err")"
179
+ err_t bot.clone_failed "$REPLY"
180
+ ligne "$depot" "$mode" "✖ $REPLY" "—"
181
+ erreurs=$((erreurs + 1))
182
+ continue
183
+ fi
184
+ git -C "$clone" config user.name "$NOM_BOT"
185
+ git -C "$clone" config user.email "$COURRIEL_BOT"
186
+
187
+ # Protection : vérifiée, appliquée seulement en auto (hors --dry-run)
188
+ verifications_r "$depot" "$conf"
189
+ checks="$REPLY"
190
+ if sortie="$(cd "$clone" && bash "$BIN/proteger" --repo "$depot" --checks "$checks" --verifier 2>&1)"; then
191
+ t bot.prot_ok
192
+ elif [ "$mode" = pr ] || [ -n "$DRY_RUN" ]; then
193
+ t bot.prot_stale
194
+ elif sortie="$(cd "$clone" && bash "$BIN/proteger" --repo "$depot" --checks "$checks" 2>&1)"; then
195
+ t bot.prot_applied
196
+ else
197
+ derniere_erreur_r "$sortie"
198
+ t bot.error "$REPLY"
199
+ erreurs=$((erreurs + 1))
200
+ fi
201
+ protection="$REPLY"
202
+ info "$protection"
203
+
204
+ # Migration : par PR seulement (dans le clone jetable en --dry-run)
205
+ options=(--pr)
206
+ [ -z "$DRY_RUN" ] || options=()
207
+ if sortie="$(cd "$clone" && bash "$BIN/migrer" ${options[@]+"${options[@]}"} 2>&1)"; then
208
+ case "$sortie" in
209
+ *"$MIG_RIEN"*) t bot.mig_none ;;
210
+ *"$MIG_OUVERTE"*) t bot.mig_open ;;
211
+ *) if [ -n "$DRY_RUN" ]; then t bot.mig_todo; else t bot.mig_pr; fi ;;
212
+ esac
213
+ else
214
+ derniere_erreur_r "$sortie"
215
+ t bot.error "$REPLY"
216
+ erreurs=$((erreurs + 1))
217
+ fi
218
+ migration="$REPLY"
219
+ info "$migration"
220
+ ligne "$depot" "$mode" "$protection" "$migration"
221
+ done
222
+
223
+ t bot.report_head "$(date -u '+%Y-%m-%d %H:%M UTC')"
224
+ entete="$REPLY"
225
+ [ -z "$DRY_RUN" ] || { t bot.report_dry; entete="$entete $REPLY"; }
226
+ rapport="## $TITRE_RAPPORT"$'\n\n'"$entete"$'\n\n'"$lignes"
227
+ echo
228
+ echo "$rapport"
229
+ [ -z "$RAPPORT" ] || echo "$rapport" >"$RAPPORT"
230
+ [ -z "${GITHUB_STEP_SUMMARY:-}" ] || echo "$rapport" >>"$GITHUB_STEP_SUMMARY"
231
+ [ "$erreurs" = 0 ] || { err_t bot.failed "$erreurs"; exit 1; }