devzone 0.0.0-stage → 0.5.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/.devcontainer/Dockerfile +72 -0
- package/.devcontainer/demarrage.sh +10 -0
- package/.devcontainer/devcontainer-lock.json +34 -0
- package/.devcontainer/devcontainer.json +91 -0
- package/.devcontainer/devzone +138 -0
- package/.devcontainer/git-identite.sh +28 -0
- package/.devcontainer/kube-config.sh +80 -0
- package/.devcontainer/metrics-server.sh +61 -0
- package/.devcontainer/post-create.sh +40 -0
- package/.gitattributes +5 -0
- package/LICENSE +21 -0
- package/Makefile +39 -0
- package/README.md +270 -2
- package/bin/devzone +82 -0
- package/desinstaller.sh +132 -0
- package/package.json +29 -4
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Image de base de devzone : outils sans feature officielle, versions figées.
|
|
2
|
+
# Java, Node, Python, Docker, kubectl, helm et gh sont ajoutés par les features
|
|
3
|
+
# de devcontainer.json, par-dessus cette image.
|
|
4
|
+
|
|
5
|
+
# uv : binaire de l'image officielle (pas de script curl | sh)
|
|
6
|
+
ARG UV_VERSION=0.13.1
|
|
7
|
+
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
|
|
8
|
+
|
|
9
|
+
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
|
|
10
|
+
|
|
11
|
+
# Versions : pour mettre à jour, changer la version ET l'empreinte sha256,
|
|
12
|
+
# puis Rebuild Container
|
|
13
|
+
ARG K9S_VERSION=0.51.0
|
|
14
|
+
ARG ACTIONLINT_VERSION=1.7.12
|
|
15
|
+
ARG GITLEAKS_VERSION=8.30.1
|
|
16
|
+
ARG KIND_VERSION=0.33.0
|
|
17
|
+
ARG RUFF_VERSION=0.17.0
|
|
18
|
+
ARG VULTURE_VERSION=2.16
|
|
19
|
+
# pytest : même version que la CI de repowarden (ci/requirements/e2e-python.in)
|
|
20
|
+
ARG PYTEST_VERSION=9.1.1
|
|
21
|
+
|
|
22
|
+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
23
|
+
|
|
24
|
+
# Contrôles shell de repowarden
|
|
25
|
+
RUN apt-get update \
|
|
26
|
+
&& apt-get install -y --no-install-recommends shellcheck bats \
|
|
27
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
28
|
+
|
|
29
|
+
# k9s, actionlint (mêmes versions que la CI de repowarden), gitleaks, kind :
|
|
30
|
+
# installer <nom> <url> <sha256> : empreinte vérifiée avant d'installer quoi que ce soit dans /usr/local/bin
|
|
31
|
+
# (kind est un binaire seul, sans archive : installé à part, même vérification)
|
|
32
|
+
RUN set -eu; \
|
|
33
|
+
arch="$(dpkg --print-architecture)"; \
|
|
34
|
+
case "$arch" in \
|
|
35
|
+
arm64) gl_arch=arm64; \
|
|
36
|
+
k9s_sha=3ee05c82e5f9198928a4e86133608ba6a2c10a2244d6a7789e820f78319d640c; \
|
|
37
|
+
al_sha=325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6; \
|
|
38
|
+
gl_sha=e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080; \
|
|
39
|
+
kind_sha=20022bee6cfcd5086cb7234d218e3454e6090022f2a8f55d1fa7fcf42c3867a2 ;; \
|
|
40
|
+
amd64) gl_arch=x64; \
|
|
41
|
+
k9s_sha=c3752ad51a5a4015a113819c4eeb6e55a4d0e4b8e652494797532f6fc8161dd7; \
|
|
42
|
+
al_sha=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8; \
|
|
43
|
+
gl_sha=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb; \
|
|
44
|
+
kind_sha=aee6151561422756b764a4ae28e7f44cda5af5a9eead3cc9985112b1de8d8e0d ;; \
|
|
45
|
+
*) echo "Architecture non prise en charge : $arch" >&2; exit 1 ;; \
|
|
46
|
+
esac; \
|
|
47
|
+
installer() { \
|
|
48
|
+
tmp="$(mktemp -d)"; \
|
|
49
|
+
curl -fsSL "$2" -o "$tmp/a.tar.gz"; \
|
|
50
|
+
echo "$3 $tmp/a.tar.gz" | sha256sum -c --quiet; \
|
|
51
|
+
tar -xzf "$tmp/a.tar.gz" -C "$tmp" "$1"; \
|
|
52
|
+
install -m 0755 "$tmp/$1" /usr/local/bin/"$1"; \
|
|
53
|
+
rm -rf "$tmp"; \
|
|
54
|
+
}; \
|
|
55
|
+
installer k9s "https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_${arch}.tar.gz" "$k9s_sha"; \
|
|
56
|
+
installer actionlint "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_${arch}.tar.gz" "$al_sha"; \
|
|
57
|
+
installer gitleaks "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${gl_arch}.tar.gz" "$gl_sha"; \
|
|
58
|
+
curl -fsSL "https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-linux-${arch}" -o /tmp/kind; \
|
|
59
|
+
echo "$kind_sha /tmp/kind" | sha256sum -c --quiet; \
|
|
60
|
+
install -m 0755 /tmp/kind /usr/local/bin/kind; \
|
|
61
|
+
rm -f /tmp/kind
|
|
62
|
+
|
|
63
|
+
COPY --from=uv /uv /uvx /usr/local/bin/
|
|
64
|
+
|
|
65
|
+
# ruff, vulture (repowarden code-mort a besoin des deux) et pytest, installés pour
|
|
66
|
+
# tous les utilisateurs, avec un Python géré par uv hors du home
|
|
67
|
+
# (variables limitées au build : uv tool install reste dans ~/.local pour vscode)
|
|
68
|
+
RUN export UV_TOOL_DIR=/opt/uv/tools UV_TOOL_BIN_DIR=/usr/local/bin \
|
|
69
|
+
UV_PYTHON_INSTALL_DIR=/opt/uv/python \
|
|
70
|
+
&& uv tool install "ruff==${RUFF_VERSION}" \
|
|
71
|
+
&& uv tool install "vulture==${VULTURE_VERSION}" \
|
|
72
|
+
&& uv tool install "pytest==${PYTEST_VERSION}"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# À chaque démarrage, dans l'ordre : identité git (déduite du compte gh),
|
|
3
|
+
# clusters Kubernetes, puis récapitulatif de ce qui reste à faire (devzone).
|
|
4
|
+
# Séquentiel : le récapitulatif voit l'identité tout juste posée ; l'échec
|
|
5
|
+
# d'une étape n'empêche pas les suivantes.
|
|
6
|
+
ICI="$(dirname "$0")"
|
|
7
|
+
bash "$ICI/git-identite.sh" || echo "démarrage : identité git non traitée (bash $ICI/git-identite.sh)"
|
|
8
|
+
bash "$ICI/kube-config.sh" || echo "démarrage : clusters non traités (bash $ICI/kube-config.sh)"
|
|
9
|
+
echo
|
|
10
|
+
bash "$ICI/devzone"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"features": {
|
|
3
|
+
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
|
4
|
+
"version": "2.17.0",
|
|
5
|
+
"resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c",
|
|
6
|
+
"integrity": "sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c"
|
|
7
|
+
},
|
|
8
|
+
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
9
|
+
"version": "1.1.3",
|
|
10
|
+
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:bd7ab48a832228f633239277552c30b353867fef2e5b037e064b4e64f0b843f2",
|
|
11
|
+
"integrity": "sha256:bd7ab48a832228f633239277552c30b353867fef2e5b037e064b4e64f0b843f2"
|
|
12
|
+
},
|
|
13
|
+
"ghcr.io/devcontainers/features/java:1": {
|
|
14
|
+
"version": "1.8.3",
|
|
15
|
+
"resolved": "ghcr.io/devcontainers/features/java@sha256:61a839211feda1e2a1f10646648a948e733e4d98d770f3a4b6698a3c5fc92abe",
|
|
16
|
+
"integrity": "sha256:61a839211feda1e2a1f10646648a948e733e4d98d770f3a4b6698a3c5fc92abe"
|
|
17
|
+
},
|
|
18
|
+
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
|
|
19
|
+
"version": "1.3.1",
|
|
20
|
+
"resolved": "ghcr.io/devcontainers/features/kubectl-helm-minikube@sha256:bbe8adf6b37fff8c67412ab0a4579f4c2f30bbaba1d9a5cebd9e38bade54025b",
|
|
21
|
+
"integrity": "sha256:bbe8adf6b37fff8c67412ab0a4579f4c2f30bbaba1d9a5cebd9e38bade54025b"
|
|
22
|
+
},
|
|
23
|
+
"ghcr.io/devcontainers/features/node:1": {
|
|
24
|
+
"version": "1.7.1",
|
|
25
|
+
"resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
|
|
26
|
+
"integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
|
|
27
|
+
},
|
|
28
|
+
"ghcr.io/devcontainers/features/python:1": {
|
|
29
|
+
"version": "1.8.0",
|
|
30
|
+
"resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511",
|
|
31
|
+
"integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devzone",
|
|
3
|
+
// Image de base + outils figés (k9s, uv, actionlint, gitleaks…) : voir Dockerfile
|
|
4
|
+
"build": { "dockerfile": "Dockerfile" },
|
|
5
|
+
|
|
6
|
+
"features": {
|
|
7
|
+
"ghcr.io/devcontainers/features/java:1": {
|
|
8
|
+
"version": "21",
|
|
9
|
+
"jdkDistro": "tem",
|
|
10
|
+
"installMaven": true
|
|
11
|
+
},
|
|
12
|
+
"ghcr.io/devcontainers/features/node:1": {
|
|
13
|
+
"version": "lts",
|
|
14
|
+
"pnpmVersion": "latest"
|
|
15
|
+
},
|
|
16
|
+
"ghcr.io/devcontainers/features/python:1": {
|
|
17
|
+
"version": "3.12",
|
|
18
|
+
// Pas d'outils pipx : leur lanceur « python -E » ignore PYTHONPATH et
|
|
19
|
+
// PYTHONPYCACHEPREFIX, dont repowarden a besoin pour lancer les tests
|
|
20
|
+
// (ruff, vulture et pytest viennent du Dockerfile, par uv)
|
|
21
|
+
"installTools": false
|
|
22
|
+
},
|
|
23
|
+
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
|
24
|
+
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
|
|
25
|
+
"minikube": "none"
|
|
26
|
+
},
|
|
27
|
+
// gh : tickets, PR, protection (repowarden)
|
|
28
|
+
"ghcr.io/devcontainers/features/github-cli:1": {}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Toujours /workspaces/devzone, quel que soit le nom du dossier sur l'hôte
|
|
32
|
+
// (npx devzone <dossier>) : scripts et chemins du conteneur en dépendent
|
|
33
|
+
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/devzone,type=bind,consistency=cached",
|
|
34
|
+
"workspaceFolder": "/workspaces/devzone",
|
|
35
|
+
|
|
36
|
+
// Caches persistants : survivent aux rebuilds du conteneur. Préfixe des
|
|
37
|
+
// volumes : devzone, ou DEVZONE_VOLUMES (tests fonctionnels, isolés)
|
|
38
|
+
"mounts": [
|
|
39
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-m2,target=/home/vscode/.m2,type=volume",
|
|
40
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-pnpm,target=/home/vscode/.local/share/pnpm,type=volume",
|
|
41
|
+
// Assistants IA : connexion et réglages gardés d'un rebuild à l'autre
|
|
42
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-claude,target=/home/vscode/.claude,type=volume",
|
|
43
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-opencode-config,target=/home/vscode/.config/opencode,type=volume",
|
|
44
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-opencode-data,target=/home/vscode/.local/share/opencode,type=volume",
|
|
45
|
+
// Connexion gh et identité git (~/.config/git/config, lu par git comme
|
|
46
|
+
// config globale) : le conteneur reconstruit peut commiter et pousser
|
|
47
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-gh,target=/home/vscode/.config/gh,type=volume",
|
|
48
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-git,target=/home/vscode/.config/git,type=volume",
|
|
49
|
+
// Modules activés (devzone activer|desactiver), propres à chaque utilisateur
|
|
50
|
+
"source=${localEnv:DEVZONE_VOLUMES:devzone}-config,target=/home/vscode/.config/devzone,type=volume"
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
"runArgs": [
|
|
54
|
+
// Cluster Kubernetes de l'hôte : sa config en lecture seule, adaptée au
|
|
55
|
+
// conteneur par kube-config.sh. HOME (Mac, Linux) ou USERPROFILE (Windows) ;
|
|
56
|
+
// -v plutôt que mounts : crée ~/.kube s'il manque au lieu d'échouer
|
|
57
|
+
"-v", "${localEnv:HOME}${localEnv:USERPROFILE}/.kube:/home/vscode/.kube-host:ro",
|
|
58
|
+
// host.docker.internal existe d'office avec Docker Desktop, pas sous Linux
|
|
59
|
+
"--add-host=host.docker.internal:host-gateway"
|
|
60
|
+
],
|
|
61
|
+
|
|
62
|
+
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
|
63
|
+
// Identité git, clusters, puis récapitulatif de ce qui reste à faire
|
|
64
|
+
"postStartCommand": "bash .devcontainer/demarrage.sh",
|
|
65
|
+
// Clusters kind du conteneur (config) + cluster de l'hôte (hote.config,
|
|
66
|
+
// refait à chaque démarrage par kube-config.sh)
|
|
67
|
+
"remoteEnv": {
|
|
68
|
+
"KUBECONFIG": "/home/vscode/.kube/config:/home/vscode/.kube/hote.config",
|
|
69
|
+
// Claude Code range aussi .claude.json (connexion, réglages) dans ce
|
|
70
|
+
// dossier, donc dans son volume, au lieu de ~/.claude.json
|
|
71
|
+
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude"
|
|
72
|
+
},
|
|
73
|
+
"remoteUser": "vscode",
|
|
74
|
+
|
|
75
|
+
"customizations": {
|
|
76
|
+
"vscode": {
|
|
77
|
+
"extensions": [
|
|
78
|
+
"vscjava.vscode-java-pack",
|
|
79
|
+
"vmware.vscode-boot-dev-pack",
|
|
80
|
+
"dbaeumer.vscode-eslint",
|
|
81
|
+
"esbenp.prettier-vscode",
|
|
82
|
+
"bradlc.vscode-tailwindcss",
|
|
83
|
+
"ms-python.python",
|
|
84
|
+
"charliermarsh.ruff",
|
|
85
|
+
"ms-azuretools.vscode-docker",
|
|
86
|
+
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
|
87
|
+
"redhat.vscode-yaml"
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Modules facultatifs de devzone, activés à la demande, sans rebuild :
|
|
3
|
+
# devzone état des modules
|
|
4
|
+
# devzone activer <module> active et applique tout de suite
|
|
5
|
+
# devzone desactiver <module> désactive et applique tout de suite
|
|
6
|
+
# devzone appliquer installe ce qui est actif (post-create.sh)
|
|
7
|
+
# devzone actif <module> code 0 si actif (scripts de démarrage)
|
|
8
|
+
# État propre à chaque utilisateur : ~/.config/devzone/modules (volume
|
|
9
|
+
# devzone-config, survit aux rebuilds), une ligne « module=on|off ».
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
ETAT="${DEVZONE_ETAT:-$HOME/.config/devzone/modules}"
|
|
13
|
+
ICI="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
14
|
+
MODULES=(ia kubernetes)
|
|
15
|
+
|
|
16
|
+
# Assistants IA, versions figées : mettre à jour ici, puis devzone appliquer
|
|
17
|
+
# (ou Rebuild Container)
|
|
18
|
+
IA_PAQUETS=(@anthropic-ai/claude-code@2.1.286 opencode-ai@1.18.34)
|
|
19
|
+
IA_NOMS=(@anthropic-ai/claude-code opencode-ai)
|
|
20
|
+
|
|
21
|
+
description() {
|
|
22
|
+
case "$1" in
|
|
23
|
+
ia) echo "assistants IA : Claude Code, opencode" ;;
|
|
24
|
+
kubernetes) echo "connexion au cluster de l'hôte et metrics-server au démarrage" ;;
|
|
25
|
+
esac
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
connu() {
|
|
29
|
+
local m
|
|
30
|
+
for m in "${MODULES[@]}"; do [ "$m" = "$1" ] && return 0; done
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Premier démarrage (aucun état) : tout désactivé, sauf ia si une connexion
|
|
35
|
+
# existe déjà dans les volumes (pas de régression pour qui s'en servait)
|
|
36
|
+
initialiser() {
|
|
37
|
+
[ -f "$ETAT" ] && return 0
|
|
38
|
+
mkdir -p "$(dirname "$ETAT")"
|
|
39
|
+
local ia=off
|
|
40
|
+
if [ -f "$HOME/.claude/.credentials.json" ] || [ -f "$HOME/.local/share/opencode/auth.json" ]; then
|
|
41
|
+
ia=on
|
|
42
|
+
# stderr : initialiser est aussi appelé dans $(valeur …)
|
|
43
|
+
echo "devzone : connexion d'un assistant IA trouvée, module ia activé (devzone desactiver ia pour le retirer)" >&2
|
|
44
|
+
fi
|
|
45
|
+
printf 'ia=%s\nkubernetes=off\n' "$ia" >"$ETAT"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
valeur() {
|
|
49
|
+
initialiser
|
|
50
|
+
local v
|
|
51
|
+
v="$(grep -E "^$1=" "$ETAT" | tail -1 | cut -d= -f2 || true)"
|
|
52
|
+
[ "$v" = on ] && echo on || echo off
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ecrire() {
|
|
56
|
+
initialiser
|
|
57
|
+
local tmp
|
|
58
|
+
tmp="$(mktemp)"
|
|
59
|
+
grep -vE "^$1=" "$ETAT" >"$tmp" || true
|
|
60
|
+
echo "$1=$2" >>"$tmp"
|
|
61
|
+
mv "$tmp" "$ETAT"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
appliquer_ia() {
|
|
65
|
+
if [ "$1" = on ]; then
|
|
66
|
+
npm install -g --no-fund --no-audit --loglevel=error "${IA_PAQUETS[@]}" >/dev/null
|
|
67
|
+
echo "devzone : ia actif : claude (puis /login), opencode auth login"
|
|
68
|
+
else
|
|
69
|
+
npm uninstall -g "${IA_NOMS[@]}" >/dev/null 2>&1 || true
|
|
70
|
+
echo "devzone : ia désactivé (connexions gardées dans leurs volumes)"
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
appliquer_kubernetes() {
|
|
75
|
+
if [ "$1" = on ]; then
|
|
76
|
+
bash "$ICI/kube-config.sh"
|
|
77
|
+
else
|
|
78
|
+
rm -f "$HOME/.kube/hote.config"
|
|
79
|
+
echo "devzone : kubernetes désactivé (metrics-server reste installé sur les clusters où il l'a été)"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# État complet : ✔ prêt, ✖ nécessaire, ○ facultatif, ⚠ à corriger ; chaque
|
|
84
|
+
# point à faire avec sa commande (affiché à chaque démarrage)
|
|
85
|
+
etat() {
|
|
86
|
+
local nom email statut m
|
|
87
|
+
echo "devzone : état (devzone pour le réafficher)"
|
|
88
|
+
nom="$(git config --get user.name || true)"
|
|
89
|
+
email="$(git config --get user.email || true)"
|
|
90
|
+
if [ -n "$nom" ] && [ -n "$email" ]; then
|
|
91
|
+
echo " ✔ identité git $nom <$email>"
|
|
92
|
+
else
|
|
93
|
+
echo " ✖ identité git nécessaire pour commiter : gh auth login -s workflow (automatique),"
|
|
94
|
+
echo " ou git config --file ~/.config/git/config user.name \"<nom>\" et user.email \"<email>\""
|
|
95
|
+
fi
|
|
96
|
+
if ! statut="$(gh auth status 2>&1)"; then
|
|
97
|
+
echo " ○ GitHub facultatif (push et PR, tickets repowarden) : gh auth login -s workflow"
|
|
98
|
+
elif grep -q "Token scopes:" <<<"$statut" && ! grep -q "'workflow'" <<<"$statut"; then
|
|
99
|
+
echo " ⚠ GitHub connecté, sans permission workflow (push de .github/workflows/ refusé) : gh auth refresh -h github.com -s workflow"
|
|
100
|
+
else
|
|
101
|
+
echo " ✔ GitHub connecté"
|
|
102
|
+
fi
|
|
103
|
+
for m in "${MODULES[@]}"; do
|
|
104
|
+
if [ "$(valeur "$m")" = on ]; then
|
|
105
|
+
printf ' ✔ module %-10s actif : %s\n' "$m" "$(description "$m")"
|
|
106
|
+
else
|
|
107
|
+
printf ' ○ module %-10s facultatif, %s : devzone activer %s\n' "$m" "$(description "$m")" "$m"
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
changer() {
|
|
113
|
+
local m="${2:-}"
|
|
114
|
+
if [ -z "$m" ] || ! connu "$m"; then
|
|
115
|
+
echo "devzone : module inconnu : ${m:-(aucun)} ; modules : ${MODULES[*]}" >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
ecrire "$m" "$1"
|
|
119
|
+
"appliquer_$m" "$1"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case "${1:-}" in
|
|
123
|
+
"" | etat | modules) etat ;;
|
|
124
|
+
activer) changer on "${2:-}" ;;
|
|
125
|
+
desactiver) changer off "${2:-}" ;;
|
|
126
|
+
appliquer)
|
|
127
|
+
[ "$(valeur ia)" = off ] || appliquer_ia on
|
|
128
|
+
;;
|
|
129
|
+
actif)
|
|
130
|
+
connu "${2:-}" || { echo "devzone : module inconnu : ${2:-(aucun)}" >&2; exit 2; }
|
|
131
|
+
[ "$(valeur "$2")" = on ]
|
|
132
|
+
;;
|
|
133
|
+
-h | --help | aide) sed -n '2,8p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' ;;
|
|
134
|
+
*)
|
|
135
|
+
echo "devzone : commande inconnue : $1 (devzone --help)" >&2
|
|
136
|
+
exit 1
|
|
137
|
+
;;
|
|
138
|
+
esac
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Identité git, à chaque démarrage tant qu'elle manque : déduite du compte gh
|
|
3
|
+
# (propre à chaque utilisateur, connexion gardée dans son volume) et écrite dans
|
|
4
|
+
# ~/.config/git/config (volume devzone-git, survit aux rebuilds).
|
|
5
|
+
# Une identité déjà définie (à la main, ou recopiée par VS Code dans
|
|
6
|
+
# ~/.gitconfig) n'est jamais remplacée. Email noreply de GitHub : les commits
|
|
7
|
+
# sont rattachés au compte sans exposer l'email personnel.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
CONFIG="$HOME/.config/git/config"
|
|
11
|
+
nom_git="$(git config --get user.name || true)"
|
|
12
|
+
email_git="$(git config --get user.email || true)"
|
|
13
|
+
|
|
14
|
+
[ -n "$nom_git" ] && [ -n "$email_git" ] && exit 0
|
|
15
|
+
|
|
16
|
+
# Sans gh : rien à déduire ; le récapitulatif de devzone le signale
|
|
17
|
+
gh auth status >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
id="" login="" nom=""
|
|
20
|
+
IFS=$'\t' read -r id login nom < <(gh api user --jq '[.id, .login, (.name // "")] | @tsv' 2>/dev/null || true)
|
|
21
|
+
if [ -z "$id" ] || [ -z "$login" ]; then
|
|
22
|
+
echo "git-identite : compte GitHub illisible (réseau ?) ; relancer : bash .devcontainer/git-identite.sh"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
[ -n "$nom_git" ] || git config --file "$CONFIG" user.name "${nom:-$login}"
|
|
27
|
+
[ -n "$email_git" ] || git config --file "$CONFIG" user.email "${id}+${login}@users.noreply.github.com"
|
|
28
|
+
echo "git-identite : $(git config --get user.name) <$(git config --get user.email)>"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Clusters Kubernetes du conteneur, à chaque démarrage (le port du cluster de
|
|
3
|
+
# l'hôte peut changer) :
|
|
4
|
+
# - ~/.kube/hote.config : copie du ~/.kube de l'hôte (monté en lecture seule) où
|
|
5
|
+
# 127.0.0.1/localhost deviennent host.docker.internal, l'hôte vu du conteneur ;
|
|
6
|
+
# tls-server-name garde le nom attendu par le certificat
|
|
7
|
+
# - ~/.kube/config : clusters kind du conteneur, jamais touché ici
|
|
8
|
+
# KUBECONFIG (devcontainer.json) combine les deux.
|
|
9
|
+
# Module kubernetes (devzone activer kubernetes) : connexion au cluster de
|
|
10
|
+
# l'hôte et metrics-server seulement s'il est actif ; les contextes kind sont
|
|
11
|
+
# toujours remis (un cluster créé à la main reste utilisable).
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
ICI="$(dirname "$0")"
|
|
15
|
+
module=off
|
|
16
|
+
bash "$ICI/devzone" actif kubernetes && module=on
|
|
17
|
+
|
|
18
|
+
# DEVZONE_KUBE_SOURCE : tests fonctionnels (jamais le cluster de l'hôte)
|
|
19
|
+
SOURCE="${DEVZONE_KUBE_SOURCE:-$HOME/.kube-host/config}"
|
|
20
|
+
HOTE="$HOME/.kube/hote.config"
|
|
21
|
+
|
|
22
|
+
mkdir -p "$HOME/.kube"
|
|
23
|
+
rm -f "$HOTE"
|
|
24
|
+
# Doit exister : sinon kind écrit dans hote.config, effacé au prochain démarrage
|
|
25
|
+
[ -f "$HOME/.kube/config" ] || install -m 600 /dev/null "$HOME/.kube/config"
|
|
26
|
+
|
|
27
|
+
if [ "$module" = on ] && [ -f "$SOURCE" ]; then
|
|
28
|
+
install -m 600 "$SOURCE" "$HOTE"
|
|
29
|
+
KUBECONFIG="$HOTE" kubectl config view \
|
|
30
|
+
-o jsonpath='{range .clusters[*]}{.name}{"\t"}{.cluster.server}{"\n"}{end}' |
|
|
31
|
+
while IFS=$'\t' read -r nom serveur; do
|
|
32
|
+
case "$serveur" in
|
|
33
|
+
https://127.0.0.1:* | https://localhost:*) ;;
|
|
34
|
+
*) continue ;;
|
|
35
|
+
esac
|
|
36
|
+
hote="${serveur#https://}"
|
|
37
|
+
hote="${hote%%:*}"
|
|
38
|
+
KUBECONFIG="$HOTE" kubectl config set-cluster "$nom" \
|
|
39
|
+
--server="https://host.docker.internal:${serveur##*:}" \
|
|
40
|
+
--tls-server-name="$hote" >/dev/null
|
|
41
|
+
done
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Clusters kind qui ont survécu au redémarrage : contexte remis dans ~/.kube/config
|
|
45
|
+
# (le Docker interne peut mettre quelques secondes à démarrer)
|
|
46
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do docker info >/dev/null 2>&1 && break; sleep 1; done
|
|
47
|
+
choix="$(KUBECONFIG="$HOME/.kube/config" kubectl config current-context 2>/dev/null || true)"
|
|
48
|
+
for cluster in $(kind get clusters 2>/dev/null); do
|
|
49
|
+
kind export kubeconfig --name "$cluster" --kubeconfig "$HOME/.kube/config" >/dev/null 2>&1 || true
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Cluster actif : celui choisi avant (kubectl config use-context), sinon celui
|
|
53
|
+
# de l'hôte s'il en a un (current-context de hote.config), sinon kind
|
|
54
|
+
export KUBECONFIG="$HOME/.kube/config:$HOTE"
|
|
55
|
+
if [ -n "$choix" ] && kubectl config use-context "$choix" >/dev/null 2>&1; then
|
|
56
|
+
:
|
|
57
|
+
elif [ -f "$HOTE" ] && [ -n "$(KUBECONFIG="$HOTE" kubectl config current-context 2>/dev/null)" ]; then
|
|
58
|
+
KUBECONFIG="$HOME/.kube/config" kubectl config unset current-context >/dev/null
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Clusters joignables, parmi ceux de l'hôte et ceux de kind
|
|
62
|
+
joignables=0
|
|
63
|
+
for contexte in $(kubectl config get-contexts -o name 2>/dev/null); do
|
|
64
|
+
if kubectl --context "$contexte" --request-timeout=3s get --raw /readyz >/dev/null 2>&1; then
|
|
65
|
+
echo "kube-config : cluster joignable : $contexte"
|
|
66
|
+
joignables=$((joignables + 1))
|
|
67
|
+
elif [[ "$contexte" == kind-* ]]; then
|
|
68
|
+
echo "kube-config : cluster kind en cours de démarrage : $contexte"
|
|
69
|
+
joignables=$((joignables + 1))
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
# Module désactivé : le récapitulatif de devzone le signale
|
|
73
|
+
[ "$module" = on ] || exit 0
|
|
74
|
+
echo "kube-config : cluster actif : $(kubectl config current-context 2>/dev/null || echo aucun)"
|
|
75
|
+
if [ "$joignables" -eq 0 ]; then
|
|
76
|
+
echo "kube-config : aucun cluster joignable ; en créer un dans le conteneur : kind create cluster"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# CPU/RAM dans k9s : metrics-server sur les clusters joignables qui ne l'ont pas
|
|
80
|
+
bash "$ICI/metrics-server.sh" || true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Installe metrics-server (CPU/RAM dans k9s, kubectl top) sur les clusters qui
|
|
3
|
+
# n'ont pas d'API Metrics. Docker Desktop et kind ne l'ont pas d'office.
|
|
4
|
+
# Usage : metrics-server.sh [contexte...] (défaut : clusters locaux joignables)
|
|
5
|
+
# --kubelet-insecure-tls : le kubelet de Docker Desktop et de kind a un
|
|
6
|
+
# certificat auto-signé, sans cette option le pod ne devient jamais Ready.
|
|
7
|
+
# Sortie en erreur si un contexte demandé est inconnu ou injoignable, ou si
|
|
8
|
+
# une installation échoue (kube-config.sh l'appelle avec || true).
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
VERSION=v0.9.0
|
|
12
|
+
MANIFESTE="https://github.com/kubernetes-sigs/metrics-server/releases/download/${VERSION}/components.yaml"
|
|
13
|
+
|
|
14
|
+
mapfile -t connus < <(kubectl config get-contexts -o name 2>/dev/null || true)
|
|
15
|
+
demandes=$#
|
|
16
|
+
if [ "$demandes" -eq 0 ]; then
|
|
17
|
+
# Clusters locaux seulement : jamais d'install automatique sur un cluster
|
|
18
|
+
# distant de l'hôte (prod, staging…) ; pour un autre, le nommer en argument
|
|
19
|
+
mapfile -t contextes < <(printf '%s\n' ${connus[@]+"${connus[@]}"} |
|
|
20
|
+
grep -E '^(kind-.*|docker-desktop|rancher-desktop|orbstack|minikube)$' || true)
|
|
21
|
+
set -- ${contextes[@]+"${contextes[@]}"}
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
code=0
|
|
25
|
+
joignables=0
|
|
26
|
+
for contexte in "$@"; do
|
|
27
|
+
if [ "$demandes" -gt 0 ] && ! printf '%s\n' ${connus[@]+"${connus[@]}"} | grep -qxF -- "$contexte"; then
|
|
28
|
+
echo "metrics-server : contexte inconnu : $contexte (voir kubectl config get-contexts)" >&2
|
|
29
|
+
code=1
|
|
30
|
+
continue
|
|
31
|
+
fi
|
|
32
|
+
k=(kubectl --context "$contexte" --request-timeout=5s)
|
|
33
|
+
if ! "${k[@]}" get --raw /readyz >/dev/null 2>&1; then
|
|
34
|
+
# Sans argument : kind peut être en cours de démarrage, ignoré
|
|
35
|
+
if [ "$demandes" -gt 0 ]; then
|
|
36
|
+
echo "metrics-server : cluster injoignable : $contexte" >&2
|
|
37
|
+
code=1
|
|
38
|
+
fi
|
|
39
|
+
continue
|
|
40
|
+
fi
|
|
41
|
+
joignables=$((joignables + 1))
|
|
42
|
+
if "${k[@]}" get apiservice v1beta1.metrics.k8s.io >/dev/null 2>&1; then
|
|
43
|
+
echo "metrics-server : déjà présent : $contexte"
|
|
44
|
+
continue
|
|
45
|
+
fi
|
|
46
|
+
if "${k[@]}" apply -f "$MANIFESTE" >/dev/null &&
|
|
47
|
+
"${k[@]}" -n kube-system patch deployment metrics-server --type=json \
|
|
48
|
+
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' >/dev/null &&
|
|
49
|
+
# Marque : make desinstaller ne retire que ce que devzone a installé
|
|
50
|
+
"${k[@]}" -n kube-system label deployment metrics-server devzone=installe --overwrite >/dev/null; then
|
|
51
|
+
echo "metrics-server : installé ($VERSION) : $contexte (métriques dans ~1 min)"
|
|
52
|
+
else
|
|
53
|
+
echo "metrics-server : échec de l'installation : $contexte" >&2
|
|
54
|
+
code=1
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [ "$demandes" -eq 0 ] && [ "$joignables" -eq 0 ]; then
|
|
59
|
+
echo "metrics-server : aucun cluster local joignable ; après kind create cluster : bash .devcontainer/metrics-server.sh"
|
|
60
|
+
fi
|
|
61
|
+
exit "$code"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Étapes qui ne peuvent pas être dans l'image (Dockerfile) :
|
|
3
|
+
# - volumes montés au démarrage seulement
|
|
4
|
+
# - npm fourni par la feature node, ajoutée après le Dockerfile
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Volumes nommés créés en root → rendre au user vscode
|
|
8
|
+
# (et ~/.config, créé par Docker pour monter ~/.config/opencode)
|
|
9
|
+
sudo chown -R vscode:vscode /home/vscode/.m2 /home/vscode/.local /home/vscode/.claude \
|
|
10
|
+
/home/vscode/.config/opencode /home/vscode/.config/gh /home/vscode/.config/git \
|
|
11
|
+
/home/vscode/.config/devzone
|
|
12
|
+
sudo chown vscode:vscode /home/vscode/.config
|
|
13
|
+
|
|
14
|
+
# Git : dépôts du conteneur déclarés sûrs (leur propriétaire peut différer de
|
|
15
|
+
# l'utilisateur qui lance git : volumes, Docker Desktop, outils lancés à part),
|
|
16
|
+
# et accès à GitHub par le jeton de gh, conservé dans son volume (https, même
|
|
17
|
+
# pour les remotes en git@github.com:, sans clé ssh à recopier)
|
|
18
|
+
git config --global --add safe.directory /workspaces/devzone
|
|
19
|
+
git config --global --add safe.directory '/workspaces/devzone/workspace/*'
|
|
20
|
+
git config --global credential.https://github.com.helper ''
|
|
21
|
+
git config --global --add credential.https://github.com.helper '!gh auth git-credential'
|
|
22
|
+
git config --global url.https://github.com/.insteadOf git@github.com:
|
|
23
|
+
|
|
24
|
+
# repowarden, version figée (il installe des hooks git globaux) ; mettre à jour
|
|
25
|
+
# en changeant la version ici, puis Rebuild Container
|
|
26
|
+
npm install -g repowarden@4.8.3
|
|
27
|
+
repowarden install --global --lang fr
|
|
28
|
+
|
|
29
|
+
# Modules facultatifs (assistants IA, Kubernetes) : commande devzone, état
|
|
30
|
+
# gardé dans le volume devzone-config ; installe ce qui est actif
|
|
31
|
+
sudo ln -sf /workspaces/devzone/.devcontainer/devzone /usr/local/bin/devzone
|
|
32
|
+
devzone appliquer
|
|
33
|
+
|
|
34
|
+
# Ce qui reste à faire (identité git, GitHub, modules) : récapitulatif de
|
|
35
|
+
# devzone, affiché à chaque démarrage (demarrage.sh)
|
|
36
|
+
|
|
37
|
+
recap="java $(java -version 2>&1 | head -1 | cut -d'"' -f2), node $(node -v), python $(python3 --version | cut -d' ' -f2), repowarden $(repowarden --version)"
|
|
38
|
+
! command -v claude >/dev/null || recap+=", claude $(claude --version | cut -d' ' -f1)"
|
|
39
|
+
! command -v opencode >/dev/null || recap+=", opencode $(opencode --version)"
|
|
40
|
+
echo "devzone prêt : $recap"
|
package/.gitattributes
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SimBienvenueHoulBoumi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Makefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Pilotage du devcontainer sans VS Code
|
|
2
|
+
DC := npx -y @devcontainers/cli
|
|
3
|
+
WS := --workspace-folder $(CURDIR)
|
|
4
|
+
FILTRE := --filter "label=devcontainer.local_folder=$(CURDIR)"
|
|
5
|
+
LOG := $(CURDIR)/.devzone.log
|
|
6
|
+
|
|
7
|
+
.DEFAULT_GOAL := help
|
|
8
|
+
.PHONY: help up shell stop down rebuild status test-fonctionnel desinstaller
|
|
9
|
+
|
|
10
|
+
help: ## Liste les commandes
|
|
11
|
+
@grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " make %-16s %s\n", $$1, $$2}'
|
|
12
|
+
|
|
13
|
+
up: ## Lance le conteneur (le crée si absent)
|
|
14
|
+
@$(DC) up $(WS) >$(LOG) 2>&1 && echo "devzone lancé" \
|
|
15
|
+
|| { tail -15 $(LOG); echo "échec — log complet : .devzone.log"; exit 1; }
|
|
16
|
+
|
|
17
|
+
shell: up ## Ouvre un shell dans le conteneur
|
|
18
|
+
@$(DC) exec $(WS) bash
|
|
19
|
+
|
|
20
|
+
stop: ## Arrête le conteneur (état conservé)
|
|
21
|
+
@ids=$$(docker ps -q $(FILTRE)); if [ -z "$$ids" ]; then echo "déjà arrêté"; else docker stop $$ids >/dev/null && echo "devzone arrêté"; fi
|
|
22
|
+
|
|
23
|
+
down: ## Supprime le conteneur (volumes, image et Docker interne conservés)
|
|
24
|
+
@ids=$$(docker ps -aq $(FILTRE)); if [ -z "$$ids" ]; then echo "aucun conteneur"; else \
|
|
25
|
+
docker inspect $$ids --format 'IMAGE={{.Config.Image}}{{range .Mounts}}{{if eq .Destination "/var/lib/docker"}} DIND={{.Name}}{{end}}{{end}}' | head -1 | tr ' ' '\n' > .devzone.etat; \
|
|
26
|
+
docker rm -f $$ids >/dev/null && echo "devzone supprimé (désinstallation complète : make desinstaller)"; fi
|
|
27
|
+
|
|
28
|
+
rebuild: ## Reconstruit de zéro (après modif de .devcontainer/)
|
|
29
|
+
@$(DC) up $(WS) --remove-existing-container >$(LOG) 2>&1 && echo "devzone reconstruit" \
|
|
30
|
+
|| { tail -15 $(LOG); echo "échec — log complet : .devzone.log"; exit 1; }
|
|
31
|
+
|
|
32
|
+
status: ## État du conteneur
|
|
33
|
+
@docker ps -a $(FILTRE) --format '{{.Status}}' | grep . || echo "aucun conteneur"
|
|
34
|
+
|
|
35
|
+
test-fonctionnel: ## Tests fonctionnels : devzone isolé, construit puis supprimé (5 à 10 min)
|
|
36
|
+
@bash test/fonctionnel.sh
|
|
37
|
+
|
|
38
|
+
desinstaller: ## Supprime tout ce que devzone a créé, après confirmation (GARDER=connexions : garde les connexions)
|
|
39
|
+
@GARDER="$(GARDER)" bash desinstaller.sh
|
package/README.md
CHANGED
|
@@ -1,3 +1,271 @@
|
|
|
1
|
-
#
|
|
1
|
+
# devzone
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Environnement de développement complet dans un conteneur (devcontainer) : les mêmes outils, aux mêmes versions, sur Mac, Linux et Windows. Rien à installer sur ta machine à part Docker.
|
|
4
|
+
|
|
5
|
+
| Domaine | Outils |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Langages | Java 21 + Maven, Node LTS + pnpm, Python 3.12 + uv |
|
|
8
|
+
| Conteneurs, Kubernetes | Docker (Docker-in-Docker), kubectl, helm, k9s, kind ; connexion au cluster de l'hôte : [module](#modules) `kubernetes` |
|
|
9
|
+
| GitHub, qualité | gh, repowarden (hooks git globaux), shellcheck, bats, actionlint, gitleaks, ruff, vulture, pytest |
|
|
10
|
+
| Assistants IA | Claude Code, opencode : [module](#modules) `ia`, désactivé par défaut |
|
|
11
|
+
|
|
12
|
+
**Ce qui est facultatif est désactivé par défaut** et s'active à la demande, sans rebuild (voir [Modules](#modules)) : les assistants IA (`devzone activer ia`) et la connexion au cluster Kubernetes de ta machine (`devzone activer kubernetes`). devzone ne crée jamais de cluster : sans celui de ta machine, tu en crées un si besoin avec `kind create cluster` (voir [Kubernetes](#kubernetes)).
|
|
13
|
+
|
|
14
|
+
## Démarrage rapide
|
|
15
|
+
|
|
16
|
+
1. **Prérequis** : Docker (Docker Desktop, ou Docker Engine sous Linux), puis **soit** VS Code + extension *Dev Containers*, **soit** Node (pour `npx`) et `make`. Facultatif, pour Kubernetes : activer le cluster de Docker Desktop (*Settings → Kubernetes → Enable Kubernetes*). Sous Windows : cloner avec Git for Windows (les scripts restent en fins de ligne LF grâce à `.gitattributes`).
|
|
17
|
+
2. **Obtenir devzone**, au choix :
|
|
18
|
+
- `npx devzone mon-devzone` : crée le dossier `mon-devzone/` et lance le conteneur (paquet npm `devzone`, une fois publié) ;
|
|
19
|
+
- ou `git clone https://github.com/SimBienvenueHoulBoumi/devzone.git`.
|
|
20
|
+
|
|
21
|
+
**Lancer** depuis ce dossier : `make shell` (ou, dans VS Code, *Dev Containers: Reopen in Container*). Le premier build prend quelques minutes (4 à 5 min mesurées, plus le téléchargement des images de base sur une machine neuve).
|
|
22
|
+
3. **Identité git**, une seule fois (nécessaire pour commiter, gardée ensuite, même après `make rebuild`) :
|
|
23
|
+
- avec GitHub : `gh auth login -s workflow` ; l'identité est déduite du compte ;
|
|
24
|
+
- sans GitHub : `git config --file ~/.config/git/config user.name "<nom>"`, puis de même `user.email "<email>"`.
|
|
25
|
+
|
|
26
|
+
**Facultatif** : GitHub (push et PR GitHub, tickets et bots repowarden), `devzone activer ia` (assistants IA), `devzone activer kubernetes` (cluster de ta machine) ; voir [Connexions et identité](#connexions-et-identité) et [Modules](#modules).
|
|
27
|
+
4. **Vérifier** : `java -version`, `node -v`, `python3 --version`, `repowarden --version` ; avec un cluster : `kubectl get nodes`.
|
|
28
|
+
|
|
29
|
+
**À chaque démarrage, un récapitulatif** dit ce qui est prêt et ce qui reste à faire, chaque point avec sa commande ; `devzone` le réaffiche à tout moment :
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
devzone : état (devzone pour le réafficher)
|
|
33
|
+
✖ identité git nécessaire pour commiter : gh auth login -s workflow (automatique),
|
|
34
|
+
ou git config --file ~/.config/git/config user.name "<nom>" et user.email "<email>"
|
|
35
|
+
○ GitHub facultatif (push et PR, tickets repowarden) : gh auth login -s workflow
|
|
36
|
+
○ module ia facultatif, assistants IA : Claude Code, opencode : devzone activer ia
|
|
37
|
+
○ module kubernetes facultatif, connexion au cluster de l'hôte et metrics-server au démarrage : devzone activer kubernetes
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
✔ prêt, ✖ nécessaire, ○ facultatif, ⚠ à corriger (par exemple GitHub connecté sans la permission `workflow`). Dans VS Code, il s'affiche dans le terminal de création du conteneur.
|
|
41
|
+
|
|
42
|
+
## Modules
|
|
43
|
+
|
|
44
|
+
Les fonctionnalités facultatives sont des modules, **désactivés par défaut**, activés à la demande depuis le conteneur, sans rebuild :
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
devzone # état : identité git, GitHub, modules
|
|
48
|
+
devzone activer <module> # active et applique tout de suite
|
|
49
|
+
devzone desactiver <module> # désactive et applique tout de suite
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Module | Activé | Désactivé |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `ia` | Claude Code et opencode installés (versions figées), réinstallés à chaque rebuild ; se connecter : `claude` (puis `/login`), `opencode auth login` | désinstallés ; les connexions restent dans leurs volumes |
|
|
55
|
+
| `kubernetes` | à chaque démarrage : connexion au cluster de ta machine et metrics-server sur les clusters locaux (CPU et RAM dans k9s) | aucune connexion au cluster de ta machine ; kubectl, helm, k9s et kind restent installés, un cluster kind créé à la main reste utilisable ; metrics-server n'est pas retiré des clusters où il a été installé |
|
|
56
|
+
|
|
57
|
+
Le choix est **propre à chaque utilisateur** et gardé dans le volume `devzone-config` (il survit aux rebuilds). Au premier démarrage, si une connexion Claude Code ou opencode existe déjà dans les volumes, le module `ia` est activé (et signalé) : rien ne disparaît pour qui s'en servait.
|
|
58
|
+
|
|
59
|
+
## Commandes
|
|
60
|
+
|
|
61
|
+
| Action | VS Code (`Cmd+Shift+P`) | `make` | Sans `make` (Windows) |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| Lancer | *Dev Containers: Reopen in Container* | `make up` | `npx @devcontainers/cli up --workspace-folder .` |
|
|
64
|
+
| Ouvrir un shell | terminal de VS Code | `make shell` | `npx @devcontainers/cli exec --workspace-folder . bash` |
|
|
65
|
+
| Arrêter | *Remote: Close Remote Connection* (le conteneur s'arrête avec la fenêtre) | `make stop` | — |
|
|
66
|
+
| Supprimer le conteneur | — | `make down` | — |
|
|
67
|
+
| Reconstruire (après une modification de `.devcontainer/`) | *Dev Containers: Rebuild Container* | `make rebuild` | `npx @devcontainers/cli up --workspace-folder . --remove-existing-container` |
|
|
68
|
+
| État | — | `make status` | — |
|
|
69
|
+
| **Désinstaller** (tout ce que devzone a créé, après confirmation) | — | `make desinstaller` | `bash desinstaller.sh` |
|
|
70
|
+
|
|
71
|
+
`make` seul affiche la liste. Les journaux de `up` et `rebuild` vont dans `.devzone.log` ; seule la fin s'affiche en cas d'échec.
|
|
72
|
+
|
|
73
|
+
> Ne pas lancer `make up` pendant que VS Code ouvre ou reconstruit le conteneur : deux lancements simultanés créent des conteneurs en double.
|
|
74
|
+
|
|
75
|
+
## Connexions et identité
|
|
76
|
+
|
|
77
|
+
**GitHub (facultatif)** : sert à pousser et ouvrir des PR sur GitHub, aux tickets et bots repowarden, et donne l'identité git automatiquement. Sans GitHub, git fonctionne normalement (dépôts locaux, autre hébergeur) ; seule l'identité se règle à la main. Pour l'utiliser : `gh auth login -s workflow`. Git passe alors par le jeton de `gh` (https, même pour les remotes en `git@github.com:`), sans clé ssh à recopier. Les dépôts de `/workspaces/devzone` sont déclarés sûrs (`safe.directory`).
|
|
78
|
+
- **Permission `workflow`** : sans elle, GitHub refuse tout push qui modifie `.github/workflows/` (fréquent avec repowarden). Compte déjà connecté : `gh auth refresh -h github.com -s workflow`.
|
|
79
|
+
- **Depuis `make shell`**, aucun navigateur ne s'ouvre depuis le conteneur : copier le code affiché par `gh` et ouvrir https://github.com/login/device à la main. L'avertissement « No clipboard utilities available » est sans conséquence.
|
|
80
|
+
|
|
81
|
+
**Identité git** (nécessaire pour commiter) : sans GitHub, à régler à la main (`git config --file ~/.config/git/config user.name "<nom>"` et `user.email "<email>"`) ; avec GitHub, déduite du compte `gh` à chaque démarrage, tant qu'elle manque (`git-identite.sh`) :
|
|
82
|
+
- nom du profil GitHub (sinon le login) ;
|
|
83
|
+
- email noreply `<id>+<login>@users.noreply.github.com` : les commits sont rattachés au compte, l'email personnel n'est pas exposé.
|
|
84
|
+
|
|
85
|
+
Une identité déjà définie n'est jamais remplacée. Pour un autre email : `git config --file ~/.config/git/config user.email "<email>"`. Après un `gh auth login` en cours de session : `bash .devcontainer/git-identite.sh`.
|
|
86
|
+
|
|
87
|
+
**Assistants IA** (module `ia`, voir [Modules](#modules)) : `devzone activer ia`, puis `claude` (puis `/login`), `opencode auth login`.
|
|
88
|
+
|
|
89
|
+
Chaque connexion est **propre à l'utilisateur** : elle vit dans les volumes Docker de sa machine (voir [Données conservées](#données-conservées)). Le dépôt ne contient aucun identifiant.
|
|
90
|
+
|
|
91
|
+
## Mettre en route un projet
|
|
92
|
+
|
|
93
|
+
Tes projets vont dans `workspace/` (`/workspaces/devzone/workspace` dans le conteneur), chacun avec son propre dépôt git. Les hooks repowarden y sont actifs d'office.
|
|
94
|
+
|
|
95
|
+
**Projet existant, déjà équipé de repowarden** (`.repowarden.conf` et workflows présents) : il suffit de le cloner.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd /workspaces/devzone/workspace
|
|
99
|
+
git clone https://github.com/<compte>/<projet>.git
|
|
100
|
+
cd <projet>
|
|
101
|
+
repowarden # état du poste et du dépôt, étape suivante
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Nouveau projet, ou projet sans repowarden** :
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd /workspaces/devzone/workspace
|
|
108
|
+
mkdir <projet> && cd <projet> && git init -b main # ou git clone d'un dépôt existant
|
|
109
|
+
# … premiers fichiers, puis un premier commit : git add . && git cc
|
|
110
|
+
gh repo create <compte>/<projet> --private --source . --push # dépôt GitHub (si absent)
|
|
111
|
+
repowarden bots # CI, releases, tickets, GitHub App, protection : chacun avec ton accord
|
|
112
|
+
repowarden # vérifier
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`repowarden bots` demande le flux (`main` seule, ou `develop` → `main`), montre ce qu'il va créer, puis attend ta confirmation ; un fichier existant n'est jamais écrasé. Il faut `gh` connecté et les droits d'administration du dépôt GitHub.
|
|
116
|
+
|
|
117
|
+
## Travailler sur un projet
|
|
118
|
+
|
|
119
|
+
- **Hooks repowarden** : installés globalement (`repowarden install --global --lang fr`), ils s'appliquent à tout dépôt cloné dans le conteneur (messages de commit, secrets, formatage, tests). Ensuite, suivre les règles du projet (`AGENTS.md`, `CONTRIBUTING.md`) : `repowarden ticket <n>`, `repowarden cc …`.
|
|
120
|
+
- **GitHub App** (proposée par `repowarden bots`, ou `repowarden app init`) : sans elle, les workflows agissent sous le bot `github-actions`, dont les actions ne déclenchent aucun autre workflow (PR brouillon sans CI, merge automatique sans préversion…) ; voir [Dépannage](#dépannage).
|
|
121
|
+
- **Vérifications de la CI de repowarden** en local : `shellcheck`, `actionlint`, `bats test/`. actionlint est à la même version que la CI ; shellcheck et bats sont ceux d'Ubuntu 24.04 et peuvent différer légèrement.
|
|
122
|
+
|
|
123
|
+
## Kubernetes
|
|
124
|
+
|
|
125
|
+
devzone fournit les outils, pas le cluster, et n'en crée jamais tout seul. Avec le module `kubernetes` actif (`devzone activer kubernetes`), il se connecte à chaque démarrage au cluster de l'hôte s'il existe et répond (Docker Desktop sur Mac, Windows ou Linux). Sinon, ou en plus, créer un cluster dans le conteneur :
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
kind create cluster # ~2 min la première fois
|
|
129
|
+
kubectl config get-contexts # clusters de l'hôte + clusters kind
|
|
130
|
+
kubectl config use-context <nom>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **Cluster actif** : celui de l'hôte par défaut ; un choix fait avec `use-context` est gardé aux redémarrages.
|
|
134
|
+
- **Clusters kind** : tournent dans le Docker interne, redémarrent avec le conteneur, sont conservés par `make rebuild` et `make down` ; supprimés par `kind delete cluster` (ou `make desinstaller`).
|
|
135
|
+
- **CPU et RAM dans k9s** (`kubectl top`) : module `kubernetes` actif, metrics-server est installé au démarrage sur les clusters **locaux** qui ne l'ont pas (Docker Desktop, kind…), jamais sur un cluster distant. Après un `kind create cluster` : `bash .devcontainer/metrics-server.sh` (ou `… metrics-server.sh <contexte>` pour un cluster précis).
|
|
136
|
+
- **Cluster de l'hôte** : `~/.kube` est monté en lecture seule, jamais modifié ; sa copie adaptée est `~/.kube/hote.config` (`127.0.0.1` remplacé par `host.docker.internal`). Après un changement de cluster sur l'hôte : `make stop && make up`.
|
|
137
|
+
- **Messages de démarrage** (`kube-config : …`, `metrics-server : …`, récapitulatif) : dans le terminal de création, ou en relançant `bash .devcontainer/demarrage.sh`.
|
|
138
|
+
|
|
139
|
+
Le cluster de l'hôte n'est pas joignable dans deux cas (kind fonctionne quand même) :
|
|
140
|
+
- **Codespaces, machine distante** : pas de `~/.kube` local ;
|
|
141
|
+
- **Linux avec Docker Engine** : un cluster kind ou minikube de l'hôte écoute sur `127.0.0.1`, invisible du conteneur. Un cluster qui écoute sur toutes les interfaces (k3s, kubeadm) est joignable.
|
|
142
|
+
|
|
143
|
+
## Données conservées
|
|
144
|
+
|
|
145
|
+
Le conteneur est jetable ; ce qui compte vit dans des volumes Docker, sur ta machine.
|
|
146
|
+
|
|
147
|
+
| Volume | Contenu | `make stop` | `make down` | `make rebuild` |
|
|
148
|
+
|---|---|---|---|---|
|
|
149
|
+
| `devzone-gh` | connexion GitHub (`gh`) | gardé | gardé | gardé |
|
|
150
|
+
| `devzone-git` | identité git (`~/.config/git/config`) | gardé | gardé | gardé |
|
|
151
|
+
| `devzone-claude` | connexion et réglages de Claude Code | gardé | gardé | gardé |
|
|
152
|
+
| `devzone-opencode-config`, `devzone-opencode-data` | connexion et réglages d'opencode | gardé | gardé | gardé |
|
|
153
|
+
| `devzone-config` | modules activés (`devzone activer`) | gardé | gardé | gardé |
|
|
154
|
+
| `devzone-m2`, `devzone-pnpm` | caches Maven (`~/.m2`) et pnpm | gardé | gardé | gardé |
|
|
155
|
+
| volume du Docker interne | images Docker et clusters kind du conteneur | gardé | gardé | gardé |
|
|
156
|
+
| `workspace/` (dossier de l'hôte) | tes projets | gardé | gardé | gardé |
|
|
157
|
+
|
|
158
|
+
Tout le reste du conteneur (paquets installés à la main, fichiers hors de ces emplacements) est perdu à `make down` et `make rebuild`. Le Docker interne est isolé : ses images ne sont pas partagées avec Docker Desktop.
|
|
159
|
+
|
|
160
|
+
## Désinstaller
|
|
161
|
+
|
|
162
|
+
`make down` ne supprime que le conteneur. Pour retirer **tout ce que devzone a créé** sur ta machine :
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
make desinstaller # liste ce qui sera supprimé, puis demande « oui »
|
|
166
|
+
make desinstaller GARDER=connexions # pareil, mais garde gh, git, Claude Code, opencode et les modules
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Installé par npm : `devzone desinstaller` dans le dossier (même chose), **puis** `npm uninstall -g devzone`. npm ne retire que la commande `devzone` : sans la désinstallation, image, volumes et Docker interne resteraient.
|
|
170
|
+
|
|
171
|
+
| Supprimé | Jamais touché |
|
|
172
|
+
|---|---|
|
|
173
|
+
| conteneur et image `vsc-devzone-…` | `workspace/` (tes projets) |
|
|
174
|
+
| volumes `devzone-*` | l'image de base `mcr.microsoft.com/devcontainers/base` (partagée) |
|
|
175
|
+
| volume du Docker interne (images et clusters kind) | un metrics-server que devzone n'a pas installé |
|
|
176
|
+
| metrics-server **installé par devzone** sur les clusters locaux de ta machine (il porte l'étiquette `devzone=installe` ; vérifié avec le `kubectl` de ta machine) | |
|
|
177
|
+
|
|
178
|
+
Fonctionne aussi après un `make down` : celui-ci note l'image et le Docker interne dans `.devzone.etat`. À la fin, la commande affiche ce qui reste à retirer toi-même si tu le souhaites : le cache `npx` (`~/.npm/_npx`), l'autorisation « GitHub CLI » de ton compte (*GitHub → Settings → Applications*) et le dossier `devzone/`. Sans terminal : `CONFIRMER=oui make desinstaller`.
|
|
179
|
+
|
|
180
|
+
## Mettre à jour un outil
|
|
181
|
+
|
|
182
|
+
| Outil | Où | Puis |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| k9s, actionlint, gitleaks, kind | `.devcontainer/Dockerfile` : `ARG …_VERSION` **et** les empreintes `sha256` (arm64 + amd64) | `make rebuild` |
|
|
185
|
+
| uv, ruff, vulture, pytest | `.devcontainer/Dockerfile` : `ARG …_VERSION` | `make rebuild` |
|
|
186
|
+
| repowarden | `.devcontainer/post-create.sh` : `repowarden@x.y.z` | `make rebuild` |
|
|
187
|
+
| Claude Code, opencode | `.devcontainer/devzone` : `IA_PAQUETS` (`@anthropic-ai/claude-code@x.y.z`, `opencode-ai@x.y.z`) | `devzone activer ia` (ou `make rebuild`) |
|
|
188
|
+
| metrics-server | `.devcontainer/metrics-server.sh` : `VERSION` | `bash .devcontainer/metrics-server.sh` sur un nouveau cluster |
|
|
189
|
+
| Java, Node, Python, Docker, kubectl, helm, gh | `.devcontainer/devcontainer.json` (features), versions figées dans `devcontainer-lock.json` | `npx @devcontainers/cli upgrade --workspace-folder .` puis `make rebuild` |
|
|
190
|
+
|
|
191
|
+
Empreinte d'une archive : `curl -fsSL <url> | shasum -a 256`.
|
|
192
|
+
|
|
193
|
+
## Tests
|
|
194
|
+
|
|
195
|
+
| Tests | Commande | Où | Durée |
|
|
196
|
+
|---|---|---|---|
|
|
197
|
+
| Commande `devzone` du conteneur et commande npm (unitaires) | `bats test/` | dans le conteneur | quelques secondes |
|
|
198
|
+
| Fonctionnels : un devzone construit et utilisé comme par un utilisateur (outils, modules, Kubernetes avec kind, hooks repowarden, redémarrage, rebuild) | `make test-fonctionnel` | sur ta machine (Docker, Node) | 5 à 10 min |
|
|
199
|
+
|
|
200
|
+
Les tests fonctionnels travaillent sur une copie du dépôt, avec leurs propres volumes (`devzone-test-…`), puis suppriment tout (conteneur, volumes, image), même en cas d'échec : ton devzone, tes connexions et le cluster de ta machine ne sont jamais touchés (seul un cluster kind de test est utilisé). En CI, le workflow `fonctionnel` les lance sur chaque PR qui modifie `.devcontainer/`, `test/` ou le `Makefile`, et à la main (*Actions → fonctionnel → Run workflow*).
|
|
201
|
+
|
|
202
|
+
## Versions et livraison
|
|
203
|
+
|
|
204
|
+
Flux `develop` → `main`, géré par repowarden, **sans geste après le merge d'un ticket** :
|
|
205
|
+
1. Le travail entre dans `develop` (branche par défaut), par une PR liée à un ticket. **Son merge est la validation humaine.** Une PR de travail ouverte vers `main` est reciblée vers `develop` par la CI.
|
|
206
|
+
2. Le bot de release publie aussitôt une **préversion** `vX.Y.Z-next.N` et tient à jour la **PR de livraison** `develop` → `main`.
|
|
207
|
+
3. La GitHub App **merge la PR de livraison** dès que ses vérifications sont vertes (`repowarden`, et `livraison` : préversion publiée). Ce merge publie le tag `vX.Y.Z` et la [release GitHub](https://github.com/SimBienvenueHoulBoumi/devzone/releases). Une vérification rouge bloque la livraison, visible sur la PR.
|
|
208
|
+
|
|
209
|
+
`main` reste ainsi à jour avec `develop`. La version suit les Conventional Commits : `fix` → correctif, `feat` → mineure ; une livraison sans `fix` ni `feat` (documentation, CI…) met `main` à jour sans nouvelle version.
|
|
210
|
+
|
|
211
|
+
**Réglages, au choix du mainteneur** :
|
|
212
|
+
|
|
213
|
+
| Réglage | Où | Effet |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| Livraison automatique (défaut de devzone) | rien | la PR de livraison part d'elle-même (job `livraison-auto` de `release.yml`) |
|
|
216
|
+
| Livraison manuelle | variable de dépôt `LIVRAISON_AUTO=non` (*Settings → Secrets and variables → Actions → Variables*) | la PR de livraison attend ton merge |
|
|
217
|
+
| Préversion à chaque merge (devzone) ou le soir | `release.yml`, job `release` : `preversion-on-push: true` ou rien | sans préversion à chaque merge, la livraison attend celle du soir |
|
|
218
|
+
| Préversion obligatoire avant une version stable (défaut, celui de devzone) | `release.yml`, job `release` : rien, ou `preversion-obligatoire: false` pour la désactiver | désactivée, la vérification `livraison` passe d'office (« préversion non exigée ») : la PR de livraison n'attend plus de préversion, mais la version stable n'a pas été testée en préversion |
|
|
219
|
+
|
|
220
|
+
Prérequis de la livraison automatique : la GitHub App du dépôt (`repowarden app init`, lancé sur ta machine ; voir [Dépannage](#dépannage)). Sans elle, un merge fait par le jeton des Actions ne déclencherait pas la release.
|
|
221
|
+
|
|
222
|
+
## Dépannage
|
|
223
|
+
|
|
224
|
+
| Symptôme | Cause | Solution |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `git push` refusé : « refusing to allow an OAuth App to create or update workflow … without `workflow` scope » | jeton `gh` sans la permission `workflow` | `gh auth refresh -h github.com -s workflow` |
|
|
227
|
+
| PR bloquée, run `action_required`, bandeau « Approve workflows to run » | PR ouverte ou poussée par le bot `github-actions` : pour les événements `pull_request` `opened`, `synchronize` et `reopened`, GitHub attend l'accord d'une personne ayant l'écriture ([doc GitHub](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)) | vérifier les changements (surtout `.github/workflows/`), puis *Approve workflows to run* ; pour ne plus avoir à le faire : `repowarden app init` dans le projet |
|
|
228
|
+
| PR de livraison bloquée : « aucune préversion vX.Y.Z-next.N » | la préversion n'est pas encore publiée (sans `preversion-on-push`, elle part le soir) | attendre celle du soir, *Actions → release → Run workflow* sur `develop`, `preversion-on-push: true`, ou ne plus l'exiger : `preversion-obligatoire: false` |
|
|
229
|
+
| PR de livraison restée ouverte | une vérification rouge (voir la PR), `LIVRAISON_AUTO=non`, ou GitHub App absente (job `livraison-auto` en échec) | corriger la vérification ; `repowarden app init` sur ta machine |
|
|
230
|
+
| `repowarden app init` depuis `make shell` : la page `http://127.0.0.1:…` ne s'ouvre pas, ou GitHub ne revient pas | la page est servie dans le conteneur, injoignable depuis le navigateur de ta machine | lancer la commande **sur ta machine** : `npx -y repowarden app init --repo <compte>/<dépôt>` |
|
|
231
|
+
| `repowarden app init` : « nom déjà utilisé » | un essai précédent a créé l'App sans en récupérer les identifiants | supprimer l'App orpheline (*GitHub → Settings → Developer settings → GitHub Apps*), ou `--nom <autre-nom>` |
|
|
232
|
+
| `git commit` : « Please tell me who you are » | identité git absente | avec GitHub : `gh auth login -s workflow` puis `bash .devcontainer/git-identite.sh` ; sans GitHub : `git config --file ~/.config/git/config user.name "<nom>"` et `user.email "<email>"` |
|
|
233
|
+
| `gh auth login` : « Press Enter to open … in your browser », rien ne s'ouvre | pas de navigateur dans le conteneur (`make shell`) | ouvrir https://github.com/login/device à la main et saisir le code |
|
|
234
|
+
| `k9s` : `Error: exit status 1` puis son aide ; `kubectl` : `localhost:8080` … `connection refused` | aucun cluster actif (module `kubernetes` désactivé, ou pas de cluster sur ta machine) | `devzone activer kubernetes` (cluster de ta machine), `kind create cluster`, ou `kubectl config use-context <nom>` |
|
|
235
|
+
| `claude` ou `opencode` : « command not found » | module `ia` désactivé | `devzone activer ia` |
|
|
236
|
+
| CPU et RAM vides dans k9s | metrics-server absent du cluster | `bash .devcontainer/metrics-server.sh` |
|
|
237
|
+
| Deux conteneurs devzone | `make up` lancé pendant que VS Code ouvrait le conteneur | `make down`, puis un seul lancement |
|
|
238
|
+
| Windows (Git Bash, MSYS) : `~/.kube` introuvable | `HOME` et `USERPROFILE` concaténés dans le chemin monté | lancer depuis VS Code ou PowerShell |
|
|
239
|
+
| Pas de `~/.kube` sur l'hôte | Docker le crée vide au démarrage (sous Linux avec Docker Engine, il appartient à root) | rien à faire : le conteneur démarre quand même |
|
|
240
|
+
|
|
241
|
+
## Organisation
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
devzone/
|
|
245
|
+
├── .devcontainer/
|
|
246
|
+
│ ├── Dockerfile # image de base + outils figés (versions et sha256)
|
|
247
|
+
│ ├── devcontainer.json # features (langages, Docker, Kubernetes, gh), volumes, extensions VS Code
|
|
248
|
+
│ ├── devcontainer-lock.json # versions figées des features
|
|
249
|
+
│ ├── devzone # commande devzone : modules (ia, kubernetes), versions des assistants IA
|
|
250
|
+
│ ├── post-create.sh # premier démarrage : droits des volumes, git, repowarden, modules actifs
|
|
251
|
+
│ ├── demarrage.sh # chaque démarrage : identité git, clusters, puis récapitulatif (devzone)
|
|
252
|
+
│ ├── kube-config.sh # contextes kind ; module kubernetes : cluster de l'hôte, metrics-server
|
|
253
|
+
│ ├── metrics-server.sh # metrics-server sur les clusters locaux (CPU et RAM dans k9s)
|
|
254
|
+
│ └── git-identite.sh # identité git déduite du compte gh si elle manque
|
|
255
|
+
├── .github/workflows/
|
|
256
|
+
│ ├── fonctionnel.yml # tests fonctionnels (PR touchant .devcontainer/, test/ ou le Makefile)
|
|
257
|
+
│ ├── repowarden.yml # vérifications repowarden de chaque PR (PR mal visée reciblée vers develop)
|
|
258
|
+
│ ├── tickets.yml # suivi des tickets : branche, PR brouillon, statuts jusqu'à la production
|
|
259
|
+
│ ├── release.yml # préversions sur develop, livraison develop → main
|
|
260
|
+
│ └── nettoyage.yml # branches mergées supprimées (main et develop conservées)
|
|
261
|
+
├── .repowarden.conf # réglages repowarden : flux develop → main
|
|
262
|
+
├── Makefile # pilotage sans VS Code
|
|
263
|
+
├── desinstaller.sh # make desinstaller : retire tout ce que devzone a créé
|
|
264
|
+
├── bin/devzone # commande npm : npx devzone <dossier>, devzone desinstaller
|
|
265
|
+
├── package.json, LICENSE # paquet npm devzone (licence MIT)
|
|
266
|
+
├── test/
|
|
267
|
+
│ ├── devzone.bats # tests de la commande devzone (bats test/)
|
|
268
|
+
│ ├── cli.bats # tests de la commande npm et du contenu du paquet
|
|
269
|
+
│ └── fonctionnel.sh # tests fonctionnels (make test-fonctionnel)
|
|
270
|
+
└── workspace/ # tes projets (ignorés par git, chacun son dépôt)
|
|
271
|
+
```
|
package/bin/devzone
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Commande devzone sur ta machine (paquet npm) :
|
|
3
|
+
# devzone <dossier> crée un devzone dans <dossier> puis le lance
|
|
4
|
+
# devzone desinstaller dans un dossier devzone : retire tout ce qu'il a créé
|
|
5
|
+
# devzone --version | --help
|
|
6
|
+
# Dans le conteneur, « devzone » est une autre commande : les modules
|
|
7
|
+
# (.devcontainer/devzone).
|
|
8
|
+
# DEVZONE_SANS_DEMARRER=1 : copie seule, sans lancer le conteneur (tests).
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
PAQUET="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")/.." && pwd)"
|
|
12
|
+
# Fichiers d'un devzone (même liste que « files » de package.json, sans bin/)
|
|
13
|
+
FICHIERS=(.devcontainer Makefile .gitattributes desinstaller.sh README.md LICENSE)
|
|
14
|
+
|
|
15
|
+
version() { node -p "require('$PAQUET/package.json').version" 2>/dev/null || echo inconnue; }
|
|
16
|
+
|
|
17
|
+
aide() {
|
|
18
|
+
cat <<EOF
|
|
19
|
+
devzone $(version) : environnement de développement conteneurisé
|
|
20
|
+
|
|
21
|
+
devzone <dossier> crée un devzone dans <dossier> puis le lance (make up)
|
|
22
|
+
devzone desinstaller dans un dossier devzone : retire conteneur, image, volumes
|
|
23
|
+
(à lancer avant npm uninstall -g devzone)
|
|
24
|
+
devzone --version
|
|
25
|
+
EOF
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
creer() {
|
|
29
|
+
local cible="$1"
|
|
30
|
+
if [ -e "$cible" ] && [ -n "$(ls -A "$cible" 2>/dev/null)" ]; then
|
|
31
|
+
echo "✖ $cible existe et n'est pas vide : rien n'est écrasé. Choisis un autre dossier." >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
mkdir -p "$cible/workspace"
|
|
35
|
+
cp -R "${FICHIERS[@]/#/$PAQUET/}" "$cible/"
|
|
36
|
+
touch "$cible/workspace/.gitkeep"
|
|
37
|
+
# npm n'embarque jamais .gitignore : écrit ici
|
|
38
|
+
cat >"$cible/.gitignore" <<'EOF'
|
|
39
|
+
# Chaque projet du workspace a son propre dépôt git
|
|
40
|
+
workspace/*
|
|
41
|
+
!workspace/.gitkeep
|
|
42
|
+
|
|
43
|
+
.DS_Store
|
|
44
|
+
.devzone.log
|
|
45
|
+
.devzone.etat
|
|
46
|
+
EOF
|
|
47
|
+
chmod +x "$cible/desinstaller.sh"
|
|
48
|
+
cible="$(cd "$cible" && pwd)"
|
|
49
|
+
echo "✔ devzone $(version) créé dans $cible"
|
|
50
|
+
if [ -n "${DEVZONE_SANS_DEMARRER:-}" ]; then
|
|
51
|
+
echo "Pour le lancer : cd $cible && make up"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
echo "▶ Lancement (quelques minutes la première fois)…"
|
|
55
|
+
if command -v make >/dev/null 2>&1; then
|
|
56
|
+
make -C "$cible" up
|
|
57
|
+
else
|
|
58
|
+
npx -y @devcontainers/cli up --workspace-folder "$cible" >"$cible/.devzone.log" 2>&1 || {
|
|
59
|
+
tail -15 "$cible/.devzone.log"
|
|
60
|
+
echo "✖ échec ; journal : $cible/.devzone.log" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
}
|
|
63
|
+
fi
|
|
64
|
+
echo "Ensuite : cd $cible && make shell (ou ouvre le dossier dans VS Code : Reopen in Container)."
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case "${1:-}" in
|
|
68
|
+
"" | -h | --help | aide) aide ;;
|
|
69
|
+
-v | --version) version ;;
|
|
70
|
+
desinstaller)
|
|
71
|
+
if [ ! -f desinstaller.sh ] || [ ! -d .devcontainer ]; then
|
|
72
|
+
echo "✖ À lancer dans un dossier devzone (desinstaller.sh introuvable ici)." >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
bash desinstaller.sh
|
|
76
|
+
;;
|
|
77
|
+
-*)
|
|
78
|
+
echo "✖ option inconnue : $1 (devzone --help)" >&2
|
|
79
|
+
exit 1
|
|
80
|
+
;;
|
|
81
|
+
*) creer "$1" ;;
|
|
82
|
+
esac
|
package/desinstaller.sh
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Désinstallation complète de devzone, lancée sur l'hôte :
|
|
3
|
+
# make desinstaller tout, après confirmation
|
|
4
|
+
# make desinstaller GARDER=connexions tout sauf les connexions (gh, git,
|
|
5
|
+
# Claude Code, opencode, modules)
|
|
6
|
+
# Supprime : conteneur, image, volume du Docker interne (images et clusters
|
|
7
|
+
# kind), volumes devzone-*, metrics-server installé par devzone sur les
|
|
8
|
+
# clusters locaux de l'hôte. Ne touche jamais : workspace/ (tes projets),
|
|
9
|
+
# l'image de base, un metrics-server que devzone n'a pas installé.
|
|
10
|
+
# Sans terminal : CONFIRMER=oui.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
ICI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
ETAT="$ICI/.devzone.etat"
|
|
15
|
+
GARDER="${GARDER:-}"
|
|
16
|
+
CONNEXIONS=(gh git claude opencode-config opencode-data config)
|
|
17
|
+
LOCAUX='^(kind-.*|docker-desktop|rancher-desktop|orbstack|minikube)$'
|
|
18
|
+
|
|
19
|
+
case "$GARDER" in "" | connexions) ;; *)
|
|
20
|
+
echo "GARDER : valeur inconnue : $GARDER (seule valeur : connexions)" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
;;
|
|
23
|
+
esac
|
|
24
|
+
|
|
25
|
+
# --- Inventaire --------------------------------------------------------------
|
|
26
|
+
echo "▶ Inventaire (dossier $ICI)"
|
|
27
|
+
conteneurs=() images=() dind=()
|
|
28
|
+
# Étiquette posée avec le chemin tel qu'ouvert (VS Code, make) : chemin
|
|
29
|
+
# logique ou physique selon les liens symboliques ; les deux sont cherchés
|
|
30
|
+
ids="$(for chemin in "$ICI" "$(cd "$ICI" && pwd -P)"; do docker ps -aq --filter "label=devcontainer.local_folder=$chemin"; done | sort -u)"
|
|
31
|
+
for id in $ids; do
|
|
32
|
+
conteneurs+=("$id")
|
|
33
|
+
images+=("$(docker inspect "$id" --format '{{.Config.Image}}')")
|
|
34
|
+
v="$(docker inspect "$id" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/docker"}}{{.Name}}{{end}}{{end}}')"
|
|
35
|
+
[ -z "$v" ] || dind+=("$v")
|
|
36
|
+
done
|
|
37
|
+
# Conteneur déjà supprimé par make down : image et Docker interne notés alors
|
|
38
|
+
if [ "${#conteneurs[@]}" -eq 0 ] && [ -f "$ETAT" ]; then
|
|
39
|
+
# Lu, jamais exécuté
|
|
40
|
+
IMAGE="$(sed -n 's/^IMAGE=//p' "$ETAT")"
|
|
41
|
+
DIND="$(sed -n 's/^DIND=//p' "$ETAT")"
|
|
42
|
+
[ -z "${IMAGE:-}" ] || ! docker image inspect "$IMAGE" >/dev/null 2>&1 || images+=("$IMAGE")
|
|
43
|
+
[ -z "${DIND:-}" ] || ! docker volume inspect "$DIND" >/dev/null 2>&1 || dind+=("$DIND")
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Volumes : noms exacts de devcontainer.json (préfixe DEVZONE_VOLUMES compris)
|
|
47
|
+
volumes=() gardes=()
|
|
48
|
+
config="$(npx -y @devcontainers/cli read-configuration --workspace-folder "$ICI" 2>/dev/null)" || {
|
|
49
|
+
echo "✖ lecture de .devcontainer/devcontainer.json impossible (npx @devcontainers/cli)" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
# shellcheck disable=SC2016 # ${m.source} : gabarit JavaScript, pas du shell
|
|
53
|
+
for nom in $(node -e '
|
|
54
|
+
const c = JSON.parse(require("fs").readFileSync(0, "utf8")).configuration;
|
|
55
|
+
for (const m of c.mounts || []) {
|
|
56
|
+
const s = typeof m === "string" ? m : `source=${m.source},type=${m.type}`;
|
|
57
|
+
if (/type=volume/.test(s)) { const r = /source=([^,]+)/.exec(s); if (r) console.log(r[1]); }
|
|
58
|
+
}' <<<"$config"); do
|
|
59
|
+
docker volume inspect "$nom" >/dev/null 2>&1 || continue
|
|
60
|
+
garde=false
|
|
61
|
+
if [ "$GARDER" = connexions ]; then
|
|
62
|
+
for c in "${CONNEXIONS[@]}"; do [[ "$nom" == *-"$c" ]] && garde=true; done
|
|
63
|
+
fi
|
|
64
|
+
if [ "$garde" = true ]; then gardes+=("$nom"); else volumes+=("$nom"); fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
# metrics-server installé par devzone (étiquette devzone=installe), sur les
|
|
68
|
+
# clusters locaux de l'hôte joignables ; jamais un autre
|
|
69
|
+
metrics=() etrangers=()
|
|
70
|
+
if command -v kubectl >/dev/null 2>&1; then
|
|
71
|
+
for ctx in $(kubectl config get-contexts -o name 2>/dev/null | grep -E "$LOCAUX" || true); do
|
|
72
|
+
k=(kubectl --context "$ctx" --request-timeout=5s)
|
|
73
|
+
"${k[@]}" get --raw /readyz >/dev/null 2>&1 || continue
|
|
74
|
+
marque="$("${k[@]}" -n kube-system get deployment metrics-server -o jsonpath='{.metadata.labels.devzone}' 2>/dev/null)" || continue
|
|
75
|
+
if [ "$marque" = installe ]; then metrics+=("$ctx"); else etrangers+=("$ctx"); fi
|
|
76
|
+
done
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- Confirmation ------------------------------------------------------------
|
|
80
|
+
afficher() { if [ "$#" -eq 0 ]; then echo " (rien)"; else printf ' %s\n' "$@"; fi; }
|
|
81
|
+
echo "À supprimer :"
|
|
82
|
+
echo " conteneur(s) :"; afficher ${conteneurs[@]+"${conteneurs[@]}"}
|
|
83
|
+
echo " image(s) :"; afficher ${images[@]+"${images[@]}"}
|
|
84
|
+
echo " Docker interne (images et clusters kind) :"; afficher ${dind[@]+"${dind[@]}"}
|
|
85
|
+
echo " volumes :"; afficher ${volumes[@]+"${volumes[@]}"}
|
|
86
|
+
echo " metrics-server installé par devzone, sur :"; afficher ${metrics[@]+"${metrics[@]}"}
|
|
87
|
+
[ "${#gardes[@]}" -eq 0 ] || { echo "Gardés (GARDER=connexions) :"; printf ' %s\n' "${gardes[@]}"; }
|
|
88
|
+
if [ "${#conteneurs[@]}" -eq 0 ] && [ ! -f "$ETAT" ] && [ "${#images[@]}" -eq 0 ]; then
|
|
89
|
+
echo "ℹ Aucun conteneur ni trace de l'image : si un make down d'une version antérieure les a laissés,"
|
|
90
|
+
echo " les retrouver avec : docker images 'vsc-$(basename "$ICI")-*' ; docker volume ls -q -f name=dind-var-lib-docker"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
total=$((${#conteneurs[@]} + ${#images[@]} + ${#dind[@]} + ${#volumes[@]} + ${#metrics[@]}))
|
|
94
|
+
if [ "$total" -eq 0 ]; then
|
|
95
|
+
echo "✔ Rien à supprimer."
|
|
96
|
+
else
|
|
97
|
+
if [ "${CONFIRMER:-}" != oui ]; then
|
|
98
|
+
if [ ! -t 0 ]; then
|
|
99
|
+
echo "✖ Confirmation impossible sans terminal : CONFIRMER=oui make desinstaller" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
read -r -p "Supprimer définitivement ? Taper oui : " reponse
|
|
103
|
+
[ "$reponse" = oui ] || { echo "Abandon : rien n'a été supprimé."; exit 0; }
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# --- Suppression ---------------------------------------------------------
|
|
107
|
+
for id in ${conteneurs[@]+"${conteneurs[@]}"}; do docker rm -f "$id" >/dev/null && echo "✔ conteneur supprimé : $id"; done
|
|
108
|
+
for i in ${images[@]+"${images[@]}"}; do docker rmi "$i" >/dev/null && echo "✔ image supprimée : $i"; done
|
|
109
|
+
for v in ${dind[@]+"${dind[@]}"} ${volumes[@]+"${volumes[@]}"}; do docker volume rm "$v" >/dev/null && echo "✔ volume supprimé : $v"; done
|
|
110
|
+
if [ "${#metrics[@]}" -gt 0 ]; then
|
|
111
|
+
version="$(sed -n 's/^VERSION=//p' "$ICI/.devcontainer/metrics-server.sh")"
|
|
112
|
+
manifeste="https://github.com/kubernetes-sigs/metrics-server/releases/download/${version}/components.yaml"
|
|
113
|
+
for ctx in "${metrics[@]}"; do
|
|
114
|
+
kubectl --context "$ctx" delete -f "$manifeste" --ignore-not-found >/dev/null &&
|
|
115
|
+
echo "✔ metrics-server retiré : $ctx"
|
|
116
|
+
done
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
rm -f "$ICI/.devzone.log" "$ETAT"
|
|
120
|
+
|
|
121
|
+
# --- Ce qui reste ------------------------------------------------------------
|
|
122
|
+
echo
|
|
123
|
+
echo "Non touché (à retirer toi-même si tu le souhaites) :"
|
|
124
|
+
echo " workspace/ : tes projets"
|
|
125
|
+
for ctx in ${etrangers[@]+"${etrangers[@]}"}; do
|
|
126
|
+
echo " metrics-server sur $ctx : non installé par devzone (kubectl --context $ctx -n kube-system delete deployment metrics-server)"
|
|
127
|
+
done
|
|
128
|
+
command -v kubectl >/dev/null 2>&1 || echo " metrics-server : kubectl absent de l'hôte, clusters de l'hôte non vérifiés"
|
|
129
|
+
echo " image de base mcr.microsoft.com/devcontainers/base (partagée) : docker rmi mcr.microsoft.com/devcontainers/base:ubuntu-24.04"
|
|
130
|
+
echo " cache npx du CLI devcontainer : ~/.npm/_npx"
|
|
131
|
+
echo " autorisation « GitHub CLI » sur ton compte : GitHub → Settings → Applications"
|
|
132
|
+
echo " ce dossier : $ICI"
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devzone",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Zone de dev conteneurisée : Java, Node, Python, Docker, Kubernetes et outils repowarden, dans un devcontainer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/SimBienvenueHoulBoumi/devzone.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/SimBienvenueHoulBoumi/devzone#readme",
|
|
11
|
+
"bugs": "https://github.com/SimBienvenueHoulBoumi/devzone/issues",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"devcontainer",
|
|
14
|
+
"docker",
|
|
15
|
+
"kubernetes",
|
|
16
|
+
"repowarden",
|
|
17
|
+
"environnement"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"devzone": "bin/devzone"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
".devcontainer/",
|
|
25
|
+
"Makefile",
|
|
26
|
+
".gitattributes",
|
|
27
|
+
"desinstaller.sh",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
]
|
|
31
|
+
}
|