Building with GitHub Actions: CI/CD Workflows for Modern Web Projects | SoniNow Blog

Limited TimeLearn More

github actionsci/cdautomationworkflowsdevops

Building with GitHub Actions: CI/CD Workflows for Modern Web Projects

Published

2026-06-23

Read Time

4 mins

Building with GitHub Actions: CI/CD Workflows for Modern Web Projects

GitHub Actions has become the default CI/CD platform for a reason: tight repository integration, a rich ecosystem of community actions, and no additional infrastructure to manage. A well-crafted workflow automates linting, testing, building, and deploying with minimal configuration overhead.

Structuring Your Workflow Files

Start with the right directory structure. Each workflow file should have a clear purpose:

.github/workflows/
├── ci.yml              # Lint, test, build on every push
├── deploy-staging.yml  # Deploy to staging on main
├── deploy-production.yml # Manual or tag-triggered deploy
├── security-scan.yml   # Weekly dependency scanning
└── cleanup.yml         # Daily artifact/cache cleanup

Separate workflows keep logic focused and make changes less risky. A CI fix doesn't affect deployment logic, and vice versa.

Matrix Builds for Cross-Version Testing

Matrix strategies test against multiple versions or configurations in parallel:

name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]

    steps:
      - uses: actions/checkout@v4

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

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

      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage --shard=${{ matrix.shard || 1 }}/${{ matrix.shard-total || 1 }}
      - run: npm run build

The cache action with the hash of package-lock.json as part of the key ensures npm caches invalidate only when dependencies change. The restore-keys fallback uses the most recent cache even if dependencies changed partially.

Environment Secrets and Deployment Workflows

GitHub Environments support protected deployments with required reviewers and secret isolation:

name: Deploy Production

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci
      - run: 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: us-east-1

      - name: Deploy to S3 and invalidate CloudFront
        run: |
          aws s3 sync dist/ s3://app-prod-bucket/ --delete
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_DIST_ID }} \
            --paths "/*"

      - name: Notify deployment
        uses: slackapi/slack-github-action@v2
        with:
          webhook: ${{ secrets.SLACK_DEPLOY_WEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "Production deployed v${{ github.ref_name }}"
            }

The production environment requires manual approval by a designated reviewer before the deploy job starts. Separate environment secrets ensure production credentials are never accessible from CI runs triggered by pull requests.

Custom Actions for Reusable Logic

When a workflow step gets complex or needs reuse across repositories, extract it into a custom action:

# .github/actions/deploy-to-k8s/action.yml
name: 'Deploy to Kubernetes'
description: 'Build, push Docker image, and deploy to Kubernetes cluster'
inputs:
  image-name:
    description: 'Docker image name'
    required: true
  kubeconfig:
    description: 'Kubeconfig contents'
    required: true
  namespace:
    description: 'Kubernetes namespace'
    required: false
    default: 'default'

runs:
  using: 'composite'
  steps:
    - name: Build and push Docker image
      run: |
        docker build -t ${{ inputs.image-name }}:${{ github.sha }} .
        docker push ${{ inputs.image-name }}:${{ github.sha }}
      shell: bash

    - name: Deploy to Kubernetes
      run: |
        echo "${{ inputs.kubeconfig }}" | base64 -d > /tmp/kubeconfig
        kubectl --kubeconfig=/tmp/kubeconfig \
          -n ${{ inputs.namespace }} \
          set image deployment/app \
          app=${{ inputs.image-name }}:${{ github.sha }}
      shell: bash

Use composite actions (rather than Docker-based actions) for simpler maintenance—they run directly on the runner without building a container.

Caching Strategies Beyond Dependencies

Dependency caching is table stakes. Extend caching to build artifacts and Docker layers:

- name: Cache Next.js build
  uses: actions/cache@v4
  with:
    path: |
      .next/cache
      public/sw.js
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-
- name: Cache Docker layers
  uses: docker/setup-buildx-action@v3
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

GitHub Actions cache is limited to 10 GB per repository. Use Docker's inline cache (mode=max) to store layer cache within the registry instead.

Concurrency and Cancel-in-Progress

Prevent wasted runner minutes when pushing multiple commits in quick succession:

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

This cancels any in-progress workflow run on the same branch when a new push happens. The group ensures refs are isolated—a main branch CI run doesn't cancel a feature branch run.

Build Your CI/CD with SoniNow

GitHub Actions brings robust CI/CD to any GitHub-hosted project with minimal setup overhead. At SoniNow, we help teams design and optimize CI/CD pipelines that catch issues early and deploy with confidence.