Week 12

Spring profiles

CI/CD Concepts

GitHub Actions

Docker in CI

Deploying from a Pipeline

Quality Gates

Practice

Assignment

Backend Track

Your first quality gate

You met GitHub Actions in the core programme, where a workflow ran a linter and tests on your pull requests. Today you write one yourself, for your Spring Boot project — and by the end of this chapter, broken code will physically not be able to reach main.

Anatomy of a workflow

A workflow is a YAML file in a folder GitHub watches: .github/workflows/. Create .github/workflows/ci.yaml in your Week 11 project:

name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-java@v5
        with:
          distribution: temurin
          java-version: '25'
          cache: maven
      - name: Run tests
        run: mvn --batch-mode verify

Here is a quick reminder on how Github Actions work:

<aside> 💡

That little cache: maven line saves the downloaded dependencies between runs. Without it, every run re-downloads all of Maven's jars; with it, later runs are minutes faster. You will measure the difference in the practice exercises.

</aside>

<aside> ⚠️

YAML survival rules. YAML is a new language and it is picky:

  1. indentation is the structure — two spaces per level, consistently
  2. spaces only, never tabs — a single tab breaks the file
  3. Quote version numbers like '25' unquoted, YAML may read them as numbers and confuse tools. Most “my workflow does not start” problems are one of these three.

Need a refresher? Check out Appendix 1: YAML syntax from the Core program

</aside>

The first run — and the first red build

Commit the file on a branch, push, and open a pull request. Go to the Actions tab and watch your workflow appear and start running.

Then, most likely: ❌. Open the log and you will find your integration tests failing with something like:

org.postgresql.util.PSQLException:
  Connection to localhost:5432 refused. Check that the hostname and port
  are correct and that the postmaster is accepting TCP/IP connections.

Of course. Your integration tests talk to PostgreSQL — and the runner is a fresh machine with no database on it. On your laptop, Postgres is simply there because you started it once with Docker Compose and it stayed. The runner has only what you declared, and you declared no database.

This error is the whole point of CI in one line: the runner just told you exactly what your project silently assumes about the machine it runs on.

Service containers: a database for the length of one job

You already know the fix conceptually — in Docker Compose, your api service had a postgres service next to it. GitHub Actions has the same idea, called a service container: a container that starts before your job and is thrown away after it. Add it to the test job, and give the test step the connection details as environment variables — relaxed binding, exactly as in Docker Compose and on Render:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:18
        env:
          POSTGRES_DB: testdb
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-java@v5
        with:
          distribution: temurin
          java-version: '25'
          cache: maven
      - name: Run tests
        run: mvn --batch-mode verify
        env:
          SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb
          SPRING_DATASOURCE_USERNAME: postgres
          SPRING_DATASOURCE_PASSWORD: postgres

Two details worth noticing:

Push again. Green. ✅

<aside> ⚠️

This throwaway database is for tests only. Your real data stays in Neon; the pipeline never touches it. Every run starts with an empty testdb — which is exactly what tests want: a clean, predictable starting point.

</aside>

Reading a red build like a professional

Red builds are not failures of yours — they are the system working. What matters is how fast you can read one:

  1. Actions tab → the failing run → the failing job. Steps with a ❌ are expanded; everything above them succeeded.
  2. Find the first error, not the last. Search the log (Cmd/Ctrl+F works) for ERROR or BUILD FAILURE. Later errors are usually consequences of the first one.
  3. Read the test summary. Maven prints Tests run: 14, Failures: 1 … and names the class and method. That is your starting point, not the stack trace's last line.
  4. Reproduce locally. Run the same command — mvn verify — on your machine. If it is green locally and red in CI, the difference is the environment, and now you know where to look.

💬 Question: the build is green on your branch, you merge, and the push to main turns red — without you changing anything. How can that happen?

Branch protection: make the gate real

Right now the check runs, but a red ❌ is only advice — the Merge button still works. Let's take that button away:

  1. In your repository: Settings → Branches → Add branch ruleset (or Add rule on older UIs) for main.
  2. Enable Require status checks to pass and select your test job.
  3. Enable Require a pull request before merging.

From now on, code enters main only through a pull request, and only when CI is green. The merge button literally greys out on a red build.

<aside> ⌨️

Hands on: set the ruleset up now, then test it — make a test fail on purpose on a branch, open a PR, and watch GitHub refuse the merge. Fix the test, push, and watch the button come back. That is your gate working.

</aside>

Extra resources


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.