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