Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected
Tutorial

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.

July 2026 10 min read 1 views 0 hearts

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:

  1. Look at the logs – The CI output shows exactly what went wrong
  2. Check your dependencies – Make sure requirements.txt is up to date
  3. Verify environment variables – CI doesn't have your local .env file
  4. 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.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.