Automating Your CI/CD Pipeline with GitHub Actions
Tech Setup
Reviewed July 27, 2026

Introduction
Continuous Integration and Continuous Deployment (CI/CD) is the practice of automatically building, testing, and deploying code changes. Every time a developer pushes to the main branch, the pipeline runs the test suite, builds the application, and deploys it to production — without manual intervention.
GitHub Actions is GitHub's built-in CI/CD platform. It is tightly integrated with your repository, supports any language or framework, and has a massive marketplace of pre-built actions. In this guide, you will build a complete CI/CD pipeline from scratch.
How GitHub Actions Works
GitHub Actions uses YAML workflow files stored in .github/workflows/. Each workflow is triggered by an event (push, pull request, schedule, etc.) and runs a series of jobs. Each job runs on a virtual machine (runner) and executes a sequence of steps.
Event → Workflow → Job → Steps → Actions
Step 1: Create Your First Workflow
Create the directory structure:
mkdir -p .github/workflows
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
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 linting
run: npm run lint
- name: Run tests
run: npm test
- name: Build
run: npm run build
Push this file and watch the workflow run in the Actions tab of your repository.
Step 2: Add Caching
Caching dependencies speeds up your pipeline significantly. GitHub Actions can cache npm, pip, or any package manager:
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Or use the built-in caching in setup-node:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
Step 3: Add Docker Build and Push
If your application uses Docker, build and push the image on every merge to main:
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/myapp:latest,${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}
Store your Docker credentials as repository secrets in Settings → Secrets → Actions.
Step 4: Deploy to Production
Add a deployment job that runs after the build:
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
Step 5: Add Notifications
Get notified when the pipeline fails:
notify:
needs: [test, build-and-push, deploy]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Send Slack notification
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "CI/CD failed for ${{ github.repository }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Step 6: Environment-Specific Deployments
Use branches to control which environment gets deployed:
name: Deploy
on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- name: Deploy
run: |
echo "Deploying to ${{ github.ref_name }}"
# Deploy based on branch
Configure environments in Settings → Environments with required reviewers for production.
Advanced Patterns
Reusable Workflows
Extract common steps into reusable workflows:
# .github/workflows/reusable-test.yml
name: Reusable Test
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
- run: npm test
Use it in other workflows:
jobs:
test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: '20'
Matrix Builds
Test across multiple versions:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
Conclusion
GitHub Actions makes CI/CD accessible to every developer. With a single YAML file, you can automate testing, building, deployment, and notifications. Start with the basic CI workflow, then incrementally add Docker builds, deployment, and environment-specific configurations. The time you invest in setting up CI/CD will pay for itself within the first week.