<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Zenika</title>
    <description>The latest articles on Forem by Zenika (@zenika).</description>
    <link>https://forem.com/zenika</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1234%2F61529416-7337-4c59-ab27-aaa666d850fc.png</url>
      <title>Forem: Zenika</title>
      <link>https://forem.com/zenika</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zenika"/>
    <language>en</language>
    <item>
      <title>Agent Development Kit 2.0, ADK-java 1,1 et Go 1.0 🚀</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Wed, 15 Apr 2026 07:30:32 +0000</pubDate>
      <link>https://forem.com/zenika/agent-development-kit-20-adk-java-11-et-go-10-2be5</link>
      <guid>https://forem.com/zenika/agent-development-kit-20-adk-java-11-et-go-10-2be5</guid>
      <description>&lt;p&gt;La grosse nouvelle de la fin mars dans le monde du développement d’agent IA vient de Google avec les releases de la version 2.0 Alpha d’ADK Python et la 1.0 des versions java et Go !&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ ADK
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://adk.dev?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Agent Development Kit&lt;/a&gt; est un framework créé par Google permettant de facilement créer et déployer des agents IA. L’interface fournie permet de les débugger et de suivre l’orchestration des applications multi agents.&lt;/p&gt;

&lt;p&gt;ADK a été conçu au départ pour les applications Python, c’est pour cela que la version 2.0 est uniquement présente pour ce langage. &lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://adk.dev?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;https://adk.dev/&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  🙌 Les apports de la v2
&lt;/h2&gt;

&lt;p&gt;La version 2.0 arrive pour le moment en mode “Alpha”,  il est donc (fortement) recommandé de ne pas utiliser cette version en production. Cette version est compatible avec les agents créés avec ADK 1.0 mais attention, il est impératif de ne pas partager les stockages entre vos projets ADK 1.0 et 2.0.&lt;/p&gt;

&lt;p&gt;Cette release apporte &lt;strong&gt;3 principales fonctionnalités&lt;/strong&gt; : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;les Graph-based workflows&lt;/li&gt;
&lt;li&gt;la coordination de sous agents&lt;/li&gt;
&lt;li&gt;les workflow dynamiques&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Les &lt;strong&gt;Graph-based workflows&lt;/strong&gt; permettent de chainer des actions ou requêtes Le &lt;code&gt;root_agent&lt;/code&gt; est un agent qui exécute le workflow qui est défini par un nom, et un tableau de noeuds avec la possibilité de diriger des traitement en fonction d’un résultat précédent.&lt;/p&gt;

&lt;p&gt;Cela permet par exemple de chaîner un ensemble de traitements ou bien de définir un ensemble d’actions exécutées en parallèle. Et quand je dis traitement, ce sont des appels à des LLM ou bien du code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-2.png" alt="ADK schema" width="777" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Image provenant de &lt;a href="https://adk.dev/workflows?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;https://adk.dev/workflows/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La &lt;strong&gt;seconde nouveauté&lt;/strong&gt; intervient au niveau des agents et de leur mode d'interaction. Des sous-agents peuvent désormais être créés et orchestrés par un agent “&lt;strong&gt;coordinateur&lt;/strong&gt;”. Ces sous agents disposent de trois modes de fonctionnement : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chat, le comportement actuel où le retour est fait à l’agent parent de manière manuelle&lt;/li&gt;
&lt;li&gt;Task, permet une interaction avec l’utilisateur pour avoir des compléments d’informations&lt;/li&gt;
&lt;li&gt;Simple échange (Single turn), l’agent travaille et renvoie son résultat. Peut-être exécuté en parallèle. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Les &lt;strong&gt;workflow dynamiques&lt;/strong&gt; d’ADK sont la dernière nouveauté de cette release et permettent de s’affranchir de la structure rigide du &lt;strong&gt;graph-based&lt;/strong&gt; vu précédemment. Avec l’annotation &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/node"&gt;@node&lt;/a&gt; **ou le wrapper **FunctionNode&lt;/strong&gt;, les workflow dynamiques permettent de supporter les exécutions parallèles, les boucles itératives et les compléments d’information de la part des utilisateurs (Human-in-the-loop) à l’aide de lignes de code.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://adk.dev/2.0?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;https://adk.dev/2.0/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎉 Versions 1.0 des SDK Java et Go
&lt;/h2&gt;

&lt;p&gt;Dans le même temps que la nouvelle version majeure d’ADK Python, les SDK Java et Go voient arriver leur première version majeure, le signe d’une maturité et stabilité validée. &lt;/p&gt;

&lt;p&gt;La version 1.0 du &lt;strong&gt;SDK Go&lt;/strong&gt; inclut l’arrivée native d’OpenTelemetry via le &lt;code&gt;TraceProvider&lt;/code&gt;. De plus, un nouveau système de plugin va permettre d’inclure des fonctionnalités transverses (logs, sécurité, etc.). Un plugin intéressant, “&lt;strong&gt;Retry and Reflect&lt;/strong&gt;”, permet d’intercepter les erreurs et de les renvoyer au modèle pour les corriger et les prendre en compte. &lt;/p&gt;

&lt;p&gt;Cette version supporte aussi la définition d’agent directement via des fichiers YAML. \&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://developers.googleblog.com/adk-go-10-arrives?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;https://developers.googleblog.com/adk-go-10-arrives/&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Du côté de &lt;strong&gt;Java&lt;/strong&gt;, les releases 1.0 et 1.1 créées quelques jours plus tard incluent l’arrivée de nouveaux outils comme : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GoogleMapsTool&lt;/code&gt; pour récupérer des informations sur Google Maps.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UrlContextTool&lt;/code&gt; pour de récupérer les urls fournies. Cela vous permet par exemple de faire un résumé d’une page web.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ContainerCodeExecutor&lt;/code&gt; et &lt;code&gt;VertexAiCodeExecutor&lt;/code&gt; pour exécuter du code locallement dans des containeurs Docker ou sur Vertex AI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ComputerUseTool&lt;/code&gt; pour piloter un navigateur web.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;App&lt;/code&gt; est la &lt;strong&gt;nouvelle classe de plus haut niveau&lt;/strong&gt; pour créer une application agentique. Elle va pouvoir recevoir une liste de plugins qui sont appliqués à tous les sous-agents. Pratique pour harmoniser les logs par exemple avec &lt;code&gt;LoggingPlugin&lt;/code&gt; ou bien donner des instructions globales à l’application avec le plugin &lt;code&gt;GlobalInstructionPlugin&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Une &lt;strong&gt;stratégie de compression&lt;/strong&gt; est également applicable sur la classe &lt;code&gt;App&lt;/code&gt; et réduit notamment la taille de votre contexte et donc la taille de vos tokens. &lt;/p&gt;

&lt;p&gt;L’outil &lt;code&gt;ToolConfirmation&lt;/code&gt; permet de mettre en pause le traitement le temps d’avoir un complément d’information pour l’utilisateur·trice.&lt;/p&gt;

&lt;p&gt;De nouveaux services permettent de sauvegarder le cycle de vie d’une conversation, en fonction de votre besoin, vos données en mémoire, dans VertexAI ou bien dans une collection Firestore (avec &lt;code&gt;InMemorySessionService&lt;/code&gt;, &lt;code&gt;VertexAiSessionService&lt;/code&gt; et &lt;code&gt;FirestoreSessionService&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Pour conserver vos informations à travers plusieurs sessions, cela peut se faire localement ou bien dans Firestore (avec &lt;code&gt;InMemoryMemoryService&lt;/code&gt;  et &lt;code&gt;FirestoreMemoryService&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://developers.googleblog.com/announcing-adk-for-java-100-building-the-future-of-ai-agents-in-java?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;https://developers.googleblog.com/announcing-adk-for-java-100-building-the-future-of-ai-agents-in-java/&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Démo
&lt;/h2&gt;

&lt;p&gt;Je me suis prêté l’exercice de migrer &lt;a href="https://gitlab.com/jeanphi-baconnais-experiments/demos/adk-demo" rel="noopener noreferrer"&gt;ce projet&lt;/a&gt; me servant de démo avec les nouvelles fonctionnalités :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="no"&gt;APP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"trip-planner-app"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rootAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ROOT_AGENT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggingPlugin&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GlobalInstructionPlugin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Please add related emojis to your responses to make them more engaging."&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eventsCompactionConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EventsCompactionConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compactionInterval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;overlapSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;L'effet "Wow 🤩, une version majeure arrive" est bien là, et je me rends compte que les évolutions apportées sont importantes et vont permettre d'accroître les possibilités de nos agents.&lt;/p&gt;

&lt;p&gt;J’avais mis à jour quelques uns de mes projets utilisant la version d'ADK 1.1 sans avoir d'impact dans le code. En lisant les release notes, il y a bien des ajustements à faire dans la conception des agents pour notamment se poser des questions sur la gestion de la mémoire, la compression des informations et l’utilisation des plugins amène un gros plus pour optimiser les traitements et le code. &lt;/p&gt;

&lt;p&gt;A suivre 🚀&lt;/p&gt;

</description>
      <category>adk</category>
      <category>ai</category>
      <category>google</category>
      <category>java</category>
    </item>
    <item>
      <title>🦊 GitLab CI: Automated Testing of Job Rules</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:52:47 +0000</pubDate>
      <link>https://forem.com/zenika/gitlab-ci-automated-testing-of-job-rules-1i03</link>
      <guid>https://forem.com/zenika/gitlab-ci-automated-testing-of-job-rules-1i03</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;1. The problem: CI rules complexity&lt;/li&gt;
&lt;li&gt;
2. The solution: automated job list validation

&lt;ul&gt;
&lt;li&gt;gitlab-ci-local: the cornerstone&lt;/li&gt;
&lt;li&gt;How it works&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

3. Setting up the test infrastructure

&lt;ul&gt;
&lt;li&gt;Directory structure&lt;/li&gt;
&lt;li&gt;Test case definition&lt;/li&gt;
&lt;li&gt;Reference CSV files&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;4. The validation script&lt;/li&gt;

&lt;li&gt;5. The CI job&lt;/li&gt;

&lt;li&gt;6. Workflow: adding or modifying jobs&lt;/li&gt;

&lt;li&gt;

7. Testing with rules:changes

&lt;ul&gt;
&lt;li&gt;The problem with rules:changes and gitlab-ci-local&lt;/li&gt;
&lt;li&gt;The force-build label workaround&lt;/li&gt;
&lt;li&gt;Per-module test cases&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;8. Documentation that writes itself&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;li&gt;Further reading&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Initial thoughts
&lt;/h1&gt;

&lt;p&gt;As a CICD engineer, you've likely experienced this frustrating scenario: you modify a job's &lt;code&gt;rules:&lt;/code&gt; to optimize pipeline execution, only to discover later that some jobs no longer trigger in specific situations. Or worse, jobs that should be mutually exclusive now run together, wasting resources and causing confusion.&lt;/p&gt;

&lt;p&gt;GitLab CI's &lt;code&gt;rules:&lt;/code&gt; syntax is powerful but complex. With &lt;code&gt;workflow:rules&lt;/code&gt;, job-level &lt;code&gt;rules:&lt;/code&gt;, &lt;code&gt;extends:&lt;/code&gt;, &lt;code&gt;!reference&lt;/code&gt;, and &lt;code&gt;changes:&lt;/code&gt; all interacting, predicting which jobs will run for a given pipeline type becomes increasingly difficult as your CI configuration grows.&lt;/p&gt;

&lt;p&gt;What if we could &lt;strong&gt;automatically test&lt;/strong&gt; that the right jobs appear for each pipeline type? This article presents a practical solution: using &lt;code&gt;gitlab-ci-local&lt;/code&gt; to validate job presence across all your pipeline variants, with reference files that serve as both tests and documentation.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. The problem: CI rules complexity
&lt;/h1&gt;

&lt;p&gt;In a mature GitLab CI setup, you typically have multiple pipeline types — especially in mono-repos with complex &lt;code&gt;workflow:rules&lt;/code&gt; and module-based builds. Here, we focus on &lt;strong&gt;testing&lt;/strong&gt; that your rules produce the expected jobs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Merge Request&lt;/td&gt;
&lt;td&gt;Developer pushes to a feature branch with an open MR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Protected Branch&lt;/td&gt;
&lt;td&gt;Push to &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, or release branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual Pipeline&lt;/td&gt;
&lt;td&gt;Triggered from GitLab UI with variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tag (auto)&lt;/td&gt;
&lt;td&gt;Tag push triggering preprod deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tag (manual)&lt;/td&gt;
&lt;td&gt;Tag pipeline for production deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scheduled&lt;/td&gt;
&lt;td&gt;Nightly builds, cache warmup, full test suites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each pipeline type should have a &lt;strong&gt;specific set of jobs&lt;/strong&gt;. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MR pipelines should run tests related to changed modules only&lt;/li&gt;
&lt;li&gt;Protected branch pipelines should run all tests and prepare deployable artifacts&lt;/li&gt;
&lt;li&gt;Scheduled pipelines might run expensive security scans or full regression tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you have 50+ jobs with complex &lt;code&gt;rules:&lt;/code&gt;, maintaining this becomes a nightmare. Change one rule, break three jobs, sometimes in further pipeline types — the butterfly effect, CI edition. And if you've ever endured the &lt;a href="https://dev.to/zenika/gitlab-ci-yaml-modifications-tackling-the-feedback-loop-problem-4ib1"&gt;slow feedback loop of CI YAML modifications&lt;/a&gt;, you know that discovering these regressions through push-and-pray is not sustainable.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. The solution: automated job list validation
&lt;/h1&gt;

&lt;p&gt;The solution is surprisingly simple and will be detailed step by step in this article:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define test cases&lt;/strong&gt; as variable files simulating each pipeline type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;gitlab-ci-local --list-csv&lt;/code&gt;&lt;/strong&gt; to get the jobs that would run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare with reference CSV files&lt;/strong&gt; committed to the repository&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail the CI if the job list differs&lt;/strong&gt; from the expected reference&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  gitlab-ci-local: the cornerstone
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/firecow/gitlab-ci-local" rel="noopener noreferrer"&gt;gitlab-ci-local&lt;/a&gt; is an open-source tool that parses GitLab CI YAML locally. Among its many features, &lt;code&gt;--list-csv&lt;/code&gt; outputs the jobs that would run given a set of variables, without actually executing anything.&lt;/p&gt;

&lt;p&gt;This is perfect for our use case: we simulate pipeline conditions and check the resulting job list. Think of it as a flight simulator for your CI — all the turbulence, none of the crashes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; gitlab-ci-local

&lt;span class="c"&gt;# List jobs as CSV with custom variables&lt;/span&gt;
gitlab-ci-local &lt;span class="nt"&gt;--list-csv&lt;/span&gt; &lt;span class="nt"&gt;--variables-file&lt;/span&gt; ci/test/D-tag-preprod.variables.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Load the test case&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run gitlab-ci-local&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture the CSV output&lt;/strong&gt; listing all jobs that would run:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   name;stage;when;allowFailure;needs
   📦🌐api-build;📦 Package;on_success;false;[]
   🗄️✅back-unit-tests;✅ Test;on_success;false;[]
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compare with the reference file&lt;/strong&gt; &lt;code&gt;ci/test/D-tag-preprod.csv&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;✅ Match → Test passes&lt;/li&gt;
&lt;li&gt;❌ Diff → Show diff, fail test&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When job name / stage / &lt;code&gt;when&lt;/code&gt; condition / &lt;code&gt;allowFailure&lt;/code&gt; flag / needed jobs change, the CI will let you know, and you will commit the change or fix the CI regression.&lt;/p&gt;
&lt;h1&gt;
  
  
  3. Setting up the test infrastructure
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Directory structure
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;ci/test/&lt;/code&gt; directory to store test definitions and reference files:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ci/
└── test/
    [...]
    ├── D-tag-preprod.variables.yml      # Tag triggering preprod deployment
    ├── D-tag-preprod.csv                 # Expected jobs (reference)
    ├── F-scheduled.variables.yml         # Scheduled nightly pipeline
    └── F-scheduled.csv                   # Expected jobs (reference)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The naming convention &lt;code&gt;{Type}-{Description}.variables.yml&lt;/code&gt; makes it easy to identify test scenarios.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test case definition
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;.variables.yml&lt;/code&gt; file contains the GitLab CI predefined variables that simulate a specific pipeline context:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D-tag-preprod.variables.yml&lt;/strong&gt; - Tag triggering preprod deployment:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CI_COMMIT_TAG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1.2.3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;F-scheduled.variables.yml&lt;/strong&gt; - Scheduled nightly pipeline:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CI_PIPELINE_SOURCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule"&lt;/span&gt;
&lt;span class="na"&gt;CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The key is to set the variables that your &lt;code&gt;workflow:rules&lt;/code&gt; and job &lt;code&gt;rules:&lt;/code&gt; check to determine pipeline behavior.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reference CSV files
&lt;/h2&gt;

&lt;p&gt;The reference CSV files contain the expected job list. They're auto-generated on first run and then committed. Here's an example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;F-scheduled.csv&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name;description;stage;when;allowFailure;needs
🔎🖥️front-lint;"";🔎 Check;on_success;false;[]
✅🖥️front-unit-tests;"";🔎 Check;on_success;false;[]
📦🖥️front-build;"";📦 Package;on_success;false;[]
📦🌐api-build;"";📦 Package;on_success;false;[]
🗄️✅back-unit-tests;"";✅ Test;on_success;false;[]
🗄️🧩✅back-integration-tests;"";✅ Test;on_success;false;[]
🕵️💯security-scan;"";🕵 Quality;on_success;false;[]
🕵ᯤ🌐api-sonar;"";🕵 Quality;on_success;false;[📦🌐api-build]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This file serves as &lt;strong&gt;documentation&lt;/strong&gt; and &lt;strong&gt;test oracle&lt;/strong&gt; simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers can quickly see which jobs run for scheduled pipelines&lt;/li&gt;
&lt;li&gt;CI validates that reality matches expectations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqs4q3paepnxe6ucoqrpi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqs4q3paepnxe6ucoqrpi.jpg" alt="Testing fox"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  4. The validation script
&lt;/h1&gt;

&lt;p&gt;Here's a Bash script that automates the validation. It discovers test cases, runs &lt;code&gt;gitlab-ci-local&lt;/code&gt;, and compares with reference files:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ci/test-ci-jobs-list.sh&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Tests CI non-regression by comparing gitlab-ci-local --list-csv&lt;/span&gt;
&lt;span class="c"&gt;# output with committed reference files.&lt;/span&gt;
&lt;span class="c"&gt;# Creates/updates reference files when differences are detected.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./ci/test-ci-jobs-list.sh [test-case-name]&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;TOTAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="c"&gt;# Optional: run a single test case&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci/test/&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;.variables.yml"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci/test/*.variables.yml"&lt;/span&gt;
&lt;span class="k"&gt;fi

for &lt;/span&gt;varsFile &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$FILES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$varsFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue

    &lt;/span&gt;&lt;span class="nv"&gt;baseName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$varsFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .variables.yml&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;referenceFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci/test/&lt;/span&gt;&lt;span class="nv"&gt;$baseName&lt;/span&gt;&lt;span class="s2"&gt;.csv"&lt;/span&gt;

    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🔍 Testing &lt;/span&gt;&lt;span class="nv"&gt;$baseName&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
    &lt;span class="o"&gt;((&lt;/span&gt;TOTAL++&lt;span class="o"&gt;))&lt;/span&gt;

    &lt;span class="c"&gt;# Generate current job list (filter out logs, keep only CSV)&lt;/span&gt;
    gitlab-ci-local &lt;span class="nt"&gt;--list-csv&lt;/span&gt; &lt;span class="nt"&gt;--variables-file&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$varsFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^[^[]'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^$'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

    &lt;span class="c"&gt;# Ensure header is present&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^name;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;gitlab-ci-local &lt;span class="nt"&gt;--list-csv&lt;/span&gt; &lt;span class="nt"&gt;--variables-file&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$varsFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^name;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.header"&lt;/span&gt;
        &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.header"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.new"&lt;/span&gt;
        &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.new"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt;
        &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.header"&lt;/span&gt;
    &lt;span class="k"&gt;fi

    &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="c"&gt;# Check for git differences&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git diff &lt;span class="nt"&gt;--exit-code&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$referenceFile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ FAILED: &lt;/span&gt;&lt;span class="nv"&gt;$baseName&lt;/span&gt;&lt;span class="s2"&gt; - jobs list has changed"&lt;/span&gt;
        &lt;span class="o"&gt;((&lt;/span&gt;FAILED++&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;&lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ PASSED: &lt;/span&gt;&lt;span class="nv"&gt;$baseName&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=========================================="&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Results: &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;TOTAL &lt;span class="o"&gt;-&lt;/span&gt; FAILED&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$TOTAL&lt;/span&gt;&lt;span class="s2"&gt; passed"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=========================================="&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$FAILED&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Review the diffs above and commit updated reference files."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note&lt;/strong&gt;: A PowerShell version is available on request for Windows-based runners.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;
  
  
  5. The CI job
&lt;/h1&gt;

&lt;p&gt;Add a job resembling this to your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;✅🦊validate-ci-jobs-list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;✅ Test&lt;/span&gt;
  &lt;span class="na"&gt;resource_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avoid-gcl-concurrency&lt;/span&gt;  &lt;span class="c1"&gt;# gitlab-ci-local may not be thread-safe&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;your-runner-tag&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20-alpine&lt;/span&gt;
  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install -g gitlab-ci-local&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./ci/test-ci-jobs-list.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resource_group&lt;/code&gt;&lt;/strong&gt;: Prevent concurrent executions that might conflict&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rules:&lt;/code&gt;&lt;/strong&gt;: you may want to run this on merge request when CI has changed, and on long-lived branches&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  6. Workflow: adding or modifying jobs
&lt;/h1&gt;

&lt;p&gt;When you modify CI rules, follow this workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make your changes&lt;/strong&gt; to &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; or included files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run locally&lt;/strong&gt; (optional but recommended):
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ./ci/test-ci-jobs-list.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Review the diffs&lt;/strong&gt; - the script shows exactly what changed:&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code&gt;  🗄️✅back-unit-tests;"";✅ Test;on_success;false;[]
  🗄️🧩✅back-integration-tests;"";✅ Test;on_success;false;[]
&lt;span&gt;− 🕵ᯤsonar;"";✅ Test;manual;true;[]&lt;/span&gt;
&lt;span&gt;+ 🕵ᯤsonar;"";✅ Test;on_success;false;[]&lt;/span&gt;
  🕵ᯤ🌐api-sonar;"";🕵 Quality;on_success;false;[📦🌐api-build]&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If intentional&lt;/strong&gt;, commit the updated CSV files along with your CI changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If unintentional&lt;/strong&gt;, fix your rules before committing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This creates a &lt;strong&gt;self-documenting system&lt;/strong&gt;: the git history of CSV files shows exactly when and why job behavior changed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xwsidbcjp62z86i5w5m.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xwsidbcjp62z86i5w5m.jpg" alt="Testing fox"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  7. Testing with rules:changes
&lt;/h1&gt;

&lt;p&gt;The examples above cover pipeline types where all jobs of a category run (protected branches, tags, scheduled). But on a real project with module-based &lt;code&gt;rules:&lt;/code&gt; using &lt;code&gt;changes:&lt;/code&gt;, MR pipelines only trigger jobs for modules whose files were modified. This is where things get spicy — and where regressions love to hide.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem with rules:changes and gitlab-ci-local
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gitlab-ci-local&lt;/code&gt; does evaluate &lt;code&gt;rules:changes&lt;/code&gt; — it has access to the git diff. But since our test job runs &lt;strong&gt;inside the MR pipeline&lt;/strong&gt;, the diff contains whatever files happen to be modified in the current MR. A test for "backend-only pipeline" would suddenly include frontend jobs if someone touched a frontend file in the same branch. The results depend on the MR content, not on CI configuration — the opposite of a reliable test.&lt;/p&gt;

&lt;p&gt;The solution is to &lt;strong&gt;guard &lt;code&gt;changes:&lt;/code&gt; rules with &lt;code&gt;$GITLAB_CI == "true"&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;.api-mr-rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_MERGE_REQUEST_ID &amp;amp;&amp;amp; $GITLAB_CI == "true"&lt;/span&gt;
      &lt;span class="na"&gt;changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/Api/**/*&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/Commons/**/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In real GitLab CI, &lt;code&gt;$GITLAB_CI&lt;/code&gt; is always &lt;code&gt;"true"&lt;/code&gt;, so the rule works normally — &lt;code&gt;changes:&lt;/code&gt; is evaluated against the MR diff. But &lt;code&gt;gitlab-ci-local&lt;/code&gt; sets &lt;code&gt;$GITLAB_CI&lt;/code&gt; to &lt;code&gt;"false"&lt;/code&gt;, which makes the &lt;code&gt;if:&lt;/code&gt; condition fail &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;changes:&lt;/code&gt; is even evaluated. The entire rule is cleanly skipped, no ambiguity. Schrödinger's job: it exists in your YAML but never appears in the output — by design.&lt;/p&gt;

&lt;p&gt;This is what makes test results &lt;strong&gt;deterministic&lt;/strong&gt;: regardless of which files are modified in the current MR, the &lt;code&gt;changes:&lt;/code&gt; rules are consistently neutralized, and only the label-based fallback (below) controls which jobs appear.&lt;/p&gt;
&lt;h2&gt;
  
  
  The force-build label workaround
&lt;/h2&gt;

&lt;p&gt;The trick is to add a &lt;strong&gt;label-based bypass&lt;/strong&gt; to every module's rules. In your actual CI, this serves double duty: developers use it when they need to force-rebuild a module (dependencies changed outside the &lt;code&gt;changes:&lt;/code&gt; scope, cache issues, cosmic rays), and tests use it to simulate "this module has changes":&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;.api-mr-rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_MERGE_REQUEST_ID &amp;amp;&amp;amp; $GITLAB_CI == "true"&lt;/span&gt;
      &lt;span class="na"&gt;changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/Api/**/*&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/Commons/**/*&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_MERGE_REQUEST_LABELS =~ /force-build-back/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In real CI, the first rule handles normal operation (job runs when matching files change). In &lt;code&gt;gitlab-ci-local&lt;/code&gt;, the first rule is dead — only the label matters. Now the test variables file simply sets the right label:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A-MR-back.variables.yml&lt;/strong&gt; — simulating backend changes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CI_MERGE_REQUEST_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12345"&lt;/span&gt;
&lt;span class="na"&gt;CI_MERGE_REQUEST_LABELS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;force-build-back"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;A-MR-front.variables.yml&lt;/strong&gt; — simulating frontend changes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CI_MERGE_REQUEST_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12345"&lt;/span&gt;
&lt;span class="na"&gt;CI_MERGE_REQUEST_LABELS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;force-build-front"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;A-MR-no-change.variables.yml&lt;/strong&gt; — the MR where only non-module files changed (docs, README, etc.):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;CI_MERGE_REQUEST_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12345"&lt;/span&gt;
&lt;span class="na"&gt;CI_MERGE_REQUEST_LABELS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This last one is particularly valuable: it verifies that common jobs (config generation, post-deploy checks) still run even when no module was touched. The CI equivalent of checking that the lights still work when nobody's home.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 We won't dive deeper into label-based pipeline control here — that topic deserves its own article. Stay tuned.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Per-module test cases
&lt;/h2&gt;

&lt;p&gt;With this approach, the &lt;code&gt;ci/test/&lt;/code&gt; directory naturally reflects your module structure:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ci/test/
├── A-MR-no-change.variables.yml          # MR with no module changes
├── A-MR-no-change.csv
├── A-MR-back.variables.yml               # Backend module changes
├── A-MR-back.csv
├── A-MR-front.variables.yml              # Frontend module changes
├── A-MR-front.csv
├── A-MR-migrations.variables.yml         # Database migrations
├── A-MR-migrations.csv
├── A-MR-all-force-build-labels.variables.yml  # Everything activated
├── A-MR-all-force-build-labels.csv
├── B-protected-branch.csv                # Non-MR types (no changes: involved)
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The resulting CSV diffs tell a precise story. For instance, &lt;code&gt;A-MR-back.csv&lt;/code&gt; might list 33 jobs while &lt;code&gt;A-MR-front.csv&lt;/code&gt; lists only 18 — you can instantly verify that backend Sonar jobs don't sneak into frontend-only pipelines, and that shared deployment jobs appear in both.&lt;/p&gt;

&lt;p&gt;On the project that inspired this approach (7 modules, 50+ jobs, 5 pipeline types), we ended up with 11 test cases covering every meaningful combination. The entire suite runs in under 10 seconds. That's less time than it takes to explain to a colleague why their MR pipeline is mysteriously empty.&lt;/p&gt;
&lt;h1&gt;
  
  
  8. Documentation that writes itself
&lt;/h1&gt;

&lt;p&gt;The CSV reference files serve a dual purpose: &lt;strong&gt;automated validation&lt;/strong&gt; and &lt;strong&gt;always up-to-date documentation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you use a doc-as-code tool like &lt;a href="https://vitepress.dev/" rel="noopener noreferrer"&gt;VitePress&lt;/a&gt;, &lt;a href="https://asciidoctor.org/" rel="noopener noreferrer"&gt;Asciidoctor&lt;/a&gt;, or &lt;a href="https://docusaurus.io/" rel="noopener noreferrer"&gt;Docusaurus&lt;/a&gt;, you can render CSV files as HTML tables at build time — either natively or through a custom plugin. Most tools support CSV includes out of the box or with minimal effort, and any AI assistant can generate the glue code for your specific stack in seconds.&lt;/p&gt;

&lt;p&gt;In your documentation markdown, it would look something like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Pipeline Type D: Tag (preprod deployment)&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;csv-table ci/test/D-tag-preprod.csv
&lt;/span&gt;&lt;span class="sb"&gt;```

## Pipeline Type F: Scheduled (nightly)
&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;csv-table ci/test/F-scheduled.csv
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note&lt;/strong&gt;: Adapt the &lt;code&gt;csv-table&lt;/code&gt; syntax to your tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same &lt;code&gt;ci/test/*.csv&lt;/code&gt; files then serve two purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CI Validation&lt;/strong&gt;: &lt;code&gt;gitlab-ci-local&lt;/code&gt; compares actual job lists against these reference files → pipeline passes or fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation build&lt;/strong&gt;: your doc tool renders these CSV files as HTML tables → always accurate documentation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No more outdated documentation. No more "the wiki says X but CI does Y". The CSV files are the single source of truth — enforced by CI, displayed by docs, and immune to the corporate amnesia that plagues most wikis.&lt;/p&gt;
&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;This approach provides several benefits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🛡️ &lt;strong&gt;Regression prevention&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Catch unintended rule changes before they reach production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📖 &lt;strong&gt;Living documentation&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;CSV files document expected behavior for each pipeline type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔍 &lt;strong&gt;Clear diffs&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;See exactly which jobs were added, removed, or modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚡ &lt;strong&gt;Fast feedback&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Tests run in seconds, not minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎯 &lt;strong&gt;Granular testing&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Test specific pipeline variants independently&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight is treating your CI configuration as code that deserves its own tests. Just as you wouldn't ship application code without tests, you shouldn't ship CI changes without validating their effects.&lt;/p&gt;

&lt;p&gt;This technique has saved countless hours of debugging "why doesn't this job run anymore?" issues. The upfront investment in setting up the test infrastructure pays off quickly as your CI configuration grows in complexity. Combined with other &lt;a href="https://dev.to/zenika/gitlab-ci-10-best-practices-to-avoid-widespread-anti-patterns-2mb5"&gt;CI best practices to avoid widespread anti-patterns&lt;/a&gt;, it builds a solid foundation for maintainable pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnrumu64v2kqwmnkexv32.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnrumu64v2kqwmnkexv32.jpg" alt="Testing fox"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Further reading
&lt;/h1&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__hidden-navigation-link"&gt;All Articles by Theme&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bcouetil" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" alt="bcouetil profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bcouetil" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Benoit COUETIL 💫
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Benoit COUETIL 💫
                
              
              &lt;div id="story-author-preview-content-3268957" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bcouetil" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Benoit COUETIL 💫&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" id="article-link-3268957"&gt;
          All Articles by Theme
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gitlab"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gitlab&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;em&gt;This article was enhanced with the assistance of an AI language model to ensure clarity and accuracy in the content, as English is not my native language.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>testing</category>
      <category>cicd</category>
    </item>
    <item>
      <title>⚗️ A local Agent with ADK and Docker Model Runner</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Fri, 20 Mar 2026 15:55:05 +0000</pubDate>
      <link>https://forem.com/zenika/a-local-agent-with-adk-and-docker-model-runner-283j</link>
      <guid>https://forem.com/zenika/a-local-agent-with-adk-and-docker-model-runner-283j</guid>
      <description>&lt;p&gt;&lt;em&gt;A French version is available &lt;a href="https://jeanphi-baconnais.gitlab.io/post/2026-adk-dmr" rel="noopener noreferrer"&gt;on my website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ⛅ Using AI in the Cloud
&lt;/h2&gt;

&lt;p&gt;I’m the first to admit it: I use AI models provided in the Cloud. As a user, it suits me perfectly. The models are powerful and give me good answers.&lt;/p&gt;

&lt;p&gt;But is the scope of these models really adapted to my needs? They do the job, that’s a fact, but aren't they a bit too general-purpose for my specific use cases?&lt;/p&gt;

&lt;p&gt;Running models locally is not new. It was actually one of the very first requirements that emerged: trying to deploy a model on a local machine.&lt;/p&gt;

&lt;p&gt;The apparition of &lt;strong&gt;Small Language Models (SLM)&lt;/strong&gt; has offered a concrete answer to this problem, making it possible to adapt models to specific use cases and simplify their deployment.&lt;/p&gt;

&lt;p&gt;There are now various alternatives for deploying these models. I have first heard about &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, as it was, if I’m right, one of the first tools to simplify this process.&lt;/p&gt;

&lt;p&gt;If you’re interested in Small Language Models, I highly recommend Philippe Charrière's articles available on &lt;a href="https://k33g.hashnode.dev" rel="noopener noreferrer"&gt;his blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I took some time to make my own experience, and that’s what I’m sharing with you in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 My Context
&lt;/h2&gt;

&lt;p&gt;For this experiment, I wanted to use my usual agent framework for its simplicity: Google’s &lt;strong&gt;Agent Development Kit (ADK)&lt;/strong&gt;:&lt;a href="https://google.github.io/adk-docs/" rel="noopener noreferrer"&gt; https://google.github.io/adk-docs/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A Java agent can be quickly created with ADK and these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;BaseAgent&lt;/span&gt; &lt;span class="nf"&gt;initAgent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LlmAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"trip-planner-agent"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A simple trip helper with ADK"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gemini.model"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
        You are A simple Trip Planner Agent. Your goal is to give me information about one destination.
    """&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐳 Docker Model Runner
&lt;/h2&gt;

&lt;p&gt;I had already tested Ollama to run models on my machine. But for this experiment, I wanted to try Docker's solution: &lt;strong&gt;&lt;a href="https://docs.docker.com/ai/model-runner/" rel="noopener noreferrer"&gt;Docker Model Runner&lt;/a&gt; (DMR)&lt;/strong&gt;. This project provides new Docker commands to pull, run, and list models available on your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    docker model pull
    docker model list
    docker model run 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick configuration is required in the Docker Desktop settings to enable this service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-dmr%2F1-docker-config.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-dmr%2F1-docker-config.png" alt="Docker settings" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, DMR allows you to easily fetch a model and run it on your machine. For example, to use the “gemma3” model, simply run the following command. The model will be downloaded if you don’t already have it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    docker model run gemma3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once started, the agent can respond to you as shown in this example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-dmr%2F2-dmr-run.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-dmr%2F2-dmr-run.png" alt="Docker model run" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 The link between ADK / DMR
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/langchain4j/langchain4j" rel="noopener noreferrer"&gt;&lt;strong&gt;langchain4j&lt;/strong&gt;&lt;/a&gt; library will link the model specified in ADK and the model provided by DMR. This dependency must to be added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.langchain4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;langchain4j-open-ai&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.1.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code will be a little bit modified. Instead of calling the model with a string, we use an instance of the LangChain4j class, defined with an OpenAiChatModel instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;OpenAiChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"not-needed-for-local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;adkModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LangChain4j&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LlmAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Travel Agent"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Travel expert using a local model"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adkModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🙌 A Locally Available Agent
&lt;/h2&gt;

&lt;p&gt;The agent is now plugged into a local model. The application can be started with the Maven command &lt;code&gt;mvn compile exec:java -Dexec.mainClass=adk.agent.TravelAgent&lt;/code&gt; after starting your model with Docker Model Runner. For development, this is perfect.&lt;/p&gt;

&lt;p&gt;Once the application was functional, I decided to set up a docker-compose.yml file to start the model and the application with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker attach adk-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;💡To avoid repackaging the application every time, a cache is implemented in the Dockerfile.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-dmr%2F3-app-ok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-adk-dmr%2F3-app-ok.png" alt="Docker model run" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TADA!&lt;/strong&gt; 🥳&lt;/p&gt;

&lt;h2&gt;
  
  
  🤔 A "Local" Future?
&lt;/h2&gt;

&lt;p&gt;This project was just an experimentation, but it gives some other questions. My primary use of AI is for development. Wouldn't it be better to dedicate a locally deployed agent to this activity?&lt;/p&gt;

&lt;p&gt;When I create agents planned to be deployed on the Cloud, perhaps a local model would be more than sufficient during the development phase? These questions have been answered by others who have tested this, but can it be implemented (and quickly) in a corporate context?&lt;/p&gt;

&lt;p&gt;We can, of course, go even further by building our own custom model for our specific context, but that’s a topic for another day! 😁&lt;/p&gt;

</description>
      <category>ai</category>
      <category>development</category>
      <category>docker</category>
      <category>adk</category>
    </item>
    <item>
      <title>📝 Dev.to Writers Community: One Command to Maintain Them All</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Sat, 28 Feb 2026 17:19:00 +0000</pubDate>
      <link>https://forem.com/zenika/devto-writers-community-one-command-to-maintain-them-all-2feo</link>
      <guid>https://forem.com/zenika/devto-writers-community-one-command-to-maintain-them-all-2feo</guid>
      <description>&lt;ul&gt;
&lt;li&gt;The Community&lt;/li&gt;
&lt;li&gt;What I Built&lt;/li&gt;
&lt;li&gt;
Demo

&lt;ul&gt;
&lt;li&gt;Content enrichment&lt;/li&gt;
&lt;li&gt;Bulk operations&lt;/li&gt;
&lt;li&gt;Environment&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Code&lt;/li&gt;

&lt;li&gt;How I Built It&lt;/li&gt;

&lt;li&gt;Further reading&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Community
&lt;/h1&gt;

&lt;p&gt;The people writing technical articles right here on dev.to — the ones who keep a folder full of markdown files and occasionally wonder why they didn't just use the browser editor. Occasionally.&lt;/p&gt;

&lt;h1&gt;
  
  
  What I Built
&lt;/h1&gt;

&lt;p&gt;A &lt;a href="https://github.com/bcouetil/devto-cli" rel="noopener noreferrer"&gt;fork of devto-cli&lt;/a&gt; — a Node.js CLI that turns local markdown files into dev.to articles. The &lt;a href="https://github.com/sinedied/devto-cli" rel="noopener noreferrer"&gt;original tool&lt;/a&gt; by &lt;a href="https://dev.to/sinedied"&gt;Yohan Lasorsa&lt;/a&gt; handles article creation, push to dev.to via the API, stats, GitHub-hosted images with automatic URL rewriting, and a &lt;a href="https://github.com/sinedied/publish-devto" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; for automated publishing.&lt;/p&gt;

&lt;p&gt;I just kept running into small things it didn't cover, so I forked it and added a bunch of features. Here's what came out.&lt;/p&gt;

&lt;h1&gt;
  
  
  Demo
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Content enrichment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Diagrams as code with Kroki
&lt;/h3&gt;

&lt;p&gt;Write a diagram in your markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- diagram-name: shell-runner-well-sized-before-optimization --&amp;gt;&lt;/span&gt;
'''mermaid
%%{init: {'theme':'forest'}}%%
gantt
    title Shell Executor - Well-Sized Server (Before Extreme Optimization)
    dateFormat X
    axisFormat %s
    section Waiting
    Sufficient capacity                    :active, wait, 0, 1s
    section Server
    Resource allocation                    :active, prep, after wait, 1s
    section OS
    Native system (no container)           :active, env, after prep, 1s
    section Source Code
    Git fetch (local reuse)                :active, git, after env, 2s
    section Cache
    Restore (local filesystem)             :active, cache, after git, 2s
    section Artifacts
    Download artifacts                     :done, art, after cache, 3s
    section Execution
    Scripts                                :done, exec, after art, 10s
    section Termination
    Upload cache (local)                   :active, save1, after exec, 1s
    Upload artifacts (GitLab network)      :done, save2, after save1, 4s
'''
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev diaggen my-article.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70kvru02ld8h2qj7xxjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70kvru02ld8h2qj7xxjh.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The CLI sends the block to &lt;a href="https://kroki.io" rel="noopener noreferrer"&gt;Kroki&lt;/a&gt;, gets back an image, and saves it locally. After committing the image to your assets repo, &lt;code&gt;dev push&lt;/code&gt; replaces the code block with an image link pointing to GitHub — dev.to never sees the mermaid source. But your local source is preserved, letting you fix the diagram when needed. Supports mermaid, PlantUML, BlockDiag, and &lt;a href="https://kroki.io/#support" rel="noopener noreferrer"&gt;many others&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;See 13 diagrams generated this way in &lt;a href="https://dev.to/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma"&gt;GitLab Runners: Which Topology for Fastest Job Execution?&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Automatic table of contents
&lt;/h3&gt;

&lt;p&gt;Add two markers anywhere in your article:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- TOC start --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- TOC end --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev push &lt;span class="nt"&gt;--update-toc&lt;/span&gt; my-article.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The CLI scans headings and generates anchored links between the markers. The current article uses it.&lt;/p&gt;
&lt;h3&gt;
  
  
  ANSI-styled terminal output
&lt;/h3&gt;

&lt;p&gt;Choosing colors in a displayed log is a must. The CLI converts a simple pseudo-ANSI syntax into styled HTML blocks.&lt;/p&gt;

&lt;p&gt;In your markdown, use &lt;code&gt;{{TAG}}&lt;/code&gt; to start a color. The &lt;code&gt;{{/}}&lt;/code&gt; closing tag is &lt;strong&gt;optional&lt;/strong&gt; — without it, the color continues until the next tag or end of block. This means multiline content stays colored naturally:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;'''ansi
{{RED}}The quick brown fox{{BOLD_RED}} jumps over the lazy dog
{{GREEN}}The quick brown fox{{BOLD_GREEN}} jumps over the lazy dog
{{YELLOW}}The quick brown fox{{BOLD_YELLOW}} jumps over the lazy dog
{{ORANGE}}The quick brown fox{{BOLD_ORANGE}} jumps over the lazy dog
{{BLUE}}The quick brown fox
{{BOLD_BLUE}}jumps over the lazy dog
{{MAGENTA}}The quick brown fox{{BOLD_MAGENTA}} jumps over the lazy dog
{{CYAN}}The quick brown fox{{BOLD_CYAN}} jumps over the lazy dog
{{WHITE}}The quick brown fox{{BOLD_WHITE}} jumps over the lazy dog
{{GRAY}}The quick brown fox{{BOLD_GRAY}} jumps over the lazy dog
{{BG_RED}}The quick brown fox{{/}} jumps over the lazy dog
{{BG_GREEN}}The quick brown fox{{BG_YELLOW}} jumps over the lazy dog
{{BG_ORANGE}}The quick brown fox
jumps over the lazy dog
{{BG_BLUE}}The quick brown fox{{BG_MAGENTA}} jumps over the lazy dog
{{BG_CYAN}}The quick brown fox{{BG_WHITE}} jumps over the lazy dog
{{BG_GRAY}}The quick brown fox{{/}} jumps over the lazy dog
'''
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;After &lt;code&gt;dev push&lt;/code&gt;, this renders as a properly colored terminal block on dev.to — 9 base colors × 3 variants (plain, bold, background). No closing tag needed: colors flow until the next tag, even across lines. All colors are overridable via &lt;code&gt;.env&lt;/code&gt; variables (&lt;code&gt;ANSI_RED=#ff6161&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox
&lt;/span&gt;&lt;span&gt;jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt; jumps over the lazy dog
&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox
jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt;&lt;span&gt; jumps over the lazy dog
&lt;/span&gt;&lt;span&gt;The quick brown fox&lt;/span&gt; jumps over the lazy dog&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu2nmkbuldj4u7y91wre.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu2nmkbuldj4u7y91wre.jpg" alt="illustration description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Bulk operations
&lt;/h2&gt;

&lt;p&gt;Run one command, affect all your articles at once.&lt;/p&gt;

&lt;p&gt;A shared footer file (e.g. "Further reading" links) kept in sync across all articles. Update the footer file once, then push all — the CLI finds the &lt;code&gt;# Further reading&lt;/code&gt; marker in each article and replaces everything below it with the shared footer content:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev push &lt;span class="nt"&gt;--update-toc&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Smart file renaming
&lt;/h3&gt;

&lt;p&gt;When an article title changes, the filename should follow:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev rename &lt;span class="s2"&gt;"*.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🦊 GitLab CI: A Battle-Tested Mono-Repo CI/CD Architecture
  GITLAB_mono-repo-architecture.md → GITLAB_ci-battle-tested-mono-repo-ci-cd-architecture.md

🔍 Every Developer Should Review Code — Not Just Seniors
  MISC_every-developer-should-review-code.md → MISC_every-developer-review-code-seniors.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Strips emoji, filters 80+ English stop words (a, the, and, how…), keeps the first 5 significant words, preserves the date/category prefix.&lt;/p&gt;
&lt;h3&gt;
  
  
  Article badges generation
&lt;/h3&gt;

&lt;p&gt;Generate visual badges for a &lt;a href="https://github.com/bcouetil" rel="noopener noreferrer"&gt;GitHub profile README&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev badges &lt;span class="nt"&gt;--jpg&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each badge overlays the article's cover image with its &lt;strong&gt;title, publication date, view count, and reading time&lt;/strong&gt;. Articles are auto-grouped by category (extracted from filename prefix) and sorted by date within each group.&lt;/p&gt;

&lt;p&gt;The most viewed articles get &lt;strong&gt;golden stars&lt;/strong&gt; ⭐ — popularity is measured relative to the 2nd most viewed article (to avoid one viral post skewing everything). ≥90% of that reference = 3 stars, ≥50% = 2, ≥25% = 1. Stars and stats refresh on every run.&lt;/p&gt;

&lt;p&gt;Produces a &lt;code&gt;_ARTICLES.md&lt;/code&gt; file with badge images linking to each article. Here's my GitLab section:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-achieving-3-second-jobs-on-million-line-codebases-3nlm"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-achieving-3-second-jobs-million-line.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI: Achieving 3-Second Jobs on Million-Line Codebases"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Frunners-topology-fastest-job-execution.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab Runners: Which Topology for Fastest Job Execution?"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/efficient-git-workflow-for-web-apps-advancing-progressively-from-scratch-to-thriving-3af6"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fefficient-workflow-web-apps-advancing.jpg%3Fv%3D20260219" width="100%" alt="🔀 Efficient Git Workflow for Web Apps: Advancing Progressively from Scratch to Thriving"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-forget-gitkraken-here-are-the-only-git-commands-you-need-4ckj"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fgitlab-forget-gitkraken-commands-need.jpg%3Fv%3D20260219" width="100%" alt="🔀🦊 GitLab: Forget GitKraken, Here are the Only Git Commands You Need"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-a-python-script-displaying-latest-pipelines-in-groups-projects-5b5a"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fpython-script-displaying-latest-pipelines.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab: A Python Script Displaying Latest Pipelines in a Group's Projects"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-a-python-script-calculating-dora-metrics-258o"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fpython-script-calculating-dora-metrics.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab: A Python Script Calculating DORA Metrics"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-deploy-a-majestic-single-server-runner-on-aws-d3"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-deploy-majestic-single-server.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI: Deploy a Majestic Single Server Runner on AWS"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-the-majestic-single-server-runner-1b5b"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-majestic-single-server-runner.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI: The Majestic Single Server Runner"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-yaml-modifications-tackling-the-feedback-loop-problem-4ib1"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-yaml-modifications-tackling-feedback.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI YAML Modifications: Tackling the Feedback Loop Problem"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-optimization-15-tips-for-faster-pipelines-55al"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-optimization-tips-faster-pipelines.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI Optimization: 15+ Tips for Faster Pipelines"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-10-best-practices-to-avoid-widespread-anti-patterns-2mb5"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-best-practices-avoid-widespread.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI: 10+ Best Practices to Avoid Widespread Anti-Patterns"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-pages-preview-the-no-compromise-hack-to-serve-per-branch-pages-5599"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fpages-per-branch-no-compromise-hack.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab Pages per Branch: The No-Compromise Hack to Serve Preview Pages"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/chatgpt-if-you-please-make-me-a-gitlab-jobs-attributes-sorter-3co3"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Fci-jobs-attributes-sorter-python.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab CI Jobs Attributes Sorter: A Python Script for Consistent YAML Files"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-runners-topologies-pros-and-cons-2pb1"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbcouetil%2Farticles%2Fmain%2Fimages%2Fbadges%2Frunners-topologies-pros-cons.jpg%3Fv%3D20260219" width="100%" alt="🦊 GitLab Runners Topologies: Pros and Cons"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Embed the file in a &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k"&gt;pinned article&lt;/a&gt; and all your articles become browsable from one place. Badges stay up to date with every &lt;code&gt;dev badges&lt;/code&gt; run.&lt;/p&gt;
&lt;h3&gt;
  
  
  Broken link checking
&lt;/h3&gt;

&lt;p&gt;Before publishing:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev checklinks my-article.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Catches dead URLs before readers do. Smart enough to treat HTTP 403/429 as "probably fine" (bot blocking, not a dead link). Also validates cover image and canonical URL from front matter. Works on all articles at once with &lt;code&gt;dev checklinks "*.md"&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;

&lt;p&gt;Configure once, forget about it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Organization publishing
&lt;/h3&gt;

&lt;p&gt;One line in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DEVTO_ORG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zenika
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The CLI adds &lt;code&gt;organization: zenika&lt;/code&gt; to the front matter and resolves the org ID automatically on push.&lt;/p&gt;
&lt;h3&gt;
  
  
  Proxy support for corporate environments
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HTTPS_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://proxy.company.com:8080
dev push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All API calls, Kroki requests, and link checks go through the proxy. Self-signed certificates handled too.&lt;/p&gt;
&lt;h1&gt;
  
  
  Code
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/bcouetil/devto-cli" rel="noopener noreferrer"&gt;github.com/bcouetil/devto-cli&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  How I Built It
&lt;/h1&gt;

&lt;p&gt;This is a TypeScript / Node.js CLI, forked from &lt;a href="https://github.com/sinedied/devto-cli" rel="noopener noreferrer"&gt;sinedied/devto-cli&lt;/a&gt;. Each feature was added incrementally — proxy support first, then Kroki diagrams, TOC generation, organization publishing, footer management, link checking, file renaming, ANSI blocks, and finally badge generation.&lt;/p&gt;

&lt;p&gt;This fork started as a one-line proxy fix and kept growing from there. Every feature was added because I needed it, not because it looked good on a feature list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplsjsnxchzuab4deoxcj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplsjsnxchzuab4deoxcj.jpg" alt="illustration description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Draw Things using Flux.1 [Schnell] model&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Further reading
&lt;/h1&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__hidden-navigation-link"&gt;All Articles by Theme&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bcouetil" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" alt="bcouetil profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bcouetil" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Benoit COUETIL 💫
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Benoit COUETIL 💫
                
              
              &lt;div id="story-author-preview-content-3268957" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bcouetil" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Benoit COUETIL 💫&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" id="article-link-3268957"&gt;
          All Articles by Theme
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gitlab"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gitlab&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;em&gt;This article was enhanced with the assistance of an AI language model to ensure clarity and accuracy in the content, as English is not my native language.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>🦊 GitLab CI: Achieving 3-Second Jobs on Million-Line Codebases</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Thu, 19 Feb 2026 21:17:00 +0000</pubDate>
      <link>https://forem.com/zenika/gitlab-ci-achieving-3-second-jobs-on-million-line-codebases-3nlm</link>
      <guid>https://forem.com/zenika/gitlab-ci-achieving-3-second-jobs-on-million-line-codebases-3nlm</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;Prerequisites: The right foundation&lt;/li&gt;
&lt;li&gt;Classic optimizations first&lt;/li&gt;
&lt;li&gt;Extreme optimizations: going beyond&lt;/li&gt;
&lt;li&gt;
Breaking down the 3-second job

&lt;ul&gt;
&lt;li&gt;Job execution on a well-sized shell runner&lt;/li&gt;
&lt;li&gt;Phase 1: Waiting (~0s)&lt;/li&gt;
&lt;li&gt;Phase 2: Server (~0s)&lt;/li&gt;
&lt;li&gt;Phase 3: OS / Shell (~1s)&lt;/li&gt;
&lt;li&gt;Phase 4: Git Clone/Fetch (~1s)&lt;/li&gt;
&lt;li&gt;Phase 5: Cache (~0s)&lt;/li&gt;
&lt;li&gt;Phase 6: Artifacts (none)&lt;/li&gt;
&lt;li&gt;Phase 7: Script (~1s)&lt;/li&gt;
&lt;li&gt;Phase 8: Termination (~0s)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Final timing breakdown&lt;/li&gt;

&lt;li&gt;Real project results&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;li&gt;Further reading&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Initial thoughts
&lt;/h1&gt;

&lt;p&gt;Is it really possible to run GitLab CI jobs in just &lt;strong&gt;3 seconds&lt;/strong&gt; on a codebase with several million lines of code? The answer is yes, and this article will show you how.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx096djqkyte6ko4hyc2f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx096djqkyte6ko4hyc2f.jpg" alt="Job 3 seconds"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After comparing different runner topologies in &lt;a href="https://dev.to/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma"&gt;GitLab Runners: Which Topology for Fastest Job Execution?&lt;/a&gt;, we found that Shell and Docker executors offer the best potential for fast job execution. Now it's time to push those runners to their absolute limits with extreme optimizations.&lt;/p&gt;

&lt;p&gt;This isn't about theoretical performance—these are real, production results achieved on actual projects. The 3-second example is our lightest job (a JIRA/MR synchronization check), and it consistently runs in ~3-5 seconds on our 1,500,000+ line mono-repo actively maintained by 15 developers. We have jobs at various fixed speeds: one at ~3-5s, some at a few seconds, others at ~2min for resource-heavy builds, and end-to-end tests at ~15min. &lt;strong&gt;The optimizations in this article apply to all jobs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Time to hunt down every millisecond of overhead — no mercy. Because, as French president Macron said, "Pipeline sometimes is too slow".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdgbos98zsp0zi5i78hv9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdgbos98zsp0zi5i78hv9.jpg" alt="sometimes-is-too-slow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Prerequisites: The right foundation
&lt;/h1&gt;

&lt;p&gt;Before diving into extreme optimizations, we need the right infrastructure foundation. Based on our topology comparison, this means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure choice&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Shell executor (fastest) or Docker executor (fast with isolation)&lt;/li&gt;
&lt;li&gt;✅ Single well-provisioned server (not autoscaling)&lt;/li&gt;
&lt;li&gt;✅ Local SSD storage (NVMe preferred)&lt;/li&gt;
&lt;li&gt;✅ Sufficient CPU/RAM for concurrent jobs&lt;/li&gt;
&lt;li&gt;✅ Fast network connection to GitLab instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;: &lt;a href="https://dev.to/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma"&gt;Every other topology adds fundamental latencies&lt;/a&gt; (VM provisioning, pod scheduling, remote cache, shared resources) that cannot be eliminated through configuration. Starting with the wrong topology means you've already lost.&lt;/p&gt;

&lt;h1&gt;
  
  
  Classic optimizations first
&lt;/h1&gt;

&lt;p&gt;Before going extreme, apply standard GitLab CI optimizations from &lt;a href="https://dev.to/zenika/gitlab-ci-optimization-15-tips-for-faster-pipelines-55al"&gt;GitLab CI Optimization: 15+ Tips for Faster Pipelines&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But these optimizations alone won't get you to 3 seconds&lt;/strong&gt;. They'll improve your average pipeline duration. To reach sub-10-second jobs, we need to dig deeper into GitLab runners arcanes.&lt;/p&gt;

&lt;h1&gt;
  
  
  Extreme optimizations: going beyond
&lt;/h1&gt;

&lt;p&gt;Now we enter extreme optimization territory. These techniques are specific to Shell/Docker runners and exploit their local filesystem advantages.&lt;/p&gt;

&lt;p&gt;Here's what we'll optimize to near-zero overhead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Waiting time&lt;/strong&gt; → Proper server sizing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server provisioning&lt;/strong&gt; → Already exists (no VM/pod creation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS/Container startup&lt;/strong&gt; → Native shell (1s) or cached images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git operations&lt;/strong&gt; → Shallow fetches, reused local clones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt; → Local filesystem, preserved directories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifacts&lt;/strong&gt; → None in our fastest jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script execution&lt;/strong&gt; → Minimal in our fastest jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Termination&lt;/strong&gt; → No cache/artifact uploads in our fastest jobs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's break down each phase and see how to optimize it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Breaking down the 3-second job
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Job execution on a well-sized shell runner
&lt;/h2&gt;

&lt;p&gt;First, let's visualize what a well-optimized shell runner job timeline looks like before extreme optimizations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70kvru02ld8h2qj7xxjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F70kvru02ld8h2qj7xxjh.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total&lt;/strong&gt;: already fast on fastest jobs scripts. Now let's optimize each phase to the extreme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Waiting (~0s)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;: Jobs queue when runners are saturated. Your developers are staring at a spinner. Somewhere, a PM is asking &lt;em&gt;"is the pipeline stuck again?"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution : proper server sizing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Size the VM for peak concurrent load (not average)&lt;/li&gt;
&lt;li&gt;CPU: 16 cores for a 15 developers team on a mono-repo. Not the smallest, but cheap comparing to salaries&lt;/li&gt;
&lt;li&gt;Disk I/O: NVMe SSD essential for concurrent git/cache operations&lt;/li&gt;
&lt;li&gt;Concurrency tuned (~16 concurrent jobs for 16 CPU)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⏱️ &lt;strong&gt;Result: ~0s waiting&lt;/strong&gt; (when properly sized). No more "grab a coffee" excuses — the job finishes before you stand up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Server (~0s)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Server already exists&lt;/strong&gt; — shocking, right?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shell runner: no provisioning at all&lt;/li&gt;
&lt;li&gt;Docker runner: no VM creation, just container scheduling&lt;/li&gt;
&lt;li&gt;Resources immediately available&lt;/li&gt;
&lt;li&gt;No cloud API calls needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a fundamental advantage of single-server topologies over autoscaling. While Kubernetes is busy scheduling pods, our job is already done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: OS / Shell (~1s)
&lt;/h2&gt;

&lt;p&gt;Think of it this way: Shell executor is a barefoot sprinter, Docker is a sprinter with fancy shoes. Both are fast, but one has less to put on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Shell executor&lt;/strong&gt; (fastest, ~1s):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No Docker image needed&lt;/li&gt;
&lt;li&gt;No container startup&lt;/li&gt;
&lt;li&gt;Direct shell execution&lt;/li&gt;
&lt;li&gt;Native OS environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For Docker executor&lt;/strong&gt; (fast with isolation ~1-4s):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-pull images to server&lt;/li&gt;
&lt;li&gt;Use lightweight base images (alpine)&lt;/li&gt;
&lt;li&gt;Layer caching on local Docker&lt;/li&gt;
&lt;li&gt;Keep containers warm when possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phase 4: Git Clone/Fetch (~1s)
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. Or rather, where the magic &lt;em&gt;doesn't&lt;/em&gt; happen — because the best git operation is the one you barely do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy and depth
&lt;/h3&gt;

&lt;p&gt;Obviously, to achieve 3s on a large codebase, a close version of the code must already be present locally. The default algorithm is a fetch (which is perfect), meaning that the runner will just reconstruct a few commits in a build directory that already has been used by a previous job, preferably with nearly same code but different commits.&lt;/p&gt;

&lt;p&gt;The default is 20 commits reconstructed. In fact, you only need &lt;strong&gt;one&lt;/strong&gt; most of the time (when not doing git stuff).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GIT_DEPTH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# default to 20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Fetch flags
&lt;/h3&gt;

&lt;p&gt;The default fetch flags are &lt;code&gt;--force --prune --tags&lt;/code&gt;. &lt;code&gt;force&lt;/code&gt; and &lt;code&gt;prune&lt;/code&gt; are very useful to handle git fetch problems and keep local repo size reasonable. You can experiment with these on short-lived runners, at your own risks. We tried this but had to step back for consistency, even on our daily runners.&lt;/p&gt;

&lt;p&gt;But fetching &lt;code&gt;tags&lt;/code&gt; is almost never a good idea, at least by default. Even though we heavily rely on tag pipelines, we still don't need to fetch any tag. Except for a job that uses older tags to extract versions; for it we manually fetch tags.&lt;/p&gt;

&lt;p&gt;Optionally, we can define the refmap to fetch, avoiding unnecessary fetching.&lt;/p&gt;

&lt;p&gt;Here are our flags for merge requests :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;GIT_FETCH_EXTRA_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--force --prune --no-tags&lt;/span&gt;
    &lt;span class="s"&gt;--refmap "+refs/merge-requests/${CI_MERGE_REQUEST_IID}/head:refs/remotes/origin/merge-requests/${CI_MERGE_REQUEST_IID}/head"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note: add &lt;code&gt;--verbose&lt;/code&gt; to show what's happening, and be sure to know what takes time and what not. We still have this flag because there only a few lines added when fetch is optimised, and it does not slow down the job.&lt;/p&gt;
&lt;h3&gt;
  
  
  Situational: tailored clone path
&lt;/h3&gt;

&lt;p&gt;When long-lived branches have a fair amount of difference (the longer the branch and/or the higher the developer count, the more differences), fetch takes time. On our projects we have thousands commits of differences between long-lived branches at worse time of our cycle!&lt;/p&gt;

&lt;p&gt;To keep a sub-second fetch time, we configure clone in paths depending on the target branch. And yes, &lt;a href="https://docs.gitlab.com/ci/pipelines/merge_request_pipelines/" rel="noopener noreferrer"&gt;MR pipelines secret mode&lt;/a&gt; is required for this.&lt;/p&gt;

&lt;p&gt;So our clone path depend on the pipeline type :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# for MRs. Concurrent ID is after the branch name, to ease old branches cleaning&lt;/span&gt;
  &lt;span class="na"&gt;GIT_CLONE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME/$CI_CONCURRENT_ID&lt;/span&gt;
  &lt;span class="c1"&gt;# for long-lived branches (same path, different variable)&lt;/span&gt;
  &lt;span class="na"&gt;GIT_CLONE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_COMMIT_REF_NAME/$CI_CONCURRENT_ID&lt;/span&gt;
  &lt;span class="c1"&gt;# for tags (rare, no need for distinction)&lt;/span&gt;
  &lt;span class="na"&gt;GIT_CLONE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_BUILDS_DIR/$CI_PROJECT_NAME/tags/$CI_CONCURRENT_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This can only be used when &lt;code&gt;custom_build_dir&lt;/code&gt; is enabled in the runner’s configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx2e1e43agbiv8pucv1f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvx2e1e43agbiv8pucv1f.jpg" alt="Mechanical orange fox running at extreme speed"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 5: Cache (~0s)
&lt;/h2&gt;

&lt;p&gt;Downloading and extracting remote cache takes time. Even with local GitLab cache, you still have to zip/unzip the folder elsewhere. That's like packing and unpacking your suitcase every time you go to the kitchen.&lt;/p&gt;
&lt;h3&gt;
  
  
  Shared package managers folders
&lt;/h3&gt;

&lt;p&gt;Shared package managers folders on the server will reuse downloaded assets without overhead. When local cache is warm, there are no download/unzip and no zip/upload.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NPM_CONFIG_CACHE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/cache/gitlab-runner/.npm&lt;/span&gt;
  &lt;span class="na"&gt;NUGET_PACKAGES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_BUILDS_DIR/NuGetPackages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note: For safety, it is still better to have different folders for dev/staging/prod.&lt;/p&gt;
&lt;h3&gt;
  
  
  Optional : do not clean unpacked assets
&lt;/h3&gt;

&lt;p&gt;Before executing the scripts, GitLab deletes any past produced files. We can save precious seconds, even minutes, by not deleting them, and, most importantly, reuse them. This is especially useful for &lt;code&gt;node_modules&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GIT_CLEAN_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-ffdx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--exclude=**/node_modules/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This works best for custom GIT_CLONE_PATH discussed earlier. This could lead to strange behavior in theory : we took the risk for feature branches, and never encountered problems for our 15 developers mono-repo.&lt;/p&gt;

&lt;p&gt;Note: For safety, it is still better to clean for staging/prod environment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 6: Artifacts (none)
&lt;/h2&gt;

&lt;p&gt;The fastest jobs in pipelines do not handle artifacts. We consider none here. Zero. Nada. The fastest artifact is the one that doesn't exist.&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 7: Script (~1s)
&lt;/h2&gt;

&lt;p&gt;For the 3-second target, we're specifically measuring fast jobs like linting, formatting checks. Any standard job will naturally take longer. But hey, even our "slow" jobs appreciate having 2 seconds less overhead — that's 2 seconds of their life they'll never get back.&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 8: Termination (~0s)
&lt;/h2&gt;

&lt;p&gt;At the end, the job handles artifacts and the cache. The fastest jobs produce no artifact and use local custom cache or none. The job exits so fast, it doesn't even say goodbye.&lt;/p&gt;
&lt;h1&gt;
  
  
  Final timing breakdown
&lt;/h1&gt;

&lt;p&gt;Here's what the fully optimized timeline looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31yq645nqiki288w5dz2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31yq645nqiki288w5dz2.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⚡ &lt;strong&gt;Total: ~3 seconds&lt;/strong&gt; per job (with sub-1s script)&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;We've reduced overhead to ~2s&lt;/strong&gt;, leaving all remaining time for actual work. Your CI is now faster than your &lt;code&gt;npm start&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;
  
  
  Real project results
&lt;/h1&gt;

&lt;p&gt;Again, these aren't theoretical numbers: we experience this extreme speed on a daily basis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shell runner - 3 seconds&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx096djqkyte6ko4hyc2f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx096djqkyte6ko4hyc2f.jpg" alt="Job 3 seconds"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker runner - 13 seconds&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe0y6kx8s7j8r8osx29rg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe0y6kx8s7j8r8osx29rg.jpg" alt="Job 13 seconds"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;: If you need isolation, Docker is still very fast with these optimizations. But Shell executor is unbeatable for raw speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example development pipeline - 1min45 for 20 jobs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyzz4vma80zphi3a9oky8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyzz4vma80zphi3a9oky8.jpg" alt="Pipeline 1min45"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most jobs run in parallel on each stage. The pipeline spends minimal time on overhead and maximum time on actual work.&lt;/p&gt;
&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Achieving 3-second jobs on a multi-million line codebase is possible with the right combination.&lt;/p&gt;

&lt;p&gt;These techniques show that &lt;strong&gt;single-server Shell/Docker runners, when properly optimized, vastly outperform autoscaling solutions&lt;/strong&gt; for typical development workflows. The local filesystem advantages are impossible to beat.&lt;/p&gt;

&lt;p&gt;Not every job can be 3 seconds—builds and full test suites will always take longer. But for fast-feedback jobs, sub-10-second execution is absolutely achievable and dramatically improves developer experience. Your developers will wonder if the pipeline is broken... because it's &lt;em&gt;too fast&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvnqcdpcywk4i83gnbg1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvnqcdpcywk4i83gnbg1.jpg" alt="Mechanical orange fox crossing finish line"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Draw Things using Flux.1 [Schnell] model&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Further reading
&lt;/h1&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__hidden-navigation-link"&gt;All Articles by Theme&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bcouetil" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" alt="bcouetil profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bcouetil" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Benoit COUETIL 💫
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Benoit COUETIL 💫
                
              
              &lt;div id="story-author-preview-content-3268957" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bcouetil" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Benoit COUETIL 💫&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" id="article-link-3268957"&gt;
          All Articles by Theme
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gitlab"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gitlab&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;em&gt;This article was enhanced with the assistance of an AI language model to ensure clarity and accuracy in the content, as English is not my native language.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>cicd</category>
      <category>performance</category>
    </item>
    <item>
      <title>How GiLab Duo Agent Platform &amp; Antigravity can collaborate to improve the quality of our applications</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Fri, 13 Feb 2026 13:37:56 +0000</pubDate>
      <link>https://forem.com/zenika/how-gilab-duo-agent-platform-antigravity-can-collaborate-to-improve-the-quality-of-our-38bm</link>
      <guid>https://forem.com/zenika/how-gilab-duo-agent-platform-antigravity-can-collaborate-to-improve-the-quality-of-our-38bm</guid>
      <description>&lt;h2&gt;
  
  
  🔎 Short introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitLab Duo Agent Platform&lt;/strong&gt; is the new AI solution available in the DevOps platform. Integrated in the Premium and Ultimate, this offers a lot of very interesting features powered by AI I tried to resume in this cheatsheet (available on &lt;a href="https://dev.to/zenika/gitlab-cheatsheet-15-gitlab-duo-3fhg"&gt;dev.to&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F1-duo-cheatsheet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F1-duo-cheatsheet.png" alt="GitLab Duo Agent Platform Cheatsheet" width="800" height="870"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antigravity&lt;/strong&gt; is a new agent developer platform created by Google which notably changes the developer mindset by turning them into an agent manager or orchestrator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F2-antigravity.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F2-antigravity.png" alt="Antigravity logo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🤔 Why use these 2 AI tools?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Antigravity&lt;/strong&gt; is a great development tool, mainly focused on the development of features or to resolve issues on projects. **GitLab Duo Agent Platform, **available for Premium and Ultimate GitLab, is present throughout the DevOps platform, helping us from project conception (epics, issues) to the CICD.&lt;/p&gt;

&lt;p&gt;I have the chance to have access to GitLab Duo Agent Platform for personal or demo projects. For my other projects, I use Antigravity and wait, why not use Antigravity on my projects that already have GitLab Duo Agent Platform access?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🎯 This is the objective of this blog post. Show you the result of using these two tools and explain how much this can be useful to improve my projects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For this blog post, I created a new application, very classic : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a React front end component, &lt;/li&gt;
&lt;li&gt;a Springboot backend with a Gemini integration to generate images&lt;/li&gt;
&lt;li&gt;a Docker compose configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤗 How to combine these two Agentic Developer Platform?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitLab Duo Agent Platform&lt;/strong&gt; can help you generate a description of epics and issues from the title or notes you can initiate.&lt;/p&gt;

&lt;p&gt;The  first conclusion of this usage of AI (even outside the combination of GitLab Duo and Antigravity) is that it encourages me to write a maximum of information on my issues. Even on projects where I don’t have access to GitLab Duo, I maintain this behavior and try to give now more importance to the description of issues.&lt;/p&gt;

&lt;p&gt;The next step is to initiate a Merge Request (MR) directly from the issue using the button “Generate MR with Duo”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F3-duo-issue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F3-duo-issue.png" alt="GitLab issue" width="550" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the result is very interesting. GitLab launches an agent session (visible in the menu “Automate”) and after a few minutes, a MR is created and one commit appears.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F4-issue-duo-done.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F4-issue-duo-done.png" alt="GitLab issue done" width="517" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F5-duo-session.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F5-duo-session.png" alt="GitLab Duo session" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F6-duo-plan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F6-duo-plan.png" alt="Duo session" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F7-duo-mr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F7-duo-mr.png" alt="Duo MR" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Creating an issue and letting GitLab Duo work on it to initialize one MR is a great game changer. Of course, as with any MR, human reviews are still required, but a high quality is here.&lt;/p&gt;

&lt;p&gt;I forgot to mention this point, Antigravity is based on a fork of Visual Studio Code, where you can install the GitLab Workflow extension including GitLab Duo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F8-gitlab-extension.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F8-gitlab-extension.png" alt="GitLab extension" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F9-duo-antigravity.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F9-duo-antigravity.png" alt="Duo integration in Antigravity" width="800" height="943"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 AI Review of AI generated code
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;💡Before reviewing this MR, can I imagine delegate this task to Antigravity?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Of course. Let’s see the result. After opening the Git branch in Antigravity, I ask it to review this MR. On the previous Merge Request, Antigravity found and fixed several issues like a CORS problem, and reviewed the Java class architecture. After examining the fixes, I agree with that and I commit and push.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F10-duo-review.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F10-duo-review.png" alt="Duo review" width="800" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;*💡 Next step? Does GitLab Duo agree with the changes? *&lt;/p&gt;

&lt;p&gt;In the MR, we can ping GitLab Duo. “&lt;em&gt;Hey @GitLabDuo, what do you think about the last commit&lt;/em&gt;”? &lt;/p&gt;

&lt;p&gt;After a few minutes, GitLab Duo provided its review. All the items detected during the initial review are listed with their status updated with the new commits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F11-duo-recommandations.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F11-duo-recommandations.png" alt="Duo recommandations" width="717" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, a demo application to generate LEGO images, the implementation of Gemini is missing but for the first step of this application, this is acceptable. I can merge it.&lt;/p&gt;

&lt;p&gt;👉 This approach is interesting. Everyone knows that AI needs human review. While this workflow might not immediately include your human review, for complex or big features, this AI review step can fix issues the first model didn’t see. On my MR, I saw interesting discussions and conflicting opinions.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Go further
&lt;/h2&gt;

&lt;p&gt;After playing with both AI tools, I noticed one action which is repeated: the manual request for GitLab Duo to make a review. To fix it, I connected the GitLab MCP, which allows me to facilitate actions on issues, merge requests, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F12-gitlab-mcp-configuration.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F12-gitlab-mcp-configuration.png" alt="GitLab MCP configuration" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this configuration, I can ask Antigravity to ping GitLab Duo for a new review. I can stay on my IDE and keep focused on another task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F13-gitlab-mcp-work.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F13-gitlab-mcp-work.png" alt="GitLab MCP working" width="523" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can be further improved by utilizing &lt;strong&gt;Skills&lt;/strong&gt;. This new standard maintained by Anthropic  introduces a way to specify instructions, scripts and resources that AI agents can integrate and use to perform tasks. &lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/agentskills/agentskills" rel="noopener noreferrer"&gt;https://github.com/agentskills/agentskills&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Antigravity integrated this standard (cf doc &lt;a href="https://antigravity.google/docs/skills" rel="noopener noreferrer"&gt;https://antigravity.google/docs/skills&lt;/a&gt;) and skills have to be in the &lt;code&gt;.agent/skills&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;To reduce manual action, I create a &lt;code&gt;request_review&lt;/code&gt; skill designed to automatically add a note into the Merge Request asking GitLab Duo to review it. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F14-skills.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F14-skills.png" alt="Skills example" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this file, we can also add a request to GitLab Duo after each commit. As you can see in the next screenshot, I commit the skill file and we see a glab (the GitLab CLI) command being executed to add a note in the MR.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F15-antigravity-status.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F15-antigravity-status.png" alt="Antigravity status" width="694" height="201"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F16-duo-review-by-antigravity.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F16-duo-review-by-antigravity.png" alt="Duo review by Antigravity" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 A future with some AI tools?
&lt;/h2&gt;

&lt;p&gt;I have used the pair GitLab Duo &amp;amp; Antigravity across different contexts and issues. From creating new projects to migrating Java versions or adding a feature, using these two tools was interesting. The “discussion” between them raised many questions and reflections.&lt;/p&gt;

&lt;p&gt;For this blog post, my Lego application is done and works locally in two issues. Of course I don’t let AI create all the code, but the majority was done by GitLab Duo and Antigravity.&lt;/p&gt;

&lt;p&gt;I am aware that this usage is primarily for demo projects or short PoCs, but I am sure that the power of these two tools can be complementary and significantly improve the quality of our application.&lt;/p&gt;

&lt;p&gt;As I explained in this &lt;a href="https://dev.to/zenika/google-antigravity-lere-des-ide-agentique-109i"&gt;blog post&lt;/a&gt; and at the beginning of this one, we, developers, must change our workflow by migrating from synchronous work to asynchronously, running tasks in parallel and focusing on agent orchestration. However, we must also be careful to respect the limitations of our cognitive capacity. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F17-lego-app.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-duo-antigravity%2F17-lego-app.png" alt="Lego Generator application" width="677" height="876"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>antigravity</category>
      <category>google</category>
      <category>giltlabduo</category>
    </item>
    <item>
      <title>🦊 GitLab Runners: Which Topology for Fastest Job Execution?</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Fri, 30 Jan 2026 17:28:43 +0000</pubDate>
      <link>https://forem.com/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma</link>
      <guid>https://forem.com/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;Understanding job execution phases&lt;/li&gt;
&lt;li&gt;
Comparing runner types

&lt;ul&gt;
&lt;li&gt;GitLab Shared Runners (SaaS)&lt;/li&gt;
&lt;li&gt;Kubernetes Executor (Fixed Cluster)&lt;/li&gt;
&lt;li&gt;Kubernetes Executor (Autoscaling Cluster)&lt;/li&gt;
&lt;li&gt;Docker Executor (Single Server)&lt;/li&gt;
&lt;li&gt;Shell Executor (Single Server)&lt;/li&gt;
&lt;li&gt;Docker Autoscaler (Fleeting)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Job speed comparison summary&lt;/li&gt;

&lt;li&gt;

Recommendation: Shell or Docker executors for fastest jobs

&lt;ul&gt;
&lt;li&gt;Why single-server executors win on speed&lt;/li&gt;
&lt;li&gt;Shell vs Docker trade-offs&lt;/li&gt;
&lt;li&gt;The performance/price/maintenance sweet spot&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Wrapping up&lt;/li&gt;

&lt;li&gt;Further reading&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;With multiple executor types available—from shared SaaS runners to Kubernetes clusters to single-server setups—understanding how each impacts job speed helps you make informed architectural decisions. &lt;strong&gt;Spoiler: the simplest topologies often deliver the fastest jobs.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Initial thoughts
&lt;/h1&gt;

&lt;p&gt;Choosing the right GitLab Runner topology is crucial for &lt;strong&gt;fast job execution&lt;/strong&gt;. With multiple executor types available—from shared SaaS runners to self-hosted solutions—understanding how each impacts job speed helps you make informed architectural decisions.&lt;/p&gt;

&lt;p&gt;This article focuses on a single question: &lt;strong&gt;which runner topology executes jobs the fastest?&lt;/strong&gt; We analyze different topologies through the lens of job execution phases, comparing the time overhead each infrastructure type adds before, during, and after script execution. Whether you're starting fresh or optimizing an existing setup, understanding these trade-offs is essential to minimize job duration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Note&lt;/strong&gt;: This article focuses on intrinsic speed characteristics. Some disadvantages mentioned here can be mitigated through configuration tricks (idle pools, spot instances, over-provisioning, etc.). For detailed pros/cons and mitigation strategies, see &lt;a href="https://dev.to/zenika/gitlab-runners-topologies-pros-and-cons-2pb1"&gt;GitLab Runners Topologies: Pros and Cons&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Understanding job execution phases
&lt;/h1&gt;

&lt;p&gt;From runner selection to final cleanup, every job goes through multiple technical phases. Understanding these phases is crucial to optimizing pipeline performance.&lt;/p&gt;

&lt;p&gt;Here's a detailed breakdown of all the technical steps involved in running a single GitLab CI job:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ccu2r1rrt9rh0bx8xzp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ccu2r1rrt9rh0bx8xzp.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each phase adds latency to job execution. Your script might take 10 seconds, but the overhead can easily triple that — death by a thousand papercuts. Different runner topologies handle these phases very differently.&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparing runner types
&lt;/h1&gt;

&lt;p&gt;Let's analyze the performance characteristics of each GitLab Runner infrastructure type, examining how they handle the execution phases differently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📊 Reading the charts&lt;/strong&gt;: Throughout this article, the durations shown in Gantt diagrams are &lt;strong&gt;relative and illustrative&lt;/strong&gt;—actual times vary based on project size, network conditions, and infrastructure specs. What matters is the &lt;strong&gt;color coding&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;Green&lt;/strong&gt;: Faster than average across runner types&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;Grey&lt;/strong&gt;: Average performance&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;Red&lt;/strong&gt;: Slower than average across runner types&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  GitLab Shared Runners (SaaS)
&lt;/h2&gt;

&lt;p&gt;GitLab.com provides shared runners available to all users without any setup. While convenient, these runners compete for resources with thousands of other projects, making them the slowest option for job execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology&lt;/strong&gt;: Shared SaaS infrastructure - all users share the same pool of runners and cache storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmf2rrq0twowqmh4vyif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmf2rrq0twowqmh4vyif.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance characteristics&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4kiieviuk66m49pabse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4kiieviuk66m49pabse.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No wait for infrastructure provisioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speed disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Slowest overall performance (all resources shared)&lt;/li&gt;
&lt;li&gt;❌ Every job pulls images from scratch&lt;/li&gt;
&lt;li&gt;❌ Shared network bandwidth slows git operations&lt;/li&gt;
&lt;li&gt;❌ Unpredictable performance due to multi-tenancy&lt;/li&gt;
&lt;li&gt;❌ Slow artifact uploads/downloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: When speed is not a priority and zero maintenance is essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Executor (Fixed Cluster)
&lt;/h2&gt;

&lt;p&gt;A fixed-size Kubernetes cluster provides consistent resources for CI jobs. While more complex to set up than shared runners, it offers better performance through image caching and dedicated resources—though still limited by network-based cache and pod startup overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology&lt;/strong&gt;: Fixed-size cluster with shared remote cache (S3/MinIO).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1kd5uvi32jiiimzw9d0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1kd5uvi32jiiimzw9d0.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance characteristics&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5wfd7pu69gmhwk0v31r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5wfd7pu69gmhwk0v31r.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Fast when warm (images cached on nodes)&lt;/li&gt;
&lt;li&gt;✅ Pod creation relatively quick on existing nodes&lt;/li&gt;
&lt;li&gt;✅ Consistent performance with dedicated resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speed disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Jobs queue when capacity is reached&lt;/li&gt;
&lt;li&gt;❌ Remote cache adds network latency&lt;/li&gt;
&lt;li&gt;❌ Pod startup overhead (even on warm nodes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams with existing Kubernetes infrastructure where moderate speed is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Executor (Autoscaling Cluster)
&lt;/h2&gt;

&lt;p&gt;Autoscaling Kubernetes clusters dynamically add nodes when demand increases. This eliminates queuing issues but introduces significant cold-start delays—new nodes take time to provision, and each job starts with a fresh environment requiring full image pulls and git clones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology&lt;/strong&gt;: Cluster with autoscaler that adds/removes nodes based on load, pods distributed dynamically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5we1qwf8pm1nolupbsh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5we1qwf8pm1nolupbsh.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance characteristics&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fxus5k560kwubohxime.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fxus5k560kwubohxime.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No queue time when scaling up&lt;/li&gt;
&lt;li&gt;✅ Dedicated resources per job once running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speed disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Very slow cold starts (node provisioning 30s-2min)&lt;/li&gt;
&lt;li&gt;❌ Full image pulls on new nodes&lt;/li&gt;
&lt;li&gt;❌ Complete git clones on ephemeral pods&lt;/li&gt;
&lt;li&gt;❌ Remote cache network latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Variable workloads where cold-start delays are acceptable trade-offs for unlimited capacity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekimfdfglgydq2ii8nbw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekimfdfglgydq2ii8nbw.jpg" alt="Mechanical racing foxes competing in a cyberpunk race"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Executor (Single Server)
&lt;/h2&gt;

&lt;p&gt;A single server running Docker provides the sweet spot between isolation and performance. Containers start quickly when images are cached, git repositories are reused locally, and cache access is lightning-fast through the local filesystem—all while maintaining job isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology&lt;/strong&gt;: Single server with Docker Engine - container isolation but shared local cache via volumes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrzttdb8mjnra1g1bkqr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrzttdb8mjnra1g1bkqr.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance characteristics&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyig2o1b6hrweglwnbslt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyig2o1b6hrweglwnbslt.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Very fast local cache access (filesystem)&lt;/li&gt;
&lt;li&gt;✅ Image layers cached locally&lt;/li&gt;
&lt;li&gt;✅ Quick container startup when warm&lt;/li&gt;
&lt;li&gt;✅ Local git repository reuse&lt;/li&gt;
&lt;li&gt;✅ No network latency for cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speed disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Jobs queue when server capacity is reached&lt;/li&gt;
&lt;li&gt;❌ Container overhead (minimal but present)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams needing container isolation with near-optimal speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shell Executor (Single Server)
&lt;/h2&gt;

&lt;p&gt;The shell executor is the fastest possible configuration—no containers, no image pulls, no pod scheduling. Everything runs directly on the host system with instant access to local git repos and filesystem cache. The trade-off? No job isolation, requiring trust in your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology&lt;/strong&gt;: Everything is local on a single server - runner, shell execution, and cache share the same filesystem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xyfum1sumbi5g19u2od.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xyfum1sumbi5g19u2od.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance characteristics&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo40urcdf71vu210n75yz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo40urcdf71vu210n75yz.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Absolute fastest&lt;/strong&gt; (zero container overhead)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Instant cache access&lt;/strong&gt; (direct filesystem)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Fastest git operations&lt;/strong&gt; (local clones reused)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No image pulls ever&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Immediate job start&lt;/strong&gt; (no container creation)&lt;/li&gt;
&lt;li&gt;✅ Best optimization potential&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speed disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Jobs queue when server capacity is reached&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Maximum speed when job isolation is not required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Autoscaler (Fleeting)
&lt;/h2&gt;

&lt;p&gt;Docker Autoscaler uses the &lt;a href="https://gitlab.com/gitlab-org/fleeting/fleeting" rel="noopener noreferrer"&gt;Fleeting&lt;/a&gt; plugin system to spawn VMs on cloud providers. It dynamically provisions VMs when demand increases and terminates them when idle, providing unlimited capacity at the cost of cold-start delays.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology&lt;/strong&gt;: Autoscaling VMs - plugin-based architecture supporting multiple cloud providers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15vkzcdfmrkycgt0dbmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15vkzcdfmrkycgt0dbmk.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance characteristics&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc65wdie0ptmifdyrw39h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc65wdie0ptmifdyrw39h.png" alt="Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No queue time (infinite capacity)&lt;/li&gt;
&lt;li&gt;✅ Dedicated resources once VM is ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speed disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Slow cold starts (15+ seconds VM provisioning)&lt;/li&gt;
&lt;li&gt;❌ Full git clone on each VM&lt;/li&gt;
&lt;li&gt;❌ Full image pull on each VM&lt;/li&gt;
&lt;li&gt;❌ Cloud cache network latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Variable workloads requiring autoscaling where cold-start delays are acceptable.&lt;/p&gt;

&lt;h1&gt;
  
  
  Job speed comparison summary
&lt;/h1&gt;

&lt;p&gt;Here's a comprehensive comparison of &lt;strong&gt;job execution speed&lt;/strong&gt; across all execution phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legend&lt;/strong&gt;: 🟢 Fast · ⚪ Medium · 🔴 Slow&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runner Type&lt;/th&gt;
&lt;th&gt;Wait&lt;/th&gt;
&lt;th&gt;VM/Pod&lt;/th&gt;
&lt;th&gt;OS&lt;/th&gt;
&lt;th&gt;Git&lt;/th&gt;
&lt;th&gt;Script&lt;/th&gt;
&lt;th&gt;Cache&lt;/th&gt;
&lt;th&gt;Artifacts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitLab Shared&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;K8S Fixed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;K8S Autoscaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Autoscaler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key insights&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitLab Shared&lt;/strong&gt;: Great for zero maintenance but slowest overall&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;K8S Fixed&lt;/strong&gt;: Balanced approach with good warm performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;K8S Autoscaling&lt;/strong&gt;: Best for variable loads but cold starts are slow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Single Server&lt;/strong&gt;: Fast local operations with isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell Single Server&lt;/strong&gt;: Fastest local operations, best optimization potential&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Autoscaler&lt;/strong&gt;: Maximum scalability but slowest cold starts&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Recommendation: Shell or Docker executors for fastest jobs
&lt;/h1&gt;

&lt;p&gt;After analyzing all runner types, &lt;strong&gt;Shell and Docker executors on single servers offer the fastest job execution&lt;/strong&gt; for most teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why single-server executors win on speed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Local everything = minimal latency&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git repositories cached locally&lt;/li&gt;
&lt;li&gt;Dependencies and cache on local filesystem&lt;/li&gt;
&lt;li&gt;No network round-trips for cache operations&lt;/li&gt;
&lt;li&gt;Instant resource availability (no VM/pod provisioning)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Vertical scaling is underrated&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modern servers can handle dozens of concurrent jobs&lt;/li&gt;
&lt;li&gt;SSD storage makes local caching extremely fast&lt;/li&gt;
&lt;li&gt;RAM caching for frequently accessed data&lt;/li&gt;
&lt;li&gt;CPU cores scale linearly for parallel jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Warm infrastructure advantage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker images already pulled and cached&lt;/li&gt;
&lt;li&gt;Git repos incrementally fetched (not full clones)&lt;/li&gt;
&lt;li&gt;Dependencies preserved between jobs&lt;/li&gt;
&lt;li&gt;No cold-start penalties&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Shell vs Docker trade-offs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose Shell when&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need maximum speed&lt;/li&gt;
&lt;li&gt;Your codebase is trusted&lt;/li&gt;
&lt;li&gt;You can maintain consistent tooling&lt;/li&gt;
&lt;li&gt;Security isolation is less critical&lt;/li&gt;
&lt;li&gt;You want to push performance limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose Docker when&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need job isolation&lt;/li&gt;
&lt;li&gt;Multiple projects with different dependencies&lt;/li&gt;
&lt;li&gt;Security/multi-tenancy matters&lt;/li&gt;
&lt;li&gt;Slightly slower speed is acceptable trade-off&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The performance/price/maintenance sweet spot
&lt;/h2&gt;

&lt;p&gt;📈 &lt;strong&gt;Vertical scalability&lt;/strong&gt; compensates for the lack of horizontal scalability&lt;/p&gt;

&lt;p&gt;💪 A properly sized server with local SSD can handle &lt;strong&gt;dozens of simultaneous jobs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;💰 &lt;strong&gt;Predictable costs&lt;/strong&gt;: No per-job pricing, one server cost&lt;/p&gt;

&lt;p&gt;🔧 &lt;strong&gt;Low maintenance&lt;/strong&gt;: Simple architecture, fewer moving parts&lt;/p&gt;

&lt;p&gt;📖 &lt;strong&gt;Reference&lt;/strong&gt;: &lt;a href="https://dev.to/zenika/gitlab-ci-the-majestic-single-server-runner-1b5b"&gt;GitLab CI: The Majestic Single Server Runner&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Choosing the right GitLab Runner topology depends on your specific needs, but if &lt;strong&gt;minimizing job execution time&lt;/strong&gt; is your primary concern, Shell or Docker executors on well-provisioned single servers consistently deliver the fastest jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared runners&lt;/strong&gt; are great for getting started but have performance limitations due to multi-tenancy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt; solutions offer good isolation and scalability but add network latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-server executors&lt;/strong&gt; (Shell/Docker) provide the fastest local operations and best optimization potential&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autoscaling&lt;/strong&gt; solutions handle variable loads well but suffer from cold-start penalties&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical scaling&lt;/strong&gt; of single servers is often more effective than complex horizontal scaling&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The comparison shows that while autoscaling solutions offer flexibility, a properly configured single-server runner often provides the best performance for teams with predictable workloads or those willing to size for peak capacity.&lt;/p&gt;

&lt;p&gt;In our follow-up article &lt;a href="https://dev.to/zenika/gitlab-ci-achieving-3-second-jobs-on-million-line-codebases-3nlm"&gt;GitLab CI: Achieving 3-Second Jobs on Million-Line Codebases&lt;/a&gt;, we dive deep into extreme optimizations you can apply to Shell and Docker runners to push performance even further—achieving job times as low as 3 seconds on multi-million line codebases!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dl438mt99iin0vn0zlv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dl438mt99iin0vn0zlv.jpg" alt="Mechanical racing fox crossing the finish line"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Draw Things using Flux.1 [Schnell] model&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Further reading
&lt;/h1&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__hidden-navigation-link"&gt;All Articles by Theme&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bcouetil" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" alt="bcouetil profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bcouetil" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Benoit COUETIL 💫
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Benoit COUETIL 💫
                
              
              &lt;div id="story-author-preview-content-3268957" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bcouetil" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Benoit COUETIL 💫&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" id="article-link-3268957"&gt;
          All Articles by Theme
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gitlab"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gitlab&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;This article was enhanced with the assistance of an AI language model to ensure clarity and accuracy in the content, as English is not my native language.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>cicd</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to build and serve custom simplified maps with OpenMapTiles</title>
      <dc:creator>Noan</dc:creator>
      <pubDate>Tue, 13 Jan 2026 08:32:21 +0000</pubDate>
      <link>https://forem.com/zenika/how-to-build-and-serve-custom-simplified-maps-with-openmaptiles-hfn</link>
      <guid>https://forem.com/zenika/how-to-build-and-serve-custom-simplified-maps-with-openmaptiles-hfn</guid>
      <description>&lt;p&gt;If you need to display a map on your website and do not plan to use online mapping services such as MapTiler or Mapbox, you may want to use &lt;a href="https://openmaptiles.org/" rel="noopener noreferrer"&gt;OpenMapTiles&lt;/a&gt; to generate and serve custom tiles. This approach is also useful when you are not interested in all the features a map can display. &lt;br&gt;
In this tutorial, we will create a map featuring only water, such as coastlines, lakes and rivers.&lt;br&gt;
This tutorial was tested on Ubuntu 24.04.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://osmcode.org/osmium-tool/" rel="noopener noreferrer"&gt;Osmium&lt;/a&gt;: a tool used to process OpenStreetMap data.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/openmaptiles/openmaptiles" rel="noopener noreferrer"&gt;OpenMapTiles repository&lt;/a&gt; cloned on your computer.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/install/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;, to run the OpenMapTiles containers.
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;build-essential&lt;/code&gt; package (to run the &lt;code&gt;make&lt;/code&gt; command).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  1. Download the source data
&lt;/h2&gt;

&lt;p&gt;OpenStreetMap provides an &lt;a href="https://planet.openstreetmap.org/" rel="noopener noreferrer"&gt;up-to-date export of the entire planet&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Pay attention to the licenses of the tools and data used. For OpenStreetMap and OpenMapTiles, proper attribution must be displayed on your map, for example: &lt;a href="http://openmaptiles.org/" rel="noopener noreferrer"&gt;© OpenMapTiles&lt;/a&gt; &lt;a href="http://www.openstreetmap.org/copyright" rel="noopener noreferrer"&gt;© OpenStreetMap contributors&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;planet.osm.pbf&lt;/code&gt; file is about 85GB. This is very large and can take hours to process even you are when only interested in the water data. During development, it is recommended to use an extract from &lt;a href="https://download.geofabrik.de/" rel="noopener noreferrer"&gt;GeoFabrik&lt;/a&gt; covering a smaller region that you know well.&lt;br&gt;
In my case, I used the file &lt;a href="https://download.geofabrik.de/europe/france/bretagne.html" rel="noopener noreferrer"&gt;&lt;code&gt;bretagne.osm.pbf&lt;/code&gt;&lt;/a&gt; and then the file &lt;a href="https://download.geofabrik.de/europe/france.html" rel="noopener noreferrer"&gt;&lt;code&gt;france.osm.pbf&lt;/code&gt;&lt;/a&gt;. They can be processed in a few seconds or minutes.  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A &lt;code&gt;.osm.pbf&lt;/code&gt; file is a compressed format for OpenStreetMap data, if you decompress it with the command &lt;code&gt;osmium cat &amp;lt;your-region&amp;gt;.osm.pbf --output &amp;lt;your-region&amp;gt;.osm&lt;/code&gt; you will find that the &lt;code&gt;.osm&lt;/code&gt; file is an XML file listing nodes, ways, and polygons. The documentation for this format is available on the &lt;a href="//wiki.openstreetmap.org/wiki/OSM_XML"&gt;OpenStreetMap wiki&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  2. Extract the relevant data
&lt;/h2&gt;

&lt;p&gt;Since we are only interested in water data, the process can be sped up by reducing the input file size. Let's filter out everything that is not related to water:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osmium tags-filter &amp;lt;your-region&amp;gt;.osm.pbf  rw/waterway rw/water &lt;span class="nt"&gt;-o&lt;/span&gt; water.osm.pbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Generate the vector tiles
&lt;/h2&gt;

&lt;p&gt;At the root of the OpenMapTiles repository, edit the file &lt;code&gt;openmaptiles.yaml&lt;/code&gt; to comment out all layers except those related to water:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tileset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;layers/water/water.yaml&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;layers/waterway/waterway.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# All other layers are commented out&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/landcover/landcover.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/landuse/landuse.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/mountain_peak/mountain_peak.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/park/park.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/boundary/boundary.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/aeroway/aeroway.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/transportation/transportation.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/building/building.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/water_name/water_name.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/transportation_name/transportation_name.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/place/place.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/housenumber/housenumber.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/poi/poi.yaml&lt;/span&gt;
    &lt;span class="c1"&gt;# - layers/aerodrome_label/aerodrome_label.yaml&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OpenMapTiles&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.15.0&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openmaptiles&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tileset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;showcasing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;layers&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OpenMapTiles.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://openmaptiles.org"&lt;/span&gt;
  &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, edit the &lt;code&gt;.env&lt;/code&gt; file to specify the zoom levels to generate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;...
&lt;span class="c"&gt;# Which zooms to generate with make generate-tiles-pg
&lt;/span&gt;&lt;span class="n"&gt;MIN_ZOOM&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;MAX_ZOOM&lt;/span&gt;=&lt;span class="m"&gt;10&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move the &lt;code&gt;water.osm.pbf&lt;/code&gt; you generated earlier into the &lt;code&gt;data/&lt;/code&gt; directory&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;openmaptiles/data/
&lt;span class="nb"&gt;mv &lt;/span&gt;water.osm.pbf openmaptiles/data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run OpenMapTiles to generate the tiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;openmaptiles
./quickstart.sh water
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this process will: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Populate a PostGIS database with map features. Note that the &lt;a href="https://hub.docker.com/r/openmaptiles/postgis-preloaded" rel="noopener noreferrer"&gt;database&lt;/a&gt; already contains &lt;a href="https://www.naturalearthdata.com/" rel="noopener noreferrer"&gt;Natural Earth&lt;/a&gt; data for low zoom levels. At low zoom levels, coastlines in the resulting map will come frome Natural Earth data.&lt;/li&gt;
&lt;li&gt;Create views and functions in this database responsible for querying each layer for each zoom level.&lt;/li&gt;
&lt;li&gt;Query the PostGIS database to create a file named &lt;code&gt;tiles.mbtiles&lt;/code&gt;.  It is a SQLite database containing vector tile data for each &lt;a href="https://dev.to/geoapify-maps-api/understanding-map-zoom-levels-and-xyz-tile-coordinates-55da"&gt;(x, y, z) coordinate&lt;/a&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Serve the tiles
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make start-tileserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And visit &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yf6q4vacqcvsmlbjb1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yf6q4vacqcvsmlbjb1t.png" alt="Preview of the water map on localhost:8080" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or if you want to serve the tiles on a server without the whole OpenMapTiles project&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Make sure you have Node.js version 18.17.0 or later installed&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tileserver-gl
tileserver-gl &lt;span class="nt"&gt;--file&lt;/span&gt; tiles.mbtiles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we have introduced techniques for generating simple vector maps: where to retrieve the data, how it can be filtered with Osmium, how OpenMapTiles processes it to generate vector tiles, and finally how tileserver-gl can serve them (although &lt;a href="https://tileserver.readthedocs.io/en/latest/deployment.html" rel="noopener noreferrer"&gt;it is not  production-ready&lt;/a&gt; on its own).&lt;br&gt;&lt;br&gt;
From here, you may then want to style your map with &lt;a href="https://maputnik.github.io/" rel="noopener noreferrer"&gt;Maputnik&lt;/a&gt;, explore the user interactions provided by &lt;a href="https://maplibre.org/" rel="noopener noreferrer"&gt;MapLibre&lt;/a&gt;, or dig deeper into the &lt;a href="https://github.com/openmaptiles/openmaptiles/tree/master/layers" rel="noopener noreferrer"&gt;transformations&lt;/a&gt; performed by OpenMapTiles for each layer.&lt;/p&gt;

</description>
      <category>map</category>
      <category>openmaptiles</category>
    </item>
    <item>
      <title>🔍 Every Developer Should Review Code — Not Just Seniors</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Sat, 03 Jan 2026 15:37:07 +0000</pubDate>
      <link>https://forem.com/zenika/every-developer-should-review-code-not-just-seniors-2abc</link>
      <guid>https://forem.com/zenika/every-developer-should-review-code-not-just-seniors-2abc</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;1. Reviewing accelerates learning and skill development&lt;/li&gt;
&lt;li&gt;2. Junior developers provide unique value and grow fastest&lt;/li&gt;
&lt;li&gt;3. Reviewing enhances your own code quality and habits&lt;/li&gt;
&lt;li&gt;4. It fosters system-wide understanding and reduces silos&lt;/li&gt;
&lt;li&gt;5. Universal review enforces standards, consistency, and security&lt;/li&gt;
&lt;li&gt;6. More reviewers distribute workload, reducing bottlenecks and accelerating delivery&lt;/li&gt;
&lt;li&gt;7. It builds psychological safety, team cohesion, and positive culture&lt;/li&gt;
&lt;li&gt;8. Code reviews make for better estimates&lt;/li&gt;
&lt;li&gt;9. Google's Review Culture: A Proven Model for Excellence&lt;/li&gt;
&lt;li&gt;10. "But we don't have time for everyone to review" — Common objections answered&lt;/li&gt;
&lt;li&gt;How to Review Effectively at Any Level&lt;/li&gt;
&lt;li&gt;Making Universal Review Work: Practical Implementation&lt;/li&gt;
&lt;li&gt;Wrapping up&lt;/li&gt;
&lt;li&gt;Further reading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code review shouldn't be reserved for a handful of senior developers. &lt;strong&gt;Every team member — regardless of experience level — must participate in reviewing code.&lt;/strong&gt; Junior developers bring fresh perspectives that catch issues seniors miss, while simultaneously accelerating their own learning. Universal review distributes knowledge, prevents bottlenecks, and transforms code quality from a gate into a shared team responsibility.&lt;/p&gt;

&lt;h1&gt;
  
  
  Initial thoughts
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Picture this:&lt;/strong&gt; A team of 10 developers where only 2–3 senior engineers handle all code reviews. Those seniors become overwhelmed gatekeepers, review queues grow longer, and junior developers submit code without ever learning to critically evaluate others' work. Sound familiar? Welcome to the review bottleneck club.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mandatory code review&lt;/strong&gt; consistently separates high-performing engineering teams from the rest. This approach mirrors the rigorous review culture that propelled Google to become one of the world's leading software providers in its early days.&lt;/p&gt;

&lt;p&gt;Codacy.com 2024 article &lt;a href="https://blog.codacy.com/impact-of-code-reviews-on-developer-productivity-and-code-quality-a-quantitative-assessment" rel="noopener noreferrer"&gt;Code review process: how to improve developer productivity&lt;/a&gt; shows dominance of code review on code quality:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7mpaij1llaenvo8f5cj3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7mpaij1llaenvo8f5cj3.png" alt="Bar chart showing code review as having the biggest impact on code quality compared to other development process changes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But code review is too often perceived as a quality gate run by seniors to catch mistakes. &lt;strong&gt;This is fundamentally wrong.&lt;/strong&gt; In reality, the primary beneficiary of reviewing someone else's code is the reviewer themselves — and the entire team benefits from the shared knowledge and improved practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core message:&lt;/strong&gt; Every developer should review code. Not just approve it with a quick scan, but genuinely engage with it, ask questions, and provide feedback. Here's why.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Reviewing accelerates learning and skill development
&lt;/h1&gt;

&lt;p&gt;Reading production code written by others exposes patterns, architectural decisions, edge-case handling, and new techniques at a rate no amount of Stack Overflow browsing can match.&lt;/p&gt;

&lt;p&gt;A developer who only writes code sees one mental model. A developer who reviews 5–10 merge requests per week sees dozens of approaches, trade-offs, and real-world solutions every sprint. This bidirectional mentoring — juniors learning from seniors, and vice versa — grows skills across the team.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Junior developers provide unique value and grow fastest
&lt;/h1&gt;

&lt;p&gt;The objection &lt;strong&gt;"I'm too junior to review"&lt;/strong&gt; is consistently proven wrong in practice.&lt;/p&gt;

&lt;p&gt;Engineers new to a codebase are uniquely positioned to detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous naming&lt;/strong&gt; (they still have to think about what something means)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing documentation or comments&lt;/strong&gt; (context that seniors assume is obvious)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overly clever constructs&lt;/strong&gt; that violate the principle of least astonishment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inconsistent style&lt;/strong&gt; that seniors have become blind to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fresh eyes often catch security vulnerabilities, performance issues, or logic errors overlooked due to familiarity. Meanwhile, reviewing senior code is one of the most powerful learning tools for juniors — they see production-quality patterns in context, not just in documentation.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Reviewing enhances your own code quality and habits
&lt;/h1&gt;

&lt;p&gt;Developers who regularly review begin anticipating reviewer questions before submitting changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commit messages become descriptive&lt;/li&gt;
&lt;li&gt;Changes are split into logical, reviewable chunks&lt;/li&gt;
&lt;li&gt;Tests are written upfront&lt;/li&gt;
&lt;li&gt;Public APIs receive extra care&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "Hawthorne effect" kicks in: knowing your code will be reviewed magically makes you write it better the first time. Over time, this feedback loop is one of the most reliable skill accelerators observed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdk6qrdcxns4bvdt1mqj8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdk6qrdcxns4bvdt1mqj8.jpg" alt="colored illustration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  4. It fosters system-wide understanding and reduces silos
&lt;/h1&gt;

&lt;p&gt;When every engineer reviews code across the codebase, knowledge silos disappear.&lt;/p&gt;

&lt;p&gt;Team members gain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insight into why certain retry policies or architectural choices exist&lt;/li&gt;
&lt;li&gt;Understanding of how authentication, authorization, and critical paths actually work&lt;/li&gt;
&lt;li&gt;Familiarity with the full system, reducing the "bus factor"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shared context is essential for resilience, refactoring, fast incident response, better estimates, and true team flexibility. When knowledge is distributed, team members can take time off without the project grinding to a halt — no single developer holds the codebase hostage.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Universal review enforces standards, consistency, and security
&lt;/h1&gt;

&lt;p&gt;Reviews ensure adherence to coding standards, patterns, and best practices, unifying the codebase and preventing style drift.&lt;/p&gt;

&lt;p&gt;They catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security vulnerabilities (e.g., input validation failures or duplicated code risks)&lt;/li&gt;
&lt;li&gt;Readability and maintainability issues&lt;/li&gt;
&lt;li&gt;Opportunities for design improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This early-cycle quality control promotes secure development and a maintainable codebase.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. More reviewers distribute workload, reducing bottlenecks and accelerating delivery
&lt;/h1&gt;

&lt;p&gt;The higher the number of reviewers involved, the faster code reviews tend to complete. Distributing review requests across the team minimizes bottlenecks — someone available can pick up the review promptly.&lt;/p&gt;

&lt;p&gt;This leads to quicker merges, faster time to market, and improved overall team velocity — no more waiting three days for a single thumbs-up.&lt;/p&gt;

&lt;h1&gt;
  
  
  7. It builds psychological safety, team cohesion, and positive culture
&lt;/h1&gt;

&lt;p&gt;When everyone both gives and receives feedback regularly, code review stops feeling personal.&lt;/p&gt;

&lt;p&gt;Teams that practice universal review report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer surprises in production&lt;/li&gt;
&lt;li&gt;Faster decision making&lt;/li&gt;
&lt;li&gt;Higher engineering satisfaction and motivation&lt;/li&gt;
&lt;li&gt;Stronger bonds through constructive, positive feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  8. Code reviews make for better estimates
&lt;/h1&gt;

&lt;p&gt;For teams performing estimation, this is a team exercise, and the team makes better estimates as product knowledge is spread across the team.&lt;/p&gt;

&lt;p&gt;As new features are added to the existing code, the original developer can provide good feedback and estimation. In addition, any code reviewer is also exposed to the complexity, known issues, and concerns of that area of the code base.&lt;/p&gt;

&lt;p&gt;The code reviewer, then, shares in the knowledge of the original developer of that part of the code base. This practice creates multiple, informed inputs which, when used for a final estimate, always make that estimate stronger and more reliable.&lt;/p&gt;

&lt;h1&gt;
  
  
  9. Google's Review Culture: A Proven Model for Excellence
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://google.github.io/eng-practices/review/" rel="noopener noreferrer"&gt;Google's early dominance as a software provider&lt;/a&gt; was deeply rooted in its mandatory pre-commit code review process. Every change required a "Looks Good To Me" (LGTM) approval from a qualified engineer before landing.&lt;/p&gt;

&lt;p&gt;This culture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caught bugs early, preventing production incidents&lt;/li&gt;
&lt;li&gt;Maintained exceptionally high code quality standards&lt;/li&gt;
&lt;li&gt;Avoided the "broken windows" effect&lt;/li&gt;
&lt;li&gt;Promoted openness, teamwork, and security awareness&lt;/li&gt;
&lt;li&gt;Enabled organic knowledge transfer and rapid onboarding&lt;/li&gt;
&lt;li&gt;Ensured consistency across a rapidly growing codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These factors produced scalable, reliable products (Search, Maps, Gmail) and laid the foundation for Google's engineering reputation that still endures today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0evlkpnlxty1fvkl7qc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0evlkpnlxty1fvkl7qc.jpg" alt="colored illustration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  10. "But we don't have time for everyone to review" — Common objections answered
&lt;/h1&gt;

&lt;p&gt;Let's address the elephant in the room — the one wearing a "too busy" t-shirt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objection:&lt;/strong&gt; "Junior reviews slow us down"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reality:&lt;/strong&gt; A 10–15 minute review prevents hours or days of incident response, debugging, and rework. When only 2–3 seniors on a 10-person team handle all reviews, those seniors experience overload and burnout, review queues grow, and defect leakage increases due to rushed or fatigued reviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objection:&lt;/strong&gt; "Juniors don't catch the important stuff"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reality:&lt;/strong&gt; Juniors catch different things — ambiguous naming, missing docs, inconsistent patterns. These "surface" issues are exactly what cause maintenance headaches down the road. Plus, they're learning while reviewing, making them better reviewers over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objection:&lt;/strong&gt; "We can't afford the time investment"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reality:&lt;/strong&gt; Investing just 5% of development time in review typically &lt;a href="https://www.atlassian.com/agile/software-development/code-reviews" rel="noopener noreferrer"&gt;reduces bug-related work by 30–70%&lt;/a&gt; (Google, Microsoft, Atlassian benchmarks). Distributing the load prevents bottlenecks and saves far more time overall.&lt;/p&gt;

&lt;p&gt;According to Codacy's 2024 study on &lt;a href="https://blog.codacy.com/impact-of-code-reviews-on-developer-productivity-and-code-quality-a-quantitative-assessment" rel="noopener noreferrer"&gt;code review productivity&lt;/a&gt;, teams practicing code review see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10% reduction&lt;/strong&gt; in time fixing bugs (from 35% to 25% of dev time)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10% improvement&lt;/strong&gt; in time building new features (from 45% to 55%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17% reduction&lt;/strong&gt; in perceived lack of maintenance time (from 76% to 59%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The math is simple: you can't afford NOT to have everyone reviewing.&lt;/p&gt;
&lt;h1&gt;
  
  
  How to Review Effectively at Any Level
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;For everyone:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with what you understand confidently&lt;/strong&gt; — naming, tests, documentation, security basics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask questions rather than dictate solutions&lt;/strong&gt; — "Why did you choose X?" beats "Change this to Y"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep comments focused&lt;/strong&gt; — one clear point beats a wall of text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target high-impact areas&lt;/strong&gt; — correctness, design, complexity, standards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember the goal&lt;/strong&gt; — improve the codebase, help the author, and learn&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Use Conventional Comments for clarity:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://conventionalcomments.org/" rel="noopener noreferrer"&gt;Conventional Comments&lt;/a&gt; prefix feedback with labels that clarify intent, making reviews more constructive and less ambiguous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;praise:&lt;/strong&gt; Highlight excellent work ("praise: Great error handling here!")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nitpick:&lt;/strong&gt; Minor style suggestions ("nitpick: Consider renaming &lt;code&gt;tmp&lt;/code&gt; to &lt;code&gt;tempResult&lt;/code&gt;")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;suggestion:&lt;/strong&gt; Propose improvements ("suggestion: We could extract this into a helper function")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;issue:&lt;/strong&gt; Flag problems that must be addressed ("issue: This will fail when input is null")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;question:&lt;/strong&gt; Ask for clarification ("question: Why are we using a timeout here?")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;thought:&lt;/strong&gt; Share considerations without requiring action ("thought: This might impact performance at scale")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach helps juniors understand which comments require action vs. are optional, reduces defensive reactions, and creates consistent review language across the team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For junior reviewers specifically:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus on readability: "I don't understand what this function does"&lt;/li&gt;
&lt;li&gt;Check test coverage: "Is there a test for the error case?"&lt;/li&gt;
&lt;li&gt;Verify documentation: "Could you add a comment explaining why we need this?"&lt;/li&gt;
&lt;li&gt;Question assumptions: "What happens if this API call fails?"&lt;/li&gt;
&lt;li&gt;Don't apologize for asking questions — they're valuable contributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For seniors reviewing juniors' reviews:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Praise good catches publicly&lt;/li&gt;
&lt;li&gt;Provide context when juniors miss architectural issues&lt;/li&gt;
&lt;li&gt;Model constructive feedback tone&lt;/li&gt;
&lt;li&gt;Never dismiss or override junior comments without explanation&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Making Universal Review Work: Practical Implementation
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;1. Make it official policy&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Explicitly state that everyone reviews code. Add it to onboarding documentation and team agreements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Rotate reviewers&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Avoid always pairing the same people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Set clear expectations&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviews should happen within &lt;a href="https://engineering.fb.com/2022/11/16/culture/meta-code-review-time-improving/" rel="noopener noreferrer"&gt;4–24 hours&lt;/a&gt; (depending on your workflow)&lt;/li&gt;
&lt;li&gt;At least one approval required to merge, ideally from someone who didn't write the code&lt;/li&gt;
&lt;li&gt;Block merging without review (enforce via branch protection rules)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Create review rituals&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily "review hour" where the team focuses on pending reviews&lt;/li&gt;
&lt;li&gt;Celebrate good reviews in standups or retrospectives&lt;/li&gt;
&lt;li&gt;Track and visualize review participation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Provide training&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run workshops on &lt;a href="https://www.swarmia.com/blog/a-complete-guide-to-code-reviews/" rel="noopener noreferrer"&gt;effective code review&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Share examples of good review comments&lt;/li&gt;
&lt;li&gt;Create a review guidelines document specific to your codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Lead by example&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Seniors must model the behavior: review promptly, give constructive feedback, accept feedback graciously, and encourage junior participation.&lt;/p&gt;
&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Universal code review is not a gate. It is one of the highest-ROI engineering practices available.&lt;/p&gt;

&lt;p&gt;Every developer — regardless of tenure — must participate. The team's velocity, quality, security, and individual growth depend on it.&lt;/p&gt;

&lt;p&gt;The question isn't "Can we afford to have everyone review?" It's "Can we afford not to?"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc865wbz97tcoiwt9fnri.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc865wbz97tcoiwt9fnri.jpg" alt="colored illustration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Draw Things using Flux.1 [Schnell] model&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Further reading
&lt;/h1&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__hidden-navigation-link"&gt;All Articles by Theme&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bcouetil" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" alt="bcouetil profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bcouetil" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Benoit COUETIL 💫
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Benoit COUETIL 💫
                
              
              &lt;div id="story-author-preview-content-3268957" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bcouetil" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Benoit COUETIL 💫&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" id="article-link-3268957"&gt;
          All Articles by Theme
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gitlab"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gitlab&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;This article was enhanced with the assistance of an AI language model to ensure clarity and accuracy in the content, as English is not my native language.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>Google Antigravity : l'ère des IDE Agentiques</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Thu, 18 Dec 2025 08:52:21 +0000</pubDate>
      <link>https://forem.com/zenika/google-antigravity-lere-des-ide-agentique-109i</link>
      <guid>https://forem.com/zenika/google-antigravity-lere-des-ide-agentique-109i</guid>
      <description>&lt;p&gt;Si la “Developer eXperience” (DX) est en perpétuelle amélioration, l'intelligence artificielle générative bouscule actuellement les paradigmes de développement. Des outils comme Firebase Studio (cf &lt;a href="https://firebase.blog/posts/2025/04/introducing-firebase-studio/" rel="noopener noreferrer"&gt;annonce faite par Google&lt;/a&gt;) ont déjà introduit la puissance de l'IA dans les &lt;strong&gt;&lt;a href="https://docs.google.com/presentation/d/e/2PACX-1vQDLXqF7tlITWWYsYOj5nbV3YQR_drMSbfzT3z2M_FmVhCkf7pz56I_5mpP187iZPKoiXRjMuwCsv3G/pub?slide=id.g32959efb8b4_0_2472" rel="noopener noreferrer"&gt;Cloud Development Environment&lt;/a&gt;&lt;/strong&gt; (CDE) pour assister les développeurs·euses dans la création d'interfaces, la génération de code et l'intégration de services.&lt;/p&gt;

&lt;p&gt;Mais Google a déjà préparé l’étape suivante : l’arrivée des agents dans le monde du développement avec &lt;strong&gt;Google Antigravity&lt;/strong&gt;. Lancée en novembre 2025, cette nouvelle plateforme vise à redéfinir la façon dont nous interagissons avec les agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premiers pas avec Antigravity : Installation et Configuration
&lt;/h2&gt;

&lt;p&gt;Google Antigravity est une nouvelle &lt;strong&gt;plateforme de développement « agentique ».&lt;/strong&gt; La promesse : l’IA ne se contente plus de suggérer des lignes de code, mais elle prend le clavier, pilote l’éditeur, le terminal et le navigateur.&lt;/p&gt;

&lt;p&gt;Antigravity s'appuie principalement sur les modèles Gemini. Toutefois, la plateforme est construite pour la flexibilité des modèles incluant le support pour Claude Sonnet 4.5 et Claude Opus 4.5 d'Anthropic ainsi queGPT-OSS d'OpenAI.&lt;/p&gt;

&lt;p&gt;L’installation d’Antigravity s’effectue rapidement à partir d’un package téléchargeable sur votre machine (Windows, Mac ou Linux) : &lt;a href="https://antigravity.google/download" rel="noopener noreferrer"&gt;https://antigravity.google/download&lt;/a&gt;. Actuellement en preview, Antigravity est accessible publiquement avec une limite dans les quotas d’utilisation des modèles (cf &lt;a href="https://antigravity.google/pricing" rel="noopener noreferrer"&gt;https://antigravity.google/pricing&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Une fois installé, une courte phase de configuration permet de définir le comportement des agents avec quelques options. Antigravity vous propose 4 modes de fonctionnement prédéfinis : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Développement piloté par agent (Agent-driven development)&lt;/li&gt;
&lt;li&gt;Développement assisté par agent (Agent-assisted development)&lt;/li&gt;
&lt;li&gt;Développement piloté par les avis (Review-driven development)&lt;/li&gt;
&lt;li&gt;Configuration personnalisée (Custom configuration)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ces profils sont des préréglages des options présentes sur la partie droite de l’écran, que vous pouvez ajuster manuellement si elles ne vous conviennent pas. Vous pourrez également les modifier par la suite à tout moment dans les préférences d’Antigravity, avec la possibilité d’ajouter des commandes autorisées ou interdites par les agents.  &lt;/p&gt;

&lt;p&gt;Ces options consistent à configurer l’autonomie des agents dans l’exécution, avec ou sans votre validation humaine, de commandes dans le terminal, de commandes Javascript ou pour valider les revues. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F1-configuration.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F1-configuration.png" alt="Antigravity, configuration" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  La Rupture : L'IDE Agentique vs. L'Assistance Classique
&lt;/h2&gt;

&lt;p&gt;Avec les IDEs classiques, l'IA restait limitée à des rôles d'assistance, comme la complétion de lignes, des suggestions de changement dans le code, ou les réponses dans un chat latéral. Les développeurs·euses devaient attendre que l'IA ait fini de générer du code avant de poser la question suivante. L’approche agentique change la donne. Désormais les agents intègrent leurs modifications dans nos fichiers avec ou sans notre approbation.&lt;/p&gt;

&lt;p&gt;Avec Antigravity, la logique est celle de la délégation : la mission est confiée, l’IA effectue la majeure partie du travail. L’IA n'est plus seulement un outil d'écriture de code, mais un acteur autonome capable de planifier, d'exécuter, de valider et d'itérer sur des tâches complexes&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;Caractéristiques&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;IDEs Classiques&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;ADE (Google Antigravity)&lt;/strong&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;Objectif&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;Aider à écrire le code plus rapidement (complétion, snippets)
   &lt;/td&gt;
   &lt;td&gt;Orchestrer et exécuter des tâches complexes (délégation)
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;Mode de Travail&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;Synchrone, linéaire (le développeur/développeuse doit attendre la génération)
   &lt;/td&gt;
   &lt;td&gt;Agent-first, asynchrone, parallélisation (plusieurs agents travaillent simultanément)
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;Rôle de l'IA&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;Assistant, chat latéral, outil d'écriture de code
   &lt;/td&gt;
   &lt;td&gt;Agent autonome qui pilote l'éditeur, le terminal et le navigateur
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;Validation&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;Implicite (le développeur vérifie le code généré)
   &lt;/td&gt;
   &lt;td&gt;Explicite par production d'artefacts (screenshots, vidéos, etc)
   &lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Google Antigravity : IDE Agent-First
&lt;/h2&gt;

&lt;p&gt;Antigravity articule son expérience utilisateur autour de deux vues principales distinctes. En plus d’avoir sa &lt;strong&gt;vue de développement&lt;/strong&gt; comme tout IDE, la nouveauté réside dans l’apparition d’un écran de supervision des interactions avec les agents, l’&lt;strong&gt;Agent Manager&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Le Gestionnaire d'Agents (Agent Manager)
&lt;/h3&gt;

&lt;p&gt;Le Gestionnaire d’Agents (ou Agent Manager) est un tableau de bord où le·la développeur·euse agit en tant qu'architecte. Il peut :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Déléguer, orchestrer et surveiller plusieurs agents travaillant de manière asynchrone sur différentes tâches (exemples : refactoriser le module d'authentification, mettre à jour l'arborescence des dépendances, générer une suite de tests pour une API).&lt;/li&gt;
&lt;li&gt;Visualiser l'état de chaque agent, les artefacts produits et les demandes d'approbation humaine en attente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cette architecture permet à un développeur de déléguer jusqu’à cinq agents différents pour travailler sur cinq bugs différents simultanément.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F2-agent-manager.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F2-agent-manager.png" alt="Agent Manager" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Les artefacts
&lt;/h3&gt;

&lt;p&gt;Déléguer des tâches complexes nécessite une certaine confiance envers l’agent ainsi que la nécessité de vérifier facilement le travail produit. Antigravity résout ce problème en exigeant des agents qu'ils génèrent des “&lt;strong&gt;artefacts&lt;/strong&gt;“.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F3-artefacts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F3-artefacts.png" alt="Antigravity artefacts" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Un artefact peut être :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un plan d’implémentation détaillé.&lt;/li&gt;
&lt;li&gt;Les “diffs” de code standardisés montrant les lignes exactes qui seront modifiées.&lt;/li&gt;
&lt;li&gt;Des captures d'écran de l'UI (avant et après une modification).&lt;/li&gt;
&lt;li&gt;Des enregistrements d’écran de navigateur pour vérifier que les exigences fonctionnelles sont respectées lors des interactions.&lt;/li&gt;
&lt;li&gt;Des journaux structurés des résultats de tests (réussis/échoués).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ces artefacts sont interactifs : si un élément semble incorrect, il est possible de laisser un commentaire directement sur l'artefact et l'agent intégrera ce retour pour itérer sans interrompre son flux d'exécution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F4-artefacts-interactions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F4-artefacts-interactions.png" alt="Antigravity workflow" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Un navigateur embarqué pour les tests automatisés
&lt;/h3&gt;

&lt;p&gt;Un élément différenciateur est la capacité des agents à lancer un navigateur embarqué, Google Chrome, pour interagir avec des pages Web. Ces agents ont accès à une variété d'outils leur permettant de cliquer, faire défiler, taper, lire les logs de la console et même prendre des vidéos de leurs actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F5-browser.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F5-browser.png" alt="Browser" width="800" height="1256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cette capacité permet aux agents de démarrer l’application localement, d’ouvrir un serveur et d'exécuter un scénario utilisateur complet (comme tester une nouvelle fonctionnalité ou reproduire un bug). L'utilisateur·trice est notifié·e lorsque l'agent prend le contrôle, souvent par une bordure bleue visible autour de la fenêtre du navigateur. L'enregistrement de toutes les actions est disponible dans les artefacts, permettant de vérifier la logique fonctionnelle d'un simple coup d'œil.&lt;/p&gt;

&lt;p&gt;Lors de la première configuration d’Antigravity et de son browser, une fenêtre Google Chrome s’ouvre vous demandant d’installer l’extension “Antigravity Browser Extension” disponible ici &lt;a href="https://chromewebstore.google.com/detail/antigravity-browser-exten/eeijfnjmjelapkebgockoeaadonbchdd" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/antigravity-browser-exten/eeijfnjmjelapkebgockoeaadonbchdd&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F6-extension-.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F6-extension-.png" alt="Extensions" width="800" height="65"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Un changement d’état d’esprit
&lt;/h2&gt;

&lt;p&gt;Au-delà de la nouveauté technique, Antigravity amène un changement de paradigme dans les équipes de développement.&lt;/p&gt;

&lt;p&gt;Habituellement concentré sur le développement d’une fonctionnalité ou de la résolution d’un incident, l’aspect agentique permet de déléguer aux agents plusieurs tâches en parallèle.&lt;/p&gt;

&lt;p&gt;La partie &lt;strong&gt;« Inbox »&lt;/strong&gt; centralise le suivi des agents et de leurs discussions. Les développeurs•euses doivent s’habituer à regarder cette « nouvelle boîte de réception » pour observer les notifications informant de la fin de traitement d’un agent et contrôler le résultat des traitements des autres agents. Cela amène un important « context switching » qui peut être nouveau et déstabilisant. Une gymnastique devra être à appréhender pour orchestrer ces tâches exécutées en arrière-plan tout en étant concentré sur l’architecture et les éléments de développement sur lesquels nous voulons garder la main.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules, workflow et customisation
&lt;/h2&gt;

&lt;p&gt;Antigravity est personnalisable grâce aux “&lt;strong&gt;Rules&lt;/strong&gt;” et “&lt;strong&gt;Workflows&lt;/strong&gt;” qui permettent d'adapter le comportement des agents à vos habitudes de travail. Les “&lt;strong&gt;Rules&lt;/strong&gt;” guident le comportement des agents avec des consignes que vous pouvez leur donner. Ces règles peuvent être enregistrées dans le répertoire &lt;code&gt;.agent/rules/&lt;/code&gt;de votre projet ou dans le fichier &lt;code&gt;~/.gemini/GEMINI.md&lt;/code&gt; et précisent vos attentes en termes de code, de tests ou l’utilisation de librairies sans avoir à les répéter dans chaque conversation. Par exemple, vous pouvez définir une règle spécifiant que tous les composants React doivent être fonctionnels ou bien que le handler de l'API HTTP ne doit jamais appeler la couche ORM directement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F7-custom-rules.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F7-custom-rules.png" alt="Custom rules" width="598" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les “&lt;strong&gt;workflows&lt;/strong&gt;” sont quant à eux des commandes personnalisées que vous utilisez fréquemment et qui pourront être exécutés rapidement. Ces commandes peuvent être enregistrées dans le répertoire &lt;code&gt;.agent/workflows/&lt;/code&gt; de votre projet ou bien à la racine de votre compte utilisateur dans le fichier &lt;code&gt;~/.gemini/antigravity/global_workflows/global-workflow.md.&lt;/code&gt;  Un exemple de workflow pourrait être de demander de générer des tests unitaires sur le code ajouté sur cette branche de développement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F8-custom-workflow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F8-custom-workflow.png" alt="Custom workflow" width="684" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F9-workflow-rules.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F9-workflow-rules.png" alt="Workflows rules" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les Model Context Protocol (MCP), utilisés dans les assistants IA pour faciliter les liens avec des services tiers, sont intégrables dans Antigravity. Une liste de MCP pré-configurée vous permet de rapidement en ajouter à votre projet. Les MCP absents de cette liste peuvent également être ajouté en éditant (ou créant) le fichier &lt;code&gt;mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F10-mcp-store.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F10-mcp-store.png" alt="MCP Store" width="730" height="766"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Une fois configuré, vous avez la main sur l’activation ou désactivation des services des MCP configurés.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F11-mcp-example.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F11-mcp-example.png" alt="MCP integration example" width="800" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F12-mcp-config.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fantigravity%2F12-mcp-config.png" alt="MCP configuration" width="800" height="729"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Complément ou futur de Firebase Studio ?
&lt;/h2&gt;

&lt;p&gt;Antigravity n’est pas le seul à proposer une plateforme agentique pour développer des applications. &lt;a href="https://cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; est une alternative qui propose un contexte équivalent et qui est assez connue et répandue. &lt;a href="https://about.gitlab.com/gitlab-duo/agent-platform/" rel="noopener noreferrer"&gt;GitLab Duo&lt;/a&gt; propose également une solution de développement agentique qui va cependant plus loin en intégrant des agents dans toute sa plateforme DevOps. &lt;/p&gt;

&lt;p&gt;Dans de précédentes conférences, nous soulignions les fonctionnalités d'IA de Firebase Studio, notamment dans le prototypage et l'intégration cloud. Avec l'arrivée de la plateforme Antigravity, qui introduit un changement de paradigme vers le développement « agentique », on peut légitimement se demander si l'approche d'assistance de Firebase Studio est remise en cause.&lt;/p&gt;

&lt;p&gt;L'atout principal de &lt;strong&gt;Firebase Studio&lt;/strong&gt; réside dans la rapidité de réalisation de prototypages transformant une simple intention en un plan d’action avec des guidelines graphiques, un déploiement simple et rapide sur les services Google Cloud et le tout depuis votre navigateur. L'exécution n'a pas lieu sur votre ordinateur mais sur la plateforme Cloud de Google (GCP).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Antigravity&lt;/strong&gt;, quant-à lui, exécute votre projet en local et l'IA n'est plus un assistant, mais un acteur autonome capable de piloter l'éditeur, le terminal et un navigateur embarqué. Sa force réside dans la délégation de tâches complexes de bout en bout, comme le refactoring d'un module ou la génération d'une suite de tests complète. Avec son navigateur, Antigravity peut tester lui-même son code et générer un compte rendu de ses actions. Son gestionnaire d’agents permet l'orchestration asynchrone de plusieurs agents travaillant en parallèle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase Studio&lt;/strong&gt; est l'outil idéal pour l'entrée en matière et le déploiement rapide d'un PoC dans le cloud, tandis qu'&lt;strong&gt;Antigravity&lt;/strong&gt; prend le relais pour du travail complexe en arrière-plan.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Article écrit avec &lt;a href="https://www.linkedin.com/in/bourgeoisbenjamin" rel="noopener noreferrer"&gt;Benjamin Bourgeois&lt;/a&gt; 🙌&lt;/em&gt;&lt;/p&gt;

</description>
      <category>google</category>
      <category>ai</category>
      <category>antigravity</category>
      <category>development</category>
    </item>
    <item>
      <title>ADK, Gemini &amp; Javelit = 😍</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Mon, 15 Dec 2025 08:37:47 +0000</pubDate>
      <link>https://forem.com/zenika/adk-gemini-javelit--i05</link>
      <guid>https://forem.com/zenika/adk-gemini-javelit--i05</guid>
      <description>&lt;p&gt;Tandis que les épisodes de notre podcast &lt;strong&gt;Zenikast&lt;/strong&gt; se succèdent, la nécessité de produire la transcription de chaque épisode est bien entendu toujours d’actualité (cf &lt;a href="https://dev.to/zenika/rendre-son-podcast-accessible-avec-lia-au-service-de-la-transcription-e01"&gt;précédent article&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;Si une première approche s’était révélée concluante avec &lt;strong&gt;Vertex AI&lt;/strong&gt;, une solution qui nous avait clairement aidé dans la production de qualité de ces transcriptions, nous voulions aller plus loin en rendant disponible ce moyen de transcription à l’ensemble des collaborateurs·trices de Zenika, via une interface graphique, indépendamment de leur connaissance en Vertex AI, Google Cloud Platform ou autre item technique.&lt;/p&gt;

&lt;p&gt;En tant que développeur, l’idée de démarrer un projet, un “nième side project”, vient rapidement à l’esprit 😁. Javaiste depuis mes études, il était évident pour moi que ce nouveau projet allait se faire avec du Java !&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 ADK
&lt;/h2&gt;

&lt;p&gt;La première question tourne autour du “comment”. Comment intégrer Gemini dans une API? Une librairie de bas niveau de Google, &lt;a href="https://github.com/googleapis/java-genai" rel="noopener noreferrer"&gt;java-genia&lt;/a&gt;, couvre clairement ce besoin. &lt;/p&gt;

&lt;p&gt;Mais mon regard s’est finalement porté vers &lt;strong&gt;&lt;a href="https://google.github.io/adk-docs/" rel="noopener noreferrer"&gt;Agent Development Kit&lt;/a&gt;&lt;/strong&gt; (ADK), un framework créé par Google facilitant la création d’agents. Initialement créé en &lt;a href="https://github.com/google/adk-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, des versions &lt;a href="https://github.com/google/adk-java" rel="noopener noreferrer"&gt;Java&lt;/a&gt; et dernièrement en &lt;a href="https://github.com/google/adk-go" rel="noopener noreferrer"&gt;Go&lt;/a&gt; sont disponibles.&lt;/p&gt;

&lt;p&gt;Après avoir créé votre projet maven, vous aurez besoin de cette dépendance :&lt;/p&gt;

&lt;p&gt;La librairie &lt;code&gt;google-adk&lt;/code&gt; amène plusieurs classes dont voici les 3 principales et nécessaires que vous allez manipuler :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BaseAgent&lt;/li&gt;
&lt;li&gt;LlmAgent&lt;/li&gt;
&lt;li&gt;InMemoryRunner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La classe LlmAgent représente le builder de votre agent. C’est à cet endroit que vous allez définir votre agent avec un nom, une description, le modèle à utiliser et des instructions.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&amp;gt; A noter que le choix du modèle est libre et peut être un autre modèle que Gemini.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Une instance de InMemoryRunner permet d’initier et de gérer le cycle de vie de votre agent, notamment l’allocation mémoire à son fonctionnement. &lt;/p&gt;

&lt;p&gt;Une session est ensuite à initier à partir de ce runner afin de maintenir l’état conversationnel avec l’agent :&lt;/p&gt;

&lt;p&gt;L’exécution de votre agent peut se faire avec cet exemple : &lt;/p&gt;

&lt;p&gt;Le résultat est récupéré à partir de l’&lt;code&gt;events&lt;/code&gt; :&lt;/p&gt;

&lt;p&gt;Une fois configuré, l’agent est exécuté avec cette commande et lorsque la réponse sera obtenue, votre programme sera arrêté. Votre agent est fonctionnel 🎉.&lt;/p&gt;

&lt;p&gt;ADK met à disposition une seconde dépendance, &lt;code&gt;google-adk-dev&lt;/code&gt;, offrant une interface graphique pour tester, utiliser et debbuger vos agents.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;⚠️ Cette dépendance est à utiliser lors de vos développements.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La commande à exécuter est légèrement différente. Elle exécute la classe &lt;code&gt;AdkWebServer&lt;/code&gt;disponible dans la dépendance d’ADK. &lt;/p&gt;

&lt;p&gt;Cette interface est composée de deux parties. La première, le menu de gauche, détaille les différents événements et étapes de réflexions de votre agent, comme les logs, l’état des sessions en cours et passées ainsi que les outils que l’agent peut être amené à utiliser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fadk-javelit%2Fadk-ui-menu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fadk-javelit%2Fadk-ui-menu.png" alt="ADK UI menu" width="800" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La partie centrale quant à elle, est une zone de conversation où vous interagissez avec votre agent. Dans cet exemple, je lui envoie un passage audio pour vérifier la qualité de la transcription (et de mon prompt associé).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fadk-javelit%2Fadk-ui.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fadk-javelit%2Fadk-ui.jpg" alt="ADK UI menu" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉Retrouvez toute la documentation d’ADK sur &lt;a href="https://google.github.io/adk-docs" rel="noopener noreferrer"&gt;https://google.github.io/adk-docs&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Gemini 3
&lt;/h2&gt;

&lt;p&gt;La sortie de la version 3 de Gemini a été remarquée de par sa progression par rapport à la précédente version et notamment grâce à son entraînement multimodal. Google a sorti une gamme de modèles très performant, que ce soit Gemini 3 pro, mais aussi sur son modèle d’images, &lt;a href="https://gemini.google/lu/overview/image-generation/?hl=fr" rel="noopener noreferrer"&gt;Nano Banana&lt;/a&gt; ou bien vidéo avec &lt;a href="https://gemini.google/fr/overview/video-generation/?hl=fr" rel="noopener noreferrer"&gt;Veo 3&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Après avoir testé Gemini 3 pour nos transcriptions, l’amélioration de la qualité est clairement présente. &lt;/p&gt;

&lt;h2&gt;
  
  
  🎨 Javelit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/javelit/javelit" rel="noopener noreferrer"&gt;Javelit&lt;/a&gt; est un projet créé par &lt;a href="https://fr.linkedin.com/in/cyril-de-catheu" rel="noopener noreferrer"&gt;Cyril de Catheu&lt;/a&gt;. Inspiré de &lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Streamlit&lt;/a&gt;, une librairie de création de composants graphiques en Python, Javelit est une variante pour … Java. Ce projet n’a pas vocation àconcurrencer les frameworks Front end comme Angular, Vue ou React. Il offre la possibilité de créer rapidement des applications web dans le même langage que votre API, et ça, c’est super intéressant. &lt;/p&gt;

&lt;p&gt;A partir de l’import de la dépendance et du package &lt;code&gt;core&lt;/code&gt;, vous avez accès à des classes qui vous permettent d’avoir votre premier composant graphique en Java.&lt;/p&gt;

&lt;p&gt;Dans cet exemple, je mentionne un composant “markdown” mais le catalogue de composants est très complet, avec par exemple : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jt.title pour mettre un titre à votre page&lt;/li&gt;
&lt;li&gt;Jt.text pour afficher du texte&lt;/li&gt;
&lt;li&gt;Jt.code pour afficher du code &lt;/li&gt;
&lt;li&gt;Jt.html pour afficher du code HTML l’exécution de Javascript n’est pas possible&lt;/li&gt;
&lt;li&gt;Jt.echarts pour afficher des graphiques (basés sur la librairie &lt;a href="https://echarts.icepear.org/#/" rel="noopener noreferrer"&gt;ECharts&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Jt.button, Jt.radio, Jt.textInput pour faire des formulaires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉Retrouvez la documentation complète sur &lt;a href="https://docs.javelit.io/" rel="noopener noreferrer"&gt;https://docs.javelit.io/&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  🙌 ADK + Javelit = ♥️
&lt;/h2&gt;

&lt;p&gt;La puissance et la simplicité d’ADK pour créer un agent, combinées à l’efficacité de Javelit pour développer des interfaces graphiques permettent de transformer rapidement des idées en applications fonctionnelles de A à Z. L’infrastructure “mono composant” amène aussi des simplicités dans le déploiement, par exemple avec Cloud Run il est rapidement possible de déployer et mettre à disposition une application.&lt;/p&gt;

&lt;p&gt;C’est avec ce raisonnement que l’application de transcription de podcast Zenika a été créée et déployée sur Google Cloud Platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fadk-javelit%2Fagent-Z-transcribe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjeanphi-baconnais.gitlab.io%2Fimg%2Fadk-javelit%2Fagent-Z-transcribe.png" alt="ADK UI menu" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plusieurs options ont été rajoutées pour modifier ou adapter le prompt en fonction des besoins comme l’affichage de l’horodatage dans la transcription (merci Théophile pour l’idée), ou bien pour générer une transcription stricte (équivalente aux mots des intervenants·es) ou une version plus lisible.&lt;/p&gt;

&lt;p&gt;👉 Retrouvez le projet Agent-Z-transcribe-podcast sur GitHub : &lt;a href="http://github.com/zenika-open-source/agent-Z-transcribe-podcast/" rel="noopener noreferrer"&gt;http://github.com/zenika-open-source/agent-Z-transcribe-podcast/&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Ce trinôme &lt;strong&gt;ADK, Gemini + Javelit&lt;/strong&gt; est une belle découverte. Déployée sur une instance serverless comme Cloud Run, notre application de transcription de podcast est graphiquement agréable, sans trop de prise de tête avec du CSS 😅, et utilise la puissance du dernier modèle de Gemini pour me générer une transcription de qualité.&lt;/p&gt;

&lt;p&gt;Cela donne des idées par la suite pour faire rapidement des projets. Si on ajoute à cette expérience, l’utilisation de l’IDE “agentique” &lt;a href="https://antigravity.google/" rel="noopener noreferrer"&gt;Antigravity&lt;/a&gt;, la production et surtout la réalisation de nouveaux projets vont pouvoir voir le jour beaucoup plus rapidement 🚀.&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>adk</category>
      <category>ai</category>
      <category>development</category>
    </item>
    <item>
      <title>Web3 : Le bilan de Google Cloud sur la blockchain en 2025</title>
      <dc:creator>Benjamin Bourgeois</dc:creator>
      <pubDate>Thu, 11 Dec 2025 09:48:02 +0000</pubDate>
      <link>https://forem.com/zenika/web3-le-bilan-de-google-cloud-sur-la-blockchain-en-2025-431p</link>
      <guid>https://forem.com/zenika/web3-le-bilan-de-google-cloud-sur-la-blockchain-en-2025-431p</guid>
      <description>&lt;p&gt;Comme chaque année (&lt;a href="https://medium.com/zenika/web3-le-bilan-de-google-cloud-sur-blockchain-80da566e628d" rel="noopener noreferrer"&gt;2023&lt;/a&gt; et &lt;a href="https://dev.to/zenika/web3-le-bilan-de-google-cloud-sur-la-blockchain-en-2024-2i4"&gt;2024&lt;/a&gt;), je fais le point sur les avancées de &lt;strong&gt;Google Cloud sur le Web3&lt;/strong&gt;. En 2025, ce domaine est également concerné par l’arrivée de l’Intelligence Artificielle (IA). Le géant du cloud mise désormais sur la &lt;strong&gt;convergence entre la blockchain et l’IA&lt;/strong&gt;. De l’infrastructure aux agents autonomes, en passant par les paiements programmables, Google étend son empreinte bien au-delà des simples services de nœuds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqrp9v4zt5asp83h2522.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqrp9v4zt5asp83h2522.png" alt="Schéma de l'IA dans le Web3 pour GCP" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 Des agents IA connectés à la blockchain
&lt;/h2&gt;

&lt;p&gt;L’un des temps forts de l’année est la publication par Google d’un &lt;strong&gt;guide complet pour créer des « Web3 AI agents »&lt;/strong&gt;, des agents autonomes capables d’interagir directement avec des smart-contracts et des données « on-chain ». S’appuyant sur &lt;strong&gt;Vertex AI Agent Engine&lt;/strong&gt; et &lt;strong&gt;Agent Development Kit (ADK)&lt;/strong&gt;, ces agents peuvent lire des transactions, exécuter des contrats et gérer des flux en toute autonomie.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/build-web3-ai-agents-with-google-cloud?hl=en" rel="noopener noreferrer"&gt;Lire l’article officiel sur le blog Google Cloud&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google ne se contente plus de fournir des outils d’accès à la blockchain, il devient un acteur du Web3 intelligent, où les IA ne sont plus seulement “off-chain” mais véritablement connectées à l’écosystème décentralisé. Les agents deviennent des entités économiques à part entière, capables d’échanger, d’apprendre et d’agir dans un écosystème numérique interconnecté et sécurisé.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏦 Stablecoins et “Universal Ledger” : Google veut sa propre infrastructure de paiement
&lt;/h2&gt;

&lt;p&gt;L’annonce la plus stratégique de 2025 reste toutefois la mise en avant d’un &lt;strong&gt;protocole de paiement pour agents IA&lt;/strong&gt;, appuyé sur les stablecoins ainsi que sa &lt;strong&gt;propre blockchain maison&lt;/strong&gt; baptisée &lt;a href="https://cloud.google.com/application/web3/universal-ledger" rel="noopener noreferrer"&gt;Google Cloud Universal Ledger&lt;/a&gt;. GCUL ne reposerait pas sur l’Ethereum Virtual Machine (EVM) mais plutôt sur une infrastructure indépendante avec des smart contracts écrits en Python. &lt;a href="https://www.theblock.co/post/368399/google-cloud-blockchain-gcul" rel="noopener noreferrer"&gt;Selon&lt;/a&gt; &lt;em&gt;Rich Widmann&lt;/em&gt;, responsable Web3 chez Google Cloud, GCUL est conçue pour être une plateforme stable, flexible, conforme aux réglementations et « crédiblement neutre », dédiée aux paiements institutionnels et les cas d’usage IA-to-IA. &lt;/p&gt;

&lt;p&gt;Cette initiative rapproche Google des acteurs comme &lt;strong&gt;Stripe&lt;/strong&gt; et &lt;strong&gt;Circle&lt;/strong&gt;, tout en renforçant son positionnement face à des besoins émergents : permettre à des &lt;strong&gt;agents autonomes d’effectuer des transactions stables, traçables et programmables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://journalducoin.com/actualites/economie-google-lance-protocole-paiement-pour-ia-booste-stablecoins/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Article du Journal du Coin sur le protocole de paiement IA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://journalducoin.com/economie/google-cloud-universal-ledger-nouvelle-solution-paiement-stablecoin/" rel="noopener noreferrer"&gt;Article du Journal du Coin sur GCUL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://cryptoast.fr/google-construit-propre-blockchain-voici-dernieres-infos-officielles/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Cryptoast - Google construirait sa propre blockchain&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://cryptodnes.bg/fr/google-cloud-blockchain-stripe-circle/" rel="noopener noreferrer"&gt;CryptoDNES - Google Cloud lance sa blockchain pour concurrencer Stripe et Circle&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Un écosystème Web3 en expansion
&lt;/h2&gt;

&lt;p&gt;En parallèle, Google Cloud a multiplié les &lt;strong&gt;partenariats dans l’écosystème blockchain&lt;/strong&gt;, confirmant sa volonté de ne pas rester seul fournisseur mais de jouer un rôle de catalyseur :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Collaboration avec &lt;strong&gt;Lava Network&lt;/strong&gt; autour de la “&lt;strong&gt;future économie autonome&lt;/strong&gt;”, mêlant IA et blockchain. 👉 &lt;a href="https://www.cryptopolitan.com/fr/google-cloud-and-lava-network-lead-discussion-on-the-future-of-the-autonomous-economy/" rel="noopener noreferrer"&gt;Article sur Cryptopolitan&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Partenariat avec &lt;strong&gt;Etherlink&lt;/strong&gt; pour renforcer les outils de développement Web3 et améliorer les performances des infrastructures décentralisées. 👉 &lt;a href="https://phemex.com/news/article/google-cloud-and-etherlink-collaborate-to-enhance-web3-development-28751" rel="noopener noreferrer"&gt;Source Phemex News&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Coopérations avec des projets blockchain pour stimuler l’innovation Web3 à l’échelle mondiale. 👉 &lt;a href="https://cryptodnes.bg/en/blockchain-project-partners-with-google-cloud-to-strengthen-web3-innovation/" rel="noopener noreferrer"&gt;Article de CryptoDnes&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ces alliances traduisent une stratégie pragmatique : &lt;strong&gt;favoriser un écosystème performant plutôt qu’un modèle fermé&lt;/strong&gt;, tout en s’assurant que les briques critiques, données, IA, paiements, passent par Google Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu’il faut retenir
&lt;/h2&gt;

&lt;p&gt;En résumé, voici les quelques pistes à explorer en priorité :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expérimenter les architectures d’agents IA connectés à la blockchain&lt;/strong&gt; : les tutoriels et SDK proposés par Google facilitent les prototypes. Il y a d’ailleurs un codelab &lt;a href="https://www.skills.google/focuses/61475?parent=catalog" rel="noopener noreferrer"&gt;ici&lt;/a&gt; ou encore un exemple &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/build-web3-ai-agents-with-google-cloud" rel="noopener noreferrer"&gt;là&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surveiller de près Google Cloud Universal Ledger et les initiatives de paiement stablecoin&lt;/strong&gt; : elles pourraient transformer la manière dont les agents IA échangent de la valeur.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Garder un œil sur la gouvernance et la neutralité&lt;/strong&gt; : plus Google s’implique dans l’infrastructure financière on-chain, plus la question de la “neutralité crédible” deviendra importante.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;En 2025, Google Cloud franchit une nouvelle étape : du simple accès à la blockchain à la création d’une infrastructure complète pour les agents intelligents et les paiements programmables. Avec ses nouveaux outils, ses partenariats stratégiques et ses ambitions dans les paiements, le cloud de Google devient un acteur structurant de la convergence &lt;strong&gt;Web3 × IA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Les clouds de demain seront-ils « &lt;em&gt;on-chain&lt;/em&gt; et &lt;em&gt;intelligents »&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Rendez-vous en 2026 !&lt;/p&gt;

</description>
      <category>web3</category>
      <category>blockchain</category>
      <category>googlecloud</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
