Back to Blog
GitHub Actions CI/CD: Complete Pipeline Setup Guide

GitHub Actions CI/CD: Complete Pipeline Setup Guide

December 13, 2024
8 min read
Tushar Agrawal

Build robust CI/CD pipelines with GitHub Actions. Learn workflow syntax, testing automation, Docker builds, deployments to AWS/Vercel, secrets management, and advanced patterns for production systems.

Introduction

GitHub Actions has become the go-to CI/CD solution for modern development teams. At Dr. Dangs Lab, we've built pipelines that automatically test, build, and deploy across multiple environments. This guide covers everything from basics to advanced patterns.

Workflow Basics

Anatomy of a Workflow

.github/workflows/ci.yml

name: CI Pipeline

Triggers

on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 0 *' # Daily at midnight workflow_dispatch: # Manual trigger

Environment variables (workflow-level)

env: NODE_VERSION: '20' PYTHON_VERSION: '3.11'

Jobs

jobs: test: name: Run Tests runs-on: ubuntu-latest

# Job-level env vars env: DATABASE_URL: postgresql://localhost/test

steps: - name: Checkout code uses: actions/checkout@v4

- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm'

- name: Install dependencies run: npm ci

- name: Run tests run: npm test

Trigger Events

on:
  # Push to specific branches
  push:
    branches:
      - main
      - 'release/'
    paths:
      - 'src/'
      - 'package.json'
    paths-ignore:
      - '.md'
      - 'docs/'

# Pull request events pull_request: types: [opened, synchronize, reopened] branches: [main]

# Manual trigger with inputs workflow_dispatch: inputs: environment: description: 'Deployment environment' required: true default: 'staging' type: choice options: - staging - production

# Triggered by other workflows workflow_call: inputs: config-file: required: true type: string secrets: token: required: true

Complete CI Pipeline

Testing and Linting

name: CI

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

jobs: lint: name: Lint Code runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'

- name: Install dependencies run: npm ci

- name: Run ESLint run: npm run lint

- name: Run Prettier check run: npm run format:check

type-check: name: Type Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'

- run: npm ci - run: npm run type-check

test: name: Unit Tests runs-on: ubuntu-latest needs: [lint, type-check] # Run after lint and type-check

services: postgres: image: postgres:15 env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

redis: image: redis:7-alpine ports: - 6379:6379

steps: - uses: actions/checkout@v4

- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'

- run: npm ci

- name: Run tests with coverage run: npm run test:coverage env: DATABASE_URL: postgresql://test:test@localhost:5432/test REDIS_URL: redis://localhost:6379

- name: Upload coverage report uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info fail_ci_if_error: true

e2e: name: E2E Tests runs-on: ubuntu-latest needs: test

steps: - uses: actions/checkout@v4

- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'

- run: npm ci

- name: Install Playwright browsers run: npx playwright install --with-deps

- name: Run E2E tests run: npm run test:e2e

- name: Upload test artifacts uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/ retention-days: 7

Docker Build and Push

name: Build and Push Docker

on: push: branches: [main] tags: ['v*']

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

jobs: build: name: Build Docker Image runs-on: ubuntu-latest

permissions: contents: read packages: write

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

steps: - name: Checkout uses: actions/checkout@v4

- name: Set up QEMU uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3

- 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=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix=

- name: Build and push id: build uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | BUILD_DATE=${{ github.event.head_commit.timestamp }} GIT_SHA=${{ github.sha }}

scan: name: Security Scan runs-on: ubuntu-latest needs: build

steps: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ${{ needs.build.outputs.image-tag }} format: 'sarif' output: 'trivy-results.sarif'

- name: Upload scan results uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif'

Deployment Workflows

Deploy to AWS ECS

name: Deploy to AWS

on: workflow_run: workflows: ["Build and Push Docker"] types: [completed] branches: [main]

jobs: deploy: name: Deploy to ECS runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }}

environment: name: production url: https://api.example.com

steps: - name: Checkout uses: actions/checkout@v4

- name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1

- name: Download task definition run: | aws ecs describe-task-definition \ --task-definition my-app \ --query taskDefinition > task-definition.json

- name: Update task definition id: task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: task-definition.json container-name: api image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

- name: Deploy to ECS uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def.outputs.task-definition }} service: my-app-service cluster: my-cluster wait-for-service-stability: true

Deploy to Vercel

name: Deploy to Vercel

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

jobs: deploy: name: Deploy runs-on: ubuntu-latest

steps: - uses: actions/checkout@v4

- name: Install Vercel CLI run: npm install -g vercel@latest

- name: Pull Vercel Environment run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

- name: Build Project run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy to Vercel id: deploy run: | url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}) echo "url=$url" >> $GITHUB_OUTPUT

- name: Comment on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: '🚀 Preview deployed: ${{ steps.deploy.outputs.url }}' })

Matrix Builds

name: Matrix Build

on: [push, pull_request]

jobs: test: name: Test on ${{ matrix.os }} with Node ${{ matrix.node }} runs-on: ${{ matrix.os }}

strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] node: [18, 20, 22] exclude: - os: windows-latest node: 18 include: - os: ubuntu-latest node: 20 coverage: true

steps: - uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: 'npm'

- run: npm ci - run: npm test

- name: Upload coverage if: matrix.coverage uses: codecov/codecov-action@v3

Reusable Workflows

Creating a Reusable Workflow

.github/workflows/reusable-deploy.yml

name: Reusable Deploy Workflow

on: workflow_call: inputs: environment: required: true type: string image-tag: required: true type: string secrets: AWS_ACCESS_KEY_ID: required: true AWS_SECRET_ACCESS_KEY: required: true

jobs: deploy: name: Deploy to ${{ inputs.environment }} runs-on: ubuntu-latest environment: ${{ inputs.environment }}

steps: - uses: actions/checkout@v4

- name: Configure AWS uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1

- name: Deploy run: | echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}" # Deployment commands here

Using the Reusable Workflow

.github/workflows/main.yml

name: Main Pipeline

on: push: branches: [main]

jobs: build: # ... build job

deploy-staging: needs: build uses: ./.github/workflows/reusable-deploy.yml with: environment: staging image-tag: ${{ needs.build.outputs.image-tag }} secrets: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

deploy-production: needs: [build, deploy-staging] uses: ./.github/workflows/reusable-deploy.yml with: environment: production image-tag: ${{ needs.build.outputs.image-tag }} secrets: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Secrets and Environment Management

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com

steps: # Access secrets - name: Use secrets env: API_KEY: ${{ secrets.API_KEY }} DATABASE_URL: ${{ secrets.DATABASE_URL }} run: | echo "Deploying with configured secrets"

# Mask sensitive output - name: Generate token id: token run: | TOKEN=$(generate-token) echo "::add-mask::$TOKEN" echo "token=$TOKEN" >> $GITHUB_OUTPUT

Advanced Patterns

Conditional Jobs

jobs:
  check-changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            frontend:
              - 'frontend/'
            backend:
              - 'backend/'

build-frontend: needs: check-changes if: needs.check-changes.outputs.frontend == 'true' runs-on: ubuntu-latest steps: - run: echo "Building frontend"

build-backend: needs: check-changes if: needs.check-changes.outputs.backend == 'true' runs-on: ubuntu-latest steps: - run: echo "Building backend"

Caching Dependencies

steps:
  - uses: actions/checkout@v4

# npm cache - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'

# Custom cache - name: Cache Playwright browsers uses: actions/cache@v4 id: playwright-cache with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- name: Install Playwright if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps

Notification on Failure

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # ... build steps

notify: needs: build if: failure() runs-on: ubuntu-latest steps: - name: Notify Slack uses: 8398a7/action-slack@v3 with: status: failure channel: '#deployments' fields: repo,commit,author,action,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Key Takeaways

1. Start simple - Add complexity as needed 2. Use caching - Speed up builds significantly 3. Leverage matrix builds - Test across environments 4. Create reusable workflows - DRY principle 5. Secure secrets - Use environments and OIDC 6. Monitor and alert - Know when things break 7. Optimize for speed - Parallel jobs, caching

Conclusion

GitHub Actions provides a powerful, flexible CI/CD platform integrated directly into your repository. Start with basic workflows, then gradually adopt advanced patterns like reusable workflows and matrix builds. The key is to automate everything that can be automated.

---

Building CI/CD pipelines? Connect on LinkedIn to discuss automation strategies.

Share this article

Related Articles