Changer son environnement fullstack en un clin d'oeil : partie 2 avec Maven

hcross

Hoani CROSS

Posted on February 12, 2023

Changer son environnement fullstack en un clin d'oeil : partie 2 avec Maven

Vous êtes développeur consultant et vous travaillez pour plusieurs clients ? Vous travaillez aussi sur des projets Open Source ?

Vous avez certainement été confronté au problème de changement de contexte lorsque vous devez passer d'un projet à un autre : changement de version de Java, de Node.js, de configuration Git ou de signature GnuPG...

J'ai commencé une série d'articles sur ce sujet avec un premier article au sujet du changement de version de Java.

Je continue ici cette série et j'aimerais aborder la capacité de Maven à changer de contexte de développement.

Ma façon préférée d'installer Maven

Pour installer Maven, vous pouvez le faire de multiples façons... Passer par le gestionnaire de paquets de votre distribution Linux, utiliser brew sur MacOS, utiliser Chocolatey sous Windows, voire télécharger directement le Zip du projet.

Me concernant, je préfère utiliser l'outil dont je vous ai parlé dans mon premier article : SDKMan !

Avec SDKMan, pour installer Maven, rien de plus simple, il suffit de taper la commande suivante dans votre terminal :

~❯ sdk install maven
Downloading: maven 3.9.0
In progress...
########################################################################## 100,0%
Installing: maven 3.9.0
Done installing!
Do you want maven 3.9.0 to be set as default? (Y/n): 
Setting maven 3.9.0 as default.
Enter fullscreen mode Exit fullscreen mode

Durant l'installation, SDKMan vous demandera si vous désirez définir la version de Maven téléchargée comme étant le version à utiliser par défaut. Vous pourrez répondre par l'affirmative en appuyant simplement sur la touche 'Entrée'.

On pourra vérifier la bonne installation de Maven à l'aide de la commande suivante :

~❯ mvn -version
Apache Maven 3.9.0 (9b58d2bad23a66be161c4664ef21ce219c2c8584)
Maven home: /Users/hoanicross/.sdkman/candidates/maven/current
Java version: 19.0.2, vendor: Oracle Corporation, runtime: /Users/hoanicross/.sdkman/candidates/java/19.0.2-open
Default locale: fr_FR, platform encoding: UTF-8
OS name: "mac os x", version: "13.2", arch: "aarch64", family: "mac"
Enter fullscreen mode Exit fullscreen mode

Comme vous pouvez le remarquer, j'utilise un Mac avec une puce ARM d'Apple 😊, en l'occurrence il s'agit d'un Macbook Air M2 !

Comment Maven gère les environnements de développement

Maven intègre en son sein un moyen assez efficace de gérer les différents environnements de développement avec les profils !

Il peuvent s'activer de diverses manières : présence d'une variable d'environnement, détection du type de l'OS ou encore présence d'un fichier...

Cependant, en y regardant de plus près, on remarque que les profils ont surtout été créés pour gérer les différents environnements d'exécution (dev, prod ou preprod par exemple) mais pas vraiment les environnements de développement.

Typiquement, ce qu'on va chercher à changer dans le fichier de configuration de Maven pour passer d'un contexte de développement à un autre, c'est :

  • l'éventuel proxy utilisé dans votre mission
  • l'éventuel mirroir utilisé dans votre mission

Ces deux parties sont configurables sous la forme d'un élément respectivement <proxy> ou <mirror> que l'on retrouvera dans des éléments <proxies> ou <mirrors> juste sous l'élément racine <settings> du fichier settings.xml de Maven.

En l'occurrence, dans la mission où je travaille à l'heure où j'écris ces lignes, nous devons utiliser un référentiel interne mirroir de tous les référentiels Maven.

Dans le fichier settings.xml, cela se présente de la façon suivante :

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
          
    <!-- Miroir pour la gestion des dépendances-->
    <mirrors>
        <mirror>
            <id>nexus</id>
            <mirrorOf>*</mirrorOf>
            <url>https://nexus.domaine-interne-de-mon-client.com/repo/maven/</url>
        </mirror>
    </mirrors>
    
</settings>
Enter fullscreen mode Exit fullscreen mode

Dès qu'une dépendance doit être téléchargée de quelque référentiel Maven que ce soit, c'est le serveur Nexus de mon client qui sera interrogé via l'URL https://nexus.domaine-interne-de-mon-client.com/repo/maven/ qui n'est accessible QUE lorsque le VPN est activé...

Clairement, si je ne suis plus connecté au VPN du client et que je souhaite travailler sur un projet Java open source perso avec Maven, ça ne va plus marcher : les dépendances vont être téléchargées depuis le Nexus qui est indisponible (le VPN n'est pas monté) et la construction va échouer.

Pour continuer à travailler sur ce projet, il me faudrait éditer le fichier ~/.m2/settings.xml, et mettre en commentaire le bloc <mirrors>...</mirrors> à chaque fois...

Et la même opération devra être réalisée lorsque je devrais à nouveau travailler sur un projet de mon client.

Une vraie purge ☹️

Il me fallait donc absolument trouver une solution pour que cette bascule se fasse automatiquement !

Comment ChatGPT m'a mis sur la voie

Dans cette quête de pouvoir obtenir mon mécanisme de changement de contexte de développement, je me suis dit que ChatGPT pouvait peut-être m'aider.

Voici la requête que je lui ai soumis :

Hello, I would like to activate the ability of maven settings to setup a local repository to be the mirror of all existing repository, especially the public ones, only when I am connected to their network. For all other situation, I want this mirroring property to be deactivated. Do you have a solution for my problem ?

Ici, je lui ai demandé de pouvoir activer automatiquement la notion de référentiel local en tant que mirroir de toutes les autres, seulement lorsque je suis connecté au réseau du client.

Dans sa réponse, il fait justement référence à la notion de profil dont je vous ai parlé précédemment :

Yes, you can configure your Maven settings to mirror remote repositories when you are connected to a specific network, and to not mirror them when you are not connected to that network. To do this, you will need to create two separate profiles in your settings.xml file: one for the scenario where you are connected to the network, and one for when you are not.

In the profile for when you are connected to the network, you can specify a mirror that points to the local repository, and include a <mirrorOf>*</mirrorOf> element to indicate that all remote repositories should be mirrored to the local repository.

In the profile for when you are not connected to the network, you can remove the mirror from the profile, and instead include the remote repositories that you want to use directly in the <repositories> element.

You can use <activeByDefault> and <activation> elements to specify which profile is active depending on your network state.

It is important to note that, if you want to use this feature, you need to have a local repository already configured, and you should consider the amount of space that the repository will need.

Ah mais c'est super ça 😃 ! Je vais donc pouvoir utiliser les profils pour activer ou non les mirroirs des référentiel Maven... Il fallait que j'en sache plus et j'ai donc demandé à ChatGPT :

Can you provide me an example of a settings.xml maven file that implements those needs ?

Et voici le fichier exemple qu'il m'a fournit :

<settings>
    <mirrors>
        <mirror>
            <id>mirror-local-repo</id>
            <mirrorOf>*</mirrorOf>
            <url>file:///path/to/local/repo</url>
        </mirror>
    </mirrors>

    <profiles>
        <profile>
            <id>on-corporate-network</id>
            <activation>
                <property>
                    <name>onCorporateNetwork</name>
                    <value>true</value>
                </property>
            </activation>
            <repositories>
                <repository>
                    <id>central</id>
                    <url>http://central</url>
                    <releases>
                        <enabled>true</enabled>
                        <updatePolicy>always</updatePolicy>
                    </releases>
                </repository>
            </repositories>
        </profile>
        <profile>
            <id>off-corporate-network</id>
            <activation>
                <property>
                    <name>onCorporateNetwork</name>
                    <value>false</value>
                </property>
            </activation>
            <repositories>
                <repository>
                    <id>central</id>
                    <url>http://central</url>
                    <releases>
                        <enabled>true</enabled>
                        <updatePolicy>always</updatePolicy>
                    </releases>
                </repository>
                <repository>
                    <id>other-repo</id>
                    <url>http://other-repo</url>
                    <releases>
                        <enabled>true</enabled>
                        <updatePolicy>always</updatePolicy>
                    </releases>
                </repository>
            </repositories>
        </profile>
    </profiles>
</settings>
Enter fullscreen mode Exit fullscreen mode

Euuuh 🧐 le bloc <proxies> n'est pas inclus où même associé à la partie concernant les différents profils et je m'empresse d'en faire mention à ChatGTP :

However, in the example you provided, the mirror functionnality will be activated for both profiles since it is not part of the definition of the <profile> element. I really want the mirror to be deactivated when the off-corporate-network is active.

Et admirez ce que ChatGPT me propose comme fichier settings.xml :

<settings>
    <profiles>
        <profile>
            <id>on-corporate-network</id>
            <activation>
                <property>
                    <name>onCorporateNetwork</name>
                    <value>true</value>
                </property>
            </activation>
            <mirrors>
                <mirror>
                    <id>mirror-local-repo</id>
                    <mirrorOf>*</mirrorOf>
                    <url>file:///path/to/local/repo</url>
                </mirror>
            </mirrors>
            <repositories>
                <repository>
                    <id>central</id>
                    <url>http://central</url>
                    <releases>
                        <enabled>true</enabled>
                        <updatePolicy>always</updatePolicy>
                    </releases>
                </repository>
            </repositories>
        </profile>
        <profile>
            <id>off-corporate-network</id>
            <activation>
                <property>
                    <name>onCorporateNetwork</name>
                    <value>false</value>
                </property>
            </activation>
            <repositories>
                <repository>
                    <id>central</id>
                    <url>http://central</url>
                    <releases>
                        <enabled>true</enabled>
                        <updatePolicy>always</updatePolicy>
                    </releases>
                </repository>
                <repository>
                    <id>other-repo</id>
                    <url>http://other-repo</url>
                    <releases>
                        <enabled>true</enabled>
                        <updatePolicy>always</updatePolicy>
                    </releases>
                </repository>
            </repositories>
        </profile>
    </profiles>
</settings>
Enter fullscreen mode Exit fullscreen mode

Eh bien voilà ! C'était tellement simple... Mais je voulais quand même en avoir le coeur net car la documentation de Maven ne mentionne pas du tout cette possibilité.

Voici ce que j'ai dit à ChatGPT :

Reading the version 1.0.0 of the Maven settings XSD, the <profile> element can only contain direct child elements of type <id>, <activation>, <repositories>, <pluginRepositories> or <properties>. It cannot contain a <mirrors> child element.

Et vous savez quoi ? Il a admi son erreur et il est revenu sur sa première proposition qui ne permettait pas de conditionner l'activation des mirroirs selon les profils... On se marche sur la tête 🤨

C'est alors que je me suis dit que si le fonctionnement interne de Maven ne permettait pas d'activer ou non dynamiquement la notion de mirroir, il se pourrait qu'un outil externe puisse le faire à la place de Maven.

J'ai commencé à rédiger ma requête pour ChatGPT quand je me suis dit : "je peux le faire moi-même, sans l'aide de ChatGPT".

Ainsi, en me faisant tourner en bourrique, ChatGPT m'a ouvert sur une possibilité externe à Maven : jongler avec plusieurs fichiers de configuration !

Avoir plusieurs fichiers de configuration

Par défaut, Maven va chercher sa configuration dans :

  • le fichier pom.xml du projet courant
  • le fichier ~/.m2/settings.xml
  • le fichier conf/settings.xml du répertoire d'installation de Maven

Cela ne laisse pas beaucoup de place pour insérer quelque chose...

Si on tape mvn --help dans un terminal, on peut lire cette option de maven :

 -s,--settings <arg>                    Alternate path for the user
                                        settings file
Enter fullscreen mode Exit fullscreen mode

C'est exactement ce dont j'avais besoin !

Lorsque je travaille sur un projet d'un client, on va dire lambda, je veux utiliser le fichier de configuration ~/.m2/settings-lambda.xml.

Pour les autres projets Open Source perso, le fichier ~/.m2/settings.xml d'origine fera très bien l'affaire !

Pour un projet Open Source sur lequel je travail, je peux utiliser la commande mvn comme à l'accoutumée.

Pour un projet de mon client lambda, par contre, je vais donc devoir faire :

~❯ mvn -s ~/.m2/settings-lambda.xml clean install
Enter fullscreen mode Exit fullscreen mode

Bon on va dire que c'est mieux que d'éditer le fichier settings.xml de Maven mais ça ne me convient toujours pas ! Moi ce que j'aimerais c'est de trouver un moyen de configurer ça automatiquement pour toute une arborescence !

Un wrapper magique pour mvn

Ma solution idéale voudrait que j'ai juste à utiliser la commande mvn comme j'ai l'habitude de le faire et que le fichier de configuration spécifique soit ajouté à la commande à l'aide de l'option -s, uniquement quand c'est nécessaire.

Dans mon cas, je veux que ça soit activé lorsque je suis dans mon répertoire parent de tous les projets de mon client lambda, à savoir ~/devel/lambda.

Retrouver le nom du fichier de configuration spécifique

Par convention, j'ai donc décidé de dire que lorsqu'un fichier nommé .m2_settings était trouvé dans le répertoire courant ou dans un de ses répertoires parents, son contenu sera lu et considédé comme le chemin d'un fichier de configuration spécifique à prendre en compte au lancement de la commande mvn.

En simple langage shell, j'en suis arrivé au code suivant pour obtenir le contenu de ce fameux fichier .m2_settings :

# Find the given file going up in the parent directories
# until found, return "" either
find_up() {
  pth=$(pwd)
  while [[ "$pth" != "" && ! -e "$pth/$1" ]]; do
    pth=${pth%/*}
  done
  echo "$pth"
} 

M2_SETTINGS_CONFIG_NAME=.m2_settings

check_mvn_settings() {
  MVN_SETTINGS=~/.m2/settings.xml
  fnd_dir=$(find_up $M2_SETTINGS_CONFIG_NAME)
  if [[ "" != "$fnd_dir" ]]; then
    MVN_SETTINGS=$(cat $fnd_dir/$M2_SETTINGS_CONFIG_NAME)
  fi
  echo "${MVN_SETTINGS/#\~/$HOME}"
}
Enter fullscreen mode Exit fullscreen mode

La fonction find_up() trouve un fichier dont le nom est passé en paramètres dans le répertoire courant et ses répertoires parents jusqu'à ce qu'il soit trouvé, et dans le cas où ce fichier serait introuvable, une chaîne vide sera retournée.

La fonction check_mvn_settings() retournera :

  • le nom du fichier de configuration sécifique trouvé dans le fichier .m2_settings s'il est trouvé
  • $HOME/.m2/settings.xml sinon

Notez que l'expression ${MVN_SETTINGS/#\~/$HOME} permet de remplacer toute occurence de ~ par $HOME dans la variable d'environnement MVN_SETTINGS avant de la retourner car sans cela ça générait des problèmes d'interprétation.

Trouver le binaire mvn d'origine

Mon wrapper sera intitulé mvn comme la commande d'origine. Je ne veux pas changer mes habitudes.

Le chemin vers ce dernier sera d'ailleurs prioritaire au chemin du binaire mvn d'origine pour qu'il puisse faire cette action magique d'ajout automatique du fichier de configuration spécifique...

Seulement voilà : je ne voulais pas avoir à préciser dans ce dernier l'emplacement du binaire de Maven d'origine.

Je voulais que mon script le trouve tout seul.

Voici le bout de code shell qui réalise cette opération :

get_mvn_bin() {
  directory_to_remove=$(cd $(dirname $0) && pwd)
  PATH=:$PATH:
  PATH=${PATH//:$directory_to_remove:/:}
  PATH=${PATH#:}; PATH=${PATH%:}
  echo $(whereis mvn | awk '{print$2}')
}
Enter fullscreen mode Exit fullscreen mode

Je sais, c'est magique. Et en plus ça marche 😝

Pour résumer le fonctionnement de ce code, ce dernier :

  • récupère le répertoire du wrapper
  • retire ce répertoire de la variable d'environnement PATH
  • appelle la commande whereis pour trouver le chemin de la commande mvn d'origine (le wrapper n'étant plus trouvable ici)
  • retourne ce chemin à l'appelant

Le corps principal du wrapper

Il ne reste plus qu'a appeler toutes ces fonctions et gérer le cas où Maven n'est pas trouvé :

MVN_CMD=$(get_mvn_bin)
if [[ "" == "$MVN_CMD" ]]; then
  >&2 echo "mvn command not found in PATH variable !"
  exit -1
fi
MVN_SETTINGS=$(check_mvn_settings)
$MVN_CMD -s $MVN_SETTINGS $@
Enter fullscreen mode Exit fullscreen mode

Et voilà !

Le script complet

Voici le code du script complet :

#!/bin/sh

# Find the given file going up in the parent directories
# until found, return "" either
find_up() {
  pth=$(pwd)
  while [[ "$pth" != "" && ! -e "$pth/$1" ]]; do
    pth=${pth%/*}
  done
  echo "$pth"
} 

M2_SETTINGS_CONFIG_NAME=.m2_settings

check_mvn_settings() {
  MVN_SETTINGS=~/.m2/settings.xml
  fnd_dir=$(find_up $M2_SETTINGS_CONFIG_NAME)
  if [[ "" != "$fnd_dir" ]]; then
    MVN_SETTINGS=$(cat $fnd_dir/$M2_SETTINGS_CONFIG_NAME)
  fi
  echo "${MVN_SETTINGS/#\~/$HOME}"
}

get_mvn_bin() {
  directory_to_remove=$(cd $(dirname $0) && pwd)
  PATH=:$PATH:
  PATH=${PATH//:$directory_to_remove:/:}
  PATH=${PATH#:}; PATH=${PATH%:}
  echo $(whereis mvn | awk '{print$2}')
}

MVN_CMD=$(get_mvn_bin)
if [[ "" == "$MVN_CMD" ]]; then
  >&2 echo "mvn command not found in PATH variable !"
  exit -1
fi
MVN_SETTINGS=$(check_mvn_settings)
$MVN_CMD -s $MVN_SETTINGS $@
Enter fullscreen mode Exit fullscreen mode

Faire un wrapper vraiment complet

Rediriger la commande mvn ne me semblait pas suffisant.

En effet, je voulais notamment que cette commande se retrouve dans un répertoire dont la structure est identique à celle d'une installation de Maven classique.

J'ai opté pour la convention suivante :

  • le répertoire d'installation de mon wrapper sera ~/.wrapper/maven
  • tout sont contenu sera un lien symbolique du répertoire d'installation de la version courante de Maven dans le répertoire de SDKMan
  • à l'exception du répertoire bin qui contiendra mon wrapper magique et des liens symboliques vers les autres binaires

Voilà ce que ça donne :

~❯ ls -lah .wrapper/maven
total 0
drwxr-xr-x  9 hoanicross  staff   288B 26 jan 08:07 .
drwxr-xr-x  3 hoanicross  staff    96B 26 jan 08:04 ..
lrwxr-xr-x  1 hoanicross  staff    58B 26 jan 08:06 LICENSE -> /Users/hoanicross/.sdkman/candidates/maven/current/LICENSE
lrwxr-xr-x  1 hoanicross  staff    57B 26 jan 08:06 NOTICE -> /Users/hoanicross/.sdkman/candidates/maven/current/NOTICE
lrwxr-xr-x  1 hoanicross  staff    61B 26 jan 08:07 README.txt -> /Users/hoanicross/.sdkman/candidates/maven/current/README.txt
drwxr-xr-x  8 hoanicross  staff   256B 12 fév 23:33 bin
lrwxr-xr-x  1 hoanicross  staff    55B 26 jan 08:06 boot -> /Users/hoanicross/.sdkman/candidates/maven/current/boot
lrwxr-xr-x  1 hoanicross  staff    55B 26 jan 08:06 conf -> /Users/hoanicross/.sdkman/candidates/maven/current/conf
lrwxr-xr-x  1 hoanicross  staff    54B 26 jan 08:06 lib -> /Users/hoanicross/.sdkman/candidates/maven/current/lib

~❯ ls -lah .wrapper/maven/bin
total 8
drwxr-xr-x  8 hoanicross  staff   256B 12 fév 23:33 .
drwxr-xr-x  9 hoanicross  staff   288B 26 jan 08:07 ..
lrwxr-xr-x  1 hoanicross  staff    62B 26 jan 08:08 m2.conf -> /Users/hoanicross/.sdkman/candidates/maven/current/bin/m2.conf
-rwxr-xr-x@ 1 hoanicross  staff   876B  6 fév 11:14 mvn
lrwxr-xr-x  1 hoanicross  staff    62B 26 jan 08:07 mvn.cmd -> /Users/hoanicross/.sdkman/candidates/maven/current/bin/mvn.cmd
lrwxr-xr-x  1 hoanicross  staff    63B 26 jan 08:07 mvnDebug -> /Users/hoanicross/.sdkman/candidates/maven/current/bin/mvnDebug
lrwxr-xr-x  1 hoanicross  staff    67B 26 jan 08:08 mvnDebug.cmd -> /Users/hoanicross/.sdkman/candidates/maven/current/bin/mvnDebug.cmd
lrwxr-xr-x  1 hoanicross  staff    61B 26 jan 08:08 mvnyjp -> /Users/hoanicross/.sdkman/candidates/maven/current/bin/mvnyjp
Enter fullscreen mode Exit fullscreen mode

Comme vous pouvez le voir, j'ai donc utilisé la commande ln -s à outrance pour arriver à mes fins !

Rendre le wrapper disponible dans la variable d'environnement PATH

Il ne reste plus maintenant qu'à rendre ce merveilleux wrapper disponible dans la variable d'environnement PATH en priorité.

On pourra notamment créer une variable d'environnement MAVEN_HOME pointant vers le répertoire d'installation du wrapper et on ajoutera $MAVEN_HOME/bin au tout début de la variable d'environnement PATH.

Cela pourra notamment être rendu possible en ajoutant les lignes suivantes à la fin de votre script d'initialisation de votre interpréteur de commande (~/.bashrc si vous utilisez bash ou ~/.zshrc si vous utilisez zsh) :

export MAVEN_HOME=~/.wrapper/maven
export PATH=$MAVEN_HOME/bin;$PATH
Enter fullscreen mode Exit fullscreen mode

Relancez votre terminal et vous voilà paré pour changer d'environnement de développement à la volée avec Maven 🤤 en utilisant la commande mvn comme à l'accoutumée...

Limite de ce procédé dans les IDE

Ce wrapper est surtout magique en ligne de commande.

Dans votre IDE, cela ne marchera pas de façon transparente.

J'ai notamment pu constater que dans IntelliJ IDEA, étant donné que ce dernier utilise une installation de Maven embarquée par défaut, le changement de fichier de configuration ne se fait pas automatiquement.

Néanmoins, il reste possible de configurer le fichier de configuration à prendre en considération pour le projet en cours, ce qui nécessite une manipulation à faire une seule fois pour chaque projet. Pour moi cette contrainte est largement acceptable !

J'en profite aussi pour définir, dans le champ consacré à cet effet, le répertoire d'installation de Maven comme étant le répertoire d'installation de mon wrapper.

Et la suite ?

Dans mon prochain article, je vous expliquerais comment j'ai réussi à jongler avec mes identités et signatures Git en fonction des projets sur lesquels je travaile !

Si vous voulez savoir quand je sortirais un nouvel article, suivez-moi sur LinkedIn ou Dev.to !

J'aimerais aussi terminer en vous expliquant rapidement qui je suis 😄

Je m'appelle Hoani CROSS. Je suis originaire de Polynésie Française.

Passionné d'informatique depuis toujours (j'ai commencé à développer en CP, il y a environ 40 ans), j'ai évolué dans beaucoup d'environnements et occupé de nombreux poste, jusqu'à celui de DSI...

Aujourd'hui je suis manager et backend architect chez SFEIR, une Néo-ESN française. Une des particularités de SFEIR, c'est que les managers sont également des exécutants en mission chez des clients. On comprends ce que font les employés qu'on manage et croyez-moi, ça fait une sacré différence ! Et enfin, ce qui nous relie tous chez SFEIR, c'est la passion pour l'informatique... Étant donné qu'elle me suit depuis 40 ans, autant vous dire que je suis tombé au bon endroit !..

D'ailleurs, si vous êtes passionné comme moi, il y certainement une mission de rêve qui vous attends chez SFEIR alors n'hésitez pas à me contacter si vous voulez changer d'horizon.

Enfin, je suis aussi le propriétaire du blog Geek & Rentable, que je vous invite à aller découvrir.

Dans ce blog, je souhaite accompagner les développeurs/développeuses ou autre acteurs/actrices de la tech, passionné.e.s mais malheureusement incompris.es, de manière à ce qu'ils ou elles puissent évoluer vers de bien meilleurs conditions de travail.

À très bientôt.

Illustration par Ang3llor3 sur DeviantArt

💖 💪 🙅 🚩
hcross
Hoani CROSS

Posted on February 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related