Skip to content
beginner Phase 6 · CI/CD Pipelines

GitHub Actions

Build CI/CD workflows with GitHub Actions using YAML syntax, reusable actions, and matrix builds.

1h
0 problems
Topic Progress 0%

GitHub Actions Workflow Basics

GitHub Actions Workflow Basics

Workflow Structure

# .github/workflows/ci.yml
name: CI Pipeline

# Trigger on events
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

# Environment variables
env:
  NODE_VERSION: '20'
  REGISTRY: ghcr.io

# Jobs run in parallel by default
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Checkout code
      - uses: actions/checkout@v4

      # Setup Node.js
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      # Install dependencies
      - run: npm ci

      # Run tests
      - run: npm test

      # Build
      - run: npm run build

      # Upload artifacts
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

Triggers

on:
  # Push to branches
  push:
    branches: [main]
    tags: ['v*']
    paths: ['src/**', 'package.json']    # Only trigger on changes
    paths-ignore: ['docs/**', '*.md']    # Ignore certain paths

  # Pull requests
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

  # Scheduled (cron)
  schedule:
    - cron: '0 2 * * 1'    # Every Monday at 2 AM UTC

  # Manual dispatch
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy target'
        required: true
        type: choice
        options: [staging, production]

  # Reusable from another workflow
  workflow_call:
    inputs:
      image-tag:
        required: true
        type: string

Conditional Execution

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

  deploy-production:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [deploy-staging]    # Wait for staging to complete

  test:
    runs-on: ubuntu-latest
    steps:
      - name: Skip on docs-only PR
        if: ${{ contains(github.event.head_commit.message, '[skip ci]') }}
        run: echo "Skipping"

Matrix Builds

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
      fail-fast: false    # Don't cancel other jobs on failure
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

Actions and Reusable Workflows

Actions and Reusable Workflows

Using Marketplace Actions

steps:
  # Docker build and push
  - uses: docker/build-push-action@v5
    with:
      context: .
      push: true
      tags: user/app:latest

  # Deploy to AWS
  - 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: us-east-1

  # Deploy to Kubernetes
  - uses: azure/k8s-deploy@v4
    with:
      manifests: k8s/
      images: user/app:${{ github.sha }}

  # Notify Slack
  - uses: slackapi/slack-github-action@v1
    with:
      payload: |
        {"text": "Deploy complete: ${{ github.sha }}"}

Custom Actions

# action.yml — Define a composite action
name: 'Setup and Test'
description: 'Install deps, lint, and test'
inputs:
  node-version:
    description: 'Node.js version'
    default: '20'
runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
    - run: npm ci
      shell: bash
    - run: npm run lint
      shell: bash
    - run: npm test
      shell: bash

# Usage in workflow
steps:
  - uses: actions/checkout@v4
  - uses: ./.github/actions/setup-and-test
    with:
      node-version: '22'

Reusable Workflows

# .github/workflows/reusable-deploy.yml (called by other workflows)
name: Reusable Deploy
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      deploy-key:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - run: echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
# Calling workflow
jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image-tag: ${{ github.sha }}
    secrets:
      deploy-key: ${{ secrets.DEPLOY_KEY }}

Artifacts and Caching

steps:
  # Build and upload artifact
  - run: npm run build
  - uses: actions/upload-artifact@v4
    with:
      name: dist
      path: dist/
      retention-days: 7

  # Download artifact from another job
  - uses: actions/download-artifact@v4
    with:
      name: dist
      path: dist/

  # Cache npm dependencies
  - uses: actions/cache@v4
    with:
      path: ~/.npm
      key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
      restore-keys: ${{ runner.os }}-npm-
``

Secrets, Environments, and Protection

Secrets, Environments, and Protection

Managing Secrets

steps:
  # Repository secrets (Settings → Secrets and variables → Actions)
  - run: echo "Password is ${{ secrets.DB_PASSWORD }}"

  # Environment secrets
  - run: echo "Key is ${{ secrets.API_KEY }}"
    environment: production

  # Never log secrets!
  - run: echo "This is safe: ${{ secrets.MY_SECRET }}"
  - run: echo "WARNING: $MY_SECRET"    # DANGEROUS — logs the value
    env:
      MY_SECRET: ${{ secrets.MY_SECRET }}    # Use env, not inline

Environments with Protection Rules

# .github/workflows/deploy.yml
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging    # No approval needed
    steps:
      - run: echo "Deploying to staging"

  deploy-production:
    runs-on: ubuntu-latest
    environment: production    # Requires approval + wait timer
    steps:
      - run: echo "Deploying to production"
    # Set up in GitHub: Settings → Environments → production
    # - Required reviewers
    # - Wait timer (e.g., 15 minutes)
    # - Branch restrictions

OIDC for Cloud Authentication (No Static Keys)

jobs:
  deploy-aws:
    runs-on: ubuntu-latest
    permissions:
      id-token: write    # Required for OIDC
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions
          aws-region: us-east-1

      - run: aws s3 sync dist/ s3://my-bucket/

Complete CI/CD Pipeline Example

name: Full CI/CD Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-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 lint
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    needs: lint-and-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  docker:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy-staging:
    needs: docker
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
        env:
          KUBECONFIG: ${{ secrets.KUBE_CONFIG }}

  deploy-production:
    needs: deploy-staging
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
        env:
          KUBECONFIG: ${{ secrets.KUBE_CONFIG }}