Skip to main content

GitHub Actions (CI/CD)

 GitHub Actions (CI/CD): Your Complete Guide to Automation on GitHub

Everything you need to know about building, testing, and deploying code automatically—from basic concepts to production pipelines—explained in plain, practical language.

📅 Published: Feb 2026
⏱️ Estimated Reading Time: 22 minutes
🏷️ Tags: GitHub Actions, CI/CD, Automation, DevOps, Workflows, Deployment


 Introduction: What is CI/CD?

The Automation Revolution

Imagine you're launching a rocket. You wouldn't build it, strap yourself in, and then manually check every bolt, calculate the trajectory by hand, and light the engine with a match. You'd use computers to automate everything—testing, calculations, countdown, launch.

Software deployment is no different. Yet for decades, developers did exactly that manual process:



CI/CD (Continuous Integration and Continuous Delivery) changes everything. It's the automation that turns software delivery from a stressful, error-prone manual process into a reliable, repeatable machine.


CI/CD: The Two Pillars

CI (Continuous Integration) — Every time you push code, it's automatically:

  •  Tested (unit tests, integration tests)

  •  Built (compiled, packaged)

  •  Analyzed (linting, security scanning)

The "integration" part means merging changes frequently—multiple times a day—so conflicts are caught early and small.

CD (Continuous Delivery / Deployment) — After passing CI, code is automatically:

  • Deployed to staging environments

  •  Verified with acceptance tests

  •  Deployed to production (if you choose Continuous Deployment)

  •  Notified to the team

The difference between Delivery and Deployment:

Continuous DeliveryContinuous Deployment
What happensCode is always ready to deployCode is deployed automatically
Human approvalYes, someone clicks "deploy"No, fully automated
When to useMost teamsHigh-confidence teams, SaaS products

Why CI/CD Matters

Before CI/CD (the old way):

  •  Deployments took hours or days

  •  Developers dreaded "deployment day"

  •  Bugs discovered weeks after they were introduced

  •  Merge conflicts that took days to resolve

  •  No one remembered what was deployed when

With CI/CD:

  •  Deployments in minutes

  •  Deploy anytime, confidently

  •  Bugs caught within minutes of being introduced

  •  Small merges, rarely any conflicts

  •  Perfect audit trail of what deployed when

But the biggest benefit isn't technical—it's psychological. When deployment is automated and safe, developers deploy more often. When they deploy more often, changes are smaller. When changes are smaller, bugs are easier to find and fix. It's a virtuous cycle.


🏗️ GitHub Actions Core Concepts

What is GitHub Actions?

GitHub Actions is CI/CD built directly into GitHub. It's not an add-on or a separate tool—it's part of every GitHub repository, free for public repositories and included with private repository plans.

Think of GitHub Actions as a automation platform that responds to events:

  • When someone pushes code → Run tests

  • When a pull request opens → Build the app

  • When a release is published → Deploy to production

  • Every night → Run security scans




Workflows: The Blueprint

A workflow is a YAML file that defines what to run, when to run it, and how. It lives in .github/workflows/ in your repository.

yaml
# .github/workflows/ci.yml
name: CI  # What this workflow is called

on:       # When to run it
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:     # What to run
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: npm test

Every workflow has three key parts:

PartPurposeExample
NameHuman-readable identifiername: "CI Pipeline"
onWhat triggers this workflowpush: branches: [main]
jobsWhat work to dotest:build:deploy:

Jobs: Units of Work

A job is a set of steps that run on a single runner. Jobs can run in parallel or sequentially (depending on dependencies).

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test
  
  build:
    runs-on: ubuntu-latest
    needs: test  # Wait for test to complete
    steps:
      - run: npm run build
  
  deploy:
    runs-on: ubuntu-latest
    needs: [test, build]  # Wait for both
    if: github.ref == 'refs/heads/main'  # Only on main branch
    steps:
      - run: npm run deploy

Key job features:

  • runs-on — Which operating system to use (ubuntu-latest, windows-latest, macOS-latest, or self-hosted)

  • needs — Dependencies on other jobs

  • if — Conditional execution

  • strategy — Matrix builds (test on multiple versions)


Runners: The Execution Environment

A runner is a server that executes your workflow jobs. GitHub provides hosted runners, or you can host your own.

GitHub-Hosted Runners (free, no maintenance):

LabelOSSpecsBest for
ubuntu-latestUbuntu 22.042-core, 7GB RAMMost Linux/Node.js/Python apps
windows-latestWindows Server 20222-core, 7GB RAM.NET, Windows-specific builds
macos-latestmacOS 143-core, 14GB RAMiOS/macOS builds, Apple ecosystem

Self-Hosted Runners (your own machines):

  • Use when you need: specific hardware, internal network access, longer run times, cost control

  • You manage the infrastructure (updates, security, availability)

Choosing a runner:

yaml
jobs:
  test:
    runs-on: ubuntu-latest  # Fast, free, most common
  
  windows-build:
    runs-on: windows-latest  # For .NET/Windows apps
  
  mac-build:
    runs-on: macos-latest    # For iOS/macOS apps
  
  internal:
    runs-on: self-hosted     # Your own server

Steps: The Commands

A step is a single action—either a command to run or a pre-built action to use.

yaml
steps:
  # Using an action (reusable component)
  - uses: actions/checkout@v4
  
  # Running a command
  - name: Install dependencies
    run: npm install
  
  # Multiple commands
  - name: Build and test
    run: |
      npm run build
      npm test
  
  # Environment-specific command
  - name: Deploy
    if: github.ref == 'refs/heads/main'
    run: npm run deploy

Why use actions vs. run?

  • Actions are reusable, pre-built components (like actions/checkout, which clones your repository)

  • run executes shell commands directly

  • Use actions for common tasks (checkout, setup-node, cache)

  • Use run for your specific commands (npm test, make, python script)


🔄 Building Your First Workflow

Step-by-Step: CI Pipeline for a Node.js App

Step 1: Create the workflow file

Create .github/workflows/ci.yml in your repository:

yaml
name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run linting
        run: npm run lint
      
      - name: Run tests
        run: npm test
      
      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

Step 2: Commit and push

bash
git add .github/workflows/ci.yml
git commit -m "ci: add GitHub Actions workflow"
git push origin main

Step 3: Watch it run

Go to your repository on GitHub → Actions tab. You'll see your workflow running!


Understanding Workflow Syntax

Let's break down that workflow piece by piece:

yaml
name: CI

Just a human-readable name. Shows up in GitHub's UI.

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

Triggers: Run when:

  • Someone pushes to main

  • Someone opens a pull request targeting main

yaml
jobs:
  test:
    runs-on: ubuntu-latest

A single job named "test" running on the latest Ubuntu runner.

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

First step: Use the checkout action to download your repository code onto the runner.

yaml
  - name: Setup Node.js
    uses: actions/setup-node@v4
    with:
      node-version: '18'
      cache: 'npm'

Set up Node.js version 18 and cache npm dependencies (speeds up future runs).

yaml
  - name: Install dependencies
    run: npm ci

npm ci is like npm install but faster and stricter—perfect for CI.

yaml
  - name: Run tests
    run: npm test

Run your test suite. If any test fails, the workflow fails.

yaml
  - name: Upload coverage report
    uses: actions/upload-artifact@v4
    with:
      name: coverage-report
      path: coverage/

Save the coverage report as an artifact (downloadable from the workflow run).


Matrix Builds: Test Across Multiple Configurations

Want to test on multiple Node.js versions? Use a matrix:

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

This creates three parallel jobs—one for each Node.js version!

Matrix on multiple dimensions:

yaml
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node-version: [18, 20]
    exclude:
      # Don't test Node 18 on Windows (optional)
      - os: windows-latest
        node-version: 18

Now you're testing on 5 different configurations (2 Node versions × 3 OSes, minus 1 excluded). All in parallel!


🧪 Testing Pipelines: Ensuring Quality

Types of Tests to Run

Test TypePurposeWhen to RunExample
LintingCode style, formattingEvery pushnpm run lint
Unit TestsIndividual functionsEvery pushnpm test
Integration TestsHow components work togetherPR to mainnpm run test:integration
E2E TestsFull user journeysBefore deploymentnpm run test:e2e
Security ScanVulnerabilitiesDaily, before releasenpm audit

Complete Testing Workflow Example

yaml
name: Test Suite

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

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run lint

  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: docker-compose up -d
      - run: npm run test:integration
      - run: docker-compose down

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm audit --production
      - uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

What's happening:

  • lint checks code style

  • unit-tests runs unit tests and uploads coverage

  • integration-tests spins up Docker containers, runs integration tests

  • security scans for vulnerabilities


🚀 Deployment Pipelines: Getting to Production

Environment-Based Deployment

Different environments for different branches:

yaml
name: Deploy

on:
  push:
    branches:
      - main
      - staging
      - develop

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy to Staging
        run: npm run deploy:staging
        env:
          API_KEY: ${{ secrets.STAGING_API_KEY }}

  deploy-production:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: 
      name: production
      url: https://example.com
    needs: deploy-staging  # Wait for staging to succeed
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy to Production
        run: npm run deploy:production
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}

Key deployment features:

FeaturePurposeExample
environmentGroups deployment historyenvironment: staging
ifConditional executionif: github.ref == 'refs/heads/main'
needsJob dependenciesneeds: deploy-staging
environment.urlLink in GitHub UIurl: https://example.com

Multi-Environment Promotion Pipeline

A common pattern: promote through environments automatically:

yaml
name: Promote to Production

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to promote'
        required: true

jobs:
  promote:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy version ${{ github.event.inputs.version }}
        run: |
          echo "Deploying version ${{ github.event.inputs.version }}"
          ./deploy.sh ${{ github.event.inputs.version }}

This workflow is triggered manually (workflow_dispatch), asking which version to deploy.


Deployment Examples by Platform

AWS S3 (static website):

yaml
- name: Deploy to S3
  run: aws s3 sync build/ s3://my-bucket --delete
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

AWS ECS (containers):

yaml
- name: Build Docker image
  run: docker build -t myapp:${{ github.sha }} .
- name: Push to ECR
  run: |
    aws ecr get-login-password | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}
    docker push ${{ secrets.ECR_REGISTRY }}/myapp:${{ github.sha }}
- name: Deploy to ECS
  run: |
    aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment

GitHub Pages:

yaml
- name: Build
  run: npm run build
- name: Deploy to GitHub Pages
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./build

Vercel:

yaml
- name: Deploy to Vercel
  uses: amondnet/vercel-action@v20
  with:
    vercel-token: ${{ secrets.VERCEL_TOKEN }}
    vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
    vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}

🔐 Secrets & Environment Variables

What Are GitHub Secrets?

GitHub Secrets are encrypted variables stored in your repository. They're never shown in logs, never exposed in UI, and only available to GitHub Actions during workflow runs.

yaml
# Using a secret
env:
  API_KEY: ${{ secrets.API_KEY }}

# In a step
- name: Deploy
  run: ./deploy.sh --key ${{ secrets.API_KEY }}

Types of secrets:

WhereScopeWhen to Use
Repository secretsOne repoSingle project
Environment secretsOne environment (dev/staging/prod)Different values per environment
Organization secretsMultiple reposShared across team

Setting Up Secrets

Repository secrets:

  1. Go to your repository → Settings → Secrets and variables → Actions

  2. Click New repository secret

  3. Name: API_KEY (uppercase, underscores)

  4. Value: sk_live_1234567890

  5. Click Add secret

Environment secrets:

  1. Create an environment: Settings → Environments → New environment

  2. Name it productionstaging, etc.

  3. Add secrets just for that environment

Using environment secrets:

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # Uses production environment secrets
    steps:
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}  # From production environment

Environment Variables in Workflows

Three types of variables:

yaml
jobs:
  example:
    runs-on: ubuntu-latest
    
    # 1. Workflow-level environment variables
    env:
      GLOBAL_VAR: "value"
    
    steps:
      # 2. Step-level environment variables
      - name: Example
        env:
          STEP_VAR: "value"
        run: echo $STEP_VAR
      
      # 3. Context variables (built-in)
      - name: Show context
        run: |
          echo "Branch: ${{ github.ref_name }}"
          echo "Commit: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"

Common context variables:

VariableWhat it contains
github.repositoryusername/repo-name
github.ref_nameBranch name (e.g., main)
github.shaCommit hash
github.actorWho triggered the workflow
github.event_namepushpull_request, etc.
github.run_idUnique ID for this run

Security Best Practices

✅ DO:

  • Store all secrets in GitHub Secrets, never in code

  • Use environment-specific secrets for different deployments

  • Rotate secrets regularly (every 30-90 days)

  • Use OIDC when possible (no secrets at all!)

  • Scope secrets to minimum required permissions

  • Review secret access regularly

❌ DON'T:

  • Print secrets to logs (echo ${{ secrets.API_KEY }})

  • Commit secrets to repository

  • Share secrets across services without need

  • Use the same secret for dev and prod

Printing secrets safely (they'll be masked in logs):

yaml
- name: Debug secret (masked in logs)
  run: echo "API_KEY is ${{ secrets.API_KEY }}"
  # Output: API_KEY is ***

OIDC: The Secret-Free Way

OIDC (OpenID Connect) allows GitHub Actions to authenticate to cloud providers without storing any secrets.

yaml
# AWS OIDC (no access keys needed!)
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
    aws-region: us-west-2

Benefits:

  • No long-lived secrets to rotate

  • Temporary credentials (15-60 minutes)

  • Auditable—every run leaves a trail

  • More secure than static keys

Supported providers:

  • AWS (via OIDC)

  • Azure (via workload identity)

  • GCP (via workload identity federation)

  • Many others


🎯 Real-World Pipeline Examples

Example 1: Node.js Application

yaml
name: Node.js CI/CD

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

env:
  NODE_VERSION: '18'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build
          path: dist/

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build
          path: dist/
      - name: Deploy to Staging
        run: |
          aws s3 sync dist/ s3://staging-bucket --delete
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_STAGING_ACCESS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_STAGING_SECRET_KEY }}

  deploy-production:
    runs-on: ubuntu-latest
    needs: [test, build]
    if: github.ref == 'refs/heads/main'
    environment: 
      name: production
      url: https://example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build
          path: dist/
      - name: Deploy to Production
        run: |
          aws s3 sync dist/ s3://production-bucket --delete
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_PROD_ACCESS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_PROD_SECRET_KEY }}

Example 2: Python Application

yaml
name: Python CI/CD

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.9', '3.10', '3.11']
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
      - name: Lint
        run: |
          pip install flake8
          flake8 src/
      - name: Test
        run: |
          pip install pytest pytest-cov
          pytest --cov=src tests/

  docker-build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: |
          docker build -t myapp:${{ github.sha }} .
      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker tag myapp:${{ github.sha }} myregistry/myapp:latest
          docker push myregistry/myapp:latest

Example 3: Multiple Languages (Monorepo)

yaml
name: Monorepo CI

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

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            frontend:
              - 'frontend/**'
              - 'package.json'
            backend:
              - 'backend/**'
              - 'requirements.txt'

  frontend:
    needs: changes
    if: ${{ needs.changes.outputs.frontend == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: cd frontend && npm ci
      - run: cd frontend && npm test
      - run: cd frontend && npm run build

  backend:
    needs: changes
    if: ${{ needs.changes.outputs.backend == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: cd backend && pip install -r requirements.txt
      - run: cd backend && pytest

📋 GitHub Actions Best Practices

Performance

  • ✅ Cache dependencies to speed up workflows

  • ✅ Use matrix builds to parallelize tests

  • ✅ Run jobs in parallel (by default they do!)

  • ✅ Use larger runners for heavy builds (but they cost more)

  • ✅ Cancel in-progress jobs when new code is pushed

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

Security

  • ✅ Use OIDC instead of long-lived secrets when possible

  • ✅ Scope secrets to environments (not global)

  • ✅ Pin actions by commit hash (not just version tag)

  • ✅ Review third-party actions before using

  • ✅ Never echo secrets to logs

yaml
# ❌ Don't do this
- run: echo "Secret: ${{ secrets.API_KEY }}"

# ✅ Do this (secrets are automatically masked)
- run: echo "Secret: ${{ secrets.API_KEY }}"
  # Output: Secret: ***

Reliability

  • ✅ Use actions/checkout@v4 not @v3 or @main

  • ✅ Set timeouts to prevent hung jobs

  • ✅ Add retry logic for flaky steps

  • ✅ Use if conditions to skip unnecessary steps

yaml
- name: Flaky step
  uses: nick-invision/retry@v2
  with:
    timeout_minutes: 10
    max_attempts: 3
    command: npm run test:e2e

Maintainability

  • ✅ Use environment variables for reusable values

  • ✅ Break large workflows into multiple files

  • ✅ Name jobs and steps clearly

  • ✅ Use uses for common actions (don't reinvent the wheel)


🧪 Practice Exercises

Exercise 1: Create Your First Workflow

Task: Create a workflow that runs on every push and prints "Hello, World!"

Solution:

yaml
# .github/workflows/hello.yml
name: Hello

on: [push]

jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello, World!"

Push this file and watch it run!


Exercise 2: Add Testing to Your Project

Task: Add a workflow that runs tests on every pull request.

Prerequisite: Your project has a npm test or pytest or similar.

Solution:

yaml
# .github/workflows/test.yml
name: Test

on:
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test

Exercise 3: Matrix Build

Task: Test your project on Node.js 18, 20, and 22.

Solution:

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

📚 Summary: CI/CD is a Superpower

GitHub Actions transforms your repository from a place where code sits to a place where code does things automatically.

Before GitHub ActionsAfter GitHub Actions
Manual testingAutomatic tests on every push
Deploy on Fridays (scary)Deploy anytime (confident)
"It works on my machine"It works in CI
Secrets in codeSecrets in GitHub
No deployment historyFull audit trail

The most important takeaway: Start small. Add one workflow—maybe just npm test on push. Then add a build. Then add a deployment to staging. Then, when you're confident, automate production.

You don't need to build the perfect pipeline overnight. You just need to start.


🔗 Master GitHub Actions with Hands-on Labs

The best way to learn CI/CD is to build pipelines yourself—writing workflows, debugging failures, and watching deployments happen automatically.

👉 Practice GitHub Actions with guided exercises and real deployment scenarios at:
https://devops.trainwithsky.com/

Our platform provides:

  • Real workflow creation exercises

  • Test pipeline implementation challenges

  • Multi-environment deployment practice

  • Secrets management simulations

  • OIDC configuration labs


Frequently Asked Questions

Q: How many minutes do I get for free?

A: Public repositories: Unlimited minutes, free. Private repositories: 2000 minutes/month free for GitHub Free, 3000 minutes/month for GitHub Pro, more for teams.

Q: Can I use GitHub Actions for private repositories?

A: Yes! Private repositories get free minutes each month (2000-3000 depending on plan). After that, you pay per minute.

Q: What's the difference between npm ci and npm install?

A: npm ci is faster, stricter, and perfect for CI. It uses package-lock.json exactly and fails if it doesn't match. Use npm ci in CI, npm install locally.

Q: Can I run workflows on a schedule?

A: Yes! Use schedule trigger:

yaml
on:
  schedule:
    - cron: '0 2 * * *'  # 2 AM daily

Q: How do I debug a failing workflow?

A:

  • Check the logs (click on the failed step)

  • Add run: set -x to see detailed command output

  • Use actions/upload-artifact to save logs

  • Add ACTIONS_RUNNER_DEBUG: true in env

Q: Can I use GitHub Actions to deploy to my own servers?

A: Absolutely! Use self-hosted runners on your infrastructure, or use SSH actions to deploy to remote servers.

Q: What's the difference between uses and run?

A: uses calls a reusable action (like actions/checkout@v4). run executes shell commands directly. Use actions for common tasks, run for your specific commands.


Have questions about GitHub Actions? Stuck on a workflow? Need help designing your CI/CD pipeline? Share your scenario in the comments below—our community includes thousands of developers who've built pipelines for every language and platform! 💬

Comments

Popular posts from this blog

Introduction to Terraform – The Future of Infrastructure as Code

  Introduction to Terraform – The Future of Infrastructure as Code In today’s fast-paced DevOps world, managing infrastructure manually is outdated . This is where Terraform comes in—a powerful Infrastructure as Code (IaC) tool that allows you to define, provision, and manage cloud infrastructure efficiently . Whether you're working with AWS, Azure, Google Cloud, or on-premises servers , Terraform provides a declarative, automation-first approach to infrastructure deployment. Shape Your Future with AI & Infinite Knowledge...!! Read In-Depth Tech & Self-Improvement Blogs http://www.skyinfinitetech.com Watch Life-Changing Videos on YouTube https://www.youtube.com/@SkyInfinite-Learning Transform Your Skills, Business & Productivity – Join Us Today! In today’s digital-first world, agility and automation are no longer optional—they’re essential. Companies across the globe are rapidly shifting their operations to the cloud to keep up with the pace of innovatio...

📊 Monitoring & Logging in Kubernetes – Tools like Prometheus, Grafana, and Fluentd

  Monitoring & Logging in Kubernetes – Tools like Prometheus, Grafana, and Fluentd Monitoring and logging are essential for maintaining a healthy and well-performing Kubernetes cluster. In this guide, we’ll cover why monitoring is important, key monitoring tools like Prometheus and Grafana, and logging tools like Fluentd to help you gain visibility into your cluster’s performance and logs. Shape Your Future with AI & Infinite Knowledge...!! Want to Generate Text-to-Voice, Images & Videos? http://www.ai.skyinfinitetech.com Read In-Depth Tech & Self-Improvement Blogs http://www.skyinfinitetech.com Watch Life-Changing Videos on YouTube https://www.youtube.com/@SkyInfinite-Learning Transform Your Skills, Business & Productivity – Join Us Today! 🚀 Introduction In today’s fast-paced cloud-native environment, Kubernetes has emerged as the de-facto container orchestration platform. But deploying and managing applications in Kubernetes is just half the ba...

🔒 Kubernetes Security – RBAC, Network Policies, and Secrets Management

  Kubernetes Security – RBAC, Network Policies, and Secrets Management Security is a critical aspect of managing Kubernetes clusters. In this guide, we'll cover essential security mechanisms like Role-Based Access Control (RBAC) , Network Policies , and Secrets Management to help you secure your Kubernetes environment effectively. Shape Your Future with AI & Infinite Knowledge...!! Want to Generate Text-to-Voice, Images & Videos? http://www.ai.skyinfinitetech.com Read In-Depth Tech & Self-Improvement Blogs http://www.skyinfinitetech.com Watch Life-Changing Videos on YouTube https://www.youtube.com/@SkyInfinite-Learning Transform Your Skills, Business & Productivity – Join Us Today! 🚀 Introduction: Why Kubernetes Security Is Non-Negotiable As Kubernetes becomes the backbone of modern cloud-native infrastructure, security is no longer optional—it’s mission-critical . With multiple moving parts like containers, pods, services, nodes, and more, Kuberne...