GitHub Actions CI/CD: A Practical Guide

Wednesday, Nov 5, 2025 | 16 minute read | Updated at Wednesday, Nov 5, 2025

GitHub Actions CI/CD: A Practical Guide

Continuous Integration and Continuous Deployment (CI/CD) have become essential practices in modern software development. GitHub Actions, as GitHub’s native automation tool, offers simplicity, generous free tier, and deep integration with the GitHub ecosystem, making it a popular choice for developers and teams.

In this guide, we’ll build a complete CI/CD pipeline from scratch using real production configurations, covering frontend/backend separation, Docker image building, multi-platform binary releases, and automated issue management.

Why GitHub Actions?

Before diving into the implementation, let’s understand the core advantages:

Key Benefits

  • Native Integration - No third-party platforms needed, configure directly in your repository
  • Free Tier - Unlimited for public repos, 2000 minutes/month for private repos
  • Rich Ecosystem - Thousands of reusable Actions in GitHub Marketplace
  • Flexible Triggers - Push, PR, schedule, manual triggers, and more
  • Matrix Builds - Easy multi-version, multi-platform parallel testing
  • GitHub Ecosystem - Native support for Packages, Issues, Releases

Comparison with Other CI/CD Tools

Feature GitHub Actions Jenkins GitLab CI CircleCI
Setup Complexity Low High Medium Medium
Free Tier 2000 min/mo Self-hosted 400 min/mo 6000 min/mo
Marketplace Rich Many plugins Medium Medium
GitHub Integration Native Requires plugins Requires config Requires config
Learning Curve Gentle Steep Moderate Moderate

Project Architecture

We’ll build a CI/CD pipeline for a full-stack application with frontend/backend separation:

project/
├── frontend/          # React/Vue frontend
│   ├── src/
│   ├── package.json
│   └── Dockerfile
├── backend/           # Go/Node backend
│   ├── main.go
│   ├── go.mod
│   └── Dockerfile
└── .github/
    └── workflows/
        ├── frontend-ci.yml      # Frontend CI
        ├── backend-ci.yml       # Backend CI
        ├── backend-cd.yml       # Backend CD
        ├── backend-lint.yml     # Backend linting
        └── release.yml          # Multi-platform release

Part 1: Frontend CI Pipeline

Use Case

A Node.js + npm frontend project that needs to automatically run the following on every commit and PR:

  1. Dependency installation
  2. Linting (ESLint)
  3. Unit tests
  4. Build verification
  5. Auto-create Issue on failure

Complete Configuration

Create .github/workflows/frontend-ci.yml:

name: Frontend CI

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  test:
    name: Test Frontend
    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'
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json

    - name: Install dependencies
      run: |
        cd frontend
        npm ci

    - name: Run lint
      run: |
        cd frontend
        npm run lint

    - name: Run tests
      run: |
        cd frontend
        npm run test:run

    - name: Build
      run: |
        cd frontend
        npm run build

    - name: Create issue on failure
      if: failure() && github.event_name == 'push'
      uses: actions/github-script@v7
      with:
        script: |
          const title = `Frontend CI Failed on ${context.ref.replace('refs/heads/', '')}`;
          const body = `## Frontend CI Failure Report

          **Branch:** ${context.ref.replace('refs/heads/', '')}
          **Commit:** ${context.sha.substring(0, 7)}
          **Workflow Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}
          **Triggered by:** @${context.actor}

          The frontend CI pipeline has failed. Please check the workflow run for details.

          ### Steps to investigate:
          1. Check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for error logs
          2. Review the recent changes in the frontend directory
          3. Reproduce the issue locally

          ---
          *This issue was automatically created by GitHub Actions*`;

          const issues = await github.rest.issues.listForRepo({
            owner: context.repo.owner,
            repo: context.repo.repo,
            state: 'open',
            labels: 'ci-failure,frontend'
          });

          if (issues.data.length === 0) {
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: title,
              body: body,
              labels: ['ci-failure', 'frontend', 'bug']
            });
          } else {
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issues.data[0].number,
              body: `Frontend CI failed again on commit ${context.sha.substring(0, 7)}. [View run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
            });
          }

Configuration Breakdown

1. Trigger Configuration

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
  • push: Triggers on code push to main/master branches
  • pull_request: Triggers when creating or updating a PR
  • Catches issues before merging to protect main branch

2. Dependency Caching

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'
    cache-dependency-path: frontend/package-lock.json

Key Points:

  • cache: 'npm' automatically caches node_modules
  • cache-dependency-path specifies package-lock.json location
  • Reduces dependency installation from minutes to seconds

3. npm ci vs npm install

- name: Install dependencies
  run: |
    cd frontend
    npm ci

Why npm ci instead of npm install?

Feature npm ci npm install
Speed Faster Slower
package-lock Strictly follows May update
node_modules Deletes then installs Incremental update
Use Case CI/CD environments Local development

4. Automated Issue Creation

This is a powerful feature that automatically creates Issues when CI fails:

Core Logic:

  1. Only triggers on push events and failures (failure() && github.event_name == 'push')
  2. Checks if an Issue with the same labels already exists
  3. Creates new Issue if none exists, otherwise comments on existing Issue
  4. Includes detailed error information and debugging steps

Benefits:

  • Immediate problem notification
  • Avoids duplicate Issues
  • Tracks problem history automatically
  • Provides debugging guidance

Part 2: Backend CI/CD Separation

In production environments, we typically separate CI (testing) from CD (deployment) for:

  • Clearer separation of concerns
  • Independent trigger and failure handling
  • More flexible deployment control

Backend CI - Testing Pipeline

Create .github/workflows/backend-ci.yml:

name: Backend CI

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  test:
    name: Test Backend
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: '1.25'

    - name: Install dependencies
      run: |
        cd backend
        go mod download
        go mod tidy

    - name: Run go vet
      run: |
        cd backend
        go vet ./...

    - name: Run tests
      run: |
        cd backend
        go test -v ./...

    - name: Create issue on failure
      if: failure() && github.event_name == 'push'
      uses: actions/github-script@v7
      with:
        script: |
          const title = `Backend CI Failed on ${context.ref.replace('refs/heads/', '')}`;
          const body = `## Backend CI Failure Report

          **Branch:** ${context.ref.replace('refs/heads/', '')}
          **Commit:** ${context.sha.substring(0, 7)}
          **Workflow Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}
          **Triggered by:** @${context.actor}

          The backend CI pipeline has failed. Please check the workflow run for details.

          ### Steps to investigate:
          1. Check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for error logs
          2. Review the recent changes in the backend directory
          3. Reproduce the issue locally with \`go test -v ./...\`

          ---
          *This issue was automatically created by GitHub Actions*`;

          const issues = await github.rest.issues.listForRepo({
            owner: context.repo.owner,
            repo: context.repo.repo,
            state: 'open',
            labels: 'ci-failure,backend'
          });

          if (issues.data.length === 0) {
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: title,
              body: body,
              labels: ['ci-failure', 'backend', 'bug']
            });
          }

Backend CD - Docker Build and Push

Create .github/workflows/backend-cd.yml:

name: Backend CD - Build and Push Docker Image

on:
  workflow_run:
    workflows: [ "Backend CI" ]
    branches: [ main, master ]
    types: [ completed ]
  workflow_dispatch:
    inputs:
      docker_tag:
        description: 'Custom Docker image tag (optional, defaults to branch-commit)'
        required: false
        type: string

permissions:
  contents: read
  packages: write

jobs:
  build-and-push:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set Docker metadata
      id: meta
      run: |
        REPO=${{ github.repository }}
        REPO_LOWER=$(echo $REPO | tr '[:upper:]' '[:lower:]')

        if [ -n "${{ github.event.inputs.docker_tag }}" ]; then
          TAG=${{ github.event.inputs.docker_tag }}
        else
          BRANCH=$(echo ${{ github.ref }} | sed 's|refs/heads/||')
          COMMIT=$(echo ${{ github.sha }} | cut -c1-7)
          TAG="$BRANCH-$COMMIT"
        fi

        echo "registry=ghcr.io" >> $GITHUB_OUTPUT
        echo "image=ghcr.io/$REPO_LOWER/backend" >> $GITHUB_OUTPUT
        echo "tag=$TAG" >> $GITHUB_OUTPUT

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to GHCR
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: ./backend
        file: ./backend/Dockerfile
        push: true
        tags: |
          ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}
          ${{ steps.meta.outputs.image }}:latest
        cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache
        cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache,mode=max
        labels: |
          org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
          org.opencontainers.image.revision=${{ github.sha }}
          org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')

    - name: Print deployment info
      run: |
        echo "✅ Docker image built and pushed successfully!"
        echo ""
        echo "📦 Image Details:"
        echo "   Image: ${{ steps.meta.outputs.image }}"
        echo "   Tag: ${{ steps.meta.outputs.tag }}"
        echo ""
        echo "🚀 Pull the image:"
        echo "   docker pull ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}"

Key CD Configuration Points

1. workflow_run Trigger

on:
  workflow_run:
    workflows: [ "Backend CI" ]
    branches: [ main, master ]
    types: [ completed ]

Benefits:

  • CD only runs after CI succeeds
  • Prevents deploying untested code
  • Clear dependency relationship

Important:

  • Must explicitly check workflow_run.conclusion == 'success'
  • Workflow name must match exactly

2. workflow_dispatch - Manual Trigger

workflow_dispatch:
  inputs:
    docker_tag:
      description: 'Custom Docker image tag'
      required: false
      type: string

Use Cases:

  • Emergency fixes requiring immediate deployment
  • Rolling back to previous versions
  • Testing CD pipeline
  • Rebuilding images

Usage: Navigate to Actions tab in GitHub repo, click the workflow, and select “Run workflow”.

3. GHCR (GitHub Container Registry)

- name: Log in to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

Why Choose GHCR?

Feature GHCR Docker Hub
Integration Native Requires extra config
Private Repos Unlimited free 1 free
Speed Fast Average
Permissions GitHub permissions Separate management
Best For GitHub projects Public images

4. Docker BuildKit Cache

cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache,mode=max

Cache Performance:

  • First build: 5-10 minutes
  • With cache: 30 seconds - 2 minutes
  • Saves 80-90% build time

How it Works:

  1. cache-from: Pull cache layers from registry
  2. cache-to: Push cache after build
  3. mode=max: Cache all intermediate layers

Part 3: Multi-Platform Binary Release

For compiled languages like Go or Rust, we can use GitHub Actions for cross-compilation and automatically release multi-platform binaries.

Complete Release Pipeline

Create .github/workflows/release.yml:

name: Release

on:
  workflow_run:
    workflows: ["Backend CI"]
    types: [ completed ]
    branches: [ main, master ]

jobs:
  release:
    name: Build and Release
    runs-on: ubuntu-latest
    if: github.event.workflow_run.conclusion == 'success'

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: '1.25'

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json

    - name: Generate version
      id: version
      run: |
        VERSION=$(date -u +'%Y.%m.%d.%H%M%S')
        echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
        echo "Generated version: $VERSION"

    - name: Build frontend
      run: |
        cd frontend
        npm ci
        npm run build

    - name: Build backend binaries
      run: |
        cd backend
        mkdir -p ../builds

        # Windows AMD64
        GOOS=windows GOARCH=amd64 go build -o ../builds/backend-windows-amd64.exe .

        # Linux AMD64
        GOOS=linux GOARCH=amd64 go build -o ../builds/backend-linux-amd64 .

        # Linux ARM64
        GOOS=linux GOARCH=arm64 go build -o ../builds/backend-linux-arm64 .

        # macOS Intel
        GOOS=darwin GOARCH=amd64 go build -o ../builds/backend-darwin-amd64 .

        # macOS Apple Silicon
        GOOS=darwin GOARCH=arm64 go build -o ../builds/backend-darwin-arm64 .

    - name: Package frontend build
      run: |
        cd frontend/dist
        tar -czf ../../builds/frontend-dist.tar.gz .

    - name: Create Release
      id: create_release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: v${{ steps.version.outputs.VERSION }}
        release_name: Release v${{ steps.version.outputs.VERSION }}
        body: |
          ## Release v${{ steps.version.outputs.VERSION }}

          Auto-generated release from commit ${{ github.sha }}

          ### Downloads
          - **Windows (AMD64)**: `backend-windows-amd64.exe`
          - **Linux (AMD64)**: `backend-linux-amd64`
          - **Linux (ARM64)**: `backend-linux-arm64`
          - **macOS (Intel)**: `backend-darwin-amd64`
          - **macOS (Apple Silicon)**: `backend-darwin-arm64`
          - **Frontend**: `frontend-dist.tar.gz`

          ### Installation
          1. Download the appropriate binary for your platform
          2. Make it executable (Unix): `chmod +x backend-*`
          3. Run: `./backend-*`
        draft: false
        prerelease: false

    - name: Upload Windows Binary
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ./builds/backend-windows-amd64.exe
        asset_name: backend-windows-amd64.exe
        asset_content_type: application/octet-stream

    - name: Upload Linux AMD64 Binary
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ./builds/backend-linux-amd64
        asset_name: backend-linux-amd64
        asset_content_type: application/octet-stream

    - name: Upload Frontend Build
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ./builds/frontend-dist.tar.gz
        asset_name: frontend-dist.tar.gz
        asset_content_type: application/gzip

Go Cross-Compilation

Go’s cross-compilation is straightforward with environment variables:

GOOS=<target-os> GOARCH=<target-arch> go build

Supported Platform Combinations

GOOS GOARCH Platform Use Case
linux amd64 Linux 64-bit Servers, cloud hosts
linux arm64 Linux ARM64 Raspberry Pi, ARM servers
darwin amd64 macOS Intel Intel Macs
darwin arm64 macOS ARM M1/M2 Macs
windows amd64 Windows 64-bit Windows PCs
windows 386 Windows 32-bit Legacy Windows

Version Generation Strategy

VERSION=$(date -u +'%Y.%m.%d.%H%M%S')

Example Output: 2025.11.04.142530

Alternative Strategies:

  1. Semantic Versioning - v1.2.3
  2. Git Tag - Based on latest git tag
  3. Commit Hash - git rev-parse --short HEAD
  4. Hybrid - v1.2.3-g<commit-hash>

Recommendations:

  • Official releases: use semantic versioning
  • Automated releases: use timestamps
  • Test versions: add -beta suffix

Part 4: Code Quality Checks

Linting should run early in the CI process, ideally separate from testing.

Backend Go Linting

Create .github/workflows/backend-lint.yml:

name: Backend Lint

on:
  push:
    branches: [ main, master ]
    paths:
      - 'backend/**'
      - '.github/workflows/backend-lint.yml'
  pull_request:
    branches: [ main, master ]
    paths:
      - 'backend/**'

jobs:
  lint:
    name: Go Vet Linting
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: '1.24'

    - name: Install dependencies
      run: |
        cd backend
        go mod download
        go mod tidy

    - name: Run go vet
      id: govet
      continue-on-error: true
      run: |
        cd backend
        OUTPUT=$(go vet ./... 2>&1 || true)
        echo "$OUTPUT"
        echo "vet_output<<EOF" >> $GITHUB_OUTPUT
        echo "$OUTPUT" >> $GITHUB_OUTPUT
        echo "EOF" >> $GITHUB_OUTPUT
        if [ ! -z "$OUTPUT" ]; then
          exit 1
        fi

    - name: Create issue on failure
      if: failure() && github.event_name == 'push'
      uses: actions/github-script@v7
      with:
        script: |
          const title = `⚠️ Backend Go Vet Issues on ${context.ref.replace('refs/heads/', '')}`;
          const vetOutput = `${{ steps.govet.outputs.vet_output }}`;
          const body = `## Go Vet Linting Issues

          **Branch:** ${context.ref.replace('refs/heads/', '')}
          **Commit:** ${context.sha.substring(0, 7)}

          ### Go Vet Output
          \`\`\`
          ${vetOutput}
          \`\`\`

          ### Steps to fix:
          1. Run \`go vet ./...\` locally in the backend directory
          2. Fix the issues and commit the changes

          ---
          *This issue was automatically created by GitHub Actions*`;

          const issues = await github.rest.issues.listForRepo({
            owner: context.repo.owner,
            repo: context.repo.repo,
            state: 'open',
            labels: 'linting,backend'
          });

          if (issues.data.length === 0) {
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: title,
              body: body,
              labels: ['linting', 'backend', 'code-quality']
            });
          }

Path Filtering

on:
  push:
    paths:
      - 'backend/**'
      - '.github/workflows/backend-lint.yml'

Benefits:

  • Only triggers when relevant files change
  • Saves CI minutes
  • Improves execution efficiency

Best Practices:

  • Include the workflow file itself to test workflow changes
  • Use paths-ignore to exclude irrelevant files
  • Combine with branches for better control

Best Practices

1. Workflow Organization

.github/workflows/
├── frontend-ci.yml        # Frontend CI
├── frontend-lint.yml      # Frontend linting
├── backend-ci.yml         # Backend CI
├── backend-lint.yml       # Backend linting
├── backend-cd.yml         # Backend CD
└── release.yml            # Release management

Principles:

  • One workflow per clear task
  • Separate CI from CD
  • Independent lint jobs
  • Chain workflows with workflow_run

2. Caching Strategies

npm Dependencies

- uses: actions/setup-node@v4
  with:
    cache: 'npm'
    cache-dependency-path: frontend/package-lock.json

Go Modules

- uses: actions/setup-go@v5
  with:
    cache: true
    cache-dependency-path: backend/go.sum

Docker Image Layers

- uses: docker/build-push-action@v5
  with:
    cache-from: type=registry,ref=myimage:buildcache
    cache-to: type=registry,ref=myimage:buildcache,mode=max

Cache Performance:

Operation Without Cache With Cache Savings
npm install 2-3 min 10-20 sec 85%
go mod download 1-2 min 5-10 sec 90%
Docker build 5-10 min 30 sec-2 min 80%

3. Secret Management

Adding Secrets

  1. Go to repository Settings → Secrets and variables → Actions
  2. Click “New repository secret”
  3. Enter name and value

Using in Workflows

- name: Deploy to production
  env:
    API_KEY: ${{ secrets.API_KEY }}
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
  run: ./deploy.sh

Secret Types:

  • Repository secrets - Available to single repository
  • Organization secrets - Shared across organization repos
  • Environment secrets - Specific to environments (e.g., production)

Important Notes:

  • Secrets are automatically masked in logs
  • Avoid using sensitive secrets in PRs
  • Use Environment protection rules for deployment restrictions

4. Concurrency Control

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

Effect:

  • New commits cancel old workflows on same branch
  • Saves CI time
  • Prevents resource waste

Use Cases:

  • Frequent commits on dev branches
  • Workflows where intermediate states don’t matter
  • PR preview deployments

Not Suitable For:

  • Production deployment workflows
  • Workflows requiring complete execution history
  • Stateful operations

5. Matrix Builds

For testing multiple versions:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20, 22]
        exclude:
          - os: macos-latest
            node-version: 18

    steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm test

Benefits:

  • Automatically generates multiple jobs
  • Parallel execution
  • Covers multiple environments

6. Conditional Execution

- name: Deploy to production
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  run: ./deploy.sh

- name: Comment on PR
  if: github.event_name == 'pull_request'
  run: echo "PR #${{ github.event.pull_request.number }}"

- name: Cleanup on failure
  if: failure()
  run: ./cleanup.sh

Common Conditions:

  • success() - All previous steps succeeded
  • failure() - Any step failed
  • always() - Always execute
  • cancelled() - Workflow was cancelled
  • github.ref == 'refs/heads/main' - Main branch
  • github.event_name == 'push' - Push event

Common Issues and Solutions

1. workflow_run Not Triggering

Problem: CD workflow with workflow_run doesn’t trigger after CI completes

Causes:

  • Workflow file not in default branch
  • Workflow name mismatch
  • Permission issues

Solution:

on:
  workflow_run:
    workflows: [ "Backend CI" ]  # Must match CI name exactly
    types: [ completed ]

2. Insufficient GITHUB_TOKEN Permissions

Problem: Pushing Docker images or creating Releases fails

Solution:

permissions:
  contents: write      # For creating Releases
  packages: write      # For pushing to GHCR
  issues: write        # For creating Issues
  pull-requests: write # For PR comments

3. Cache Not Working

Problem: Dependencies downloaded every time

Causes:

  • Cache key changes
  • Cache expired (7 days)
  • Cache size limit exceeded (10GB)

Solution:

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

4. Slow Docker Builds

Optimization Strategies:

  1. Use BuildKit Cache
cache-from: type=registry,ref=myimage:buildcache
cache-to: type=registry,ref=myimage:buildcache,mode=max
  1. Optimize Dockerfile Layer Order
# Put rarely changing layers first
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Put frequently changing layers last
COPY . .
RUN npm run build
  1. Use Multi-Stage Builds
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

5. Timeout Issues

Default Timeout: 6 hours (per job)

Set Custom Timeout:

jobs:
  build:
    timeout-minutes: 30  # Job level
    steps:
      - name: Long running task
        timeout-minutes: 10  # Step level
        run: ./slow-task.sh

Performance Optimization

Before vs After Optimization

Optimization Before After Improvement
Dependency Install 3 min 15 sec 91%
Docker Build 8 min 1.5 min 81%
Total Execution 15 min 3 min 80%

Optimization Checklist

  • Enable dependency caching
  • Use Docker BuildKit cache
  • Optimize Dockerfile layer order
  • Use path filtering for unnecessary triggers
  • Concurrency control to cancel old runs
  • Matrix builds for parallel testing
  • Minimize dependency installation
  • Pre-build base images

Advanced Topics

1. Reusable Workflows

Create a reusable workflow:

# .github/workflows/reusable-docker-build.yml
name: Reusable Docker Build

on:
  workflow_call:
    inputs:
      image-name:
        required: true
        type: string
      context:
        required: true
        type: string
    outputs:
      image-tag:
        value: ${{ jobs.build.outputs.tag }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tag }}
    steps:
      # Build steps...

Use reusable workflow:

jobs:
  build-backend:
    uses: ./.github/workflows/reusable-docker-build.yml
    with:
      image-name: backend
      context: ./backend

2. Scheduled Tasks

name: Scheduled Tasks

on:
  schedule:
    - cron: '0 0 * * *'      # Daily at midnight
    - cron: '0 */6 * * *'    # Every 6 hours

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Cleanup old artifacts
        run: echo "Cleaning up..."

Cron Expression Format:

┌─────────── minute (0 - 59)
│ ┌───────── hour (0 - 23)
│ │ ┌─────── day (1 - 31)
│ │ │ ┌───── month (1 - 12)
│ │ │ │ ┌─── weekday (0 - 6) (0 is Sunday)
│ │ │ │ │
* * * * *

Common Examples:

  • 0 0 * * * - Daily at midnight
  • 0 */6 * * * - Every 6 hours
  • 0 9 * * 1-5 - Weekdays at 9 AM
  • */15 * * * * - Every 15 minutes

Learning Resources

Official Documentation

Community Resources

Useful Tools

Summary

In this guide, we’ve covered:

  1. Fundamentals

    • GitHub Actions core concepts
    • Workflow file structure
    • Triggers and events
  2. CI/CD Pipelines

    • Frontend CI: testing, building, deployment
    • Backend CI/CD separation architecture
    • Automated Docker image building
  3. Automation

    • Auto-create Issues on failure
    • Multi-platform binary releases
    • GitHub Release management
  4. Best Practices

    • Caching optimization strategies
    • Secret security management
    • Concurrency control
    • Matrix builds
  5. Advanced Techniques

    • Reusable workflows
    • Custom Actions
    • Multi-environment deployment
    • Scheduled tasks

Next Steps

Now that you’ve mastered GitHub Actions fundamentals, you can:

  1. Implement these configurations in your own projects
  2. Adjust and optimize based on your needs
  3. Explore GitHub Marketplace for more tools
  4. Develop custom Actions for special requirements
  5. Follow upcoming articles on Kubernetes deployment, monitoring integration, etc.

© 2025 Zero9

About Me

Hi, there! This is Will, nice to meet you.

Check out my GitHub !