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
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/
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
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
Top comments (0)