<?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>DEV Community: Zenika</title>
    <description>The latest articles on DEV Community by Zenika (zenika).</description>
    <link>https://dev.to/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.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1234%2F61529416-7337-4c59-ab27-aaa666d850fc.png</url>
      <title>DEV Community: Zenika</title>
      <link>https://dev.to/zenika</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zenika"/>
    <language>en</language>
    <item>
      <title>🦊 GitLab CI Job Logs: The Art of Self-Documenting Pipelines</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:22:46 +0000</pubDate>
      <link>https://dev.to/zenika/gitlab-ci-job-logs-the-art-of-self-documenting-pipelines-1je1</link>
      <guid>https://dev.to/zenika/gitlab-ci-job-logs-the-art-of-self-documenting-pipelines-1je1</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;Prerequisite: clear stage and job names&lt;/li&gt;
&lt;li&gt;1. Enable GitLab's built-in logging features&lt;/li&gt;
&lt;li&gt;2. Choose the right CI log level — and stick to it&lt;/li&gt;
&lt;li&gt;3. Bring color to your logs&lt;/li&gt;
&lt;li&gt;4. Structure your output with collapsible sections&lt;/li&gt;
&lt;li&gt;5. Synchronize and validate before moving on&lt;/li&gt;
&lt;li&gt;6. Use warning jobs as early signals&lt;/li&gt;
&lt;li&gt;7. Make warnings and errors actionable&lt;/li&gt;
&lt;li&gt;8. Log the variable, path, and reason behind every decision&lt;/li&gt;
&lt;li&gt;9. Know when NOT to log&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;The best CI/CD engineer is one who becomes unnecessary. &lt;strong&gt;If developers constantly ping us to understand why a pipeline failed, our pipelines are not doing their job.&lt;/strong&gt; With the right logging strategy, every failure becomes a self-service debugging experience — and we get to work on actually interesting problems.&lt;/p&gt;

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

&lt;p&gt;We have all been there: a developer opens a ticket saying "the pipeline is broken", and after 15 minutes of scrolling through a wall of undifferentiated text, we find that one missing failing test buried in line 847. 99% of the time, after pipelines have been stabilised, the culprit is the developer's code. Else is momentary and/or infrastructure problem.&lt;/p&gt;

&lt;p&gt;The goal is simple: &lt;strong&gt;a developer should be able to diagnose and fix 90% of pipeline failures without ever pinging the CI/CD team.&lt;/strong&gt; This is not about writing less code or cutting corners. It is about treating CI/CD logs as a user interface — one that developers interact with daily.&lt;/p&gt;

&lt;p&gt;After years of maintaining pipelines across dozens of projects and as discussed in &lt;a href="https://dev.to/zenika/gitlab-ci-10-best-practices-to-avoid-widespread-anti-patterns-2mb5"&gt;GitLab CI: 10+ Best Practices to Avoid Widespread Anti-Patterns&lt;/a&gt;, we have distilled a collection of practices that consistently make the difference between "I need help" and "I already fixed it".&lt;/p&gt;

&lt;h1&gt;
  
  
  Prerequisite: clear stage and job names
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Self-documenting logs make even more sense on top of a pipeline that is already readable at a glance.&lt;/strong&gt; Stage and job names must be clear, specific, and kept to a just-right count — enough to tell the story of what CI/CD is doing when we watch it run, not so many that the pipeline graph becomes noise.&lt;/p&gt;

&lt;p&gt;On a typical three-tier app — frontend, BFF, and API — deployed to Kubernetes, an MR pipeline already reads like a table of contents:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffb3gxomuzbgzdtkcvu5l.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffb3gxomuzbgzdtkcvu5l.png" alt="Diagram" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four stages, eleven jobs — a developer glancing at the pipeline UI knows exactly where they are: package, test, deploy to K8s, validate. The logging practices below assume this foundation is in place.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Enable GitLab's built-in logging features
&lt;/h1&gt;

&lt;p&gt;Before writing a single &lt;code&gt;echo&lt;/code&gt;, GitLab already offers several &lt;a href="https://docs.gitlab.com/runner/configuration/feature-flags.html" rel="noopener noreferrer"&gt;feature flags and variables&lt;/a&gt; that dramatically improve log readability. These quick wins cost nothing — just a few lines in our top-level &lt;code&gt;variables:&lt;/code&gt; block:&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="c1"&gt;# prepend ISO 8601 timestamps to every line&lt;/span&gt;
  &lt;span class="na"&gt;FF_TIMESTAMPS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="c1"&gt;# auto-wrap each script command in a collapsible section&lt;/span&gt;
  &lt;span class="na"&gt;FF_SCRIPT_SECTIONS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="c1"&gt;# smoother real-time log streaming&lt;/span&gt;
  &lt;span class="na"&gt;FF_USE_DYNAMIC_TRACE_FORCE_SEND_INTERVAL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="c1"&gt;# convention honored by npm, jest, webpack, dotnet...&lt;/span&gt;
  &lt;span class="na"&gt;FORCE_COLOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;FF_TIMESTAMPS&lt;/code&gt; gives us millisecond-precision timing on every line — invaluable for spotting slow steps without manually adding timers. &lt;code&gt;FF_SCRIPT_SECTIONS&lt;/code&gt; automatically wraps each script line in a collapsible section, giving structure for free. And &lt;code&gt;FORCE_COLOR: 1&lt;/code&gt; is a widely adopted convention that tells tools to output color even when they detect a non-TTY environment — without it, most CI output falls back to monochrome.&lt;/p&gt;

&lt;p&gt;Timestamps, color, and structure — before we even start customizing. Welcome to the "free improvements" club.&lt;/p&gt;
&lt;h1&gt;
  
  
  2. Choose the right CI log level — and stick to it
&lt;/h1&gt;

&lt;p&gt;The most common mistake is binary logging: either we print everything, or we print nothing. Both are equally useless.&lt;/p&gt;

&lt;p&gt;A CI job should follow the same log level discipline as any application:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;When to use&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;TRACE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;debug data, usually hidden&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Variable expansion: $DEPLOY_ENV → staging&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INFO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key steps the developer needs to follow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Deploying service user-api to staging...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WARNING&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Something unexpected but non-blocking&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cache miss for node_modules, full install triggered&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ERROR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Something failed that needs attention&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Deployment failed: health check timeout after 120s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a real deploy job, the four levels read like a 10-second story (more on how to do this later) :&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[TRACE] Variable expansion: $DEPLOY_ENV → staging
&lt;/span&gt;&lt;span&gt;[INFO] Deploying service user-api to staging...
&lt;/span&gt;&lt;span&gt;[WARNING] Cache miss for node_modules, full install triggered
&lt;/span&gt;&lt;span&gt;[ERROR] Deployment failed: health check timeout after 120s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The golden rule: &lt;strong&gt;a successful pipeline should produce a few lines of INFO, not 500.&lt;/strong&gt; If we need more detail, we wrap verbose output in collapsible sections (see section 4).&lt;/p&gt;

&lt;p&gt;The key is a &lt;strong&gt;centralized &lt;code&gt;log&lt;/code&gt; function&lt;/strong&gt; shared across all jobs. Write it once in a sourced helper script (&lt;code&gt;source ci/utils.sh&lt;/code&gt;), and every job gets consistent formatting for free:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;INFO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; color_map
  color_map[&lt;span class="s2"&gt;"TRACE"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[38;5;250m"&lt;/span&gt;     &lt;span class="c"&gt;# gray&lt;/span&gt;
  color_map[&lt;span class="s2"&gt;"INFO"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[1;94m"&lt;/span&gt;          &lt;span class="c"&gt;# bright blue&lt;/span&gt;
  color_map[&lt;span class="s2"&gt;"WARNING"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[1;38;5;214m"&lt;/span&gt; &lt;span class="c"&gt;# bold orange&lt;/span&gt;
  color_map[&lt;span class="s2"&gt;"ERROR"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[48;5;1m"&lt;/span&gt;       &lt;span class="c"&gt;# background red&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;color_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[%s] %s&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0m&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$level&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No need to repeat color codes in every &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;. A simple &lt;code&gt;source ci/utils.sh&lt;/code&gt; in &lt;code&gt;before_script&lt;/code&gt; and the entire team speaks the same logging language.&lt;/p&gt;

&lt;p&gt;Too much logging is noise. Too little is silence. The sweet spot is a concise narrative that a developer can read in 10 seconds and understand exactly what happened — like a well-written commit message, but for runtime.&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%2Fw5socix8wt7tb18un3hc.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%2Fw5socix8wt7tb18un3hc.jpg" alt="orange fox engineer handing a glowing instruction manual to a group of curious developers, anime style, warm lighting, CI/CD pipeline hologram in the background" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  3. Bring color to your logs
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Colors used by GitLab Runner&lt;/strong&gt; — avoid these for custom messages, as they blend with the runner's own output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Shell code&lt;/th&gt;
&lt;th&gt;Hex example&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;Bold Green&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;32m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#5cf759&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runner command echoes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold Cyan&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;36m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#00bdbd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runner generic messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold White&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;37m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ffffff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runner neutral outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold Red&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;31m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff6161&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runner error messages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a real job log, the runner's four colors look like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;Using docker executor with image node:20-alpine
&lt;/span&gt;&lt;span&gt;$ docker compose build --parallel
&lt;/span&gt;&lt;span&gt;$ docker compose up -d&lt;/span&gt;
  #1 [web] building with "buildx"
  #2 [api] building with "buildx"
&lt;span&gt;Cleaning up project directory and file based variables
&lt;/span&gt;&lt;span&gt;ERROR: Job failed: exit status 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Suggested colors for custom log messages:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Shell code&lt;/th&gt;
&lt;th&gt;Hex example&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;Gray&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[38;5;250m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#bcbcbc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TRACE — verbose/debug output, present but discreet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold Bright Blue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;94m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#5797ff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INFO — normal flow narrative and informational steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold Orange&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;38;5;214m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ffaf00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WARNING — alternative for bolder warnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background Red&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[48;5;1m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff6161&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ERROR — stands out from GitLab's own bold red errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold Bright Magenta&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;95m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#8e44ad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Situational&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold Bright Yellow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\033[1;93m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#f4d03f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Situational&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;TRACE — verbose/debug output, present but discreet
&lt;/span&gt;&lt;span&gt;INFO — normal flow narrative and informational steps
&lt;/span&gt;&lt;span&gt;WARNING — alternative for bolder warnings
&lt;/span&gt;&lt;span&gt;ERROR — stands out from GitLab's own bold red errors
&lt;/span&gt;&lt;span&gt;Situational
&lt;/span&gt;&lt;span&gt;Situational
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A monochrome wall of text is hostile to human eyes. Color creates a visual hierarchy that lets developers &lt;strong&gt;spot errors in seconds&lt;/strong&gt; instead of reading every line.&lt;/p&gt;

&lt;p&gt;GitLab job logs support &lt;a href="https://docs.gitlab.com/ci/yaml/script/" rel="noopener noreferrer"&gt;ANSI escape codes&lt;/a&gt; natively. Combined with &lt;code&gt;FORCE_COLOR: 1&lt;/code&gt; from section 1, most tools will also output color automatically. For our custom messages, the centralized &lt;code&gt;log&lt;/code&gt; function from section 2 already handles this.&lt;/p&gt;

&lt;p&gt;Why background red instead of bold red for errors? Because when a job fails, GitLab's runner already prints its own error in plain bold red — and those messages are almost never helpful:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  #3 [api 3/4] RUN go build -o /app ./cmd/server
&lt;span&gt;  #3 ERROR: process "go build" did not complete successfully
&lt;/span&gt;&lt;span&gt;  &amp;gt; [api 3/4] RUN go build -o /app ./cmd/server:&lt;/span&gt;
  cmd/server/main.go:42:18: undefined: handlers.NewRouter
&lt;span&gt;[10:15:22] Build failed for service 'api', please fix above error(s)
&lt;/span&gt;&lt;span&gt;Cleaning up project directory and file based variables
&lt;/span&gt;&lt;span&gt;ERROR: Job failed: exit status 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By using &lt;strong&gt;background red&lt;/strong&gt; for our own errors, the developer can instantly distinguish our actionable messages from the runner's generic noise. The white-on-red line is our message — it says what failed and what to fix. Everything else is the runner repeating "something threw an exception somewhere". Thanks, runner. Very helpful.&lt;/p&gt;

&lt;p&gt;Consistency matters more than aesthetics. Once developers learn that background red means "read this first" and the runner's plain red means "ignore unless desperate", they navigate logs instinctively.&lt;/p&gt;
&lt;h1&gt;
  
  
  4. Structure your output with collapsible sections
&lt;/h1&gt;

&lt;p&gt;GitLab supports native &lt;a href="https://docs.gitlab.com/ci/jobs/job_logs/#custom-collapsible-sections" rel="noopener noreferrer"&gt;collapsible sections&lt;/a&gt; in job logs. This is our most powerful weapon against log noise. A deployment job with 800 lines of raw output becomes this scannable overview:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;gt; Installing dependencies&lt;/span&gt;
&lt;span&gt;▼ Database migration&lt;/span&gt;
  Applying migration '20240115_AddUserPreferences'...
  Applying migration '20240122_IndexOptimization'...
  [...]
  Done. 250 migrations applied.
&lt;span&gt;▼ Deploying to staging&lt;/span&gt;
  Syncing build artifacts to server...
  [...]
  Restarting application pool...
&lt;span&gt;[INFO] Health check passed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;&amp;gt;&lt;/code&gt; indicates a collapsed section — the developer can click to expand it if needed. The &lt;code&gt;▼&lt;/code&gt; sections are expanded by default, showing the core output immediately. An 800-line log that reads like a 10-line summary? That is not logging — that is user experience design.&lt;/p&gt;

&lt;p&gt;The implementation uses two helper functions:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;start_section&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;collapsed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$collapsed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"collapsed"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"[collapsed=true]"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[0Ksection_start:&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;opts&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\r\e&lt;/span&gt;&lt;span class="s2"&gt;[0K&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[1;36m&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[0m"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

end_section&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[0Ksection_end:&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\r\e&lt;/span&gt;&lt;span class="s2"&gt;[0K"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Not all output deserves the same treatment. We use &lt;strong&gt;three categories&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No section needed&lt;/strong&gt; — simple one-liner messages (&lt;code&gt;log INFO ...&lt;/code&gt;) do not need to be wrapped in a section. They are already short and scannable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collapsed by default&lt;/strong&gt; — verbose but predictable output (dependency install, Docker pull, cache restore). The developer only opens these when something goes wrong:&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;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;start_section "npm_install" "Installing dependencies" collapsed&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_section "npm_install"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Collapsible, open by default&lt;/strong&gt; — the core action of the job. This is what the developer came to see: migration output, test results, deployment logs. It is wrapped in a section for structure, but expanded so the output is immediately visible:&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;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;start_section "db_migrate" "Database migration"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx prisma migrate deploy&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;end_section "db_migrate"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h1&gt;
  
  
  5. Synchronize and validate before moving on
&lt;/h1&gt;

&lt;p&gt;A deployment is not done when the artifact lands on the server. It is done when we have &lt;strong&gt;proof it works.&lt;/strong&gt; One of the trickiest CI/CD debugging situations is when an asynchronous operation fails &lt;strong&gt;after&lt;/strong&gt; the pipeline has moved on — the error appears in the wrong context, or worse, the pipeline reports success while the actual deployment is still rolling out.&lt;/p&gt;

&lt;p&gt;The rule is straightforward: &lt;strong&gt;if a step produces a result we depend on later, we wait for confirmation before continuing.&lt;/strong&gt; This means waiting for rollouts to complete, then actively validating the result.&lt;/p&gt;

&lt;p&gt;On success, the developer sees a clear narrative:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;$ ./ci/deploy.sh staging&lt;/span&gt;
  Pushing image user-api:e4f5g6h to registry...
  Done.
&lt;span&gt;[INFO] Deploying user-api to staging...
&lt;/span&gt;&lt;span&gt;$ kubectl rollout status deployment/user-api -n staging --timeout=300s&lt;/span&gt;
  Waiting for deployment "user-api" rollout to finish: 0 of 3 updated replicas are available...
  Waiting for deployment "user-api" rollout to finish: 2 of 3 updated replicas are available...
  deployment "user-api" successfully rolled out
&lt;span&gt;[INFO] Validating deployment...
&lt;/span&gt;&lt;span&gt;[TRACE] Attempt 1/30: got version 'a1b2c3d', expected 'e4f5g6h'
&lt;/span&gt;&lt;span&gt;[TRACE] Attempt 2/30: got version 'e4f5g6h', expected 'e4f5g6h'
&lt;/span&gt;&lt;span&gt;[INFO] Deployment validated: version e4f5g6h is live
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On failure, the error is impossible to miss — whether the rollout itself fails:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;$ ./ci/deploy.sh staging&lt;/span&gt;
  Pushing image user-api:e4f5g6h to registry...
  Done.
&lt;span&gt;[INFO] Deploying user-api to staging...
&lt;/span&gt;&lt;span&gt;$ kubectl rollout status deployment/user-api -n staging --timeout=300s&lt;/span&gt;
  Waiting for deployment "user-api" rollout to finish: 0 of 3 updated replicas are available...
  Waiting for deployment "user-api" rollout to finish: 1 of 3 updated replicas are available...
  error: deployment "user-api" exceeded its progress deadline
&lt;span&gt;[ERROR] Rollout failed or timed out for user-api in staging — run: kubectl
describe pod -l app=user-api -n staging
&lt;/span&gt;&lt;span&gt;Cleaning up project directory and file based variables
&lt;/span&gt;&lt;span&gt;ERROR: Job failed: exit status 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or the validation catches the wrong version:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;$ ./ci/deploy.sh staging&lt;/span&gt;
&lt;span&gt;[INFO] Validating deployment...
&lt;/span&gt;&lt;span&gt;[TRACE] Attempt 29/30: got version 'a1b2c3d', expected 'e4f5g6h'
&lt;/span&gt;&lt;span&gt;[TRACE] Attempt 30/30: got version 'a1b2c3d', expected 'e4f5g6h'
&lt;/span&gt;&lt;span&gt;[ERROR] Deployment validation failed after 5 minutes — expected version
e4f5g6h, last received a1b2c3d, health endpoint
https://staging.example.com/api/health
&lt;/span&gt;&lt;span&gt;Cleaning up project directory and file based variables
&lt;/span&gt;&lt;span&gt;ERROR: Job failed: exit status 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Good validation goes beyond "is it running?":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version check&lt;/strong&gt;: confirm the exact commit is deployed, not a cached old version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health endpoint&lt;/strong&gt;: verify the service can reach its dependencies (database, cache, external APIs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoke tests&lt;/strong&gt;: run a minimal request that exercises the critical path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback trigger&lt;/strong&gt;: if validation fails, automatically roll back and log the reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This applies to more than just Kubernetes deployments — database migrations, infrastructure provisioning, async API calls, DNS propagation. When a deployment validation fails, the developer knows immediately — not 30 minutes later when a user reports a bug. Schrödinger's deployment: simultaneously succeeded and failed until someone actually checks.&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%2F9mo23c6wd9d9lcs5xk0t.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%2F9mo23c6wd9d9lcs5xk0t.jpg" alt="orange fox engineer handing a glowing instruction manual to a group of curious developers, anime style, warm lighting, CI/CD pipeline hologram in the background" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  6. Use warning jobs as early signals
&lt;/h1&gt;

&lt;p&gt;Not every issue deserves a pipeline failure. Some problems — like linter violations in legacy code, or a growing list of &lt;code&gt;TODO&lt;/code&gt; comments — are real but not urgent. Blocking the pipeline on day one would just train developers to ignore the warnings (or worse, remove the job entirely).&lt;/p&gt;

&lt;p&gt;Instead, we add the check as a &lt;strong&gt;non-blocking job&lt;/strong&gt; using &lt;code&gt;allow_failure: true&lt;/code&gt;. The job runs, reports its findings, and shows as an orange warning in the pipeline. It does not block the merge, but it stays visible — a gentle, persistent reminder that something needs attention:&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;eslint&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;quality&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;npx eslint src/ --format stylish&lt;/span&gt;
  &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The orange icon acts as a soft nudge. Sooner or later, a developer picks up the task, fixes the violations, and the job turns green. At that point, we remove &lt;code&gt;allow_failure: true&lt;/code&gt; and the check becomes mandatory — without any drama or big-bang migration.&lt;/p&gt;

&lt;p&gt;This works for any gradual adoption pattern: stricter TypeScript settings, new security rules, accessibility checks, or dependency update policies. The pipeline documents the target standard before it enforces it.&lt;/p&gt;

&lt;p&gt;Warning jobs also shine for &lt;strong&gt;detecting slow-moving disasters&lt;/strong&gt; — things that are fine today but will explode next month. We can use &lt;code&gt;allow_failure: exit_codes&lt;/code&gt; to turn specific exit codes into warnings:&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="k"&gt;for &lt;/span&gt;drive &lt;span class="k"&gt;in &lt;/span&gt;C D&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; /&lt;span class="nv"&gt;$drive&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR==2{printf "%.0f", $5}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$usage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 90 &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;log WARNING &lt;span class="s2"&gt;"Disk &lt;/span&gt;&lt;span class="nv"&gt;$drive&lt;/span&gt;&lt;span class="s2"&gt;: usage is &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;usage&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;% (&amp;gt; 90%) — ask infra to clean up old releases"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;99
  &lt;span class="k"&gt;fi
  &lt;/span&gt;log TRACE &lt;span class="s2"&gt;"Disk &lt;/span&gt;&lt;span class="nv"&gt;$drive&lt;/span&gt;&lt;span class="s2"&gt;: usage is &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;usage&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
  &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;exit_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;99&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# disk space&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The job shows as a &lt;strong&gt;warning&lt;/strong&gt; (orange) in the pipeline instead of a hard failure. The developer sees the problem, but the pipeline is not blocked. This pattern works well for any ticking clock: disk space, certificate expiry, security scan thresholds, or GitLab Pages size limits approaching the quota.&lt;/p&gt;
&lt;h1&gt;
  
  
  7. Make warnings and errors actionable
&lt;/h1&gt;

&lt;p&gt;The difference between a good log and a great log is the "what to do next" part. Every warning and error should include a &lt;strong&gt;remediation hint&lt;/strong&gt; — a specific action the developer can take.&lt;/p&gt;

&lt;p&gt;A bare error leaves the developer stranded:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[ERROR] Build artifacts not found at dist/api — sync cannot proceed
&lt;/span&gt;&lt;span&gt;Cleaning up project directory and file based variables
&lt;/span&gt;&lt;span&gt;ERROR: Job failed: exit status 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The same error with a remediation hint becomes self-service:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[ERROR] Build artifacts not found at dist/api and fallback dist/api-main
&lt;/span&gt;&lt;span&gt;[WARNING] add 'force-build-api' label on your MR and launch a new pipeline,
or run a manual pipeline on the target branch to create a fallback
&lt;/span&gt;&lt;span&gt;Cleaning up project directory and file based variables
&lt;/span&gt;&lt;span&gt;ERROR: Job failed: exit status 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One line, all the context. Here are more patterns from real-world pipelines:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[ERROR] Pre/Production deployment requires Maintainer privileges (level &amp;gt;=
40), current user jdupont has level 30 — ask a project Owner to promote you
in Settings &amp;gt; Members&lt;/span&gt;

&lt;span&gt;[ERROR] https://recette3.example.com/health failed with HTTP 503 —
localhost fallback returned "Cannot find module '@prisma/client'" — check
application logs on app-rec-03&lt;/span&gt;

&lt;span&gt;[ERROR] Current branch is behind main or has conflicts — E2E tests would be
meaningless, please rebase or back-merge before retrying&lt;/span&gt;

&lt;span&gt;[WARNING] E2E test results found in cache (18.3h old), skipping tests — to
force a full run, delete the cache or add the 'force-e2e' label&lt;/span&gt;

&lt;span&gt;[WARNING] Kafka consumer service configured as Manual startup (GroupId is
empty) — set a GroupId in the environment config to enable automatic
startup
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The pattern is always the same: &lt;strong&gt;what happened&lt;/strong&gt; + &lt;strong&gt;why&lt;/strong&gt; + &lt;strong&gt;what to do about it&lt;/strong&gt;, all on a single line. Long lines scroll horizontally in GitLab's log viewer — and a developer can copy-paste one line into Slack and the recipient has the full picture without follow-up questions.&lt;/p&gt;

&lt;p&gt;A developer reading this at 6 PM on a Friday should not need to ask anyone for help.&lt;/p&gt;
&lt;h1&gt;
  
  
  8. Log the variable, path, and reason behind every decision
&lt;/h1&gt;

&lt;p&gt;Most CI logs describe what is happening. Great CI logs explain &lt;strong&gt;why&lt;/strong&gt; a branch was taken — and with &lt;strong&gt;enough context to reconstruct the decision without opening the YAML&lt;/strong&gt;: the variable or label that triggered it, the file or cache key that was found or missing, the path that was created or skipped. This is the single most effective way to reduce support tickets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching is the #1 source of "why" questions.&lt;/strong&gt; When a job is slow, the first thing a developer wonders is: "did it use the cache?" Without explicit logging, the only way to find out is to read the YAML and hope the runner logs are detailed enough. Instead, we log the decision:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[INFO] Cache found: node_modules checksum matches (a7f3b2c)
&lt;/span&gt;&lt;span&gt;[INFO] Skipping npm ci
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[WARNING] Cache miss: node_modules checksum changed (a7f3b2c → e91d4f8)
&lt;/span&gt;&lt;span&gt;[INFO] Running full npm ci
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[INFO] E2E test results found in cache (less than 24h old)
&lt;/span&gt;&lt;span&gt;[INFO] Skipping E2E tests for this module
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[WARNING] E2E test results expired (cache is 36h old, max: 24h)
&lt;/span&gt;&lt;span&gt;[INFO] Running full E2E suite
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Depending on the conditions, the developer sees exactly why tests were skipped — or why they were not:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[WARNING] Skipping tests (SKIP_TESTS=true)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;[INFO] Running full test suite (no skip conditions met)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When a pipeline behaves unexpectedly, the "why" logs are the first thing developers look for. Without them, they are reverse-engineering our CI logic from YAML — a slow and error-prone process that almost certainly ends in a support ticket.&lt;/p&gt;
&lt;h1&gt;
  
  
  9. Know when NOT to log
&lt;/h1&gt;

&lt;p&gt;The temptation after reading this article is to wrap every command in &lt;code&gt;log()&lt;/code&gt;. Resist it. Over-logging is almost as bad as under-logging — it buries the signal in noise.&lt;/p&gt;

&lt;p&gt;Here is what an over-logged &lt;code&gt;npm ci&lt;/code&gt; looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;$ cd front &amp;amp;&amp;amp; npm ci --prefer-offline
&lt;/span&gt;&lt;span&gt;[INFO] Starting npm ci...
&lt;/span&gt;&lt;span&gt;[INFO] Running: npm ci --prefer-offline
&lt;/span&gt;&lt;span&gt;[INFO] Working directory: /builds/project/front
&lt;/span&gt;&lt;span&gt;[INFO] Node version: 20.11.0
&lt;/span&gt;&lt;span&gt;[INFO] npm version: 10.2.4&lt;/span&gt;
npm warn deprecated svgo@1.3.2: ...
added 1,847 packages in 42s
&lt;span&gt;[INFO] npm ci completed successfully
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here is the same step, done right:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span&gt;$ cd front &amp;amp;&amp;amp; npm ci --prefer-offline&lt;/span&gt;
npm warn deprecated svgo@1.3.2: ...
added 1,847 packages in 42s&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No custom logging at all. &lt;code&gt;npm ci&lt;/code&gt; already tells us everything we need to know: what it installed, how long it took, and whether it succeeded (via its exit code). Adding &lt;code&gt;log INFO&lt;/code&gt; around it is pure noise.&lt;/p&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;log when the pipeline makes a decision, not when it executes a well-known command.&lt;/strong&gt; Specifically, we add logging for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous operations&lt;/strong&gt; — cache hit or miss? skip or run? which environment was selected?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations that can fail silently&lt;/strong&gt; — a sync that might succeed partially, a health check with a timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions with side effects&lt;/strong&gt; — "deploying to production" is worth logging; "running npm ci" is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Standard tools like &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;go build&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt; produce their own output. Our job is to add context &lt;strong&gt;around&lt;/strong&gt; them, not &lt;strong&gt;on top of&lt;/strong&gt; them. If we find ourselves logging "Starting npm ci" right before &lt;code&gt;npm ci&lt;/code&gt;, we have achieved the logging equivalent of a meeting that could have been an email.&lt;/p&gt;
&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Self-documenting pipelines are an investment that pays off immediately. From GitLab's built-in features, through structured log levels, meaningful colors, and collapsible sections, to synchronization barriers, deployment validation, context-rich errors, actionable remediation, decision-context logging, and knowing when to stay silent — &lt;strong&gt;we transform CI/CD logs from a debugging nightmare into a developer self-service tool.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ultimate test: when a pipeline fails at 2 AM, can the on-call developer fix it without escalating to the CI/CD team? If yes, we have done our job. The real metric is not code coverage or deployment frequency — it is the number of CI/CD support tickets trending toward zero.&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%2Fn4erx8haibarw25yyy4c.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%2Fn4erx8haibarw25yyy4c.jpg" alt="orange fox engineer handing a glowing instruction manual to a group of curious developers, anime style, warm lighting, CI/CD pipeline hologram in the background" width="800" height="350"&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" width="500" height="500"&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="" width="500" height="500"&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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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 crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&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>tutorial</category>
    </item>
    <item>
      <title>Organiser ses worktrees dans Antigravity 2.0</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Wed, 17 Jun 2026 16:37:51 +0000</pubDate>
      <link>https://dev.to/zenika/organiser-ses-worktrees-dans-antigravity-20-1oe3</link>
      <guid>https://dev.to/zenika/organiser-ses-worktrees-dans-antigravity-20-1oe3</guid>
      <description>&lt;p&gt;&lt;em&gt;Image de couverture prise par &lt;a href="https://unsplash.com/fr/@kimileee" rel="noopener noreferrer"&gt;Kimi Lee&lt;/a&gt; sur Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: an english version is available &lt;a href="https://jeanphi-baconnais.gitlab.io/post/2026-antigravity-worktrees-en/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  📕 Les Git worktrees
&lt;/h2&gt;

&lt;p&gt;Les &lt;strong&gt;worktrees&lt;/strong&gt; sont apparus dans Git dans la version 2.5.0, sortie en Juillet 2015 (cf &lt;a href="https://github.blog/open-source/git/git-2-5-including-multiple-worktrees-and-triangular-workflows/" rel="noopener noreferrer"&gt;https://github.blog/open-source/git/git-2-5-including-multiple-worktrees-and-triangular-workflows/&lt;/a&gt;) . Pourtant, cette fonctionnalité a mis du temps à se faire connaître et à intégrer les commandes du quotidien des équipes de développement. Des articles expliquant ce concept sortent encore de nos jours, comme &lt;a href="https://github.blog/ai-and-ml/github-copilot/what-are-git-worktrees-and-why-should-i-use-them/" rel="noopener noreferrer"&gt;celui-ci&lt;/a&gt; sorti en juin 2026 sur le blog de GitHub.&lt;/p&gt;

&lt;p&gt;Le principe des &lt;strong&gt;worktrees&lt;/strong&gt; est assez simple. Git associe votre projet à un ou plusieurs répertoires (qu’il soit dans votre projet Git ou non). Cette “copie” est lien vers votre dépôt principal, ce qui permet d’avoir plusieurs branches extraites simultanément dans des répertoires distincts. Vous pouvez donc basculer entre les branches sans effectuer de commandes Git comme le &lt;code&gt;git stash&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Avec quelques commandes, la compréhension sera je l’espère plus facile.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Création d’un worktree pour une nouvelle fonctionnalité. Je stocke mes worktrees dans un répertoire spécifique pour éviter les confusions.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/my-new-feature ./worktrees/feature/my-new-feature origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Lister les worktrees
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&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%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F2026-antigravity-worktrees%2F1-worktree-examples.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%2F2026-antigravity-worktrees%2F1-worktree-examples.jpg" alt="Worktrees examples" width="797" height="35"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suite à ces modifications, la branche courante reste la branche main. Des modifications peuvent donc être apportées sans impacter ce worktree. Et c’est là un des avantages à utiliser les worktree. Cela vous évite des bascules de branches en stockant vos modifications dans des &lt;code&gt;git stash&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;La bascule sur un worktree est transparente : une fois dans le répertoire, vous êtes directement sur la branche associée, sans avoir besoin de commandes Git.
&lt;/li&gt;
&lt;/ol&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;worktrees/feature/my-new-feature/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Vous êtes désormais libre de faire vos modifications, de les ajouter avec un &lt;code&gt;git add&lt;/code&gt;puis de pousser sur votre repository avec un &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Une fois la pull /merge request (PR/MR) faite et mergée, le worktree pourra être supprimé avec la commande &lt;code&gt;git worktree remove&lt;/code&gt;. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🤔 Et Antigravity dans tout ça ?
&lt;/h2&gt;

&lt;p&gt;Avec la croissance des développements assistés par l’intelligence artificielle agentique, il est facile de demander à plusieurs agents d’apporter des modifications dans votre projet. &lt;strong&gt;Sauf&lt;/strong&gt; que, si les modifications sont conséquentes et impactent les mêmes fichiers, vous risquez de voir des conflits apparaître.&lt;/p&gt;

&lt;p&gt;Dans &lt;strong&gt;Antigravity 2.0&lt;/strong&gt;, il est possible, via une option disponible sous votre requête, de préciser que les travaux peuvent se faire dans un &lt;strong&gt;worktree Git&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%2F2026-antigravity-worktrees%2F2-antigravity-worktree.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%2F2026-antigravity-worktrees%2F2-antigravity-worktree.jpg" alt="Worktrees dans Antigravity" width="754" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Antigravity crée la copie de vos &lt;em&gt;worktrees&lt;/em&gt; dans son répertoire de configuration &lt;code&gt;/Users/xxx/.gemini/antigravity/worktrees&lt;/code&gt;. Après un déplacement via la commande &lt;code&gt;cd&lt;/code&gt;, vous trouverez les fichiers modifiés et pourrez travailler sur la fonctionnalité demandée.&lt;/p&gt;

&lt;p&gt;Dans Antigravity IDE, les worktrees sont visibles graphiquement avec dans la vue “Source control”. &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-antigravity-worktrees%2F3-worktrees-antigravity.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%2F2026-antigravity-worktrees%2F3-worktrees-antigravity.jpg" alt="Worktrees dans Antigravity" width="799" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Avec Antigravity, l’accent est mis sur la gestion et l’orchestration des agents. Avec la possibilité d’isoler les évolutions des projets dans des Git worktrees, les évolutions des projets vont pouvoir d’autant plus être accélérées et parallélisées sans devoir se battre avec les conflits Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Un Skill ?
&lt;/h2&gt;

&lt;p&gt;Si la modification apportée par Antigravity convient, il est possible de demander de commiter et pusher le code. Antigravity le fait très bien mais le worktree n’est pas supprimé. Peut-être que cela n’est pas dans vos pratiques, mais je préfère supprimer le worktree directement et manipuler Git pour revenir sur cette branche si jamais quelque chose est détectée dans la MR.&lt;/p&gt;

&lt;p&gt;Pour faire ce tri et éviter que ce soit le “chaos” dans les worktrees, j’ai créé ce skill :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;--
name: finalize-feature
&lt;span class="gh"&gt;description: Clean up the current git worktree after a feature branch is successfully validated, committed, and pushed.
---
&lt;/span&gt;
&lt;span class="gh"&gt;# Finalize Feature Skill&lt;/span&gt;
Use this skill when the user has approved your changes on a feature branch and requested to clean up the current working directory / worktree.&lt;span class="sb"&gt;


&lt;/span&gt;&lt;span class="gu"&gt;## Instructions&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Verify git status**&lt;/span&gt;:
  Ensure all local changes are fully committed and pushed to the remote repository.
&lt;span class="p"&gt;  -&lt;/span&gt; Run &lt;span class="sb"&gt;`git status`&lt;/span&gt; to verify the working tree is clean.
&lt;span class="p"&gt;  -&lt;/span&gt; Run &lt;span class="sb"&gt;`git log -n 1 @{u}`&lt;/span&gt; or check &lt;span class="sb"&gt;`git status`&lt;/span&gt; to verify there are no unpushed commits.
&lt;span class="p"&gt;
2.&lt;/span&gt; &lt;span class="gs"&gt;**Retrieve paths**&lt;/span&gt;:
&lt;span class="p"&gt;  -&lt;/span&gt; Current worktree path: &lt;span class="sb"&gt;`$(pwd)`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; Main repository path: Run &lt;span class="sb"&gt;`git rev-parse --git-common-dir`&lt;/span&gt; and resolve it to its parent directory.
&lt;span class="p"&gt;
3.&lt;/span&gt; &lt;span class="gs"&gt;**Navigate to the main repository**&lt;/span&gt;:
  Change directory to the main repository. For example:
  &lt;span class="sb"&gt;`cd /Users/xxx/dev/git/your-project`&lt;/span&gt;
&lt;span class="p"&gt;
4.&lt;/span&gt; &lt;span class="gs"&gt;**Remove the worktree**&lt;/span&gt;:
  Delete the worktree directory using git worktree command:
  &lt;span class="sb"&gt;`git worktree remove &amp;lt;worktree-path&amp;gt;`&lt;/span&gt;
  For example:
  &lt;span class="sb"&gt;`git worktree remove /Users/xxx/.gemini/antigravity/worktrees/your-project/feature-branch`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lors de la demande de pousser le code, le Skill est bien utilisé et, je cite Antigravity, “Le dépôt est propre et à jour avec la branche distante” et “Le worktree temporaire a été entièrement et proprement supprimé”.&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-antigravity-worktrees%2F4-use-skills.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%2F2026-antigravity-worktrees%2F4-use-skills.jpg" alt="Utilisation d'un skills" width="799" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Antigravity &amp;amp; worktrees
&lt;/h2&gt;

&lt;p&gt;L’utilisation des worktrees par Antigravity amène encore un “plus” dans l’accélération du développement de nos projets et de la Developer Expérience (DX) en réduisant notre charge mentale liée à la gestion potentielle des conflits qui pourraient arriver lors de l’utilisation d’agents en parallèle. Je n’étais initialement pas un adepte des Git worktrees mais avec l’intégration de l’IA agentique dans notre quotidien et de la manière dont on orchestre des agents pour faire évoluer nos projets, je suis prêt à parier que cette fonctionnalité de Git va connaître une réelle montée en popularité 😁.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>antigravity</category>
      <category>development</category>
    </item>
    <item>
      <title>Retour sur le Google Cloud Summit 2026 ⛅</title>
      <dc:creator>Jean-Phi Baconnais</dc:creator>
      <pubDate>Thu, 11 Jun 2026 11:40:36 +0000</pubDate>
      <link>https://dev.to/zenika/retour-sur-le-google-cloud-summit-2026-193e</link>
      <guid>https://dev.to/zenika/retour-sur-le-google-cloud-summit-2026-193e</guid>
      <description>&lt;p&gt;Le jeudi 4 juin, l’Accor Arena de Bercy recevait le &lt;strong&gt;Google&lt;/strong&gt; &lt;strong&gt;Cloud Summit Paris.&lt;/strong&gt; Près de 4 000 personnes, des partenaires, des clients et des passionnés des produits Google Cloud se sont retrouvés pour découvrir les nouveautés des produits Google et partager des retours d’expérience. &lt;/p&gt;

&lt;p&gt;Un événement orienté plus technique était organisé en parallèle du Cloud Summit : le &lt;strong&gt;&lt;a href="https://cloud.google.com/events/intl/fr-fr/builder-connect-france-2026" rel="noopener noreferrer"&gt;Builder Connect&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Zenika, en tant que partenaire, était bien entendu présent. Une équipe prête à démontrer l’expertise de Zenika sur la partie Cloud et IA avec une démonstration créée pour l’évènement était présent sur notre stand. &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-cloud-summit%2F2-stand-zenika.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-cloud-summit%2F2-stand-zenika.png" alt="Stand Zenika au Google Cloud Summit" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo prise par Clément Pitorre.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La journée a débuté par la keynote d'Anthony Cirot, &lt;em&gt;Vice-président Google Cloud EMEA&lt;/em&gt;, qui a ouvert une table ronde dédiée au cloud souverain « made with Google » avec la solution S3ns. Né d’une collaboration avec Thales, S3ns vient d’être récemment certifié “&lt;strong&gt;SecNumCloud&lt;/strong&gt;”. &lt;/p&gt;

&lt;p&gt;S3ns ne s’arrête pas là. Une nouvelle entité allemande a été annoncée (cd &lt;a href="https://www.pole-excellence-cyber.org/evenements/thales-google-cloud-souverain-allemagne/" rel="noopener noreferrer"&gt;https://www.pole-excellence-cyber.org/evenements/thales-google-cloud-souverain-allemagne/&lt;/a&gt;) et illustre la volonté de créer des infrastructures sécurisées en Europe.&lt;/p&gt;

&lt;p&gt;Olivier Nollent, &lt;em&gt;Managing Director&lt;/em&gt; de. SAP France était sur scène, et confirmait son choix de basculer son infrastructure vers S3ns amenant à ses clients une conformité et un cadre juridique sécurisé.&lt;/p&gt;

&lt;p&gt;Suite à cette table ronde, Hamidou Dia, &lt;em&gt;VP Applied AI Engineering&lt;/em&gt;, est intervenu pour faire un retour de la Google Cloud Next. Son message était clair : la phase d'expérimentation de l’IA est terminée et le déploiement de l'IA agentique au sein des applications métier en production est le nouveau terrain de jeux et Google est prêt avec sa ligne verticale de produits, allant de la conception des chipsets jusqu'au monitoring complet des agents.&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-cloud-summit%2F4-news.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%2F26-cloud-summit%2F4-news.jpg" alt="Hamidou Dia sur la scène du Google Cloud Summit" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hamidou a ensuite parcouru les nouveaux de chaque étape verticale, allant des nouveaux processeurs 8t et 8i, notamment utilisés pour leurs propres services comme YouTube et Chrome jusqu’à Gemini Enterprise Agent Platform permettant de concevoir et produire des agents.&lt;/p&gt;

&lt;p&gt;Voici quelques nouvelles : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduction de Gemini Enterprise Agent Platform pour la conception et production d’agents.&lt;/li&gt;
&lt;li&gt;L’arrivée d’Agent Identity (traçabilité), Agent Registry (gestion d'agents), Agent Gateway (gestion des politiques) et Model Armor.&lt;/li&gt;
&lt;li&gt;Optimisation et Outils : Mise en prévisualisation de l'Agent Observability, ainsi que l'arrivée prochaine de Gemini Spark et l'intégration d'Antigravity au sein de Gemini Enterprise.&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%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-cloud-summit%2F5-agentic-era.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%2F26-cloud-summit%2F5-agentic-era.jpg" alt="Slide sur Gemini Enterprise Agent Platform" width="800" height="600"&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-cloud-summit%2F6-gemini.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%2F26-cloud-summit%2F6-gemini.jpg" alt="Démonstration d'application Marathon de Paris" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La matinée s’est poursuivie avec une démonstration de l’intégration de l’IA dans une application. Son exemple, une application autour d’un (futur?) Marathon de Paris « by night ».&lt;/p&gt;

&lt;p&gt;De la préparation du trajet au coaching personnalisé des athlètes tout en passant par l’estimation des temps d’arrivée pour les organisateurs·trice, Gemini permet de répondre à ces fonctionnalités, rendant l’application très complète et utilise pour les différents cas d’usage cités.&lt;/p&gt;

&lt;p&gt;Suite à cette démo client, j’ai dû quitter la grande salle de l’Accord Arena pour aller vérifier la configuration de mon poste dans la salle où, avec Benjamin Bourgeois, nous animerons un atelier. Car oui, lors de cette journée, nous avons eu la chance de présenter notre workshop autour d’Antigravity, l’outil de développement agentique de Google lors du Builder Connect !&lt;/p&gt;

&lt;p&gt;Pendant la Builder Connect, des “open code lab” étaient mis en place. L’idée ? Les personnes viennent avec leur ordinateur, font un ou plusieurs codes lab sur des produits Google, comme ADK, Antigravity, A2UI etc et des Googler sont disponibles pour aider en cas de besoin ou éclaircir certaines choses. En tant que Google Developer Expert (GDE), nous avons pu intervenir pour aider les Googler à répondre aux questions des personnes présentes dans ces bulles de code lab.&lt;/p&gt;

&lt;p&gt;Pendant cette journée du côté du Builder Connect j’ai pu assister à deux conférences : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generative UI : L'avenir des applications ? de Elaine Dias Batista - Senior Staff Engineer, Google Developer Expert - Sfeir 
Une conférence intéressante autour de GenUI SDK que je découvrais réellement. Cet outil opensource permet d’avoir une UI qui s’adapte aux réponses que votre agent peut vous donner. &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%2Fjeanphi-baconnais.gitlab.io%2Fimg%2F26-cloud-summit%2F7-genui.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%2F26-cloud-summit%2F7-genui.jpg" alt="Conférence Generative UI par Elaine Dias Batista" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qui a marqué le plus de buts ? Construire un agent IA qui interroge des données en langage naturel de Mazlum Tosun, Google Developer Expert, GroupBees &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conférence que je voyais pour la seconde fois. Mazloum présente et explique le choix d’architecture de son application agentique lui permettant de faire de solliciter sa base BigQuery à travers des requêtes faites en langage naturel.&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-cloud-summit%2F8-talk-mazloum.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%2F26-cloud-summit%2F8-talk-mazloum.jpg" alt="Conférence de Mazlum Tosun sur BigQuery et IA" width="800" height="600"&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-cloud-summit%2F9-cloud-night.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%2F26-cloud-summit%2F9-cloud-night.jpg" alt="Concert de la Cloud Night" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pour terminer la soirée, la Cloud Night, un groupe de Googler ont repris 4 chansons. Plusieurs articles allaient continuer d’enflammer la salle mais du fait d’avoir notre train, nous avons dû quitter la salle.&lt;/p&gt;

&lt;p&gt;Ce fut une nouvelle édition du Google Cloud Summit passionnante, c’est toujours intéressant de voir ou revoir les nouveautés des produits Google et d’échanger avec la communauté. Le Builder Connect, plus orienté technique, est vraiment intéressant. Merci à Guillaume Laforge pour sa confiance dans notre atelier sur Antigravity 🙏  A l’année prochaine je l’espère !&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-cloud-summit%2F10-coffee.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%2F26-cloud-summit%2F10-coffee.jpg" alt="Fin du Google Cloud Summit" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>google</category>
      <category>developers</category>
      <category>cloud</category>
    </item>
    <item>
      <title>🌱 Keep Feeding Your CI/CD — Or Watch It Die</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Sun, 24 May 2026 21:10:38 +0000</pubDate>
      <link>https://dev.to/zenika/keep-feeding-your-cicd-or-watch-it-die-2ci9</link>
      <guid>https://dev.to/zenika/keep-feeding-your-cicd-or-watch-it-die-2ci9</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;A pipeline has a metabolism&lt;/li&gt;
&lt;li&gt;Anatomy of a starving pipeline&lt;/li&gt;
&lt;li&gt;The maturity ladder we're stuck on&lt;/li&gt;
&lt;li&gt;A proactive CI/CD engineer pays for themselves — eightfold&lt;/li&gt;
&lt;li&gt;What "feeding" looks like in practice&lt;/li&gt;
&lt;li&gt;The conversation cheat sheet&lt;/li&gt;
&lt;li&gt;The AI era: the organism needs to digest faster than ever&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;The day our client said "great, the CI/CD is done!" — that was its first missed meal. &lt;strong&gt;A pipeline is not a deliverable with a due date. It's a living organism.&lt;/strong&gt; And right now, across thousands of companies, it's slowly starving while everyone wonders why deployments keep getting harder.&lt;/p&gt;

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

&lt;p&gt;We've all been there. A team spends weeks — sometimes months — setting up a solid CI/CD pipeline. Linting, unit tests, integration tests, automated deployments, maybe even some security scanning. The client nods approvingly. The budget line item gets checked off. And then... nothing. The pipeline enters maintenance mode, which in practice means &lt;em&gt;no maintenance at all&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This "set it and forget it" mentality is one of the most expensive misconceptions in modern software engineering. As Jez Humble puts it: we should never &lt;a href="https://continuousdelivery.com/principles/" rel="noopener noreferrer"&gt;treat transformation as a project to be embarked on and then completed&lt;/a&gt; so we can return to business as usual. CI/CD is not a destination — it's a discipline. The Toyota Production System understood this decades ago with the concept of Kaizen: continuous improvement is not a phase, it's the default state. (Spoiler: Toyota did okay with this approach.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core problem is economic misframing.&lt;/strong&gt; Clients see CI/CD as an infrastructure cost — something you build once, like a road. But a pipeline is not a road. It's more like a garden. Or, if we're being precise, a living organism that needs regular feeding to survive.&lt;/p&gt;

&lt;p&gt;This article is for every developer who's watched a pipeline slowly rot, and for every decision-maker who genuinely doesn't understand why stopping CI/CD investment is the most expensive decision they'll make this year.&lt;/p&gt;

&lt;h1&gt;
  
  
  A pipeline has a metabolism
&lt;/h1&gt;

&lt;p&gt;A CI/CD pipeline &lt;em&gt;consumes&lt;/em&gt;. Every day, it processes new code, new dependencies, new framework versions, new security advisories, new infrastructure configurations. It has inputs (commits, merge requests, schedules) and outputs (artifacts, deployments, reports). It has a digestive system (build stages), an immune system (tests and security scans), and a nervous system (notifications, dashboards, DORA metrics).&lt;/p&gt;

&lt;p&gt;When we feed it properly — updating test suites, optimizing build times, adding new quality gates as the codebase evolves — it stays healthy. Fast builds, reliable tests, confident deployments. The team trusts it, leans on it, accelerates through it.&lt;/p&gt;

&lt;p&gt;When we stop feeding it, the symptoms appear gradually, like any organism under stress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flaky tests multiply.&lt;/strong&gt; Nobody curates the test suite, so intermittent failures become background noise. Developers start re-running pipelines "just in case." (The CI/CD equivalent of percussive maintenance on a vending machine.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build times creep up.&lt;/strong&gt; Dependencies bloat, caches expire, parallelization configs become stale. What used to take 5 minutes now takes 20. Developers context-switch. Flow state dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security scans get disabled.&lt;/strong&gt; They're slow, they flag too many false positives, nobody has time to triage. "We'll re-enable them later." We won't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline code becomes tribal knowledge.&lt;/strong&gt; The engineer who set it up left six months ago. Nobody dares touch the YAML. It is now &lt;a href="https://dora.dev/capabilities/continuous-integration/" rel="noopener noreferrer"&gt;sacred, mysterious, and feared&lt;/a&gt; — the Ark of the Covenant of your repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The DORA research program at Google documents these anti-patterns extensively. A pipeline where &lt;a href="https://dora.dev/capabilities/test-automation/" rel="noopener noreferrer"&gt;failing tests go unaddressed&lt;/a&gt; is a pipeline in immune collapse. A pipeline where &lt;a href="https://dora.dev/capabilities/continuous-integration/" rel="noopener noreferrer"&gt;builds take longer than 10 minutes&lt;/a&gt; is a pipeline with a metabolic disorder. The organism isn't dead yet — but it's on life support, and nobody's checking the monitors.&lt;/p&gt;

&lt;h1&gt;
  
  
  Anatomy of a starving pipeline
&lt;/h1&gt;

&lt;p&gt;Here's what pipeline starvation looks like month by month. If this timeline feels uncomfortably familiar, that's the point.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Month 1&lt;/strong&gt; — The pipeline is fast, green, and trusted. Developers push code with confidence. Tests catch real bugs. Deployments happen multiple times a week. Life is good. 💚&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Month 3&lt;/strong&gt; — A few flaky tests appear. They get muted with a &lt;code&gt;retry: 2&lt;/code&gt; or an &lt;code&gt;allow_failure: true&lt;/code&gt;. "We'll fix them when we have time." The test suite is now slightly porous — like a net with a few holes. Small fish start slipping through.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Month 6&lt;/strong&gt; — Build times have quietly doubled. Dependencies are two minor versions behind, three for the frameworks nobody wants to touch. Security scans get disabled "temporarily" because they add 8 minutes and flag 47 medium-severity CVEs that nobody has bandwidth to triage. The pipeline still runs. It just doesn't protect much anymore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Month 9&lt;/strong&gt; — Developers stop trusting the pipeline. Red builds are normal. "It's always red, just merge anyway." The team develops tribal workarounds: "ignore this stage," "that test only works on Tuesdays," "the deploy job needs a manual retry." New developers can't tell real failures from noise. The pipeline has become Schrödinger's quality gate: simultaneously passing and failing, useful only when observed by someone who remembers the workarounds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Month 12&lt;/strong&gt; — Manual QA is back. Hotfix cycles dominate sprint planning. The client asks why delivery is slower than last year. A consultant is brought in to assess the situation. Their first question: "When was the last time someone invested in the pipeline?" Silence. 💀&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As Martin Fowler puts it with his &lt;a href="https://martinfowler.com/articles/is-quality-worth-cost.html" rel="noopener noreferrer"&gt;kitchen metaphor&lt;/a&gt;: we can't cook without making a mess, but if we don't clean as we go, the muck dries up, gets harder to remove, and eventually prevents us from cooking at all. The pipeline &lt;em&gt;is&lt;/em&gt; the kitchen. And right now, there's dried spaghetti sauce on the ceiling and nobody can find a clean pan.&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%2Fondvdamu0n4cbd7vb2rz.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%2Fondvdamu0n4cbd7vb2rz.jpg" alt="Chibi octopus at the controls" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The maturity ladder we're stuck on
&lt;/h1&gt;

&lt;p&gt;CI/CD is not a binary state. It's a &lt;a href="https://www.infoq.com/articles/continuous-delivery-maturity-model/" rel="noopener noreferrer"&gt;spectrum of maturity&lt;/a&gt; with five clearly defined levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Base&lt;/strong&gt; — Version control exists, but builds are manual or semi-automated. Testing is an afterthought. Deployments involve a checklist, a prayer, and occasionally a sacrifice to the demo gods.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Beginner&lt;/strong&gt; — Basic CI is in place. Unit tests run automatically. The team has a pipeline, but it's fragile and partially manual. Most organizations reach this level and declare victory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intermediate&lt;/strong&gt; — This is where the industry average sits. The pipeline covers the full lifecycle: build, test, deploy. But it's maintained reactively — fixed when it breaks, ignored otherwise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Advanced&lt;/strong&gt; — A dedicated tools or platform team exists. Pipeline improvements are planned and prioritized alongside product features. Test suites are actively curated. DORA metrics are tracked and acted upon.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expert&lt;/strong&gt; — Cross-functional teams own their full delivery pipeline. Deployments are a non-event. The pipeline itself is tested, versioned, and continuously improved. Roll-forward-only strategies. Feature flags instead of release branches.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the uncomfortable truth: &lt;strong&gt;most organizations sit at Level 2 and think they're at Level 4.&lt;/strong&gt; They built a pipeline once, it worked, and they moved on. It's the engineering equivalent of going to the gym once and wondering why the abs aren't showing. But the maturity model is not a staircase we climb once — it's an escalator moving downward. Stop walking, and we slide back to where we started. Every month without active investment erodes our position on the ladder.&lt;/p&gt;

&lt;p&gt;The jump from Level 2 to Level 3 is where most of the immediate ROI lives: automated quality gates that catch bugs before they reach production, build optimizations that save developer hours every week, security scanning that finds vulnerabilities before attackers do. But this jump requires &lt;em&gt;sustained&lt;/em&gt; investment — not a one-time sprint, but a recurring budget line that funds pipeline health the same way we fund product features.&lt;/p&gt;

&lt;h1&gt;
  
  
  A proactive CI/CD engineer pays for themselves — eightfold
&lt;/h1&gt;

&lt;p&gt;Here's where we switch from metaphors to spreadsheets. Because the economic case for continuous CI/CD investment isn't just compelling — it's overwhelming. &lt;strong&gt;A dedicated CI/CD engineer doesn't cost the organization money. They print it.&lt;/strong&gt; (Legally, even.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The HP FutureSmart transformation&lt;/strong&gt; is perhaps the most documented case study in CI/CD history. A division of 400 engineers across the US, Brazil, and India &lt;a href="https://continuousdelivery.com/evidence-case-studies/" rel="noopener noreferrer"&gt;invested continuously for three years&lt;/a&gt; in test automation and continuous integration. The results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overall development costs reduced by &lt;strong&gt;~40%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Number of programs under development increased by &lt;strong&gt;~140%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Development cost per program dropped &lt;strong&gt;78%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Resources driving innovation increased &lt;strong&gt;eightfold&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that last number again. &lt;em&gt;Eightfold.&lt;/em&gt; The team didn't just get faster — they freed up so much capacity that they could pursue eight times more innovation. And the critical insight from this case study is that these savings were only possible on the basis of a &lt;em&gt;large and ongoing investment&lt;/em&gt; in pipeline infrastructure. Not a one-shot project. Three years of continuous feeding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Suncorp Group&lt;/strong&gt; (Australian financial services) &lt;a href="https://continuousdelivery.com/evidence-case-studies/" rel="noopener noreferrer"&gt;reduced 15 complex insurance systems to 2&lt;/a&gt;, decommissioned 12 legacy systems, and reported savings of &lt;strong&gt;$225 million in 2015&lt;/strong&gt; and &lt;strong&gt;$265 million in 2016&lt;/strong&gt;. Again: sustained, multi-year investment in delivery infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DORA research&lt;/strong&gt; (spanning 2014–2024, tens of thousands of respondents) consistently shows that organizations with high-performing delivery pipelines are &lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;twice as likely to exceed their profitability, market share, and productivity goals&lt;/a&gt;. Teams that combine version control with continuous delivery are &lt;a href="https://dora.dev/capabilities/continuous-delivery/" rel="noopener noreferrer"&gt;2.5x more likely&lt;/a&gt; to have high software delivery performance.&lt;/p&gt;

&lt;p&gt;The math is brutally simple. One FTE dedicated to pipeline maintenance — curating tests, optimizing builds, updating security gates, tracking metrics — can easily save three to five FTEs worth of manual QA, hotfix cycles, integration delays, and context-switching costs. HP proved the extreme case: their CI/CD investment freed up &lt;strong&gt;eight times&lt;/strong&gt; the resources they put in. The pipeline engineer is not a cost center. They are the single highest-leverage hire on the team — a force multiplier on every developer-hour in the organization. (Or, in biological terms: the veterinarian costs far less than replacing the whole herd.)&lt;/p&gt;

&lt;p&gt;As David Farley puts it: the real trade-off, over long periods of time, is between &lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;better software faster and worse software slower&lt;/a&gt;. There is no third option where we get good software cheap by not investing in delivery.&lt;/p&gt;

&lt;h1&gt;
  
  
  What "feeding" looks like in practice
&lt;/h1&gt;

&lt;p&gt;Enough theory. Here's the concrete menu for a healthy pipeline — the regular meals that keep the organism thriving.&lt;/p&gt;

&lt;h3&gt;
  
  
  Curate the test suite
&lt;/h3&gt;

&lt;p&gt;Tests are the pipeline's immune system. But an immune system that triggers on everything (false positives) or misses real threats (gaps in coverage) is worse than useless — it breeds mistrust. We need to regularly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove or fix flaky tests (quarantine, don't mute)&lt;/li&gt;
&lt;li&gt;Add tests for recent production bugs (the feedback loop)&lt;/li&gt;
&lt;li&gt;Keep the full suite &lt;a href="https://dora.dev/capabilities/continuous-integration/" rel="noopener noreferrer"&gt;under 10 minutes&lt;/a&gt; — DORA's threshold for a healthy build&lt;/li&gt;
&lt;li&gt;Review coverage reports not for percentage, but for &lt;em&gt;which critical paths&lt;/em&gt; are covered&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Optimize build times
&lt;/h3&gt;

&lt;p&gt;Speed is not a luxury. When builds are fast, developers stay in flow. When builds are slow, they context-switch, and the cost of that context-switch compounds silently. Practical levers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache aggressively (dependencies, Docker layers, build artifacts)&lt;/li&gt;
&lt;li&gt;Parallelize independent stages&lt;/li&gt;
&lt;li&gt;Fail fast — put lint and compile before heavy integration tests&lt;/li&gt;
&lt;li&gt;Measure. We detailed practical techniques in &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; and pushed them even further in &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;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Add and maintain security gates
&lt;/h3&gt;

&lt;p&gt;Security scanning is the pipeline's early warning system. But it only works if someone triages the results. An unreviewed security report is the equivalent of a fire alarm that everyone has learned to ignore — technically present, functionally absent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Track DORA metrics and act on them
&lt;/h3&gt;

&lt;p&gt;The four key metrics — deployment frequency, lead time for changes, change failure rate, and mean time to recovery — are the pipeline's vital signs. Tracking them without acting is like wearing a fitness tracker while eating pizza on the couch. Useful data, zero impact. We explored how to compute these in &lt;a href="https://dev.to/zenika/gitlab-a-python-script-calculating-dora-metrics-258o"&gt;GitLab: A Python Script Calculating DORA Metrics&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automate dependency updates
&lt;/h3&gt;

&lt;p&gt;Outdated dependencies are cholesterol in the pipeline's arteries. They accumulate silently, create invisible security exposure, and eventually cause a painful emergency upgrade that blocks everything for a week. The dreaded "update all the things" sprint — every team's least favorite holiday. Tools like Renovate or Dependabot turn this into a steady, manageable trickle instead of a catastrophic flood.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invest in pipeline UX
&lt;/h3&gt;

&lt;p&gt;A pipeline that developers can't understand is a pipeline they'll work around. &lt;a href="https://dev.to/zenika/gitlab-ci-job-logs-the-art-of-self-documenting-pipelines-1je1"&gt;GitLab CI Job Logs: The Art of Self-Documenting Pipelines&lt;/a&gt; — clear error messages, collapsible sections with context — these aren't cosmetic improvements. They're the difference between "I can fix this myself" and "I need to find the DevOps engineer." Every ticket avoided is capacity recovered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dedicate platform engineering time
&lt;/h3&gt;

&lt;p&gt;Even a fractional allocation — 20% of one engineer's time — creates compound returns. Someone who actively watches pipeline health, proposes improvements, and treats the pipeline as a product (not a tool) transforms the team's delivery capability over months. At maturity Level 4, this becomes a dedicated tools team. But it starts with simply deciding that pipeline health is someone's explicit responsibility, not everyone's implicit afterthought.&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%2F5jzmxc8ifwmmsyptosx0.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%2F5jzmxc8ifwmmsyptosx0.jpg" alt="Chibi octopus operating a control room" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The conversation cheat sheet
&lt;/h1&gt;

&lt;p&gt;We know the pipeline needs feeding. But the budget holder doesn't. Here's how we frame the conversation in a language that decision-makers understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speak in FTEs, not features
&lt;/h3&gt;

&lt;p&gt;"We need to update our test suite" means nothing to a CFO. "We can save 2 FTEs of manual QA effort per quarter by investing 0.5 FTE in pipeline maintenance" is a sentence that changes budgets. Translate every pipeline improvement into the people-hours it saves or the risk it mitigates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show the trend, not the snapshot
&lt;/h3&gt;

&lt;p&gt;A single DORA metric reading means little. A six-month trend showing lead time increasing from 2 days to 2 weeks while change failure rate climbs from 5% to 25% is a story that writes itself. Collect the data. Plot the graph. Let the numbers do the arguing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the "what if we stopped" experiment
&lt;/h3&gt;

&lt;p&gt;Ask the team: "What would happen if we removed all automated tests tomorrow?" Watch the room go pale. Then ask: "What's the difference between removing them and letting them slowly become unreliable?" The answer is: only the speed at which things break. The destination is the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the kitchen metaphor
&lt;/h3&gt;

&lt;p&gt;For non-technical stakeholders, Fowler's kitchen analogy lands perfectly: "Imagine a restaurant that decides cleaning the kitchen is a one-time project. The first week is fine. By month three, health inspectors shut them down. Our pipeline is the kitchen where we prepare every release."&lt;/p&gt;

&lt;h3&gt;
  
  
  Frame as insurance, then as investment
&lt;/h3&gt;

&lt;p&gt;Start with risk mitigation (security vulnerabilities, production incidents, compliance exposure). Once that resonates, pivot to growth: "With a healthy pipeline, we can ship twice as fast with half the defects. HP proved this with 400 engineers over 3 years."&lt;/p&gt;

&lt;h1&gt;
  
  
  The AI era: the organism needs to digest faster than ever
&lt;/h1&gt;

&lt;p&gt;Think about what a CI/CD engineer really does: they automate the automator. Developers write code that automates business processes; CI/CD engineers write systems that automate &lt;em&gt;developers&lt;/em&gt;. They are the meta-layer — the force multiplier on top of the force multiplier.&lt;/p&gt;

&lt;p&gt;Now AI is doing the exact same thing. Tools like GitHub Copilot, Cursor, and Claude Code are accelerating how fast developers produce code. More commits, more features, more merge requests — faster than ever before. And here's the uncomfortable implication: &lt;strong&gt;if the pipeline was already starving at human speed, what happens when AI-assisted developers start shipping at 3x the pace?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Boris Cherny, creator and head of Claude Code at Anthropic, &lt;a href="https://www.linkedin.com/feed/update/urn:li:share:7437841770157244417/" rel="noopener noreferrer"&gt;made this point bluntly&lt;/a&gt; during an AMA at Station F. When asked &lt;em&gt;"What prevents you from building faster today?"&lt;/em&gt;, his answer wasn't code reviews, wasn't hiring, wasn't technical debt. It was: &lt;strong&gt;"The new major issue is CI… especially GitHub Actions."&lt;/strong&gt; The person building one of the most advanced AI coding tools on the planet is bottlenecked by CI/CD. If even Anthropic's pipeline can't keep up with AI-assisted development velocity, imagine what's happening in teams that stopped investing in theirs six months ago.&lt;/p&gt;

&lt;p&gt;We are entering a world where writing code is becoming dramatically faster — but running and validating it is not. The organism isn't just hungry anymore. It's being asked to digest twice the food with half the stomach. The CI/CD engineer was already the highest-leverage hire on the team. In the AI era, they might be the most critical.&lt;/p&gt;

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

&lt;p&gt;Our pipeline is not a tool we installed and forgot. It's the heartbeat of our team's velocity — the living system that transforms code into value, commits into deployments, ideas into shipped features.&lt;/p&gt;

&lt;p&gt;Every week we feed it — curating tests, optimizing builds, updating security gates, tracking metrics — it gets a little faster, a little more reliable, a little more trusted. The compound returns are extraordinary: HP saw 8x innovation capacity; DORA shows 2x profitability for high performers.&lt;/p&gt;

&lt;p&gt;Every week we starve it, it degrades a little further. Tests rot. Builds slow. Trust erodes. And eventually, we're back to manual QA, hotfix sprints, and the question nobody wants to answer: "Why is everything taking so long?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feed your pipeline. Track its vital signs. Treat it like the living, breathing infrastructure it is.&lt;/strong&gt; The cost of feeding is measured in hours per week. The cost of starvation is measured in months of lost velocity.&lt;/p&gt;

&lt;p&gt;And if someone tells us "the CI/CD is done" — we now have the data, the metaphors, and the case studies to explain why that sentence is the most expensive thing they'll say all year.&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%2Fn24vndpyrya9c613juaa.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%2Fn24vndpyrya9c613juaa.jpg" alt="Chibi octopus multitasking at the console" width="800" height="350"&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" width="500" height="500"&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="" width="500" height="500"&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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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 crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&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>devops</category>
      <category>cicd</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>📝 Documentation-as-Code Has Silently Won For Tech Content</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Wed, 29 Apr 2026 11:53:51 +0000</pubDate>
      <link>https://dev.to/zenika/documentation-as-code-has-silently-won-for-tech-content-e5o</link>
      <guid>https://dev.to/zenika/documentation-as-code-has-silently-won-for-tech-content-e5o</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;
1. The power of developer tools

&lt;ul&gt;
&lt;li&gt;Git: complete traceability&lt;/li&gt;
&lt;li&gt;Pull requests: quality through review&lt;/li&gt;
&lt;li&gt;Write once, publish everywhere&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

2. Live documentation

&lt;ul&gt;
&lt;li&gt;The wiki problem: drift and duplication&lt;/li&gt;
&lt;li&gt;Same PR, same review, same deployment&lt;/li&gt;
&lt;li&gt;CI/CD enforcement&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;3. Everything-as-code applied to documentation&lt;/li&gt;

&lt;li&gt;

4. The AI revolution

&lt;ul&gt;
&lt;li&gt;AI-powered writing assistance&lt;/li&gt;
&lt;li&gt;AI-readable context&lt;/li&gt;
&lt;li&gt;AI and diagram-as-code: a powerful combination&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

5. Technical vs. non-technical contributors: choosing the right approach

&lt;ul&gt;
&lt;li&gt;The barrier is real&lt;/li&gt;
&lt;li&gt;For technical teams: docs-as-code is the clear winner&lt;/li&gt;
&lt;li&gt;For functional/business contributors: consider alternatives&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;Documentation has long been the neglected sibling of software development. &lt;strong&gt;For technical teams, a clear winner has emerged: Documentation-as-Code.&lt;/strong&gt; By treating docs like source code — versioned in Git, reviewed in pull requests, built in CI/CD — developers achieve unprecedented quality, collaboration, and maintainability. But this approach isn't for everyone — and that's okay.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Picture this:&lt;/strong&gt; A team updates their API, but the documentation lives in Confluence. Two weeks later, a developer spends hours debugging an integration issue, only to discover the docs are outdated. The fix? Someone eventually updates the wiki — until the next API change.&lt;/p&gt;

&lt;p&gt;This scenario plays out daily across thousands of organizations. The fundamental problem? &lt;strong&gt;Documentation that lives outside the development workflow inevitably drifts from reality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Documentation-as-Code (docs-as-code) solves this by bringing docs into the same workflow as code: same repository, same review process, same CI/CD pipeline. This philosophy has emerged from &lt;a href="https://konghq.com/blog/learning-center/what-is-docs-as-code" rel="noopener noreferrer"&gt;best practices&lt;/a&gt; at leading tech companies like Google, Microsoft, and GitHub. The core insight? &lt;strong&gt;Documentation is code&lt;/strong&gt; — it has syntax, structure, and dependencies. Treating it any differently creates unnecessary friction.&lt;/p&gt;

&lt;p&gt;The strength of docs-as-code builds progressively: &lt;strong&gt;developer tools&lt;/strong&gt;, &lt;strong&gt;live documentation&lt;/strong&gt;, &lt;strong&gt;everything-as-code&lt;/strong&gt;, and ultimately &lt;strong&gt;the AI revolution&lt;/strong&gt;. Let's see how.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. The power of developer tools
&lt;/h1&gt;

&lt;p&gt;Docs-as-code leverages the sophisticated tooling developers already use daily — and most of it is &lt;strong&gt;free and open-source&lt;/strong&gt;, eliminating expensive CCMS licensing costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git: complete traceability
&lt;/h2&gt;

&lt;p&gt;Git provides capabilities that no wiki can match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete history&lt;/strong&gt; — Every change tracked, every author identified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blame&lt;/strong&gt; — Instantly find who wrote what and when&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bisect&lt;/strong&gt; — Track down when documentation became incorrect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching&lt;/strong&gt; — Work on updates without affecting the live version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt; — Git-based tools are faster and less buggy than enterprise CCMS platforms (goodbye, random 500 errors)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pull requests: quality through review
&lt;/h2&gt;

&lt;p&gt;Teams report up to &lt;a href="https://aws.amazon.com/blogs/infrastructure-and-automation/reduce-project-delays-with-docs-as-code-solution/" rel="noopener noreferrer"&gt;50% reduction in documentation time&lt;/a&gt; compared to traditional approaches. The key? Pull requests that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get reviewed by subject matter experts&lt;/li&gt;
&lt;li&gt;Preserve discussions alongside changes&lt;/li&gt;
&lt;li&gt;Ensure nothing goes live without approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose documentation to the entire team&lt;/strong&gt; — unlike wiki edits that go unnoticed, PR-based docs get visibility from everyone watching the repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mirrors the benefits of code review itself — as we discussed in &lt;a href="https://dev.to/zenika/every-developer-should-review-code-not-just-seniors-2abc"&gt;Every Developer Should Review Code — Not Just Seniors&lt;/a&gt;, review isn't just a quality gate, it's a learning accelerator. The same applies to documentation: &lt;strong&gt;more eyes, fewer lies&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write once, publish everywhere
&lt;/h2&gt;

&lt;p&gt;With Markdown or AsciiDoc and modern static site generators (Hugo, MkDocs, Antora, Docusaurus), the same content produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Searchable websites&lt;/li&gt;
&lt;li&gt;PDF documentation&lt;/li&gt;
&lt;li&gt;API references&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slides and presentations&lt;/strong&gt; — Frameworks like &lt;a href="https://revealjs.com/" rel="noopener noreferrer"&gt;reveal.js&lt;/a&gt; generate slides from the same source; IDE extensions like &lt;a href="https://marketplace.visualstudio.com/items?itemName=evilz.vscode-reveal" rel="noopener noreferrer"&gt;vscode-reveal&lt;/a&gt; let you present Markdown documentation instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also gain &lt;strong&gt;full control over site appearance and features&lt;/strong&gt; — no vendor lock-in, no waiting for feature requests. For a complete example, see our open-source &lt;a href="https://github.com/Zenika/asciidoc-stack" rel="noopener noreferrer"&gt;asciidoc-stack&lt;/a&gt; — a mono-repository that generates HTML, PDF, and reveal.js presentations from the same AsciiDoc sources.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Live documentation
&lt;/h1&gt;

&lt;p&gt;With developer tools in place, the real payoff emerges: &lt;strong&gt;live documentation&lt;/strong&gt; that stays synchronized with reality. This is the natural consequence of treating docs like code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wiki problem: drift and duplication
&lt;/h2&gt;

&lt;p&gt;Traditional wiki-based documentation suffers from two fatal flaws:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Drift&lt;/strong&gt; — Documentation becomes outdated because it's disconnected from the development workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplication&lt;/strong&gt; — The same information gets copied to multiple places, creating inconsistencies&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At Google, documentation had become so bad that it was the &lt;a href="https://www.idaszak.com/posts/docs-as-code" rel="noopener noreferrer"&gt;number one developer complaint&lt;/a&gt; on internal surveys. Parts were obsolete, others duplicated, and there was no easy way to report errors. The fix? Same as for buggy code: moving all documentation under source control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same PR, same review, same deployment
&lt;/h2&gt;

&lt;p&gt;With docs-as-code, documentation changes happen &lt;strong&gt;in the same pull request as code changes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API endpoint changes? Update the docs in the same PR&lt;/li&gt;
&lt;li&gt;New feature? Documentation is part of the "definition of done"&lt;/li&gt;
&lt;li&gt;Bug fix? Update the troubleshooting guide alongside the code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.gitbook.com/blog/what-is-docs-as-code" rel="noopener noreferrer"&gt;Outdated documentation&lt;/a&gt; confuses customers and increases support requests, while inaccurate internal docs slow down onboarding and increase technical debt. Under time pressure, documentation &lt;a href="https://t2informatik.de/en/blog/documentation-in-code-pros-and-cons" rel="noopener noreferrer"&gt;often doesn't get maintained&lt;/a&gt;, drifting from the updated code. Live documentation solves both problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD enforcement
&lt;/h2&gt;

&lt;p&gt;Pipelines can &lt;strong&gt;enforce&lt;/strong&gt; documentation freshness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fail builds when code changes lack corresponding doc updates&lt;/li&gt;
&lt;li&gt;Run link checkers to catch broken references&lt;/li&gt;
&lt;li&gt;Validate code examples actually compile and run&lt;/li&gt;
&lt;li&gt;Deploy preview sites for documentation changes — as we explored in &lt;a href="https://dev.to/zenika/gitlab-pages-preview-the-no-compromise-hack-to-serve-per-branch-pages-5599"&gt;GitLab Pages per Branch: The No-Compromise Hack to Serve Preview Pages&lt;/a&gt;, every branch can have its own documentation preview&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? &lt;strong&gt;Continuous delivery of documentation&lt;/strong&gt; — docs that cannot drift from code because they're part of the same workflow. No more "the docs say X but the code does Y" debates.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Everything-as-code applied to documentation
&lt;/h1&gt;

&lt;p&gt;The docs-as-code philosophy extends far beyond prose. &lt;strong&gt;Everything that can be expressed as code should be&lt;/strong&gt; — diagrams, charts, timelines, even presentations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagrams&lt;/strong&gt; are a prime example. Traditional diagrams in Draw.io or simular suffer from the same drift problems as wiki documentation: binary or complex files that can't be diffed easily, separate tools requiring context switching, and versions that silently rot in a forgotten folder. With diagrams-as-code, we write text that renders automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Popular diagram tools include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://mermaid.js.org/" rel="noopener noreferrer"&gt;Mermaid&lt;/a&gt; — Native in GitHub/GitLab, flowcharts, sequence diagrams, ER diagrams, Gantt charts, mind maps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://plantuml.com/" rel="noopener noreferrer"&gt;PlantUML&lt;/a&gt; — UML (class, sequence, use case, activity), C4 architecture, network diagrams, wireframes, JSON/YAML visualization&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kroki.io/" rel="noopener noreferrer"&gt;Kroki&lt;/a&gt; — Universal gateway to 20+ diagram engines (D2, Graphviz, BPMN, Structurizr...)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://excalidraw.com/" rel="noopener noreferrer"&gt;Excalidraw&lt;/a&gt; — Hand-drawn style diagrams; stores as JSON that AI can read and tweak (still early days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: &lt;strong&gt;diagrams become reviewable in pull requests&lt;/strong&gt;, just like prose. And because they're text, &lt;strong&gt;AI can create and modify them directly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beyond diagrams&lt;/strong&gt;, &lt;a href="https://github.com/Zenika/asciidoc-stack" rel="noopener noreferrer"&gt;asciidoc-stack&lt;/a&gt; demonstrates many more as-code artifacts: Chart-as-code, Git-graph-as-code, Timeline-as-code, Pyramid-as-code, Word-cloud-as-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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvwuibcfgyw6igdwm05e.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%2Flvwuibcfgyw6igdwm05e.jpg" alt="doc-writer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  4. The AI revolution
&lt;/h1&gt;

&lt;p&gt;The natural outcome of everything-as-code — and the one that changes everything — is AI playground. Plain text formats are the perfect substrate for AI tools, and this is where docs-as-code quietly became non-negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-powered writing assistance
&lt;/h2&gt;

&lt;p&gt;Because Markdown and AsciiDoc are plain text, &lt;strong&gt;the same AI assistant you use for coding works for documentation&lt;/strong&gt;. In your IDE, any AI coding tool with the right model can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draft&lt;/strong&gt; initial documentation from code analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve&lt;/strong&gt; clarity and readability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translate&lt;/strong&gt; to multiple languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate&lt;/strong&gt; examples and tutorials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarize&lt;/strong&gt; complex technical content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No context switching, no separate tools — documentation becomes part of your coding flow. Binary formats like Word or Confluence pages don't get this benefit.&lt;/p&gt;

&lt;p&gt;The result? &lt;strong&gt;Documentation that would never have been written now exists.&lt;/strong&gt; When drafting a Markdown/Asciidoc takes minutes instead of hours, teams document edge cases, add more examples, and keep content current. AI doesn't just speed up writing — it raises the bar for completeness.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-readable context
&lt;/h2&gt;

&lt;p&gt;But the revolution goes both ways. AI doesn't just &lt;strong&gt;write&lt;/strong&gt; documentation — it &lt;strong&gt;reads&lt;/strong&gt; it. When documentation lives as plain text alongside code, AI coding assistants automatically use it as context to understand intent, architecture, and conventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown/Asciidoc files&lt;/strong&gt; help AI suggest code consistent with the project's design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline comments and Architecture Decision Records&lt;/strong&gt; give AI the "why" behind decisions, not just the "what"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Documentation in a wiki or a PDF is invisible to your AI assistant. Documentation-as-code is part of its world. &lt;strong&gt;The better your docs, the smarter your AI becomes&lt;/strong&gt; — creating a virtuous cycle where good documentation directly improves code quality.&lt;/p&gt;

&lt;p&gt;AI integration is &lt;a href="https://dev.to/iam_randyduodu/why-docs-as-code-is-the-key-to-better-software-documentation-4e45"&gt;rapidly becoming a differentiator&lt;/a&gt; for documentation platforms — and docs-as-code with its plain text formats is uniquely positioned to benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI and diagram-as-code: a powerful combination
&lt;/h2&gt;

&lt;p&gt;Because diagrams-as-code are plain text, &lt;strong&gt;AI can both read and produce them&lt;/strong&gt;. This is a game-changer compared to binary diagram formats that remain opaque to AI tools.&lt;/p&gt;

&lt;p&gt;AI can generate and maintain diagrams in many situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture visualization&lt;/strong&gt; — Ask AI to produce a Mermaid or PlantUML diagram from a code analysis or a description of your system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow documentation&lt;/strong&gt; — AI generates sequence or activity diagrams from existing code paths or user stories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision trees&lt;/strong&gt; — Transform complex business rules into readable flowcharts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure mapping&lt;/strong&gt; — Generate network or deployment diagrams from infrastructure-as-code files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagram updates&lt;/strong&gt; — When the code changes, AI can update the corresponding diagrams in the same pull request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a concrete example, &lt;a href="https://dev.to/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma"&gt;this article on GitLab Runner topologies&lt;/a&gt; has &lt;strong&gt;all its diagrams produced and maintained by AI&lt;/strong&gt; — from architecture overviews to timelines. No manual drawing tool was involved.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Technical vs. non-technical contributors: choosing the right approach
&lt;/h1&gt;

&lt;p&gt;Here's the honest truth that docs-as-code advocates sometimes gloss over: &lt;strong&gt;this approach has a real learning curve, and it's not suitable for everyone.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The barrier is real
&lt;/h2&gt;

&lt;p&gt;Some critics argue that &lt;a href="https://thisisimportant.net/posts/docs-as-code-broken-promise/" rel="noopener noreferrer"&gt;docs-as-code is a broken promise&lt;/a&gt;. Others &lt;a href="https://idratherbewriting.com/blog/thoughts-on-docs-as-code-promise" rel="noopener noreferrer"&gt;add nuance&lt;/a&gt; — at Amazon, one team's Git workflow became so complex after a premature merge incident that &lt;strong&gt;a new writer admitted she was afraid to fix a simple typo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The challenges are genuine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git concepts (branches, merges, rebases) are genuinely confusing for non-developers&lt;/li&gt;
&lt;li&gt;Command-line tools intimidate many people&lt;/li&gt;
&lt;li&gt;Markdown/Asciidoc syntax, while simple, is still syntax to learn&lt;/li&gt;
&lt;li&gt;Pull request workflows feel foreign to those outside software development&lt;/li&gt;
&lt;li&gt;Complex Git workflows can paralyze even willing contributors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style consistency&lt;/strong&gt; requires discipline — no CCMS to enforce it automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't problems to dismiss — they're real barriers that affect real people. The solution? &lt;strong&gt;Separate technical and non-technical documentation&lt;/strong&gt;, as we'll see below.&lt;/p&gt;

&lt;h2&gt;
  
  
  For technical teams: docs-as-code is the clear winner
&lt;/h2&gt;

&lt;p&gt;If your documentation contributors are primarily &lt;strong&gt;developers, DevOps engineers, SREs, or other technical roles&lt;/strong&gt;, docs-as-code is unambiguously the best choice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;th&gt;Why it matters for techs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same tools-as-code&lt;/td&gt;
&lt;td&gt;No context switching — stay in VS Code, use Git, leverage existing skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review workflow&lt;/td&gt;
&lt;td&gt;Code review culture extends naturally to docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD integration&lt;/td&gt;
&lt;td&gt;Docs deploy alongside code, same pipeline, same automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI assistance&lt;/td&gt;
&lt;td&gt;Copilot and similar tools work seamlessly with plain text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version control&lt;/td&gt;
&lt;td&gt;Track changes, blame, bisect — all the tools you know&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Technical contributors already live in this ecosystem. Asking them to switch to Confluence or Google Docs for documentation creates friction and reduces quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  For functional/business contributors: consider alternatives
&lt;/h2&gt;

&lt;p&gt;When documentation involves &lt;strong&gt;product managers, business analysts, support teams, or other non-technical stakeholders&lt;/strong&gt;, forcing docs-as-code can backfire.&lt;/p&gt;

&lt;p&gt;The reality is pragmatic: these contributors have &lt;strong&gt;limited familiarity with developer tools&lt;/strong&gt;, established habits with existing platforms, and often &lt;strong&gt;company-wide tooling constraints&lt;/strong&gt; that aren't worth fighting. If everyone already uses Confluence, introducing Git-based workflows for a subset of documentation creates friction without clear benefit. &lt;strong&gt;Choose your battles&lt;/strong&gt; — the goal is documentation that gets written, not ideological consistency.&lt;/p&gt;

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

&lt;p&gt;Documentation-as-Code is &lt;strong&gt;the definitive approach for technical teams&lt;/strong&gt;. When developers write documentation, treating it like code — with Git, pull requests, CI/CD, and AI assistance — delivers unmatched quality and maintainability.&lt;/p&gt;

&lt;p&gt;But we must be pragmatic: &lt;strong&gt;forcing Git workflows on non-technical contributors often backfires&lt;/strong&gt;. The best documentation is the documentation that gets written and maintained. For mixed teams, hybrid approaches or user-friendly platforms with Git backends offer the best of both worlds.&lt;/p&gt;

&lt;p&gt;Choose the right tool for your audience. For your technical docs? Docs-as-code, without hesitation.&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%2Fj3cjh8q726whitlr0arn.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%2Fj3cjh8q726whitlr0arn.jpg" alt="doc-writer"&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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.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;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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>documentation</category>
      <category>devops</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <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://dev.to/zenika/agent-development-kit-20-adk-java-11-et-go-10-2be5</link>
      <guid>https://dev.to/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://dev.to/zenika/gitlab-ci-automated-testing-of-job-rules-1i03</link>
      <guid>https://dev.to/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 yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;ci/&lt;/span&gt;
&lt;span class="s"&gt;└── test/&lt;/span&gt;
    &lt;span class="s"&gt;[...]&lt;/span&gt;
    &lt;span class="s"&gt;├── D-tag-preprod.variables.yml&lt;/span&gt;       &lt;span class="c1"&gt;# Tag triggering preprod deployment&lt;/span&gt;
    &lt;span class="s"&gt;├── D-tag-preprod.csv&lt;/span&gt;                 &lt;span class="c1"&gt;# Expected jobs (reference)&lt;/span&gt;
    &lt;span class="s"&gt;├── F-scheduled.variables.yml&lt;/span&gt;         &lt;span class="c1"&gt;# Scheduled nightly pipeline&lt;/span&gt;
    &lt;span class="s"&gt;└── F-scheduled.csv&lt;/span&gt;                   &lt;span class="c1"&gt;# Expected jobs (reference)&lt;/span&gt;
&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" width="800" height="350"&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" width="800" height="350"&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 yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;ci/test/&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-no-change.variables.yml&lt;/span&gt;          &lt;span class="c1"&gt;# MR with no module changes&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-no-change.csv&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-back.variables.yml&lt;/span&gt;               &lt;span class="c1"&gt;# Backend module changes&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-back.csv&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-front.variables.yml&lt;/span&gt;              &lt;span class="c1"&gt;# Frontend module changes&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-front.csv&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-migrations.variables.yml&lt;/span&gt;         &lt;span class="c1"&gt;# Database migrations&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-migrations.csv&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-all-force-build-labels.variables.yml&lt;/span&gt;  &lt;span class="c1"&gt;# Everything activated&lt;/span&gt;
&lt;span class="s"&gt;├── A-MR-all-force-build-labels.csv&lt;/span&gt;
&lt;span class="s"&gt;├── B-protected-branch.csv&lt;/span&gt;                &lt;span class="c1"&gt;# Non-MR types (no changes: involved)&lt;/span&gt;
&lt;span class="s"&gt;└── ...&lt;/span&gt;
&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. When tests catch a regression, &lt;a href="https://dev.to/zenika/gitlab-ci-job-logs-the-art-of-self-documenting-pipelines-1je1"&gt;GitLab CI Job Logs: The Art of Self-Documenting Pipelines&lt;/a&gt; ensures developers can actually understand what the pipeline is telling them. Treating CI configuration as a living system — not a one-time deliverable — is the central argument of &lt;a href="https://dev.to/zenika/keep-feeding-your-cicd-or-watch-it-die-2ci9"&gt;Keep Feeding Your CI/CD — Or Watch It Die&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%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" width="800" height="350"&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" width="500" height="500"&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="" width="500" height="500"&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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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 crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&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://dev.to/zenika/a-local-agent-with-adk-and-docker-model-runner-283j</link>
      <guid>https://dev.to/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://dev.to/zenika/devto-writers-community-one-command-to-maintain-them-all-2feo</link>
      <guid>https://dev.to/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. If you're curious why plain text won, we dig into that in &lt;a href="https://dev.to/zenika/documentation-as-code-has-silently-won-for-tech-content-e5o"&gt;Documentation-as-Code Has Silently Won For Tech Content&lt;/a&gt;.&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" width="584" height="316"&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;
  
  
  Charts from CSV data
&lt;/h3&gt;

&lt;p&gt;dev.to has no native chart support. Describe a chart as CSV inside a &lt;code&gt;&lt;/code&gt;`&lt;code&gt;chart&lt;/code&gt; block — first line is chart type and options, then data rows. Category charts (&lt;code&gt;bar&lt;/code&gt;, &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;area&lt;/code&gt;…) use a header row starting with &lt;code&gt;x&lt;/code&gt;; &lt;code&gt;pie&lt;/code&gt;, &lt;code&gt;donut&lt;/code&gt;, and &lt;code&gt;gauge&lt;/code&gt; use one &lt;code&gt;name,value&lt;/code&gt; row per slice.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;dev diaggen&lt;/code&gt; to render PNGs locally, commit them to GitHub, then &lt;code&gt;dev push&lt;/code&gt; — same workflow as Kroki diagrams. The blocks below are live: they stay as source in this file and become images on dev.to at push time.&lt;/p&gt;

&lt;p&gt;Supported chart types (C3.js): &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;spline&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;area&lt;/code&gt;, &lt;code&gt;area-spline&lt;/code&gt;, &lt;code&gt;area-step&lt;/code&gt;, &lt;code&gt;bar&lt;/code&gt;, &lt;code&gt;scatter&lt;/code&gt;, &lt;code&gt;pie&lt;/code&gt;, &lt;code&gt;donut&lt;/code&gt;, &lt;code&gt;gauge&lt;/code&gt;. Add &lt;code&gt;stacked=true&lt;/code&gt; on &lt;code&gt;bar&lt;/code&gt; for percentage stacked bars. Options: &lt;code&gt;x-type=category&lt;/code&gt;, &lt;code&gt;x-label&lt;/code&gt;, &lt;code&gt;y-label&lt;/code&gt;, &lt;code&gt;y-range=min_max&lt;/code&gt;, &lt;code&gt;data-labels=true&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;horizontal=true&lt;/code&gt;, &lt;code&gt;x-tick-angle&lt;/code&gt;, &lt;code&gt;legend=bottom&lt;/code&gt;, &lt;code&gt;order=desc&lt;/code&gt;. (&lt;code&gt;scatter&lt;/code&gt; requires paired x/y columns — not yet supported by the CSV format.)&lt;/p&gt;

&lt;p&gt;Five live examples below — close variants are grouped rather than repeated. Each uses several data rows (one per series):&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;bar&lt;/code&gt; — grouped bars (and &lt;code&gt;stacked=true&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Swap in &lt;code&gt;stacked=true&lt;/code&gt; on the first line for percentage stacked bars.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfzqfwros6fl4po1l7s8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfzqfwros6fl4po1l7s8.png" alt="Diagram" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;spline&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt; — trends over time
&lt;/h4&gt;

&lt;p&gt;Same CSV — only the type token changes. Shown here as &lt;code&gt;spline&lt;/code&gt;; use &lt;code&gt;line&lt;/code&gt; for straight segments or &lt;code&gt;step&lt;/code&gt; for stairs.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7cpqq8ymqz8l96c757e4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7cpqq8ymqz8l96c757e4.png" alt="Diagram" width="799" height="389"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;area&lt;/code&gt;, &lt;code&gt;area-spline&lt;/code&gt; — filled curves
&lt;/h4&gt;

&lt;p&gt;Same CSV — swap &lt;code&gt;area-spline&lt;/code&gt; for &lt;code&gt;area&lt;/code&gt; to drop smoothing. Three series below.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhd665y0jjb59n3d55zy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhd665y0jjb59n3d55zy.png" alt="Diagram" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;pie&lt;/code&gt;, &lt;code&gt;donut&lt;/code&gt; — part-of-whole
&lt;/h4&gt;

&lt;p&gt;Same &lt;code&gt;name,value&lt;/code&gt; rows — swap &lt;code&gt;pie&lt;/code&gt; for &lt;code&gt;donut&lt;/code&gt; on the first line. One row per slice.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjzigb5c5scrfg4qr1ti.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjzigb5c5scrfg4qr1ti.png" alt="Diagram" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;gauge&lt;/code&gt; — single KPI
&lt;/h4&gt;

&lt;p&gt;Gauge charts accept one value — the exception to the multi-row pattern.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yrm28mmtf102xxy6d2o.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yrm28mmtf102xxy6d2o.png" alt="Diagram" width="800" height="640"&gt;&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;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;markdown&lt;/p&gt;





&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
dev push --update-toc my-article.md&lt;br&gt;
`&lt;/code&gt;&lt;/p&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;/p&gt;

&lt;p&gt;&lt;code&gt;`markdown&lt;br&gt;
'''ansi&lt;br&gt;
{{RED}}The quick brown fox{{BOLD_RED}} jumps over the lazy dog&lt;br&gt;
{{GREEN}}The quick brown fox{{BOLD_GREEN}} jumps over the lazy dog&lt;br&gt;
{{YELLOW}}The quick brown fox{{BOLD_YELLOW}} jumps over the lazy dog&lt;br&gt;
{{ORANGE}}The quick brown fox{{BOLD_ORANGE}} jumps over the lazy dog&lt;br&gt;
{{BLUE}}The quick brown fox&lt;br&gt;
{{BOLD_BLUE}}jumps over the lazy dog&lt;br&gt;
{{MAGENTA}}The quick brown fox{{BOLD_MAGENTA}} jumps over the lazy dog&lt;br&gt;
{{CYAN}}The quick brown fox{{BOLD_CYAN}} jumps over the lazy dog&lt;br&gt;
{{WHITE}}The quick brown fox{{BOLD_WHITE}} jumps over the lazy dog&lt;br&gt;
{{GRAY}}The quick brown fox{{BOLD_GRAY}} jumps over the lazy dog&lt;br&gt;
{{BG_RED}}The quick brown fox{{/}} jumps over the lazy dog&lt;br&gt;
{{BG_GREEN}}The quick brown fox{{BG_YELLOW}} jumps over the lazy dog&lt;br&gt;
{{BG_ORANGE}}The quick brown fox&lt;br&gt;
jumps over the lazy dog&lt;br&gt;
{{BG_BLUE}}The quick brown fox{{BG_MAGENTA}} jumps over the lazy dog&lt;br&gt;
{{BG_CYAN}}The quick brown fox{{BG_WHITE}} jumps over the lazy dog&lt;br&gt;
{{BG_GRAY}}The quick brown fox{{/}} jumps over the lazy dog&lt;br&gt;
'''&lt;br&gt;
`&lt;/code&gt;&lt;/p&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" width="800" height="350"&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;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
dev push --update-toc "*.md"&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Smart file renaming
&lt;/h3&gt;

&lt;p&gt;When an article title changes, the filename should follow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
dev rename "*.md"&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;bash&lt;br&gt;
🦊 GitLab CI: A Battle-Tested Mono-Repo CI/CD Architecture&lt;br&gt;
  GITLAB_mono-repo-architecture.md → GITLAB_ci-battle-tested-mono-repo-ci-cd-architecture.md&lt;/p&gt;

&lt;p&gt;🔍 Every Developer Should Review Code — Not Just Seniors&lt;br&gt;
  MISC_every-developer-should-review-code.md → MISC_every-developer-review-code-seniors.md&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&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;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
dev badges --jpg "*.md"&lt;br&gt;
`&lt;/code&gt;&lt;/p&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;tbody&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="500" alt="🦊 GitLab CI: Achieving 3-Second Jobs on Million-Line Codebases" height="208"&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="500" alt="🦊 GitLab Runners: Which Topology for Fastest Job Execution?" height="208"&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="500" alt="🔀 Efficient Git Workflow for Web Apps: Advancing Progressively from Scratch to Thriving" height="208"&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="500" alt="🔀🦊 GitLab: Forget GitKraken, Here are the Only Git Commands You Need" height="208"&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="500" alt="🦊 GitLab: A Python Script Displaying Latest Pipelines in a Group's Projects" height="208"&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="500" alt="🦊 GitLab: A Python Script Calculating DORA Metrics" height="208"&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="500" alt="🦊 GitLab CI: Deploy a Majestic Single Server Runner on AWS" height="208"&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="500" alt="🦊 GitLab CI: The Majestic Single Server Runner" height="208"&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="500" alt="🦊 GitLab CI YAML Modifications: Tackling the Feedback Loop Problem" height="208"&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="500" alt="🦊 GitLab CI Optimization: 15+ Tips for Faster Pipelines" height="208"&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="500" alt="🦊 GitLab CI: 10+ Best Practices to Avoid Widespread Anti-Patterns" height="208"&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="500" alt="🦊 GitLab Pages per Branch: The No-Compromise Hack to Serve Preview Pages" height="208"&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="500" alt="🦊 GitLab CI Jobs Attributes Sorter: A Python Script for Consistent YAML Files" height="208"&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="500" alt="🦊 GitLab Runners Topologies: Pros and Cons" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&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;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
dev checklinks my-article.md&lt;br&gt;
`&lt;/code&gt;&lt;/p&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;/p&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
DEVTO_ORG=zenika&lt;br&gt;
`&lt;/code&gt;&lt;/p&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;p&gt;&lt;code&gt;`bash&lt;br&gt;
export HTTPS_PROXY=http://proxy.company.com:8080&lt;br&gt;
dev push&lt;br&gt;
`&lt;/code&gt;&lt;/p&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, badge generation, GitLab CI pipeline diagrams, and finally chart rendering.&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" width="800" height="350"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" alt="bcouetil profile" class="crayons-avatar__image" width="500" height="500"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg" class="crayons-avatar__image" alt="" width="500" height="500"&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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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 crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&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://dev.to/zenika/gitlab-ci-achieving-3-second-jobs-on-million-line-codebases-3nlm</link>
      <guid>https://dev.to/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;Of course, sub-10-second jobs are only worth celebrating if the pipeline stays healthy over time — that's the subject of &lt;a href="https://dev.to/zenika/keep-feeding-your-cicd-or-watch-it-die-2ci9"&gt;Keep Feeding Your CI/CD — Or Watch It Die&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%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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.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;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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://dev.to/zenika/how-gilab-duo-agent-platform-antigravity-can-collaborate-to-improve-the-quality-of-our-38bm</link>
      <guid>https://dev.to/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://dev.to/zenika/gitlab-runners-which-topology-for-fastest-job-execution-5bma</link>
      <guid>https://dev.to/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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.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;11&lt;span class="hidden s:inline"&gt;&amp;nbsp;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;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;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>
  </channel>
</rss>
