CI/CD with GitHub Actions: Complete Guide 2025
GitHub Actions guide: CI/CD workflows, matrix builds, secrets, caching and deploy. Best practices 2025 and ready examples.
Introduction to GitHub Actions
GitHub Actions automates your software development workflows directly in your repository. From continuous integration and deployment to code review automation and scheduled tasks, Actions provides a flexible platform for building robust DevOps pipelines.
This guide covers everything from basic workflow syntax to advanced patterns for production CI/CD.
Core Concepts
Workflows, Jobs, and Steps
- Workflow: An automated process defined in a YAML file, triggered by events
- Job: A set of steps that run on the same runner
- Step: Individual task that runs commands or actions
- Action: Reusable unit of code (from marketplace or custom)
- Runner: Server that executes your workflows
Workflow Structure
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
Triggers (Events)
Common Triggers
on:
# Push to specific branches
push:
branches: [main, develop]
paths:
- 'src/**'
- '!src/**/*.md' # Ignore markdown files
# Pull requests
pull_request:
types: [opened, synchronize, reopened]
# Scheduled (cron)
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
# Manual trigger
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
# On release
release:
types: [published]
# Workflow completion
workflow_run:
workflows: [Build]
types: [completed]
Path and Branch Filters
on:
push:
branches:
- main
- 'release/**'
- '!release/old-*' # Exclude pattern
paths:
- 'apps/api/**'
- 'packages/shared/**'
paths-ignore:
- '**/*.md'
- 'docs/**'
Jobs and Runners
Runner Types
jobs:
linux:
runs-on: ubuntu-latest # or ubuntu-22.04
macos:
runs-on: macos-latest # or macos-13
windows:
runs-on: windows-latest # or windows-2022
self-hosted:
runs-on: [self-hosted, linux, x64]
Job Dependencies
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
test:
needs: build # Wait for build to complete
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [build, test] # Wait for both
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Matrix Builds
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: macos-latest
node: 18
include:
- os: ubuntu-latest
node: 22
experimental: true
fail-fast: false # Continue other matrix jobs if one fails
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
Steps and Actions
Using Marketplace Actions
steps:
# Checkout repository
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for versioning
# Setup Node.js
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Setup Python
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# Cache dependencies
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Running Commands
steps:
- name: Install dependencies
run: npm ci
- name: Multi-line command
run: |
echo "Building..."
npm run build
echo "Done!"
- name: With working directory
run: npm test
working-directory: ./apps/web
- name: With environment variables
run: npm run deploy
env:
API_KEY: ${{ secrets.API_KEY }}
NODE_ENV: production
- name: Continue on error
run: npm run optional-task
continue-on-error: true
Environment Variables and Secrets
Setting Variables
# Workflow level
env:
NODE_ENV: production
CI: true
jobs:
build:
# Job level
env:
DATABASE_URL: postgres://localhost/test
steps:
- name: Build
# Step level
env:
API_KEY: ${{ secrets.API_KEY }}
run: npm run build
Using Secrets
# Access repository secrets
- name: Deploy
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: ./deploy.sh
# GitHub token (auto-generated)
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create v1.0.0
Dynamic Variables
steps:
- name: Set output
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Use output
run: echo "Short SHA is ${{ steps.vars.outputs.sha_short }}"
- name: Set environment variable
run: echo "VERSION=1.2.3" >> $GITHUB_ENV
- name: Use env var
run: echo "Version is $VERSION"
Conditions and Expressions
jobs:
deploy:
# Run only on main branch
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to production
if: github.event_name == 'push'
run: ./deploy-prod.sh
- name: Deploy to staging
if: github.event_name == 'pull_request'
run: ./deploy-staging.sh
# Always run (even if previous steps failed)
- name: Cleanup
if: always()
run: ./cleanup.sh
# Only on failure
- name: Notify on failure
if: failure()
run: ./send-alert.sh
# Complex conditions
- name: Conditional step
if: |
github.event_name == 'push' &&
contains(github.event.head_commit.message, '[deploy]')
run: ./deploy.sh
Artifacts and Caching
Upload/Download Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: ./deploy.sh
Dependency Caching
steps:
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
# Or use built-in caching with setup-node
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
Docker and Container Actions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
Container Jobs
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20-alpine
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: npm test
env:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/test
Reusable Workflows
Creating Reusable Workflow
# .github/workflows/deploy.yml
name: Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
deploy-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.deploy-key }}
Calling Reusable Workflow
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
deploy-staging:
uses: ./.github/workflows/deploy.yml
with:
environment: staging
secrets:
deploy-key: ${{ secrets.STAGING_KEY }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/deploy.yml
with:
environment: production
secrets:
deploy-key: ${{ secrets.PRODUCTION_KEY }}
Security Best Practices
- Never hardcode secrets in workflows
- Use environment protection rules for production
- Pin action versions to specific SHA (not just tag)
- Review third-party actions before using
- Use CODEOWNERS to protect workflow files
- Enable required reviews for workflow changes
# Pin to specific commit SHA for security
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Debugging Workflows
steps:
# Enable debug logging
- name: Debug info
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
# Dump context for debugging
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"
For validating workflow YAML syntax, use our JSON/YAML tools and Diff Checker to compare workflow versions.
Complete CI/CD Example
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm test
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage
path: coverage/
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Conclusion
GitHub Actions provides a powerful, integrated CI/CD platform that scales from simple automation to complex deployment pipelines. By mastering workflows, reusable components, and security practices, you can automate your entire software delivery process.
Key takeaways:
- Start simple and add complexity as needed
- Use caching to speed up workflows
- Create reusable workflows for common patterns
- Protect secrets and use environment approvals
- Pin action versions for security
For more developer resources, explore our free online tools. For official documentation, see GitHub Actions docs and the Actions Marketplace.