Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

June 2024

·

2 min read

CI/CD with GitHub Actions: Automate Testing & Deployment

Build a complete CI/CD pipeline with GitHub Actions — lint, test, Docker build, and deploy to AWS ECS automatically on every merge to main.

Introduction

  • CI/CD eliminates manual deploy steps and catches regressions before they reach production.
  • GitHub Actions is built into GitHub — no extra CI server to manage.
  • Goal: a pipeline that lints, tests, builds a Docker image, pushes to ECR, and deploys to ECS on every merge to main.

Core Concepts

  • Workflow: a YAML file in .github/workflows/ that defines automation.
  • Trigger (on): when the workflow runs (push, pull_request, schedule, workflow_dispatch).
  • Job: a group of steps that run on the same runner.
  • Step: a single command or Action.
  • Action: a reusable workflow unit (from the Marketplace or your own).
  • Runner: the VM that executes jobs (ubuntu-latest, macos-latest, or self-hosted).

Anatomy of a Workflow File

name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test

Step 1: Lint and Type Check

- name: Install dependencies
  run: npm ci
 
- name: Lint
  run: npm run lint
 
- name: Type check
  run: npx tsc --noEmit
  • npm ci instead of npm install for reproducible installs (uses package-lock.json).
  • Cache node_modules with actions/setup-node cache: 'npm' to avoid reinstalling on every run.

Step 2: Run Tests

- name: Run tests
  run: npm test -- --coverage
 
- name: Upload coverage
  uses: actions/upload-artifact@v4
  with:
    name: coverage
    path: coverage/

Step 3: Build Docker Image

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3
 
- name: Build Docker image
  uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    tags: my-app:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max
    build-args: |
      NEXT_PUBLIC_SITE_URL=${{ vars.SITE_URL }}
  • cache-from: type=gha — cache Docker layers in GitHub Actions cache; dramatically speeds up subsequent builds.

Step 4: Push to AWS ECR (Main Branch Only)

deploy:
  needs: lint-and-test
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  permissions:
    id-token: write   # required for OIDC
    contents: read
 
  steps:
    - uses: actions/checkout@v4
 
    - name: Configure AWS credentials (OIDC — no access keys)
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789012:role/github-deploy-role
        aws-region: ap-southeast-1
 
    - name: Login to ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2
 
    - name: Build and push to ECR
      uses: docker/build-push-action@v6
      with:
        context: .
        push: true
        tags: ${{ steps.login-ecr.outputs.registry }}/my-app:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

Step 5: Deploy to ECS

    - name: Deploy to ECS
      run: |
        aws ecs update-service \
          --cluster my-cluster \
          --service my-app-service \
          --force-new-deployment \
          --region ap-southeast-1
  • --force-new-deployment triggers a rolling update with the latest task definition.
  • ECS pulls the new image tag by updating the task definition first (use aws ecs register-task-definition for blue/green).

Branch Protection + Required Checks

  • In GitHub Settings → Branches → Add rule for main:
    • Require status checks to pass (select lint-and-test).
    • Require pull request reviews.
    • Block direct pushes to main.
  • This ensures no broken code merges without CI passing.

Secrets and Variables

TypeUse forHow to set
secrets.*Sensitive values (tokens, passwords)Settings → Secrets → Actions
vars.*Non-sensitive config (URLs, region)Settings → Variables → Actions
github.* contextBuilt-in (SHA, branch, actor, repo)Available automatically
  • Never hardcode secrets in workflow files.
  • Use OIDC to authenticate with AWS — eliminates the need for AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY secrets entirely.

Reusable Workflows and Composite Actions

# .github/workflows/reusable-lint.yml
on:
  workflow_call:
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run lint
# Call it from another workflow
jobs:
  run-lint:
    uses: ./.github/workflows/reusable-lint.yml

Monitoring and Notifications

  • GitHub Actions Slack app: post workflow status to a Slack channel.
  • actions/github-script: create a GitHub issue on repeated deploy failures.
  • Job summaries: write markdown to $GITHUB_STEP_SUMMARY for a custom CI report.

Conclusion

  • CI/CD with GitHub Actions removes the "it works on my machine" problem entirely.
  • OIDC + least-privilege IAM roles (see IAM article) mean zero long-lived secrets in GitHub.
  • Start simple (lint + deploy); layer in tests and Docker caching once the basics are solid.