GitHub Actions and CI/CD
Set up continuous integration and continuous deployment workflows directly in your GitHub repositories.
GitHub Actions and CI/CD
GitHub Actions transforms software development automation by building CI/CD directly into your repositories. This tutorial will teach you how to create powerful workflows to automate tests, builds, and deployments.
Introduction to GitHub Actions
What is GitHub Actions?
GitHub Actions is an automation platform that lets you:
- Run workflows on Git events
- Automate tests and validation
- Deploy your applications automatically
- Manage releases and notifications
Key concepts
Workflow: An automated process defined in YAML Job: A set of steps executed on a runner Step: An individual action within a job Action: A reusable component Runner: A virtual machine that executes jobs
Workflow structure
Basic file
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Triggers
# Git events
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
release:
types: [published]
# Scheduled events
on:
schedule:
- cron: '0 2 * * 1' # Every Monday at 2:00 AM
# Manual trigger
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
Essential workflows
Basic CI for Node.js
# .github/workflows/node-ci.yml
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Run build
run: npm run build
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
CI for Python
# .github/workflows/python-ci.yml
name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', 3.11]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Lint with flake8
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127
- name: Test with pytest
run: |
pytest --cov=./ --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Automatic deployment
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install and build
run: |
npm ci
npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Advanced actions
Test matrix
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
include:
- os: ubuntu-latest
node-version: 20
coverage: true
exclude:
- os: windows-latest
node-version: 16
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Run tests
run: npm test
- name: Upload coverage
if: matrix.coverage
uses: codecov/codecov-action@v3
Conditions and expressions
jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
if: contains(github.event.head_commit.message, '[staging]')
run: echo "Deploying to staging"
- name: Deploy to production
if: startsWith(github.ref, 'refs/tags/v')
run: echo "Deploying to production"
- name: Notify on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Artifacts and cache
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install and build
run: |
npm ci
npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy
run: echo "Deploying files from dist/"
Security and secrets
Managing secrets
# Configuring secrets in GitHub
# Settings > Secrets and variables > Actions
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
aws s3 sync ./dist s3://my-bucket
Protected environments
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
run: echo "Deploying to production environment"
GITHUB_TOKEN permissions
name: Secure Workflow
on: [push]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run security scan
uses: github/super-linter@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Custom actions
JavaScript action
// .github/actions/hello-world/index.js
const core = require('@actions/core');
const github = require('@actions/github');
try {
const nameToGreet = core.getInput('who-to-greet');
console.log(`Hello ${nameToGreet}!`);
const time = (new Date()).toTimeString();
core.setOutput("time", time);
const payload = JSON.stringify(github.context.payload, undefined, 2);
console.log(`The event payload: ${payload}`);
} catch (error) {
core.setFailed(error.message);
}
# .github/actions/hello-world/action.yml
name: 'Hello World'
description: 'Greet someone and record the time'
inputs:
who-to-greet:
description: 'Who to greet'
required: true
default: 'World'
outputs:
time:
description: 'The time we greeted you'
runs:
using: 'node20'
main: 'index.js'
Using the action
jobs:
hello_world_job:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Hello world action step
uses: ./.github/actions/hello-world
id: hello
with:
who-to-greet: 'GitHub Actions'
- name: Get the output time
run: echo "The time was ${{ steps.hello.outputs.time }}"
Complex workflows
Multi-environment pipeline
# .github/workflows/multi-env-deploy.yml
name: Multi-Environment Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
deploy-staging:
needs: test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: echo "Deploying to staging"
deploy-production:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: echo "Deploying to production"
notify:
needs: [deploy-staging, deploy-production]
if: always()
runs-on: ubuntu-latest
steps:
- name: Notify team
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Automatic release
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Generate changelog
id: changelog
uses: metcalfc/changelog-generator@v4.0.1
with:
myToken: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/app.zip
asset_name: app.zip
asset_content_type: application/zip
Hands-on exercises
Exercise 1: Basic CI
# .github/workflows/basic-ci.yml
name: Basic CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: |
echo '{"name": "test-app", "scripts": {"test": "echo \"Tests passed!\""}}' > package.json
npm install
- name: Run tests
run: npm test
- name: Success message
run: echo "✅ All tests passed!"
Exercise 2: Conditional deployment
# .github/workflows/conditional-deploy.yml
name: Conditional Deploy
on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
if: github.ref == 'refs/heads/staging'
run: |
echo "🚀 Deploying to staging environment"
echo "Environment: staging"
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: |
echo "🚀 Deploying to production environment"
echo "Environment: production"
- name: Notify deployment
run: |
echo "📧 Sending notification"
echo "Deployment completed for branch: ${{ github.ref_name }}"
Exercise 3: Workflow with artifacts
# .github/workflows/artifacts-demo.yml
name: Artifacts Demo
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create build artifacts
run: |
mkdir -p dist
echo "Built application v1.0.0" > dist/app.txt
echo "Build timestamp: $(date)" > dist/build-info.txt
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: build-artifacts
path: ./downloaded/
- name: Test artifacts
run: |
echo "📦 Testing downloaded artifacts:"
ls -la ./downloaded/
cat ./downloaded/app.txt
cat ./downloaded/build-info.txt
Monitoring and debugging
Logs and debugging
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Debug information
run: |
echo "🔍 Debug Information:"
echo "Runner OS: ${{ runner.os }}"
echo "GitHub Actor: ${{ github.actor }}"
echo "Repository: ${{ github.repository }}"
echo "Branch: ${{ github.ref_name }}"
echo "Commit SHA: ${{ github.sha }}"
- name: Enable debug logging
run: echo "::debug::This is a debug message"
- name: Set environment variable
run: echo "CUSTOM_VAR=hello-world" >> $GITHUB_ENV
- name: Use environment variable
run: echo "Custom variable: $CUSTOM_VAR"
Notifications
jobs:
notify:
runs-on: ubuntu-latest
if: always()
steps:
- name: Slack notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
- name: Email notification
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 465
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: "Build ${{ job.status }}"
body: "Build completed with status: ${{ job.status }}"
to: team@company.com
Best Practices
Workflow Organization
.github/
├── workflows/
│ ├── ci.yml # Tests and validation
│ ├── deploy-staging.yml # Staging deployment
│ ├── deploy-prod.yml # Production deployment
│ ├── release.yml # Release management
│ └── security.yml # Security scans
├── actions/
│ └── custom-action/ # Custom actions
└── ISSUE_TEMPLATE/ # Issue templates
Performance Optimization
jobs:
optimized:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Dependency caching
- name: Cache Node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# Parallelization
- name: Install dependencies
run: npm ci --prefer-offline --no-audit
# Avoid unnecessary steps
- name: Run tests
if: github.event_name == 'pull_request'
run: npm test
Security
# Minimal permissions
permissions:
contents: read
jobs:
secure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Security scan
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
# Secret validation
- name: Check for secrets
uses: trufflesecurity/trufflehog@main
with:
path: ./
Additional Resources
Popular Actions
- actions/checkout: Check out your code
- actions/setup-node: Set up Node.js
- actions/cache: Cache dependencies
- docker/build-push-action: Build and push Docker images
- peaceiris/actions-gh-pages: Deploy to GitHub Pages
Marketplace
Documentation
Conclusion
GitHub Actions turns your repository into a complete DevOps platform. By mastering these concepts, you can automate your entire development pipeline, from continuous integration to production deployment.
Congratulations! You have completed this comprehensive course on Git and GitHub. You now have all the skills you need to manage your projects effectively with Git, collaborate on GitHub, and automate your development workflows.
Estimated reading time: 40 minutes
Level: Advanced
Previous step: Git Submodules and Subtrees
🎉 Course complete! You now master Git and GitHub!