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 testStep 1: Lint and Type Check
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npx tsc --noEmitnpm ciinstead ofnpm installfor reproducible installs (usespackage-lock.json).- Cache
node_moduleswithactions/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=maxStep 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-deploymenttriggers 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-definitionfor 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.
- Require status checks to pass (select
- This ensures no broken code merges without CI passing.
Secrets and Variables
| Type | Use for | How to set |
|---|---|---|
secrets.* | Sensitive values (tokens, passwords) | Settings → Secrets → Actions |
vars.* | Non-sensitive config (URLs, region) | Settings → Variables → Actions |
github.* context | Built-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_KEYsecrets 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.ymlMonitoring 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_SUMMARYfor 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.