CI/CD With GitHub Actions

Spread the love
CI/CD with GitHub Actions β€” The Complete Guide
CI/CD Β· Automation Β· Beginner β†’ Interview-Ready

Ship faster with GitHub Actions.

Automate your build, test, and deploy pipeline β€” from your very first workflow file to matrix builds, secrets, reusable workflows, and production deploys.

17 chapters 25+ workflow examples 18 interview Q&A Copy-ready YAML
Jump to a chapter
01 β€” The problem

What is CI/CD?

Every time you write code, three things eventually have to happen: it gets built, it gets tested, and it gets deployed. CI/CD is about making a machine do all three, automatically, every time you push.

Picture the old way: a developer finishes a feature, manually runs the build on their laptop, manually runs some tests (maybe), zips it up, and copies it to a server by hand. Multiply that across a team and you get integration nightmares, “it worked on my machine” deployments, broken releases discovered by users, and hours lost to repetitive manual steps. CI/CD replaces all of that with an automated pipeline.

CI β€” Continuous Integration
Every push automatically builds the code and runs the test suite, so integration problems are caught in minutes, not weeks.
CD β€” Continuous Delivery/Deployment
Once code passes, it’s automatically packaged and released β€” to a staging environment, or all the way to production.

What a pipeline gives you

  • Fast feedback β€” you learn a change broke something within minutes of pushing it, while it’s still fresh.
  • Consistency β€” the build and deploy happen the same way every time, on a clean machine, removing “works on my machine.”
  • Confidence to ship β€” automated tests gate every release, so you deploy small changes often instead of scary big-bang releases.
  • Less toil β€” no more manual builds, copies, and checklists. Humans do the thinking; the pipeline does the repetition.
In one sentenceCI/CD is an automated pipeline that builds, tests, and ships your code every time it changes β€” turning release day from an event into a non-event.
02 β€” The distinction

CI vs Continuous Delivery vs Continuous Deployment

Three terms, often blurred together, that interviewers love to probe. The difference comes down to how far automation goes β€” and where a human still presses the button.

TermWhat’s automatedWho deploys to production
Continuous IntegrationBuild + test on every push/PRNobody yet β€” CI stops at “it’s verified”
Continuous DeliveryBuild, test, and prepare a release that’s ready to deployA human clicks “deploy” when they choose
Continuous DeploymentEverything β€” build, test, and release to productionNobody β€” every passing change ships automatically

So they stack on top of each other. Continuous Integration is the foundation: merge often, and let a machine build and test each change. Continuous Delivery adds the guarantee that main is always in a deployable state, with the actual production release kept as a manual, one-click decision. Continuous Deployment removes even that click β€” if the pipeline is green, it goes live.

The memorable versionContinuous Delivery = ready to deploy at any moment (human approves). Continuous Deployment = deployed automatically (no human in the loop). Same “CD” abbreviation, one word of difference, big change in risk appetite.
03 β€” The tool

Why GitHub Actions?

CI/CD is a concept; you need a tool to run it. GitHub Actions is that tool built directly into GitHub β€” no separate server to babysit, triggered by the events already happening in your repo.

Historically, teams ran a separate CI server like Jenkins β€” powerful, but something you had to host, secure, and maintain. GitHub Actions folds CI/CD into the place your code already lives. Push a commit, open a pull request, or cut a release, and a workflow runs automatically on machines GitHub provides.

Built into GitHub
No external service to wire up. Workflows live in your repo as YAML files and react to repo events directly.
Event-driven
Trigger on push, PR, schedule, release, manual click, and dozens more β€” CI/CD that fits your actual workflow.
Marketplace of actions
Thousands of reusable building blocks β€” check out code, set up languages, deploy to clouds β€” with one line.
Hosted runners
GitHub provides clean Linux, Windows, and macOS machines on demand. A generous free tier for public repos.

How it compares to the alternatives

ToolIn a nutshell
GitHub ActionsCI/CD native to GitHub; YAML workflows, hosted runners, huge marketplace.
GitLab CI/CDEqually native β€” but to GitLab. Excellent if your code is on GitLab.
JenkinsThe veteran. Self-hosted, endlessly flexible via plugins, but you maintain it.
CircleCI / Travis CIStandalone cloud CI services that connect to your Git host.
Bottom lineIf your code is on GitHub, Actions is the path of least resistance: nothing to install, it reacts to your repo’s events, and the marketplace means you rarely write automation from scratch.
04 β€” Architecture

Core concepts & architecture

GitHub Actions has exactly six moving parts. Once you can name them and see how they nest, every workflow file becomes readable.

Event β†’ Workflow β†’ Jobs (on Runners) β†’ Steps β†’ Actions

Workflow
An automated process defined in a YAML file under .github/workflows/. A repo can have many.
Event
What triggers a workflow β€” a push, a pull request, a schedule, a manual click, and more.
Job
A set of steps that run together on one runner. Jobs run in parallel by default.
Step
A single task inside a job β€” either running a shell command or using an action.
Action
A reusable, packaged unit of work (e.g. “check out my code”) that a step can call with uses.
Runner
The machine that executes a job β€” GitHub-hosted (Linux/Win/macOS) or one you host yourself.
The mental modelAn event triggers a workflow. The workflow contains jobs, each running on a runner. Each job runs steps, and steps either run a command or use an action. That’s the whole system.
05 β€” Hello, pipeline

Your first workflow

A workflow is just a YAML file in a special folder. Add one, push it, and GitHub starts running it automatically. Let’s build the simplest possible one.

Where workflows live

Every workflow is a .yml file inside the .github/workflows/ directory at the root of your repo. GitHub watches that folder β€” any workflow file it finds becomes active.

.github/workflows/hello.yml
name: Hello Pipeline

# WHEN should this run?
on: [push]

# WHAT should it do?
jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello from GitHub Actions! πŸš€"

That’s a complete, working pipeline. Commit it, push, and open the Actions tab of your repo β€” you’ll see the run appear, the job execute on a fresh Ubuntu machine, and a green check when it finishes.

Reading it line by line

  • name β€” a friendly label shown in the Actions tab.
  • on β€” the event(s) that trigger it. Here, any push.
  • jobs β€” the work to do. This one has a single job called greet.
  • runs-on β€” the runner (machine) to use β€” a GitHub-hosted Ubuntu box.
  • steps β€” the ordered tasks. This one just runs an echo command.
YAML is whitespace-sensitiveIndentation is meaningful in YAML β€” use spaces, never tabs, and keep it consistent. A single misaligned line is the most common reason a workflow won’t run.
06 β€” The building blocks

Workflow syntax, explained

Almost every workflow you’ll ever read is made of the same handful of keywords. Learn these and you can write real pipelines.

.github/workflows/ci.yml β€” a realistic Node build
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4          # pull the repo onto the runner

      - name: Set up Node
        uses: actions/setup-node@v4
        with:                             # inputs for the action
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test
KeywordWhat it does
nameDisplay name of the workflow (or a step).
onThe event(s) that trigger the workflow, with optional filters.
jobsThe map of jobs to run.
runs-onWhich runner/OS the job uses.
stepsThe ordered list of tasks in a job.
usesRun a prebuilt action (from the Marketplace or a repo).
runRun a shell command directly on the runner.
withPass input parameters to an action.
envSet environment variables for a step, job, or whole workflow.
uses vs runuses pulls in someone’s packaged action (“check out my code,” “set up Python”). run executes raw shell commands you write yourself. Most steps are one or the other.
07 β€” The triggers

Events & triggers

The on key decides when a workflow runs. GitHub exposes dozens of events β€” here are the ones you’ll actually use, and how to filter them.

push
Runs when commits are pushed. The workhorse trigger for CI.
pull_request
Runs on PR open/update β€” perfect for gating merges with tests.
schedule
Runs on a cron timer β€” nightly builds, dependency checks, cleanups.
workflow_dispatch
Adds a manual “Run workflow” button in the UI.
release
Runs when you publish a release β€” great for deploy pipelines.
workflow_call
Lets other workflows call this one β€” reusable pipelines.
filters, schedules & manual runs
on:
  push:
    branches: [main, "release/**"]   # only these branches
    paths: ["src/**"]                # only when these files change
    tags: ["v*"]                      # and version tags

  schedule:
    - cron: "0 6 * * 1"              # every Monday at 06:00 UTC

  workflow_dispatch:                   # manual button in the Actions tab
Save minutesUse branches and paths filters so workflows only run when they need to. A docs-only change shouldn’t trigger your full test matrix.
08 β€” Where work happens

Jobs, runners & dependencies

Jobs are the unit of parallelism. By default they all run at once, on separate machines β€” but you can chain them into a sequence when order matters.

Runners

runs-on picks the machine. GitHub-hosted runners give you a clean environment every time:

  • ubuntu-latest β€” the fast, cheap default; use it unless you have a reason not to.
  • windows-latest β€” for Windows-specific builds and tests.
  • macos-latest β€” for iOS/macOS builds (more expensive).
  • Self-hosted runners β€” your own machines, for special hardware, private networks, or heavy workloads.

Sequencing jobs with needs

Jobs run in parallel unless you declare a dependency. needs makes one job wait for another β€” the classic “test before deploy” chain.

build β†’ test β†’ deploy, in order
jobs:
  build:
    runs-on: ubuntu-latest
    steps: [...]

  test:
    needs: build              # waits for build to succeed
    runs-on: ubuntu-latest
    steps: [...]

  deploy:
    needs: test               # only runs if test passed
    runs-on: ubuntu-latest
    steps: [...]
Fan-out, fan-inneeds can take a list β€” needs: [lint, test] β€” so a deploy job waits for several jobs at once. This lets you run independent checks in parallel, then converge.
09 β€” Test everywhere at once

Matrix builds

Need to test across Node 18, 20, and 22 β€” on Linux, Windows, and macOS? A matrix runs every combination in parallel from a few lines of config.

test across versions & operating systems
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false          # let all combos finish even if one fails
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

That single job expands into six parallel runs (2 operating systems Γ— 3 Node versions). You can also include extra one-off combinations or exclude ones that don’t make sense.

fail-fastBy default a matrix is fail-fast β€” one failure cancels the rest. Set fail-fast: false when you want to see every combination’s result, which is usually what you want while debugging.
10 β€” Reusable building blocks

Actions & the Marketplace

An “action” is a packaged, shareable step. Instead of scripting common tasks yourself, you pull them in with one uses line β€” and the Marketplace has thousands.

Actions you’ll use constantly

ActionWhat it does
actions/checkout@v4Clones your repo onto the runner. Almost every workflow starts here.
actions/setup-node@v4Installs a specific Node.js version (siblings exist for Python, Java, Go…).
actions/cache@v4Caches dependencies between runs to speed things up.
actions/upload-artifact@v4Saves build outputs so you can download them or pass them on.
docker/build-push-actionBuilds a Docker image and pushes it to a registry.

How to call one

uses + with
- name: Set up Python
  uses: actions/setup-python@v5   # owner/repo@version
  with:                              # inputs the action accepts
    python-version: "3.12"
    cache: "pip"

Types of actions

  • JavaScript actions β€” run directly on the runner; fast and common.
  • Docker container actions β€” run inside a container, bundling their own environment.
  • Composite actions β€” bundle several steps into one reusable action you define yourself.
Pin your versionsAlways pin actions to a version (@v4) β€” or, for maximum safety, a full commit SHA. Referencing a floating branch means someone else’s update could silently change (or compromise) your pipeline.
11 β€” Handling sensitive data

Secrets, variables & environments

Deploys need credentials β€” API keys, tokens, passwords. You must never hardcode them in a workflow file. GitHub gives you encrypted secrets instead.

Secrets vs variables

  • Secrets are encrypted, hidden in logs, and used for anything sensitive. Set them in Settings β†’ Secrets and variables β†’ Actions.
  • Variables are plain configuration values (non-sensitive), like a region name or feature flag.
using a secret
steps:
  - name: Deploy
    env:
      API_TOKEN: ${{ secrets.API_TOKEN }}   # injected securely
    run: ./deploy.sh
Never print a secretSecrets are masked in logs automatically, but don’t fight that β€” never echo a secret or write it to a file that gets uploaded as an artifact. Treat them as write-only.

GITHUB_TOKEN

Every workflow run automatically gets a temporary GITHUB_TOKEN to interact with your repo β€” commenting on PRs, pushing tags, creating releases. It expires when the run ends, and you should scope it with least privilege:

least-privilege permissions
permissions:
  contents: read          # start read-only...
  pull-requests: write    # ...grant only what you need

Environments

An environment (like staging or production) groups deployment settings and adds protection rules β€” most usefully, required reviewers who must approve before a deploy job runs. This is how you turn Continuous Delivery’s “human clicks deploy” into an enforced gate.

a gated production deploy
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production     # requires approval if rules are set
    steps: [...]
Cloud credentials, the modern wayInstead of storing long-lived cloud keys as secrets, use OIDC β€” your workflow requests a short-lived token from AWS/GCP/Azure at run time. Nothing sensitive is stored, and there’s nothing to leak or rotate.
12 β€” Speed & outputs

Caching & artifacts

Two features that look similar but solve opposite problems: caching makes runs faster; artifacts move files out of a run.

Caching β€” reuse dependencies between runs

Re-downloading every dependency on each run is slow. A cache stores them keyed by your lockfile, so unchanged dependencies are restored in seconds. Many setup actions cache automatically; you can also do it explicitly:

cache npm dependencies
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

Artifacts β€” save & pass files

An artifact is a file (or folder) you save from a run β€” a build output, a test report, a coverage file. Upload it to download later, or to hand it to a downstream job.

upload in one job, download in another
# In the build job:
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/

# In a later job (needs: build):
- uses: actions/download-artifact@v4
  with:
    name: dist
Cache vs artifactCache = a speed optimization, transparently restored, may be evicted. Artifact = a deliberate output you want to keep or pass along. Don’t use one for the other’s job.
13 β€” Dynamic workflows

Expressions, contexts & conditionals

The ${{ }} syntax is how workflows become dynamic β€” reading data about the run and making decisions.

Contexts β€” data about the run

Inside ${{ }} you can read contexts: structured data GitHub exposes. The ones you’ll use most:

github
Info about the event β€” github.ref, github.sha, github.actor, github.event_name.
secrets
Your encrypted secrets β€” secrets.API_TOKEN.
matrix
The current matrix combination β€” matrix.node.
needs
Outputs from jobs this one depended on β€” needs.build.outputs.x.
env
Environment variables in scope β€” env.NODE_ENV.
steps
Outputs from earlier steps β€” steps.id.outputs.value.

Conditionals with if

An if on a step or job decides whether it runs. Combine contexts with status functions like success(), failure(), and always().

run only on main, and only if earlier steps passed
- name: Deploy to production
  if: ${{ github.ref == 'refs/heads/main' && success() }}
  run: ./deploy.sh

- name: Notify on failure
  if: ${{ failure() }}          # runs only if something failed
  run: ./alert.sh
Handy functionssuccess() (default), failure(), always() (run no matter what), and helpers like contains(), startsWith(), and hashFiles() cover almost every condition you’ll need.
14 β€” Copy, adapt, ship

Real-world workflows

Complete, working pipelines for the jobs you’ll actually automate. Drop them into .github/workflows/ and adjust.

Node CI

Lint, build & test on every PR

.github/workflows/ci.yml
name: CI
on:
  pull_request:
  push: { branches: [main] }
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npm run lint
      - run: npm run build --if-present
      - run: npm test
Docker

Build an image & push it to a registry

Ties into your Docker knowledge β€” build on every push to main, then push to GitHub Container Registry.

.github/workflows/docker.yml
name: Docker
on: { push: { branches: [main] } }
permissions: { contents: read, packages: write }
jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
Deploy

Deploy a static site to GitHub Pages

.github/workflows/deploy.yml
name: Deploy
on: { push: { branches: [main] } }
permissions: { pages: write, id-token: write }
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-pages-artifact@v3
        with: { path: dist }
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: github-pages
    steps:
      - uses: actions/deploy-pages@v4
15 β€” Craft & safety

Best practices & security

The habits that separate a fragile pipeline from a professional one β€” most of them about speed, reuse, and not leaking anything.

Pin action versions
Reference @v4 or a full commit SHA β€” never a moving branch that could change under you.
Least-privilege tokens
Set permissions: to the minimum. Start read-only and grant write only where needed.
Never hardcode secrets
Always use encrypted secrets or OIDC. Nothing sensitive in the YAML, ever.
Cache dependencies
Use built-in caching to cut minutes off every run.
Cancel stale runs
Use concurrency to auto-cancel superseded runs on the same branch/PR.
Fail fast, keep it small
Put quick checks (lint) first, keep jobs focused, and filter triggers by path.
Reuse, don’t repeat
Extract shared logic into reusable workflows (workflow_call) or composite actions.
Beware pull_request_target
It runs with write access on PRs from forks β€” a known injection risk. Use it only when you must, and carefully.
cancel superseded runs
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
Third-party actions are code you runAn action from the Marketplace executes with access to your runner and, potentially, your secrets. Vet what you use, prefer verified publishers, and pin to a commit SHA for anything sensitive.
16 β€” Reference

Cheat sheet

The syntax, triggers, and CLI commands worth keeping within reach. Bookmark this section.

Common triggers (the on: key)
on: [push, pull_request]
Run on pushes and PRs (most common).
on: push: branches: [main]
Only pushes to specific branches.
on: push: paths: [“src/**”]
Only when matching files change.
on: schedule: – cron: “0 6 * * 1”
On a timer (Mon 06:00 UTC).
on: workflow_dispatch
Adds a manual “Run workflow” button.
on: release: types: [published]
When a release is published.
Job & step keys
runs-on: ubuntu-latest
Choose the runner OS.
needs: [build, test]
Wait for other jobs first.
strategy: matrix:
Run combinations in parallel.
uses: actions/checkout@v4
Run a prebuilt action.
run: npm test
Run a shell command.
with: { key: value }
Pass inputs to an action.
env: { NODE_ENV: production }
Set environment variables.
if: ${{ github.ref == ‘refs/heads/main’ }}
Run a step/job conditionally.
Frequently used contexts
${{ github.sha }}
The commit SHA that triggered the run.
${{ github.ref }}
The branch/tag ref.
${{ github.repository }}
owner/repo name.
${{ secrets.NAME }}
An encrypted secret.
${{ matrix.node }}
Current matrix value.
${{ runner.os }}
The runner’s OS (Linux/Windows/macOS).
GitHub CLI (gh)
gh workflow list
List workflows in the repo.
gh workflow run ci.yml
Manually trigger a workflow.
gh run list
Show recent runs and their status.
gh run watch
Follow a run live in the terminal.
gh run view <id> –log
View a run’s logs.
17 β€” Interview prep

Interview questions

The CI/CD and GitHub Actions questions that come up again and again, with concise answers you can say out loud. Tap any question to reveal the answer.

What is CI/CD?
An automated pipeline that builds, tests, and ships code on every change. CI (Continuous Integration) means frequently merging and automatically building/testing. CD means automatically delivering or deploying the verified result.
Continuous Delivery vs Continuous Deployment?
Both automate everything up to production. With Continuous Delivery, the release is always ready but a human approves the final deploy. With Continuous Deployment, there’s no human gate β€” every passing change goes live automatically.
What is GitHub Actions?
GitHub’s built-in CI/CD platform. You define event-driven workflows in YAML files under .github/workflows/, and they run automatically on GitHub-hosted (or self-hosted) runners in response to repo events.
Explain workflow, job, step, and action.
A workflow is the whole automated process (a YAML file). It contains jobs, which run on runners. Each job has ordered steps, and a step either runs a shell command or uses an action β€” a reusable packaged unit of work.
Where do workflow files live?
In the .github/workflows/ directory at the root of the repository, as .yml (or .yaml) files. GitHub automatically detects and runs them.
Do jobs run in parallel or sequence?
In parallel by default, each on its own runner. Use needs to make a job wait for others β€” e.g. deploy with needs: test only runs after tests pass.
What is a runner?
The machine that executes a job. GitHub-hosted runners (Linux, Windows, macOS) are clean VMs provisioned per run; self-hosted runners are your own machines, used for special hardware, private networks, or heavy loads.
What is a matrix build?
A strategy that runs a job across multiple combinations of parameters (e.g. several OSes Γ— language versions) in parallel, defined once under strategy.matrix. Great for cross-version/cross-platform testing.
How do you handle secrets?
Store them as encrypted secrets in repo/org/environment settings and reference them with ${{ secrets.NAME }}. They’re masked in logs. Never hardcode credentials in the YAML.
What is GITHUB_TOKEN?
A temporary, auto-generated token each run gets for interacting with the repo (comment on PRs, push tags, etc.). It expires when the run ends. Scope it with the permissions key using least privilege.
Caching vs artifacts β€” what’s the difference?
Caching speeds up runs by restoring dependencies between them (transparent, may be evicted). Artifacts deliberately save files out of a run (build outputs, reports) to download or pass to a later job.
How do you trigger a workflow manually?
Add the workflow_dispatch event, which puts a “Run workflow” button in the Actions tab. You can also trigger it via the API or gh workflow run.
How do you run a step conditionally?
With an if expression, e.g. if: ${{ github.ref == 'refs/heads/main' }}. Combine with status functions like success(), failure(), or always().
uses vs run?
uses runs a prebuilt action (referenced as owner/repo@version). run executes shell commands you write directly on the runner.
What are reusable workflows?
Workflows defined with the workflow_call trigger that other workflows can call, passing inputs and secrets. They let you share one pipeline definition across many repos or workflows instead of copy-pasting.
Why pin action versions to a SHA?
A tag or branch can be moved or compromised by the action’s author; a full commit SHA is immutable. Pinning to a SHA guarantees you run exactly the code you reviewed β€” important for supply-chain security.
How would you speed up a slow pipeline?
Cache dependencies, run independent jobs in parallel, use path/branch filters so workflows only run when relevant, put fast checks first, use concurrency to cancel stale runs, and split long jobs.
GitHub Actions vs Jenkins?
Actions is native to GitHub, event-driven, and needs no server to maintain, with a large marketplace. Jenkins is self-hosted and extremely flexible via plugins but requires you to host, secure, and maintain it. Actions wins on convenience when your code is on GitHub.

Best prep of all: create a throwaway repo, add a workflow, and watch it run in the Actions tab. Seeing a green check (and debugging a red X) teaches more than any list.