Skip to content

How to Create Your First GitHub Actions CI Pipeline in 2026: Step-by-Step Guide

I pushed my code to GitHub, and five minutes later, my teammate was complaining that the build was broken. Again. I forgot to run the tests locally. Again.

This was the third time this week. My manager suggested I look into “CI/CD” and “GitHub Actions.” I had no idea what those meant. After a weekend of trial and error, I now have automated tests running on every single commit. Here’s what I learned.

The Problem: Manual Testing is Unreliable

Every developer has been there. You make a quick fix, commit it, push it, and move on. Then someone messages you: “Hey, the build is failing.” You forgot to run the tests. Or the linter. Or both.

This happened to me constantly. I’d get into a flow state, make changes, and completely skip the local verification steps. The result? Broken main branches, angry teammates, and embarrassing “fix the build” commits.

The Solution: GitHub Actions CI Pipeline

GitHub Actions is GitHub’s built-in automation platform. It runs workflows automatically when certain events happen—like pushing code or opening a pull request.

A CI (Continuous Integration) pipeline does three things:

  1. Checks out your code from the repository
  2. Installs dependencies in a clean environment
  3. Runs tests and linters automatically

If any step fails, you know immediately. No more broken builds on main.

My First Attempt (It Failed)

I created a file at .github/workflows/ci.yml:

ci.yml
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test

I pushed it and waited. The workflow ran. And failed. The error: npm: command not found.

The problem? I didn’t set up Node.js in the CI environment. The ubuntu-latest runner has a lot of tools, but it doesn’t have every version of every language pre-installed.

The Working Solution

After reading the documentation, I added the setup-node action:

ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
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"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linter
run: npm run lint

This worked. Here’s what each part does:

Triggers (on):

  • push: branches: [main, develop] — runs when pushing to main or develop
  • pull_request: branches: [main] — runs when opening PRs to main

Steps:

  1. actions/checkout@v4 — downloads your repository code
  2. actions/setup-node@v4 — installs Node.js 20 with npm caching enabled
  3. npm ci — installs dependencies from package-lock.json (faster and more reliable than npm install)
  4. npm test — runs your test suite
  5. npm run lint — runs your linter (if configured)

Why npm ci Instead of npm install?

This was a key learning. npm ci (Clean Install) is designed for CI environments:

CommandBehavior
npm installUpdates lockfile, may install different versions
npm ciUses exact versions from lockfile, fails if lockfile doesn’t match package.json

In CI, you want reproducible builds. npm ci ensures everyone gets the same dependencies.

Adding Python Support

My project also has a Python backend. Here’s the workflow I created:

python-ci.yml
name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests with coverage
run: pytest --cov=. --cov-report=xml

The matrix strategy tests against both Python 3.11 and 3.12 simultaneously. This caught a compatibility bug I wouldn’t have found otherwise.

Common Mistakes I Made

1. Forgetting to cache dependencies

My first builds took 3-4 minutes. Most of that was downloading npm packages. Adding cache: "npm" to setup-node reduced it to 45 seconds.

2. Not specifying exact versions

I initially used node-version: "latest". This caused issues when Node 23 came out and broke some dependencies. Pinning to node-version: "20" ensures consistency.

3. Running tests only on push, not PRs

I had on: push only. PRs weren’t being tested before merge. Adding pull_request triggers catches issues earlier in the review process.

Viewing Results

After pushing the workflow file, go to your repository’s Actions tab. You’ll see every workflow run listed with its status (success/failure/pending).

Click on a run to see detailed logs for each step. This is invaluable for debugging failed builds.

How This Changed My Workflow

Now, every time I push code:

  1. Tests run automatically
  2. Linter checks code style
  3. I get an email if anything fails
  4. I can’t merge a PR until CI passes

The initial setup took about 30 minutes. The time saved from broken builds and debugging production issues? Countless hours.

Summary

In this post, I showed how to set up a GitHub Actions CI pipeline from scratch. The key point is that one YAML file with checkout, setup, install, and test steps gives you automatic testing on every commit.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments