CI/CD·8 min read·

A Minimal CI Pipeline with GitHub Actions YAML

Build a clean, working GitHub Actions workflow from scratch with a step-by-step YAML walkthrough you can copy.

NB

Netbay Cloud Team

Netbay Engineering

On this page

GitHub Actions is one of the fastest ways to get a real CI pipeline running. You write a YAML file, commit it, and GitHub hosts the runners that execute it — no infrastructure to manage for the basics. This walkthrough builds a minimal but correct workflow from an empty repository to a pipeline that lints, tests, and builds on every push, then explains every key line.

How Actions Are Structured

An Actions workflow is a YAML file stored under .github/workflows/. The top-level keys are: name, on (the triggering events), jobs, and inside each job runs-on, steps, and per-step uses or run.

The mental model is simple. A *workflow* responds to an event. A *job* runs on a machine. A *step* is one command or one reusable action. Jobs can run in parallel by default, which matters for speed.

Here is the complete minimal workflow. Store it as .github/workflows/ci.yml.

yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  check:
    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 test
      - run: npm run build

Walking Through Each Line

Let us unpack what this file actually does, because the defaults hide a lot.

  • name: CI: a label shown in the Actions tab. Choose something descriptive.
  • on.push.branches: [main]: run when anything lands on main. on.pull_request: runs the workflow for every pull request targeting the repo. Triggering on both is the standard healthy setup — PRs get validated before merge, and merged code gets validated again on main.
  • jobs.check: a job named check. The name is up to you; use several jobs later for parallelism.
  • runs-on: ubuntu-latest: the runner image. GitHub provides hosted runners with common tools preinstalled. For now Ubuntu is fine.
  • actions/checkout@v4: a reusable action that clones your repository onto the runner. Without it the runner would not have your code.

The Setup Step Does Real Work

yaml
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci

setup-node installs the requested Node version and, because we pass cache: npm, it automatically caches the npm dependency directory between runs. That single line dramatically speeds up repeat runs. The cache key reads your package lockfile to build a restore key, and the run: npm ci installs dependencies deterministically from that lockfile. Use npm ci instead of npm install in CI — it fully removes and reinstalls from the lock, producing predictable builds.

The remaining steps run the actual checks: lint, test, and build. Each run is a separate step, so the Actions UI shows them as distinct colored rows, making it easy to see which command failed.

Adding a Second Job for Speed

One job runs its steps in sequence. If lint does not need tests to finish, run them in parallel as separate jobs. Actions runs independent jobs concurrently by default, which cuts total wall-clock time.

yaml
jobs:
  lint:
    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

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm test

Here needs: lint makes the test job wait for lint to finish first. That is usually unnecessary for unrelated checks, but you will use needs constantly once you add build and deploy jobs that must run after tests pass.

push / pull_request lint job parallel test job parallel flow starts checkout

Branch Protection Ties It Together

A pipeline is only useful if a red build actually blocks bad code. For the checks to protect main, enable branch protection on the repository: go to repository settings, choose the main branch, and require status checks to pass before merging. Once enabled, a failing lint or test job red-lights the pull request and the merge button stays disabled.

Practical Notes for Speed and Reliability

  • Pin action versions with Git tags or commit SHAs rather than floating @main.
  • Add a concurrency group so pushing twice does not run two identical builds.
  • Keep the job under a few minutes; if your suite is slow, cache more.
  • Log key outputs with echo so failures are easier to trace.

Takeaway

A finished Actions workflow is a few dozen lines: trigger on push and PRs, checkout, set up the toolchain, then run lint, test, and build in jobs. Commit it in a pull request, protect main, and you have real CI the same day.

When your test suite outgrows what you want on shared runners, a Netbay VPS can host a self-hosted runner that keeps your builds inside your own environment — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance