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