Your First Pipeline
Last module you watched a pipeline run and read this site’s deploy in GitHub’s UI. Today you build one yourself: a real GitHub Actions workflow that checks out splitease-api, installs Java 21, and runs mvn verify on every push. Then you’ll break a test on purpose and watch your own pipeline go red — the moment CI/CD stops being a diagram and becomes a tool you own.
The Goal
By the end of this module you can:
- Write a working GitHub Actions workflow file from scratch — by hand, understanding every line
- Explain what
checkout,setup-java, andmvn verifyeach do and why they’re in that order - Trigger a real pipeline by pushing a commit, and read the result in the Actions tab
- Read a green run step by step, and a red run straight to the failing line
- Break a test on purpose and confirm the pipeline catches it — fail-fast in your own repo
The Lesson
Where the file lives — and why that exact path
A workflow is a YAML file in one specific folder: .github/workflows/. GitHub scans that folder on every push; any .yml it finds there becomes a pipeline. The path is not a convention you can move — GitHub looks only there.
splitease-api/
├── .github/
│ └── workflows/
│ └── ci.yml ← your pipeline lives here
├── src/
├── pom.xml
└── ...
The name ci.yml is yours to choose; the folder is not. Create the two nested folders if they don’t exist (the leading dot matters — .github, not github).
The whole file, then every line
Here is a complete, real CI workflow for splitease-api. Read it once top to bottom, then we’ll walk it line by line.
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Set up Java 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build and test
run: mvn --batch-mode verify
Now decode it. Indentation is structural in YAML — two spaces per level, never tabs — so the nesting is the meaning.
| Line | What it does |
|---|---|
name: CI | The label shown in the Actions tab. Cosmetic, but name it for the human reading the run later. |
on: | The events that trigger this workflow — this is the “push” in push → build → test. |
push: branches: [ main ] | Run on every push to main. |
pull_request: branches: [ main ] | Also run on PRs targeting main, so a change is tested before it merges. This is the gate that protects the branch. |
jobs: | The work, grouped into named jobs. We have one: build. |
build: | Our job’s name (yours to choose). |
runs-on: ubuntu-latest | The runner — a fresh Ubuntu VM GitHub provisions, runs the job on, then destroys. This is the clean room from module 01. |
steps: | The ordered checklist. Each step is either a prebuilt uses: action or a shell run: command. |
uses: actions/checkout@v4 | Step 1: pull your exact commit onto the empty runner. Without this, the runner has no code. @v4 pins the version so the step behaves the same forever. |
uses: actions/setup-java@v4 | Step 2: install a JDK on the runner. |
with: java-version: '21' | Java 21 — the same LTS you compile splitease-api against locally (see how Java runs). The runner starts bare; you must install the JDK or mvn has nothing to run. |
distribution: 'temurin' | Which JDK build — Eclipse Temurin, the free production-grade one. |
cache: maven | Cache downloaded Maven dependencies between runs so the next run is faster. A speed nicety, not correctness. |
run: mvn --batch-mode verify | Step 3: the actual CI work — compile, then run the tests. |
Why these three steps, in this order
The steps aren’t arbitrary — each depends on the one before:
flowchart LR
A[fresh empty runner] --> B[checkout pulls your code]
B --> C[setup-java installs the JDK]
C --> D[mvn verify compiles and tests]
D --> E[green or red]
- Checkout first — the runner boots empty. No code, nothing to build.
actions/checkoutclones your exact commit onto it. - setup-java second — now there’s code, but no compiler. The runner doesn’t ship a JDK by default;
setup-javainstalls Java 21 and putsjavaandmvnon the PATH. Skip this andmvnfails with “command not found.” - mvn verify last — code is present, JDK is installed, now do the work.
Order is causal: you can’t compile code you haven’t downloaded with a compiler you haven’t installed.
What mvn verify actually does
mvn verify is the workhorse line. Maven has a lifecycle — an ordered chain of phases — and naming a phase runs it and every phase before it. verify sits near the end, so this one command runs the whole CI sequence:
flowchart LR
A[compile] --> B[test]
B --> C[package]
C --> D[verify]
| Phase (run in this order) | What it does |
|---|---|
compile | Compile src/main/java — catches compile errors. |
test | Run the unit tests in src/test/java. A failing test stops everything here. |
package | Assemble the jar. |
verify | Run integration tests and any final checks. |
So mvn verify is build → test → package in a single command — exactly the CI half of the pipeline from module 01. The --batch-mode flag tells Maven it’s running unattended (no colour codes, no prompts), which is what you always want on a CI runner. If any phase fails, Maven exits non-zero, the step goes red, and — fail-fast — the pipeline stops. (Tests live in Spring Boot 08 — Testing; this is where they pay off, running automatically on every push instead of only when you remember.)
How GitHub knows it passed or failed
The runner reports the exit code of each step. A shell command (or mvn) that succeeds exits 0; any failure exits non-zero. GitHub reads that: 0 is a green check, anything else is a red X and the job stops. You never tell GitHub “this passed” — it infers it from the exit code, which is why a failing test (which makes mvn exit non-zero) automatically fails the pipeline with no extra config.
Check The Concept
How This Shows Up At Work
- Day one on a new team. You clone the repo and the first file you open is
.github/workflows/. Reading it tells you how the project is built and tested faster than any README. Being fluent in this YAML is table stakes. - The PR that can’t merge. A teammate’s pull request shows a red ✗ next to “CI / build.” They can’t merge until it’s green. You point them at the failing step’s log; the failing test name is right there. This loop happens dozens of times a week on any real team.
- The “add a check to CI” task. A bug slips through, so the team adds a step (a linter, a coverage gate) to the workflow. Knowing where steps go and how
uses:vsrun:differ is what lets you make that change in five minutes instead of asking around. - The interview live-task. “Here’s a repo, add CI that runs the tests on every push.” If you can write the checkout → setup-java →
mvn verifyworkflow from memory and explain the step order, you’ve demonstrated the practical half of CI/CD on the spot.
Build This / Try This
You’ll add a real pipeline to splitease-api, push it, watch it go green, then break it on purpose and watch it go red. All in PowerShell from the project root.
- Create the folder and file. From the splitease-api repo root:
New-Item -ItemType Directory -Force .github\workflows
Then create .github\workflows\ci.yml with the exact YAML from the Lesson (the full file). Type it — your fingers learning the indentation is the point.
- Commit and push — the push is the trigger:
git add .github/workflows/ci.yml
git commit -m "ci: run mvn verify on every push"
git push
- Watch it run. Open your repo on GitHub → the Actions tab. A run named CI appears within seconds, with a spinning amber dot. Click it → click the
buildjob → expand the steps. You’ll see them execute in order:
✓ Set up job
✓ Check out the code
✓ Set up Java 21
✓ Build and test ← mvn verify runs here, the slowest step
✓ Complete job
When the dot turns green, you just ran your first pipeline. Read the “Build and test” log — you’ll see Maven’s BUILD SUCCESS and the test count, the same output you’d get locally, now produced by a robot on a clean machine.
-
Confirm the trigger works again. Make any trivial change (a comment), commit, push. A second run appears. Every push is a run — that’s CI.
-
Break it on purpose — watch it go red. Open any test in
src/test/javaand sabotage one assertion so it must fail. For example, change a settle-up test’s expected value:
// was: assertThat(balance).isEqualTo(0);
assertThat(balance).isEqualTo(999); // deliberately wrong
Commit and push:
git add .
git commit -m "test: break a test on purpose to see CI go red"
git push
-
Read the red run. Back in the Actions tab, the new run gets a red ✗. Click it → the
buildjob → the Build and test step is red, and no steps after it ran — fail-fast, in your own repo. Expand the log and scroll to the failure: Maven prints the failing test’s class, method, and the expected-vs-actual mismatch (expected: 0 but was: 999). That is the pipeline doing its entire job — a bad change caught in two minutes, never reaching anyone. -
Fix it back. Restore the assertion, commit, push. The next run goes green. You’ve now seen both halves: green proves safe, red holds the gate. Leave the workflow in place — it’ll protect every future push to splitease-api.
Interview Practice
These come up in backend and SDE rounds at Indian product companies — narrate the answer, don’t just name a tool.
1. Walk me through writing a CI workflow for a Java project from scratch.
Put a YAML file in .github/workflows/. Set on: push (and pull_request) so it triggers on changes. Define a build job with runs-on: ubuntu-latest for a fresh runner. Then three ordered steps: actions/checkout to pull the code onto the empty runner, actions/setup-java with java-version: '21' and distribution: 'temurin' to install the JDK, and a run: mvn verify to compile and test. GitHub reads the exit code of mvn — non-zero fails the run.
2. Why does checkout have to come before everything else?
The runner is a brand-new empty VM — it has no copy of your code. actions/checkout clones your exact commit onto it. Every later step (compile, test) operates on that code, so without checkout first there’s literally nothing to build. The step order is causal, not stylistic.
3. What does mvn verify do, and why use it in CI instead of mvn test?
Maven phases are an ordered chain; naming verify runs it and every phase before it — compile, test, package, then verify. So one command does the whole build → test → package CI sequence. mvn test stops after unit tests and never packages or runs integration tests, so verify gives a more complete gate. It’s the CI half of the pipeline in a single line.
4. How does GitHub Actions decide a step passed or failed?
By the step’s exit code. A command that succeeds exits 0; any failure exits non-zero. GitHub reads it: 0 is a green check, non-zero is a red X and the job stops. You never explicitly report status — a failing test makes mvn exit non-zero, which automatically fails the run. That’s why fail-fast needs no extra configuration.
5. What is a runner, and what does runs-on: ubuntu-latest give you?
A runner is the machine that executes a job. ubuntu-latest tells GitHub to provision a fresh Ubuntu virtual machine, run the job on it, and destroy it afterward. It’s the clean room: every run starts from nothing, so a passing build can’t depend on leftover state. GitHub hosts and patches these runners; for public repos the minutes are free.
6. Why also trigger on pull_request, not just push?
Triggering on pull_request runs the pipeline against a change before it merges into main. Combined with a branch protection rule requiring the check to pass, a teammate physically cannot merge a change that breaks the build. Push-only would test main after the bad code is already in it. PR triggers move the gate earlier, where it’s cheap to fix.
7. Your pipeline passes locally with mvn verify but fails on the runner. Where do you look first?
Most often it’s an environment difference the clean room exposed — a different Java version (check setup-java’s java-version), a dependency that was only on your local Maven cache, or a test that depends on local state (a file, a timezone, a running database) the runner doesn’t have. The runner being clean is the feature: it surfaced a hidden assumption your laptop was quietly satisfying. Read the runner’s log for the exact error, then reproduce it by clearing your own state.
Where to Practice
| Resource | What to do | How long |
|---|---|---|
| docs.github.com | Follow “Building and testing Java with Maven” — the official walkthrough of this exact workflow | 30 min |
| docs.github.com | Read “Workflow syntax for GitHub Actions” — the reference for on, jobs, steps, uses, run | 25 min |
| docs.github.com | Read the actions/setup-java README on the Marketplace — the options you can pass under with | 15 min |
Check Yourself
- What’s the exact folder a workflow file must live in?
- Name the three steps of the splitease-api workflow, in order, and what each does.
- Why must
checkoutrun beforesetup-java? - What does
mvn verifyrun, and why is it more thorough thanmvn test? - How does GitHub know a step passed or failed?
- What does
runs-on: ubuntu-latestgive you, and why does “fresh every time” matter? - Why also trigger on
pull_requestand not onlypush? - You broke a test; describe exactly what you’d see in the Actions tab.
Answers
.github/workflows/(leading dot, at the repo root). The file name is yours; the folder path is fixed.actions/checkoutpulls your commit onto the empty runner;actions/setup-javainstalls JDK 21 (Temurin);run: mvn verifycompiles and tests the code.- The runner boots empty — checkout puts the code there, and setup-java/mvn have nothing to act on until it’s present. Order is causal.
- It runs compile → test → package → verify (every phase up to and including verify).
mvn teststops after unit tests;verifyalso packages and runs integration tests, a more complete gate. - By the step’s exit code:
0= green pass, non-zero = red fail and the job stops. A failing test makesmvnexit non-zero. - A fresh Ubuntu VM that’s created, used, and destroyed per run. Fresh-every-time means no leftover state, so a passing build doesn’t secretly depend on your machine.
- So the pipeline tests a change before it merges; with branch protection, a broken change can’t be merged at all. Push-only tests
mainonly after the bad code is already in. - A red ✗ run; clicking in shows the
buildjob red at the “Build and test” step, no steps after it ran (fail-fast), and the log naming the failing test with expected-vs-actual.
Explain it out loud: Without looking, recite the workflow file — name, on, jobs, runs-on, the three steps — and say what each line does and why the steps are in that order. Then explain what happens, step by step, from git push to a green check in the Actions tab. If you can’t say why checkout comes first, re-read the step-order section.
Why AI Can’t Do This For You
AI will generate this YAML instantly, and for a standard Maven project it’ll even be correct. But the skill isn’t the file — it’s reading your red run at 11pm: why did mvn verify pass on your laptop and fail on the runner? Was it a missing service container, a Java version mismatch, a test that depended on your local timezone? AI can’t see your runner’s log, your test’s hidden assumptions, or what you changed last commit.
You built and broke this pipeline yourself today, so when a real one goes red you know exactly where to click and what the failing line means. The YAML is cheap and copyable; the debugging instinct — formed by watching your own pipeline go green and red — is the thing that makes you useful on a team, and no prompt hands it to you.
Module done? Add it to today’s tracker