Tools: GitHub Actions for DevOps: The Complete CI/CD Setup Guide (2026) - Expert Insights

Tools: GitHub Actions for DevOps: The Complete CI/CD Setup Guide (2026) - Expert Insights

Why GitHub Actions?

Basic Pipeline Structure

Key Concepts

Job Dependencies

Caching

Secrets Management

Matrix Builds

Production Best Practices

1. Branch Protection Rules

2. Environment Protection

3. Concurrency Control

4. Timeout Limits

5. Artifact Retention

Docker Build and Push

AWS Deployment Example

Monitoring Your Pipeline

Common Mistakes

Cost Optimization GitHub Actions has become the default CI/CD tool for most engineering teams. Here's how to set up a production-grade pipeline from scratch. Every GitHub Actions workflow lives in .github/workflows/. Here's a production-ready starting point: Use needs: to create a pipeline flow: If tests fail, build and deploy never run. This prevents broken code from reaching production. Always cache dependencies. Without caching, npm ci runs fresh every time (30-60 seconds wasted). Never hardcode credentials. Use GitHub Secrets: Test across multiple versions simultaneously: Prevent multiple deploys running simultaneously: Most production apps deploy containers: Deploy to ECS, Lambda, or S3: What does your CI/CD pipeline look like? Drop a comment with your setup. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - run: -weight: 500;">npm test - run: -weight: 500;">npm run lint build: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - run: -weight: 500;">npm run build - uses: actions/upload-artifact@v4 with: name: build path: dist/ deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: build path: dist/ - name: Deploy to production run: | # Your deploy command here echo "Deploying to production..." name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - run: -weight: 500;">npm test - run: -weight: 500;">npm run lint build: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - run: -weight: 500;">npm run build - uses: actions/upload-artifact@v4 with: name: build path: dist/ deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: build path: dist/ - name: Deploy to production run: | # Your deploy command here echo "Deploying to production..." name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - run: -weight: 500;">npm test - run: -weight: 500;">npm run lint build: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' - run: -weight: 500;">npm ci - run: -weight: 500;">npm run build - uses: actions/upload-artifact@v4 with: name: build path: dist/ deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: build path: dist/ - name: Deploy to production run: | # Your deploy command here echo "Deploying to production..." test → build → deploy test → build → deploy test → build → deploy - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' # This caches node_modules - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' # This caches node_modules - uses: actions/setup-node@v4 with: node-version: 20 cache: '-weight: 500;">npm' # This caches node_modules - name: Deploy to AWS env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: aws s3 sync dist/ s3://my-bucket - name: Deploy to AWS env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: aws s3 sync dist/ s3://my-bucket - name: Deploy to AWS env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: aws s3 sync dist/ s3://my-bucket strategy: matrix: node-version: [18, 20, 22] os: [ubuntu-latest, macos-latest] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} strategy: matrix: node-version: [18, 20, 22] os: [ubuntu-latest, macos-latest] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} strategy: matrix: node-version: [18, 20, 22] os: [ubuntu-latest, macos-latest] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} deploy: environment: production # Requires manual approval runs-on: ubuntu-latest deploy: environment: production # Requires manual approval runs-on: ubuntu-latest deploy: environment: production # Requires manual approval runs-on: ubuntu-latest concurrency: group: deploy-production cancel-in-progress: false concurrency: group: deploy-production cancel-in-progress: false concurrency: group: deploy-production cancel-in-progress: false jobs: test: timeout-minutes: 10 # Kill stuck jobs jobs: test: timeout-minutes: 10 # Kill stuck jobs jobs: test: timeout-minutes: 10 # Kill stuck jobs - uses: actions/upload-artifact@v4 with: name: build path: dist/ retention-days: 7 # Don't store forever - uses: actions/upload-artifact@v4 with: name: build path: dist/ retention-days: 7 # Don't store forever - uses: actions/upload-artifact@v4 with: name: build path: dist/ retention-days: 7 # Don't store forever - name: Build and push Docker image uses: -weight: 500;">docker/build-push-action@v5 with: context: . push: true tags: ghcr.io/myorg/myapp:latest cache-from: type=gha cache-to: type=gha,mode=max - name: Build and push Docker image uses: -weight: 500;">docker/build-push-action@v5 with: context: . push: true tags: ghcr.io/myorg/myapp:latest cache-from: type=gha cache-to: type=gha,mode=max - name: Build and push Docker image uses: -weight: 500;">docker/build-push-action@v5 with: context: . push: true tags: ghcr.io/myorg/myapp:latest cache-from: type=gha cache-to: type=gha,mode=max - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/deploy aws-region: us-east-1 - name: Deploy to S3 + CloudFront run: | aws s3 sync dist/ s3://my-bucket --delete aws cloudfront create-invalidation \ --distribution-id E1234 --paths "/*" - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/deploy aws-region: us-east-1 - name: Deploy to S3 + CloudFront run: | aws s3 sync dist/ s3://my-bucket --delete aws cloudfront create-invalidation \ --distribution-id E1234 --paths "/*" - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/deploy aws-region: us-east-1 - name: Deploy to S3 + CloudFront run: | aws s3 sync dist/ s3://my-bucket --delete aws cloudfront create-invalidation \ --distribution-id E1234 --paths "/*" - Free for public repos, 2,000 minutes/month for private - Native GitHub integration — no third-party OAuth, no webhook setup - Massive marketplace — 20,000+ pre-built actions - Matrix builds — test across multiple OS/language versions simultaneously - Require PR reviews before merging to main - Require -weight: 500;">status checks (CI must pass) - No direct pushes to main - Build time — should be under 5 minutes - Success rate — aim for 95%+ - Queue time — if jobs wait, add self-hosted runners - Flaky tests — identify and fix tests that fail intermittently - Not caching dependencies — adds 30-60s per run - Running everything sequentially — use needs: for parallelism - No timeout — stuck jobs burn through minutes quota - Deploying from PRs — always gate deploys to main branch only - Hardcoded secrets — use GitHub Secrets, never commit credentials - Use ubuntu-latest — Linux runners are cheapest - Cancel redundant runs — use concurrency to -weight: 500;">stop outdated builds - Self-hosted runners — free minutes, you pay for compute - Cache aggressively — saves minutes on every run