Interviews / DevOps & Cloud Infrastructure / Workflows are consuming too many minutes and running slowly. Optimize for speed and cost.
A critical deployment workflow is failing intermittently. Debug and fix the issue.
100 repositories duplicate the same CI workflow. Design a reusable workflow architecture.
Workflows are consuming too many minutes and running slowly. Optimize for speed and cost.
Workflows use long-lived credentials that could be leaked. Implement secure authentication with OIDC.
GitHub-hosted runners don't meet our requirements. Configure self-hosted runners at scale.
We need to test across multiple OS, Node versions, and configurations. Implement efficient matrix builds.
A workflow is vulnerable to script injection attacks. Identify and fix the security issues.
Every workflow run downloads the same dependencies. Implement an effective caching strategy.
Multiple workflows share the same setup steps. Create a composite action for reuse.
Releases are manual and error-prone. Automate with semantic versioning and changelogs.
Design a deployment workflow with environment approvals, staging, and production rollbacks.
Our monorepo builds everything on every change. Implement efficient path-based workflows.
Questions
Workflows are consuming too many minutes and running slowly. Optimize for speed and cost.
The Scenario
Your organization’s GitHub Actions usage is out of control:
Monthly GitHub Actions Report:
- Total minutes used: 125,000 (limit: 50,000)
- Overage cost: $1,200
- Average workflow duration: 18 minutes
- Cache hit rate: 15%
- Jobs run: 8,500
Top offenders:
1. Full CI on every commit to all branches
2. Matrix testing 25 combinations for every PR
3. No caching - downloading 800MB of node_modules each run
The Challenge
Optimize workflows to reduce execution time by 70% and stay within the free tier, while maintaining code quality and test coverage.
Wrong Approach
A junior engineer might just skip tests, reduce matrix combinations without analysis, or throw money at the problem by upgrading plans. These approaches reduce quality, miss real issues, or waste budget.
Right Approach
A senior engineer analyzes workflow patterns, implements intelligent caching, uses path filters to skip unnecessary runs, parallelizes effectively, and uses concurrency controls to cancel redundant jobs.
Step 1: Implement Aggressive Caching
name: Optimized CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Cache npm dependencies
- name: Cache npm dependencies
uses: actions/cache@v4
id: npm-cache
with:
path: |
~/.npm
node_modules
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
# Only install if cache missed
- name: Install dependencies
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci
# Cache build outputs
- name: Cache build
uses: actions/cache@v4
with:
path: |
.next/cache
dist
key: build-${{ runner.os }}-${{ hashFiles('src/**', 'package-lock.json') }}
- name: Build
run: npm run build
# Cache Playwright browsers
- name: Cache Playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
# Cache Docker layers
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
cache-from: type=gha
cache-to: type=gha,mode=maxStep 2: Use Path Filters to Skip Unnecessary Runs
name: Smart CI
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- '.gitignore'
- 'LICENSE'
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
backend: ${{ steps.changes.outputs.backend }}
infrastructure: ${{ steps.changes.outputs.infrastructure }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'frontend/**'
- 'package.json'
backend:
- 'backend/**'
- 'requirements.txt'
infrastructure:
- 'terraform/**'
- 'k8s/**'
frontend-tests:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
working-directory: frontend
backend-tests:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt && pytest
working-directory: backend
# Always run if nothing specific changed (safety net)
full-ci:
needs: changes
if: needs.changes.outputs.frontend != 'true' && needs.changes.outputs.backend != 'true'
runs-on: ubuntu-latest
steps:
- run: echo "No specific changes detected, running minimal checks"Step 3: Implement Concurrency Controls
name: CI with Concurrency
on:
push:
branches: [main, develop]
pull_request:
# Cancel in-progress runs for the same branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm testStep 4: Optimize Matrix Builds
name: Smart Matrix
on:
push:
branches: [main]
pull_request:
jobs:
# Quick tests on PR - limited matrix
test-pr:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
strategy:
matrix:
node: [18] # Only latest LTS for PRs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
# Full matrix only on main branch
test-full:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
fail-fast: false # Don't cancel other jobs if one fails
matrix:
node: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]
exclude:
# Skip expensive combinations
- os: macos-latest
node: 16
- os: windows-latest
node: 16
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm testStep 5: Parallelize Test Execution
name: Parallel Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Setup
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- run: npm ci
- name: Run tests (shard ${{ matrix.shard }}/4)
run: |
npm run test -- --shard=${{ matrix.shard }}/4
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.shard }}
path: coverage/
merge-coverage:
needs: test
runs-on: ubuntu-latest
steps:
- name: Download all coverage
uses: actions/download-artifact@v4
with:
pattern: coverage-*
merge-multiple: true
- name: Merge and upload
run: |
# Merge coverage reports
npx nyc merge coverage merged-coverage.json
npx nyc report --reporter=text --reporter=lcovStep 6: Use Larger Runners for CPU-Intensive Tasks
name: Optimized Build
on:
push:
branches: [main]
jobs:
# Quick jobs on standard runners
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run lint
# CPU-intensive on larger runner (4x faster, 2x cost = 2x savings)
build:
runs-on: ubuntu-latest-4-cores # or self-hosted
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- run: npm ci
# Parallel build using all cores
- run: npm run build -- --parallel
env:
NODE_OPTIONS: "--max-old-space-size=8192"Step 7: Complete Optimized Workflow
name: Optimized CI/CD
on:
push:
branches: [main, develop]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
paths-ignore:
- '**.md'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
NODE_VERSION: 18
CACHE_VERSION: v1 # Increment to invalidate all caches
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- uses: actions/checkout@v4
- id: cache-key
run: |
echo "key=${{ env.CACHE_VERSION }}-${{ hashFiles('**/package-lock.json') }}" >> $GITHUB_OUTPUT
install:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: modules-${{ needs.setup.outputs.cache-key }}
- if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
lint-and-typecheck:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: modules-${{ needs.setup.outputs.cache-key }}
- run: npm run lint & npm run typecheck & wait
test:
needs: install
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2]
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: modules-${{ needs.setup.outputs.cache-key }}
- run: npm test -- --shard=${{ matrix.shard }}/2
build:
needs: [lint-and-typecheck, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: modules-${{ needs.setup.outputs.cache-key }}
- uses: actions/cache@v4
with:
path: .next/cache
key: nextjs-${{ hashFiles('src/**') }}
- run: npm run build Optimization Results
| Metric | Before | After | Improvement |
|---|---|---|---|
| Avg duration | 18 min | 5 min | 72% faster |
| Monthly minutes | 125,000 | 35,000 | 72% reduction |
| Cache hit rate | 15% | 85% | 5.6x improvement |
| Monthly cost | $1,200 | $0 | Within free tier |
| Queue time | 5 min | 30 sec | 90% reduction |
Practice Question
What is the most impactful optimization for reducing GitHub Actions minutes for a typical Node.js project?