<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mahendra Singh</title>
    <description>The latest articles on Forem by Mahendra Singh (@akoode_tech).</description>
    <link>https://forem.com/akoode_tech</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%2F3913898%2F22861018-69f3-4bf5-bba3-2d6a546b6512.jpg</url>
      <title>Forem: Mahendra Singh</title>
      <link>https://forem.com/akoode_tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/akoode_tech"/>
    <language>en</language>
    <item>
      <title>GitHub Actions vs Jenkins vs GitLab CI: A Developer's Honest Comparison (2026)</title>
      <dc:creator>Mahendra Singh</dc:creator>
      <pubDate>Tue, 26 May 2026 18:49:48 +0000</pubDate>
      <link>https://forem.com/akoode_tech/github-actions-vs-jenkins-vs-gitlab-ci-a-developers-honest-comparison-2026-481k</link>
      <guid>https://forem.com/akoode_tech/github-actions-vs-jenkins-vs-gitlab-ci-a-developers-honest-comparison-2026-481k</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Actions vs Jenkins vs GitLab CI: A Developer's Honest Comparison (2026)
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This article is a technical rewrite of the original comparison published at &lt;a href="https://www.akoode.com/blog/github-actions-vs-jenkins-vs-gitlab-ci" rel="noopener noreferrer"&gt;akoode.com&lt;/a&gt;. Code examples and architecture breakdowns added for Dev.to readers.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've set up pipelines on all three. Migrated a team off Jenkins to GitHub Actions. Watched a startup pick GitLab CI because the security scanning was bundled. Watched another stay on Jenkins because their infra literally couldn't reach the internet.&lt;/p&gt;

&lt;p&gt;Here's the honest breakdown — no vendor spin, just the practical trade-offs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Philosophical Difference
&lt;/h2&gt;

&lt;p&gt;Before the feature tables, get this mental model right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; — pipeline-as-code that lives &lt;em&gt;inside your repo&lt;/em&gt;, triggered by GitHub events. No server to manage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins&lt;/strong&gt; — a self-hosted automation &lt;em&gt;server&lt;/em&gt; you fully control. Maximum flexibility, maximum maintenance cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab CI&lt;/strong&gt; — pipeline engine &lt;em&gt;inside a complete DevOps platform&lt;/em&gt;. If you want one tool for code + CI + security scanning + registry, this is it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb in 2026: &lt;strong&gt;pipeline tools follow platform gravity&lt;/strong&gt;. The best CI/CD tool is usually the one closest to where your code lives. That's why GitHub Actions and GitLab CI have eaten Jenkins' lunch for new projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Each Tool Actually Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Workflows live at &lt;code&gt;.github/workflows/*.yml&lt;/code&gt;. Triggered by any GitHub event — push, PR, release, schedule, manual dispatch, or an external webhook.&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;# .github/workflows/ci.yml&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;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jobs run on &lt;strong&gt;runners&lt;/strong&gt; — GitHub-hosted VMs (Ubuntu, Windows, macOS) or self-hosted machines you register. The &lt;a href="https://github.com/marketplace?type=actions" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt; has 20,000+ reusable actions. Most integrations are already there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jenkins
&lt;/h3&gt;

&lt;p&gt;Jenkins uses a &lt;strong&gt;controller-agent architecture&lt;/strong&gt;. The controller handles scheduling and the UI. Agents run the actual build jobs — you can scale to hundreds of distributed agents.&lt;/p&gt;

&lt;p&gt;Pipelines are &lt;code&gt;Jenkinsfile&lt;/code&gt;s written in Groovy (or declarative syntax):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jenkinsfile&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;agent&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;

  &lt;span class="n"&gt;stages&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="s1"&gt;'Install'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'npm ci'&lt;/span&gt;
      &lt;span class="o"&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="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'npm test'&lt;/span&gt;
      &lt;span class="o"&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="s1"&gt;'Build'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'npm run build'&lt;/span&gt;
      &lt;span class="o"&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="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sshPublisher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;publishers:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;sshPublisherDesc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;configName:&lt;/span&gt; &lt;span class="s1"&gt;'prod-server'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;transfers:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sshTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;sourceFiles:&lt;/span&gt; &lt;span class="s1"&gt;'dist/**'&lt;/span&gt;&lt;span class="o"&gt;)])&lt;/span&gt;
        &lt;span class="o"&gt;])&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Groovy is a real programming language — loops, conditionals, shared libraries. YAML tools hit ceilings on complex logic. Jenkins doesn't.&lt;/p&gt;

&lt;p&gt;Over 1,800 plugins cover basically every tool in the DevOps ecosystem. If it exists, Jenkins can probably integrate with it.&lt;/p&gt;

&lt;p&gt;The cost: &lt;strong&gt;you run the server&lt;/strong&gt;. Security patches, plugin compatibility, scaling — all yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab CI
&lt;/h3&gt;

&lt;p&gt;Config lives at &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; in the repo root. Syntax is YAML, similar feel to GitHub Actions:&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;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;test&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&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;npm ci&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

&lt;span class="na"&gt;build&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;build&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;npm run build&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dist/&lt;/span&gt;

&lt;span class="na"&gt;deploy&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;deploy&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;rsync -avz dist/ user@prod-server:/var/www/app/&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;environment&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;production&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://myapp.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes GitLab different is the &lt;strong&gt;platform depth&lt;/strong&gt;. One installation gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source code management&lt;/li&gt;
&lt;li&gt;CI/CD runners&lt;/li&gt;
&lt;li&gt;Container registry&lt;/li&gt;
&lt;li&gt;Package registry&lt;/li&gt;
&lt;li&gt;Built-in SAST, DAST, dependency scanning&lt;/li&gt;
&lt;li&gt;Kubernetes deployment integration&lt;/li&gt;
&lt;li&gt;Feature flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For regulated industries, that bundled security scanning is a real compliance lever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;GitHub Actions&lt;/th&gt;
&lt;th&gt;Jenkins&lt;/th&gt;
&lt;th&gt;GitLab CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Hours–Days&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config language&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;Groovy / Declarative&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Cloud-managed&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;Cloud or self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance overhead&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low–Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;20,000+ Actions&lt;/td&gt;
&lt;td&gt;1,800+ plugins&lt;/td&gt;
&lt;td&gt;Built-in suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-SCM support&lt;/td&gt;
&lt;td&gt;GitHub only&lt;/td&gt;
&lt;td&gt;Any VCS&lt;/td&gt;
&lt;td&gt;GitLab only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security scanning&lt;/td&gt;
&lt;td&gt;Via 3rd-party actions&lt;/td&gt;
&lt;td&gt;Via plugins&lt;/td&gt;
&lt;td&gt;Built-in SAST/DAST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes integration&lt;/td&gt;
&lt;td&gt;Via actions&lt;/td&gt;
&lt;td&gt;Via plugins&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier (private)&lt;/td&gt;
&lt;td&gt;2,000 min/month&lt;/td&gt;
&lt;td&gt;Free (infra costs)&lt;/td&gt;
&lt;td&gt;400 min/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor lock-in&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low–Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Jenkins vs GitHub Actions: Where Each Wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions wins on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero-to-pipeline time (seriously, under an hour)&lt;/li&gt;
&lt;li&gt;Maintenance burden (GitHub manages the infra)&lt;/li&gt;
&lt;li&gt;Developer experience and discoverability&lt;/li&gt;
&lt;li&gt;Marketplace ecosystem&lt;/li&gt;
&lt;li&gt;Cost for standard workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Jenkins wins on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Air-gapped / private network environments&lt;/li&gt;
&lt;li&gt;Multi-SCM support (GitHub + GitLab + Bitbucket + SVN simultaneously)&lt;/li&gt;
&lt;li&gt;Complex pipeline logic that YAML can't cleanly express&lt;/li&gt;
&lt;li&gt;Full infrastructure control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest take: Jenkins teams in 2026 are there because their infra &lt;em&gt;requires&lt;/em&gt; it, or because migrating 200+ existing pipelines isn't worth the effort yet. For new projects with no constraints, GitHub Actions is almost always the faster path.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitLab CI vs Jenkins: Platform vs Flexibility
&lt;/h2&gt;

&lt;p&gt;GitLab CI only works natively with GitLab repos. If your code is elsewhere, it's not a practical choice.&lt;/p&gt;

&lt;p&gt;Where GitLab CI beats Jenkins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integration depth&lt;/strong&gt; — no plugin wrangling, everything's built-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security tooling&lt;/strong&gt; — SAST, DAST, secret detection, dependency scanning without configuring separate tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single auth model&lt;/strong&gt; — one platform, one login, one audit trail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where Jenkins beats GitLab CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;True flexibility&lt;/strong&gt; — Jenkins doesn't care where your code lives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data isolation&lt;/strong&gt; — a Jenkins controller on-prem means your pipeline data, secrets, and logs never leave your network. GitLab Self-Managed achieves similar isolation but you're now maintaining a full GitLab installation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Can You Use GitHub Actions + Jenkins Together?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, and it's a legitimate architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Common hybrid model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub PR opened
      │
      ▼
GitHub Actions  ←── Runs unit tests, lint, builds Docker image
      │                 pushes to registry
      │
      ▼
Jenkins (self-hosted) ←── Handles deploy to internal infrastructure,
                           legacy systems, compliance environments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire them together using GitHub Actions &lt;strong&gt;self-hosted runners&lt;/strong&gt; registered on Jenkins agents. GitHub-triggered workflows execute on your Jenkins infrastructure. Cloud-native triggers, on-prem execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing: Total Cost of Ownership
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;Paid Entry&lt;/th&gt;
&lt;th&gt;Real Cost Driver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;2,000 min/month (private)&lt;/td&gt;
&lt;td&gt;$4/user/month&lt;/td&gt;
&lt;td&gt;Extra minutes @ $0.008/min (Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jenkins&lt;/td&gt;
&lt;td&gt;Free (open source)&lt;/td&gt;
&lt;td&gt;Infra costs only&lt;/td&gt;
&lt;td&gt;Engineer time for maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab CI&lt;/td&gt;
&lt;td&gt;400 min/month&lt;/td&gt;
&lt;td&gt;$29/user/month (Premium)&lt;/td&gt;
&lt;td&gt;$99/user/month (Ultimate) for security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jenkins looks free. It isn't.&lt;/p&gt;

&lt;p&gt;If a DevOps engineer spends 20% of their time on Jenkins maintenance — plugin updates, security patches, agent scaling — that's easily $20–40K/year in burdened labour cost at typical engineering salaries. GitHub Actions or GitLab CI Premium is almost certainly cheaper at any team size over 5 people.&lt;/p&gt;

&lt;p&gt;The breakeven point where Jenkins makes economic sense: you need the flexibility, you have dedicated platform engineering capacity, and cloud-managed runners genuinely can't reach your deployment targets.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Use GitHub Actions when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your code is on GitHub&lt;/li&gt;
&lt;li&gt;You're a startup or small-to-mid team without a dedicated platform team&lt;/li&gt;
&lt;li&gt;You want minimum setup and maximum focus on shipping product&lt;/li&gt;
&lt;li&gt;You're building web apps, APIs, open-source libraries&lt;/li&gt;
&lt;li&gt;You want to scale without hiring a CI/CD specialist&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Use Jenkins when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your environment is air-gapped or on-premises&lt;/li&gt;
&lt;li&gt;You have code across multiple VCS (GitHub + GitLab + Bitbucket + SVN)&lt;/li&gt;
&lt;li&gt;You have complex, multi-stage pipeline logic that YAML can't express&lt;/li&gt;
&lt;li&gt;You have a dedicated DevOps team to own the infra&lt;/li&gt;
&lt;li&gt;You're deeply embedded in an existing Jenkins ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Use GitLab CI when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your code already lives on GitLab&lt;/li&gt;
&lt;li&gt;You want one platform for source control + CI/CD + security scanning&lt;/li&gt;
&lt;li&gt;You're in a regulated industry where built-in SAST/DAST reduces compliance overhead&lt;/li&gt;
&lt;li&gt;You want self-hosted DevOps infra but don't want to depend on GitHub&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform gravity is real&lt;/strong&gt; — pick the CI/CD tool that lives closest to your code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins is free to license, not free to run&lt;/strong&gt; — factor engineering maintenance time into your TCO calculation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions + Jenkins hybrid&lt;/strong&gt; is a legitimate middle ground, not a compromise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab Ultimate's bundled security scanning&lt;/strong&gt; can replace multiple standalone tool subscriptions — do the math before assuming it's expensive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For new projects in 2026 with no infra constraints:&lt;/strong&gt; GitHub Actions is the default. It's not even close&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix builds are underused&lt;/strong&gt; — test across Node 18/20/22, Python 3.11/3.12 in parallel without duplicating jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted runners&lt;/strong&gt; solve the "GitHub Actions can't reach my private network" problem without switching tools entirely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Decision Flowchart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is your code on GitHub?
├── Yes → GitHub Actions (unless air-gapped)
└── No
    ├── Is your code on GitLab?
    │   └── Yes → GitLab CI
    └── Multiple VCS / air-gapped / complex logic?
        └── Yes → Jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right tool is the one that matches where your code lives, how much operational overhead your team can absorb, and what your compliance requirements demand.&lt;/p&gt;

&lt;p&gt;There's no universal winner. But there is a clear default for most teams.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>githubactions</category>
      <category>jenkins</category>
    </item>
    <item>
      <title>"Building a CI/CD Pipeline From Scratch: A Practical Guide for Developers (with GitHub Actions)"</title>
      <dc:creator>Mahendra Singh</dc:creator>
      <pubDate>Mon, 25 May 2026 19:49:06 +0000</pubDate>
      <link>https://forem.com/akoode_tech/building-a-cicd-pipeline-from-scratch-a-practical-guide-for-developers-with-github-actions-f98</link>
      <guid>https://forem.com/akoode_tech/building-a-cicd-pipeline-from-scratch-a-practical-guide-for-developers-with-github-actions-f98</guid>
      <description>&lt;h1&gt;
  
  
  Building a CI/CD Pipeline From Scratch: A Practical Guide for Developers
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Originally inspired by &lt;a href="https://www.akoode.com/blog/how-to-build-a-ci-cd-pipeline" rel="noopener noreferrer"&gt;Akoode's CI/CD pipeline guide&lt;/a&gt; — rewritten here with more depth, code, and less hand-waving.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've seen teams spend hours manually running tests, zipping build artifacts, SSHing into servers, and crossing fingers before every deploy. CI/CD pipelines exist to kill that workflow. This guide skips the theory lecture and gets into how to actually build one.&lt;/p&gt;

&lt;p&gt;We'll use &lt;strong&gt;GitHub Actions&lt;/strong&gt; as the CI/CD platform — it's free for public repos, tightly integrated with GitHub, and requires zero external infrastructure to get started.&lt;/p&gt;




&lt;h2&gt;
  
  
  What CI/CD Actually Does (Plain English)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI (Continuous Integration):&lt;/strong&gt; Every time code is pushed or a PR is opened, automatically run your build and tests. Catch breakage early, not in prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CD (Continuous Delivery/Deployment):&lt;/strong&gt; After CI passes, automatically ship the artifact to staging or production — no human clicking "deploy" required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline is just a sequence of automated steps triggered by a git event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline Architecture
&lt;/h2&gt;

&lt;p&gt;git push / PR open&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│   Trigger   │  ← GitHub webhook fires&lt;br&gt;
└─────┬───────┘&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│    Build    │  ← Install deps, compile, bundle&lt;br&gt;
└─────┬───────┘&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│    Test     │  ← Unit, integration, lint&lt;br&gt;
└─────┬───────┘&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
┌─────────────┐&lt;br&gt;
│   Deploy    │  ← Push to staging/prod&lt;br&gt;
└─────────────┘&lt;br&gt;
Each stage is a &lt;strong&gt;job&lt;/strong&gt;. Jobs run on &lt;strong&gt;runners&lt;/strong&gt; (GitHub-hosted VMs or your own). They can run in parallel or sequentially with dependencies between them.&lt;/p&gt;


&lt;h2&gt;
  
  
  Setting Up Your First Pipeline with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Create this file in your repo:&lt;/p&gt;

&lt;p&gt;.github/&lt;br&gt;
workflows/&lt;br&gt;
ci-cd.yml&lt;/p&gt;
&lt;h3&gt;
  
  
  Minimal CI Pipeline (Node.js Example)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&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;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Set up Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&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;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run linter&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&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;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&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;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. Push this file, and every PR gets auto-tested. No server, no webhook config.&lt;/p&gt;


&lt;h3&gt;
  
  
  Adding CD: Deploy to a Server
&lt;/h3&gt;

&lt;p&gt;After CI passes, deploy to production. Here we'll SSH into a VPS and pull + restart:&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;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-and-test&lt;/span&gt;       &lt;span class="c1"&gt;# only runs if CI passes&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;github.ref == 'refs/heads/main'&lt;/span&gt;   &lt;span class="c1"&gt;# only on main branch&lt;/span&gt;

    &lt;span class="na"&gt;steps&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;Deploy via SSH&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_SSH_KEY }}&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;cd /var/www/myapp&lt;/span&gt;
            &lt;span class="s"&gt;git pull origin main&lt;/span&gt;
            &lt;span class="s"&gt;npm ci --omit=dev&lt;/span&gt;
            &lt;span class="s"&gt;pm2 restart myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your SSH key and server IP in &lt;strong&gt;GitHub Secrets&lt;/strong&gt; (&lt;code&gt;Settings → Secrets and variables → Actions&lt;/code&gt;). Never hardcode credentials in the YAML.&lt;/p&gt;




&lt;h3&gt;
  
  
  Docker-Based Deploy (More Portable)
&lt;/h3&gt;

&lt;p&gt;If you're deploying containers:&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;build-and-push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Log in to Docker Hub&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_TOKEN }}&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;Build and push Docker image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;push&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;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourusername/myapp:${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the commit SHA as the image tag gives you a clean audit trail — every deploy is traceable to a specific commit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Separation
&lt;/h2&gt;

&lt;p&gt;Don't deploy everything to production. Use branch-based environment targeting:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;       &lt;span class="c1"&gt;# → production&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;    &lt;span class="c1"&gt;# → staging env&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feat/**'&lt;/span&gt;  &lt;span class="c1"&gt;# → preview envs (optional)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair with GitHub Environments (&lt;code&gt;Settings → Environments&lt;/code&gt;) to add manual approval gates before production:&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;deploy-prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&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;production&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://myapp.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub will pause and require an approver before proceeding. Useful for regulated teams or high-stakes deploys.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching Dependencies
&lt;/h2&gt;

&lt;p&gt;Don't reinstall &lt;code&gt;node_modules&lt;/code&gt; from scratch on every run. Cache 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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;        &lt;span class="c1"&gt;# ← this line handles caching automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Python:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pip'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone can cut pipeline runtime by 60–70% on most projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matrix Testing: Test Across Multiple Versions
&lt;/h2&gt;

&lt;p&gt;Need to support Node 18 and 20? Don't write two jobs:&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;18&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;22&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node-version }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub runs these in parallel — fast and zero duplication.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secrets Management
&lt;/h2&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store secrets in GitHub Secrets, not in &lt;code&gt;.env&lt;/code&gt; files committed to the repo&lt;/li&gt;
&lt;li&gt;Use environment-scoped secrets for prod vs staging differences&lt;/li&gt;
&lt;li&gt;Rotate secrets regularly (SSH keys, API tokens)&lt;/li&gt;
&lt;li&gt;Never &lt;code&gt;echo&lt;/code&gt; secrets in run steps — they'll be masked in logs, but it's still bad practice
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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;Deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROD_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When to Use CI/CD
&lt;/h2&gt;

&lt;p&gt;✅ Any team with more than one developer&lt;br&gt;&lt;br&gt;
✅ Frequent deploys (more than once a week)&lt;br&gt;&lt;br&gt;
✅ You have a test suite (even a small one)&lt;br&gt;&lt;br&gt;
✅ Multiple environments (dev, staging, prod)&lt;br&gt;&lt;br&gt;
✅ Open source projects where contributors submit PRs  &lt;/p&gt;


&lt;h2&gt;
  
  
  When NOT to Use (or Keep It Simple)
&lt;/h2&gt;

&lt;p&gt;❌ Solo hobby project with no test suite — a basic deploy script is fine&lt;br&gt;&lt;br&gt;
❌ Legacy monolith where builds take 45 minutes — fix the build first&lt;br&gt;&lt;br&gt;
❌ Highly regulated environments where automated prod deploys are prohibited — use CD to staging only, with manual prod promotion  &lt;/p&gt;


&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Not pinning action versions&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="c1"&gt;# Bad — can break silently when the action updates&lt;/span&gt;
&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@main&lt;/span&gt;

&lt;span class="c1"&gt;# Good — locked to a specific version&lt;/span&gt;
&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Running everything on every push&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use path filters to skip unnecessary runs:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package.json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Storing secrets in env files&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Don't commit &lt;code&gt;.env.production&lt;/code&gt; to the repo. Use GitHub Secrets + a secrets manager (HashiCorp Vault, AWS Secrets Manager) for anything sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No rollback plan&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Tag your Docker images with the git SHA. If prod breaks, you can redeploy the previous image in 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Pipeline at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI/CD Pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&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;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Build Docker image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t myapp:${{ github.sha }} .&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;Push to registry&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo ${{ secrets.DOCKER_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin&lt;/span&gt;
          &lt;span class="s"&gt;docker push myapp:${{ github.sha }}&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;Deploy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_SSH_KEY }}&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;docker pull myapp:${{ github.sha }}&lt;/span&gt;
            &lt;span class="s"&gt;docker stop myapp || true&lt;/span&gt;
            &lt;span class="s"&gt;docker run -d --name myapp -p 3000:3000 myapp:${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Practical Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start small: even a single &lt;code&gt;npm test&lt;/code&gt; in CI adds real value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;needs:&lt;/code&gt; keyword is your sequencing primitive — use it&lt;/li&gt;
&lt;li&gt;Branch protection rules + required CI checks = no broken code on main&lt;/li&gt;
&lt;li&gt;Commit SHA tagging on Docker images = instant rollback capability&lt;/li&gt;
&lt;li&gt;Cache dependencies — it's free performance&lt;/li&gt;
&lt;li&gt;Use GitHub Environments for approval gates before prod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't a perfect pipeline on day one. It's getting &lt;em&gt;something&lt;/em&gt; automated, then adding stages as your confidence and test coverage grow.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>github</category>
      <category>devops</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
