Înapoi la Blog
CI/CDGitHub ActionsDockerDevOps

Building a Production CI/CD Pipeline with GitHub Actions

V
Vadym
10 iunie 20264 min citire
Building a Production CI/CD Pipeline with GitHub Actions

GitHub Actions has become the default choice for CI/CD in most projects. It's integrated directly into the repository, the free tier is generous, and the ecosystem of reusable actions is huge. But most pipelines I see in the wild are either too simple or too complex. This guide covers building a solid, production-grade pipeline.

The Pipeline We're Building

For a typical containerised application, we want:

  1. On every pull request: lint, test, build (but don't deploy)
  2. On merge to main: build Docker image, push to registry, deploy to staging
  3. On tag/release: deploy to production with manual approval gate

Basic Structure

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

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

Step 1: Lint and Test

Run these on every push and PR. They should be fast (under 3 minutes ideally):

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage
          path: coverage/

The cache: "npm" line is important. Caching node_modules makes subsequent runs significantly faster.

Step 2: Build and Push Docker Image

Only run this when tests pass and we're on main or a tag:

  build-image:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request'
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The cache-from: type=gha enables GitHub Actions cache for Docker layers. This makes builds much faster after the first run.

Step 3: Deploy to Staging

  deploy-staging:
    needs: build-image
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: staging
      url: https://staging.myapp.com

    steps:
      - name: Deploy to staging
        env:
          IMAGE_TAG: ${{ needs.build-image.outputs.image-tag }}
          DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
        run: |
          # Example: update Kubernetes deployment
          kubectl set image deployment/myapp \
            app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} \
            --namespace=staging

Step 4: Production Deployment with Manual Approval

This is where GitHub Environments shine. Set up a production environment in repo settings with required reviewers. The pipeline will pause and wait for approval before proceeding.

  deploy-production:
    needs: build-image
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    environment:
      name: production
      url: https://myapp.com

    steps:
      - name: Deploy to production
        env:
          IMAGE_TAG: ${{ needs.build-image.outputs.image-tag }}
        run: |
          kubectl set image deployment/myapp \
            app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} \
            --namespace=production

Secret Management

Never put secrets directly in workflow files. Use:

  • GitHub Secrets for sensitive values (API keys, deploy keys)
  • GitHub Variables for non-sensitive configuration
  • OIDC for cloud provider authentication instead of long-lived keys

For AWS, use OIDC authentication instead of access keys:

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions-role
          aws-region: eu-west-1

No access keys stored anywhere — the pipeline authenticates via short-lived tokens.

Useful Patterns

Reusable workflows — extract common jobs into .github/workflows/shared-*.yml and call them from other workflows with uses: ./.github/workflows/shared-build.yml.

Concurrency control — cancel in-progress runs when a new commit is pushed:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Dependency review — add the dependency review action to catch vulnerable dependencies in PRs:

      - uses: actions/dependency-review-action@v4

A good CI/CD pipeline gives engineers confidence to ship changes quickly. The goal is fast feedback on problems, not slow processes that block progress.

Ai nevoie de ajutor cu infrastructura?

Să lucrăm împreună

Disponibil pentru proiecte B2B remote la nivel global.

Ia Legătura