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.
Jump to a chapter
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.
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.
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.
| Term | What’s automated | Who deploys to production |
|---|---|---|
| Continuous Integration | Build + test on every push/PR | Nobody yet β CI stops at “it’s verified” |
| Continuous Delivery | Build, test, and prepare a release that’s ready to deploy | A human clicks “deploy” when they choose |
| Continuous Deployment | Everything β build, test, and release to production | Nobody β 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.
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.
How it compares to the alternatives
| Tool | In a nutshell |
|---|---|
| GitHub Actions | CI/CD native to GitHub; YAML workflows, hosted runners, huge marketplace. |
| GitLab CI/CD | Equally native β but to GitLab. Excellent if your code is on GitLab. |
| Jenkins | The veteran. Self-hosted, endlessly flexible via plugins, but you maintain it. |
| CircleCI / Travis CI | Standalone cloud CI services that connect to your Git host. |
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
.github/workflows/. A repo can have many.uses.run a command or use an action. That’s the whole system.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.
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, anypush.jobsβ the work to do. This one has a single job calledgreet.runs-onβ the runner (machine) to use β a GitHub-hosted Ubuntu box.stepsβ the ordered tasks. This one just runs anechocommand.
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.
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
| Keyword | What it does |
|---|---|
name | Display name of the workflow (or a step). |
on | The event(s) that trigger the workflow, with optional filters. |
jobs | The map of jobs to run. |
runs-on | Which runner/OS the job uses. |
steps | The ordered list of tasks in a job. |
uses | Run a prebuilt action (from the Marketplace or a repo). |
run | Run a shell command directly on the runner. |
with | Pass input parameters to an action. |
env | Set environment variables for a step, job, or whole workflow. |
uses 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.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.
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
branches and paths filters so workflows only run when they need to. A docs-only change shouldn’t trigger your full test matrix.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.
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: [...]
needs 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.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.
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-fast: false when you want to see every combination’s result, which is usually what you want while debugging.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
| Action | What it does |
|---|---|
actions/checkout@v4 | Clones your repo onto the runner. Almost every workflow starts here. |
actions/setup-node@v4 | Installs a specific Node.js version (siblings exist for Python, Java, Goβ¦). |
actions/cache@v4 | Caches dependencies between runs to speed things up. |
actions/upload-artifact@v4 | Saves build outputs so you can download them or pass them on. |
docker/build-push-action | Builds a Docker image and pushes it to a registry. |
How to call one
- 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.
@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.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.
steps: - name: Deploy env: API_TOKEN: ${{ secrets.API_TOKEN }} # injected securely run: ./deploy.sh
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:
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.
jobs: deploy: runs-on: ubuntu-latest environment: production # requires approval if rules are set steps: [...]
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:
- 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.
# 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
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.ref, github.sha, github.actor, github.event_name.secrets.API_TOKEN.matrix.node.needs.build.outputs.x.env.NODE_ENV.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().
- 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
success() (default), failure(), always() (run no matter what), and helpers like contains(), startsWith(), and hashFiles() cover almost every condition you’ll need.Real-world workflows
Complete, working pipelines for the jobs you’ll actually automate. Drop them into .github/workflows/ and adjust.
Lint, build & test on every PR
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
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.
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 a static site to GitHub Pages
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
Best practices & security
The habits that separate a fragile pipeline from a professional one β most of them about speed, reuse, and not leaking anything.
@v4 or a full commit SHA β never a moving branch that could change under you.permissions: to the minimum. Start read-only and grant write only where needed.concurrency to auto-cancel superseded runs on the same branch/PR.workflow_call) or composite actions.concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
Cheat sheet
The syntax, triggers, and CLI commands worth keeping within reach. Bookmark this section.
on: key)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?
Continuous Delivery vs Continuous Deployment?
What is GitHub Actions?
.github/workflows/, and they run automatically on GitHub-hosted (or self-hosted) runners in response to repo events.Explain workflow, job, step, and action.
Where do workflow files live?
.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?
needs to make a job wait for others β e.g. deploy with needs: test only runs after tests pass.What is a runner?
What is a matrix build?
strategy.matrix. Great for cross-version/cross-platform testing.How do you handle secrets?
${{ secrets.NAME }}. They’re masked in logs. Never hardcode credentials in the YAML.What is GITHUB_TOKEN?
permissions key using least privilege.Caching vs artifacts β what’s the difference?
How do you trigger a workflow manually?
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?
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?
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?
How would you speed up a slow pipeline?
concurrency to cancel stale runs, and split long jobs.GitHub Actions vs Jenkins?
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.