Zaratan@next

Anatomie d'une gem Ruby

Cet article a pour but de couvrir tout le processus d'édition d'une gem. Il sera découpé en deux grandes parties. Tout d'abord ce qu'il faut savoir à minima pour créer sa propre gem (honnêtement, pas grand-chose) pour que vous puissiez créer la vôtre sans souci. Je vais ensuite donner mes recommandations (subjectives) quant aux outils et bonnes pratiques concernant l'écriture et le maintien de cette gem.

Savoir minimal

Tout d'abord on va voir ce qu'il faut savoir à minima. Ce "chapitre" va essayer de couvrir tout ce qui rend différent une gem d'un script ruby qu'on écrit dans un coin.

Création d'une nouvelle gem

Tout d'abord, comment on "crée" une gem ? La bonne question est, qu'est-ce qu'est le "minimal" pour avoir une gem ?

  • Un dossier duquel on va pouvoir require des fichiers ruby
  • Un fichier en .gemspec Donc par rapport à un script normal, tout ce qui change c'est le fichier .gemspec. On va détailler ses différentes sections plus bas dans cet article.

Cependant on peut utiliser un utilitaire qui va nous créer un squelette un peu moins "vide". Bundler que vous avez tous déjà utilisé permet de créer un bon premier squelette.

La commande que j'utilise la plupart du temps est :

bundle gem nom_de_ma_gem --coc --mit --test=rspec
  • --coc va créer un fichier de code of conduct.
  • --mit va ajouter le fichier de license de [MIT].
  • --test=rspec va instancier les quelques fichiers par défaut de rspec.

Dans la section suivante on va détailler les différentes parties de cette arborescence.

Arborescence par défaut

Voilà l'arborescence crée par bundler :

Arborescence des différents fichiers d'une nouvelle gem.
  • CODE_OF_CONDUCT.md contient un coc minimaliste.
  • LICENSE.txt va contenir la license MIT pour votre gem.
  • Rakefile va contenir les taches de déploiement d'une gem.
  • lib/ma_belle_gem.rb va contenir le fichier vide qui contiendra plus tard le code d'initialisation de votre gem. Ce fichier sera automatiquement require quand quelqu'un ajoutera votre gem à son Gemfile.
  • lib/ma_belle_gem/version.rb va définir la constante MaBelleGem::VERSION qui contiendra le numéro de version courant de votre gem.
  • Le dossier spec va contenir les quelques fichiers classiques de tests en rspec.
  • Le dossier bin va contenir les scripts utiles au développement.
    • setup va servir à setup l'environment de développement (en général juste bundle install).
    • console va lancer une console ruby avec votre gem préchargée.
Le fichier .gemspec

Le fichier .gemspec est particulier et possède de nombreuses sections plus-ou-moins optionnelles. On va décortiquer celui qui est généré par bundler, mais vous pouvez aller voir ici pour la documentation complète.

mabellegem.gemspec
# Charge votre fichier qui défini la version de votre gem dans la constante MaBelleGem::VERSION.
require_relative 'lib/ma_belle_gem/version'

Gem::Specification.new do |spec|
  # Le nom de votre gem (Obligatoire). Ce nom sera utilisé pour:
  # * Le nom de votre gem dans rubygems;
  # * Le nom du fihcier qui sera chargé par défaut (lib/ma_belle_gem.rb).
  spec.name          = "ma_belle_gem"
  # Votre numéro de version. Sa valeur sera utilisée dans rubygems. (Obligatoire)
  spec.version       = MaBelleGem::VERSION
  # La liste des auteurs de la gem (Obligatoire d'avoir au moins un auteur)
  spec.authors       = ["Denis <Zaratan> Pasin"]
  # La liste des emails des auteurs de la gem.
  # Attention cette information est publique sur rubygems. (Obligatoire d'avoir au moins un email)
  spec.email         = ["zaratan@hey.com"]

  # summary doit contenir un rapide résumé de ce que fait votre gem.
  # Je ne suis pas sur de où cette information est utilisée. (Obligatoire)
  spec.summary       = %q{TODO: Write a short summary, because RubyGems requires one.}
  # description doit contenir une "longue" description de ce à quoi sert votre gem.
  # Cette information sera affichée tel quelle sur rubygems. (Optionel (mais pas vraiment en pratique))
  spec.description   = %q{TODO: Write a longer description or delete this line.}
  # En général la homepage sera votre repo github a moins que vous ne créiez un site dédié à votre gem.
  spec.homepage      = "TODO: Put your gem's website or public repo URL here."
  # Le shortname de la license
  spec.license       = "MIT"
  # La version de Ruby minimale.
  # Je vous conseille de set cette version à la dernière version de Ruby qui n'est pas en EOL (End Of Life).
  # Voir: https://www.ruby-lang.org/en/downloads/
  spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")

  # Section optionnelle. Si ce n'est pas scécifié c'est rubygems.org (en général je supprime cette section).
  spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"

  spec.metadata["homepage_uri"] = spec.homepage
  # L'url de l'emplacement de votre code
  spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
  # L'url qui permet d'acceder à votre changelog. Cette section est optionnelle. Si vous n'avez pas de changelog supprimez la.
  spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."

  # La section files va permettre de
  # supprimer des fichier qui ne doivent pas être inclus dans la release de votre gem (genre les tests).
  # En général cette section reste inchangée.
  # Specify which files should be added to the gem when it is released.
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
  spec.files         = Dir.chdir(File.expand_path('..', __FILE__)) do
    `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
  end
  # Le dossier dans lequel les executable seront mis
  # (par exemple `bundle` pour la gem de bundler et `rails` pour la gem de rails)
  # Ce dossier peut ne pas exister.
  spec.bindir        = "exe"
  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
  # Quels dossier seront "require" automatiquement au démarrage de votre gem.
  # Pareil en général, pas de changement ici.
  spec.require_paths = ["lib"]
end

Les deux types de commandes qui seront très souvent présentes dans un gemspec sont :

  • spec.add_development_dependency

Va ajouter une gem qui ne sera installée que dans le contexte du développement de la gem (rspec par exemple). La syntaxe est la même que dans un Gemfile :

spec.add_development_dependency "rubocop", "> 0.58"
  • spec.add_dependency

Va ajouter une dépendance directe de votre gem. Celle-ci sera installée par tous les projets qui utilisent votre gem.

Pareil, même syntaxe que dans un Gemfile :

spec.add_dependency "activesupport", "> 2.0"

Le déploiement

Une fois que votre gemspec est défini on peut déployer sur rubygems.org et rentre notre gem disponible au monde.

Il faut absolument avoir un compte créé sur rubygems.org.

Ensuite, si vous avez utilisé bundler pour créer votre gem. Il suffit de faire : rake release dans un terminal et Rake s'occupera pour vous de la publication. À savoir :

  • Créer un package avec le code de votre gem.
  • Créer un tag git avec votre numéro de version.
  • Faire un git push avec vos commits et vos tags.
  • Publier votre gem sur rubygems (Il est possible/probable que vous deviez entrer votre username/password ici).

Voilà. Après quelques minutes, n'importe qui pourra utiliser votre gem.

Des outils pratiques

Dans la partie précédente on a pu voir ce qu’il fallait pour avoir une gem minimale. On va maintenant passer en revue quelques outils (gems) que j’inclus dans quasiment toutes les gems que j’utilise. Notez que quasiment toutes ces recommandations s’appliqueraient aussi à un projet Rails. Ces outils ne sont effectivement pas du tout liés au processus de création d’une gem.

Rubocop

Commençons par Rubocop que vous devez tous plus ou moins connaitre. Pour moi, c’est plus ou moins obligatoire de l’utiliser dans un projet (Ou un “équivalent” genre reek).

Utiliser rubocop permet plusieurs choses :

  • C’est une façon de normaliser le style du code à travers un projet. Ça permet d’ouvrir n’importe quel fichier et d’avoir un “style” consistant.
  • Ça règle toute discussion futile autour du style dans une PR. Si le linter ne force pas un style, alors cette façon de coder est valide.
  • Ça permet de s’assurer que certaines règles de performance sont respectées, que certaines syntaxes dangereuses ne sont pas utilisées ou encore que certaine erreurs évitables le sont.

Pour ajouter Rubocop à notre gem on ajoute dans le .gemspec :

spec.add_development_dependency "rubocop"
spec.add_development_dependency "rubocop-performance"

Voici un exemple de configuration que j’utilise dans presque toutes mes gems :

.rubocop.yml
# Utiliser une version (peu) édulcorée des règles de base de rubocop
inherit_from:
  - http://relaxed.ruby.style/rubocop.yml

# Ajoute des règles de performance aux règles de base de Rubocop
require:
  - rubocop-performance

AllCops:
  # Chaque version de rubocop ajoute de nouvelles règles.
  # Ceci les active par défaut.
  NewCops: enable
  DisplayStyleGuide: true
  DisplayCopNames: true
  Exclude:
    - "bin/*"
    - "vendor/**/*"

# Certains fichiers sont de gigantesques block.
# Ne pas les compter.
Metrics/BlockLength:
  Exclude:
    - "spec/**/*.rb"
    - "Guardfile"
    - "vendor/bundle"
    - "*.gemspec"

# Les règles qui vont suivre sont des règles de style
# permettant de split les lignes.
Layout/DotPosition:
  Enabled: true
  EnforcedStyle: trailing

Style/TrailingCommaInArrayLiteral:
  Enabled: true
  EnforcedStyleForMultiline: comma

Style/TrailingCommaInHashLiteral:
  Enabled: true
  EnforcedStyleForMultiline: comma

Layout/MultilineArrayLineBreaks:
  Enabled: true

Layout/MultilineHashKeyLineBreaks:
  Enabled: true

Layout/MultilineMethodArgumentLineBreaks:
  Enabled: true

Layout/FirstArrayElementLineBreak:
  Enabled: true

Layout/FirstHashElementLineBreak:
  Enabled: true

Layout/FirstMethodArgumentLineBreak:
  Enabled: true

Layout/MultilineAssignmentLayout:
  Enabled: true

# Ajoute une limite maximum à la longueur d'une ligne
Layout/LineLength:
  Enabled: true
  Max: 120
  # Cette option fait en sorte que Rubocop essaye d'ajouter
  # des retours à la ligne là où il faut.
  AutoCorrect: true
  Exclude:
    - Gemfile
    - Guardfile
Bonus : Autocorrect rapide

Il arrive souvent qu’on veuille lancer Rubocop sur l’ensemble du projet et corriger chaque fichier qui a un problème. Rubocop à deux options mutuellement exclusives :

  • -P permet de vérifier les fichiers en parallèle et donc de profiter des nombreux cœurs de nos CPU modernes
  • -A permet de corriger les fichiers automatiquement.

Comme on ne peut pas utiliser les deux options en même temps on va définir un alias qui utilise automatiquement le résultat de du mode rapide (-P) comme paramètre du mode (-A) et ne corriger (lentement) que les fichiers qui ont des problèmes.

alias fastcop="rubocop -P -f fi | xargs rubocop -A"

Solargraph

Solargraph est une gem qui permet d’ajouter un “Language Server” à votre projet Ruby. Qu’est-ce qu’un language server me direz-vous ? C’est un utilitaire qui va tourner en tache de fond et permettre a votre IDE de lui poser des questions telles que : “Quelle est la documentation de cette méthode ?” ou “Quelles sont les méthodes disponibles sur cet classe/objet ?” ou encore “Quels sont les arguments de cette méthode ?”. Ça permet ensuite à notre IDE de fournir une bien meilleure autocomplétion.

On l’ajoute à notre projet de la manière suivante :

spec.add_development_dependency "solargraph"

On peut ensuite demander à solargraph de télécharger les documentations disponibles.

# Télécharge la documentation de tel ruby
solargraph download-core 2.7.2

# Compile la documentation de toutes les gems
# utilisées dans le projet
solargraph bundle

Chaque IDE a sa propre façon de se connecter à solargraph. Le comment lier son IDE a solargraph sera laissé en exercice au lecteur.

Bonus VS Code configuration

Je vais quand même fournir quelques options de configuration que j’utilise pour solargraph à l’intérieur de VS Code.

"solargraph.diagnostics": true,
"solargraph.formatting": true,
"solargraph.completion": true,
"solargraph.folding": true,
"solargraph.hover": true,

Je laisse à solargraph le travail d’appeler Rubocop avec formatting et diagnostics. Ou de savoir comment “replier” le code avec folding. L’autocomplétion s’active avec completion. Et hover permet d’afficher les informations des méthodes en cas de hover.

Bundle audit et Bundle outdated

Des fois on veut pouvoir vérifier directement en console l’état de nos dépendances. Pour ce faire, on va utiliser deux outils.

bundle outdated

Cette commande vient de base avec bundler et lui permet de lister toutes les gems de notre Gemfile.lock qui ne sont pas à jour. De plus ça nous indique pour chaque gem quelle est la version à jour vers laquelle update. Il ne reste alors plus qu’à faire bundle update

bundle audit

Bundle audit est une extension de bundler qui permet de vérifier si une de nos gems à une faille de sécurité connue (CVE). On utilise la gem de cette manière :

On l’ajoute au .gemspec:

spec.add_development_dependency "bundle-audit"

Puis :

# Met à jour ta liste des CVEs
bundle audit update
# Vérifie si une de mes gems à une CVE connue
bundle audit

Overcommit

On a déjà plein d’outils de qualité de code qu’on aimerait lancer automatiquement pour éviter de les oublier. Pour cela on va utiliser la gem overcommit. Elle permet de facilement définir une liste de taches à lancer avant chaque commit ou push git.

Comme d’habitude on ajoute la gem à notre .gemspec

spec.add_development_dependency "overcommit"

On va ensuite “installer” les git hooks :

bundle exec overcommit -i

Enfin, on va configurer overcommit pour lui indiquer quelles actions lancer :

.overcommit.yml
# Avant chaque commit
PreCommit:
  # Lance rubocop sur les fichiers ruby qui ont changé.
  # Si il y a un problème, empêche le commit.
  RuboCop:
    enabled: true
    # Utilise le mode rapide de rubocop
    command: ["rubocop", "-P"]
    quiet: false
  # Vérifie si il y a des gems outdated et affiche un warning sinon
  BundleOutdated:
    enabled: true
  # Vérifie si il y a des CVEs et bloque le commit si oui.
  BundleAudit:
    enabled: true

# Avant chaque push
PrePush:
  # Lance rspec et empêche le push si il y a un problème.
  RSpec:
    enabled: true
    command: ["rspec", "-f", "p"]
    quiet: false

C'est tout.

On ne lance pas RSpec à chaque commit car ceux-ci peuvent être très longs et on va vouloir encourager de petits commits. Si chaque commit prend 20 minutes, on ne va jamais commit.

Note : À chaque fois que le fichier .overcommit.yml change pour des raisons de sécurité on va devoir faire bundle exec overcommit --sign

Bonus : lancer rubocop (rapide) sur l’ensemble du projet avant le push

À l’heure où cet article est écrit, il n’existe pas de façon pré-écrite de lancer Rubocop sur tous les fichiers avant chaque push. Voilà une recette pour remédier au problème :

On ajoute le fichier :

.git-hooks/pre_push/rubocop.rb
# frozen_string_literal: true

module Overcommit
  module Hook
    module PrePush
      # Runs `rubocop` on every files.
      class Rubocop < Base
        def run
          result = execute(['rubocop', '-P'])
          return :pass if result.success?

          output = result.stdout + result.stderr
          [:fail, output]
        end
      end
    end
  end
end

Ensuite on modifie notre .overcommit.yml

# Avant chaque push
PrePush:
  # […]
  Rubocop:
    enabled: true

Voilà !

Yard

Il est souvent bon d’écrire une documentation des différentes méthodes à même notre code et ce pour plusieurs raisons :

  • Ça permet aux autres personnes qui voudraient utiliser notre gem de savoir ce à quoi servent les différentes classes et méthodes
  • Ça permet à solargraph de faire de l’autocomplétion

Pour ce faire on va utiliser une gem classique : Yard.

Comme d’habitude on l’ajoute au .gemspec

spec.add_development_dependency "yard"

Ensuite on va documenter chaque méthode de la manière suivante :

# Use a context and inject its content at this place in the code
#
# @param [String, Symbol] context_name The context namespace
# @param [*Any] args Any arg to be passed down to the injected context
# @param [String, Symbol] namespace namespace name where to look for the context
# @param [String, Symbol] ns Alias for :namespace
# @param block Content that will be re-injected (see #execute_tests)
def in_context(context_name, *args, namespace: nil, ns: nil, &block)

Je vous laisse vous référer à la documentation de Yard pour plus d’informations.

Zeitwerk

Le dernier outil que j’utilise par défaut dans quasiment toutes les gems est Zeitwerk. Dès que notre gem contient un peu plus de 3 fichiers, faire les require à la main de chacun de ceux-ci en haut du premier fichier de notre gem peut vite devenir un cauchemar (car certains fichiers vont dépendre sur d’autres et vont donc devoir être require avant, etc.).

Zeitwerk permet de simplifier ce processus en faisant un autoload à la Rails sur nos fichiers. Il suffira ensuite que chaque fichier définisse la constante correspondant au path et nom du fichier pour que Zeitwerk s’occupe de faire des require automatique.

Sa configuration est assez simple.

On l’ajoute au .gemspec :

spec.add_dependency 'zeitwerk', '~> 2'

Ensuite, suivant la documentation, dans notre fichier principal presque tout en haut on fait :

lib/active_shotgun.rb
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup # ready!

C'est tout. Les différents fichiers seront require correctement.

Des bonnes pratiques

Je ne vais pas m’étendre ici sur le pourquoi écrire des tests est crucial pour une gem qui va ensuite être utilisé par plein de gens. Écrivez. Des. Tests.

On va voir quelques points qu’il est, je pense, important de traiter.

Écrire un changelog

Si vous comptez sur le fait que votre gem soit utilisée par d’autres personnes, il est important de leur fournir un moyen de savoir ce qu’y change entre chaque version de votre gem. Pour cela, le plus simple est de maintenir un fichier CHANGELOG.md à la racine de votre gem.

De plus, n’oubliez pas d’ajouter l’url menant à celui-ci dans votre .gemspec.

spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."

Pour ce qui est du format de changelog, je conseille de suivre le très bon (et simple) format de keepachangelog. Honnêtement n’importe quel format fera l’affaire.

Écrire de la documentation

On a déjà parlé de documentation à l’intérieur du code avec Yard. Pensez aussi à compléter votre Readme avec des cas d’usage classiques (voir toute la documentation si la gem est simple) avec des liens vers d’autres fichiers en .md ou des pages web si nécéssaire.

Rappelez-vous : Si y a pas de doc ou si elle est nulle, il faut VRAIMENT que les gens n’aient pas le choix pour utiliser votre outil.

Automatiser son déploiement

Les gens qui me connaissent un peu savent que j’oublie toujours tout et que j’automatise au maximum mes différentes taches pour éviter toute erreur humaine.

On a vu précédemment que le déploiement se faisait par une simple commande rake. Cependant, il ne faut pas oublier de la lancer, ni d’augmenter le numéro de version avant de le faire.

On va donc voir ici comment le faire via des actions Github.

On va avoir 3 actions que je vais décortiquer plus bas. Je pars du principe que toute PR qui est merge dans master doit créer une nouvelle version (éventuellement mineure). J’ai trop vu des gems qui avaient des correctifs dans master/main pendant des mois sans qu’un déploiement se fasse sur rubygems. Les utilisateurs se retrouvent alors à référencer le projet Github directement dans leur Gemfile ce qui en fait quelque chose de pas très stable.

La branche principale s’appelle main. On ajoutera une protection dessus pour empêcher les commit directs.

Partant de ce constat, les 3 actions seront :

  • Sur chaque commit lié a une branche qui n’est pas main, on lance les tests dans toutes les versions de ruby que supporte la gem. Ça permet de s’assurer que si jamais on change quelque chose, ça ne va pas casser dans une version de ruby en particulier. En tant qu’utilisateur je trouve ça rassurant.
  • Sur chaque PR à destination de main on vérifie que le numéro de version a bien changé et que ce numéro de version est bien supérieur au précédent.
  • Sur chaque commit dans main, on commence par lancer les tests dans toutes les versions de ruby. Si ces tests passent et que la version a été changée (ce qui devrait toujours être le cas), on va déployer notre nouvelle version de gem.
Vérifier la version sur chaque PR vers main

Pour ajouter des actions Github, rien de plus simple. On crée dans le dossier .github/workflows/ des fichiers correspondant à nos actions.

.github/workflows/verify_version_change.yml
# Le nom qui sera affiché lors de l'exécution de notre action
name: Verify version change

# A quel moment l'action doit être lancée
on:
  pull_request:
    branches:
      - main

# Quoi faire
jobs:
  version_change:
    runs-on: ubuntu-latest

    # Les étapes de l'action
    steps:
      # on récupère le code
      - uses: actions/checkout@v2
      # On fetch la branche principale
      # (pour pouvoir faire des comparaison avec la branche courante).
      - name: Fetch main branch
        run: git fetch origin main:main
      # On fait un diff avec main et on vérifie
      # qu'il y a bien un changement dans le fichier de version
      - name: Verify if there's a change in version
        run: "git diff main -- lib/rspec_in_context/version.rb | grep VERSION"
      # Pour éventuellement débug
      - name: Print new version
        run: 'git diff main -- lib/rspec_in_context/version.rb | grep -E "^\+.*VERSION" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?"'
      # Magie noire en bash pour vérifier que
      # le numéro de version est bien plus grand.
      - name: Verify if higher version
        run: '[[ $(git diff main -- lib/rspec_in_context/version.rb | grep -E "^\+.*VERSION" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?") > $(git diff main -- lib/rspec_in_context/version.rb | grep -E "^-.*VERSION" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?") ]]'
Lancer les tests a chaque commit sauf sur main

Ici pour lancer les tests sur toutes les versions de ruby on va utiliser une matrice :

.github/workflows/test_only.yml
name: Tests

on:
  push:
    branches-ignore:
      - main

jobs:
  tests:
    # On défini ici la matrice. Comme on a qu'une seule variable,
    # les "combinaisons" sont faciles. C'est un simple tableau de 4 éléments
    strategy:
      matrix:
        ruby: [2.5, 2.6, 2.7, 3.0]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      # Cette action va setup le bon ruby et installer les gems.
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          # On va chercher la version de ruby dans la matrice
          ruby-version: ${{ matrix.ruby }}
          # On cache l'instalation des gems
          bundler-cache: true
      # On lance rubocop
      - name: Run linter
        run: bundle exec rubocop
      # On lance les tests
      - name: Run tests
        run: bundle exec rspec
Lancer les tests et déployer au besoin sur les push dans la branche main

Cette fois-ci on va utiliser encore un nouvel outil des actions Github. On va utiliser la notion de dépendances entre jobs. De plus on va utiliser les secrets pour stocker notre clé API de rubygems.

.github/workflows/verify_version_change.yml
ame: Test and Release

on:
  push:
    branches:
      - main

jobs:
  # Ce job est identique à celui de test_only
  tests:
    strategy:
      matrix:
        ruby: [2.5, 2.6, 2.7, 3.0]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
          bundler-cache: true
      - name: Run linter
        run: bundle exec rubocop
      - name: Run tests
        run: bundle exec rspec
  # On a un deuxième job de release
  release:
    # On indique qu'il a besoin que le job de tests soit fini et positif
    needs: [tests]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.0
          bundler-cache: true
      # On prépare le fichier ~/.gem/credentials
      # qui va permettre de s'authentifier sur rubygems
      - name: Prepare credentials
        env:
          # On lit dans les secrets du repo github
          # pour le mettre en variable d'environment
          RUBYGEM_KEY: ${{ secrets.RUBYGEM_KEY }}
        run: "mkdir -p ~/.gem && echo -e \"---\\r\\n:rubygems_api_key: $RUBYGEM_KEY\" > ~/.gem/credentials && chmod 0600 ~/.gem/credentials"
      # Comme on va créer un tag git on a besoin d'avoir choisi un email/username
      - name: Setup username/email
        run: 'git config --global user.email zaratan@hey.com && git config --global user.name "Denis <Zaratan> Pasin"'
      # On récupère tous les tags existants
      - name: Fetch tags from remote
        run: "git fetch -t"
      # On verifie si le fichier de version.rb à changé depuis le dernier tag
      # Si oui, on lance rake release
      - name: Publish if version change
        run: 'git diff `git tag | tail -1` -- lib/rspec_in_context/version.rb | grep -E "^\+.*VERSION" && rake release || echo "No release for now"'

Conclusion

Voilà, avec tout ça vous devriez être capable de créer vous-même vos propres gems. Dans la seconde partie j’y ai mis plein de suggestions et de bonnes pratiques mais honnêtement, aucune n’est obligatoire. Si ça vous semble trop intimidant, commencez petit et ajoutez les choses petit à petit. N’oubliez pas que ce que vous lisez ici correspond à un grand nombre d’itérations de ma part ; au début je n’utilisais rien de tout ça.

Pour me poser plus de questions ou réagir à cet article vous pouvez aller directement vers la PR correspondante : #10. Et si l'article vous a plus vous pouvez toujours m'offrir un thé ici : Ko-fi.

Pour aller plus loin :

  • Ma gem de rspec_in_context qui utilise toutes ces techniques (sauf Zeitwerk, car il n’y a que 2-3 fichiers).