May 15, 2025
CI isn’t just a nice-to-have—it’s your early warning system against code chaos. Think of it as the oracular gatekeeper of your project: it may come with some setup costs, but those are nothing compared to the cost of bugs slipping through unnoticed. With the right CI strategy, we don’t just prevent our codebase from degrading—we actively guide it toward better quality, cleaner structure, and faster feedback. In this article, we’ll walk through how to configure CI across multiple platforms, with patterns you can reuse like building blocks (and yes, a couple of developer jokes along the way—because we’ve all had CI pipelines that felt like they were laughing at us).
Want to Set Up CI and Boost Code Quality?
Looking to improve your development workflow, catch issues early, and keep your codebase clean and maintainable?
The Common CI Recipe
Let’s start with the shared ingredients — the “base dough,” if you will:
- A custom Docker image (with love, probably from Docker Hub)
- PostgreSQL and Redis services
- Environment variables like RAILS_ENV, SESSION_EXP_MINUTES, and MAILER_SENDER
- Database setup using db:create db:migrate
- RSpec tests , because what’s more satisfying than green dots?
Now let’s slice it platform by platform:
1. GitHub Actions
File: .github/workflows/ci.yml
name: CI
on: [pull_request]
jobs:
test:
container:
image: [IMAGE]:[VERSION]
credentials:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pipeline
ports: ["5432:5432"]
redis:
image: redis
ports: ["6379:6379"]
env:
RAILS_ENV: test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pipeline
SESSION_EXP_MINUTES: 30
UI_HOST_URL: http://react-app
MAILER_SENDER: [email protected]
steps:
- name: Configure hosts and dependencies
run: |
echo "127.0.0.1 postgres" >> /etc/hosts
apt-get update && apt-get install -y [ANY_EXTRA_DEPS]
- name: Setup database
run: |
bundle exec rake db:create db:migrate RAILS_ENV=test
- name: Run tests
run: bundle exec rspec
2. GitLab CI/CD
image:
name: [IMAGE]:[VERSION]
entrypoint: [""]
services:
- name: postgres
alias: postgres
- name: redis
alias: redis
variables:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pipeline
RAILS_ENV: test
SESSION_EXP_MINUTES: 30
UI_HOST_URL: http://react-app
MAILER_SENDER: [email protected]
test:
before_script:
- echo "127.0.0.1 postgres" >> /etc/hosts
- bundle install
script:
- bundle exec rake db:create db:migrate
- bundle exec rspec
Bonus: If .gitlab-ci.yml breaks and you spend 3 hours fixing indentation, congratulations — you’ve unlocked “YAML Warrior” .
3. Jenkins (Declarative)
pipeline {
agent {
docker {
image '[IMAGE]:[VERSION]'
args '-v /etc/hosts:/etc/hosts'
registryUrl 'https://index.docker.io/v1/'
registryCredentialsId 'DOCKER_HUB_CREDS'
}
}
environment {
POSTGRES_USER = 'postgres'
POSTGRES_PASSWORD = 'pipeline'
RAILS_ENV = 'test'
SESSION_EXP_MINUTES = '30'
UI_HOST_URL = 'http://react-app'
MAILER_SENDER = '[email protected]'
}
stages {
stage('Setup') {
steps {
sh 'echo "127.0.0.1 postgres" >> /etc/hosts'
sh 'apt-get update && apt-get install -y [ANY_EXTRA_DEPS]'
}
}
stage('Test') {
steps {
sh 'bundle exec rake db:create db:migrate'
sh 'bundle exec rspec'
}
}
}
}
Jenkins: because you like your CI with a retro web UI and just the right amount of pain .
4. CircleCI
version: 2.1
jobs:
test:
docker:
- image: [IMAGE]:[VERSION]
auth:
username: $DOCKER_HUB_USERNAME
password: $DOCKER_HUB_PASSWORD
- image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pipeline
- image: redis
environment:
RAILS_ENV: test
SESSION_EXP_MINUTES: 30
UI_HOST_URL: http://react-app
MAILER_SENDER: [email protected]
steps:
- run:
name: Configure hosts
command: echo "127.0.0.1 postgres" >> /etc/hosts
- run:
name: Setup database
command: bundle exec rake db:create db:migrate
- run:
name: Run tests
command: bundle exec rspec
CircleCI: for when you want your pipeline to look like a circle, but work like a rocket .
5. Bitbucket Pipelines
image: [IMAGE]:[VERSION]
pipelines:
default:
- step:
name: Run Tests
services:
- postgres
- redis
caches:
- bundler
script:
- echo "127.0.0.1 postgres" | tee -a /etc/hosts
- bundle install --path vendor/bundle
- bundle exec rake db:create db:migrate
- bundle exec rspec
definitions:
services:
postgres:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pipeline
redis:
image: redis
options:
size: 2x # Double resources if needed
Notes:
- Docker image : use your custom image with Ruby, Node, etc.
- Services : Postgres and Redis are easily attachable.
- ENV vars : you can inject RAILS_ENV, SESSION_EXP_MINUTES, etc. via Repository Settings → Repository variables.
- Hosts hack : Bitbucket containers sometimes need /etc/hosts updated just like the others.
Key Patterns Across All Platforms
-
Use custom Docker images with auth
-
Spin up Redis and Postgres as services
-
Standardize ENV variables
-
Use /etc/hosts hack to make your services resolvable
-
Setup database , then run tests
Bonus Tip: Speed Up Builds Like a Pro
Dependency Caching (GitHub Actions Example)
- name: Cache Bundler
uses: actions/cache@v3
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
- name: Cache Yarn
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
“Caching saves time. And so does skipping meetings. But caching won’t get you fired.”
Parallel Testing (RSpec Example)
jobs:
test:
strategy:
matrix:
files: [spec/controllers/*_spec.rb, spec/models/*_spec.rb, ...]
steps:
- run: bundle exec rspec ${{ matrix.files }}
Split tests, conquer time. Why run them one-by-one when your CI runners are just waiting to flex?
And sure, at the end of the process, you might just get an error message in bright red , with a giant glaring at you like it’s judging your life choices. But hey— it’s better to be warned by your CI than yelled at by a frustrated user when the system goes down. Errors caught early are lessons; errors in production are tickets. Lots of tickets.
Final Thoughts
CI doesn’t have to be confusing. By unifying your setup across platforms, you can maintain consistent behavior, reduce surprises, and impress that one developer on your team who always says “Did you lint this?”
So plug in your [IMAGE], add your [VERSION], and go forth — because real devs don’t push straight to main without tests. (Right?… Right??)
Top comments (0)