GitHub Actions and CI/CD - Git & GitHub Collaboration Master Part 5
Build Powerful CI/CD Pipelines with GitHub Actions
Introduction: Why Do We Need CI/CD?
When working on a project, you need to repeatedly run tests, build, and deploy every time you modify code. This is manageable when developing alone, but becomes quite cumbersome when working as a team. If you've ever said "It works on my machine?", you've probably felt the need for CI/CD.
Today, in Part 5 of our Git & GitHub Collaboration Master series, we'll dive deep into CI/CD using GitHub Actions. We'll start from the basics so even beginners can understand, and together we'll create practical workflows that you can apply directly to your projects.
1. What is CI/CD?
1.1 CI (Continuous Integration)
CI is a development practice where developers frequently and regularly integrate code changes into the main branch. It's not just about merging code - every time you integrate, it automatically builds and runs tests to verify there are no problems.
Let me give you an example. Imagine three developers are working on different features. Without CI, when they try to merge all their separately developed code after a few weeks, massive conflicts could occur. But with CI, code is integrated multiple times a day, even daily, and automatically verified, so problems can be discovered and resolved early.
1.2 CD (Continuous Delivery/Deployment)
CD is used with two meanings. Continuous Delivery means code changes automatically go through testing and are prepared for a deployable state, while Continuous Deployment goes one step further, automatically deploying to production environments.
In summary:
- Continuous Delivery: Code is automatically prepared to be deployable at any time (humans press the deploy button)
- Continuous Deployment: When all tests pass, it's automatically deployed to production
1.3 Real Benefits of CI/CD
The biggest change I felt when actually implementing CI/CD was that "the fear of deployment disappeared." I used to hesitate to deploy on Friday afternoons because if something went wrong, I'd have to work through the weekend. But with automated testing and deployment pipelines in place, I could deploy confidently at any time.
2. Introduction to GitHub Actions
2.1 What is GitHub Actions?
GitHub Actions is a CI/CD platform provided directly by GitHub. Since its official release in 2019, it has grown rapidly and is now one of the most popular CI/CD tools. Its biggest advantage is that it's perfectly integrated with GitHub repositories, so you can use it immediately without setting up external services.
Key features of GitHub Actions:
- GitHub Native: Use directly in repositories without external service integration
- YAML-based Configuration: Easy-to-understand declarative syntax
- Marketplace: Thousands of pre-built actions available for reuse
- Free Usage: Unlimited for public repositories, 2,000 minutes/month free for private
- Multiple Runners: Supports Linux, Windows, and macOS environments
2.2 Comparison with Other CI/CD Tools
There are other CI/CD tools like Jenkins, CircleCI, and Travis CI. Each has its pros and cons, but if you primarily use GitHub, choosing GitHub Actions is usually a good choice. It's easy to configure, requires no additional infrastructure, and integrates naturally with GitHub events (Push, PR, etc.).
3. Understanding Workflow File Structure
3.1 Workflow File Location
GitHub Actions workflow files are located in the .github/workflows/ directory of your repository. Create a YAML file in this directory, and GitHub will automatically recognize and run it.
my-project/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ └── test.yml
├── src/
├── tests/
└── package.json
3.2 Workflow Components
Workflows have the following hierarchical structure:
- Workflow: The entire automated process, defined in one YAML file
- Event: The trigger that starts the workflow
- Job: A collection of steps that run on the same runner
- Step: An individual unit of work, executes shell commands or actions
- Action: A reusable unit of work
4. Mastering Basic Syntax
4.1 name - Workflow Name
Specify the workflow name. This is the name displayed in the GitHub Actions tab, so make it easy to identify.
name: CI Pipeline
4.2 on - Event Triggers
Define when the workflow should run. Let's look at the most commonly used events.
# Push event
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- '!src/**/*.md'
# Pull Request event
on:
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened ]
# Schedule (cron syntax)
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM (UTC)
# Manual trigger
on:
workflow_dispatch:
inputs:
environment:
description: 'Select deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
4.3 jobs - Job Definitions
Define the actual tasks to perform. You can define multiple jobs, and by default they run in parallel.
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
4.4 runs-on - Specifying Runner Environment
Specify the virtual environment where the job will run. You can use GitHub-hosted runners or self-hosted runners.
# GitHub-hosted runners
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
runs-on: windows-latest
runs-on: macos-latest
# Self-hosted runner
runs-on: self-hosted
4.5 uses and run - Executing Steps
Use uses when using pre-built actions, and run when executing shell commands directly.
steps:
# Using an action
- name: Checkout code
uses: actions/checkout@v4
# Running shell command
- name: Run tests
run: npm test
# Multiple line commands
- name: Build and deploy
run: |
npm run build
npm run deploy
5. Commonly Used Actions
5.1 actions/checkout
The most basic action that checks out your repository code to the workflow environment. It's used as the first step in almost every workflow.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch entire history (when tag, branch info needed)
ref: ${{ github.head_ref }} # Checkout specific branch
5.2 actions/setup-node
Sets up the Node.js environment. You can specify version and cache settings together.
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Automatically cache node_modules
registry-url: 'https://registry.npmjs.org' # Needed for npm publishing
5.3 actions/cache
Cache dependencies or build artifacts to reduce workflow execution time.
- name: Restore cache
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
5.4 Other Useful Actions
# Python setup
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
# Java setup
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
# Docker build and push
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: user/app:latest
6. Implementing Test Automation
6.1 Basic Test Workflow
A workflow that automatically runs tests when a PR is created or updated.
name: Test
on:
pull_request:
branches: [ main, develop ]
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint check
run: npm run lint
- name: Type check
run: npm run type-check
- name: Unit tests
run: npm test -- --coverage
- name: Test coverage report
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
6.2 Test Result Reporting
Leaving test results as comments on the PR allows reviewers to quickly check them.
- name: Test result report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Jest Tests
path: junit.xml
reporter: jest-junit
7. Deployment Automation
7.1 GitHub Pages Deployment
A workflow that automatically deploys a static site to GitHub Pages.
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build
run: |
npm ci
npm run build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
7.2 Vercel Deployment
Configuration for automatic deployment to Vercel. You can deploy to separate Preview and Production environments.
name: Deploy to Vercel
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm install -g vercel@latest
- name: Pull Vercel project info
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Production deploy
if: github.ref == 'refs/heads/main'
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
7.3 AWS S3 + CloudFront Deployment
name: Deploy to AWS
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: '20'
cache: 'npm'
- name: Build
run: |
npm ci
npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Upload to S3
run: aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }} --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
8. Secrets Management
8.1 How to Configure Secrets
Sensitive information like API keys and tokens should never be written directly in code. Use GitHub Secrets to manage them securely.
Configuration steps:
- Go to repository Settings
- Select "Secrets and variables" -> "Actions" from left menu
- Click "New repository secret"
- Enter name and value, then save
8.2 Secrets Usage Example
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
echo "API key has been set"
npm run deploy
8.3 Environment-specific Secrets Management
Using the Environments feature, you can manage different secrets for different environments like staging and production.
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: deploy --env staging
env:
API_URL: ${{ secrets.API_URL }} # staging environment API_URL
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
run: deploy --env production
env:
API_URL: ${{ secrets.API_URL }} # production environment API_URL
9. Matrix Builds
9.1 What is Matrix Strategy?
Using matrix builds, you can run tests simultaneously across multiple environments (OS, language versions, etc.). For example, if you want to test on Node.js versions 18, 20, and 22, you don't need to create separate jobs for each - just define it with a matrix.
name: Cross-version Test
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
exclude:
- os: windows-latest
node-version: 18
fail-fast: false # Continue running others even if one fails
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
9.2 Dynamic Matrix
You can also construct a matrix dynamically using JSON.
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: echo "matrix={\"include\":[{\"project\":\"api\"},{\"project\":\"web\"}]}" >> $GITHUB_OUTPUT
build:
needs: setup
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
steps:
- run: echo "Building ${{ matrix.project }}"
10. Practical Workflow Examples
10.1 Complete CI Pipeline
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run type-check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v3
build:
needs: [lint, type-check, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
10.2 Automatic Release Workflow
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Generate release notes
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10.3 PR Auto-labeling
name: PR Labeler
on:
pull_request:
types: [opened, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
10.4 Scheduled Security Checks
name: Security Check
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM (UTC)
workflow_dispatch:
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Dependency security check
run: npm audit --audit-level=high
- name: Check for dependency updates
run: npm outdated || true
- name: Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Conclusion: Boost Development Productivity with CI/CD
In this article, we explored building CI/CD pipelines using GitHub Actions. The configuration files might seem complex at first, but once you set them up, you'll continuously enjoy the benefits of automation.
The important thing is not to try to create a perfect pipeline from the start. Begin with simple test automation and expand gradually. I also started with a workflow that simply ran npm test, then added linting, type checking, deployment automation, and more as needed.
In Part 6, we'll cover contributing to open source projects. We'll explore Fork, PR workflows, contribution guidelines, and more - everything you need to actually contribute to open source projects.