GitHub Actions #1 Anatomy of a Workflow: Events, Jobs, Runners, and Your First Run

5 min read

Every time you push, you run the tests locally. Reviewers keep asking “did you run the tests?” On release day, someone types the build commands in order, by hand. As long as a human repeats this, one day something gets skipped — and that tends to be the day things break. GitHub Actions is the tool that runs specified work automatically whenever an event (a push, a new PR, and so on) happens in your repository, and since it requires no extra installation for repositories already on GitHub, it has become the de facto default for CI/CD. This series covers it in ten parts, from a first workflow to organization-wide reuse, security, and operational patterns. Today’s goal is to grasp the five concepts that form the structure and get a first workflow running.

Five concepts: workflow, event, job, step, runner #

Everything in GitHub Actions can be explained with five words.

Execution flow
An event (push, PR, ...) happens
  → a workflow (.github/workflows/*.yml) wakes up
    → the jobs in the workflow each run on their own runner (a VM)
      → each job executes its steps from top to bottom
  • Workflow: one unit of automation. It lives as a YAML file in the repository’s .github/workflows/ directory — the path is a convention you cannot change.
  • Event: what wakes a workflow up. Pushes, pull requests, scheduled times, manual buttons — there are dozens.
  • Job: the unit of execution. A workflow can hold several jobs, and by default they run in parallel.
  • Step: each line of work inside a job — either a shell command (run) or an action someone has packaged (uses).
  • Runner: the machine a job runs on. Using GitHub’s hosted virtual machines is the default.

A first workflow: tests on every push #

Let’s wire test automation into a Python project. Adding one file to the repository is all it takes.

.github/workflows/test.yml
# .github/workflows/test.yml
name: test

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: pip install -r requirements.txt
      - run: pytest

All five concepts are here. on is the event (pushes to main and every PR), jobs.test is a single job, runs-on picks the runner, and the four lines under steps are the steps. The new thing is the action pulled in with uses. actions/checkout downloads your repository onto the runner, and actions/setup-python installs Python. Runners start as clean machines every time, so even “get the code” is our responsibility — actions are the packaged parts that handle this kind of repetitive work, and the marketplace hosts tens of thousands of them. The @v4 suffix pins the action’s version; the full syntax and the safe way to pin come in the next part and #9 respectively.

Commit and push this file, and the workflow is live from that very push. There is no registration step — the existence of the workflow file is the registration — which is a big part of why GitHub Actions has such a low barrier to entry.

Reading the results: the Actions tab #

Right after the push, the run appears in the repository’s Actions tab. Read it in order: run list → job → steps. On failure, the failed step unfolds automatically to show its log, and the check result also shows up on the PR screen, so reviewers never need to ask about the tests again. Add a status badge to the README and the latest run status shows on the repository’s front page.

README.md
![test](https://github.com/<owner>/<repo>/actions/workflows/test.yml/badge.svg)

Events beyond push #

A quick look at the main events on accepts doubles as a map of this series.

EventWhen it firesTypical use
pushA branch is pushedTests, builds
pull_requestA PR is opened or updatedPre-review verification
scheduleA cron time arrivesNightly checks, recurring work
workflow_dispatchA human clicks a buttonManual deploys, one-off tasks
releaseA release is publishedPackage publishing

schedule and workflow_dispatch mean workflows can run without any repository event at all — the piece that turns GitHub Actions from CI into a general automation tool. Waking workflows precisely with conditions (specific paths, specific branches) is part of next part’s syntax.

Runners and billing, the essentials #

runs-on: ubuntu-latest means a GitHub-hosted Ubuntu VM; macOS and Windows runners exist too. Billing depends on the account type: GitHub-hosted runners are free for public repositories, while private repositories get a monthly free allowance (varying by plan) and pay per minute beyond it, with macOS runners billed at a higher multiplier than Linux. Exact limits and rates change, so treat the pricing docs as the source of truth — but the instinct worth keeping now is “macOS jobs in private repos are expensive.” The cache strategies that cut cost and self-hosted runners are the subjects of #4 and #8.

Recap #

What we covered in this post:

  • GitHub Actions is structured as event → workflow → job → step, and jobs run on clean VMs called runners
  • A workflow is a YAML file under .github/workflows/, and the file’s existence is its registration
  • Runners start fresh every time, so work begins with checkout, and repetitive tasks are handled by marketplace actions
  • Beyond push and pull_request, schedule and workflow_dispatch cover general automation
  • Hosted runners are free for public repos; private repos pay per minute past the free allowance

In the next post (#2 Core Syntax), we properly nail down the YAML we skimmed today: contexts and expressions for passing values between steps, and if conditions for running steps selectively.

X