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:
- Checks out your code from the repository
- Installs dependencies in a clean environment
- 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:
name: CI
on: push
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install - run: npm testI 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:
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 lintThis worked. Here’s what each part does:
Triggers (on):
push: branches: [main, develop]— runs when pushing to main or developpull_request: branches: [main]— runs when opening PRs to main
Steps:
actions/checkout@v4— downloads your repository codeactions/setup-node@v4— installs Node.js 20 with npm caching enablednpm ci— installs dependencies frompackage-lock.json(faster and more reliable thannpm install)npm test— runs your test suitenpm 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:
| Command | Behavior |
|---|---|
npm install | Updates lockfile, may install different versions |
npm ci | Uses 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:
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=xmlThe 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:
- Tests run automatically
- Linter checks code style
- I get an email if anything fails
- 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:
- 👨💻 GitHub Actions Documentation
- 👨💻 actions/checkout
- 👨💻 actions/setup-node
- 👨💻 actions/setup-python
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments