Un peu plus d’un mois après la création de mon article sur le déploiement de mon blog, je tombe sur cet article, qui parle notamment d’une nouvelle fonctionnalité de gitea, l’intégration de CI/CD directement dans l’outil.
Je me dis que je testerai bien tout ça, et quoi de mieux que déployer mon blog avec.

Pour ceci, je ne touche pas à la configuration nginx que j’avais déjà, vous pouvez la retrouver dans mon précédent article.

Gitea Actions

Gitea Actions n’est pas sans rappeler Github Actions, et pour cause, c’est exactement la même syntaxe, techniquement si vous êtes sous github, vous pouvez quasiment copier votre repository de github à gitea, avec simplement le répertoire .github à renommer en .gitea (et encore, je ne suis pas sûr que ce soit réellement utile).

Je découvre

Personnellement, je n’avais jamais utilisé Github Actions avant, donc c’est un nouveau format que je découvre en écrivant cet article. Juste pour votre information, je me suis principalement basé sur la documentation officielle de Github, et notamment la syntaxe et les évènements.
Pour manipuler un peu, j’ai forké un projet sur Github, un plugin asdf (d’ailleurs ça fera l’objet d’un futur article), pour lequel j’ai créé une petite pipeline.

Installation

Pour ceci, il vous faudra au minimum la version 1.19 de gitea.

Configuration de gitea

Je pars du principe que vous avez déjà un gitea d’installé, cependant il y a une petite configuration à faire, il suffit de modifier le fichier app.ini :

[actions]
ENABLED = true

Ensuite dans les paramètres (onglet Repository) de votre dépôt git, vous aurez une option Enable Repository Actions qu’il suffit d’activer.

Installation d’un runner

Service systemd

Avant de l’installer, nous allons créer un utilisateur pour notre runner, et le mettre dans le groupe docker :

$ useradd --system --shell /bin/bash --home-dir /var/lib/gitea-runner --create-home gitea-runner
$ usermod -G docker -a gitea-runner

On télécharge l’agent :

$ wget https://dl.gitea.com/act_runner/main/act_runner-main-linux-amd64 -O /usr/local/bin/act_runner
$ chmod +x /usr/local/bin/act_runner

Nous allons créer un petit service systemd également :

# /etc/systemd/system/gitea_runner.service
[Unit]
Description=Gitea Actions Runner
After=network-online.target

[Service]
Type=simple
WorkingDirectory=/var/lib/gitea-runner/
User=gitea-runner
Group=gitea-runner
ExecStart=/usr/local/bin/act_runner daemon
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

Nous ne l’activons pas maintenant

Configuration de gitea

Il faut maintenant aller sur gitea, avec un compte administrateur, pour aller enregistrer un nouveau runner. Pour ceci il faut aller dans Administration du site puis Runners, et on clique sur Create new Runner. Il vous donnera donc un token.

Enregistrement du runner

Puis on se connecte avec cet utilisateur pour enregistrer notre runner :

$ sudo -i -u gitea-runner

et on lancera une commande du style :

$ ./act_runner register --instance http://<your_gitea_instance> --token <your_runner_token> --no-interactive

Il faut savoir que le runner à un système de label, qui permets de savoir sur quel runner lancer nos workflows, mais aussi qu’elle image lancer.
Par défaut, nous avons 4 labels (de souvenir), ubuntu-latest, ubuntu-22.04, ubuntu-20.04 et ubuntu-18.04. En réalité, ces 4 labels utilisent la même image docker, à savoir node:16-bullseyes (quoi qu’il me semble que ubuntu-18.04 utilise une base buster).
De ce que j’ai compris, beaucoup d'Actions utilisent nodejs pour s’exécuter, par exemple le plus utilisé, qui est actions/checkout@v3, ne fonctionnera pas si vous n’avez pas nodejs sur le conteneur (on y reviendra).
Donc nous pouvons utilisez nos propres labels, personnellement, j’ai mis ceci :

  • linux
  • linux-x86
  • linux-amd64
  • ubuntu-latest (car c’est souvent ce qu’on a quand on fait un copier/coller bêtement ^^)

Nous pouvons aussi associé une image, en utilisant cette syntaxe : alpine:docker://alpine:3.17. Mais puisqu’il faut nodejs sur l’image utilisée, ce n’est pas forcément utile.

Donc pour enregistrer mon runner, j’ai lancé ceci :

$ ./act_runner register --instance http://<your_gitea_instance> --token <your_runner_token> --no-interactive --labels "linux,linux-x86,linux-amd64,ubuntu-latest""

gitea_runner

Une fois qu’il apparait bien dans votre gitea, nous pouvons lancer le service systemd :

$ systemctl daemon-reload
$ systemctl enable --now gitea_runner.service

Et voilà, notre runner est maintenant fonctionnel, je compte en créer un autre sur un raspberry Pi, pour avoir un runner ARM.

Workflow

Si vous êtes familier avec les github actions, vous savez donc qu’il y a 3 niveaux de définition :

  • Les workflows : Les workflows sont composés de jobs, et seront exécuter sur un évènement (push, tag, pull_requests …)
  • Les jobs : Les jobs sont composés de steps, ce sera un ensemble de tâches à exécuter ()
  • Les steps : Les steps vont êtres les tâches à exécuter, soit on utilise une actions, soit on utilise une commande.

Mon workflow

Si vous avez lu mon précédent article, vous avez dû voir qu’avec droneio, j’utilisais le principe de promote pour passer à la prod. Nous n’avons plus ce principe coté gitea Actions, donc nous utiliserons les Pulls Requests :

  • J’écris mon article sur une nouvelle branche, ça me crée un environnement temporaire.
  • Je crée un Pull Request.
  • Une fois ok, je merge mon article vers la main.
  • De cet évènement, nous supprimons mon environnement temporaire.
  • Un push (via le merge) s’effectue sur ma branche main, et lance une pipeline de déploiement vers la production.

Pour ceci, j’ai créé 3 workflows différents :

  • Dev, sur push sur une branche autre que main
  • Delete_env, sur clôture et merge d’une Pull Request
  • Production, sur push sur main

Dev

Sur ce workflow, nous allons avoir plusieurs jobs :

  • build : Cette partie va lancer la commande hugo pour générer mon blog
  • test : Quelques tests, notamment vérifier que le html est correcte, et une passe orthographique
  • deploy : Déploiement sur un environnement de dev

Pour chaque jobs, j’ai ajouté un step pour la notification, pour le build et le test, je ne veux qu’en cas d’erreur, par contre, pour le deploy, je veux tout le temps recevoir une notification.
Voici ce que ça donne sur discord : discord

Et voici donc mon fichier :

name: Build and Test blog
run-name: buildandtest
on:
  push:
    branches-ignore:
      - main
jobs:
  build:
    steps:
      - name: checkout
        uses: https://github.com/actions/checkout@v3
      - name: configure container
        run: apt update && apt install curl
      - name: Setup Hugo
        uses: https://github.com/peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.102.1'
          extended: true
      - name: Build Blog
        env:
          TZ: 'Europe/Paris'
        run: hugo --minify --environment production
      - name: Push artifacts
        run: |
          cd public && \
          tar czvf ../blog.tar.gz * && \
          cd .. && \
          GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | cut -d"/" -f1) && \
          GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d"/" -f2) && \
          curl -XPUT -H "Authorization: token ${{ secrets.CI_GITEA_PACKAGES_TOKEN }}" --upload-file blog.tar.gz $GITHUB_SERVER_URL/api/packages/${GITHUB_REPO_OWNER}/generic/${GITHUB_REPO_NAME}/${GITHUB_SHA}/blog.tar.gz          
      - name: Notify if failed
        uses: https://github.com/sarisia/actions-status-discord@v1
        if: ${{ failure() }}
        with:
          webhook: ${{ secrets.DISCORD_WEBHOOK }}
          status: "Failure"
          content: "Hey"
          title: "build"
          description: "Build ${{ env.CI_GIT_BRANCH }} Failed"
          color: 0xff0000
  test:
    needs: [build]
    steps:
      - name: checkout
        uses: https://github.com/actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Download artifacts
        run: |
          GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | cut -d"/" -f1) && \
          GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d"/" -f2) && \
          curl -XGET -H "Authorization: token ${{ secrets.CI_GITEA_PACKAGES_TOKEN }}" $GITHUB_SERVER_URL/api/packages/${GITHUB_REPO_OWNER}/generic/${GITHUB_REPO_NAME}/${GITHUB_SHA}/blog.tar.gz > blog.tar.gz && \
          mkdir public && tar xzf blog.tar.gz -C public && rm -f blog.tar.gz          
      - name: html5validator
        uses: https://github.com/Cyb3r-Jak3/html5validator-action@v7.2.0
        with:
          root: public/
      - name: Setup java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '11'
      - name: Languagetool
        run: |
          wget -4 -q https://languagetool.org/download/LanguageTool-5.6.zip
          unzip -q LanguageTool-5.6.zip 
          for file in $(git diff --name-only origin/main | grep ".md"); do echo "Test $file : " && sed -e '/---/,/---/d' -e '/```/,/```/d' $file | java -jar LanguageTool-5.6/languagetool-commandline.jar -d "WHITESPACE_RULE,LAB,APOS_INCORRECT" -l fr -c utf-8 -; done          
      - name: Notify if failed
        uses: https://github.com/sarisia/actions-status-discord@v1
        if: ${{ failure() }}
        with:
          webhook: ${{ secrets.DISCORD_WEBHOOK }}
          status: "Failure"
          content: "Hey"
          title: "test"
          description: "Test ${{ env.CI_GIT_BRANCH }} Failed"
          color: 0xff0000
  deploy:
    needs: [build]
    steps:
      - name: Download artifacts
        run: |
          GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | cut -d"/" -f1) && \
          GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d"/" -f2) && \
          curl -XGET -H "Authorization: token ${{ secrets.CI_GITEA_PACKAGES_TOKEN }}" $GITHUB_SERVER_URL/api/packages/${GITHUB_REPO_OWNER}/generic/${GITHUB_REPO_NAME}/${GITHUB_SHA}/blog.tar.gz > blog.tar.gz && \
          tar xzf blog.tar.gz && rm -f blog.tar.gz          
      - name: Set branch name
        run: |
                    echo "CI_GIT_BRANCH=$(basename $GITHUB_REF_NAME)" | tee -a $GITHUB_ENV
      - name: Deploy blog
        uses: https://github.com/appleboy/scp-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: "*"
          target: "/opt/www/blog/${{ env.CI_GIT_BRANCH }}/"
          rm: true
      - name: Configure variables for notify
        if: ${{ always() }}
        run: |
          if ${{ success() }}; then
            echo "DISCORD_COLOR=0x00ff00" | tee -a $GITHUB_ENV
            echo "DISCORD_STATUS=Success" | tee -a $GITHUB_ENV
          elif ${{ failure() }}; then
            echo "DISCORD_COLOR=0xff0000" | tee -a $GITHUB_ENV
            echo "DISCORD_STATUS=Failure" | tee -a $GITHUB_ENV
          else
            echo "DISCORD_COLOR=0xeca714" | tee -a $GITHUB_ENV
            echo "DISCORD_STATUS=Unknown" | tee -a $GITHUB_ENV
          fi           
      - name: Notify
        uses: https://github.com/sarisia/actions-status-discord@v1
        if: ${{ always() }}
        with:
          webhook: ${{ secrets.DISCORD_WEBHOOK }}
          status: "${{ env.DISCORD_STATUS }}"
          content: "Hey"
          title: "deploy"
          description: "Deploy ${{ env.CI_GIT_BRANCH }} done"
          color: ${{ env.DISCORD_COLOR }}
          url: "https://${{ env.CI_GIT_BRANCH }}.blog.xataz.net"

delete_env

Comme dit précédemment, je souhaites supprimer mon environnement une fois que c’est en production. J’ai donc créer un workflow spécifique à ceci, qui sera exécuté lors de la fermeture et du merge d’une Pull Request.

name: Delete dev environment
run-name: deleteenv
on:
  pull_request:
    types:
      - closed

jobs:
  if_merged:
    if: github.event.pull_request.merged == true
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Get branch to Delete
      run: |
                echo "CI_BRANCH_TO_DELETE=$(git log --oneline | head -1 | sed -e 's/^.*from \(.*\) into \(.*\)$/\1/')" >> $GITHUB_ENV
    - name: Delete environment 
      uses: https://github.com/appleboy/ssh-action@v0.1.9
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_KEY }}
        port: ${{ secrets.SSH_PORT }}
        script: rm -rf /opt/www/blog/${{ env.CI_BRANCH_TO_DELETE }}

Production

Et pour finir, une fois le merge effectué, cela lance mon workflow pour le déploiement en production :

name: Deploy on Prod
run-name: production
on:
  push:
    branches:
      - main
      - master
jobs:  
  deploy:
    steps:
      - name: Download artifacts
        run: |
          GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | cut -d"/" -f1) && \
          GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d"/" -f2) && \
          curl -XGET -H "Authorization: token ${{ secrets.CI_GITEA_PACKAGES_TOKEN }}" $GITHUB_SERVER_URL/api/packages/${GITHUB_REPO_OWNER}/generic/${GITHUB_REPO_NAME}/${GITHUB_SHA}/blog.tar.gz > blog.tar.gz && \
          tar xzf blog.tar.gz && rm -f blog.tar.gz          
      - name: Set branch name
        run: |
                    echo "CI_GIT_BRANCH=$(basename $GITHUB_REF_NAME)" >> $GITHUB_ENV
      - name: Deploy blog
        uses: https://github.com/appleboy/scp-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: "*"
          target: "/opt/www/blog/${{ env.CI_GIT_BRANCH }}/"
          rm: true
      - name: Configure variables for notify
        if: ${{ always() }}
        run: |
          if ${{ success() }}; then
            echo "DISCORD_COLOR=0x00ff00" | tee -a $GITHUB_ENV
            echo "DISCORD_STATUS=Success" | tee -a $GITHUB_ENV
          elif ${{ failure() }}; then
            echo "DISCORD_COLOR=0xff0000" | tee -a $GITHUB_ENV
            echo "DISCORD_STATUS=Failure" | tee -a $GITHUB_ENV
          else
            echo "DISCORD_COLOR=0xeca714" | tee -a $GITHUB_ENV
            echo "DISCORD_STATUS=Unknown" | tee -a $GITHUB_ENV
          fi           
      - name: Notify
        uses: https://github.com/sarisia/actions-status-discord@v1
        if: ${{ always() }}
        with:
          webhook: ${{ secrets.DISCORD_WEBHOOK }}
          status: "${{ env.DISCORD_STATUS }}"
          content: "Hey"
          title: "deploy"
          description: "Deploy production done"
          color: ${{ env.DISCORD_COLOR }}
          url: "https://blog.xataz.net"

Quelques limites

Gitea Actions est à ces débuts, donc tout ne fonctionne pas comme sur github, notamment avec les workflow_run, qui malgré mes efforts, n’a pas voulu fonctionner (un simple copier/coller vers github, et ça fonctionnait). Il y a surement d’autres types d’évènements qui ne fonctionnement pas, certainement pour une future version de gitea. J’ai également eu des soucis avec les variables needs.<jobname>.result, pour faire des dépendances poussés entre les jobs.

Conclusion

Petite découverte très sympa, et j’avoue que j’aime bien. Malgré que j’adore drone, je trouve que c’est une très bonne alternative. Avec cette nouvelle fonctionnalité, Gitea commence à jouer dans la cours des grands, et propose une bonne alternative à Gitlab ou Github.
Encore une fois, vous lisez cette article qui a été déployé par cette pipeline.