33 GitHub Actions CI/CD Interview Questions

·28 min read
By ·Updated
cicdgithub-actionsdevopsautomationinterview-preparation

CI/CD pipelines are central to modern software development. Whether you're applying for backend, full-stack, or DevOps roles, interviewers expect you to understand how code goes from commit to production.

This 2026 guide covers 33 CI/CD interview questions, practical GitHub Actions knowledge, workflow security, and deployment trade-offs. The examples favor clarity; in production, pin third-party actions to reviewed full commit SHAs because a mutable version tag is not a supply-chain boundary.

Table of Contents

  1. CI/CD Fundamentals Questions
  2. GitHub Actions Core Questions
  3. Workflow Triggers Questions
  4. Job Dependencies and Parallelism Questions
  5. Matrix Builds Questions
  6. Secrets and Environment Variables Questions
  7. Caching and Performance Questions
  8. Artifacts and Job Communication Questions
  9. Deployment Strategies Questions
  10. Production Pipeline Questions
  11. Reusable Workflows Questions
  12. Troubleshooting and Best Practices Questions

CI/CD Fundamentals Questions

Understanding the distinction between CI and CD is often the opening question in DevOps interviews.

What is the difference between CI and CD?

Many candidates give vague answers like "CI/CD is automated deployment," which misses the key distinctions. CI and CD address different problems in the software delivery process and can be implemented independently.

Continuous Integration (CI) focuses on code quality and integration:

  • Automatically builds and tests code when changes are pushed
  • Catches integration issues early (merge conflicts, test failures)
  • Developers integrate small changes frequently
  • The pipeline provides shared evidence about a specific revision

Continuous Delivery (CD) focuses on release readiness:

  • Code is always in a deployable state
  • The path to a releasable artifact is automated
  • Release timing can still be a product, compliance, or governance decision
  • "Could deploy at any time"

Continuous Deployment takes CD further:

  • Fully automated deployment to production
  • Every change satisfying the release policy is deployed automatically
  • Requires strong automated checks, observability, and recovery controls
  • "Release without a routine human gate"
flowchart LR
    subgraph ci["CI"]
        Push --> Build --> Test
    end
    subgraph cd["CD"]
        Test --> Staging --> Approval{"Approval?"} --> Production
    end

Why is Continuous Integration important?

CI catches problems early when they're cheapest to fix. Without CI, developers work in isolation for days or weeks, then face painful "integration hell" when merging. The longer code diverges from main, the more conflicts and bugs accumulate.

With CI, every push triggers automated builds and tests. A broken build is immediately visible to the entire team. This creates social pressure to keep the build green and encourages small, frequent commits rather than large, risky changes.

Key benefits:

  • Fast feedback on code changes (minutes, not days)
  • Reduced integration risk through frequent merging
  • Automated quality gates (tests, linting, security scans)
  • Single source of truth for what works

What is the difference between Continuous Delivery and Continuous Deployment?

This distinction confuses many candidates. Both start with "CD" and both automate the path to production, but they differ in the final step.

Continuous Delivery keeps accepted changes in a releasable state. A manual approval is one possible production policy, not part of the definition; release windows, compliance controls, or an explicit product decision can determine when the artifact is promoted.

Continuous Deployment makes production release the automatic result of satisfying the pipeline's policy. It requires high-confidence checks, feature flags or another way to separate deployment from exposure, progressive delivery where useful, observability, and a tested recovery path.


GitHub Actions Core Questions

GitHub Actions is the most common CI/CD platform for GitHub repositories and appears frequently in interviews.

What are workflows, jobs, and steps in GitHub Actions?

Understanding the hierarchy is essential for writing and debugging pipelines. Each level has different characteristics and constraints that affect how you structure your automation.

A workflow is a YAML file in .github/workflows/ that defines an automated process. Workflows are triggered by events (push, pull request, schedule) and contain one or more jobs.

Jobs are units of work scheduled on runner environments. Jobs without dependencies can run in parallel. Standard GitHub-hosted jobs normally receive a fresh runner image, but self-hosted runners can retain state and require deliberate cleanup and isolation. Use artifacts, caches, or outputs instead of assuming two jobs share a filesystem.

Steps are sequential tasks within a job. They share the same runner and filesystem. Steps can be shell commands (run:) or reusable actions (uses:).

# .github/workflows/ci.yml
name: CI Pipeline                    # Workflow name
 
on:                                   # Triggers
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:                                 # Jobs run in parallel by default
  test:
    runs-on: ubuntu-latest           # Runner environment
    steps:                            # Steps run sequentially
      - uses: actions/checkout@v7    # Pin a full SHA in production
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
      - run: npm ci                   # Shell command
      - run: npm test
 
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
      - run: npm ci
      - run: npm run lint

What is the difference between run and uses in a step?

Steps execute work in two ways, and choosing correctly affects reusability and maintenance.

run: executes shell commands directly on the runner. Use this for simple commands, scripts, or when you need full control over execution. Commands run in the default shell (bash on Linux/macOS, PowerShell on Windows).

uses: invokes a reusable action—a packaged unit of automation. Actions can come from the marketplace, other repositories, or your own repo. They handle complex tasks like checking out code, setting up languages, or deploying to cloud providers.

steps:
  # Using an action - packaged, versioned, reusable
  - uses: actions/checkout@v7
 
  # Running a command - direct shell execution
  - run: npm test
 
  # Multi-line command
  - run: |
      echo "Building..."
      npm run build
      echo "Done!"

Best practice: Use actions for common tasks (checkout, setup, deploy) and run for project-specific commands.

What runners are available in GitHub Actions?

Runners are the machines that execute your jobs. GitHub provides hosted runners, or you can use self-hosted runners for more control.

GitHub-hosted runners are environments that GitHub maintains. Common standard labels include ubuntu-latest, windows-latest, and macos-latest, with explicit version labels available when you need tighter control. In September 2026, Linux labels include ubuntu-24.04, ubuntu-22.04, and an ubuntu-26.04 public preview. GitHub's -latest means the latest stable image GitHub provides, not necessarily the newest operating-system release from the vendor.

Billing, quotas, hardware, and image availability depend on repository visibility, plan, architecture, and runner type. Standard hosted jobs generally start with a clean image, but always consult the current runner catalog rather than memorizing a static list.

Self-hosted runners are machines you manage:

  • Run on your own infrastructure (on-premise, cloud)
  • Access to internal networks and resources
  • State can persist between jobs, which can improve locality but also leak data or contaminate builds
  • Capacity, patching, isolation, autoscaling, and infrastructure cost are your responsibility

Workflow Triggers Questions

Triggers determine when workflows run. Configuring them correctly prevents wasted CI minutes and ensures appropriate automation.

What events can trigger a GitHub Actions workflow?

GitHub Actions supports dozens of trigger events. Knowing the common ones and their options is essential for efficient pipelines.

on:
  # Push/PR triggers
  push:
    branches: [main, develop]
    paths:
      - 'src/**'                     # Only trigger for src changes
      - '!src/**/*.md'               # Exclude markdown files
  pull_request:
    types: [opened, synchronize, reopened]
 
  # Scheduled (cron)
  schedule:
    - cron: '0 0 * * *'              # Daily at midnight UTC
 
  # Manual trigger
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
 
  # From other workflows
  workflow_call:                      # Reusable workflow
 
  # External events
  repository_dispatch:                # API trigger

Common triggers:

  • push / pull_request - Code changes
  • schedule - Cron jobs (nightly builds, cleanup)
  • workflow_dispatch - Manual runs with inputs
  • workflow_call - Called by other workflows
  • release - When releases are published

How do you prevent running CI on documentation changes?

Path filters can reduce unnecessary work, but use them only when documentation truly cannot affect the product, generated output, packaging, or compliance checks. In a pull request, a workflow skipped by branch or path filtering can leave its associated required check in a pending state and block merging.

Use paths-ignore to skip workflows for specific patterns:

on:
  push:
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - '.github/ISSUE_TEMPLATE/**'

Alternatively, use paths to only run on specific changes:

on:
  push:
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package.json'

Important: Validate filters against monorepo dependencies and required-check rules. It is often safer to start the workflow and conditionally skip expensive jobs while still reporting an explicit successful check.

How do you trigger a workflow manually with parameters?

The workflow_dispatch event enables manual triggers with custom inputs. This is useful for deployments, data migrations, or any operation requiring human judgment.

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - staging
          - production
      debug_enabled:
        description: 'Enable debug logging'
        required: false
        type: boolean
        default: false
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ inputs.environment }}"
      - if: inputs.debug_enabled
        run: echo "Debug mode enabled"

Manual workflows appear in the Actions tab with a "Run workflow" button that shows the input form.


Job Dependencies and Parallelism Questions

Understanding job execution order is crucial for efficient pipelines that don't waste time or miss dependencies.

How do you control the order jobs run in?

By default, jobs run in parallel for maximum speed. Use the needs keyword to create dependencies when jobs must run sequentially.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."
 
  test:
    needs: build                      # Waits for build to complete
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing..."
 
  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to staging..."
 
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production           # Approval only if the environment is configured for it
    steps:
      - run: echo "Deploying to production..."

This creates a linear pipeline:

flowchart LR
    build --> test --> deploy-staging --> deploy-production

How do you run jobs in parallel with a shared dependency?

Some jobs can run in parallel but must all complete before a later job starts. Use an array in needs to wait for multiple jobs.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test
 
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - run: npm audit
 
  deploy:
    needs: [lint, test, security-scan]  # Waits for ALL three
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh
flowchart LR
    lint --> deploy
    test --> deploy
    security-scan --> deploy

Lint, test, and security-scan run simultaneously. Deploy only starts after all three succeed.

What happens if a job in the dependency chain fails?

When a job fails, all jobs that depend on it (directly or indirectly) are skipped by default. This prevents deploying broken code or wasting resources on doomed jobs.

You can override this behavior with conditionals:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test
 
  report:
    needs: test
    if: always()                      # Run even if test fails
    runs-on: ubuntu-latest
    steps:
      - run: echo "Test completed with status: ${{ needs.test.result }}"
 
  deploy:
    needs: test
    if: success()                     # Only if test succeeded (default)
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

Conditional options:

  • success() - Previous jobs succeeded (default)
  • failure() - At least one previous job failed
  • always() - Run regardless of previous job status
  • cancelled() - Workflow was cancelled

Matrix Builds Questions

Matrix builds test across multiple configurations efficiently, a common requirement for libraries and cross-platform applications.

What is a build matrix and when would you use it?

A matrix runs the same job multiple times with different configurations. GitHub Actions automatically creates a job for each combination of matrix values.

This is essential for libraries that must work across Node versions, Python versions, or operating systems. Instead of duplicating job definitions, you define the variations once.

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [22, 24, 26]
      fail-fast: false                # Don't cancel others if one fails
 
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

This creates 9 parallel jobs (3 operating systems × 3 Node versions).

How do you exclude or include specific matrix combinations?

Sometimes certain combinations don't make sense or need special handling. Use exclude and include to customize the matrix.

Excluding combinations:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node-version: [22, 24]
    exclude:
      - os: windows-latest
        node-version: 22              # Skip this supported combination

Including additional combinations with extra variables:

strategy:
  matrix:
    os: [ubuntu-latest]
    node-version: [22, 24]
    include:
      - os: ubuntu-latest
        node-version: 26
        experimental: true            # Add extra variable for this combo

You can then use matrix.experimental in conditionals or step configuration.

What does fail-fast do in a matrix build?

By default, fail-fast is true, meaning GitHub cancels all remaining matrix jobs when any job fails. This saves resources when you know the entire matrix is broken.

Set fail-fast: false when you want all combinations to complete regardless of individual failures. This is useful when:

  • Debugging which specific combinations fail
  • Each combination's results are independently valuable
  • You're testing optional/experimental configurations
strategy:
  fail-fast: false
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]

Secrets and Environment Variables Questions

Handling sensitive data correctly is critical for pipeline security. This topic appears in almost every DevOps interview.

How do you handle secrets in CI/CD pipelines?

Never hardcode secrets in code or workflow files. Secrets committed to git are compromised forever—even if deleted, they exist in history. Use your platform's secret management system.

GitHub Actions secrets are stored by GitHub and made available through the secrets context only where scope and event rules allow it. That does not make arbitrary workflow code safe: an action or shell command with access can exfiltrate a value, and transformed values may evade log redaction.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: ./deploy.sh

Best practices:

  • Prefer OIDC and short-lived cloud credentials over stored access keys
  • Give GITHUB_TOKEN explicit least-privilege permissions, ideally per job
  • Scope unavoidable secrets at the organization, repository, or environment level
  • Never execute untrusted pull-request code in a privileged context such as pull_request_target
  • Pin third-party actions to reviewed full commit SHAs and review their source
  • Rotate credentials, redact logs, and treat masking as defense in depth rather than proof of secrecy

What is the difference between repository secrets and environment secrets?

GitHub supports secrets at multiple scopes, providing flexibility for different security requirements.

Repository secrets can be referenced by eligible workflows in the repository, subject to event and policy restrictions. Secrets are not passed to workflows triggered from forks by default, and Dependabot pull requests are treated similarly. Use repository scope only when a narrower environment scope is insufficient.

Environment secrets are scoped to jobs that reference a named environment. If that environment has required reviewers, the job cannot access its secrets before approval. Availability of reviewers and secrets for private repositories depends on the GitHub plan.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production           # Uses production-specific secrets
    steps:
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

Environment protection rules can add reviewers, wait timers, branch or tag restrictions, or custom checks. Referencing environment: production alone does not create a manual gate; the repository must configure one.

How do you use OIDC for cloud authentication instead of storing credentials?

OpenID Connect (OIDC) lets workflows authenticate to cloud providers without storing long-lived credentials. The workflow requests a short-lived token that the cloud provider validates.

This reduces the exposure of long-lived access keys because tokens are short-lived and the cloud trust policy can restrict repository, branch, tag, environment, audience, or reusable-workflow claims. It is not automatically safe: keep id-token: write limited to the job that needs it and make the cloud role least-privileged.

permissions:
  id-token: write
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions
          aws-region: us-east-1
 
      - run: aws s3 sync ./dist s3://my-bucket

AWS, GCP, and Azure all support OIDC authentication with GitHub Actions.


Caching and Performance Questions

Slow pipelines waste developer time and delay feedback. Caching is the primary technique for speeding up CI.

How do you speed up CI pipelines with caching?

Caching stores files between workflow runs, avoiding repeated downloads. The most common use is caching package dependencies that rarely change.

steps:
  - uses: actions/checkout@v7
 
  - name: Cache npm downloads
    uses: actions/cache@v4
    with:
      path: ~/.npm
      key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-node-
 
  - run: npm ci
  - run: npm test

The cache key includes a hash of the lockfile, so the cache invalidates when dependencies change. The restore-keys provide fallback patterns for partial matches.

Many setup actions have built-in caching:

- uses: actions/setup-node@v6
  with:
    node-version: '24'
    cache: 'npm'                      # Caches npm's global package data

What should you cache in CI pipelines?

Cache data that is expensive to download or derive, safe to restore for less-trusted branches, and validated by a key that captures every relevant input. A cache hit is an optimization, not a correctness guarantee; the build must still work after a miss.

Common cache targets:

  • ~/.npm or another package-manager download store (prefer this to restoring node_modules blindly)
  • ~/.cache/pip (Python)
  • ~/.m2/repository (Maven)
  • ~/.gradle/caches (Gradle)
  • Docker layers (using buildx cache)
  • Compiled dependencies (Rust target directory)

Cache key strategy:

key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

This key changes when the lockfile changes. Cache entries are immutable, so a new key creates a new entry; broad restore keys may return older data, which the package manager must validate. Treat caches restored from untrusted changes as a supply-chain input and avoid placing credentials or sensitive build output in them.

What other techniques speed up CI pipelines?

Beyond caching, several strategies reduce pipeline duration.

Run jobs in parallel when they don't depend on each other:

jobs:
  lint:
    runs-on: ubuntu-latest
    # ...
  test:
    runs-on: ubuntu-latest
    # ... runs simultaneously with lint

Use path filters only after modeling dependency boundaries and required checks:

on:
  push:
    paths:
      - 'src/**'                      # Only run for source changes

Use shallow clones when you don't need full history:

  - uses: actions/checkout@v7
  with:
    fetch-depth: 1                    # Only latest commit

Run affected tests only when your dependency graph is trustworthy, and keep a periodic or pre-release full suite as a backstop. Then profile queue time, runner startup, dependency installation, test sharding, Docker layer reuse, and artifact transfer instead of optimizing only command execution.


Artifacts and Job Communication Questions

Jobs should not assume a shared filesystem. Outputs are suitable for small scalar values, while artifacts carry files between jobs or preserve evidence from a run.

How do you pass files between jobs?

Since jobs run on different runners, they don't share filesystems. Use artifacts to upload files from one job and download them in another.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm run build
 
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7
 
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
 
      - run: ./deploy.sh dist/

Artifact retention follows repository or organization settings and can be shortened per upload with retention-days, subject to plan limits. For release assets, also record a digest and provenance; a convenient download name alone does not prove what was built.

When would you use artifacts versus caching?

Artifacts and caching serve different purposes despite both storing files.

Artifacts pass data between jobs in the same workflow run or preserve outputs for later use:

  • Build outputs needed by deploy jobs
  • Test reports and coverage data
  • Logs for debugging failed runs

Caching speeds up workflows by reusing data across runs:

  • Package-manager downloads
  • Compiled binaries that don't change often
  • Downloaded tools

Key difference: an artifact is an intentional output with retention and download semantics; a cache is opportunistic reusable input selected by keys and scope. Never use a cache as the canonical release artifact.


Deployment Strategies Questions

Deployment strategies minimize risk when releasing new code. This is a common conceptual topic in DevOps interviews.

What is blue-green deployment and when would you use it?

Blue-green deployment maintains two application environments. One serves live traffic while the other receives the candidate release. After validation, the routing layer shifts traffic to the candidate. The switch may be fast, but the environments are rarely perfectly identical once shared databases, queues, caches, or external side effects are involved.

flowchart TB
    subgraph environments["Environments"]
        direction LR
        B["Blue<br/>(current)"]
        G["Green<br/>(new)"]
    end
 
    LB["Load Balancer"]
 
    LB --> B
    LB -.->|"switch"| G

The process:

  1. Blue is live, Green is idle
  2. Deploy new version to Green
  3. Test Green thoroughly
  4. Switch load balancer to Green
  5. Keep Blue available as a fallback while compatibility permits it

Advantages:

  • Fast application rollback by switching traffic back when data remains compatible
  • Full testing of production environment before traffic
  • Potentially no application downtime with correct routing and readiness checks

Disadvantages:

  • Double infrastructure cost
  • Database schema changes are complex
  • State synchronization between environments

What is canary deployment and how does it differ from blue-green?

Canary deployment gradually routes traffic to the new version, starting with a small percentage and increasing if metrics look healthy.

flowchart TB
    LB["Load Balancer"]
 
    subgraph versions["Traffic Split"]
        direction LR
        C["Current<br/>Version"]
        K["Canary<br/>(new)"]
    end
 
    LB -->|"95%"| C
    LB -->|"5%"| K

The process:

  1. Deploy new version alongside current
  2. Route 5% of traffic to canary
  3. Monitor metrics (errors, latency, business KPIs)
  4. Increase in policy-defined stages when guardrails remain healthy
  5. Halt, disable exposure, roll back, or roll forward if signals degrade

Advantages:

  • Catches issues with minimal user impact
  • Real production traffic testing
  • Gradual confidence building

Disadvantages:

  • Complex traffic routing infrastructure
  • Longer rollout time
  • Need robust monitoring and alerting

What is rolling deployment?

Rolling deployment updates instances one at a time (or in small batches) until all run the new version. It's simpler than blue-green or canary but has slower rollback.

The process:

  1. Take one instance out of the load balancer
  2. Update it to new version
  3. Health check, return to load balancer
  4. Repeat for remaining instances

Advantages:

  • No extra infrastructure needed
  • Gradual rollout
  • Simple to implement

Disadvantages:

  • Slower rollback (must re-roll forward or backward)
  • Mixed versions during deployment
  • Potential issues if old and new versions are incompatible

Production Pipeline Questions

Interviewers often ask you to walk through a complete CI/CD pipeline to assess your understanding of the full picture.

How would you structure a production CI/CD pipeline?

A production pipeline balances speed with safety. Fast feedback in CI, controlled deployment in CD.

name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
env:
  NODE_VERSION: '24'
 
permissions:
  contents: read
 
jobs:
  # ========== CI ==========
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
 
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build
          path: dist/
 
  # ========== CD ==========
  deploy-staging:
    needs: [lint, test, build]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build
          path: dist/
      - name: Deploy to Staging
        run: ./scripts/deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
 
  deploy-production:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production           # Gate applies only when configured in repository settings
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build
          path: dist/
      - name: Deploy to Production
        run: ./scripts/deploy.sh production
        env:
          DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}

Key patterns:

  • Lint, test, and build run in parallel (fast CI feedback)
  • Deploy jobs wait for all CI jobs to pass
  • Staging deploys automatically on main
  • Production can require reviewers or other protection rules configured on the environment
  • The workflow defaults GITHUB_TOKEN to read-only repository contents

For a real release, pin actions to reviewed full commit SHAs, prevent overlapping production deployments with concurrency, authenticate to the target with OIDC where possible, and promote an immutable artifact or container digest. Add provenance or an artifact attestation when consumers need to verify where and how the build was produced.

Why separate build from deploy jobs?

Separating build and deploy enables several important patterns and provides better visibility.

Reuse immutable build artifacts across environments. Build once, identify the artifact by digest, verify it, and promote that exact digest to staging and production. Merely reusing a filename does not establish identity.

Isolate failures. If deployment fails, you know the build was successful. Rerunning just the deploy job is faster than rebuilding.

Different permissions. Build jobs don't need deployment credentials. Deploy jobs don't need repository write access. Principle of least privilege.

Manual gates. You can require approval between build and production deploy without rebuilding.


Reusable Workflows Questions

Reusable workflows reduce duplication across repositories and teams.

How do you avoid duplicating workflow code?

Reusable workflows let you define common patterns once and call them from multiple workflows. They accept inputs and secrets, returning outputs.

Define the reusable workflow:

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
 
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy_token:
        required: true
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v7
      - run: ./deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.deploy_token }}

Call it from another workflow:

# .github/workflows/main.yml
jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
    secrets:
      deploy_token: ${{ secrets.STAGING_TOKEN }}
 
  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
    secrets:
      deploy_token: ${{ secrets.PROD_TOKEN }}

What is the difference between reusable workflows and composite actions?

Both reduce duplication but operate at different levels.

Reusable workflows are workflow files exposed through on: workflow_call and invoked with uses: at the job level. They can contain multiple jobs and declare inputs, secrets, and outputs. The caller supplies the actual trigger and must pass secrets through each level of a nested call chain.

Composite actions package multiple steps into one action step. They cannot define jobs, runner selection, services, or an environment gate. They do not declare a secrets interface, but a caller can deliberately pass a secret as an input or environment variable, so the action must still be treated as privileged code.

# Composite action (action.yml)
runs:
  using: composite
  steps:
    - run: npm ci
      shell: bash
    - run: npm test
      shell: bash

Troubleshooting and Best Practices Questions

Interviewers often ask scenario-based questions about handling failures and maintaining pipelines.

A deployment failed. How do you roll back?

The rollback strategy depends on your deployment approach and infrastructure. Have a plan before you need it.

Option 1: Revert and redeploy

git revert HEAD
git push origin main
# CI/CD automatically deploys the revert

Option 2: Blue-green switch back Switch traffic to the previous environment if schemas, data, sessions, queues, and downstream side effects are still compatible. This can be fast, but it is not a universal undo operation.

Option 3: Kubernetes rollout undo

kubectl rollout undo deployment/app

Option 4: Redeploy a previous immutable artifact Keep known-good digests available and verify provenance before promotion. For irreversible state changes, disable the feature or roll forward instead of pretending an application rollback restores data.

Best practices:

  • Test rollback procedures before you need them
  • Keep at least one previous version deployable
  • Monitor closely after deployments
  • Have runbooks for common failure scenarios
  • Decide separately how to compensate for database writes and external side effects

How do you handle database migrations in CI/CD?

Database migrations are tricky because they can't easily roll back and may conflict with running code. Handle them carefully.

Key principles:

  1. Use expand-and-contract - First add backward-compatible structures, deploy code that can tolerate both representations, migrate data, switch reads and writes, then remove old structures in a later release

  2. Make every rollout phase compatible - A migration may run before, during, or after application rollout depending on the change; both old and new application versions must tolerate the active schema

  3. Serialize schema changes - Run a migration once, with observability and an explicit owner, rather than from every application replica. A protected job is one possible implementation:

jobs:
  migrate:
    runs-on: ubuntu-latest
    environment: production-db        # Separate approval
    steps:
      - run: npm run migrate
 
  deploy:
    needs: migrate
    # ...
  1. Back up and rehearse recovery - A backup is not a rollback unless restore time and data-loss objectives are tested. Prefer forward fixes for large or destructive migrations, and never assume blue-green application environments duplicate the database.

What are common CI/CD pipeline anti-patterns?

Avoiding these patterns keeps pipelines maintainable and reliable.

Unmeasured pipelines - A fixed duration is not universally bad. Measure queue time and the critical path, put fast high-signal checks first, then shard or parallelize without overwhelming shared dependencies.

No failing tests - A pipeline that always passes isn't testing anything. Green means confidence, not just completion.

Hardcoded values - Environment-specific values like URLs or credentials should be secrets or environment variables, not in code.

No artifact identity or promotion - Build once, record its digest and provenance, and deploy that immutable artifact everywhere instead of rebuilding per environment.

Manual steps in "automated" pipeline - If someone must SSH in to complete deployment, it's not truly automated. Automate or document why not.


Quick Reference

ConceptPurpose
WorkflowYAML file defining automated process
JobUnit scheduled on a runner environment
StepSequential task within a job
ActionReusable unit (uses: owner/repo@version)
SecretSensitive value scoped by platform policy
ArtifactIntentional output stored or passed between jobs
MatrixRun same job with different configs
EnvironmentDeployment target with optional protection rules and secrets
CacheOpportunistic reusable input selected by keys and scope

Frequently Asked Questions

What is the difference between CI and CD?

Continuous Integration merges small changes frequently and validates them with automated feedback. Continuous Delivery keeps every accepted change releasable; a business or governance decision may still control production release. Continuous Deployment automatically releases every change that satisfies the pipeline's policy.

What are GitHub Actions workflows, jobs, and steps?

A workflow is a YAML file in .github/workflows triggered by one or more events. It contains jobs that run independently unless needs defines dependencies. Each job targets a runner environment, while its steps run sequentially in that job and share its workspace.

What is the difference between blue-green and canary deployments?

Blue-green prepares a second environment and shifts traffic to it, making application rollback fast when data and dependencies remain compatible. Canary exposes the new version to a small cohort first and expands only when technical and business signals are healthy. Neither strategy makes database or external side effects automatically reversible.

How do you handle secrets in CI/CD pipelines?

Avoid long-lived credentials where OIDC can issue short-lived cloud access. Store unavoidable values in the platform's scoped secret store, grant the GITHUB_TOKEN and cloud role the minimum permissions, restrict deployment environments, never run untrusted code with privileged secrets, and treat log masking as a safeguard rather than a guarantee.

What is a build matrix in GitHub Actions?

A matrix expands one job definition into combinations such as Node 22, 24, and 26 across several operating systems. include and exclude customize combinations, fail-fast controls cancellation after failure, and max-parallel limits concurrent matrix jobs.

How do you speed up CI/CD pipelines?

Measure the critical path first, then parallelize independent work, cache package-manager downloads with lockfile-based keys, avoid rebuilding promoted artifacts, and run affected tests only with a reliable fallback. Path filters and self-hosted runners add correctness and security tradeoffs, so optimize from evidence rather than blindly skipping work.

Sources


Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides