Contents

CI/CD for a small team means automated checks on every change and automated deployment after merging to main. On GitHub Actions it takes a day to set up: tests, linting and dependency checks on every pull request, a build and staging deploy after merge, and production deploys on a button or tag.

Here is what a pipeline consists of, a working example and common mistakes.

What is CI/CD and why does a small team need it?

CI (continuous integration) automatically checks every change: build, tests, linters. CD (continuous delivery or deployment) automatically delivers verified code to servers. For a small team the main benefit is that releases become routine and predictable instead of a manual procedure only one person knows.

What a pipeline gives you:

  • bugs are caught minutes after a change, not by users;
  • the main branch always passes its tests;
  • every deployment is identical, regardless of who runs it;
  • rolling back to the previous version takes minutes;
  • deployment history is kept: what shipped, when and by whom.

Which stages make up a pipeline?

A pipeline consists of check, build and deploy stages, each running only if the previous one passed. Checks run on every pull request; build and deploy run after merge.

Stage What happens When it runs
Checks Linter, formatting, types, tests On every pull request
Security Dependency vulnerabilities, secret scanning On every pull request
Build A versioned Docker image or artefact After merging to main
Staging deploy Automatically to the test environment After the build
Production deploy On a button, tag or automatically after checks As the team decides
Post-deploy check A smoke test of the main flow, monitoring Right after deploying

A laptop next to a mug on a wooden table Photo: Unsplash

What does a pipeline look like on GitHub Actions?

A GitHub Actions pipeline is defined in YAML files in the repository's .github/workflows directory. Below is an example for a PHP project: checks on every pull request and a deploy after merging to main.

name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:18
        env: { POSTGRES_PASSWORD: secret }
        ports: ['5432:5432']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.4' }
      - run: composer install --no-interaction --prefer-dist
      - run: vendor/bin/pint --test
      - run: composer audit
      - run: php artisan test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh staging
        env:
          DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

What matters here

  1. Tests run against a real database in a service container.
  2. Deployment runs only after checks pass and only from the main branch.
  3. Secrets live in repository and environment settings, not in code.
  4. GitHub environments can require approval before a production deploy.

We covered which tests to run in the pipeline in automated testing strategy, and building images in Docker for development and production.

How do you keep the pipeline fast?

The pipeline must be fast — a few minutes at most — or developers start working around it. Speed comes from caching dependencies, running independent steps in parallel and separating fast checks from slow ones.

What helps:

  • cache Composer and npm dependencies between runs;
  • run the linter, tests and security checks in parallel;
  • run slow end-to-end tests on the main branch, not on every commit;
  • cancel outdated runs when a new commit lands on the branch;
  • build Docker images with layer caching.

What are the most common mistakes?

Pipelines are most often broken by flaky tests, secrets in code and production deploys with no quick rollback. Each undermines trust in automation, and the team drifts back to manual steps.

Typical mistakes:

  • flaky tests that "sometimes fail" — people rerun them and stop noticing real failures;
  • keys and passwords in the repository instead of CI secrets;
  • no rollback: a broken release can only be fixed with another release;
  • database migrations incompatible with the previous code version, making rollback impossible;
  • checks can be skipped because the main branch is not protected.

Zero-downtime deployment and safe migrations are a topic of their own, covered in our article on release strategies.

In short

  • CI checks every change; CD automatically delivers verified code to servers.
  • The pipeline: checks and security on pull requests, build and staging deploy after merge, production when the team decides.
  • On GitHub Actions everything is YAML, and secrets live in repository settings.
  • Keep the pipeline fast: caching, parallel steps, slow tests separately.
  • Fix flaky tests immediately, keep secrets in CI and plan for rollback.

FAQ

  • How much does GitHub Actions cost?

    Standard runners are free for public repositories; private ones get a monthly allowance of free minutes, with paid usage beyond it. Check GitHub's pricing page for current terms.

  • Should production deploys be automatic?

    Not necessarily. Small teams often prefer automatic staging deploys and production deploys on a button with approval. Fully automatic production deploys require good tests and monitoring.

  • How does GitHub Actions differ from GitLab CI?

    The principles are the same: YAML-defined stages, runners, secrets and environments. The choice usually follows where your code lives; if the repository is on GitLab, GitLab CI is more convenient.