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
- CI/CD Fundamentals Questions
- GitHub Actions Core Questions
- Workflow Triggers Questions
- Job Dependencies and Parallelism Questions
- Matrix Builds Questions
- Secrets and Environment Variables Questions
- Caching and Performance Questions
- Artifacts and Job Communication Questions
- Deployment Strategies Questions
- Production Pipeline Questions
- Reusable Workflows Questions
- 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
endWhy 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 lintWhat 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 triggerCommon triggers:
push/pull_request- Code changesschedule- Cron jobs (nightly builds, cleanup)workflow_dispatch- Manual runs with inputsworkflow_call- Called by other workflowsrelease- 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-productionHow 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.shflowchart LR
lint --> deploy
test --> deploy
security-scan --> deployLint, 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.shConditional options:
success()- Previous jobs succeeded (default)failure()- At least one previous job failedalways()- Run regardless of previous job statuscancelled()- 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 testThis 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 combinationIncluding 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 comboYou 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.shBest practices:
- Prefer OIDC and short-lived cloud credentials over stored access keys
- Give
GITHUB_TOKENexplicit least-privilegepermissions, 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-bucketAWS, 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 testThe 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 dataWhat 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:
~/.npmor another package-manager download store (prefer this to restoringnode_modulesblindly)~/.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 lintUse path filters only after modeling dependency boundaries and required checks:
on:
push:
paths:
- 'src/**' # Only run for source changesUse shallow clones when you don't need full history:
- uses: actions/checkout@v7
with:
fetch-depth: 1 # Only latest commitRun 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"| GThe process:
- Blue is live, Green is idle
- Deploy new version to Green
- Test Green thoroughly
- Switch load balancer to Green
- 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%"| KThe process:
- Deploy new version alongside current
- Route 5% of traffic to canary
- Monitor metrics (errors, latency, business KPIs)
- Increase in policy-defined stages when guardrails remain healthy
- 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:
- Take one instance out of the load balancer
- Update it to new version
- Health check, return to load balancer
- 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_TOKENto 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: bashTroubleshooting 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 revertOption 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/appOption 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:
-
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
-
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
-
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
# ...- 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
| Concept | Purpose |
|---|---|
| Workflow | YAML file defining automated process |
| Job | Unit scheduled on a runner environment |
| Step | Sequential task within a job |
| Action | Reusable unit (uses: owner/repo@version) |
| Secret | Sensitive value scoped by platform policy |
| Artifact | Intentional output stored or passed between jobs |
| Matrix | Run same job with different configs |
| Environment | Deployment target with optional protection rules and secrets |
| Cache | Opportunistic 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
- Workflow syntax for GitHub Actions
- GitHub-hosted runners reference
- Secure use reference for GitHub Actions
- OpenID Connect reference
- Deployments and environments
- Caching dependencies to speed up workflows
- Store and share data with workflow artifacts
- Artifact attestations
- Reuse workflows
- actions/checkout releases
- actions/setup-node
Related Articles
- Complete DevOps Engineer Interview Guide - Comprehensive DevOps interview preparation
- Docker Interview Guide - Building images in CI pipelines
- Kubernetes Interview Guide - Deploying to K8s from CI/CD
- Linux Commands Interview Guide - Shell commands used in pipeline steps
- Git Rebase vs Merge Interview Guide - Version control workflows that trigger CI
