DEV Community

Timevolt
Timevolt

Posted on

The Matrix of CI/CD: Neo's Journey with GitHub Actions, GitLab, and Jenkins

The Quest Begins (The "Why")

Look, the reality is that setting up CI/CD used to feel like trying to solve a Rubik's cube blindfolded. I remember a project where every push to main triggered a flurry of manual steps: SSH into a server, pull the latest code, run the test suite by hand, and pray nothing broke. One Friday afternoon, after spending three hours chasing a flaky test that only appeared on the staging server, I thought, “There has to be a better way.” That moment was my “aha!” — the dragon I needed to slay was the endless manual toil that slowed us down and introduced human error.

The Revelation (The Insight)

The treasure I uncovered wasn’t a new tool; it was a mindset shift. CI/CD isn’t just about running scripts on a server; it’s about creating a repeatable, trustworthy pipeline that gives you confidence every time you merge code. When you treat the pipeline as a first‑class citizen — versioned, reviewed, and tested like any other piece of software — you unlock speed and safety simultaneously. The insight was simple: define your build, test, and deploy steps in code, store that definition alongside your app, and let the CI system execute it faithfully every single commit. That’s the power that turns chaos into a reliable conveyor belt.

Wielding the Power (Code & Examples)

Let’s see how this looks in practice with the three most common CI platforms. I’ll show a “before” — a messy, ad‑hoc approach — and then the “after” — a clean, versioned pipeline.

GitHub Actions

Before (the struggle):

A teammate once added a workflow file that just echoed a message and then called a local script with hard‑coded paths.

name: Build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: echo "Building..."
      - run: /home/ubuntu/myapp/build.sh   # ← hard‑coded, not portable
Enter fullscreen mode Exit fullscreen mode

After (the victory):

Now we define everything in the repo, use actions for setup, and keep the steps declarative.

name: CI

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

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test
      - name: Build
        run: npm run build
      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/
Enter fullscreen mode Exit fullscreen mode

Why this is better: every step is explicit, uses official actions, and the artifact is stored for later jobs (like deployment). No more guessing where a script lives.

GitLab CI

Before:

A GitLab‑CI yaml that relied on a shared runner with a bunch of exported environment variables set in the runner’s config, making the file cryptic.

stages:
  - test
  - deploy

test:
  stage: test
  script:
    - $RUNNER_TEST_CMD   # ← defined outside the repo
  only:
    - main

deploy:
  stage: deploy
  script:
    - $RUNNER_DEPLOY_CMD
  only:
    - main
Enter fullscreen mode Exit fullscreen mode

After:

Self‑contained, clear stages, and uses Docker images defined directly.


yaml
stages:
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "20"

test:
  stage: test
  image: node:$NODE_VERSION
  script:
    - npm ci
    - npm test
  only:
    - main

build:
  stage: build
  image: node:$NODE_VERSION
  script:
    - npm ci
Enter fullscreen mode Exit fullscreen mode

Top comments (0)