41 pages ยท 8 sections
Ctrl K
GitHub Portfolio

CI/CD โ€” GitHub Actions

GitHub Actions is a powerful CI/CD platform integrated directly into GitHub. This guide covers reusable workflows, matrix builds, secrets management, and best practices for production pipelines based on real-world experience migrating 50+ microservices from Jenkins and CircleCI.

Why GitHub Actions

After migrating Samsung's microservices fleet through Jenkins, CircleCI, and finally GitHub Actions, the advantages of GitHub Actions for GitHub-hosted repositories became clear. The platform eliminates the need for separate CI infrastructure while providing powerful features:

FeatureGitHub ActionsJenkinsCircleCI
GitHub integrationNative (same platform)Webhook-basedOAuth app
ConfigurationYAML in repoJenkinsfile / GUIYAML in repo
Runner managementHosted + self-hostedMaster + agentsCloud + self-hosted
Marketplace20,000+ actionsPlugins (1,800+)Orbs
Reusable componentsReusable workflows, composite actionsShared librariesOrbs
Pricing modelMinutes-basedFree (infrastructure cost)Minutes-based
OIDC/AWSNativePlugin requiredContext-based
Secrets managementRepository + org + environment levelCredential storeContexts + projects

The killer feature is workflow composition: reusable workflows allow a central platform team to define standard patterns (build, test, security scan, deploy) that 50+ service teams consume without copy-paste. When the platform team updates the reusable workflow, all consuming workflows get the improvement automatically.

Workflow Structure

A GitHub Actions workflow is a YAML file stored in .github/workflows/. The core components are:

ComponentPurposeRequired
nameWorkflow display nameNo
onTrigger events (push, pull_request, schedule, workflow_dispatch)Yes
envEnvironment variables available to all jobsNo
permissionsGITHUB_TOKEN and OIDC permission scopesNo (defaults to write-all)
jobsCollection of jobs that run in the workflowYes
<job_id>Unique identifier for a jobYes
runs-onRunner type (ubuntu-latest, self-hosted, etc.)Yes
stepsSequential tasks within a jobYes
usesReferences an action (from marketplace or repo)No (alternative to run)
runShell command to executeNo (alternative to uses)
needsJob dependencies; creates a DAGNo
ifConditional executionNo
strategy.matrixParallel job execution across combinationsNo
secretsSecrets passed to reusable workflowsConditional

Trigger Events Reference

on:
  push:
    branches: [main, develop]
    tags: ['v*']
    paths: ['src/**', 'Dockerfile']
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options: [dev, staging, prod]
  schedule:
    - cron: '0 2 * * 1'  # Weekly Monday 2 AM UTC
  workflow_call:
    inputs:
      node_version:
        required: true
        type: string
    secrets:
      AWS_ROLE_ARN:
        required: true

Complete Working Example: Build + Test + Deploy

The following is a production-grade workflow used for a containerized microservice. It includes build, test, security scan, artifact push, and deployment with OIDC authentication:

#.github/workflows/pipeline.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  AWS_REGION: us-east-1
  ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com
  IMAGE_NAME: my-service

permissions:
  id-token: write      # Required for OIDC
  contents: read       # Required for checkout
  pull-requests: write # Required for PR comments

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Run unit tests
        run: npm run test:unit -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

      - name: Build application
        run: npm run build

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

  security-scan:
    runs-on: ubuntu-latest
    needs: build-and-test
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Run npm audit
        run: npm audit --audit-level=moderate

  build-and-push-image:
    runs-on: ubuntu-latest
    needs: [build-and-test, security-scan]
    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Set image tag
        id: vars
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
          else
            echo "tag=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
          fi

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }}
            ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Output image digest
        run: echo "Image pushed - ${{ steps.build-push.outputs.digest }}"

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-and-push-image
    if: github.ref == 'refs/heads/main'
    environment:
      name: staging
      url: https://staging-api.example.com
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN_STAGING }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Update kubeconfig
        run: aws eks update-kubeconfig --name staging-cluster --region ${{ env.AWS_REGION }}

      - name: Deploy to Staging
        run: |
          helm upgrade --install ${{ env.IMAGE_NAME }} ./helm-chart \
            --namespace default \
            --set image.tag=sha-$(git rev-parse --short HEAD) \
            --set environment=staging \
            --wait --timeout 300s

  deploy-production:
    runs-on: ubuntu-latest
    needs: build-and-push-image
    if: startsWith(github.ref, 'refs/tags/v')
    environment:
      name: production
      url: https://api.example.com
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN_PRODUCTION }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Update kubeconfig
        run: aws eks update-kubeconfig --name production-cluster --region ${{ env.AWS_REGION }}

      - name: Deploy to Production
        run: |
          TAG=${GITHUB_REF#refs/tags/}
          helm upgrade --install ${{ env.IMAGE_NAME }} ./helm-chart \
            --namespace default \
            --set image.tag=$TAG \
            --set environment=production \
            --wait --timeout 300s

      - name: Verify deployment
        run: |
          kubectl rollout status deployment/${{ env.IMAGE_NAME }} --timeout=120s
          kubectl get pods -l app=${{ env.IMAGE_NAME }}

Reusable Workflows

Reusable workflows (triggered by workflow_call) are the most powerful feature for platform engineering teams. A central repository defines standard patterns; service repositories reference them.

Reusable Build Workflow (Platform Team Repository)

#.github/workflows/reusable-build.yml
name: Reusable Build and Push

on:
  workflow_call:
    inputs:
      image_name:
        required: true
        type: string
      dockerfile:
        required: false
        type: string
        default: './Dockerfile'
      aws_region:
        required: false
        type: string
        default: 'us-east-1'
    secrets:
      AWS_ROLE_ARN:
        required: true
    outputs:
      image_tag:
        description: "The pushed image tag"
        value: ${{ jobs.build.outputs.tag }}
      image_digest:
        description: "The image digest"
        value: ${{ jobs.build.outputs.digest }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    outputs:
      tag: ${{ steps.vars.outputs.tag }}
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ inputs.aws_region }}

      - uses: aws-actions/amazon-ecr-login@v2

      - name: Set image tag
        id: vars
        run: echo "tag=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

      - uses: docker/setup-buildx-action@v3

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ${{ inputs.dockerfile }}
          push: true
          tags: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ inputs.aws_region }}.amazonaws.com/${{ inputs.image_name }}:${{ steps.vars.outputs.tag }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Consuming the Reusable Workflow (Service Repository)

#.github/workflows/service-pipeline.yml
name: Service Pipeline

on:
  push:
    branches: [main]

jobs:
  build:
    uses: my-org/platform-workflows/.github/workflows/reusable-build.yml@main
    with:
      image_name: payment-service
      dockerfile: './Dockerfile'
    secrets:
      AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}

  deploy:
    needs: build
    uses: my-org/platform-workflows/.github/workflows/reusable-deploy.yml@main
    with:
      image_name: payment-service
      image_tag: ${{ needs.build.outputs.image_tag }}
      environment: staging
    secrets:
      AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN_STAGING }}

Matrix Strategy for Multi-Environment Builds

Matrix builds enable parallel execution across multiple combinations of configurations. This is essential for building across multiple environments, Node.js versions, or architectures:

jobs:
  test-matrix:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: [18, 20, 21]
        os: [ubuntu-latest, windows-latest]
        include:
          - node-version: 20
            os: ubuntu-latest
            coverage: true
        exclude:
          - node-version: 18
            os: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test
      - name: Upload coverage
        if: matrix.coverage == true
        uses: codecov/codecov-action@v3

Secrets Management

GitHub Actions provides secrets at multiple scopes. Proper scoping follows the principle of least privilege:

ScopeVisibilityUse CaseManagement
RepositorySingle repositoryService-specific credentialsRepo Settings > Secrets
EnvironmentSingle environment in a repoProduction credentials requiring approvalRepo Settings > Environments
OrganizationAll repos in the orgShared credentials (ECR registry, monitoring)Org Settings > Secrets
CodespacesCodespaces for the repoDevelopment environment secretsRepo Settings > Codespaces
DependabotDependabot runs onlyPrivate registry access for DependabotRepo Settings > Dependabot

OIDC Authentication with AWS (No Long-Term Credentials)

The most secure method for GitHub Actions to authenticate with AWS is OpenID Connect (OIDC). This eliminates the need to store long-lived AWS credentials as GitHub secrets. Instead, GitHub provides a short-lived JWT token that AWS IAM validates:

Step 1: Create the IAM OIDC Provider

# Terraform: GitHub OIDC Provider for AWS
resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4e98bab03faadb97b34396831e3780aea1"]

  tags = {
    Name = "github-actions-oidc"
  }
}

Step 2: Create the IAM Role with Trust Policy

# Terraform: IAM Role for GitHub Actions
resource "aws_iam_role" "github_actions" {
  name = "github-actions-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = [
              "repo:my-org/*:*",
              "repo:my-org/infrastructure:ref:refs/heads/main"
            ]
          }
        }
      }
    ]
  })

  tags = {
    Name = "github-actions-role"
  }
}

resource "aws_iam_role_policy" "github_ecr_push" {
  name = "github-ecr-push"
  role = aws_iam_role.github_actions.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload",
          "ecr:PutImage"
        ]
        Resource = "*"
      }
    ]
  })
}

Step 3: Use in Workflow

permissions:
  id-token: write    # Required for OIDC
  contents: read

steps:
  - uses: actions/checkout@v4
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
      aws-region: us-east-1
  # AWS CLI commands now use temporary credentials from OIDC
Security Warning: Never store AWS access keys in GitHub secrets if OIDC is available. OIDC is strictly more secure: credentials are short-lived (default 1 hour), automatically rotated, and tied to specific repository/branch conditions. If using self-hosted runners on EC2, prefer instance profiles instead.

Self-Hosted Runners

For workloads requiring private network access, larger compute, or specialized hardware:

# Install GitHub Actions runner (Ubuntu)
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf actions-runner-linux-x64-2.311.0.tar.gz
./config.sh --url https://github.com/my-org --token $REGISTRATION_TOKEN
sudo ./svc.sh install
sudo ./svc.sh start

# autoscaling with Kubernetes (Actions Runner Controller)
# Install ARC
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm install arc actions-runner-controller/actions-runner-controller \
  --namespace actions-runner-system \
  --set syncPeriod=1m

# Define RunnerDeployment
kubectl apply -f - <

Composite Actions

For reusable steps within a repository or organization, composite actions bundle multiple steps into a single callable unit:

#.github/actions/setup-and-build/action.yml
name: 'Setup and Build'
description: 'Sets up Node.js, installs dependencies, and builds'
inputs:
  node-version:
    description: 'Node.js version'
    required: true
    default: '20'
  build-command:
    description: 'Build command to run'
    required: true
    default: 'npm run build'
outputs:
  build-dir:
    description: 'Build output directory'
    value: ${{ steps.build.outputs.dir }}
runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    - run: npm ci
      shell: bash
    - id: build
      run: |
        ${{ inputs.build-command }}
        echo "dir=dist" >> $GITHUB_OUTPUT
      shell: bash

# Usage in workflow:
# - uses: ./.github/actions/setup-and-build
#   with:
#     node-version: '20'
#     build-command: 'npm run build:prod'

Caching Strategies

Effective caching dramatically reduces build times. GitHub Actions provides the actions/cache action and built-in caching for setup actions:

# Built-in caching (recommended for package managers)
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # Automatically caches ~/.npm

# Manual caching for Docker layers
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha    # GitHub Actions cache backend
    cache-to: type=gha,mode=max

# Manual caching for Gradle/Maven/others
- uses: actions/cache@v3
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

Concurrency Groups

Prevent multiple deployments from colliding:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || '' }}
  cancel-in-progress: true

Best Practices Summary

PracticeImplementation
Use OIDC for cloud authNever store long-lived cloud credentials in secrets
Pin action versionsUse @v4, not @master; use SHA for critical workflows
Least privilege permissionsExplicitly declare permissions block, avoid write-all
Reusable workflowsCentralize standard patterns to reduce drift
Environment protectionUse environments with required reviewers for production
Artifact managementUse upload-artifact/download-artifact for inter-job transfers
Fail fastUse needs and if conditions to skip unnecessary jobs
Timeout all jobsSet job-level and step-level timeouts to prevent runaway jobs
Matrix fail-fast false for critical testsEnsure all matrix combinations run for complete test coverage