How to set up a CI/CD pipeline with GitHub Actions
Shipping code confidently means knowing it has been built, tested, and deployed automatically every time you push. That’s the promise of a CI/CD pipeline — and GitHub Actions makes it easy to set one up without ever leaving your repository.
In this post you’ll go from zero to a working pipeline: you’ll create a repository, write a workflow file, and have automated tests running on every push and pull request.
What is CI/CD and why GitHub Actions?
CI (Continuous Integration) is the practice of automatically building and testing code every time a change is pushed to a shared branch. CD (Continuous Delivery / Deployment) extends that by automatically delivering a verified build to a staging or production environment.
GitHub Actions is GitHub’s built-in automation platform. Workflows live inside your repository as YAML files, run on GitHub-hosted virtual machines (called runners), and are free for public repositories and within generous limits for private ones.
Key concepts you’ll encounter:
- Workflow — an automated process defined in a
.ymlfile under.github/workflows/ - Event — a trigger that starts a workflow (e.g.
push,pull_request,schedule) - Job — a set of steps that run on the same runner machine
- Step — a single shell command or a pre-built action
- Action — a reusable unit of automation (e.g.
actions/checkout,actions/setup-node)
Step 1: Create a GitHub repository
Start by creating a new repository on GitHub — or use an existing one. If you’re starting from scratch:
# Initialise a local project and push it to GitHub
git init my-project
cd my-project
git remote add origin https://github.com/<your-username>/my-project.git
Make sure your repository has a default branch (usually main) before adding any workflow, since the workflow trigger below listens to pushes on that branch.
💡 Tip: If your project already has tests (e.g.
npm test,pytest,go test) you’ll see the most value from a CI pipeline immediately.
Step 2: Create the workflow file
GitHub Actions picks up any .yml file inside the .github/workflows/ directory automatically. Create that directory and add your first workflow:
mkdir -p .github/workflows
touch .github/workflows/ci.yml
Open ci.yml and add the following pipeline definition. This example targets a Node.js project, but the structure is the same for any language — only the setup action and run commands change.
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
Here’s what each block does:
on— defines when this workflow runs. The pipeline activates on every push tomainand on every pull request targetingmain.jobs.build-and-test— a single job that runs on a fresh Ubuntu virtual machine provided by GitHub.actions/checkout@v4— clones your repository into the runner so subsequent steps can access your code.actions/setup-node@v4— installs the specified Node.js version and makesnpmavailable.npm ci— installs dependencies frompackage-lock.jsonfor a clean, reproducible build.npm test/npm run build— your project’s test and build commands. Swap these forpytest,go build,mvn test, etc. as needed.
Step 3: Commit and trigger the pipeline
Push your new workflow file and watch the pipeline run:
git add .github/workflows/ci.yml
git commit -m "ci: add GitHub Actions CI/CD pipeline"
git push origin main
Navigate to your repository on GitHub and click the Actions tab. You’ll see the workflow appear and begin running within seconds. Each step updates in real time, and the overall job goes green (or red) once it finishes.
💡 Tip: A red check on a pull request will block merging when branch protection rules are configured — a great way to enforce quality gates without manual review.
Step 4: Extend your pipeline
Once the basic pipeline is green, here are common next steps:
Add a deploy stage
To deploy only after all tests pass, add a second job that depends on the first:
deploy:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to production
run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: $
The needs keyword enforces ordering — deploy only starts when build-and-test succeeds. Sensitive credentials are stored in GitHub Secrets (Settings → Secrets and variables → Actions) and injected at runtime via $, keeping them out of your source code.
Cache dependencies for faster runs
Repeated npm ci calls download the same packages every time. The cache action skips that:
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: $-node-$
restore-keys: |
$-node-
Add this step before the install step to cut build times significantly on large projects.
Run against multiple Node.js versions
Use a matrix strategy to test compatibility across versions:
strategy:
matrix:
node-version: ["18", "20", "22"]
GitHub Actions will spin up a parallel job for each matrix entry automatically.
Conclusion
You now have a fully automated CI/CD pipeline that builds and tests your code on every push and pull request. The workflow file is version-controlled alongside your code, reviewable in pull requests, and free to iterate on.
From here you can add linting, security scanning, Docker image builds, or deployment steps — all using the same YAML structure. Explore the GitHub Actions Marketplace for thousands of pre-built actions to slot into your pipeline.
References: