Hands-On CI: Building Your First GitHub Actions Workflow

In our previous post on Continuous Integration in DevOps we laid out the “what” and “why” of CI. Today we are taking you through a deep-dive into the “how,” with step-by-step instructions for setting up a linting pipeline in GitHub Actions. Along the way we’ll explain why teams need CI, how it differs from local tools like pre-commit, and exactly what happens when a linter like Ruff runs on every push. If you haven’t already, you might also find our guide to Why Your Team Needs pre-commit a useful companion.

CI workflows are a powerful tool to improve code quality. Let's cover exactly how they work.

CI workflows are a powerful tool to improve code quality. Let's cover exactly how they work, and how to add them to your team’s repos.

Why you should use CI pipelines

A CI pipeline automates repetitive checks, including linting, tests, and security scans, every time code changes. That means you catch style errors, bugs and misconfigurations before they make it into your shared branches. Developers get fast feedback on pull requests rather than waiting until someone notices a stray typo, or until production monitors go off. That leads to higher code quality, fewer review cycles, and more confidence when merging.

Linting in CI also enforces a single source of truth. Even if each developer has a local setup, machines vary. A pipeline ensures everyone’s code passes the same checks on the same environment. It’s one of the foundations any modern team needs before scaling review processes or adding deployment gates.

Local hooks versus CI

You might already use pre-commit or similar tools in your local workflow. Those let you catch and fix issues before you even commit. They’re fantastic for shifting quality checks left, and reducing friction in code reviews. But they still depend on each developer having the right configuration, and remembering to run them.

A CI pipeline, by contrast, lives in the cloud. It runs on every push or pull request, no matter who made the change or what machine they used. That gives your team a safety net: even if someone forgets to install hooks, the pipeline will catch issues. Pre-commit and CI aren’t mutually exclusive; think of them as complementary. Pre-commit gives rapid, local feedback. CI gives consistent, team-wide enforcement.

How GitHub Actions and Git workflows work

GitHub Actions is a platform for defining automated workflows that run on Git events, such as push, pull_request, release, schedule, and more. Workflows live in your repo under .github/workflows/. Each workflow is a YAML file that declares:

  1. Trigger events (for example, push or pull_request)

  2. Jobs, which run on virtual runners like Ubuntu or Windows

  3. Steps within each job, such as checking out code, installing dependencies, or running scripts

By defining a workflow called ci.yml that listens on every push, you’ll have linting kick off whenever anyone pushes new commits to any branch. You can also restrict it to pull requests only, or specific branches (for example main).

What is a linter? Introducing Ruff

A linter analyzes your source code for style violations, potential bugs, dead code, type errors and more. Ruff is a fast, Python-focused linter written in Rust. It runs in milliseconds even on large codebases and can enforce rules from tools like Flake8, PyLint, isort and more, all in one pass. Using Ruff in CI means you get all of those checks with minimal overhead.

Ruff isn’t the only option (black, flake8 and pylint are popular too), but its speed and broad rule support make it ideal for CI. You can customize Ruff’s behavior via a ruff.toml file, enabling or disabling rules, setting line-length, ignoring paths, and so on.

Creating your first GitHub Action

Below is a complete ci.yml you can drop into .github/workflows/ci.yml in your repo. It runs on every push, checks out your code, sets up Python 3.10, installs Ruff, and runs ruff check . on the entire repo.

name: CI Pipeline

on:
  push:
    branches:
      - '**'

jobs:
  lint:
    name: Lint with Ruff
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Install Ruff
        run: pip install ruff

      - name: Run Ruff
        run: ruff check --output-format=github .

Let's go line-by-line through this ci.yml file to help you understand exactly how the workflow works:

name: CI Pipeline

This sets the display name of your workflow. That’s what you’ll see in the Actions tab. You can pick any name you like, but keeping it descriptive helps your team understand its purpose.

on:

The on section tells GitHub which events trigger your workflow. Here we use:

on:
  push:
    branches:
      - '**'

  • push: The workflow runs on every push event.

  • branches: ['**']: ** is a glob that matches any branch name. That means every time you push to any branch, this workflow runs. You could narrow this to specific branches like main or develop, or add a pull_request trigger if you prefer.

jobs:

Workflows can have multiple jobs in parallel or sequence. Our ci.yml defines a single job called lint:

jobs:
  lint:
    name: Lint with Ruff
    runs-on: ubuntu-latest
    steps:
      ...

  • lint: The job’s identifier. You use this in the UI and in dependencies if you have multiple jobs.

  • name: Lint with Ruff: A human-friendly name for the job, shown in the Actions UI.

  • runs-on: ubuntu-latest: The virtual machine image to run on. ubuntu-latest gives you an up-to-date Ubuntu instance. GitHub also supports windows-latest or macos-latest if you need those environments.

steps:

Steps are executed in order:

  1. Check out code

    - name: Check out code
      uses: actions/checkout@v4
    
    

    This step clones your repository into the runner so subsequent steps have access to your files.

  2. Set up Python

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.10'
    
    

    This action installs the specified Python version. Pinning to a version ensures your runner uses the same interpreter that your project requires.

  3. Install Ruff

    - name: Install Ruff
      run: pip install ruff
    
    

    Installs Ruff from PyPI. You could pin a specific version (for example pip install ruff==0.0.365) or add it to your requirements.txt if you manage dependencies there.

  4. Run Ruff

    - name: Run Ruff
      run: ruff check --output-format=github .
    
    

    Executes Ruff against your entire codebase (.). If Ruff finds any errors or warnings, it exits with a non-zero status, causing the job (and the workflow) to fail. You’ll then see a red❌ on your commit or PR, and detailed output in the logs.

    When you run a GitHub Action and specify —-output-format=github with Ruff, any linting issues found will be displayed directly in the GitHub interface as annotations rather than just appearing in the logs. This makes any needed changes easier to see and understand right in GitHub.

That covers every bit of the ci.yml. With this breakdown you should feel confident customizing names, triggers, runners, or steps to fit your project’s needs.

Seeing it all in action

Imagine you’re a developer ready to add a new feature. You’ve created a feature branch off main and written some code in src/new_feature.py. After committing locally, you push your branch:

git push origin feature/new-awesome

On GitHub, you open a pull request targeting main. Instantly, the CI pipeline springs to life:

  1. Automatic Runner Launch
    In the PR UI under “Checks”, you see “Lint with Ruff” queued and then running. A spinner indicates progress.

  2. Checkout and Setup
    The Actions runner checks out your branch and sets up Python 3.10. This happens in seconds, and you see logs like:

    actions/checkout@v4
    actions/setup-python@v5 with python-version=3.10
    
    
  3. Installing and Running Ruff
    Next, the pipeline installs Ruff. In the logs you’ll notice:

    Collecting ruff
      ...
    Successfully installed ruff-0.x.x
    
    

    Then Ruff checks your code:

    > ruff check src/new_feature.py
    src/new_feature.py:27:5 F821 undefined name 'foo'
    src/new_feature.py:50:100 E501 line too long (120 > 88 characters)
    
    
  4. Immediate Feedback
    The PR page updates with a red ❌ next to “Lint with Ruff”. You click the “Details” link, review the error messages, then switch back to your IDE.

  5. Fix and Re-run
    Back in your local branch you fix the issues:

    # Correct the undefined name and wrap long lines
    git commit -am "Fix undefined name and wrap lines"
    git push

GitHub automatically re-runs the CI job. Now you see a green ✅ and “All checks have passed” in the PR.

6. Merge with Confidence

With a green build, reviewers know your code meets the team’s standards. You merge your pull request into `main`, confident that style and quality remain consistent.

Throughout this process, developers get fast, clear feedback without manual intervention. The CI pipeline enforces rules consistently, prevents style drift, and keeps your `main` branch healthy.

Why CI workflows make your team happier

  • Fast feedback: Developers know right away if they introduce a style error or bug pattern.

  • Consistent enforcement: No one can sneak code past your style guide.

  • Reduced review toil: Reviewers focus on design and logic, not formatting.

  • Clear documentation: Your config files (`ci.yml`, `ruff.toml`) serve as living documentation of your team’s standards.

By combining pre-commit locally with a CI pipeline in GitHub Actions, you shift quality checks left and keep them there. That leads to fewer review cycles, faster merges, and more time writing features.

If you’re curious about taking this further, check out our deep-dive on Microservices and DevOps: A Perfect Match or our guide to Continuous Deployment. Or if you’re ready to get started with CI, you can install the free Zumbro GitHub app to apply DevOps principles to improve your code quality with automations that help your team define, apply, and enforce standards. Get started free here:

Next
Next

DevOps Approaches for SOC 2 Success