Module 2: Your First Pipeline Pytest In Ci

7. Reading the CI log and the status badge

Description

You built the complete workflow and understand what each step does. Now the other half of the craft is missing: reading what it produces when it runs. A pipeline you can't interpret is a pipeline you can't fix when it turns red —and it will turn red—. This lesson teaches you to read the CI log: how it's organized (jobs that contain collapsible steps, a ✓ or ✗ for each), where exactly pytest's output lives inside the step that generated it, how a step's ✗ corresponds to lesson 6's non-zero exit code, and how the failure annotation takes you straight to the culprit line. And it closes with the status badge: that green check in the README that tells the world, at a glance, whether the suite passes.

Here the module's rule asks for its most careful nuance. We don't spin up a GitHub runner, so the log you'll see is an honest reconstruction of how the Actions tab looks —the structure, the steps, the checks—. But the pytest output embedded inside that log is the real one, the same 11 passed you ran for real in lesson 6. It's the combination that defines the module: the container (the log) is "this is how it would look"; the content that matters (the pytest output, the exit code) is real. I point it out explicitly in each block so you always know which is which.

Connection to the module: this lesson reads the result of the workflow that lessons 2 to 6 assembled. Each step's ✓/✗ is the visual representation of the exit code you understood in lesson 6 (0 → ✓, non-zero → ✗). Boundary: here you read the log to understand the structure and find pytest's output; diagnosing a failure in depth —especially one that only happens in CI— is module 3 (and the sibling failure-diagnosis guide). Today you learn to navigate the log and to know what each mark means; the deep analysis of an environment gap comes later.

The detailed supermarket receipt

Think of the receipt of a big supermarket shop. At the top, a header: the store, the date, the register. Then, the list of items, one per line, each with its price. And at the bottom, in large print, the total: the number that really matters to you, the one you look at first to know how much you paid. If the total surprises you, then you raise your eyes and go through the lines to find the expensive item you weren't expecting.

A CI log is read the same way, and in the same order. At the top is the job header (when it ran, on which machine, triggered by what). In the middle, the list of steps, one per line, each with its mark of ✓ (went well) or ✗ (failed) —they're the items of your shop—. And the "total" is the state of the whole job: green if all steps went well, red if any failed. When the job is green, you look at the total and go on with your life, like a receipt whose total is the expected one. When it's red, you do the surprise-receipt thing: you go through the steps top to bottom, find the one with the ✗, and open it to see the detail —the equivalent of looking closely at the expensive-item line—. There, inside the step that failed, is the complete output: for the pytest step, the whole run with its diagnosis.

The skill of reading a log is that: look at the total first, and when it's red, know which line to open and what to look for inside. Don't read the thousand lines every time; go to the total, and go down to the detail only when the total asks you to.

How the log is organized: jobs, steps, and the checks

When you enter your repository's Actions tab and open a run, you see a three-level hierarchy:

  1. The workflow (at the very top): the name: tests you gave it. A run belongs to a workflow.
  2. The jobs: in our case, just one, test. Each job has its own state (✓/✗) and ran on its own machine.
  3. The steps, within the job: the list of steps you wrote, plus a couple GitHub adds on its own (a setup one at the start, a cleanup one at the end). Each step is collapsible: it appears as a line with its name and its ✓ or ✗, and you click to expand it and see the output that step produced.

That last point is the one that most helps navigation: each command's output lives inside its step, collapsed by default. The log isn't a single wall of text; it's a list of drawers, and each drawer holds what its step printed. If you want to see what pip printed, you open the "Install dependencies" drawer. If you want to see the pytest run, you open the drawer of the step that runs pytest. That's why it's worth naming the steps with name: (lesson 5): a log full of steps named "Run pytest" and "Install dependencies" navigates itself; one with unnamed steps forces you to guess which is which by its command.

Here's the workflow with named steps, the form that produces a readable log —it's the one you'll use in the mini-project—:

name: tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the code
        uses: actions/checkout@v5
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.14"
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
      - name: Run the test suite
        run: pytest

Each step's name: becomes the title of its drawer in the log. The five steps you wrote, plus GitHub's two automatic ones, will be seven readable lines.

Worked example: the log of a green run

This is how the test job's log would look when the suite passes. Important (module rule): the log's structure —the steps, the checks, the times— is a reconstruction of how the Actions tab looks; we don't spin up a runner. But what appears inside the "Run the test suite" drawer is the real pytest output, the same 11 passed you ran for real in lesson 6.

What to expect (this is how the log would look, with the real pytest output embedded):

✓ test                                                    (job: green)

  ✓ Set up job                                            2s
  ✓ Check out the code                                    1s
  ✓ Set up Python                                         4s
  ✓ Install dependencies                                  6s
  ✓ Run the test suite                                    1s     ◀── we open this drawer
  ✓ Post Set up Python                                    0s
  ✓ Complete job                                          0s

The seven lines are the job's steps, each with its ✓ and the time it took. The five in the middle are yours (note the names you gave them); "Set up job" at the start and "Complete job"/"Post ..." at the end are added by GitHub to prepare and clean the machine. All with ✓: the whole job is green.

Now we open the "Run the test suite" drawer —click on that line— and inside is exactly this (real pytest output):

============================= test session starts ==============================
platform linux -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/runner/work/reservo/reservo
collected 11 items

test_availability.py ....                                                [ 36%]
test_pricing.py ....                                                     [ 72%]
test_refunds.py ...                                                      [100%]

============================== 11 passed in 0.03s ==============================

That's the complete connection: the ✓ of the "Run the test suite" step in the list above is the visual representation of the 11 passed in here, which in turn comes from the exit code 0 (lesson 6). The log doesn't invent the color; it inherits it from the number pytest returned. pytest's output in CI is identical to your terminal's —same text, same verdict— because it's the same command on the same suite. The only different thing is where you read it: inside a drawer in the Actions tab, instead of in your terminal.

Worked example: the log of a red run and the annotation

When the suite fails —for example, with lesson 6's 25%-discount bug— the log changes in two ways. Again: the structure is "this is how it would look"; the embedded pytest output is real.

What to expect (this is how the log would look in red):

✗ test                                                    (job: red)

  ✓ Set up job                                            2s
  ✓ Check out the code                                    1s
  ✓ Set up Python                                         4s
  ✓ Install dependencies                                  6s
  ✗ Run the test suite                                    1s     ◀── the step that failed
  ✓ Post Set up Python                                    0s
  ✓ Complete job                                          0s

Notice the pattern, which is pure information: most of the steps stay ✓; the only one with ✗ is "Run the test suite". That tells you at a glance that the ground was well prepared —the code was brought, Python was installed, the dependencies were put in place— and that the failure is in the tests themselves, not in the infrastructure. The step's ✗ corresponds to the non-zero exit code pytest returned (here, 1). The receipt's "total" —the job's state— is red because a line has ✗.

We open the "Run the test suite" drawer (the ✗ one) and inside is the real red run:

test_pricing.py:17: AssertionError
=========================== short test summary info ============================
FAILED test_pricing.py::test_pro_member_gets_twenty_percent_off_the_subtotal - AssertionError: assert 5625 == 6000
========================= 1 failed, 10 passed in 0.03s =========================

pytest's short test summary info is your best friend in a CI log: it's the compact list of what failed and why, without having to read the whole run. FAILED test_pricing.py::test_pro_member_... - AssertionError: assert 5625 == 6000 gives you, in one line, the file, the test, and the wrong value. With that you already know where to look before opening anything else.

Also, GitHub usually shows an annotation: a summary of the failure highlighted above the run (and, in a pull request, next to the corresponding line of code), of the style:

test_pricing.py:17: test_pro_member_gets_twenty_percent_off_the_subtotal
AssertionError: assert 5625 == 6000

The annotation is a shortcut: it takes you straight to the culprit line (test_pricing.py:17) without you having to expand drawers or search the text. It's the "expensive item pointed at with an arrow" of the receipt. From there, understanding why 5625 instead of 6000 and fixing it is the diagnosis work —which when the failure reproduces the same on your machine is direct, and when it only happens in CI is module 3's ground—.

The badge: the suite's state, hung on the README

There's one last piece, small and very visible: the status badge. It's that image —a label saying tests passing in green or tests failing in red— you see at the top of so many projects' READMEs. It communicates, at a glance and without entering the Actions tab, whether the project's suite is passing right now on the main branch.

The badge isn't an image you upload; it's a live image GitHub generates and updates on its own. Each workflow has a badge URL of this form:

https://github.com/<owner>/<repo>/actions/workflows/tests.yml/badge.svg

You change <owner> for your user or organization, <repo> for the repository name, and tests.yml for your workflow file's name. That URL returns an SVG image that reflects the workflow's latest state: green if the last run on the default branch passed, red if it failed. To hang it on the README, you put it as a Markdown image, usually linked to the Actions tab:

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

Read it in parts: ![tests](...badge.svg) is the badge image (the tests is the alt text); wrapping it in [...](...actions/workflows/tests.yml) turns it into a link, so that whoever clicks the badge goes straight to the workflow's runs. What to expect in the rendered README: a little label saying tests followed by passing in green (or failing in red), which updates on its own every time the workflow runs.

What it's for, beyond the decorative: the badge is the project's public health signal. A collaborator who arrives at the repo sees the green badge and knows, without investigating, that the suite passes and they can trust the main branch's state. A red badge is a visible alarm for anyone who enters: something is broken in main and it needs attention. It's the "receipt total" of the whole project, put on the front page.

Common mistakes

Reading the whole log top to bottom instead of going to the step with ✗ (reading without strategy). What happens: someone opens a red CI and starts reading from the first line, drawer by drawer, getting lost in the pip and setup output before reaching the failure. Why it happens: you treat the log as linear text instead of as a list of drawers with marks. How to spot it: if you've spent a while reading output from steps that are ✓, you're in the wrong place. How to fix it: look at the marks first —go straight to the step with ✗—, open it, and inside look for pytest's short test summary info. The ✓ steps don't need your attention; the ✗ is the one that asks for it. Read the total, go down to the pointed line.

Not naming the steps and navigating a log of anonymous drawers (illegible log). What happens: someone writes all the steps without name:, and in the log each drawer is titled with its raw command or a generic, making it hard to know which to open. Why it happens: naming the steps seems optional when you write the YAML, and it is —until you have to read the log—. How to spot it: if in your log you can't tell "install dependencies" from "run tests" without reading each command, you're missing names. How to fix it: put a name: on each step with a clear description ("Install dependencies", "Run the test suite"). The name you write once becomes the title of the drawer you read many times. It's courtesy to your future self and your team.

Confusing the badge with "the state of my current branch" (badge scope). What happens: someone looks at the README's green badge while working on a branch with broken tests, and believes their branch is fine. Why it happens: the badge, by default, reflects the workflow's state on the default branch (main), not on the branch you're standing on. How to spot it: if your work branch has CI red but the README badge is still green, it's not a contradiction: the badge speaks of main, your branch is something else. How to fix it: remember that the badge reports the default branch unless you ask it for another (the URL admits a branch parameter). To know how your branch is doing, look at your branch's run in the Actions tab, not the README badge.

Exercises

Exercise 1 — Locate the failure in the log. You're shown this list of steps of a red job. Without seeing anything else, say: in which step is the problem?, what does the fact that the previous steps are ✓ tell you about the infrastructure?, and which drawer would you open and what would you look for inside?

✗ test
  ✓ Set up job
  ✓ Check out the code
  ✓ Set up Python
  ✗ Install dependencies
  ⊘ Run the test suite      (did not run)
See solution
  • The problem is in "Install dependencies" (the only one with ✗).
  • About the infrastructure: that "Check out the code" and "Set up Python" are ✓ tells me the code did reach the runner and Python 3.14 installed fine. The failure is not in those steps; it's specifically in installing the dependencies —maybe a package that doesn't exist in the requested version, a requirements.txt with an error, or a network problem downloading a package—.
  • What I'd open: the "Install dependencies" drawer, and inside I'd look for pip's output —specifically an ERROR: or "Could not find a version that satisfies the requirement...", which is how pip reports it couldn't resolve or install something—.

Key detail: "Run the test suite" appears with ⊘ (did not run), not with ✗. When a step fails, the following steps don't run —the job stops at the failure—. That's why pytest never even got to run: it made no sense to run the tests if the dependencies didn't install. The failure is upstream of pytest, and fixing it (the requirements.txt) is what unblocks the rest.

Exercise 2 — Write the badge. Your GitHub user is ana-dev, your repository is called reservo, and your workflow is at .github/workflows/tests.yml. Write the Markdown line that hangs the badge on the README, linked to the workflow's runs tab.

See solution
[![tests](https://github.com/ana-dev/reservo/actions/workflows/tests.yml/badge.svg)](https://github.com/ana-dev/reservo/actions/workflows/tests.yml)

Breakdown:

  • ![tests](...) — the badge image; tests is the alt text (what's read if the image doesn't load).
  • The image URL: https://github.com/ana-dev/reservo/actions/workflows/tests.yml/badge.svg — with your owner (ana-dev), your repo (reservo), and your workflow file (tests.yml), ending in /badge.svg.
  • The [...](...) that wraps the image turns it into a link toward https://github.com/ana-dev/reservo/actions/workflows/tests.yml, the workflow's runs page. So whoever clicks the badge sees the run history.

Rendered, the README will show a little tests | passing label in green (or failing in red) that updates on its own with each run on main, and that leads to the Actions tab on click.

Exercise 3 — Green on the badge, red on your branch. A teammate is puzzled: the README badge says tests passing in green, but their add-discount-tier branch's run is red in the Actions tab. They think CI contradicts itself. Explain to them why both things are true at once.

See solution

There's no contradiction: the badge and their branch's run speak of different branches.

  • The README's badge, by default, reflects the workflow's state on the repository's default branch (usually main). Green on the badge means "the last run on main passed" —and main can be perfectly healthy—.
  • The red run they see in the Actions tab is their add-discount-tier branch's, where they're working and where, evidently, some test still fails.

Both are true: main is green (which is why the badge is) and their work branch is red (which is why their run is). It's exactly the normal state of a work in progress: you break something on your branch, your branch's CI turns red and warns you, and main stays protected and green because your change isn't merged yet. When they fix their branch's tests and merge it, then the badge (which looks at main) will reflect the state with their change inside.

The moral: to know how your branch is doing, look at your branch's run in Actions; the README badge speaks to you of main, not of where you're standing.

Summary and next step

In this lesson you learned to read the result of the pipeline you built. A CI log is read like a receipt: first the "total" (the job's state, green or red), and when it's red, you go down to the pointed line. You understood its three-level structure —workflow → jobs → collapsible steps—, that each command's output lives inside its step (that's why it's worth naming them with name:), and that each step's ✓/✗ is the visual representation of lesson 6's exit code (0 → ✓, non-zero → ✗). You saw the green log with the real pytest output embedded (11 passed), the red log where only the pytest step has ✗ —revealing that the failure is in the tests, not the infrastructure—, and how the short test summary info and the annotation take you straight to the culprit line. And you hung the badge: the live image (badge.svg) that reflects the workflow's state on main and communicates the project's health at a glance.

We kept the module's honesty precise: the structure of the log is "this is how it would look" (we don't spin up a runner), but the pytest output inside it is the real one you ran. That's the combination that makes the module reliable.

Before moving on you should be able to: navigate a log by going straight to the step with ✗; explain why naming the steps makes the log readable; find the short test summary info inside the pytest drawer; write the badge's Markdown line; and distinguish which branch the badge reports from the branch you work on.

You have all the pieces: you know how to write the complete workflow, you understand each step, you know the exit code, and you know how to read the log and hang the badge. What's next is pulling it all together with your own hands. In lesson 8, the mini-project: you write from scratch the tests.yml that runs Reservo's suite on every push, with named steps; you establish local parity by running on your machine the same commands as CI and pasting the real green output; and you draft the note of what your pipeline covers and what's left for the following modules. It's the module's practical exam.

Resources

  • Viewing workflow run history — how to navigate the Actions tab, open a run, and expand the steps to see their output. The official reference for the "read the log" part of this lesson.
  • Adding a workflow status badge — the official guide on how to build the badge URL, link it, and point it to a specific branch or event. The exact source of the badge section.
  • How to invoke pytest — output and reporting — how pytest formats its output, including the short test summary info that's so useful in a CI log, and how to control how much detail it prints. For fine-tuning what you see inside the pytest drawer.