DEV Community

Shanawaz Mohammed
Shanawaz Mohammed

Posted on Fully Autonomous

How to Add Quality Gates to a GitHub Actions Pipeline (Step by Step)

Most CI pipelines run tests. Far fewer pipelines actually stop bad code from reaching production. The difference is a quality gate: a check that must pass before the pipeline is allowed to move to the next stage.

In this tutorial, you'll build a GitHub Actions pipeline with four gates:

  1. Lint: code style and obvious errors
  2. Unit tests + coverage threshold: fail if coverage drops below a minimum
  3. API tests: verify the running service behaves correctly
  4. Manual approval: a human signs off before production

By the end, a failed gate will block deployment automatically.

Prerequisites

  • A GitHub repository with a small Python web service (the same ideas apply to any language)
  • Basic familiarity with YAML
  • pytest and pytest-cov in your requirements.txt

Step 1: Create the workflow file

Create .github/workflows/pipeline.yml:

# .github/workflows/pipeline.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install ruff
      - run: ruff check .
Enter fullscreen mode Exit fullscreen mode

This first job is your cheapest gate. Linting takes seconds, so it should run first and fail fast.

Step 2: Add unit tests with a coverage threshold

Add a second job. The key line is --cov-fail-under=80, which makes pytest exit with an error if coverage drops below 80%.

  unit-tests:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest tests/unit --cov=app --cov-fail-under=80
Enter fullscreen mode Exit fullscreen mode

needs: lint creates the gate: this job only starts if linting passed.

Tip: Don't start with an aggressive threshold on a legacy codebase. Measure your current coverage, set the threshold slightly below it, and raise it over time. A gate that always fails gets disabled.

Step 3: Add API tests against the running service

Unit tests check functions in isolation. API tests check that the service actually works when it runs. Start the app in the background, wait for it to be healthy, then run the tests:

  api-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - name: Start service
        run: |
          uvicorn app.main:app --port 8000 &
          for i in $(seq 1 30); do
            curl -sf http://localhost:8000/health && break
            sleep 1
          done
      - run: pytest tests/api --base-url http://localhost:8000
Enter fullscreen mode Exit fullscreen mode

The health-check loop matters. Without it, tests often start before the server is ready, which is one of the most common causes of flaky pipelines.

Step 4: Require manual approval before production

GitHub environments let you require a reviewer before a job runs. In your repository, go to Settings → Environments → New environment, name it production, and add yourself under Required reviewers.

Then reference it in a deploy job:

  deploy:
    runs-on: ubuntu-latest
    needs: api-tests
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh
Enter fullscreen mode Exit fullscreen mode

Now the pipeline pauses after the API tests pass and waits for an approval in the GitHub UI.

Step 5: Protect the main branch

Gates only work if nobody can bypass them. Under Settings → Branches, add a branch protection rule for main and enable Require status checks to pass before merging. Select lint, unit-tests and api-tests.

Pull requests can no longer be merged while any gate is failing.

What you built

Gate Catches Typical runtime
Lint Style issues, unused imports, syntax errors Seconds
Unit tests + coverage Logic bugs, untested code 1–3 minutes
API tests Integration and contract failures 2–5 minutes
Manual approval Business or timing risk Human decision

Ordering gates from cheapest to most expensive keeps feedback fast: most problems are caught in the first minute, and expensive checks only run on code that is already in reasonable shape.

Next steps

  • Run API tests in parallel with a matrix strategy as the suite grows.
  • Publish test reports as artifacts so failures are easy to debug.
  • Add a security scanning gate (for example, dependency scanning) before deploy.

Top comments (0)