GitHub·6 min read·

Structuring a GitHub Actions File: Workflows, Jobs and Steps

Structure an efficient GitHub Actions file into workflows, jobs and steps; parallelize with needs, scope each concern, and keep pipelines easy to review.

NB

Netbay Engineering

Netbay Engineering

On this page

A GitHub Actions file can be a two-line convenience or a sprawling build system. The difference is discipline about the three levels of abstraction the format gives you: the workflow, the job, and the step. Once you pin down what belongs at each level, your pipelines become easier to read, faster to reason about, and far simpler to debug. This post walks through that layering with realistic YAML you can adapt to any repository, and the same structure maps cleanly onto the self-hosted runners you might attach to a Netbay VPS.

The workflow: the whole file

The workflow is the outermost container. It has a name, a trigger, and any global permissions or defaults. It also declares the jobs. You should think of the workflow as the "when and what" — when should anything run, and what are the major chunks of work to perform.

Keep global concerns here: concurrency limits to avoid stampedes, permissions for the GITHUB_TOKEN, and a default shell. Everything inside a workflow is reusable across the jobs it contains.

The job: a unit of environment

A job runs on its own runner. That runner has an operating system, a fresh copy of the repository checkout, and its own set of containers or services. A job is where you choose the platform: ubuntu, windows, macos, or a self-hosted label. Each job runs in parallel with other jobs unless you add the needs keyword.

Think of a job as "one thing done in one environment." Linting on Linux, building on Windows, and testing a matrix across Node versions are each separate jobs. If something does not need a particular OS, do not split it into another job just to parallelize trivial work — jobs that cannot run in parallel on the same machine actually cost more.

The step: an atomic action

Steps are the smallest unit, and they run sequentially inside a job, in the order listed. If one step fails, later steps are skipped by default unless you mark them with continue-on-error or an if condition. A step either runs a run script or uses an action via uses.

Steps should be small and single-purpose: install dependencies, run lint, run tests, upload results. When a step fails you can quickly see exactly which command failed, which keeps debugging fast.

A minimal but complete example

Putting it together, here is a workflow that validates a repository with two jobs running in parallel:

yaml
name: CI
on: [push, pull_request]

permissions:
  contents: read

jobs:
  lint:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
  test:
    runs-on: ubuntu-22.04
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

Sequencing jobs with needs

By default jobs run in parallel. To force ordering, add needs with the names of the jobs that must finish first. GitHub Actions builds a dependency graph from needs declarations. Here the test job will not start until lint finishes successfully:

yaml
jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - run: echo building
  test:
    runs-on: ubuntu-22.04
    needs: build
    steps:
      - run: echo testing
  deploy:
    runs-on: ubuntu-22.04
    needs: [build, test]
    if: github.ref == 'refs/heads/main'
    steps:
      - run: echo deploying

A naming and scoping checklist

Before you write a workflow, run each concern through the three levels. Triggers, concurrency, permissions, and defaults belong at the workflow level. A chosen OS, a set of services, and parallel units of environment belong at the job level. Commands, checkout, cache, and artifact upload belong at the step level. If you find yourself duplicating the same five steps in every job, that is a signal to consider a composite action or a reusable workflow — both covered later in this series.

workflow on / permissions / concurrency job: lint job: test needs step: npm ci step: npm run lint step: npm ci step: npm test steps run sequentially in order

Takeaway

Structure before speed. Put triggers and policy at the workflow level, environment at the job level, and single-purpose commands at the step level. A clean hierarchy is easier to review, easier to cache, and far easier to debug when a build goes sideways. When you want a fast self-hosted runner to speed up these jobs, you can spin up an Ubuntu VPS on Netbay in under 60 seconds and wire it in with a runner token — 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