repowarden 4.8.4-next.1 → 4.9.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/app +20 -8
- package/bin/bots +21 -0
- package/bin/npm-publication +37 -67
- package/bin/proteger +24 -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 +16 -14
- package/hooks/lib/i18n/fr.sh +16 -14
- package/hooks/lib/tickets.sh +9 -5
- package/package.json +1 -1
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
|
|
|
@@ -75,9 +75,20 @@ if [ -n "$client_id" ] && grep -q "^${PREFIXE}_KEY" <<<"$(gh secret list --repo
|
|
|
75
75
|
slug="$(gh variable get "${PREFIXE}_SLUG" --repo "$REPO" 2>/dev/null || true)"
|
|
76
76
|
else
|
|
77
77
|
if [ "$(gh api "users/$owner" -q .type)" = Organization ]; then
|
|
78
|
-
|
|
78
|
+
apps="https://github.com/organizations/$owner/settings/apps"
|
|
79
79
|
else
|
|
80
|
-
|
|
80
|
+
apps="https://github.com/settings/apps"
|
|
81
|
+
fi
|
|
82
|
+
cible="$apps/new"
|
|
83
|
+
# Nom déjà pris (souvent une App orpheline d'un essai interrompu) : GitHub
|
|
84
|
+
# refuserait la création ; arrêt avec la marche à suivre. Slug : nom en
|
|
85
|
+
# minuscules, autres caractères en tirets ; une App privée d'un autre
|
|
86
|
+
# compte reste invisible (rappel à l'ouverture de la page)
|
|
87
|
+
slug_voulu="$(printf '%s' "$NOM" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')"
|
|
88
|
+
if [ "$(gh api "apps/$slug_voulu" -q .slug 2>/dev/null || true)" = "$slug_voulu" ]; then
|
|
89
|
+
err_t app.name_taken "$NOM"
|
|
90
|
+
info_t app.name_taken_fix "$apps"
|
|
91
|
+
exit 1
|
|
81
92
|
fi
|
|
82
93
|
sortie="$(mktemp)"
|
|
83
94
|
trap 'rm -f "$sortie"' EXIT
|
|
@@ -120,10 +131,11 @@ else
|
|
|
120
131
|
port="$(sed -n 's/^PORT //p' "$sortie")"
|
|
121
132
|
[ -n "$port" ] || { err_t app.server_failed; exit 1; }
|
|
122
133
|
info_t app.open "http://127.0.0.1:$port/"
|
|
134
|
+
dim_t app.name_hint "$NOM" "$apps"
|
|
123
135
|
ouvrir "http://127.0.0.1:$port/"
|
|
124
136
|
wait "$serveur" || true
|
|
125
137
|
code="$(sed -n 's/^CODE //p' "$sortie")"
|
|
126
|
-
[ -n "$code" ] || { err_t app.no_code; exit 1; }
|
|
138
|
+
[ -n "$code" ] || { err_t app.no_code; info_t app.name_taken_fix "$apps"; exit 1; }
|
|
127
139
|
# Conversion du code : identifiants de l'App, rangés sans être affichés
|
|
128
140
|
reponse="$(gh api -X POST "app-manifests/$code/conversions")"
|
|
129
141
|
client_id="$(node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).client_id' <<<"$reponse")"
|
|
@@ -153,10 +165,10 @@ installee() {
|
|
|
153
165
|
}
|
|
154
166
|
cle="${cle:-}"
|
|
155
167
|
if [ -z "$cle" ]; then
|
|
156
|
-
# Relance
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
# Relance : la clé privée n'est pas ici, et le jeton de gh ne peut pas
|
|
169
|
+
# lister les installations d'une App (user/installations : 403) ; état
|
|
170
|
+
# « non vérifié » plutôt qu'un succès non constaté
|
|
171
|
+
attention_t app.not_verified "${slug:-?}" "$REPO" "https://github.com/apps/${slug:-<slug>}/installations/new"
|
|
160
172
|
exit 0
|
|
161
173
|
fi
|
|
162
174
|
if [ -n "$slug" ] && ! installee; then
|
package/bin/bots
CHANGED
|
@@ -252,6 +252,27 @@ YAML
|
|
|
252
252
|
app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }}
|
|
253
253
|
secrets:
|
|
254
254
|
app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
|
|
255
|
+
YAML
|
|
256
|
+
fi
|
|
257
|
+
fi
|
|
258
|
+
# Tickets livrés : statut done et fermeture à la release, préprod à la
|
|
259
|
+
# préversion ; seulement avec le suivi des tickets (sinon le job échoue)
|
|
260
|
+
if choisi tickets || [ -e .github/workflows/tickets.yml ]; then
|
|
261
|
+
cat <<'YAML'
|
|
262
|
+
|
|
263
|
+
tickets:
|
|
264
|
+
needs: release
|
|
265
|
+
if: needs.release.outputs.release_created == 'true' || needs.release.outputs.prerelease_created == 'true'
|
|
266
|
+
uses: simdevWarden/repowarden/.github/workflows/tickets.yml@v4
|
|
267
|
+
permissions: { contents: write, issues: write, pull-requests: write, statuses: write }
|
|
268
|
+
with:
|
|
269
|
+
tag: ${{ needs.release.outputs.tag_name || needs.release.outputs.prerelease_tag }}
|
|
270
|
+
YAML
|
|
271
|
+
if [ -n "$AVEC_APP" ]; then
|
|
272
|
+
cat <<'YAML'
|
|
273
|
+
app-client-id: ${{ vars.REPOWARDEN_APP_CLIENT_ID }}
|
|
274
|
+
secrets:
|
|
275
|
+
app-key: ${{ secrets.REPOWARDEN_APP_KEY }}
|
|
255
276
|
YAML
|
|
256
277
|
fi
|
|
257
278
|
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
|
package/bin/proteger
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
# - tags v* : créés, déplacés ou supprimés uniquement par les workflows ;
|
|
8
8
|
# - GitHub Actions autorisé à créer des PR (releases automatiques) ;
|
|
9
9
|
# - validation humaine (optionnelle) : approbations obligatoires, revue des
|
|
10
|
-
# CODEOWNERS, environnement de déploiement approuvé par des personnes
|
|
10
|
+
# CODEOWNERS, environnement de déploiement approuvé par des personnes ;
|
|
11
|
+
# livraison approuvée (deliveryApproval deployment ou review, défaut) ou
|
|
12
|
+
# automatique (auto : vérifications exigées gardées).
|
|
11
13
|
#
|
|
12
14
|
# bin/proteger [--repo propriétaire/nom] [--checks "repowarden,tests (ubuntu-latest)"] [--relecteurs N]
|
|
13
15
|
# [--codeowners] [--environnement NOM] [--approbateurs "login …"]
|
|
@@ -54,7 +56,7 @@ while [ $# -gt 0 ]; do
|
|
|
54
56
|
--sans-tags) NO_TAGS=1 ;;
|
|
55
57
|
--dry-run) DRY_RUN=1 ;;
|
|
56
58
|
--verifier) VERIFY=1 ;;
|
|
57
|
-
-h | --help) sed -n '2,
|
|
59
|
+
-h | --help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
58
60
|
*) err_t install.unknown_arg "$1"; exit 1 ;;
|
|
59
61
|
esac
|
|
60
62
|
shift
|
|
@@ -189,14 +191,21 @@ if [ -n "$integration" ]; then
|
|
|
189
191
|
# l'approbation de l'environnement de livraison (« Approve and deploy »),
|
|
190
192
|
# exigée par une règle de main (déploiement réussi) ; la revue de PR n'est
|
|
191
193
|
# plus nécessaire (relectures configurées : gardées). review : revue de PR.
|
|
194
|
+
# auto : choix du projet, la validation humaine est le merge de chaque
|
|
195
|
+
# ticket ; la GitHub App arme le merge de la livraison (release-auto,
|
|
196
|
+
# delivery-auto), qui part quand les vérifications exigées sont vertes.
|
|
197
|
+
# Ni règle de déploiement ni environnement créé d'office ; vérifications
|
|
198
|
+
# et relectures configurées gardées.
|
|
192
199
|
cfg_r deliveryApproval deployment
|
|
193
200
|
DELIVERY="$REPLY"
|
|
194
|
-
[[ "$DELIVERY" =~ ^(deployment|review)$ ]] || { err_t ci.proteger.delivery_invalid; exit 1; }
|
|
201
|
+
[[ "$DELIVERY" =~ ^(deployment|review|auto)$ ]] || { err_t ci.proteger.delivery_invalid; exit 1; }
|
|
195
202
|
deploiement=""
|
|
196
203
|
if [ "$DELIVERY" = deployment ]; then
|
|
197
204
|
ENVIRONMENT="${ENVIRONMENT:-production}"
|
|
198
205
|
livraison="$REVIEWS"
|
|
199
206
|
deploiement="{\"type\": \"required_deployments\", \"parameters\": {\"required_deployment_environments\": [\"$ENVIRONMENT\"]}}"
|
|
207
|
+
elif [ "$DELIVERY" = auto ]; then
|
|
208
|
+
livraison="$REVIEWS"
|
|
200
209
|
fi
|
|
201
210
|
# PR à jour non exigée non plus : le merge commit d'une livraison n'existe
|
|
202
211
|
# que sur main ; strict, la livraison suivante serait bloquée tant que
|
|
@@ -228,6 +237,11 @@ if [ -n "$integration" ]; then
|
|
|
228
237
|
cfg_r autoMergeWork true
|
|
229
238
|
auto_merge="$REPLY"
|
|
230
239
|
[[ "$auto_merge" =~ ^(true|false)$ ]] || { err_t ci.proteger.automerge_invalid; exit 1; }
|
|
240
|
+
# Livraison automatique : merge automatique du dépôt indispensable
|
|
241
|
+
if [ "$DELIVERY" = auto ] && [ "$auto_merge" = false ]; then
|
|
242
|
+
err_t ci.proteger.auto_needs_automerge
|
|
243
|
+
exit 1
|
|
244
|
+
fi
|
|
231
245
|
# Sans ce workflow, les branches mergées ne seraient plus supprimées
|
|
232
246
|
if ! grep -qs "nettoyage-branches.yml" "$GIT_TOPLEVEL"/.github/workflows/*.y*ml; then
|
|
233
247
|
attention_t ci.proteger.no_cleanup
|
|
@@ -238,6 +252,11 @@ if [ -n "$integration" ]; then
|
|
|
238
252
|
! grep -qs "delivery-environment:" "$GIT_TOPLEVEL"/.github/workflows/*.y*ml; then
|
|
239
253
|
attention_t ci.proteger.no_delivery_env "$ENVIRONMENT"
|
|
240
254
|
fi
|
|
255
|
+
# Livraison automatique que rien n'arme : la PR se mergera à la main
|
|
256
|
+
if [ "$DELIVERY" = auto ] &&
|
|
257
|
+
! grep -qsE "^[[:space:]]*delivery-auto:[[:space:]]*true" "$GIT_TOPLEVEL"/.github/workflows/*.y*ml; then
|
|
258
|
+
attention_t ci.proteger.no_delivery_auto
|
|
259
|
+
fi
|
|
241
260
|
# Demande de livraison (manuelle, planifiée) qui ne remplace pas celle en
|
|
242
261
|
# attente : une demande oubliée bloquerait en silence les suivantes
|
|
243
262
|
for wf in "$GIT_TOPLEVEL"/.github/workflows/*.y*ml; do
|
|
@@ -276,6 +295,8 @@ echo_t ci.proteger.3 "$CHECKS"
|
|
|
276
295
|
echo_t ci.proteger.4 "$methods" "$delete_on_merge"
|
|
277
296
|
if [ -n "$integration" ] && [ "$DELIVERY" = deployment ]; then
|
|
278
297
|
echo_t ci.proteger.delivery_deploy "$ENVIRONMENT" "$auto_merge"
|
|
298
|
+
elif [ -n "$integration" ] && [ "$DELIVERY" = auto ]; then
|
|
299
|
+
echo_t ci.proteger.delivery_auto "$livraison"
|
|
279
300
|
elif [ -n "$integration" ]; then
|
|
280
301
|
echo_t ci.proteger.delivery "$livraison" "$auto_merge"
|
|
281
302
|
fi
|
package/bin/tickets
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
# mainteneur → backlog, sinon à valider ; --sans-validation-auto : tous à
|
|
13
13
|
# valider) ; branches des PR ouvertes et mergées inscrites sur leurs tickets.
|
|
14
14
|
# Ensuite : committer ces fichiers, exiger la vérification « ticket »
|
|
15
|
-
# (repowarden proteger --checks "…,ticket") et
|
|
15
|
+
# (repowarden proteger --checks "…,ticket") et, s'il n'y est pas, brancher le
|
|
16
|
+
# job tickets de release.yml (repowarden bots l'y écrit).
|
|
16
17
|
set -euo pipefail
|
|
17
18
|
|
|
18
19
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
|
@@ -41,7 +42,7 @@ if [ "$commande" = sync ]; then
|
|
|
41
42
|
exit 0
|
|
42
43
|
fi
|
|
43
44
|
if [ "$commande" != init ]; then
|
|
44
|
-
sed -n '2,
|
|
45
|
+
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
|
|
45
46
|
[ "$commande" = "" ] || [ "$commande" = -h ] || [ "$commande" = --help ]
|
|
46
47
|
exit
|
|
47
48
|
fi
|
|
@@ -149,4 +150,9 @@ esac
|
|
|
149
150
|
|
|
150
151
|
echo
|
|
151
152
|
ok_t tk.init_done
|
|
152
|
-
|
|
153
|
+
# Job tickets de release.yml déjà branché (modèle de repowarden bots) : rien à ajouter
|
|
154
|
+
if grep -qs 'workflows/tickets\.yml@' .github/workflows/release.yml; then
|
|
155
|
+
dim_t tk.init_next_wired
|
|
156
|
+
else
|
|
157
|
+
dim_t tk.init_next
|
|
158
|
+
fi
|
package/ci/tickets/issue.sh
CHANGED
|
@@ -31,27 +31,56 @@ accepter() {
|
|
|
31
31
|
-q '.[] | [.number, .headRefName, .headRefOid, .baseRefName, .isDraft] | @tsv')
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
# Ticket ouvert par un mainteneur (droits d'écriture) : c'est déjà sa
|
|
35
|
+
# décision, validé d'office. Vérifié par tout événement du ticket (idempotent) :
|
|
36
|
+
# GitHub ne garde qu'un run en attente par groupe de concurrence, le run
|
|
37
|
+
# « opened » peut être annulé par ceux des étiquettes posées à la création.
|
|
38
|
+
# Non si déjà validé, ou (hors ouverture) rangé au-delà de « à valider »
|
|
39
|
+
# (« validé » retiré à la main : décision respectée). Test seul : appelé dans un « if », où set -e
|
|
40
|
+
# ne s'applique pas.
|
|
41
|
+
a_valider_d_office() {
|
|
42
|
+
local labels l
|
|
43
|
+
[ "${AUTO_VALIDATE:-maintainers}" = maintainers ] || return 1
|
|
44
|
+
est_mainteneur "${ISSUE_ASSOCIATION:-}" || return 1
|
|
45
|
+
[ "${ISSUE_STATE:-open}" = open ] || return 1
|
|
46
|
+
labels="$(etiquettes "$ISSUE")"
|
|
47
|
+
! grep -qxF "$VALIDE" <<<"$labels" || return 1
|
|
48
|
+
[ "$ACTION" != opened ] || return 0
|
|
49
|
+
while IFS= read -r l; do
|
|
50
|
+
[[ "$l" == "statut: "* && "$l" != "$S_AVALIDER" ]] && return 1
|
|
51
|
+
done <<<"$labels"
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
valider_d_office() {
|
|
56
|
+
gh issue edit "$ISSUE" --add-label "$VALIDE" >/dev/null
|
|
57
|
+
statut "$ISSUE" "$S_AVALIDER"
|
|
58
|
+
accepter "$ISSUE"
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
cmd_issue() {
|
|
35
62
|
local pr sha head body
|
|
36
63
|
case "$ACTION" in
|
|
37
64
|
opened)
|
|
38
65
|
est_valide "$ISSUE" && return 0
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
est_mainteneur "${ISSUE_ASSOCIATION:-}"; then
|
|
43
|
-
gh issue edit "$ISSUE" --add-label "$VALIDE" >/dev/null
|
|
44
|
-
statut "$ISSUE" "$S_AVALIDER"
|
|
45
|
-
accepter "$ISSUE"
|
|
66
|
+
# Validé d'office, sinon un mainteneur pose « validé »
|
|
67
|
+
if a_valider_d_office; then
|
|
68
|
+
valider_d_office
|
|
46
69
|
else
|
|
47
70
|
statut "$ISSUE" "$S_AVALIDER"
|
|
48
71
|
fi
|
|
49
72
|
;;
|
|
50
73
|
labeled)
|
|
51
|
-
[ "${LABEL:-}" = "$VALIDE" ]
|
|
52
|
-
|
|
74
|
+
if [ "${LABEL:-}" = "$VALIDE" ]; then
|
|
75
|
+
accepter "$ISSUE"
|
|
76
|
+
elif a_valider_d_office; then
|
|
77
|
+
valider_d_office
|
|
78
|
+
fi
|
|
53
79
|
;;
|
|
54
80
|
assigned)
|
|
81
|
+
if a_valider_d_office; then
|
|
82
|
+
valider_d_office
|
|
83
|
+
fi
|
|
55
84
|
# Pris en charge : sa branche (s'il est validé), en cours s'il est
|
|
56
85
|
# validé ET assigné (quelle que soit la présence du label backlog,
|
|
57
86
|
# pour gérer la race validation+assignation quasi simultanée).
|
package/ci/tickets/push.sh
CHANGED
|
@@ -34,11 +34,32 @@ commit_impact_r() {
|
|
|
34
34
|
REPLY="$meilleur"
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
# REPLY =
|
|
38
|
-
#
|
|
39
|
-
#
|
|
37
|
+
# REPLY = en-tête du premier commit de la branche $1 par rapport à la branche
|
|
38
|
+
# d'intégration, sans « (#N) » final ; vide s'il n'est pas utilisable (aucun
|
|
39
|
+
# commit, non conforme, plus de 72 caractères). Lu par l'API : le checkout du
|
|
40
|
+
# workflow est celui de repowarden, pas l'historique du projet.
|
|
41
|
+
premier_commit_r() {
|
|
42
|
+
local s
|
|
43
|
+
s="$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$1" \
|
|
44
|
+
-q '.commits[0].commit.message // "" | split("\n")[0] // ""' 2>/dev/null || true)"
|
|
45
|
+
s="${s%%$'\n'*}"
|
|
46
|
+
s="${s%$'\r'}"
|
|
47
|
+
[[ "$s" =~ ^(.*[^[:space:]])[[:space:]]+\(#[0-9]+\)$ ]] && s="${BASH_REMATCH[1]}"
|
|
48
|
+
REPLY=""
|
|
49
|
+
[[ "$s" =~ $CC_PATTERN ]] || return 0
|
|
50
|
+
authored_length "$s"
|
|
51
|
+
[ "$REPLY" -le 72 ] || { REPLY=""; return 0; }
|
|
52
|
+
REPLY="$s"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# REPLY = titre de la PR brouillon du ticket $1 (branche $2) : en-tête du
|
|
56
|
+
# premier commit de la branche (déjà validé par les hooks, il devient le commit
|
|
57
|
+
# de squash), sinon « type(portée): sujet » depuis le ticket, sinon le commit
|
|
58
|
+
# conforme de plus fort impact, sinon le titre déduit de la branche
|
|
40
59
|
titre_pr_brouillon_r() {
|
|
41
60
|
local n="$1" branche="$2" type
|
|
61
|
+
premier_commit_r "$branche"
|
|
62
|
+
[ -z "$REPLY" ] || return 0
|
|
42
63
|
ticket_type_r "$(etiquettes "$n")"
|
|
43
64
|
type="$REPLY"
|
|
44
65
|
ticket_pr_title_r "$type" "$(gh issue view "$n" --json title -q .title)"
|
|
@@ -82,7 +103,8 @@ description_pr_brouillon_r() {
|
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
# Premier push sur la branche d'un ticket : PR brouillon vers la branche
|
|
85
|
-
# d'intégration,
|
|
106
|
+
# d'intégration, titrée par le premier commit (repli : le ticket), à la
|
|
107
|
+
# description reprise du ticket, citant le
|
|
86
108
|
# ticket, assignée à l'auteur du push ; ticket en cours. Ouverte par le jeton
|
|
87
109
|
# des Actions, elle ne lance pas la CI : elle tourne au push suivant, ou au
|
|
88
110
|
# passage « prête pour revue ». Tant qu'elle est en brouillon, chaque push met
|
package/hooks/lib/i18n/en.sh
CHANGED
|
@@ -284,7 +284,10 @@ _msg_en() {
|
|
|
284
284
|
ci.proteger.reviews_invalid) _M="--relecteurs: a number from 0 to 9 is expected." ;;
|
|
285
285
|
ci.proteger.codeowners_invalid) _M="codeOwnerReview: true or false." ;;
|
|
286
286
|
ci.proteger.delivery_deploy) _M=" delivery into main: approval of the \"%s\" deployment (Approve and deploy), then merged by the GitHub App; auto-merge of work PRs into develop: %s" ;;
|
|
287
|
-
ci.proteger.delivery_invalid) _M="deliveryApproval: deployment or
|
|
287
|
+
ci.proteger.delivery_invalid) _M="deliveryApproval: deployment, review or auto." ;;
|
|
288
|
+
ci.proteger.auto_needs_automerge) _M="deliveryApproval = auto needs the repository's auto-merge: remove autoMergeWork = false from .repowarden.conf, or choose deliveryApproval = deployment or review." ;;
|
|
289
|
+
ci.proteger.no_delivery_auto) _M="deliveryApproval = auto, but no workflow calls release-auto.yml with delivery-auto: true: nothing will arm the delivery merge, which will be done by hand (see docs/releases.md, automatic delivery)." ;;
|
|
290
|
+
ci.proteger.delivery_auto) _M=" delivery into main: automatic (the GitHub App merges it as soon as the required checks are green; %s approval(s) required)" ;;
|
|
288
291
|
ci.proteger.automerge_invalid) _M="autoMergeWork: true or false." ;;
|
|
289
292
|
ci.proteger.unattributed_invalid) _M="unattributedApproval: true or false." ;;
|
|
290
293
|
ci.proteger.no_branch) _M="None of the branches \"%s\" exists on %s." ;;
|
|
@@ -393,19 +396,14 @@ _msg_en() {
|
|
|
393
396
|
npm.old_npm) _M="npm %s is too old: 11.15 or later required (npm install -g npm@11)." ;;
|
|
394
397
|
npm.step_login) _M="1/3 npm account" ;;
|
|
395
398
|
npm.logged) _M="Logged in to npm: %s" ;;
|
|
396
|
-
npm.no_2fa) _M="Two-factor authentication (2FA) is not enabled on the npm account: npm requires it to
|
|
399
|
+
npm.no_2fa) _M="Two-factor authentication (2FA) is not enabled on the npm account: npm requires it to configure tokenless publishing." ;;
|
|
397
400
|
npm.hint_2fa) _M="Enable it: npm profile enable-2fa auth-and-writes (QR code for an authenticator app), or https://www.npmjs.com/settings/%s/tfa; then run this command again." ;;
|
|
398
|
-
npm.
|
|
399
|
-
npm.
|
|
400
|
-
npm.
|
|
401
|
-
npm.
|
|
402
|
-
npm.
|
|
403
|
-
npm.
|
|
404
|
-
npm.dispatched) _M="Pipeline started (%s): it publishes the latest release to npm. Waiting… (Ctrl+C: run the command again later, it resumes)" ;;
|
|
405
|
-
npm.waiting) _M="The next release publishes %s: merge a PR (or let the bot merge the release PR). Waiting… (Ctrl+C: run the command again later, it resumes)" ;;
|
|
406
|
-
npm.still_waiting) _M="still waiting (%s min)…" ;;
|
|
407
|
-
npm.not_yet) _M="Not published yet: run this command again after the next release." ;;
|
|
408
|
-
npm.first_published) _M="%s@%s published by the pipeline." ;;
|
|
401
|
+
npm.step_first) _M="2/3 First publication, from this machine (latest release)" ;;
|
|
402
|
+
npm.no_release) _M="No release yet: nothing is published. Run this command again after the first release of %s." ;;
|
|
403
|
+
npm.would_publish) _M="[dry-run] %s@%s would be published from release %s (npm publish, in this terminal)" ;;
|
|
404
|
+
npm.publishing) _M="Publishing %s@%s (release %s): npm may ask for a validation (browser or code)." ;;
|
|
405
|
+
npm.publish_failed) _M="Publication of %s refused (see above): nothing else was configured. Run this command again." ;;
|
|
406
|
+
npm.first_published) _M="%s@%s published." ;;
|
|
409
407
|
npm.step_trust) _M="3/3 Tokenless publishing (trusted publishing)" ;;
|
|
410
408
|
npm.trust_exists) _M="Trusted publishing already configured." ;;
|
|
411
409
|
npm.trust_mismatch) _M="Existing trust relationship does not match expected parameters:" ;;
|
|
@@ -467,7 +465,10 @@ _msg_en() {
|
|
|
467
465
|
app.step_install) _M="Installing on the repository" ;;
|
|
468
466
|
app.installed) _M="App %s installed on %s." ;;
|
|
469
467
|
app.not_installed) _M="App %s not installed on %s yet: run repowarden app init again once installed." ;;
|
|
470
|
-
app.
|
|
468
|
+
app.not_verified) _M="Installation of App %s on %s not verified: the private key is not on this machine (only the App can list its installations). If it is not installed, workflows will fail at the \"Jeton de l'App\" step. To install it: %s" ;;
|
|
469
|
+
app.name_taken) _M="A GitHub App named %s already exists (often an orphan App from an interrupted attempt: created on GitHub, credentials never stored)." ;;
|
|
470
|
+
app.name_taken_fix) _M="Either run again with --nom <other name>, or delete the orphan App (Settings → Developer settings → GitHub Apps → the App → Advanced → Delete): %s" ;;
|
|
471
|
+
app.name_hint) _M="If GitHub refuses the name %s (already taken): Ctrl+C, then run again with --nom <other name>, or delete the orphan App: %s" ;;
|
|
471
472
|
app.done) _M="repowarden workflows now use the App: what they do triggers the other workflows." ;;
|
|
472
473
|
app.next) _M="Without the App (variable and secret missing), they fall back to the Actions token." ;;
|
|
473
474
|
app.bot_all_repos) _M="Organization bot: when installing, choose \"All repositories\" (new repositories are covered automatically; only those with a .repowarden.conf are managed)." ;;
|
|
@@ -579,6 +580,7 @@ _msg_en() {
|
|
|
579
580
|
tk.init_auto_create_on) _M="Auto ticket creation: ENABLED (ticketAutoCreate = true)" ;;
|
|
580
581
|
tk.init_auto_create_off) _M="Auto ticket creation: DISABLED (default). To enable: git config repowarden.ticketAutoCreate true" ;;
|
|
581
582
|
tk.init_next) _M="Next: commit these files; require the \"ticket\" check (repowarden proteger --checks \"…,ticket\"); in release.yml, wire the tickets job (see the Tickets docs)." ;;
|
|
583
|
+
tk.init_next_wired) _M="Next: commit these files; require the \"ticket\" check (repowarden proteger --checks \"…,ticket\"). The tickets job of release.yml is already wired." ;;
|
|
582
584
|
tk.prerelease) _M="Available in pre-release **%s**: to be tested before production." ;;
|
|
583
585
|
tk.released) _M="Released in **%s**." ;;
|
|
584
586
|
# Ticket comment
|
package/hooks/lib/i18n/fr.sh
CHANGED
|
@@ -284,7 +284,10 @@ _msg_fr() {
|
|
|
284
284
|
ci.proteger.reviews_invalid) _M="--relecteurs : nombre de 0 à 9 attendu." ;;
|
|
285
285
|
ci.proteger.codeowners_invalid) _M="codeOwnerReview : true ou false." ;;
|
|
286
286
|
ci.proteger.delivery_deploy) _M=" livraison vers main : approbation du déploiement « %s » (Approve and deploy), puis merge par la GitHub App ; merge automatique des PR de travail vers develop : %s" ;;
|
|
287
|
-
ci.proteger.delivery_invalid) _M="deliveryApproval : deployment ou
|
|
287
|
+
ci.proteger.delivery_invalid) _M="deliveryApproval : deployment, review ou auto." ;;
|
|
288
|
+
ci.proteger.auto_needs_automerge) _M="deliveryApproval = auto demande le merge automatique du dépôt : retirer autoMergeWork = false de .repowarden.conf, ou choisir deliveryApproval = deployment ou review." ;;
|
|
289
|
+
ci.proteger.no_delivery_auto) _M="deliveryApproval = auto, mais aucun workflow n'appelle release-auto.yml avec delivery-auto: true : rien n'armera le merge de la livraison, qui se fera à la main (voir docs/releases.md, livraison automatique)." ;;
|
|
290
|
+
ci.proteger.delivery_auto) _M=" livraison vers main : automatique (la GitHub App la merge dès que les vérifications exigées sont vertes ; %s approbation(s) exigée(s))" ;;
|
|
288
291
|
ci.proteger.automerge_invalid) _M="autoMergeWork : true ou false." ;;
|
|
289
292
|
ci.proteger.unattributed_invalid) _M="unattributedApproval : true ou false." ;;
|
|
290
293
|
ci.proteger.no_branch) _M="Aucune des branches « %s » n'existe sur %s." ;;
|
|
@@ -393,19 +396,14 @@ _msg_fr() {
|
|
|
393
396
|
npm.old_npm) _M="npm %s trop ancien : 11.15 au minimum (npm install -g npm@11)." ;;
|
|
394
397
|
npm.step_login) _M="1/3 Compte npm" ;;
|
|
395
398
|
npm.logged) _M="Connecté à npm : %s" ;;
|
|
396
|
-
npm.no_2fa) _M="Double authentification (2FA) non activée sur le compte npm : npm l'exige pour
|
|
399
|
+
npm.no_2fa) _M="Double authentification (2FA) non activée sur le compte npm : npm l'exige pour configurer la publication sans jeton." ;;
|
|
397
400
|
npm.hint_2fa) _M="Activer : npm profile enable-2fa auth-and-writes (QR code pour une application d'authentification), ou https://www.npmjs.com/settings/%s/tfa ; puis relancer cette commande." ;;
|
|
398
|
-
npm.
|
|
399
|
-
npm.
|
|
400
|
-
npm.
|
|
401
|
-
npm.
|
|
402
|
-
npm.
|
|
403
|
-
npm.
|
|
404
|
-
npm.dispatched) _M="Pipeline lancée (%s) : elle publie la dernière release sur npm. Attente… (Ctrl+C : relancer la commande plus tard, elle reprendra)" ;;
|
|
405
|
-
npm.waiting) _M="La prochaine release publie %s : merge une PR (ou laisse le bot merger la PR de release). Attente… (Ctrl+C : relancer la commande plus tard, elle reprendra)" ;;
|
|
406
|
-
npm.still_waiting) _M="toujours en attente (%s min)…" ;;
|
|
407
|
-
npm.not_yet) _M="Pas encore publié : relance cette commande après la prochaine release." ;;
|
|
408
|
-
npm.first_published) _M="%s@%s publié par la pipeline." ;;
|
|
401
|
+
npm.step_first) _M="2/3 Première publication, depuis ce poste (dernière release)" ;;
|
|
402
|
+
npm.no_release) _M="Aucune release encore : rien n'est publié. Relance cette commande après la première release de %s." ;;
|
|
403
|
+
npm.would_publish) _M="[dry-run] %s@%s serait publié depuis la release %s (npm publish, dans ce terminal)" ;;
|
|
404
|
+
npm.publishing) _M="Publication de %s@%s (release %s) : npm peut demander une validation (navigateur ou code)." ;;
|
|
405
|
+
npm.publish_failed) _M="Publication de %s refusée (voir ci-dessus) : rien d'autre n'a été configuré. Relance cette commande." ;;
|
|
406
|
+
npm.first_published) _M="%s@%s publié." ;;
|
|
409
407
|
npm.step_trust) _M="3/3 Publication sans jeton (publication de confiance)" ;;
|
|
410
408
|
npm.trust_exists) _M="Publication de confiance déjà configurée." ;;
|
|
411
409
|
npm.trust_mismatch) _M="Relation de confiance existante mais ne correspond pas aux paramètres attendus :" ;;
|
|
@@ -467,7 +465,10 @@ _msg_fr() {
|
|
|
467
465
|
app.step_install) _M="Installation sur le dépôt" ;;
|
|
468
466
|
app.installed) _M="App %s installée sur %s." ;;
|
|
469
467
|
app.not_installed) _M="App %s pas encore installée sur %s : relance repowarden app init une fois installée." ;;
|
|
470
|
-
app.
|
|
468
|
+
app.not_verified) _M="Installation de l'App %s sur %s non vérifiée : la clé privée n'est pas sur ce poste (seule l'App peut lister ses installations). Si elle n'est pas installée, les workflows échoueront à l'étape « Jeton de l'App ». L'installer : %s" ;;
|
|
469
|
+
app.name_taken) _M="Une GitHub App s'appelle déjà %s (souvent une App orpheline d'un essai interrompu : créée sur GitHub, identifiants jamais rangés)." ;;
|
|
470
|
+
app.name_taken_fix) _M="Au choix : relancer avec --nom <autre nom>, ou supprimer l'App orpheline (Settings → Developer settings → GitHub Apps → l'App → Advanced → Delete) : %s" ;;
|
|
471
|
+
app.name_hint) _M="Si GitHub refuse le nom %s (déjà utilisé) : Ctrl+C, puis relancer avec --nom <autre nom>, ou supprimer l'App orpheline : %s" ;;
|
|
471
472
|
app.done) _M="Les workflows repowarden utilisent désormais l'App : ce qu'ils font déclenche les autres workflows." ;;
|
|
472
473
|
app.next) _M="Sans l'App (variable et secret absents), ils reprennent le jeton des Actions." ;;
|
|
473
474
|
app.bot_all_repos) _M="Bot d'organisation : à l'installation, choisir « All repositories » (les nouveaux dépôts seront couverts d'office ; seuls ceux avec un .repowarden.conf sont gérés)." ;;
|
|
@@ -579,6 +580,7 @@ _msg_fr() {
|
|
|
579
580
|
tk.init_auto_create_on) _M="Création auto de ticket : ACTIVÉE (ticketAutoCreate = true)" ;;
|
|
580
581
|
tk.init_auto_create_off) _M="Création auto de ticket : DÉSACTIVÉE (défaut). Pour l'activer : git config repowarden.ticketAutoCreate true" ;;
|
|
581
582
|
tk.init_next) _M="Ensuite : committer ces fichiers ; exiger la vérification « ticket » (repowarden proteger --checks \"…,ticket\") ; dans release.yml, brancher le job tickets (voir la doc Tickets)." ;;
|
|
583
|
+
tk.init_next_wired) _M="Ensuite : committer ces fichiers ; exiger la vérification « ticket » (repowarden proteger --checks \"…,ticket\"). Le job tickets de release.yml est déjà branché." ;;
|
|
582
584
|
tk.prerelease) _M="Disponible en préversion **%s** : à tester avant la production." ;;
|
|
583
585
|
tk.released) _M="Publié dans **%s**." ;;
|
|
584
586
|
# Ticket comment
|
package/hooks/lib/tickets.sh
CHANGED
|
@@ -63,8 +63,10 @@ ticket_branch_r() {
|
|
|
63
63
|
# REPLY = titre de PR d'un ticket : $1 type (« feat » par défaut), $2 titre du
|
|
64
64
|
# ticket → « feat(portée): sujet ». Portée = préfixe du titre avant le premier
|
|
65
65
|
# « : » (« Tickets : … » → « tickets »), 20 caractères au plus, omise si vide ;
|
|
66
|
-
# sujet = le reste, première lettre abaissée
|
|
67
|
-
#
|
|
66
|
+
# sujet = le reste, première lettre abaissée sauf sur un sigle (README, IA :
|
|
67
|
+
# initiale suivie d'une majuscule). 72 caractères au plus, coupé à la dernière
|
|
68
|
+
# espace, jamais dans un mot. Vide si le titre ne donne rien (l'appelant décide
|
|
69
|
+
# du repli).
|
|
68
70
|
ticket_pr_title_r() {
|
|
69
71
|
local type="${1:-feat}" titre="$2" portee sujet tete res
|
|
70
72
|
sujet="$titre"
|
|
@@ -75,15 +77,17 @@ ticket_pr_title_r() {
|
|
|
75
77
|
[ "${#portee}" -le 20 ] || portee=""
|
|
76
78
|
sujet="${BASH_REMATCH[2]}"
|
|
77
79
|
fi
|
|
78
|
-
|
|
80
|
+
[[ "${sujet:1:1}" == [[:upper:]] ]] ||
|
|
81
|
+
sujet="$(printf '%s' "${sujet:0:1}" | LC_ALL=C tr '[:upper:]' '[:lower:]')${sujet:1}"
|
|
79
82
|
tete="$type${portee:+($portee)}: "
|
|
80
83
|
REPLY=""
|
|
81
84
|
[ -n "$sujet" ] || return 0
|
|
82
85
|
res="$tete$sujet"
|
|
83
|
-
# 72 caractères au plus, coupé sur un mot entier (authored_length → REPLY)
|
|
86
|
+
# 72 caractères au plus, coupé sur un mot entier (authored_length → REPLY) ;
|
|
87
|
+
# un mot seul trop long n'est pas tronqué : titre vide
|
|
84
88
|
authored_length "$res"
|
|
85
89
|
while [ "$REPLY" -gt 72 ] && [ -n "$sujet" ]; do
|
|
86
|
-
if [[ "$sujet" == *" "* ]]; then sujet="${sujet% *}"; else sujet="
|
|
90
|
+
if [[ "$sujet" == *" "* ]]; then sujet="${sujet% *}"; else sujet=""; fi
|
|
87
91
|
res="$tete$sujet"
|
|
88
92
|
authored_length "$res"
|
|
89
93
|
done
|
package/package.json
CHANGED