DEV Community

Timevolt
Timevolt

Posted on

CI/CD Pipelines That Actually Work: My Jedi‑Level Quest with GitHub Actions, GitLab CI & Jenkins

The Quest Begins (The “Why”)

Honestly, I used to feel like a low‑level stormtrooper trying to hit a moving target with a blaster that kept jamming. Every time I pushed code, I’d pray the build would pass, only to get a cryptic “failed” email an hour later. I’d spend the morning debugging flaky tests, the afternoon untangling environment drift, and by sunset I was wondering if I’d ever see a green checkmark again.

It was one of those rainy Tuesday mornings when I stared at a Jenkinsfile that looked like ancient Sumerian script—over‑engineered, full of shared libraries nobody understood, and a stage that sometimes decided to skip itself because a plugin decided to take a coffee break. I felt like Neo in The Matrix before he took the red pill: I sensed something was off, but I couldn’t see the code.

That’s when I decided: enough! I was going to slay the dragon of flaky CI/CD once and for all. My weapons? GitHub Actions for quick feature branches, GitLab CI for our monorepo, and (yes) Jenkins for the legacy bits we couldn’t migrate yet. The goal? A pipeline that actually works—fast, reliable, and so easy to read that even a new intern could glance at it and nod in approval.

The Revelation (The Insight)

The big “aha!” moment came when I stopped treating pipelines as a monolithic beast and started seeing them as a series of small, composable spells—each one doing a single thing well, like the Force being broken into light and dark sides but still usable together.

Three principles changed everything:

  1. Declare, don’t script. Instead of burying logic in bash snippets, I let the platform’s YAML handle the flow.
  2. Cache aggressively, but invalidate wisely. Dependencies are the biggest time‑sink; a smart cache cut our build times from 12 minutes to under 4.
  3. Fail fast, fail loud. Early linting and unit tests give immediate feedback; if they fail, the pipeline stops before wasting time on integration tests.

Applying these to each system felt like discovering the lightsaber hilt after years of fighting with a blunt stick. Suddenly, the pipeline wasn’t a chore—it was a training montage worthy of Rocky (cue the soundtrack).

Wielding the Power (Code & Examples)

Below are the “spellbooks” I now use. I’ll show a before (the painful version) and an after (the victorious version) for each platform. Feel free to copy‑paste, tweak, and watch your own builds level up.

1. GitHub Actions – The Quick‑Draw Blaster

Before: A monolithic job that checked out code, installed npm, ran lint, tests, build, and deploy—all in one step. If the lint failed, we still waited for the test suite to finish (because we didn’t use continue-on-error correctly).

# .github/workflows/old-ci.yml  (the painful version)
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint   # if this fails, we still keep going
      - run: npm test
      - run: npm run build
      - run: npm run deploy   # only if we manually added a secret check
Enter fullscreen mode Exit fullscreen mode

After: Split into separate jobs, use needs for dependency, cache node_modules, and fail fast.

# .github/workflows/ci.yml  (the victorious version)
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - run: npm run deploy   # only runs if build succeeded
Enter fullscreen mode Exit fullscreen mode

Why it rocks:

  • Each job is a single responsibility (lint → test → build/deploy).
  • The cache action cuts npm ci from ~30 seconds to ~5 seconds on subsequent runs.
  • If lint fails, the test job never starts—saving minutes and CI minutes (and money).

2. GitLab CI – The Reliable Lightsaber

Before: A single giant script with inline bash, hard‑coded Docker image tags, and no caching. Every pipeline pulled the full node:20 image and rebuilt node_modules from scratch.

# .gitlab-ci.yml (the painful version)
stages:
  - test
  - build
  - deploy

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm run lint
    - npm test

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
    - npm run deploy
Enter fullscreen mode Exit fullscreen mode

After: Separate stages, use GitLab’s built‑in caching, and pin a specific image digest for reproducibility.

# .gitlab-ci.yml (the victorious version)
stages:
  - lint
  - test
  - build
  - deploy

variables:
  NODE_IMAGE: "node:20-alpine3.18@sha256:9f8c6e2b5a0d9f4c3e1b7a6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6"

.cache: &cache
  paths:
    - node_modules/
  key: "${CI_COMMIT_REF_SLUG}-${CI_JOB_NAME}-${CI_PROJECT_DIR}"
  policy: pull-push

lint:
  stage: lint
  image: $NODE_IMAGE
  <<: *cache
  script:
    - npm ci
    - npm run lint

test:
  stage: test
  image: $NODE_IMAGE
  needs: ["lint"]
  <<: *cache
  script:
    - npm ci
    - npm test

build:
  stage: build
  image: $NODE_IMAGE
  needs: ["test"]
  <<: *cache
  script:
    - npm ci
    - npm run build
    - npm run deploy
Enter fullscreen mode Exit fullscreen mode

Why it rocks:

  • The cache anchor (&cache) reuse keeps node_modules between jobs, slashing build time.
  • Pinning the image by digest guarantees the same runtime across pipelines—no more “works on my machine” surprises.
  • Using needs makes the dependency graph explicit; GitLab can run lint and test in parallel if you ever add more jobs.

3. Jenkins – The Legacy X‑Wing (still flies)

Before: A Jenkinsfile that used a shared library to wrap every step, hid the actual sh commands, and had a try/catch that swallowed errors, making debugging a nightmare.

// Jenkinsfile (the painful version)
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                script {
                    try {
                        checkout scm
                        sh 'npm ci'
                        sh 'npm run lint'
                        sh 'npm test'
                        sh 'npm run build'
                        sh 'npm run deploy'
                    } catch (Exception e) {
                        echo "Something went wrong: ${e}"
                        currentBuild.result = 'FAILED'
                        throw e
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

After: Declarative pipeline with explicit stages, optional timeout, and a post section that always publishes test results—no more hidden failures.

// Jenkinsfile (the victorious version)
pipeline {
    agent any
    options {
        timeout(time: 20, unit: 'MINUTES')
        timestamps()
    }
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Lint') {
            steps {
                sh 'npm ci'
                sh 'npm run lint'
            }
        }
        stage('Test') {
            steps {
                sh 'npm ci'
                sh 'npm test'
                junit '**/test-results/**/*.xml'   // publish JUnit reports
            }
        }
        stage('Build') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
                sh 'npm run deploy'
            }
        }
    }
    post {
        always {
            cleanWs()          // keep the workspace tidy
        }
        success {
            echo '🚀 Pipeline succeeded!'
        }
        failure {
            echo '💥 Pipeline failed. Check logs above.'
            mail to: 'dev-team@example.com',
                 subject: "Failed Build: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                 body: "Check Jenkins: ${env.BUILD_URL}"
        }]
    }
}
Enter fullscreen mode Exit fullscreen mode

Why it rocks:

  • Each stage is visible at a glance—no more guessing what a library call does.
  • timeout prevents a runaway job from hogging an executor for hours.
  • JUnit publishing gives instant test feedback in the UI, and the post block guarantees cleanup and notifications.

Why This New Power Matters

With these patterns in place, I’ve seen our mean time to recovery (MTTR) drop from hours to under ten minutes. Deployments feel like pulling the lever on a slot machine and hearing the jackpot chime—every time.

  • Speed: Caching and parallel stages cut CI minutes by 60‑80%.
  • Reliability: Fail‑fast jobs stop the pipeline before wasted work.
  • Clarity: New hires can look at a YAML/Jenkinsfile and understand the flow in seconds—no tribal knowledge required.
  • Confidence: Knowing the pipeline will actually pass (or fail fast) lets us ship more often, experiment faster, and sleep better at night.

It’s like upgrading from a rusty speeder bike to a Millennium Falcon—you still need to know how to fly, but the ship now wants to go fast.

Your Turn – The Challenge

I dare you to pick one of your own flaky pipelines and apply just one of these principles today:

  1. Add a cache for your language’s dependency manager (npm, pip, Maven, etc.).
  2. Split a monolithic job into two—lint first, then test.
  3. Pin your Docker image by digest instead of a floating tag.

When you see that green checkmark appear after a change that used to be a gamble, come back and tell me how it felt. Was it like blowing up the Death Star? Like finally using the Force to lift an X‑Wing out of the swamp?

May your builds be fast, your tests be reliable, and your deployments be… glorious. 🚀


Happy hacking, and may the CI/CD be with you!

Top comments (0)