QUANHEX.
DevOps

GitHub Actions CI/CD Patterns That Actually Work

Practical patterns for GitHub Actions workflows — caching, secret management, conditional deployments, and the mistakes that slow down pipelines.

· 6 min read

GitHub Actions is ubiquitous now — it’s the default CI/CD choice for most new projects and an increasing number of migrations away from Jenkins and CircleCI. After writing a lot of workflows, we’ve accumulated a set of patterns that make pipelines reliable and fast.

Cache Your Dependencies

The single highest-impact optimization: cache node_modules between runs. Without caching, every pipeline run reinstalls your entire dependency tree.

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

The cache: 'npm' option caches based on package-lock.json hash. If the lockfile doesn’t change, the cache hits. On a project with 500+ packages, this is the difference between a 2-minute pipeline and a 45-second one.

Separate Jobs for Separate Concerns

Don’t put everything in one job. A monolithic workflow is hard to debug and can’t parallelize.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]

  test:
    runs-on: ubuntu-latest
    steps: [...]

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps: [...]

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps: [...]

lint and test run in parallel. build waits for both to pass. deploy only runs on main. Each job’s logs are isolated, failures are obvious, and the critical path is clear.

Conditional Deployments

The if conditional on jobs is powerful and underused:

# Deploy only on main branch pushes
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

# Skip deploy on draft PRs
if: github.event.pull_request.draft == false

# Only run expensive job when relevant files change
- uses: dorny/paths-filter@v3
  id: changes
  with:
    filters: |
      src:
        - 'src/**'
        - 'package.json'

Secret Rotation Strategy

Treat pipeline secrets with the same discipline as production secrets. Rotation policy:

  • AWS credentials: rotate every 90 days, use IAM roles with minimal permissions
  • Third-party API tokens: rotate on personnel changes
  • Never put secrets in workflow files — always use ${{ secrets.SECRET_NAME }}

For complex deployments with many secrets, consider AWS Secrets Manager and retrieve them within the workflow rather than storing them all in GitHub.

The Reusable Workflow Pattern

For organizations with multiple repos that share deployment patterns, reusable workflows avoid copy-paste drift:

# .github/workflows/deploy-s3.yml (in a shared repo)
on:
  workflow_call:
    inputs:
      bucket:
        required: true
        type: string
    secrets:
      AWS_ACCESS_KEY_ID:
        required: true

Other repos call it with uses: org/shared-workflows/.github/workflows/deploy-s3.yml@main. One update propagates everywhere.

Pipeline Observability

Add a final step that posts deployment status to Slack or records a deployment marker. Silent pipelines hide problems. The team should know within minutes if a deploy failed and exactly which step broke.