<?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: Benoit COUETIL 💫</title>
    <description>The latest articles on DEV Community by Benoit COUETIL 💫 (@bcouetil).</description>
    <link>https://dev.to/bcouetil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615058%2F6cb73188-4242-460e-9d99-65bf587c237c.jpeg</url>
      <title>DEV Community: Benoit COUETIL 💫</title>
      <link>https://dev.to/bcouetil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bcouetil"/>
    <language>en</language>
    <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 plaintext"&gt;&lt;code&gt;ci/
└── test/
    [...]
    ├── D-tag-preprod.variables.yml      # Tag triggering preprod deployment
    ├── D-tag-preprod.csv                 # Expected jobs (reference)
    ├── F-scheduled.variables.yml         # Scheduled nightly pipeline
    └── F-scheduled.csv                   # Expected jobs (reference)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

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

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

&lt;/div&gt;


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

&lt;/div&gt;


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

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

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

&lt;/div&gt;


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;


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

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

&lt;/div&gt;


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

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

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

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

&lt;/div&gt;


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

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

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

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

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

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

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

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

&lt;/div&gt;


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

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

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

&lt;/div&gt;


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

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

&lt;/div&gt;


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

&lt;/div&gt;


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

&lt;/div&gt;


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

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

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

&lt;/div&gt;


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

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

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

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

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

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

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

&lt;/div&gt;


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

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

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

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

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

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

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

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

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


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


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

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

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

    &lt;/div&gt;

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


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

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

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


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


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

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>testing</category>
      <category>cicd</category>
    </item>
    <item>
      <title>📝 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.&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;/div&gt;


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

&lt;/div&gt;


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

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

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

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

&lt;/div&gt;


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

&lt;/div&gt;


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

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

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

&lt;/div&gt;


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

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

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

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

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

&lt;/div&gt;

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;


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

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

&lt;/div&gt;


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

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

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

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

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

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

&lt;/div&gt;


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

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

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

&lt;/div&gt;


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


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

&lt;/div&gt;


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

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

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

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

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

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


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


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

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

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

    &lt;/div&gt;

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


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

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

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


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


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

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>🦊 GitLab CI: Achieving 3-Second Jobs on Million-Line Codebases</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Thu, 19 Feb 2026 21:17:00 +0000</pubDate>
      <link>https://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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvnqcdpcywk4i83gnbg1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvnqcdpcywk4i83gnbg1.jpg" alt="Mechanical orange fox crossing finish line"&gt;&lt;/a&gt;&lt;/p&gt;

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


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


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

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

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

    &lt;/div&gt;

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


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

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

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


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


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

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>cicd</category>
      <category>performance</category>
    </item>
    <item>
      <title>All Articles by Theme</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Thu, 19 Feb 2026 19:04:39 +0000</pubDate>
      <link>https://dev.to/bcouetil/all-my-articles-by-theme-463k</link>
      <guid>https://dev.to/bcouetil/all-my-articles-by-theme-463k</guid>
      <description>&lt;p&gt;Here are my articles, grouped by theme and sorted by publication date (newest first), with view counts and highlights for the most read ones ⭐&lt;/p&gt;

&lt;h2&gt;
  
  
  🦊 GitLab / 🔀 Git
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/gitlab-ci-automated-testing-of-job-rules-1i03"&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-automated-testing-job-presence.jpg%3Fv%3D20260320" width="500" alt="🦊 GitLab CI: Automated Testing of Job Rules" height="208"&gt;&lt;/a&gt;
&lt;/td&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%3D20260320" width="500" alt="🦊 GitLab CI: Achieving 3-Second Jobs on Million-Line Codebases" 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-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%3D20260320" width="500" alt="🦊 GitLab Runners: Which Topology for Fastest Job Execution?" height="208"&gt;&lt;/a&gt;
&lt;/td&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%3D20260320" 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;/tr&gt;
&lt;tr&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%3D20260320" width="500" alt="🔀🦊 GitLab: Forget GitKraken, Here are the Only Git Commands You Need" 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-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%3D20260320" 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;/tr&gt;
&lt;tr&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%3D20260320" width="500" alt="🦊 GitLab: A Python Script Calculating DORA Metrics" height="208"&gt;&lt;/a&gt;
&lt;/td&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%3D20260320" width="500" alt="🦊 GitLab CI: Deploy a Majestic Single Server Runner on AWS" 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-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%3D20260320" width="500" alt="🦊 GitLab CI: The Majestic Single Server Runner" height="208"&gt;&lt;/a&gt;
&lt;/td&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%3D20260320" width="500" alt="🦊 GitLab CI YAML Modifications: Tackling the Feedback Loop Problem" 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-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%3D20260320" width="500" alt="🦊 GitLab CI Optimization: 15+ Tips for Faster Pipelines" height="208"&gt;&lt;/a&gt;
&lt;/td&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%3D20260320" width="500" alt="🦊 GitLab CI: 10+ Best Practices to Avoid Widespread Anti-Patterns" 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-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%3D20260320" 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;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%3D20260320" 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;/tr&gt;
&lt;tr&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%3D20260320" width="500" alt="🦊 GitLab Runners Topologies: Pros and Cons" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  ☸️ Kubernetes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/kubernetes-a-convenient-variable-substitution-mechanism-for-kustomize-lhm"&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%2Fconvenient-variable-substitution-mechanism-kustomize.jpg%3Fv%3D20260320" width="500" alt="☸️ Kubernetes: A Convenient Variable Substitution Mechanism for Kustomize" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/why-managed-kubernetes-is-a-viable-solution-for-even-modest-but-actively-developed-applications-357g"&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%2Fmanaged-viable-solution-even-modest.jpg%3Fv%3D20260320" width="500" alt="☸️ Why Managed Kubernetes is a Viable Solution for Even Modest but Actively Developed Applications" 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/kubernetes-from-your-docker-compose-file-to-a-cluster-with-kompose-1gn0"&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%2Fdocker-compose-file-cluster-kompose.jpg%3Fv%3D20260320" width="500" alt="☸️ Kubernetes: From Your Docker-Compose File to a Cluster with Kompose" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/kubernetes-a-pragmatic-kubectl-aliases-collection-17oc"&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%2Fpragmatic-kubectl-aliases-collection.jpg%3Fv%3D20260320" width="500" alt="☸️ Kubernetes: A Pragmatic Kubectl Aliases Collection" 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/web-application-on-kubernetes-a-tutorial-to-observability-with-the-elastic-stack-2p8a"&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%2Fweb-application-tutorial-observability-elastic.jpg%3Fv%3D20260320" width="500" alt="☸️ Web Application on Kubernetes: A Tutorial to Observability with the Elastic Stack" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/kubernetes-nginx-ingress-controller-10-complementary-configurations-for-web-applications-ken"&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%2Fnginx-ingress-controller-complementary-configurations.jpg%3Fv%3D20260320" width="500" alt="☸️ Kubernetes NGINX Ingress Controller: 10+ Complementary Configurations for Web Applications" 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/kubernetes-awesome-maintained-links-you-will-keep-using-next-year-48o8"&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%2Fawesome-maintained-links-keep-using.jpg%3Fv%3D20260320" width="500" alt="☸️ Kubernetes: Awesome Maintained Links You Will Keep Using Next Year" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/managed-kubernetes-our-dev-is-on-aws-our-prod-is-on-ovh-3nbf"&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%2Fmanaged-dev-aws-prod-ovhcloud.jpg%3Fv%3D20260320" width="500" alt="☸️ Managed Kubernetes: Our Dev is on AWS, Our Prod is on OVHCloud" 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/how-to-deploy-a-cost-efficient-awseks-kubernetes-cluster-using-terraform-in-2023-3903"&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%2Fdeploy-cost-efficient-aws-eks-cluster-using.jpg%3Fv%3D20260320" width="500" alt="☸️ How to Deploy a Cost-Efficient AWS/EKS Kubernetes Cluster Using Terraform in 2023" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/how-to-deploy-a-secured-ovh-managed-kubernetes-cluster-using-terraform-in-2023-17o7"&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%2Fdeploy-secured-ovhcloud-managed-cluster.jpg%3Fv%3D20260320" width="500" alt="☸️ How to Deploy a Secured OVHCloud Managed Kubernetes Cluster Using Terraform in 2023" 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/eks-10-tips-to-reduce-the-bill-up-to-90-on-aws-managed-kubernetes-clusters-epe"&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%2Ffinops-eks-tips-reduce-bill.jpg%3Fv%3D20260320" width="500" alt="☸️ FinOps EKS: 10 Tips to Reduce the Bill up to 90% on AWS Managed Kubernetes Clusters" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  📝 Miscellaneous
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/devto-writers-community-one-command-to-maintain-them-all-2feo"&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%2Fdevto-writers-community-one-command.jpg%3Fv%3D20260320" width="500" alt="📝 Dev.to Writers Community: One Command to Maintain Them All" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;a href="https://dev.to/zenika/every-developer-should-review-code-not-just-seniors-2abc"&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%2Fevery-developer-review-code-seniors.jpg%3Fv%3D20260320" width="500" alt="🔍 Every Developer Should Review Code — Not Just Seniors" 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/future-proof-tech-blogging-understanding-ais-core-traits-3h00"&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%2Ffuture-proof-tech-blogging-understanding-ais.jpg%3Fv%3D20260320" width="500" alt="🤖 Future-Proof Tech Blogging: Understanding AI's Core Traits" height="208"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td width="50%"&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Generated by &lt;a href="https://github.com/bcouetil/devto-cli" rel="noopener noreferrer"&gt;a fork of devto-cli&lt;/a&gt; on 2026-03-20&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>gitlab</category>
      <category>kubernetes</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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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

&lt;/div&gt;


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

    &lt;/div&gt;

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


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

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

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

&lt;/div&gt;


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

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>🔀 Efficient Git Workflow for Web Apps: Advancing Progressively from Scratch to Thriving</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Thu, 10 Oct 2024 09:29:24 +0000</pubDate>
      <link>https://dev.to/zenika/efficient-git-workflow-for-web-apps-advancing-progressively-from-scratch-to-thriving-3af6</link>
      <guid>https://dev.to/zenika/efficient-git-workflow-for-web-apps-advancing-progressively-from-scratch-to-thriving-3af6</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;1. First lines of code&lt;/li&gt;
&lt;li&gt;2. First development environments&lt;/li&gt;
&lt;li&gt;3. First stable environment&lt;/li&gt;
&lt;li&gt;4. First production environment&lt;/li&gt;
&lt;li&gt;5. First customers&lt;/li&gt;
&lt;li&gt;6. Successful application&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;In this article, we will enhance our Git Workflow by gradually adding functionalities for starting web applications. Our main goal is to improve efficiency.&lt;/p&gt;

&lt;p&gt;Developers, even experienced ones, have often struggled with the famous Gitflow :&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%2Fd54no23mkit5epzzfspu.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%2Fd54no23mkit5epzzfspu.png" alt="https://mermaid.live/edit#pako:eNqdlEFPgzAUx79KU0N6YQubmiWcTbzMiyYeDJc3-oDG0pKumBmy725HxY0xcFou7e-9__uXB21DU82RxjQIGqGEjUlDmNT5Gj9Qspgwjps6ZyFhtsASPcmglpaRDr6CEbCRuHXRJlHEDZYLGx2Sb1arFQuPcNFCzqNTuGxh1s-8bWEE2Sm8-4Y9-b2XRxnzzI394QmCRLnwo4GqIOvn2EdTXZbC-vnGgEoLUmibid1s14MGJcIWZ4v54iJfzqMe54eO6WrUJEOwtcHO5TQjLTB917U9q9HRPytLNDkOZF1qCUKdoX4DLhocVb76mWRiH1cUH_TaK0dbOhD-fAwvHNQb6fC_9_KL_3TycVfXNHr8XQbmV_xT044-TEPqrB3l7l5oD3RC24Oe0NhNpcgLm9BE7V0i1Fa_fKqUxtbUGNK64mDxQUBuoOxgBepNa7fMQG7dGrmw2jz5i6e9f_ZfNd5xRA" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, the truth is that we don't need this complexity from day one. Many developers even argue that Gitflow is never necessary — it's a symptom of an organizational problem, not a solution.&lt;/p&gt;

&lt;p&gt;Let's approach it differently: from team needs to workflow.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. First lines of code
&lt;/h1&gt;

&lt;p&gt;As we gather the team and prepare to write the first lines of code, the development team members have a few requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share a common code base&lt;/li&gt;
&lt;li&gt;Collaborate on work in progress&lt;/li&gt;
&lt;li&gt;Conduct testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most web applications consist of a frontend module and a backend module. The question of using a multi-repo or mono-repo arises.&lt;/p&gt;

&lt;p&gt;This article won't elaborate on the controversial topic, but for a starting web application project, we choose a mono-repo for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No internal versioning required&lt;/li&gt;
&lt;li&gt;Local common module dependencies&lt;/li&gt;
&lt;li&gt;One feature, one feature branch&lt;/li&gt;
&lt;li&gt;Easy continuous integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this phase of the project, we don't need more than the following branches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived feature branches that are merged into &lt;code&gt;main&lt;/code&gt; when ready, as frequently as possible&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;main&lt;/code&gt; branch for merged code&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%2F89g7oqv4p6fkw733dhep.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%2F89g7oqv4p6fkw733dhep.png" alt="https://mermaid.live/edit#pako:eNqNkk1rxCAQhv-KWIKXFNIvAp4LvWwvLfRQvEx0TKSJBldLl5D_XldbdpcemvEy88w7r4eZhUqnkHJaVYuxJnCyEDa6foefODJOmMIu9qwmLAw4YSEa4hgY-YVv4A10I-5TdxGWpGC9Cc1RfNW2LatP8CZDpZpzeJuhvlTeZdiAPof3P_Bi_KGMN5oVlmI9vqoSNrWfPMwD2b3w0pVumkwoeefByoFohBA9Xn_9VcgB5YeLgUxg7L8Gh00GE_oeN3261ZfWNJmmTKVN5hUImlcjKE_paPohCCrsmoQQg3s9WEl58BFrGmcFAR8N9B4myjWM-0RnsO_OnWpUJjj_XG4ln8z6DXPhr28" width="486" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During the initial weeks of testing and demonstrations, local computers are sufficient. This allows time for the Platform Engineer to prepare real environments.&lt;/p&gt;

&lt;p&gt;For code reviews, any Git platform, such as GitLab or GitHub, will suffice.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. First development environments
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Workflow
&lt;/h3&gt;

&lt;p&gt;After a few weeks, code and features start to accumulate. Tracking bugs and conducting proper testing becomes essential. If the tools allow, these tests should occur before merging. Demonstrations should take place on a shared environment rather than on local computers.&lt;/p&gt;

&lt;p&gt;To achieve this, we establish an integration environment that reflects the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;If we have the right CI/CD technologies, we can also create an environment per branch, which greatly enhances team efficiency. Kubernetes and GitLab can help us achieve this goal with minimal effort.&lt;/p&gt;

&lt;p&gt;Our Git workflow at this stage looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived feature branches and their corresponding environments are merged into &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; branch and environment are used for development team collaboration and client demonstrations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Separate Infrastructure Repository ?
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;This is controversial and a bit off-topic, but this still a linked matter with efficiency choice at stake.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Many teams choose to separate application and infrastructure repositories. However, I believe that the main reason for this separation is governance and politics, not efficiency.&lt;/p&gt;

&lt;p&gt;We choose to have infrastructure scripts in the same repository as the code. Although the lifecycles are different, separating them can lead to complex pipelines if everything is automated, right?&lt;/p&gt;

&lt;p&gt;But do we really need full automation for infrastructure? While infrastructure-as-code is desirable, deployment automation may not be necessary. We like to follow the rule: "do not automate before it is boring." Since our product is just starting, nothing is boring regarding infrastructure. We don't need to reinstantiate our Kubernetes cluster every week.&lt;/p&gt;

&lt;p&gt;For these reasons, we code and version the infrastructure but do not automate its deployment.&lt;/p&gt;

&lt;p&gt;For CI/CD development specifically, tools like &lt;code&gt;gitlab-ci-local&lt;/code&gt; can help iterate faster on pipeline configurations without waiting for remote runners—see &lt;a href="https://dev.to/zenika/gitlab-ci-yaml-modifications-tackling-the-feedback-loop-problem-4ib1"&gt;GitLab CI: YAML Modifications: Tackling the Feedback Loop Problem&lt;/a&gt; for a deep dive.&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%2Fney9cdfd9y53uhmyswcz.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%2Fney9cdfd9y53uhmyswcz.jpg" alt="fox as a plumber fixing intricate orange pipes, manga style" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  3. First stable environment
&lt;/h1&gt;

&lt;p&gt;Sooner or later, stakeholders will test the application. Frequent redeployment of the &lt;code&gt;main&lt;/code&gt; environment leads to downtimes and occasional bugs, which spoil the experience.&lt;/p&gt;

&lt;p&gt;It's time to introduce the &lt;code&gt;demo&lt;/code&gt; environment. This environment reflects the content of the &lt;code&gt;main&lt;/code&gt; environment and is updated before each scheduled demonstration. It remains stable in the interim.&lt;/p&gt;

&lt;p&gt;To adapt the flow, the update can be triggered by a Git tag or a Git branch. Here, we choose to create a new &lt;code&gt;demo&lt;/code&gt; Git branch.&lt;/p&gt;

&lt;p&gt;Our Git workflow now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived feature branches and their corresponding environments are merged into &lt;code&gt;main&lt;/code&gt; and then deleted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; branch and environment are merged into &lt;code&gt;demo&lt;/code&gt; before demonstrations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;demo&lt;/code&gt; branch and environment are used for demonstrations&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%2F75ok82ur9tf0e4p3wfrw.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%2F75ok82ur9tf0e4p3wfrw.png" alt="https://mermaid.live/edit#pako:eNqNkk1LxDAQhv9KiJRcKtQvCjmK4GX1oOBBepk20zbYJCWbiEvpfzdNrdtlUTqnyTPvfJEZaGUEUk6TZJBaOk4GwjrT7PATO8YJE1j6hqWEuRYVzqQG3zlGFvgGVkLZ4T5Eh0KTYKyRLpvEF3mes_QIryIUIlvD6wjrU-VNhBnUa3j7A0_S7-b0rGYzCzamMfJooW8ZiUspkPregq7aZ1gWUYaRcSRjkhR6kZPdC5_rVEYp6Wa_jJlkKvJnsEZw3uLl17miarH6MN5tK3DYVEChbfCs6aKbllvrVo3_n-U3vHUamtLQInginFH8_4LGuygoD24nm9YVtNBjEIJ35vWgK8qd9ZhS3wtw-CChsaAor6HbB9qDfjfm-EYhnbFP86HGex2_AXmr2Bk" width="586" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  4. First production environment
&lt;/h1&gt;

&lt;p&gt;With the MVP (Minimum Viable Product) just around the corner, a production environment has been built from the ground up and will soon be made public. It's time to deploy our application to the target production infrastructure.&lt;/p&gt;

&lt;p&gt;To adapt the flow, the production deployment can be triggered by a Git tag or a Git branch. We choose to use tags since we don't need to handle hot fixes at the moment, and triggering deployment jobs can be done manually.&lt;/p&gt;

&lt;p&gt;We don't cascade merges from &lt;code&gt;main&lt;/code&gt; to &lt;code&gt;demo&lt;/code&gt; to &lt;code&gt;production&lt;/code&gt;. The &lt;code&gt;demo&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; environments have decoupled lifecycles.&lt;/p&gt;

&lt;p&gt;Our Git workflow now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived feature branches and their corresponding environments are merged into &lt;code&gt;main&lt;/code&gt; and then deleted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; branch and environment are merged into &lt;code&gt;demo&lt;/code&gt; before demonstrations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;demo&lt;/code&gt; branch and environment are used for demonstrations&lt;/li&gt;
&lt;li&gt;Manual production deployment jobs are available on tags (created on &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;demo&lt;/code&gt;, depending on the team preference)&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%2F2df5nylnu03x7ujyp945.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%2F2df5nylnu03x7ujyp945.png" alt="https://mermaid.live/edit#pako:eNqVkj9rwzAQxb-KUDFa3OL0DwGNpdAl7dBCh-LlYp1lUUsKilQajL97ZbluHLI4mqTfe3e6g9fRygqknGZZp4zynHSEtVZu8BtbxgkTuA2S5YT5BjWOpIbQekYm-AFOwbbFfVS70pB4mFS-GMxX6_Wa5Ue4SlCIYg5vE6xPnXcJFlDP4f0fPCl_GMuLmo0snj5PyrODXcNIWkqDMo8OTNW8wrSItoz0PemzrDSTnWze-NinslorTzxITkq6uilKOgrb1IYMHefOE7FG8MHh9c-5o2qw-rLBL2twWNRAo5N49unkGzad-4a6416raa8LhvyXl45Jcxr_jjcRw5ZSUtKUnpIOU7RKNn6Yo49GCN6-H0xFuXcBcxp2Ajw-KZAONOU1tPtId2A-rT2-UShv3csY55Tq_heLI-Jv" width="636" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F027fayunh5zimrgdhxwt.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%2F027fayunh5zimrgdhxwt.jpg" alt="fox as a plumber fixing intricate orange pipes, manga style" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  5. First customers
&lt;/h1&gt;

&lt;p&gt;As we had hoped from the beginning, we now have happy customers.&lt;/p&gt;

&lt;p&gt;We want to ensure that we don't spoil their experience. When problems occur in their workflow, we want to be able to reproduce them in an iso-production environment, fix them, and test them in a pre-production environment as well. We will call this environment &lt;code&gt;staging&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;staging&lt;/code&gt; doesn't have real customers, we can deploy automatically using the same tags that trigger manual production deployment.&lt;/p&gt;

&lt;p&gt;Our Git workflow now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived feature branches and their corresponding environments are merged into &lt;code&gt;main&lt;/code&gt; and then deleted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; branch and environment are merged into &lt;code&gt;demo&lt;/code&gt; before demonstrations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;demo&lt;/code&gt; branch and environment are used for demonstrations&lt;/li&gt;
&lt;li&gt;Automatic &lt;code&gt;staging&lt;/code&gt; deployment and manual &lt;code&gt;production&lt;/code&gt; deployment are triggered by tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We still don't handle production hot fixes separately. The entire content of &lt;code&gt;main&lt;/code&gt; is deployed to production on tags. Unfinished content is not merged into &lt;code&gt;main&lt;/code&gt; or deactivated by a commit.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. Successful application
&lt;/h1&gt;

&lt;p&gt;Now our application is somewhat successful, with many customers using it daily.&lt;/p&gt;

&lt;p&gt;Cash is flowing, and the development team is growing. It becomes increasingly difficult to find a satisfying &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;demo&lt;/code&gt; state to be deployed to production.&lt;/p&gt;

&lt;p&gt;Now is the time to add &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggles&lt;/a&gt; (also known as feature flags or feature flipping). Anything can be deployed to production as long as it is protected by a switch to activate it or not.&lt;/p&gt;

&lt;p&gt;Our Git workflow now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived feature branches and their corresponding environments are merged into &lt;code&gt;main&lt;/code&gt; and then deleted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; branch and environment are merged into &lt;code&gt;demo&lt;/code&gt; before demonstrations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;demo&lt;/code&gt; branch and environment are used for demonstrations&lt;/li&gt;
&lt;li&gt;Automatic &lt;code&gt;staging&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; deployment are triggered by tags created on &lt;code&gt;main&lt;/code&gt;, and feature toggles are activated based on confidence&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;We followed the path of a starting project that progressively added only necessary complexity to the git flow.&lt;/p&gt;

&lt;p&gt;By following this Git workflow, development teams can simplify their processes, improve collaboration, and deliver high-quality web applications more efficiently.&lt;/p&gt;

&lt;p&gt;What is your own best experience of Git workflow ? Please share in the comments 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.amazonaws.com%2Fuploads%2Farticles%2Fjw9gcxd8e9r788yyoj6c.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%2Fjw9gcxd8e9r788yyoj6c.jpg" alt="fox as a plumber fixing intricate orange pipes, manga style" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Pinokio using Stable Cascade plugin&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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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

&lt;/div&gt;


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

</description>
      <category>git</category>
      <category>webdev</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>🔀🦊 GitLab: Forget GitKraken, Here are the Only Git Commands You Need</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Sat, 31 Aug 2024 08:11:31 +0000</pubDate>
      <link>https://dev.to/zenika/gitlab-forget-gitkraken-here-are-the-only-git-commands-you-need-4ckj</link>
      <guid>https://dev.to/zenika/gitlab-forget-gitkraken-here-are-the-only-git-commands-you-need-4ckj</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;
1. Drawbacks of git GUI clients

&lt;ul&gt;
&lt;li&gt;Price&lt;/li&gt;
&lt;li&gt;Magic&lt;/li&gt;
&lt;li&gt;Context-specific&lt;/li&gt;
&lt;li&gt;Hard to communicate&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;2. The GitLab flow&lt;/li&gt;

&lt;li&gt;

3. Everyday commands when using GitLab flow

&lt;ul&gt;
&lt;li&gt;Prepare to make changes to the feature branch&lt;/li&gt;
&lt;li&gt;Commit your work&lt;/li&gt;
&lt;li&gt;Push and merge your work&lt;/li&gt;
&lt;li&gt;Inspect past commits&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

3. Common situations that you need to be able to manage

&lt;ul&gt;
&lt;li&gt;The target branch has been updated&lt;/li&gt;
&lt;li&gt;You need to work on another branch&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

4. Going further with the command line

&lt;ul&gt;
&lt;li&gt;Someone has updated your (remote) feature branch&lt;/li&gt;
&lt;li&gt;Rewrite the history&lt;/li&gt;
&lt;li&gt;Remove old local branches&lt;/li&gt;
&lt;li&gt;Change the user name/email in commits&lt;/li&gt;
&lt;li&gt;Delete all existing files defined in .gitignore&lt;/li&gt;
&lt;li&gt;Fetch changes to a local branch without switching to it&lt;/li&gt;
&lt;li&gt;Remove changes on files against target branche&lt;/li&gt;
&lt;li&gt;Rebase only a specific commit range&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;5. Git common configuration options and aliases&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;&lt;a href="https://stackoverflow.blog/2023/01/09/beyond-git-the-other-version-control-systems-developers-use/" rel="noopener noreferrer"&gt;The 2022 stackoverflow developer survey&lt;/a&gt; found that more than 90% of developers use git for version control. Despite that, we feel that many developers are intimidated by the git CLI and use it only when their favorite version control GUI is missing a particular feature.&lt;/p&gt;

&lt;p&gt;I, too, relied solely on GitKraken after 10+ years as a developer. Fortunately, &lt;a href="https://benoitaverty.com/" rel="noopener noreferrer"&gt;Benoit Averty&lt;/a&gt; taught me everything I needed to know, through a &lt;a href="https://training.zenika.com/en-fr/training/git/description" rel="noopener noreferrer"&gt;Zenika Git training&lt;/a&gt; session. I’m now a happy Git CLI user across various GitLab projects 🤓.&lt;/p&gt;

&lt;p&gt;Fast forward 4 years, and together with Benoit, we present this (self sufficient) article for those as intimidated as my past self 🤗.&lt;/p&gt;

&lt;p&gt;Yes, the git CLI has many commands, and it can be daunting; but for everyday use you only need to know a few commands, and the git CLI is particularly good at providing guidance on what you may want to do next. In this article, we show that the CLI is not very hard to use, especially when using GitLab to host the git repository and embracing the GitLab flow that is used by many teams using GitLab.&lt;/p&gt;

&lt;p&gt;Most of what we will cover in this article should be true if you’re using GitHub and the GitHub flow instead; but small adjustments may be necessary.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Drawbacks of git GUI clients
&lt;/h1&gt;

&lt;p&gt;Git GUI clients are often seen as an easier way to use git. However, it is not always the case, and even when it is, it comes with several drawbacks that you need to be aware of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price
&lt;/h2&gt;

&lt;p&gt;Some of the best git GUIs out there are paid software. If you’re paying for them, it's probably because you think they’re worth it. Nevertheless, paid software can quickly add up to a hefty price, and we all prefer a free option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Magic
&lt;/h2&gt;

&lt;p&gt;Using a git GUI is the best way to use git without understanding git. Part of what makes git appear as a hard tool to master is the complexity of its internal structure and the concepts git introduces to manage your source code. GUIs seem easier to use because they abstract away these concepts, but the drawback is that you can use git every day without understanding it. This makes it almost impossible to handle a special case when it arises or to do something that your preferred GUI can't do.&lt;/p&gt;

&lt;p&gt;The CLI, on the other hand, stays close to the git underlying concepts and teaches them to you almost without you noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context-specific
&lt;/h2&gt;

&lt;p&gt;A notable drawback of git graphical interfaces is their context-specific nature. Each GUI represents git features in its unique fashion. The skills you gain from one client may not be directly transferable to another client. This is particularly true if your role involves helping teammates with their git usage or if you aspire to be in such a position.&lt;/p&gt;

&lt;p&gt;On the other hand, the commands of the git CLI remain standard, regardless of the specific environment in which you’re operating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard to communicate
&lt;/h2&gt;

&lt;p&gt;It is intrinsically challenging to communicate about graphical interfaces when they allow complex actions (and it is often the case with git graphical interfaces). Helping a teammate perform a git action may involve taking a screenshot or describing buttons and menus, whereas command lines can be copy-pasted without effort, including the result of specific commands that can be used to diagnose the problem.&lt;/p&gt;

&lt;p&gt;A corollary of this is that it’s easier to write and maintain documentation or best practices on the specific usage of git in your team if you’re using CLI instead of a graphical interface.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. The GitLab flow
&lt;/h1&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%2Fozur3t2k1r40qlsk2xbc.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%2Fozur3t2k1r40qlsk2xbc.png" alt="The GitLab flow" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many development teams today are using GitLab to collaborate on software projects. The GitLab flow is a branching workflow that takes advantage of the features offered by GitLab’s web interface. It is also a simpler alternative to &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/" rel="noopener noreferrer"&gt;the famous gitflow&lt;/a&gt;, introduced almost 15 years ago.&lt;/p&gt;

&lt;p&gt;The GitLab flow can be broken down to this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the GitLab web UI

&lt;ul&gt;
&lt;li&gt;Create an issue&lt;/li&gt;
&lt;li&gt;Create a MR from the issue, creating a branch&lt;/li&gt;
&lt;li&gt;(Now everyone knows on what you will work)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Locally

&lt;ul&gt;
&lt;li&gt;Synchronize code and switch to the new branch&lt;/li&gt;
&lt;li&gt;Do your magic&lt;/li&gt;
&lt;li&gt;commit and push&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;In the GitLab web UI

&lt;ul&gt;
&lt;li&gt;Review with your teammates&lt;/li&gt;
&lt;li&gt;When satisfied, merge the code (automatically deleting the branch)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In this workflow, branches aren’t created locally, and they aren’t even merged locally.&lt;/p&gt;

&lt;p&gt;In addition to that, many tasks related to git but not directly related to local code can be performed in the web UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List/create/delete branches;&lt;/li&gt;
&lt;li&gt;List/create/delete tags;&lt;/li&gt;
&lt;li&gt;Show the repository graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the tasks that still need to be done locally, let’s dive into the few commands that you need to know for everyday development.&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%2Fafw0udcpn51x2zo1qokq.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%2Fafw0udcpn51x2zo1qokq.jpg" alt="a furry fox fighting/punching/boxing/kicking a giant cyan octopus, tropical/jungle background, anime style" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Everyday commands when using GitLab flow
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Prepare to make changes to the feature branch
&lt;/h2&gt;

&lt;p&gt;If this is the first time on the project, &lt;a href="https://docs.GitLab.com/ee/GitLab-basics/start-using-git.html#clone-with-ssh" rel="noopener noreferrer"&gt;get the repo locally&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In any case, the first thing that you’ll probably want to do after creating your merge request and your branch is to update your local repository and switch to this new branch.&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;# Switch to develop/main/master and update your local repository&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch develop
&lt;span class="nv"&gt;$ &lt;/span&gt;git pull

&lt;span class="c"&gt;# Switch to the new branch that has been created when you opened your MR&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch your-new-feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first two commands are used to synchronize the main branch of the project locally. This is always a good idea because it allows you to check out the state of the main branch locally. But strictly speaking, this is not mandatory because you’re going to work on your feature branch anyway. You could replace both of them with a single &lt;code&gt;git fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The third command is the most interesting. Normally, &lt;code&gt;git switch&lt;/code&gt; in this form is used to switch to a branch that exists locally. But git is smart, and if the branch exists on the remote repository, git will create a local branch that tracks the remote branch that you’ve created previously and switch to that newly created branch. All of this is explained in the command output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;branch &lt;span class="s1"&gt;'your-new-feature-branch'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/your-new-feature-branch'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Switched to a new branch &lt;span class="s1"&gt;'your-new-feature-branch'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Commit your work
&lt;/h2&gt;

&lt;p&gt;Once you have something that is worth sharing with your teammates, or if your code is in a state that is worth saving, it is time to commit.&lt;/p&gt;

&lt;p&gt;The command that you’ll want to use next is the most important command when using the git CLI:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git status

On branch your-new-feature-branch
Your branch is up to &lt;span class="nb"&gt;date &lt;/span&gt;with &lt;span class="s1"&gt;'origin/your-new-feature-branch'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

Changes not staged &lt;span class="k"&gt;for &lt;/span&gt;commit:
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add &amp;lt;file&amp;gt;..."&lt;/span&gt; to update what will be committed&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git restore &amp;lt;file&amp;gt;..."&lt;/span&gt; to discard changes &lt;span class="k"&gt;in &lt;/span&gt;working directory&lt;span class="o"&gt;)&lt;/span&gt;
    modified:   package.json

no changes added to commit &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add"&lt;/span&gt; and/or &lt;span class="s2"&gt;"git commit -a"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why is it the most important command? Because it tells you everything there is to know before commiting, pushing, or doing anything else really. But also, it tells you what the next command will probably be. Who said command line interfaces were hard to use?&lt;/p&gt;

&lt;p&gt;In the example above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can see the list of changed files, and know immediately if there is something you don’t want to commit&lt;/li&gt;
&lt;li&gt;You have the command that you need to use to add files to the future commit&lt;/li&gt;
&lt;li&gt;You have the command that you need to use to discard changes that you’ve made.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s add changes to the future commit.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add package.json
&lt;span class="nv"&gt;$ &lt;/span&gt;git status

On branch your-new-feature-branch
Your branch is up to &lt;span class="nb"&gt;date &lt;/span&gt;with &lt;span class="s1"&gt;'origin/your-new-feature-branch'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

Changes to be committed:
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git restore --staged &amp;lt;file&amp;gt;..."&lt;/span&gt; to unstage&lt;span class="o"&gt;)&lt;/span&gt;
    modified:   package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Again, &lt;code&gt;git status&lt;/code&gt; tells you what you need to do if you want to cancel adding a file. No need to remember many commands.&lt;/p&gt;

&lt;p&gt;When you’re ready, commit your work to store it safely inside your local repository.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git commit
&lt;span class="o"&gt;[&lt;/span&gt;your-new-feature-branch 331ef24] Awesome commit message
 1 file changed, 1 insertion&lt;span class="o"&gt;(&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;, 1 deletion&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;
  
  
  Push and merge your work
&lt;/h2&gt;

&lt;p&gt;After each commit or after finishing your development task, it’s time to push your work to the GitLab server to begin gathering feedback from your teammates.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pushing is not hard when you’re using GitLab flow because there are almost never two developers working on the same branch. You will almost never need to resolve conflicts at this point or have your push rejected because the remote branch moved.&lt;/p&gt;

&lt;p&gt;Merging is also as easy as the push of a button in the GitLab webapp.&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%2F0cs0teehztxnzc08wmnz.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%2F0cs0teehztxnzc08wmnz.jpg" alt="GitLab merge button" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You probably want to delete your branch once you’ve finished working on it: check the corresponding box. If you’ve made commits that you don’t want to keep while working on your feature, check the "Squash commits" box.&lt;/p&gt;

&lt;p&gt;Note that this screenshot is taken on a project that’s configured with the "Merge commit" merge method. If you’re on a project that uses semi-linear history or fast-forward merges only, you’ll be given the option to rebase, also with the click of a button. See &lt;a href="https://docs.GitLab.com/ee/user/project/merge_requests/methods/index.html" rel="noopener noreferrer"&gt;GitLab’s documentation about merge methods&lt;/a&gt; to learn more.&lt;/p&gt;

&lt;p&gt;That’s all there is to know for everyday use when using GitLab flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git pull / git fetch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git switch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is done with the click of a button in GitLab.&lt;/p&gt;
&lt;h2&gt;
  
  
  Inspect past commits
&lt;/h2&gt;

&lt;p&gt;There's one more thing that’s often needed when working on a git project: inspecting past commits. It may be to get up to speed about recent changes made by your teammates or to refresh your memory about the changes you’ve made on your branch after working on something else or taking a vacation.&lt;/p&gt;

&lt;p&gt;Again, you could do this with the GitLab UI (in the menu, go to Code &amp;gt; Repository graph), but if you want to do it locally, here are a couple of solutions&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use git CLI: We suggest the following command, that you can store in an alias if you don’t want to memorize it
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nv"&gt;$ &lt;/span&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.graph &lt;span class="s2"&gt;"log --decorate --oneline --graph"&lt;/span&gt;
  &lt;span class="nv"&gt;$ &lt;/span&gt;git graph &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;--all&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;*&lt;/span&gt; 331ef24 &lt;span class="o"&gt;(&lt;/span&gt;HEAD -&amp;gt; 1-demo-issue, origin/1-demo-issue&lt;span class="o"&gt;)&lt;/span&gt; Changing package.json
  &lt;span class="k"&gt;*&lt;/span&gt;   79c1ae8 &lt;span class="o"&gt;(&lt;/span&gt;origin/main, origin/HEAD, main&lt;span class="o"&gt;)&lt;/span&gt; Merge branch &lt;span class="s1"&gt;'feat/previous-feature'&lt;/span&gt; into &lt;span class="s1"&gt;'main'&lt;/span&gt;
  |&lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="k"&gt;*&lt;/span&gt; 432f691 Implement tests
  | &lt;span class="k"&gt;*&lt;/span&gt; 946d022 Add an amazing feature
  |/
  &lt;span class="k"&gt;*&lt;/span&gt; 5c3c723 Hotfix &lt;span class="k"&gt;for &lt;/span&gt;the broken app incident
  &lt;span class="k"&gt;*&lt;/span&gt;   aa56183 Merge branch &lt;span class="s1"&gt;'awesome-feature'&lt;/span&gt; into &lt;span class="s1"&gt;'main'&lt;/span&gt;
  |&lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="k"&gt;*&lt;/span&gt; 10b6622 shorten even more
  &lt;span class="k"&gt;*&lt;/span&gt; | 0ac37b5 Merge branch &lt;span class="s1"&gt;'fixes'&lt;/span&gt; into &lt;span class="s1"&gt;'main'&lt;/span&gt;
  |&lt;span class="se"&gt;\|&lt;/span&gt;
  | &lt;span class="k"&gt;*&lt;/span&gt; 8180791 Fix various bugs
  |/
  &lt;span class="k"&gt;*&lt;/span&gt; Initial commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://jonas.github.io/tig/" rel="noopener noreferrer"&gt;tig&lt;/a&gt;. This TUI (terminal user interface) is halfway between the GUI and the command line. It basically does the same thing as the previous command, but it’s easier on the eyes while allowing you to stay in the terminal. It will also make it easier to see the diff for a particular commit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  3. Common situations that you need to be able to manage
&lt;/h1&gt;

&lt;p&gt;What we’ve discussed in the previous section is the nominal case. It will be enough for most days, but there are situations that will happen often enough that we would be lying if we told you don’t need to know how to handle them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The target branch has been updated
&lt;/h2&gt;

&lt;p&gt;When merging your branch, it’s likely that someone has updated the target branch (&lt;code&gt;main&lt;/code&gt;) since you’ve created your branch. If these changes have no conflict with yours, you have nothing to do: git will be able to merge your branch anyway. But if there are conflicts, you’ll need to solve them before merging.&lt;/p&gt;
&lt;h3&gt;
  
  
  Resolve conflicts in the GitLab UI
&lt;/h3&gt;

&lt;p&gt;Once again, GitLab allows to do this task right in the web UI. Click on the button, and you’ll be taken to a conflict editor that is probably enough for 99% of the conflicts you’ll have to resolve.&lt;/p&gt;

&lt;p&gt;Read more about merge conflicts resolution in &lt;a href="https://docs.GitLab.com/ee/user/project/merge_requests/conflicts.html" rel="noopener noreferrer"&gt;GitLab’s documentation about merge conflicts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you prefer to resolve conflicts locally (or if GitLab can’t do it for some reason), you can do it with the following steps.&lt;/p&gt;

&lt;p&gt;If you’re using merge commits:&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;# Fetch the changes so you'll be able to resolve the conflicts locally&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git fetch

&lt;span class="c"&gt;# If you're using merge commits, merge the target branch (here it is main) into your feature branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git merge origin/main
&lt;span class="c"&gt;# See which files are in conflict. Again, git status will tell you what to do&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git status

On branch feature-branch
You have unmerged paths.
  &lt;span class="o"&gt;(&lt;/span&gt;fix conflicts and run &lt;span class="s2"&gt;"git commit"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git merge --abort"&lt;/span&gt; to abort the merge&lt;span class="o"&gt;)&lt;/span&gt;

Unmerged paths:
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add &amp;lt;file&amp;gt;..."&lt;/span&gt; to mark resolution&lt;span class="o"&gt;)&lt;/span&gt;

    both modified:   file1.txt
    both modified:   file2.txt

no changes added to commit &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add"&lt;/span&gt; and/or &lt;span class="s2"&gt;"git commit -a"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# ...Resolve conflicts with your favourite tool (most IDEs can do it). Don't forget to add files after resolution !&lt;/span&gt;
&lt;span class="c"&gt;# Finalize conflict resolution and push&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you’re using fast-forward merges or semi-linear history, you’ll need to rebase your branch onto the target branch:&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;# Fetch the changes so you'll be able to resolve the conflicts locally&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git fetch

&lt;span class="c"&gt;# If you're using merge commits, merge the target branch (here it is main) into your feature branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git rebase origin/main
&lt;span class="c"&gt;# See which files are in conflict. Again, git status will tell you what to do&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git status

You are currently rebasing branch &lt;span class="s1"&gt;'feature-branch'&lt;/span&gt; on &lt;span class="s1"&gt;'4a56ad3'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

Unmerged paths:
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git reset HEAD &amp;lt;file&amp;gt;..."&lt;/span&gt; to unstage&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add &amp;lt;file&amp;gt;..."&lt;/span&gt; to mark resolution&lt;span class="o"&gt;)&lt;/span&gt;

    both modified:   file1.txt
    both modified:   file2.txt

no changes added to commit &lt;span class="o"&gt;(&lt;/span&gt;use &lt;span class="s2"&gt;"git add"&lt;/span&gt; and/or &lt;span class="s2"&gt;"git commit -a"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# ...Resolve conflicts with your favourite tool (most IDEs can do it). Don't forget to add files after resolution !&lt;/span&gt;
&lt;span class="c"&gt;# Continue conflict resolution&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git rebase &lt;span class="nt"&gt;--continue&lt;/span&gt;
&lt;span class="c"&gt;# ... Resolve conflicts until rebase is done&lt;/span&gt;
&lt;span class="c"&gt;# Then push your changes&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The conflict resolution itself is beyond the scope of this article. Here are some resources you can read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/help/idea/resolve-conflicts.html" rel="noopener noreferrer"&gt;Resolve git conflicts in IntelliJ Idea&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.visualstudio.com/docs/sourcecontrol/overview#_merge-conflicts" rel="noopener noreferrer"&gt;Resolve git conflicts in VSCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  You need to work on another branch
&lt;/h2&gt;

&lt;p&gt;In a professional development team, it is common to have shifting priorities or production bugs. These situations may force you to switch branches temporarily and to get back to your work later.&lt;/p&gt;

&lt;p&gt;There are two ways to store unfinished work: stash them or commit them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Commit your work and switch to the other branch
&lt;/h3&gt;

&lt;p&gt;There is nothing wrong with making a commit that contains unfinished work. Git allows you to change the commit later and/or squash it with other commits.&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;# Commit your work to save it in git&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;span class="c"&gt;# Switch to the new branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch other_branch
&lt;span class="c"&gt;# Work as usual on the other branch (dev, commit, push, merge...)&lt;/span&gt;
&lt;span class="c"&gt;# get back to the initial branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch initial_branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When you’re done with your work, remember to squash your commits together when merging your merge request so that the commit with partial work doesn’t end up in the main branch history. You can do this by checking the corresponding checkbox in the GitLab UI.&lt;/p&gt;
&lt;h3&gt;
  
  
  Stashing your changes
&lt;/h3&gt;

&lt;p&gt;If you prefer not to commit temporary work (for example, because you don’t want to squash commits together),&lt;br&gt;
you can also stash your changes. The stash is a local-only area in git designed to store partial work.&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;# Stash your changes&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git stash
&lt;span class="c"&gt;# at this point, all your changes are removed from the working copy so you can switch branches without trouble.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch other_branch
&lt;span class="c"&gt;# Work on the other branch, then get back to the initial branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch initial branch
&lt;span class="c"&gt;# apply the last stashed changes and drop the last stash entry&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git stash pop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Stashing changes is cleaner than commiting it, but the stash entries aren’t named and not tied to any specific branch so it becomes complicated if you work on more than two branches in parallel. See &lt;a href="https://git-scm.com/docs/git-stash" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; for more details on using the stash&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%2Ffzaowc2vkhexw7fx1k35.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%2Ffzaowc2vkhexw7fx1k35.jpg" alt="a furry fox fighting/punching/boxing/kicking a giant cyan octopus, tropical/jungle background, anime style" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  4. Going further with the command line
&lt;/h1&gt;

&lt;p&gt;If you’ve mastered all the commands up to this point, you can handle almost all situations, at least if you’re using GitLab and the GitLab flow.&lt;/p&gt;

&lt;p&gt;As you become more proficient with the command line, you may want to perform more advanced tasks directly in your terminal. Here are some examples of tasks that you can do in the command line that will give you a better understanding of git internals.&lt;/p&gt;
&lt;h2&gt;
  
  
  Someone has updated your (remote) feature branch
&lt;/h2&gt;

&lt;p&gt;When using the GitLab Flow, feature branches usually belong to only one developer. However, there may be some cases when you want to work on the same branch with two developers.&lt;/p&gt;

&lt;p&gt;In this situation, there may be times when you can’t push your commits because the other developer has already pushed changes on the target branch that you haven’t yet integrated into your local copy.&lt;/p&gt;

&lt;p&gt;When this happens, you’ll need to integrate the changes into your local branch. This is similar to the case when you reintegrate changes from the main branch, except here you integrate changes from the remote version of the feature branch.&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;# If you try to push but the branch has changed on the remote, you'll get an error like this. notice how git tells you everything about the situation ?&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
To gitlab.com:your_group/your_repo
 &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;rejected]        1-feature-1 -&amp;gt; 1-feature-1 &lt;span class="o"&gt;(&lt;/span&gt;fetch first&lt;span class="o"&gt;)&lt;/span&gt;
error: failed to push some refs to &lt;span class="s1"&gt;'gitlab.com:your_group/your_repo'&lt;/span&gt;
hint: Updates were rejected because the remote contains work that you &lt;span class="k"&gt;do &lt;/span&gt;not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: &lt;span class="s1"&gt;'git pull'&lt;/span&gt; before pushing again.
hint: See the &lt;span class="s1"&gt;'Note about fast-forwards'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;'git push --help'&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;details.
&lt;span class="c"&gt;# Pull the changes made by the other person on your branch. The --rebase is important here, because otherwise the history will be bloated with merge commits&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git pull &lt;span class="nt"&gt;--rebase&lt;/span&gt;
remote: Enumerating objects: 5, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Counting objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;5/5&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;2/2&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Total 3 &lt;span class="o"&gt;(&lt;/span&gt;delta 1&lt;span class="o"&gt;)&lt;/span&gt;, reused 0 &lt;span class="o"&gt;(&lt;/span&gt;delta 0&lt;span class="o"&gt;)&lt;/span&gt;, pack-reused 0 &lt;span class="o"&gt;(&lt;/span&gt;from 0&lt;span class="o"&gt;)&lt;/span&gt;
Unpacking objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;3/3&lt;span class="o"&gt;)&lt;/span&gt;, 286 bytes | 286.00 KiB/s, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
From gitlab.com:BenoitAverty/sandbox
   95d6184..feefa0b  1-feature-1 -&amp;gt; origin/1-feature-1
Auto-merging README.md
CONFLICT &lt;span class="o"&gt;(&lt;/span&gt;content&lt;span class="o"&gt;)&lt;/span&gt;: Merge conflict &lt;span class="k"&gt;in &lt;/span&gt;README.md
error: could not apply 6607d50... Edit README.md locally
hint: Resolve all conflicts manually, mark them as resolved with
hint: &lt;span class="s2"&gt;"git add/rm &amp;lt;conflicted_files&amp;gt;"&lt;/span&gt;, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s2"&gt;"git rebase --continue"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
hint: You can instead skip this commit: run &lt;span class="s2"&gt;"git rebase --skip"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
hint: To abort and get back to the state before &lt;span class="s2"&gt;"git rebase"&lt;/span&gt;, run &lt;span class="s2"&gt;"git rebase --abort"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
hint: Disable this message with &lt;span class="s2"&gt;"git config advice.mergeConflict false"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;git pull&lt;/code&gt; command does two things: fetch the changes on the remote repository, updating the remote branch on your local repo (remote branches are called origin/branch), then rebasing your local commits on top of the fetched changes. Note that there may be conflicts in this step, but you can resolve them just like when you resolved conflicts when reintegrating the main branch. Again, git tells you what to do when you’re done resolving merge conflicts.&lt;/p&gt;

&lt;p&gt;After finishing the rebase, you can push your changes, and this time the push should succeed.&lt;/p&gt;

&lt;p&gt;As a bonus, here’s a config option that you can set to avoid having to add the &lt;code&gt;--rebase&lt;/code&gt; option when pulling changes. This will instruct git to always rebase instead of merging when pulling changes in a branch from the tracked remote branch.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; branch.autosetuprebase &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# Now when you switch to a new branch that you've created in the remote repo, git tells you that it will rebase instead of merge when pulling changes&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git switch 2-my-feature
branch &lt;span class="s1"&gt;'2-my-feature'&lt;/span&gt; &lt;span class="nb"&gt;set &lt;/span&gt;up to track &lt;span class="s1"&gt;'origin/2-my-feature'&lt;/span&gt; by rebasing.
Switched to a new branch &lt;span class="s1"&gt;'2-my-feature'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Rewrite the history
&lt;/h2&gt;

&lt;p&gt;No one is perfect. Sometimes you will make changes to your project, commit something, change your mind, and commit something else. Or you will have to work on something else and make a temporary commit.&lt;br&gt;
Or you will want to change a commit message.&lt;/p&gt;

&lt;p&gt;You can always squash all your commits together in GitLab when merging, but git allows you to do all of this locally by rewriting the history. There are many ways to do this, but here are two ways that should cover most of your needs.&lt;/p&gt;

&lt;p&gt;In both cases, make sure your branch is up to date with the remote branch and your working copy is clean, because rewriting the history can get confusing it the remote has a different history than your branch.&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;# Update your branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git pull
&lt;span class="c"&gt;# Stash uncommited changes.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git stash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Make small changes to the history: interactive rebase
&lt;/h3&gt;

&lt;p&gt;The first way to rewrite the history is to use the interactive rebase feature. Interactive rebase lets you replay all the commits that you’ve made while adjusting some of them in the process. When you launch the interactive rebase, an editor will open with a list of all the commits you’ve made on the branch, and each commit will be associated with the &lt;code&gt;pick&lt;/code&gt; command, meaning that the commit is kept as is.&lt;/p&gt;

&lt;p&gt;You can change the pick command to another to perform various actions on the corresponding commit. The most commonly used commands in interactive rebase are &lt;code&gt;reword&lt;/code&gt; (the commit will be kept but an editor will open so you can change the commit message) and &lt;code&gt;fixup&lt;/code&gt; (The changes from this commit will be integrated in the previous commit).&lt;/p&gt;

&lt;p&gt;All the available commands are details in the editor when starting the interactive rebase.&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;# start an interactive rebase that contains all the commits that are in your feature branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Rewrite the history from scratch
&lt;/h3&gt;

&lt;p&gt;If you prefer to start again, you can also remove all the commits you’ve made in the branch (but keeping the changes to the code themselves) and start again.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git reset &lt;span class="nt"&gt;--soft&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This command will put your branch back to its starting point, the &lt;code&gt;main&lt;/code&gt; branch. But if you perform a &lt;code&gt;git status&lt;/code&gt;, you will see that all your code changes are still here, staged for commit. At this point you can perform a &lt;code&gt;git commit&lt;/code&gt; which will create a single commit containing all your changes (this is like the Gitlab option to squash all changes, but locally), or you can start again to create several commits by adding all your changes in small iterations.&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;# Remove the changes from the index, this will keep them in the working copy as if you had just made the changes toi the code&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# Re-commit all your changes by adding them in several iterations&lt;/span&gt;
git add some-file &lt;span class="c"&gt;# add only one file&lt;/span&gt;
git commit
git add &lt;span class="nt"&gt;-p&lt;/span&gt; other-file &lt;span class="c"&gt;# add only part of a file&lt;/span&gt;
git commit
git add other-file &lt;span class="c"&gt;# add the rest of the file&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="c"&gt;# add everything&lt;/span&gt;
git commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Pushing a rewritten history
&lt;/h3&gt;

&lt;p&gt;Once you’re satisfied with the new history, push it to the remote. Since you’ve rewritten the history, git will tell you that your local branch is different from the remote branch (as if someone else had made changes). In this case this is expected, so you can force push your changes. This is why it’s important to be up to date, because force pushing will replace the remote history by the local one you’ve just created. If there are commits in the remote that you didn’t include in the new history, they will be lost.&lt;/p&gt;

&lt;p&gt;This doesn’t happen in the classic GitLab flow because branches usually belong to only one developer.&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;# force-with-lease instead of force will tell you if you forgot to pull and someone has updated the remote branch&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Remove old local branches
&lt;/h2&gt;

&lt;p&gt;After some time working on the same project, you will have a lot of branches on your local repo. These branches are often left after the merge request is merged: GitLab will delete the remote branch, but if you do nothing, they’re kept in your local repository.&lt;/p&gt;

&lt;p&gt;Here’s a way to remove all branches that aren’t needed anymore.&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;# Remove local copies of remote branches&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git fetch &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;span class="c"&gt;# For each branch that has been deleted by prune, remove the local branch if it exists&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git branch &lt;span class="nt"&gt;-D&lt;/span&gt; &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The second step can be done in batch if there are many branches, but that solution involves a bit of scripting and it can be fragile. The best way is still to do it often, so you don’t have to use this if you don’t fully understand the way it works.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;-vv&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;': gone]'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | xargs git branch &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Change the user name/email in commits
&lt;/h2&gt;

&lt;p&gt;To change your user name/email only in last commit:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"John Doe &amp;lt;jdoe@zenika.fr&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But you may also want to change them on all commits on current branch. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start a rebase:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Set the targeted commits to "edit"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For each commit until &lt;code&gt;git rebase&lt;/code&gt; is done :&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"John Doe &amp;lt;jdoe@zenika.fr&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git rebase &lt;span class="nt"&gt;--continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Delete all existing files defined in .gitignore
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clean &lt;span class="nt"&gt;-Xfd&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Fetch changes to a local branch without switching to it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin feature-branch:feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Remove changes on files against target branche
&lt;/h2&gt;

&lt;p&gt;You have made a lot of commits on a branch, and notice some files should not have changes. Simply revert the changes on a file or folder :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git restore &lt;span class="nt"&gt;--source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;origin/develop path/to/file/or/folder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And commit any of the changes (back).&lt;/p&gt;
&lt;h2&gt;
  
  
  Rebase only a specific commit range
&lt;/h2&gt;

&lt;p&gt;Notice the first commit SHA you want to keep, then use the previous one as base :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--onto&lt;/span&gt; develop 7050d7bf296^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h1&gt;
  
  
  5. Git common configuration options and aliases
&lt;/h1&gt;

&lt;p&gt;Here are some widely adopted git configuration options:&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;# set your default user name/email&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.name &lt;span class="s2"&gt;"Benoit COUETIL"&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.email &lt;span class="s2"&gt;"bcouetil@zenika.fr"&lt;/span&gt;
&lt;span class="c"&gt;# on fetch, delete all the internal git objects that are not reachable from the remote repository&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; fetch.prune &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# re-apply local changes when rebasing&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; rebase.autostash &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# when pulling, local new commits are rebased onto distant new commits&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; pull.rebase &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# reuse recorded resolution on conflicts&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; rerere.enabled &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# display authors in interactive rebase&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; rebase.instructionFormat &lt;span class="s2"&gt;"(%an) %s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And here are some aliases (multi-platforms):&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;# graph alias: display git history as a graph&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.graph &lt;span class="s2"&gt;"!git --no-pager log --graph --date-order --date=short -10 --pretty=format:'%C(auto)%h%d %C(reset)%s %C(bold blue)%an %C(reset)%C(green)%cr (%cd)'"&lt;/span&gt;
&lt;span class="c"&gt;# coall alias: commit all files, including new files&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.coall &lt;span class="s1"&gt;'!git add -A &amp;amp;&amp;amp; git commit'&lt;/span&gt;
&lt;span class="c"&gt;# amend-to alias: amend all changes to a specific commit&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.amend-to &lt;span class="s1"&gt;'!f() { SHA=$(git rev-parse "$1"); git add -A &amp;amp;&amp;amp; git commit --fixup "$SHA" &amp;amp;&amp;amp; GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^"; }; f'&lt;/span&gt;
&lt;span class="c"&gt;# amend-staged-to alias: amend only staged changes to a specific commit&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.amend-staged-to &lt;span class="s1"&gt;'!f() { SHA=$(git rev-parse "$1"); git stash -k &amp;amp;&amp;amp; git commit --fixup "$SHA" &amp;amp;&amp;amp; GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^" &amp;amp;&amp;amp; git stash pop; }; f'&lt;/span&gt;
&lt;span class="c"&gt;# clean-branches alias: remove local branches deleted from GitLab&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.clean-branches &lt;span class="s1"&gt;'!git branch -vv | grep ":\ gone\]" | awk "{print \$1}" | xargs git branch -D &amp;amp;&amp;amp; printf "\n" &amp;amp;&amp;amp; git branch -vv'&lt;/span&gt;
&lt;span class="c"&gt;# search-remote alias: find remote branches containing a given word&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.search-remote &lt;span class="s1"&gt;'!f() { git fetch --all; git branch -r | grep -i "$1"; }; f'&lt;/span&gt;
&lt;span class="c"&gt;# (optional) content based git diff (versus timestamp based)&lt;/span&gt;
&lt;span class="c"&gt;# git config core.checkStat minimal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;In this article, we covered how the GitLab UI and the GitLab flow can simplify the tasks that need to be done locally in a git project. Given that there are fewer tasks to do locally, using the GitLab flow can make the git CLI easier to learn and use, while progressively discovering the full potential of the git CLI 🤓&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%2F7hay0gx1j9yfnphzi1o7.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%2F7hay0gx1j9yfnphzi1o7.jpg" alt="a furry fox fighting/punching/boxing/kicking a giant cyan octopus, tropical/jungle background, anime style" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by DiffusionBee 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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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


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


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

</description>
      <category>gitlab</category>
      <category>git</category>
      <category>devops</category>
      <category>gitflow</category>
    </item>
    <item>
      <title>☸️ Kubernetes: A Convenient Variable Substitution Mechanism for Kustomize</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Sun, 04 Aug 2024 17:56:53 +0000</pubDate>
      <link>https://dev.to/zenika/kubernetes-a-convenient-variable-substitution-mechanism-for-kustomize-lhm</link>
      <guid>https://dev.to/zenika/kubernetes-a-convenient-variable-substitution-mechanism-for-kustomize-lhm</guid>
      <description>&lt;ul&gt;
&lt;li&gt;1. What is Kustomize?&lt;/li&gt;
&lt;li&gt;2. Why variable substitution matters&lt;/li&gt;
&lt;li&gt;3. Limitations and advantages of kustomize by default&lt;/li&gt;
&lt;li&gt;
4. Implementing custom variable substitution

&lt;ul&gt;
&lt;li&gt;No compromise solution using renvsubst as a transformer&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;h1&gt;
  
  
  1. What is Kustomize?
&lt;/h1&gt;

&lt;p&gt;Kustomize is one of the prominent tools alongside Helm used for managing and configuring Kubernetes deployments across multiple environments. Unlike Helm, which utilizes templates, Kustomize simplifies management by primarily focusing on plain manifests without direct comparison to Helm’s approach.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Why variable substitution matters
&lt;/h1&gt;

&lt;p&gt;Variable substitution becomes crucial when environments are ephemeral, as in the case of feature branches, or when configuration settings like DNS entries need to be determined dynamically at deployment time. Kustomize assumes a fixed environment list (e.g., dev/staging/prod), but real life rarely fits into three neat boxes.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Limitations and advantages of kustomize by default
&lt;/h1&gt;

&lt;p&gt;The biggest advantage of Kustomize lies in its straightforward approach to managing manifests. It allows for easy manipulation of fields such as annotations, image names, and replicas across environments. However, Kustomize is strict about variable substitution, preferring a predefined set of options which can be limiting.&lt;/p&gt;

&lt;p&gt;To circumvent these limitations, the following official method is often employed within a &lt;code&gt;kustomization.yml&lt;/code&gt; file, after creating an environment-specific file:&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;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deployment.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ingress.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;service.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sa.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;role.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;role-binding.yml&lt;/span&gt;

&lt;span class="na"&gt;generatorOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;disableNameSuffixHash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;configMapGenerator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environment-variables&lt;/span&gt;
  &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;rendered/api-gateway.env&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create&lt;/span&gt;

&lt;span class="na"&gt;replacements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ConfigMap&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environment-variables&lt;/span&gt;
    &lt;span class="na"&gt;fieldPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data.API_GATEWAY_URL&lt;/span&gt;
  &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
    &lt;span class="na"&gt;fieldPaths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;spec.rules.0.host&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;spec.tls.0.hosts.0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
    &lt;span class="na"&gt;fieldPaths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;spec.template.spec.containers.0.env.[name=API_GATEWAY_URL].value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So much for such a simple need like variable substitution. People will even find Helm appealing at this step. Let's explain why and how to find a simpler solution.&lt;/p&gt;
&lt;h1&gt;
  
  
  4. Implementing custom variable substitution
&lt;/h1&gt;

&lt;p&gt;While Kustomize apply workflow supports basic variable substitution using tools like &lt;code&gt;envsubst&lt;/code&gt; on top of &lt;code&gt;kustomize build&lt;/code&gt;, more complex scenarios require custom solutions. One more refined approach involves using &lt;a href="https://github.com/containeroo/renvsubst" rel="noopener noreferrer"&gt;renvsubst&lt;/a&gt;, to selectively replace variables based on specific criteria (e.g., &lt;code&gt;K_*&lt;/code&gt; environment-specific or &lt;code&gt;CI_*&lt;/code&gt; CI-specific variables).&lt;/p&gt;

&lt;p&gt;We could then use in our pipelines, in ascending order of complexity :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For simple use cases : &lt;code&gt;kustomize build . | envsubst&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;To avoid replacing some variables, define it in manifests as &lt;code&gt;${DOLLAR}MY_VARIABLE&lt;/code&gt; and then:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kustomize build . | DOLLAR=$ envsubst&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;renvsubst&lt;/code&gt; to choose variables with prefixes:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kustomize build . | renvsubst --prefix="K_" --prefix="CI_"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But all these solutions have some drawbacks. You have to pipe the &lt;code&gt;kustomize build&lt;/code&gt; result before applying manifests; it breaks some convenient wrapping tools features like intelligent local updates using &lt;a href="https://tilt.dev/" rel="noopener noreferrer"&gt;tilt.dev&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  No compromise solution using renvsubst as a transformer
&lt;/h2&gt;

&lt;p&gt;For a no compromise solution using a Kustomize transformer &lt;a href="https://kubectl.docs.kubernetes.io/guides/extending_kustomize/exec_plugins/" rel="noopener noreferrer"&gt;exec plugin&lt;/a&gt; using &lt;code&gt;renvsubst&lt;/code&gt;, follow these steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install renvsubst&lt;/strong&gt;: Ensure &lt;code&gt;renvsubst&lt;/code&gt; is installed from &lt;a href="https://github.com/containeroo/renvsubst" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;set the &lt;code&gt;KUSTOMIZE_PLUGIN_HOME&lt;/code&gt; variable for kustomize to find your plugin, for example &lt;code&gt;KUSTOMIZE_PLUGIN_HOME=$PWD/devops/k8s/scripts/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up Transformer Script&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a transformer script (&lt;code&gt;renvsubst&lt;/code&gt;) at &lt;code&gt;$KUSTOMIZE_PLUGIN_HOME/transformers/renvsubst/renvsubst&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="nv"&gt;RENVSUBST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RENVSUBST_PATH&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;which renvsubst&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&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;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RENVSUBST&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: renvsubst command not found. Please install renvsubst from https://github.com/containeroo/renvsubst/releases or set RENVSUBST_PATH."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nv"&gt;$RENVSUBST&lt;/span&gt; &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"K_"&lt;/span&gt; &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CI_"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure Custom Resource&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Define a custom resource (&lt;code&gt;renvsubst.transformer.yml&lt;/code&gt;) next to your kustomization.yml using it:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;transformers&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;renvsubst&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;renvsubst&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Integrate with Kustomize&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Update your &lt;code&gt;kustomization.yml&lt;/code&gt; to include the custom resource:&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;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;transformers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;renvsubst.transformer.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By following these steps, you can enhance Kustomize's capabilities to handle dynamic environments and complex configuration needs effectively.&lt;/p&gt;
&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;While Kustomize offers simplicity in managing Kubernetes manifests, its default limitations in variable substitution can be overcome through strategic customization. By leveraging custom transformers and &lt;code&gt;renvsubst&lt;/code&gt;, Kubernetes deployments with customize can become more adaptable to diverse and evolving environments, ensuring smoother and more efficient operations.&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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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


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


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

</description>
      <category>kubernetes</category>
      <category>kustomize</category>
      <category>devops</category>
      <category>pipelines</category>
    </item>
    <item>
      <title>🦊 GitLab: A Python Script Displaying Latest Pipelines in a Group's Projects</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Sat, 29 Jun 2024 12:33:54 +0000</pubDate>
      <link>https://dev.to/zenika/gitlab-a-python-script-displaying-latest-pipelines-in-groups-projects-5b5a</link>
      <guid>https://dev.to/zenika/gitlab-a-python-script-displaying-latest-pipelines-in-groups-projects-5b5a</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;
1. Considered alternate solutions

&lt;ul&gt;
&lt;li&gt;GitLab's operations dashboard&lt;/li&gt;
&lt;li&gt;GitLab CI pipelines exporter&lt;/li&gt;
&lt;li&gt;Glab ci view&lt;/li&gt;
&lt;li&gt;Tabs grid browser plugins&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

2. The Python script

&lt;ul&gt;
&lt;li&gt;Pre-requisites&lt;/li&gt;
&lt;li&gt;Source code&lt;/li&gt;
&lt;/ul&gt;


&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 GitLab user, you may be handling multiple projects at once, triggering pipelines. Wouldn't it be great to see all your pipelines at a glance instead of clicking through 15 project pages? Spoiler: GitLab doesn't offer that out of the box.&lt;/p&gt;

&lt;p&gt;That's why we've developed a Python script that uses the power of GitLab API to display the latest pipeline runs for every projects in a group. As simple as :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python display-latest-pipelines.py &lt;span class="nt"&gt;--group-id&lt;/span&gt; 12345 &lt;span class="nt"&gt;--watch&lt;/span&gt;
&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%2Fmoxy9ae7e5a1378obe98.gif" 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%2Fmoxy9ae7e5a1378obe98.gif" alt="python-output" width="600" height="337"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  1. Considered alternate solutions
&lt;/h1&gt;

&lt;p&gt;Some alternate solutions have been explored before making a script from scratch.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitLab's operations dashboard
&lt;/h2&gt;

&lt;p&gt;For premium and ultimate GitLab users, there is the &lt;a href="https://docs.gitlab.com/ee/user/operations_dashboard/" rel="noopener noreferrer"&gt;Operations Dashboard&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%2Fer7th5xcxwsrv3rdsmoi.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%2Fer7th5xcxwsrv3rdsmoi.png" alt="operation-dashboard" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a nice start, but only the overall pipeline status is available, which is too light for pipeline-focused usages.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitLab CI pipelines exporter
&lt;/h2&gt;

&lt;p&gt;Mentioned in &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/pipeline_efficiency.html#pipeline-monitoring" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;, the &lt;a href="https://github.com/mvisonneau/gitlab-ci-pipelines-exporter" rel="noopener noreferrer"&gt;GitLab CI Pipelines Exporter&lt;/a&gt; for Prometheus fetches metrics from the API and pipeline events. It can check branches in projects automatically and get the pipeline status and duration. In combination with a Grafana dashboard, this helps build an actionable view for your operations team. Metric graphs can also be embedded into incidents making problem resolving easier. Additionally, it can also export metrics about jobs and environments. &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%2Fdu6wd8ifiph6bci2w3i7.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%2Fdu6wd8ifiph6bci2w3i7.jpg" alt="prometheus-exporter" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Very interesting, but the scope is way larger than our usage and does not follow pipelines in progress.&lt;/p&gt;
&lt;h2&gt;
  
  
  Glab ci view
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.gitlab.com/ee/editor_extensions/gitlab_cli/" rel="noopener noreferrer"&gt;GLab&lt;/a&gt; is an open source GitLab CLI tool. It brings GitLab to your terminal: next to where you are already working with Git and your code, without switching between windows and browser tabs.&lt;/p&gt;

&lt;p&gt;There is a particular command &lt;a href="https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/ci/view.md" rel="noopener noreferrer"&gt;displaying a pipeline&lt;/a&gt;, &lt;code&gt;glab ci view&lt;/code&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff6rp8tfjkpqgf4bxbl6r.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%2Ff6rp8tfjkpqgf4bxbl6r.png" alt="glab-ci-view" width="800" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Multiple problems for our usage :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limited to one project&lt;/li&gt;
&lt;li&gt;The result is large and does not allow many pipelines on the same screen&lt;/li&gt;
&lt;li&gt;You do not get "the latest" pipeline; you have to choose a branch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Tabs grid browser plugins
&lt;/h2&gt;

&lt;p&gt;Tab grids browser plugins are easy to use, but does not update status in real time, you have to refresh the given tabs&lt;/p&gt;
&lt;h1&gt;
  
  
  2. The Python script
&lt;/h1&gt;

&lt;p&gt;Here is the script, with some consideration :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitLab host, token, group-id, excluded projects list, stages width and watch mode are configurable&lt;/li&gt;
&lt;li&gt;For one shot mode (versus watch mode), projects pipelines are displayed one at a time to allow more real time display&lt;/li&gt;
&lt;li&gt;The least possible vertical space is used as a specific goal; you could obtain nicer (but more verbose) output with some minor script adjustment&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Pre-requisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Some Python packages installed

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install pytz&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A token having access to all the projects in the group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in similar GitLab API scripts, you might also find useful &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; which uses the same architecture to compute deployment frequency and lead time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#
# Display, in console, latest pipelines from each project in a given group
#
# python display-latest-pipelines.py --group-id=8784450 [--watch] [--token=$GITLAB_TOKEN] [--host=gitlab.com] [--exclude='TPs Benoit C,whatever'] [--stages-width=30]
#
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytz&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;GREEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[92m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;GREY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[90m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CYAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[96m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;RED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[91m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;YELLOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[93m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;BLUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[94m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;RESET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[0m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;NO_CHANGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Retrieve GitLab pipeline data for projects in a group&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gitlab.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hostname of the GitLab instance&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GitLab API access token (default: $GITLAB_TOKEN (exported) environment variable)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--group-id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ID of the group to retrieve projects from&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--exclude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Comma-separated list of project names to exclude (default: none)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--watch&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Run indefinitely while refreshing output&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--stages-width&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Width for stages display (default: 42)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NONE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Private-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;projects_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v4/groups/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/projects?include_subgroups=true&amp;amp;simple=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_or_gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Count the number of emojis in the input text.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;custom_lengths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\U0001F3D7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Construction sign 🏗️
&lt;/span&gt;        &lt;span class="c1"&gt;# Add more special characters as needed.
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;So&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;custom_lengths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;custom_lengths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_pipelines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projects_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;Failed to call GitLab instance: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;pipeline_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;no_pipelines_projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;excluded_projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;excluded_projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;pipeline_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pipelines?per_page=1&amp;amp;sort=desc&amp;amp;order_by=id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;no_pipelines_projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;updated_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%S.%fZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Europe/Paris&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;updated_at_human_readable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updated_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%d %b %Y at %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time_diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;updated_time&lt;/span&gt;
        &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time_diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;updated_ago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; seconds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 2 hours in seconds
&lt;/span&gt;            &lt;span class="n"&gt;updated_ago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; minutes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;172800&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 2 days in seconds
&lt;/span&gt;            &lt;span class="n"&gt;updated_ago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;updated_ago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;waiting_for_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preparing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canceled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREY&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLUE&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RED&lt;/span&gt;
        &lt;span class="nf"&gt;print_or_gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;↓ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ref&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (since &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;updated_at_human_readable&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;updated_ago&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ago)&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;job_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;jobs_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pipelines/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;job_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="nf"&gt;match &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🟢&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔵&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLUE&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔘&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NO_CHANGE&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canceled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔘&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREY&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🟠&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YELLOW&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;▶️&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NO_CHANGE&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🟠&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YELLOW&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔴&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="n"&gt;job_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RED&lt;/span&gt;
                &lt;span class="nf"&gt;case &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Sort jobs within each stage alphabetically by job name
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="c1"&gt;# Find the maximum number of jobs in any stage for this pipeline
&lt;/span&gt;        &lt;span class="n"&gt;max_jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_jobs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="c1"&gt;# stages start with a border character instead of a space
&lt;/span&gt;        &lt;span class="c1"&gt;# Print out the job data for each stage, padding to make all stages content the same length
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;job_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[ &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; ]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;╔&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stages_width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;count_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;═&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;╗ &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="c1"&gt;# emojis in job names make this exercise a bit more difficult: ljust make them expand the size, so we compensate
&lt;/span&gt;                &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stages_width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;count_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stages_width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;count_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_jobs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stages_width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print_or_gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;no_pipelines_projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print_or_gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\033&lt;/span&gt;&lt;span class="s"&gt;[90mProjects without pipeline: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;no_pipelines_projects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\033&lt;/span&gt;&lt;span class="s"&gt;[0m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_pipelines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1b&lt;/span&gt;&lt;span class="s"&gt;[2J&lt;/span&gt;&lt;span class="se"&gt;\x1b&lt;/span&gt;&lt;span class="s"&gt;[H&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Clear the screen
&lt;/span&gt;                &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;fetch_pipelines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyboardInterrupt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&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%2Ffzoly9dsig2gwoi88tae.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%2Ffzoly9dsig2gwoi88tae.jpg" alt="a humanoid fox from behind watching metrics dashboards, multiple computer monitors,manga style" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Pinokio using Stable Cascade plugin&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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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


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


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

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>python</category>
      <category>productivity</category>
    </item>
    <item>
      <title>☸️ Why Managed Kubernetes is a Viable Solution for Even Modest but Actively Developed Applications</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Wed, 05 Jun 2024 09:41:12 +0000</pubDate>
      <link>https://dev.to/zenika/why-managed-kubernetes-is-a-viable-solution-for-even-modest-but-actively-developed-applications-357g</link>
      <guid>https://dev.to/zenika/why-managed-kubernetes-is-a-viable-solution-for-even-modest-but-actively-developed-applications-357g</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;
For development/developers

&lt;ul&gt;
&lt;li&gt;Advantage 1: A rich ecosystem of development tools&lt;/li&gt;
&lt;li&gt;Advantage 2: A desired-state system to ease changes&lt;/li&gt;
&lt;li&gt;Advantage 3: advanced networking and subdomain management&lt;/li&gt;
&lt;li&gt;Advantage 4: ready-made solutions for stateful services&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

For early stages of production

&lt;ul&gt;
&lt;li&gt;Advantage 5: a rich ecosystem of production tools&lt;/li&gt;
&lt;li&gt;Advantage 6: self-healing and self-management mechanisms&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

When the product is successful

&lt;ul&gt;
&lt;li&gt;Advantage 7: the features and stability of one of the most maintained open source projects&lt;/li&gt;
&lt;li&gt;Advantage 8: a portable vendor-neutral platform with tight integrations&lt;/li&gt;
&lt;li&gt;Advantage 9: scalability for diverse workloads&lt;/li&gt;
&lt;li&gt;Advantage 10: high availability in continuous delivery context&lt;/li&gt;
&lt;li&gt;Advantage 11: security tools for peace of mind&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;h1&gt;
  
  
  Initial thoughts
&lt;/h1&gt;

&lt;p&gt;Kubernetes, has emerged as a transformative force in the world of software development and deployment. Born as an open-source project under the guidance of the Cloud Native Computing Foundation (CNCF), Kubernetes has evolved into one of the most influential technologies powering modern infrastructure and application management.&lt;/p&gt;

&lt;p&gt;Kubernetes has gained so much traction within the community that people started to use it for small usages, where the need for a container orchestrator is far from necessary. We now see Kubernetes jokes, memes, and even Kubernetes bashing.&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%2Fhutbkp1ebknrmdchbhz4.jpeg" 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%2Fhutbkp1ebknrmdchbhz4.jpeg" alt="dilbert-techy-things" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I like Kubernetes. but I've bin an even bigger fan of the KISS principle. An efficient monolith is beautiful. An application working well on a single server is beautiful. And yet, I still recommend Kubernetes for modest commercial applications. More precisely, for modest teams (at least 3 full time developers working efficiently). Modest team and not below, because lots of advantages concerns active and collaborative development.&lt;/p&gt;

&lt;p&gt;We choose the managed side of Kubernetes, because let's face it, it is hard (and costly) to maintain on your own below a certain tech team size. At least it better be managed from the dev team perspective.&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%2Fwuwss6na2armdq5ilbv6.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%2Fwuwss6na2armdq5ilbv6.png" alt="k8s-learning-curve" width="660" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we will uncover the reasons why Kubernetes still stands as a great choice for a wide array of commercial applications, even small and/or starting ones, for a various reasons, beyond scalability and self-healing.&lt;/p&gt;

&lt;h1&gt;
  
  
  For development/developers
&lt;/h1&gt;

&lt;p&gt;Way too often, we underestimate the impact of an architecture choice on the team velocity. Yes, these choices do not only impact our production environment. I would dare to say that it is the least of challenges for modest teams. The biggest challenge we face is to build a product at a fast and yet sustainable pace, to hopefully become successful someday.&lt;/p&gt;

&lt;p&gt;Some people choose an architecture or another depending on minor cloud cost differences. But the infrastructure cost on an actively maintained modest application is relatively rarely the problem. The biggest cost is (or should be) the team itself. How impactful is a 30$ a day kubernetes service when the team costs more than 2000$ a day ? Trying to save 10$ a day on these servers often leads to more than 100$ a day of team inefficiency. And there is always room for cost optimization for a given architecture without impacting performance, as described for Kubernetes in the article &lt;a href="https://dev.to/zenika/eks-10-tips-to-reduce-the-bill-up-to-90-on-aws-managed-kubernetes-clusters-epe"&gt;FinOps EKS: 10 Tips to Reduce the Bill up to 90% on AWS Managed Kubernetes Clusters&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Now that we advocated for the cost of Kubernetes clusters to not be a problem for development environments, let's talk about real advantages.&lt;/p&gt;

&lt;p&gt;Also, Kubernetes enables efficient development git workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One Environment Per Branch: Kubernetes allows you to create dedicated environments for each Git branch. This isolation aids testing, development, and collaboration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rapid Feedback Loops: Developers can quickly iterate by deploying changes to their own environments, ensuring that new features and bug fixes are thoroughly tested.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Collaboration: Kubernetes supports collaborative development by providing, if needed, an environment for each contributor to validate their work before integration, each one in their single namespace.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Advantage 1: A rich ecosystem of development tools
&lt;/h2&gt;

&lt;p&gt;Kubernetes offers an extensive set of development productivity tools. These tools simplify the development process, letting developers focus on writing code rather than dealing with infrastructure. From local development environments to CI/CD pipelines, Kubernetes provides resources that genuinely make a difference — fewer YAML headaches, more shipping.&lt;/p&gt;

&lt;p&gt;You can check some maintained lists mentioned in &lt;a href="https://dev.to/zenika/kubernetes-awesome-maintained-links-you-will-keep-using-next-year-48o8"&gt;Kubernetes: Awesome Maintained Links You Will Keep Using Next Year&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 2: A desired-state system to ease changes
&lt;/h2&gt;

&lt;p&gt;Kubernetes, with its declarative constructs and its ops friendly approach, has fundamentally changed deployment methodologies: it allows teams to use GitOps, Thanks to the &lt;a href="https://branislavjenco.github.io/desired-state-systems/" rel="noopener noreferrer"&gt;desired-state principle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When applying small changes such has configuration or single module code, Kubernetes will perform the necessary minimal actions to meet the new desired state. Once a developer understands the basics of kubernetes manifests (or Helm configuration), he can request changes without knowing how they will be performed by the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 3: advanced networking and subdomain management
&lt;/h2&gt;

&lt;p&gt;Kubernetes provides advanced networking capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ingress Controller: Kubernetes offers an Ingress Controller that allows you to manage subdomains, routing, and load balancing effectively.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Subdomain Routing: With Kubernetes, you can route traffic to different services based on subdomains, enabling you to host multiple applications under a single domain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once an Ingress Controller is installed and a wildcard domain is affected to a cluster, developers can easily affect sub-domains to sub-environment and/or services, without the need of any additional service or operational teams ((always busy, aren't they ?).&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 4: ready-made solutions for stateful services
&lt;/h2&gt;

&lt;p&gt;Even if we mostly move stateful services outside Kubernetes clusters in production, Kubernetes simplifies the deployment of stateful services for development and testing purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Databases: Kubernetes offers pre-packaged solutions for popular databases like MySQL, PostgreSQL, and MongoDB, making it easy to manage and scale your data stores.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Message Brokers: You can deploy message brokers like Apache Kafka or RabbitMQ as stateful services on Kubernetes, enabling reliable communication between microservices.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For other less common stateful usages, Kubernetes provides StatefulSets, a controller that ensures stable, unique network identities and persistent storage for stateful applications.&lt;/p&gt;

&lt;p&gt;These solutions simplify the management of stateful components, allowing you to focus on building your commercial applications.&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%2F1hbsltei95bu45qra6nx.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%2F1hbsltei95bu45qra6nx.jpg" alt="blue sailboat, ((blue sails)), tropical" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  For early stages of production
&lt;/h1&gt;

&lt;p&gt;Early stages of production life can be summed-up too often as: no time, no money, no product support, lots of bugs.&lt;/p&gt;

&lt;p&gt;In this stressful period, various reason make Kubernetes a wise choice.&lt;/p&gt;

&lt;p&gt;We still do not need those high-availability features that make funny jokes on small applications, but some others are valuable in this phase.&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%2F40i92epgue4fye24wt54.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%2F40i92epgue4fye24wt54.jpg" alt="zero-user" width="800" height="1062"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 5: a rich ecosystem of production tools
&lt;/h2&gt;

&lt;p&gt;Kubernetes has a rich ecosystem of production tools that contribute to the three pillars of observability. Let's explore these essential components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; Kubernetes provides powerful solutions such as Prometheus and Grafana for real-time monitoring and alerting. These tools ensure you can proactively maintain the health and performance of your applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logging:&lt;/strong&gt; Efficient log management is crucial for troubleshooting issues in a distributed environment. Kubernetes offers tools like Fluentd and Elasticsearch to centralize and analyze logs, providing insights into the behavior of your applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tracing:&lt;/strong&gt; Observing the flow of requests through various microservices is vital for understanding the performance of complex systems. Kubernetes supports tracing solutions, such as Jaeger or Zipkin, enabling you to trace requests as they traverse different components.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These production tools collectively enhance the reliability, performance, and overall observability of commercial applications deployed on Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 6: self-healing and self-management mechanisms
&lt;/h2&gt;

&lt;p&gt;Kubernetes has been built with self-healing and self-management in mind.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Self-Healing: Kubernetes automatically restarts containers that fail, replaces containers, and kills containers that don't respond to your user-defined health check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Self-Management: Kubernetes automatically manages the lifecycle of your containers, including scaling, upgrading, and rolling back.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hardware failure is also handled, and failing nodes/servers will trigger automatic workload balance on healthy nodes.&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%2Fclbme9ixhuheup8atspe.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%2Fclbme9ixhuheup8atspe.jpg" alt="blue sailboat, ((blue sails)), tropical" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  When the product is successful
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Advantage 7: the features and stability of one of the most maintained open source projects
&lt;/h2&gt;

&lt;p&gt;In the world of open-source software, Kubernetes is one of the most well-maintained projects. It began as an internal tool at Google called Borg and later became open-source in 2014. Since then, it has grown into a thriving community of developers, users, and contributors who keep it actively maintained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community-powered advancements
&lt;/h3&gt;

&lt;p&gt;Kubernetes benefits from a diverse global community. People from various backgrounds, organizations, and cloud providers work together to continuously enhance the platform. This collaborative effort drives innovation, ensuring that Kubernetes stays at the forefront of container orchestration technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Swift updates and issue fixes
&lt;/h3&gt;

&lt;p&gt;A well-maintained project delivers updates and fixes problems promptly. Kubernetes exemplifies this by regularly releasing updates and addressing issues efficiently. Users can rely on a stable and evolving platform that keeps up with the fast-paced world of cloud-native technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensive documentation
&lt;/h3&gt;

&lt;p&gt;Kubernetes places a strong emphasis on providing clear documentation and resources. This commitment to transparency ensures that users, regardless of their expertise, can easily access the information they need. The Kubernetes website, along with community-contributed guides, tutorials, and forums, offers valuable knowledge for users of all skill levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neutral governance
&lt;/h3&gt;

&lt;p&gt;Kubernetes is governed by the Cloud Native Computing Foundation (CNCF), a neutral body that ensures no single entity dominates decision-making. This neutrality encourages a fair and adaptable solution, free from corporate bias. As a result, Kubernetes serves as a versatile and reliable option for various commercial applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 8: a portable vendor-neutral platform with tight integrations
&lt;/h2&gt;

&lt;p&gt;Kubernetes is versatile when it comes to deployment platforms. It can be set up on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cloud Providers: Kubernetes is supported on major cloud platforms like AWS, Google Cloud, and Azure, making it easy to deploy and manage your applications in the cloud.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Private Clouds: If you prefer to run Kubernetes in your private data center, it's fully adaptable to private cloud environments, offering control and security.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On-Premises: For complete control over your infrastructure, you can deploy Kubernetes on on-premises servers, ensuring data stays within your network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lightweight or Full-Fledged: Whether you need a lightweight setup on a single VM for development or a large-scale deployment spanning multiple VMs for production, Kubernetes can accommodate your needs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes' adaptability across these platforms makes it a viable choice for various commercial applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 9: scalability for diverse workloads
&lt;/h2&gt;

&lt;p&gt;Kubernetes offers architecture scalability that can adapt to diverse workloads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Lightweight Distribution: You can use a lightweight Kubernetes distribution on a single virtual machine (VM) for smaller workloads, development, and testing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full-Fledged Deployment: For heavy production workloads, Kubernetes can scale across multiple VMs or nodes. It can handle a wide range of applications and services efficiently.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scalability can also be considered at the application level, with a manual update of the number of pods or automatically using pods autoscalers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 10: high availability in continuous delivery context
&lt;/h2&gt;

&lt;p&gt;Sure, a single VM is fine for serving a single starting application, and we can accommodate downtimes twice a month for deployment. But the more you lean toward continuous delivery, the more you need to consider the availability of your applications.&lt;/p&gt;

&lt;p&gt;Kubernetes offers high-availability solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Deployments, Daemonsets, Statefulsets: Kubernetes offers multiple controllers and deployment strategies, which allows you to create multiple replicas of a single application. This ensures that your application is highly available and can handle any unexpected load.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Canary Deployments: Canary deployments allow you to roll out new versions of your application in a controlled manner, ensuring that users are not impacted by the new features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blue/Green Deployments: Blue/Green deployments allow you to roll out new versions of your application alternatively to blue and green production environments, allowing complex upgrades with zero downtime deployments&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These solutions ensure that your applications are highly available and can handle any type of deployment anytime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantage 11: security tools for peace of mind
&lt;/h2&gt;

&lt;p&gt;Kubernetes offers solid security features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;mTLS Support: Kubernetes supports mutual TLS (mTLS) authentication, ensuring secure communication between services within your applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role-Based Access Control (RBAC): RBAC in Kubernetes allows you to define fine-grained access control policies, ensuring that only authorized users can perform specific actions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integration with secret managers: Kubernetes ecosystem provides secure ways to manage secrets, ensuring sensitive information remains protected.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These security tools allow you to build and operate commercial applications with confidence, safeguarding your data and infrastructure.&lt;/p&gt;

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

&lt;p&gt;In summary, Kubernetes is a solid choice for commercial applications. It has active maintenance, a rich development toolset, diverse production tools, and works across cloud providers, private clouds, and on-premises environments. Its scalability, advanced networking, security features, and compatibility with various stateful components further reinforce its viability.&lt;/p&gt;

&lt;p&gt;Moreover, Kubernetes supports efficient development workflows with Git branching strategies. By using Kubernetes, businesses can improve productivity, ensure scalability, and handle the challenges of modern software development.&lt;/p&gt;

&lt;p&gt;When considering architectures for commercial applications, Kubernetes should be a top contender — it earns its complexity budget.&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%2Fmhi177z55bf099f6s6p0.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%2Fmhi177z55bf099f6s6p0.jpg" alt="blue sailboat, ((blue sails)), tropical" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated locally by Automatic1111 using ToonYou 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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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

&lt;/div&gt;


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

</description>
      <category>kubernetes</category>
      <category>architecture</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>🤖 Future-Proof Tech Blogging: Understanding AI's Core Traits</title>
      <dc:creator>Benoit COUETIL 💫</dc:creator>
      <pubDate>Fri, 19 Apr 2024 11:14:57 +0000</pubDate>
      <link>https://dev.to/zenika/future-proof-tech-blogging-understanding-ais-core-traits-3h00</link>
      <guid>https://dev.to/zenika/future-proof-tech-blogging-understanding-ais-core-traits-3h00</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Initial thoughts&lt;/li&gt;
&lt;li&gt;1. The influence of your intentions&lt;/li&gt;
&lt;li&gt;2. Understanding reader engagement&lt;/li&gt;
&lt;li&gt;3. Leveraging original content to surpass AI&lt;/li&gt;
&lt;li&gt;4. Laziness? Achieving greater impact with limited time!&lt;/li&gt;
&lt;li&gt;5. Less human content? Prioritizing quality over quantity!&lt;/li&gt;
&lt;li&gt;
6. Quality content has been concealed? Search tools will evolve!

&lt;ul&gt;
&lt;li&gt;Understanding reader experience in tech blogs&lt;/li&gt;
&lt;li&gt;Tech blogging evolution: from scarcity to overabundance&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

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

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

&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article on the future of tech writing is dynamic and continuously evolving to reflect my changing perspective. Your comments are highly valued and have already influenced significant revisions to the content.&lt;/em&gt;&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I believe AI is bad, prove me wrong. For over a month now, I have been thinking, what is the value of a post if AI has become this excellent and web scraping AI programs are copying content and shunning them out in large numbers, more people are becoming lazier, and some of us who put in hard work are getting lesser and lesser value."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These words, shared by a fellow tech blogger publishing articles almost every day, encapsulate the growing concerns surrounding the impact of AI (Artificial Intelligence) on the field of tech blogging.&lt;/p&gt;

&lt;p&gt;While some may share this sentiment, we can approach this topic from a radically different perspective, shaped by our collective experience in the context of this revolution. As a time-limited tech blogger, I view AI not as a threat, but as a catalyst for innovation and growth. Let me explain why AI is actually your best ally as a tech blogger — not your replacement.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. The influence of your intentions
&lt;/h1&gt;

&lt;p&gt;if your goal is JUST a broad audience, create &lt;a href="https://en.wikipedia.org/wiki/Listicle" rel="noopener noreferrer"&gt;listicles&lt;/a&gt; on any Javascript subject 😅.&lt;/p&gt;

&lt;p&gt;For those wanting to positively impact people with their articles on any other subject, several factors determine a wide readership, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advertisement strategies on popular writing platforms, self-promotion efforts, paid plans, and reaction bots&lt;/li&gt;
&lt;li&gt;Reader needs, whether actively searching for content on search engines (traditional or AI-powered)&lt;/li&gt;
&lt;li&gt;Social sharing by enthusiastic readers&lt;/li&gt;
&lt;li&gt;Accessibility considerations, such as readability, language clarity, and compatibility with assistive technologies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other side, for tech bloggers who primarily write for personal reasons, such as showcasing their skills to superiors or close peers, the reach is less important. These writers are less likely to be significantly affected by advancements in AI.&lt;/p&gt;

&lt;p&gt;For the rest of this article, we will focus on tech blogging that aims to have a meaningful impact on fellow developers.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Understanding reader engagement
&lt;/h1&gt;

&lt;p&gt;How can we make our tech content engaging?&lt;/p&gt;

&lt;p&gt;Drawing inspiration from Aristotle's &lt;a href="https://en.wikipedia.org/wiki/Modes_of_persuasion" rel="noopener noreferrer"&gt;modes of persuasion&lt;/a&gt;, readers are influenced by logical reasoning (&lt;em&gt;Logos&lt;/em&gt;), the credibility of sources or blogger experiences in the field (&lt;em&gt;Ethos&lt;/em&gt;), and emotional connections (&lt;em&gt;Pathos&lt;/em&gt;). As a writer, address at least one of these elements to effectively engage your audience.&lt;/p&gt;

&lt;p&gt;For example, opinions gain significance when backed by logic (&lt;em&gt;Logos&lt;/em&gt;) and presented by a credible writer (&lt;em&gt;Ethos&lt;/em&gt;). Writers lacking established reputation may struggle to make a substantial impact, resonating primarily with less discerning readers. Establishing credibility (&lt;em&gt;Ethos&lt;/em&gt;) is essential for broadening your audience. However, it is worth noting that emotional appeals (&lt;em&gt;Pathos&lt;/em&gt;) can also be highly effective, as most individuals are responsive to them.&lt;/p&gt;

&lt;p&gt;Understanding and effectively incorporating the principles of &lt;em&gt;Logos&lt;/em&gt;, &lt;em&gt;Ethos&lt;/em&gt;, and &lt;em&gt;Pathos&lt;/em&gt; in your tech blogging can improve your content, engage a wider audience, and establish your reputation as a knowledgeable and persuasive writer in the field.&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%2F8jzrqffji9rik0gtb2xd.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%2F8jzrqffji9rik0gtb2xd.jpg" alt="single humanoid robot writing a technical article inside, warm ambience, white winter outside" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Leveraging original content to surpass AI
&lt;/h1&gt;

&lt;p&gt;AI has undoubtedly revolutionized content creation by synthesizing existing information in a matter of seconds with impeccable precision. Some argue that it is not, but it is a matter of months before it will be. Blogging posts saying that it is not perfect, like &lt;a href="https://medium.com/@discoverthetechdtt/is-blogging-dead-after-chatgpt-54a6b250f93c" rel="noopener noreferrer"&gt;Is Blogging Dead After ChatGPT?&lt;/a&gt;, will not age well. Let's consider a state where AI has been perfected to the point that content cannot be distinguished.&lt;/p&gt;

&lt;p&gt;Where AI falls short is in providing &lt;strong&gt;genuine novelty and unique insights&lt;/strong&gt;. While it excels at aggregating existing knowledge, it lacks the ability to generate truly original content. AI may soon become the best content synthesizer tool of all time.&lt;/p&gt;

&lt;p&gt;We must then acknowledge that not all human content is created equal. Articles that simply regurgitate information or offer generic opinions are quickly becoming superseded by AI. Instead, it is the articles that bring fresh perspectives, personal anecdotes, or profound insights that stand the test of time.&lt;/p&gt;

&lt;p&gt;Consider the following content that will not be contested by AI anytime soon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Original Thought Leadership:&lt;/strong&gt; Articles that dig into unexplored topics or offer fresh insights are inherently valuable. By being the first to tackle a subject or presenting a unique perspective, writers can capture readers' attention and establish themselves as thought leaders in their respective fields.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Credible Opinion Pieces:&lt;/strong&gt; Readers place a premium on the opinions of credible individuals. As experienced tech bloggers, we have the opportunity to use our expertise and credibility to offer valuable insights and recommendations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Emotionally Engaging Narratives:&lt;/strong&gt; Stories have the power to evoke emotions and create lasting impressions. By infusing personal anecdotes or captivating narratives into our writing, we can forge deeper connections with our audience and leave a lasting impact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In-depth Analysis:&lt;/strong&gt; Articles that offer in-depth analysis or explore complex topics in detail are highly sought after. As experienced writers, we possess the knowledge and analytical skills to dissect complex subjects and provide valuable insights to our readers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  4. Laziness? Achieving greater impact with limited time!
&lt;/h1&gt;

&lt;p&gt;Laziness is prompting ChatGPT and publishing the result as-is. But leveraging automation to handle repetitive and time-consuming tasks is a sensible approach. As tech bloggers, we are familiar with tasks that require repetition. It may include keyword research, generating topic ideas, formatting content, translation, optimizing SEO, banner prompt ideas, and even moderating comments.&lt;/p&gt;

&lt;p&gt;Additionally, writing efficiently enables us to produce a greater number of articles within the constraints of our limited time. When coupled with high-quality content, this results in an increased volume of original material being shared with the global audience, making knowledge more accessible to all.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Less human content? Prioritizing quality over quantity!
&lt;/h1&gt;

&lt;p&gt;When our blog posts revolve around rehashing existing material or, worse, generating content without a thorough understanding of the existing literature, we find ourselves competing with AI in its area of expertise. This is a futile endeavor, regardless of the level of effort exerted.&lt;/p&gt;

&lt;p&gt;In the era of AI, we may encounter peers feedback suggesting "ChatGPT could have produced superior content in mere seconds." While this feedback may seem harsh, it raises the bar; it contributes to collective improvement and encourages writers to invest their time efficiently.&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%2Fdxr3r4whaqxwidflus9f.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%2Fdxr3r4whaqxwidflus9f.jpg" alt="single humanoid robot writing a technical article inside, warm ambience, white winter outside" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  6. Quality content has been concealed? Search tools will evolve!
&lt;/h1&gt;

&lt;p&gt;Jon Randy made a well-articulated statement in the comment section:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI has absolutely trashed all tech blogging websites (that I know of). It's extremely difficult now to find anything at all interesting to read on these sites.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The innate laziness of human beings combined with the social media age's creation of a craving for 'likes', and the weird insistence that creating an online presence/brand is somehow 'essential' has resulted in a tidal wave of generated/assisted rehashed content that has been published a million times before - all in a weird style that makes you feel like you've actually learned nothing from reading the article - about the subject, or the author's actual thoughts about it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Generative AI has unfortunately drowned out most of the good content that still exists, simply because the majority of people seem to be using it as a pure 'content' producer rather than an assistive tool.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Generative AI has overshadowed quality content as many use it solely as a content generator rather than an aid. This trend is not new or irreversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding reader experience in tech blogs
&lt;/h2&gt;

&lt;p&gt;To comprehend the current state of tech blogs, we can analyze the reader experience using Peter Morville's &lt;a href="https://semanticstudios.com/user_experience_design/" rel="noopener noreferrer"&gt;User Experience Honeycomb&lt;/a&gt;. Blog content value can be categorized into six facets: &lt;em&gt;Desirable&lt;/em&gt;, &lt;em&gt;Accessible&lt;/em&gt;, &lt;em&gt;Credible&lt;/em&gt;, &lt;em&gt;Findable&lt;/em&gt;, &lt;em&gt;Usable&lt;/em&gt;, and &lt;em&gt;Useful&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv7wqd5hqmvlqtj6dtmqb.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%2Fv7wqd5hqmvlqtj6dtmqb.png" alt="User Experience Honeycomb" width="593" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can draw parallels with Aristotle's concepts: desirable/&lt;em&gt;Pathos&lt;/em&gt;, credible/&lt;em&gt;Ethos&lt;/em&gt;. The connection to &lt;em&gt;Logos&lt;/em&gt; is more abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech blogging evolution: from scarcity to overabundance
&lt;/h2&gt;

&lt;p&gt;Let's focus on content that is &lt;em&gt;credible&lt;/em&gt; and &lt;em&gt;accessible&lt;/em&gt;. Valuable tech blogging content is what users also find &lt;em&gt;usable&lt;/em&gt;, &lt;em&gt;useful&lt;/em&gt;, and/or &lt;em&gt;desirable&lt;/em&gt;. Today, &lt;em&gt;findability&lt;/em&gt; is the challenge.&lt;/p&gt;

&lt;p&gt;Blogging history can be divided into several eras:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Early Internet&lt;/strong&gt;: Valuable content was primarily found in books, limiting accessibility. Valuable content was scarce and not easily &lt;em&gt;findable&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blogging Golden Age&lt;/strong&gt;: Valuable content was readily available online, with authors sharing unique insights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inflated Online Presence&lt;/strong&gt;: There was a push for creating an online brand, and consequently valuable content was not easily &lt;em&gt;findable&lt;/em&gt;. This issue predated the rise of AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generative AI Domination&lt;/strong&gt;: AI now produces content effortlessly, more and more blurring the line between human and AI-generated content. Original content is increasingly difficult to &lt;em&gt;find&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The future will certainly bring the &lt;strong&gt;Post-Google Search Era&lt;/strong&gt;: Tech websites relying on AI-generated content are fading, and platforms like Medium and Dev.to are combating redundant articles. Tools are emerging to identify and limit low-value ones. AI could assist in evaluating an article's novelty compared to existing knowledge.&lt;/p&gt;

&lt;p&gt;Valuable content will soon becoming &lt;em&gt;findable&lt;/em&gt; again! Investing in it is surely not a waste of time.&lt;/p&gt;

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

&lt;p&gt;As tech blogging evolves with the rise of AI and ChatGPT, it's crucial to distinguish ourselves by focusing on valuable, original content that offers fresh insights or unique perspectives. By investing our time efficiently and prioritizing quality over quantity, we can create a lasting impact in the era of digital information saturation.&lt;/p&gt;

&lt;p&gt;The future holds exciting possibilities: advances in search tools will make high-quality, valuable content more discoverable, and platforms are emerging to combat redundant articles. As tech bloggers, we have an opportunity to invest our time wisely and contribute valuable insights to our ever-curious readers.&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%2Ff7oj5bjgyocrwq9lmzyl.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%2Ff7oj5bjgyocrwq9lmzyl.jpg" alt="single humanoid robot writing a technical article inside, warm ambience, white winter outside" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrations generated using Stable Cascade on HuggingFace.&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/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bcouetil/all-my-articles-by-theme-463k#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


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

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

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

&lt;/div&gt;


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

</description>
      <category>writing</category>
      <category>career</category>
      <category>ai</category>
      <category>chatgpt</category>
    </item>
  </channel>
</rss>
