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:

  1. Go to repository Settings
  2. Select "Secrets and variables" -> "Actions" from left menu
  3. Click "New repository secret"
  4. 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.