GitHub Actions — CI/CD Automation

TL;DR

GitHub Actions lets you automate workflows directly in your repo. Write YAML files in .github/workflows/ to run tests on every push, deploy on merge to main, publish packages, and more. Free for public repos, 2,000 minutes/month for private.

Explain Like I'm 12

Imagine you have a robot assistant that watches your homework folder. Every time you save a new version, the robot automatically checks your spelling, runs the math problems through a calculator, and if everything's correct, prints it out and puts it in your teacher's mailbox. That's GitHub Actions — a robot that automatically checks and ships your code.

How GitHub Actions Works

A workflow is triggered by an event (push, PR, schedule), runs jobs on virtual machines, and each job has steps that execute commands or use pre-built actions.

GitHub Actions architecture: event triggers workflow, workflow contains jobs, jobs run on runners, each job has steps using actions or shell commands

Anatomy of a Workflow

Workflows live in .github/workflows/*.yml. Here's a complete CI workflow:

# .github/workflows/ci.yml
name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Upload coverage
        if: matrix.node-version == 22
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/
Info: The matrix strategy runs the same job across multiple configurations in parallel. The example above tests on Node 18, 20, and 22 simultaneously.

Workflow Triggers

TriggerYAMLWhen It Fires
Pushon: pushCode pushed to a branch
Pull Requeston: pull_requestPR opened, updated, or reopened
Scheduleon: scheduleCron schedule (e.g., nightly builds)
Manualon: workflow_dispatchManually triggered from GitHub UI
Releaseon: releaseRelease published
Other workflowson: workflow_callCalled by another workflow (reusable)
# Run nightly at 2 AM UTC
on:
  schedule:
    - cron: '0 2 * * *'

# Manual trigger with inputs
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy to'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]
Tip: Use paths filter to only run workflows when relevant files change: on: push: paths: ['src/**', 'tests/**']. This saves CI minutes.

Actions & the Marketplace

Actions are reusable building blocks. Use community actions from the GitHub Marketplace or write your own.

Most-Used Actions

ActionPurpose
actions/checkout@v4Check out your repository code
actions/setup-node@v4Install and cache Node.js
actions/setup-python@v5Install and cache Python
actions/cache@v4Cache dependencies between runs
actions/upload-artifact@v4Save files (logs, builds) from a run
github/codeql-action@v3Security scanning for vulnerabilities
Warning: Always pin actions to a specific version (@v4) or commit SHA — never use @main. A compromised action at @main could execute malicious code in your CI pipeline.

Secrets & Environment Variables

Store sensitive data (API keys, tokens) as encrypted secrets in repo settings:

steps:
  - name: Deploy to production
    env:
      API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
    run: |
      echo "Deploying with token..."
      ./deploy.sh
Info: Secrets are never printed in logs — GitHub automatically masks them. Use Environments (staging, production) for environment-specific secrets with approval gates.

Example: Deploy on Merge to Main

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npm run build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          accountId: ${{ secrets.CF_ACCOUNT_ID }}
          projectName: my-app
          directory: dist

Performance Tips

  • Cache dependencies — use actions/cache or built-in caching in setup actions
  • Use path filters — skip CI when only docs changed
  • Run jobs in parallel — independent jobs run simultaneously by default
  • Use concurrency — cancel in-progress runs when a new push arrives
# Cancel outdated runs for the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
Tip: The concurrency setting prevents wasted CI minutes when you push multiple times in quick succession.

Test Yourself

What's the difference between on: push and on: pull_request triggers?

on: push fires when commits are pushed to a branch. on: pull_request fires when a PR is opened, updated (new commits pushed to the PR branch), or reopened. Key difference: pull_request runs against the merge result (base + head), while push runs against the pushed commit directly.

How do you store and use sensitive credentials in GitHub Actions?

Store them as encrypted secrets in Settings → Secrets and variables → Actions. Reference them in workflows with ${{ secrets.SECRET_NAME }}. GitHub automatically masks secret values in logs. For environment-specific secrets, create Environments with approval gates.

What does a matrix strategy do?

A matrix strategy runs the same job across multiple configurations in parallel. For example, matrix: { node-version: [18, 20, 22], os: [ubuntu-latest, windows-latest] } creates 6 jobs (3 versions × 2 OS). Great for testing compatibility across versions and platforms.

Why should you pin actions to a version or SHA instead of using @main?

Using @main means your CI runs whatever code is currently on the action's main branch. If the action is compromised or updated with breaking changes, your workflow breaks or runs malicious code. Pinning to @v4 or a commit SHA ensures you use a known, tested version.

How can you reduce CI minutes when pushing frequently?

Use concurrency groups with cancel-in-progress: true to cancel outdated runs. Use path filters to skip runs when only irrelevant files changed. Cache dependencies to avoid re-downloading. Run jobs in parallel instead of sequentially.

Interview Questions

Design a CI/CD pipeline for a Node.js app with staging and production environments.

Create two workflows: (1) CI — runs on all PRs: checkout, install, lint, test, build. Uses matrix for Node versions. (2) Deploy — runs on push to main: builds, deploys to staging automatically, then requires manual approval (via GitHub Environments) to deploy to production. Use secrets per environment for API keys. Add concurrency groups to cancel in-progress staging deploys.

How would you create a reusable workflow that multiple repos can share?

Create a workflow with on: workflow_call in a shared repo. Define inputs and secrets the caller must provide. Callers reference it with uses: org/shared-repo/.github/workflows/ci.yml@v1. This is different from reusable actions (which are individual steps). Reusable workflows share entire job definitions.

A developer accidentally committed an API key to a public repo. What GitHub features help prevent and detect this?

Prevention: (1) git-secrets or pre-commit hooks to block secrets locally. (2) GitHub push protection — blocks pushes containing known secret patterns. Detection: (3) Secret scanning — alerts when secrets are found in code. (4) Dependabot alerts for vulnerable dependencies. Response: rotate the key immediately, use git filter-repo to remove from history, enable branch protection to require reviews.