Getting Started with CI: Your First Automated Pipeline
Learn how to set up your first Continuous Integration pipeline using GitHub Actions. This step-by-step guide walks you through creating a CI workflow for Python projects, from basic test automation to adding linting, caching, and notifications.
Advertisement
You've probably heard the term "Continuous Integration" thrown around in developer circles, and maybe it sounds intimidating. But here's the truth: setting up CI is one of the most rewarding things you can do for your code quality. I remember the first time I saw a build fail automatically because of a typo in a variable name — it saved me hours of debugging later.
Let me walk you through setting up your first CI pipeline, step by step. No jargon overload, just practical steps that work.
What Exactly Is Continuous Integration?
Think of CI as your personal code quality assistant. Every time you push code to your repository, CI tools automatically run tests, check for errors, and make sure everything still works. It's like having a second pair of eyes that never gets tired.
At PythonSkillset, we've seen teams go from "deploy and pray" to confident releases after setting up CI. The concept is simple: integrate code changes frequently, and verify each integration automatically.
Choosing Your CI Tool
You don't need to overthink this. For beginners, I recommend starting with one of these:
- GitHub Actions – If your code lives on GitHub, this is the easiest choice. It's built right in.
- GitLab CI/CD – Great if you're using GitLab. Also very straightforward.
- CircleCI – A bit more powerful, but still beginner-friendly.
For this guide, I'll use GitHub Actions because it's what most PythonSkillset readers start with. The concepts transfer easily to other tools.
Your First CI Pipeline: Step by Step
Let's build something real. Imagine you have a Python project with tests. Here's how to set up CI that runs those tests automatically.
Step 1: Create the workflow file
In your repository, create a folder called .github/workflows/. Inside, create a file named ci.yml. This file tells GitHub what to do when you push code.
Here's a minimal example that works for most Python projects:
name: Python CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest
This file tells GitHub: "Every time someone pushes to main or opens a pull request, run these steps."
Breaking Down the Workflow
Let me explain what each part does, because understanding is better than copying blindly.
The trigger section (on:) defines when the workflow runs. I've set it to trigger on pushes to main and on pull requests. You can change this to any branch you like.
The job section defines what happens. We're using ubuntu-latest because it's free and works well for Python. The steps are straightforward:
1. Check out your code
2. Set up Python 3.11
3. Install your dependencies
4. Run your tests
Adding More Useful Steps
Once you have the basic pipeline working, you can add more checks. Here's what I recommend for a Python project:
- name: Lint with flake8
run: |
pip install flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Check formatting with black
run: |
pip install black
black --check .
These steps catch common mistakes and enforce consistent code style. At PythonSkillset, we've found that adding linting early prevents countless headaches later.
Real Example: A Django Project
Let me show you a complete example from a real project I worked on. This Django app had tests, linting, and security checks:
name: Django CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run migrations
run: |
python manage.py migrate
env:
DATABASE_URL: postgres://test_user:test_pass@localhost:5432/test_db
- name: Run tests
run: |
pytest --cov=.
env:
DATABASE_URL: postgres://test_user:test_pass@localhost:5432/test_db
Notice how we set up a PostgreSQL service container? That's a common pattern for Django projects. The CI tool spins up a temporary database, runs your migrations, then executes tests against it.
Common Pitfalls and How to Avoid Them
When I first started with CI, I made every mistake in the book. Here are the ones you should skip:
1. Forgetting environment variables Your local machine has secrets and configs that CI doesn't. Always use environment variables or GitHub Secrets for things like API keys and database passwords.
2. Ignoring the cache Without caching, every CI run downloads all dependencies from scratch. This wastes time. Add caching like this:
- name: Cache pip packages
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
3. Testing only on one Python version Your users might use different Python versions. Test on multiple versions to catch compatibility issues early:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
Adding Notifications
Once your CI is running, you'll want to know when things break. GitHub Actions can send you email notifications by default, but I prefer Slack integration for real-time updates.
Here's a simple Slack notification step:
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": "Build failed for ${{ github.repository }} on ${{ github.ref }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
What Happens When a Build Fails
This is where CI really shines. When a build fails, you get immediate feedback. The workflow run page shows exactly which step failed and why. You can see the console output, the error messages, and even download logs.
At PythonSkillset, we've seen teams catch bugs within minutes of pushing code, instead of days later in production. That's the power of CI.
Going Beyond Basic Testing
Once you have the basics working, consider adding these checks:
- Security scanning – Tools like Bandit can find common security issues in your Python code
- Code coverage – See which parts of your code aren't tested
- Dependency updates – Dependabot can automatically create PRs when your dependencies need updating
Here's a security scan step you can add:
- name: Security scan
run: |
pip install bandit
bandit -r . -f json -o bandit-report.json
A Note on Cost
Most CI tools offer free tiers for public repositories. GitHub Actions gives you 2,000 free minutes per month for private repos, and unlimited for public ones. That's plenty for personal projects and small teams.
When Things Go Wrong
Your first CI run will probably fail. That's normal. Here's what to check:
- Look at the logs – The CI output shows exactly what went wrong
- Check your dependencies – Make sure
requirements.txtis up to date - Verify environment variables – CI doesn't have your local
.envfile - Test locally first – Run the same commands on your machine to isolate issues
Making CI Part of Your Workflow
The real magic happens when CI becomes automatic. Set up branch protection rules so that pull requests can't be merged unless CI passes. This ensures every commit to main is tested.
At PythonSkillset, we've seen teams reduce production bugs by 60% just by enforcing this rule. It's that powerful.
Next Steps
Once you have basic CI running, explore these additions:
- Deploy automatically after tests pass
- Run performance benchmarks on every commit
- Generate documentation from your code
The beauty of CI is that it scales with your project. Start simple, then add more checks as you need them.
Remember: the goal isn't to have the most complex pipeline. It's to catch problems early and give you confidence in your code. Start with tests, add linting, and grow from there.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.