Introduction to Software Engineering · Chapter 08

Lab-08: DevOps Practices

Your team turns the ITC Club Hub repository into a small delivery pipeline for Sprint 2: an agreed Git workflow, a CMake build with a core library, a CLI and a test target with warnings as errors, a GitHub Actions pipeline that builds and tests with GCC and Clang, a protected main branch with real code reviews, and a first tagged release v0.1.0 with a Docker image. The challenge adds static analysis, sanitizers and the team's own DORA metrics.

Git · GitHub Actions · Docker C++20 · CMake 3.28 · GoogleTest · CTest GCC 13+ · Clang 17+ ≈ 6 hours (team of 4–5) 5 tasks + 1 challenge
How to work through this lab. Do the tasks in order; each builds on the previous. Read the Goal, follow the Steps, compare with the Expected output, and tick the Acceptance checklist (saved in your browser). Open a hint only when stuck for more than 10 minutes.
Timing. Lab-08 runs in week 10, at the hand-over to Sprint 2. Finish Tasks 1 to 3 first: from Task 4 on, every Sprint 2 story goes through the pipeline you build here. This is team work: individual contributions must be visible in the Git history (commits and reviews).
0

Setup

20 min
  1. Tools. Every member verifies the tool chain on their own machine. On Windows, use WSL 2 with Ubuntu (recommended, matches CI) or MSYS2 for GCC and Clang; Visual Studio 2022 (MSVC) also works for local builds. Docker is needed from Task 5.
    git --version          # 2.40 or later
    cmake --version        # 3.28 or later
    g++ --version          # GCC 13 or later, and/or
    clang++ --version      # Clang 17 or later
    gh --version           # optional: GitHub CLI for PRs and releases
    docker --version       # Task 5: Docker Desktop or Docker Engine
    
    # your commits must be linked to your GitHub account
    git config --global user.name  "Sok Dara"
    git config --global user.email "dara@example.edu.kh"   # an address added to GitHub
  2. Case-study brief. The box below is everything you need to know about the product for this lab; there is no other product document to read.
    Case-study brief: ITC Club Hub
    Product
    An application for student clubs at the Institute of Technology of Cambodia. The team models the full system as a web and mobile application and implements its core in C++20: domain model, business rules and a command-line front end, with CSV or JSON file storage.
    Users
    Club leaders register a club, an administrator approves it. Clubs publish events (title, date and time, location, capacity, description). Students browse, register, cancel (up to 24 hours before the start) and receive a reminder. Organisers check students in at the door and see attendance. Administrators see a monthly activity report.
    Rules to test
    Capacity cannot be exceeded (a waiting list is a Should feature) · a student cannot register twice for the same event · cancellation closes 24 hours before the start · past events cannot be registered for · only approved clubs can publish events.
    C++ names
    Namespace clubhub: Club, Event, Student, Registration, ClubStatus (Pending, Approved, Rejected), RegistrationStatus (Registered, Cancelled, CheckedIn, Waitlisted), RegistrationService with registerStudent, cancel, checkIn; interfaces EventRepository and RegistrationRepository with CSV-backed implementations.
    Layout
    CMakeLists.txt, src/, include/clubhub/, tests/ (GoogleTest via FetchContent), .github/workflows/ci.yml, one folder per lab (labNN/).
    Personal data
    Student ID, name, e-mail, phone (optional), club membership, attendance. Real data never goes into the repository, the logs or the CI; use fake sample data.
    Rhythm and roles
    Sprint 1 in weeks 9 and 10, Sprint 2 in weeks 11 and 12, submission and demo in week 14. One Product Owner (talks to the instructor or teaching assistant who plays the client), a Scrum Master rotating each sprint, the rest Developers.
  3. Inputs from earlier labs. If one of them is incomplete, use the fallback and carry on; do not stop to repair the earlier lab.
    InputWhereWhat you need from itFallback if missing
    Team repository and rolesLab-01, README.mdA GitHub repository with every member as collaborator (Write access); who is PO and Sprint 2 Scrum MasterCreate clubhub-<team> now, invite everyone, write the roles into README.md.
    Backlog and user storiesLab-05 (lab05/), GitHub IssuesIssue numbers for branch names and Closes #nOpen one issue per Sprint 2 story (for example register, cancel, check-in) before Task 1.
    Class modelLab-06 (lab06/)Class and file namesUse the C++ names in the case-study brief.
    Sprint 1 incrementLab-07: src/, include/, tests/, lab07/Code that builds and at least a few tests of RegistrationServiceStart from the Task 2 starter: Event, RegistrationService::registerStudent with the capacity rule, and the four test cases.
    Definition of DoneLab-07 (lab07/)The team's quality bar"Reviewed by one teammate, tests pass locally and in CI with GCC and Clang, zero warnings, README updated."
  4. Deliverables. Create lab08/. The tasks also add files at the repository root, because tools expect them there.
    clubhub-<team>/
    ├── CONTRIBUTING.md                     # Task 1
    ├── .gitignore                          # Task 1
    ├── CMakeLists.txt                      # Task 2 (restructured)
    ├── include/clubhub/  src/  tests/      # Task 2 (moved, warnings fixed)
    ├── .github/
    │   ├── workflows/ci.yml                # Task 3, extended in Task 5 and the challenge
    │   └── pull_request_template.md        # Task 4
    ├── README.md                           # Task 3 (CI badge)
    ├── Dockerfile  .dockerignore           # Task 5
    ├── .clang-tidy                         # Challenge
    └── lab08/
        ├── merge-conflict.md               # Task 1
        ├── build-check.md                  # Task 2 (one row per member)
        ├── branch-protection.png           # Task 4
        ├── review-log.md                   # Task 4
        ├── release-notes-v0.1.0.md         # Task 5
        ├── README.md                       # Task 5: record what changed
        ├── analysis-findings.md            # Challenge
        └── dora-metrics.md                 # Challenge
  5. Who leads what. Every member authors at least one pull request and reviews at least one; the Git history is used to check individual contributions.
    RoleLeads
    Scrum Master (Sprint 2)Task 1 (workflow agreement) and Task 4 (branch protection, review rotation)
    Build owner (one Developer)Task 2 (CMake) and Task 3 (CI); helps every member get a green local build
    Product OwnerTask 5 release notes and the decision to tag v0.1.0
    All DevelopersThe deliberate merge conflict (two members), pull requests, reviews, the Dockerfile
1

Git workflow agreement and a merge conflict

Easy 45 min · lead: Scrum Master

Goal

Agree in writing how the team uses Git (branch names, commit messages, who reviews whom), and prove with a deliberate conflict that the team can resolve one safely.

Steps

  1. The Scrum Master creates branch docs/contributing and writes CONTRIBUTING.md with the four sections of the starter: Branches (patterns feature/<issue>-<slug>, fix/<issue>-<slug>, docs/<slug>, ci/<slug>), Commits (type(scope): subject with types feat, fix, test, docs, refactor, build, ci; imperative; at most 72 characters; body explains why; Closes #n), Pull requests and reviews (at most about 400 changed lines, one approval, squash and merge, review within one working day, a rotation table that names every member), Definition of Done.
  2. Add a .gitignore for C++ and CMake (build folders, object files and executables, .vscode/, .idea/, data/*.csv except the sample file, .env, *.pem). Run git status --ignored and check that build/ is listed as ignored. If build output was committed earlier, remove it with git rm -r --cached build.
  3. Every member makes at least one commit in this task under their own account, following the convention (for example, adding their row to the review rotation table).
  4. Prepare the conflict: on main, commit include/clubhub/rules.hpp from the starter. Members A and B then branch from that same commit: demo/conflict-a and demo/conflict-b. A adds the comment // rule R3: cancel up to 24 h before start at the end of the kCancellationWindow line; B rewrites the same line as inline constexpr std::chrono::hours kCancellationWindow{24};. Both commit and push.
  5. A opens a pull request and merges it. B runs git fetch origin and git merge origin/main on demo/conflict-b, gets the conflict, keeps one correct line (with the rule comment), deletes the three marker lines, runs cmake --build build && ctest --test-dir build, then git add, git commit, git push, and opens a pull request that A reviews.
  6. B writes lab08/merge-conflict.md: the git status lines during the conflict, the conflicted hunk exactly as it looked (with markers), the resolved line, the output of git log --oneline --graph -8, and two or three sentences on why it happened and how the team avoids big conflicts (small, short-lived branches). Screenshots are allowed in addition.
  7. Open the pull request for docs/contributing, get one approval, merge.

Starter

# Contributing to ITC Club Hub
Version 1.0 · 2026-11-10 · owner: Scrum Master of Sprint 2

## Branches
- `main` is always releasable; nobody pushes to it directly.
- Pattern: `feature/<issue>-<slug>`, e.g. `feature/42-cancel-deadline`
- TODO: fix/, docs/, ci/ ; delete the branch after merging

## Commits
- Format: `type(scope): subject`, e.g. `fix(registration): close cancellation 24 h before start`
- TODO: allowed types, subject rules, body, issue links

## Pull requests and reviews
- TODO: size limit, template, approvals, who merges, merge method, review time
| Author | Reviews first | Backup reviewer |
|--------|---------------|-----------------|
| TODO   |               |                 |

## Definition of Done
- TODO: copy from Lab-07 and add "CI green with GCC and Clang"
// include/clubhub/rules.hpp: the line both members will change
#pragma once
#include <chrono>

namespace clubhub {

inline constexpr auto kCancellationWindow = std::chrono::hours{24};

}  // namespace clubhub

Expected output

The history after both pull requests should have this shape:

%%{init: {'gitGraph': {'rotateCommitLabel': false}}}%%
gitGraph
  commit id: "rules.hpp"
  branch demo/conflict-a
  commit id: "A comment"
  checkout main
  branch demo/conflict-b
  commit id: "B type"
  checkout main
  merge demo/conflict-a id: "PR A"
  checkout demo/conflict-b
  merge main id: "resolve"
  checkout main
  merge demo/conflict-b id: "PR B"
$ git log --oneline --graph -6        (on demo/conflict-b, after resolving)
*   9c1e0d4 Merge remote-tracking branch 'origin/main' into demo/conflict-b
|\
| * 5ab77f2 docs(rules): explain the 24 h cancellation rule
* | e2f4c19 refactor(rules): declare kCancellationWindow with its type
|/
* 1d93b0a build(rules): add rules.hpp with the cancellation window

Show hints

  • No conflict appeared? Both branches must start from the same commit and change the same line. Check with git log --oneline --graph --all.
  • git diff --name-only --diff-filter=U lists the files that still have conflicts; git merge --abort takes you back to before the merge.
  • VS Code offers "Accept Current / Incoming / Both". Use them, then read the result: the compiler and the tests decide, not the button.
  • For commit messages with a body, run git commit without -m; set your editor with git config --global core.editor "code --wait".

Acceptance checklist

2

CMake: core library, CLI and tests, warnings as errors

Easy 60 min · lead: build owner

Goal

One build description that every member and the CI can run with the same three commands: a core library clubhub_core, the CLI executable clubhub and the test executable clubhub_tests, all compiled with warnings as errors.

Steps

  1. On branch build/cmake-targets, move everything except main() into src/*.cpp with headers in include/clubhub/*.hpp (namespace clubhub). src/main.cpp only parses arguments and calls the core. Include headers as #include "clubhub/event.hpp", with file names in lower case and spelled exactly like on disk.
  2. Write CMakeLists.txt from the starter: project(clubhub VERSION 0.1.0 LANGUAGES CXX), C++20 required, compiler extensions off, CMAKE_EXPORT_COMPILE_COMMANDS ON, then the three targets.
  3. Tests: GoogleTest v1.15.2 through FetchContent, enable_testing(), clubhub_tests linked to clubhub_core and GTest::gtest_main, and gtest_discover_tests(clubhub_tests). Provide at least four tests that assert something: event full, duplicate registration, cancellation inside the 24 h window rejected, cancellation exactly 24 h before the start accepted.
  4. Turn on warnings as errors for the three targets with target_compile_options (-Wall -Wextra -Wpedantic -Werror, or /W4 /WX for MSVC). Fix every warning in the code. Silencing with #pragma, removing flags or disabling tests is not allowed.
  5. Every member clones the branch into a fresh folder and runs cmake -S . -B build && cmake --build build && ctest --test-dir build, then adds a row to lab08/build-check.md (name, OS, compiler and version, CMake version, tests passed, commit hash) in their own commit.
  6. The build owner runs ./build/clubhub --help (or build\Debug\clubhub.exe --help with Visual Studio), pastes the output into build-check.md, and opens the pull request.

Starter

cmake_minimum_required(VERSION 3.28)
project(clubhub VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 1. core library: every .cpp except main.cpp
add_library(clubhub_core STATIC
  src/event.cpp
  src/registration_service.cpp
  # TODO: club.cpp, csv repositories, ...
)
target_include_directories(clubhub_core PUBLIC include)

# 2. CLI
add_executable(clubhub src/main.cpp)
target_link_libraries(clubhub PRIVATE clubhub_core)

# 3. tests
include(FetchContent)
FetchContent_Declare(googletest
  URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
# TODO: add_executable(clubhub_tests ...), link, gtest_discover_tests

# 4. warnings as errors for our three targets
# TODO: CLUBHUB_WARNINGS for MSVC (/W4 /WX) and GCC/Clang, then
#       target_compile_options(... PRIVATE ${CLUBHUB_WARNINGS})
// tests/registration_service_test.cpp
#include <gtest/gtest.h>
#include "clubhub/registration_service.hpp"

namespace {

// Adapt to your Lab-07 API: build the service on in-memory repositories.
class RegistrationServiceTest : public ::testing::Test {
protected:
    // TODO: an Event "E-001" with capacity 2 that starts in 3 days
    // TODO: clubhub::RegistrationService service{...};
};

}  // namespace

TEST_F(RegistrationServiceTest, RejectsWhenEventIsFull) {
    // TODO: register S1 and S2, then
    // EXPECT_THROW(service.registerStudent("S3", "E-001", now), clubhub::EventFull);
}

TEST_F(RegistrationServiceTest, RejectsDuplicateRegistration) { /* TODO */ }
TEST_F(RegistrationServiceTest, RejectsCancellationInsideWindow) { /* TODO */ }
TEST_F(RegistrationServiceTest, AcceptsCancellationExactly24hBefore) { /* TODO */ }

Expected output

$ ctest --test-dir build
Test project /home/dara/clubhub/build
    Start 1: RegistrationServiceTest.RejectsWhenEventIsFull
1/4 Test #1: RegistrationServiceTest.RejectsWhenEventIsFull ..............   Passed    0.01 sec
    Start 2: RegistrationServiceTest.RejectsDuplicateRegistration
2/4 Test #2: RegistrationServiceTest.RejectsDuplicateRegistration ........   Passed    0.00 sec
    Start 3: RegistrationServiceTest.RejectsCancellationInsideWindow
3/4 Test #3: RegistrationServiceTest.RejectsCancellationInsideWindow .....   Passed    0.00 sec
    Start 4: RegistrationServiceTest.AcceptsCancellationExactly24hBefore
4/4 Test #4: RegistrationServiceTest.AcceptsCancellationExactly24hBefore .   Passed    0.00 sec

100% tests passed, 0 tests failed out of 4
MemberOSCompilerCMakeTestsCommit
Sok DaraUbuntu 24.04 (WSL 2)GCC 13.33.28.34/4 passeda41c9e2
Chan SokhaWindows 11MSVC 19.40 (VS 2022)3.30.14/4 passeda41c9e2
………………

Show hints

  • undefined reference to ... at link time usually means a .cpp file is missing from add_library.
  • After moving files, delete build/ and configure again; a stale cache causes strange errors.
  • Visual Studio is a multi-configuration generator: run ctest --test-dir build -C Debug.
  • If warnings come from GoogleTest headers, add SYSTEM to FetchContent_Declare(googletest ... SYSTEM) (CMake 3.25+), so its headers are treated as system headers.
  • MSVC with /W4 /WX rejects std::getenv as "unsafe" (warning C4996), although it is standard C++. In the if(MSVC) branch add add_compile_definitions(_CRT_SECURE_NO_WARNINGS); do not change the C++ code.
  • A test without EXPECT_* or ASSERT_* always passes: that is why empty tests cost points.

Acceptance checklist

3

GitHub Actions CI with a GCC and Clang matrix

Medium 60 min · lead: build owner

Goal

Every push and every pull request is built and tested automatically with GCC and Clang, and the result is visible on the pull request and in the README.

Steps

  1. On branch ci/github-actions, create .github/workflows/ci.yml from the starter.
  2. Triggers: push and pull_request. One job build named build (${{ matrix.cc }}) on ubuntu-latest, matrix entries {cc: gcc, cxx: g++} and {cc: clang, cxx: clang++}, fail-fast: false, CC and CXX set from the matrix.
  3. Steps: actions/checkout@v4, configure (-DCMAKE_BUILD_TYPE=Release), build (--parallel), test (ctest --test-dir build --output-on-failure).
  4. Push and open the pull request ci: build and test with GCC and Clang. Both jobs must be green. In each job's Configure log, find the line The CXX compiler identification is ... and copy both lines for Task 5's README.
flowchart LR
  P["push or PR"] --> G["build (gcc)"] & C["build (clang)"]
  G --> S{"required checks"}
  C --> S
  S -- "both green" --> M["merge enabled"]
  S -- "any red" --> B["merge blocked"]
  1. Prove the gate works: push a commit that breaks one test on purpose (for example expect capacity 3 instead of 2). Watch the red cross on the pull request, copy the failing test's output, then undo it with git revert HEAD and push.
  2. Add the status badge at the top of README.md: [![CI](https://github.com/ORG/REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/ORG/REPO/actions/workflows/ci.yml) with your organisation and repository names.
  3. Get one approval and merge. Check that the run on main after the merge is green too.

Starter

# .github/workflows/ci.yml
name: CI
on:
  push:
  pull_request:

jobs:
  build:
    name: build (${{ matrix.cc }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - { cc: gcc, cxx: g++ }
          # TODO: the Clang entry
    env:
      CC: ${{ matrix.cc }}
      CXX: ${{ matrix.cxx }}
    steps:
      - uses: actions/checkout@v4
      - name: Configure
        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
      # TODO: Build (cmake --build build --parallel)
      # TODO: Test  (ctest --test-dir build --output-on-failure)

Expected output

Actions › CI #12 › ci: build and test with GCC and Clang (pull_request)
  ✓ build (gcc)     1m 38s
  ✓ build (clang)   1m 45s

build (clang) › Configure
  -- The CXX compiler identification is Clang 18.1.3      (version varies)
build (gcc) › Test
  100% tests passed, 0 tests failed out of 4

After the deliberate break:
  ✗ build (gcc)    ✗ build (clang)    "Some checks were not successful"
  RejectsWhenEventIsFull ... Expected: ... Which is: 2

Show hints

  • No run appears? The file must be in .github/workflows/ on the pushed branch and end in .yml; YAML indentation uses spaces only.
  • Green with GCC, red with Clang: read the first error. A missing #include (such as <algorithm>) is common, because one standard library includes more headers indirectly than the other.
  • Green on Windows, red in CI: Linux file names are case-sensitive, so "clubhub/Event.hpp" is not event.hpp.
  • The first run downloads GoogleTest; one to two minutes per job is normal. Add workflow_dispatch: under on: if you want a manual "Run workflow" button.

Acceptance checklist

4

Protect main and review for real

Medium 75 min · lead: Scrum Master, everyone reviews

Goal

Nothing reaches main without a pull request, one approval and green CI, and the team practises reviews that improve the code rather than rubber-stamp it.

Steps

  1. In Settings → Rules → Rulesets, create a branch ruleset for main (or a classic rule under Settings → Branches): require a pull request before merging, 1 required approval, dismiss stale approvals when new commits are pushed, require conversation resolution, require the status checks build (gcc) and build (clang), block force pushes, restrict deletions, no bypass list. Save a screenshot as lab08/branch-protection.png.
  2. Add .github/pull_request_template.md from the starter; every new pull request now opens with it.
  3. Test the rule: commit a small change on your local main and run git push origin main. The push must be rejected. Paste the error into lab08/review-log.md, then reset your local main with git reset --hard origin/main.
  4. Open at least three pull requests with real Sprint 2 work (a story, its tests, a fix), by at least three different authors, each under about 400 changed lines, with the template filled in and Closes #n.
  5. On each of them, the reviewer from the rotation leaves at least one substantive comment on a specific line: a defect, a missing boundary test, a naming or const problem, an unclear error message. Use Request changes or a suggestion. The author answers with a fix commit or a reasoned reply; the reviewer resolves the conversation and approves; the author squash-merges once both checks are green.
  6. Fill in lab08/review-log.md: one row per pull request (number, title, author, reviewer, comments, what changed because of the review, hours from opened to merged).
Rulesets and branch protection are enforced on public repositories and on private repositories of paid or education organisations. If your repository is private on a free account, move it into the course organisation provided by the instructor, or make it public (no personal data in it). If neither is possible, document the rules in CONTRIBUTING.md, follow them manually, and screenshot the settings page with GitHub's message.

Starter

<!-- .github/pull_request_template.md -->
## What and why
Closes #

## How
-

## How to test
cmake --build build && ctest --test-dir build

## Checklist
- [ ] Tests added or updated, all green locally
- [ ] No new warnings (GCC and Clang)
- [ ] No secrets, no real student data in the diff
- [ ] README updated if the CLI behaviour changed
# Review log: Lab-08
| PR  | Title | Author | Reviewer | Comments | Changed after review | Open → merged |
|-----|-------|--------|----------|----------|----------------------|---------------|
| #   |       |        |          |          |                      |       h       |

Expected output

$ git push origin main
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - Changes must be made through a pull request.
remote: - 2 of 2 required status checks are expected.
 ! [remote rejected] main -> main (push declined due to repository rule violations)
(the wording differs slightly for a classic branch protection rule: GH006)
PRTitleAuthorReviewerCommentsChanged after reviewOpen → merged
#18feat(registration): cancel up to 24 h before startDaraSokha3Boundary test added for exactly 24 h; >= changed to >7 h
#19………………

Show hints

  • Status checks appear in the ruleset's list only after the workflow has run at least once; search for build (gcc).
  • Reviewers: the "Add a suggestion" button proposes an exact replacement that the author can commit with one click.
  • "LGTM" alone does not count. Ask a question you cannot answer from the diff: "What happens when the CSV file is empty?"
  • A story too big for 400 lines? Split it: domain rule and tests first, CLI command second.

Acceptance checklist

5

Package and release v0.1.0

Hard 90 min · lead: build owner, release notes: Product Owner

Goal

CI publishes the built executable, the team tags its first release v0.1.0 with release notes, and a Dockerfile builds and runs the CLI anywhere Docker runs. Finish by recording what changed.

Steps

  1. Version. Keep the version only in project(clubhub VERSION 0.1.0 ...) and pass it to the code with target_compile_definitions(clubhub PRIVATE CLUBHUB_VERSION="${PROJECT_VERSION}"). clubhub --version prints clubhub 0.1.0 (starter below).
  2. Artifact. Extend ci.yml: for the GCC job only, add the step Package executable (tar -czf clubhub-linux-x64.tar.gz -C build clubhub) and Upload artifact with actions/upload-artifact@v4, name clubhub-linux-x64, retention-days: 14. After merging, download the artifact from the green run on main, extract it on Linux or WSL and run ./clubhub --version.
  3. Release notes. The Product Owner writes lab08/release-notes-v0.1.0.md: stories included (issue numbers), known issues, how to run the executable and the image.
  4. Tag and release. When the PO agrees and CI on main is green: git switch main && git pull, git tag -a v0.1.0 -m "Sprint 2 start: first delivered increment", git push origin v0.1.0. Create a GitHub release from the tag with the notes and attach clubhub-linux-x64.tar.gz (web page or gh release create).
  5. Container. Write the multi-stage Dockerfile and .dockerignore from the starter. Run docker build -t clubhub:0.1.0 ., docker run --rm clubhub:0.1.0 --version and docker image ls clubhub; record the image size.
  6. Record what changed. Create lab08/README.md with: a table of this lab's files (root and lab08/) with the task that produced each; the earlier-lab inputs you reused with their commit hash (git log -1 --format=%h -- lab07/); links to the green CI run, the release and the reviewed pull requests; the two compiler lines from Task 3; and a change-log row.

Starter

// src/main.cpp (excerpt): print the version passed in by CMake
#include <iostream>
#include <string_view>
#include <vector>

#ifndef CLUBHUB_VERSION
#define CLUBHUB_VERSION "0.0.0-dev"
#endif

int main(int argc, char* argv[]) {
    const std::vector<std::string_view> args(argv + 1, argv + argc);
    if (!args.empty() && args.front() == "--version") {
        std::cout << "clubhub " << CLUBHUB_VERSION << '\n';
        return 0;
    }
    // ... the command dispatch from Lab-07 continues here
    return 0;
}
# Dockerfile
# ---- stage 1: build and test with the full GCC toolchain ----
FROM gcc:14 AS build
# the Debian cmake in this image may be older than 3.28: fetch one
ARG CMAKE_VER=3.31.6
RUN base=https://github.com/Kitware/CMake/releases/download \
 && curl -fsSL "$base/v$CMAKE_VER/cmake-$CMAKE_VER-linux-$(uname -m).tar.gz" \
    | tar -xz --strip-components=1 -C /usr/local
WORKDIR /src
COPY . .
# TODO: configure (Release, -static-libstdc++ -static-libgcc), build, ctest

# ---- stage 2: small runtime image ----
FROM debian:trixie-slim
# TODO: create user clubhub, copy /src/build/clubhub to /usr/local/bin,
#       switch user, set WORKDIR, ENTRYPOINT ["clubhub"], CMD ["--help"]
# .dockerignore
build/
cmake-build-*/
.git/
data/*.csv
# Lab-08: DevOps practices, record of changes
Version 1.0 · date · team name

## Files produced in this lab
| File | Task | Purpose |
|------|------|---------|
| CONTRIBUTING.md | 1 | Branch, commit and review rules |
| ... | | |

## Inputs reused from earlier labs
| Input | Lab | Commit |
|-------|-----|--------|
| Sprint 1 code and tests | Lab-07 | `3f9c2ab` |

## Evidence
- CI run on main: <link> · Release v0.1.0: <link> · Reviewed PRs: #18, #19, #21
- Compilers in CI: GCC ..., Clang ...

## Change log
| Date | Version | Change | By |
|------|---------|--------|----|
| 2026-11-14 | 0.1.0 | CI pipeline, protected main, first release | whole team |

Expected output

$ docker build -t clubhub:0.1.0 .
 ...
 => [build 5/5] RUN cmake -S . -B build ... && ctest --test-dir build ...
 => [stage-1 3/3] COPY --from=build /src/build/clubhub /usr/local/bin/clubhub
$ docker run --rm clubhub:0.1.0 --version
clubhub 0.1.0
$ docker image ls clubhub
REPOSITORY   TAG     IMAGE ID       CREATED         SIZE
clubhub      0.1.0   4f2c9a1d7e3b   2 minutes ago   ~85MB      (varies; far below the 1 GB+ build stage)

Show hints

  • CMakeCache.txt directory ... is different inside Docker: your local build/ was copied in; check .dockerignore.
  • version `GLIBCXX_3.4.32' not found when the container starts: the static -static-libstdc++ -static-libgcc linker flags are missing.
  • GitHub downloads artifacts as a zip that contains your .tar.gz; the tar file keeps the executable permission.
  • Tagged the wrong commit and nobody used it yet? git tag -d v0.1.0 and git push origin :refs/tags/v0.1.0, then tag again. Once published, release v0.1.1 instead.
  • On Apple silicon the $(uname -m) in the CMake URL selects the aarch64 download automatically.

Acceptance checklist

Challenge: static analysis, sanitizers and your DORA metrics

Hard 90–120 min · optional, extra credit

Scenario

At the Sprint 2 planning, the client asks two questions: "How do you know the code has no hidden memory bugs?" and "How reliable is your delivery?". Answer the first with two more quality gates in CI, and the second with numbers taken from your own GitHub history.

Requirements

  1. Static analysis job analysis (in ci.yml or a new .github/workflows/analysis.yml): configure with Clang so that compile_commands.json is written, then run clang-tidy with the committed .clang-tidy on the files in src/, or cppcheck --project=build/compile_commands.json --enable=warning,performance,portability --error-exitcode=1. The job fails on any finding.
  2. Sanitizer job sanitize: add the CLUBHUB_SANITIZE option to CMakeLists.txt (before the targets; -fsanitize=address,undefined -fno-omit-frame-pointer for compiling and linking), build with Clang in Debug into build-asan, run ctest --output-on-failure with UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1.
  3. Fix every real finding, each in its own small pull request. List them in lab08/analysis-findings.md: tool, check name, file and line, fix commit. At most two suppressions, each with // NOLINT(check-name): reason.
  4. Add analysis and sanitize to the required status checks of main.
  5. DORA metrics in lab08/dora-metrics.md for the 14 days that end on your submission day: first write down your definition of a "deployment" (for example a merge to main whose CI run produced an artifact, or a release tag) and of a "failure" (a deployment followed by a fix or revert pull request). Collect the data with the commands below, compute the four metrics showing the working, compare them with the bands on the DORA slide, and add one improvement as a backlog issue for Sprint 2.

Skeleton

# .clang-tidy
Checks: >
  -*,
  bugprone-*,
  -bugprone-easily-swappable-parameters,
  performance-*,
  modernize-use-nodiscard,
  modernize-use-override,
  cppcoreguidelines-init-variables
WarningsAsErrors: '*'
HeaderFilterRegex: 'include/clubhub/.*'
# additional jobs under "jobs:" in the workflow
  analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install clang-tidy
        run: sudo apt-get update && sudo apt-get install -y clang-tidy
      - name: Configure (writes compile_commands.json)
        run: cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++
      - name: clang-tidy
        run: run-clang-tidy -p build -quiet 'src/.*'

  sanitize:
    runs-on: ubuntu-latest
    env:
      CC: clang
      CXX: clang++
      UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
    steps:
      - uses: actions/checkout@v4
      # TODO: configure build-asan (Debug, -DCLUBHUB_SANITIZE=ON)
      # TODO: build, then ctest --test-dir build-asan --output-on-failure
# merged pull requests since the start date: number, opened, merged, title
gh pr list --state merged --search "merged:>=YYYY-MM-DD" --limit 100 \
  --json number,createdAt,mergedAt,title \
  --jq '.[] | [.number, .createdAt, .mergedAt, .title] | @tsv'

# CI runs on main: candidate deployments and their result
gh run list --branch main --workflow ci.yml --limit 50 \
  --json databaseId,conclusion,createdAt,headSha

# releases and tags
gh release list

Expected output

| Metric                  | Data (14 days)                                   | Result         | DORA band |
|-------------------------|--------------------------------------------------|----------------|-----------|
| Deployment frequency    | 9 merges to main with a green artifact           | 4.5 per week   | high      |
| Lead time for changes   | median PR opened → merged over 14 PRs            | 11 h           | elite     |
| Change failure rate     | 2 of 9 deployments followed by a fix/revert PR   | 22 %           | medium    |
| Time to restore service | 40 min and 5 h                                   | median 2 h 50  | high      |
Improvement for Sprint 2: #34 "Add CSV edge-case tests (empty file, missing column)"

Show hints

  • add_compile_options only affects targets created after it, so put the sanitizer block before add_library.
  • Sanitizers and MSVC do not mix in this setup; guard the block with if(CLUBHUB_SANITIZE AND NOT MSVC).
  • An empty CSV file, an event with capacity 0 and reading vector[size()] are good places to look for sanitizer findings.
  • Lead time from PR opened to merged is an approximation (the first commit may be earlier); say so in the file.
  • Use the median for times: one pull request that stayed open for a week would distort the mean.

Acceptance checklist

Grading rubric

ComponentPointsFull marks when…
Task 1 · Git workflow and merge conflict10CONTRIBUTING.md complete with a rotation naming everyone; correct .gitignore; conflict documented with markers, resolution and log graph.
Task 2 · CMake targets and warnings15Three targets as specified, warnings as errors with zero warnings, at least four meaningful tests including the boundary, a green row per member.
Task 3 · GitHub Actions CI20GCC and Clang matrix on push and pull request, tests with output on failure, the gate demonstrated red then green, badge in README.
Task 4 · Protected main and reviews20Rule enforced (screenshot, rejected push), at least three reviewed pull requests by three authors with substantive comments and a filled review log.
Task 5 · Artifact, release and container25Artifact uploaded and runnable, v0.1.0 tag and release with notes, working multi-stage Dockerfile, complete lab08/README.md.
Engineering quality10Commit messages follow the convention, pull requests are small and focused, every member visible in commits and reviews, no secrets or data in history.
★ Challenge (extra credit)+20Analysis and sanitizer jobs required and green, findings fixed and listed, DORA metrics computed from real history with one improvement issue.
Total100 (+20)
Automatic deductions:
  • −10 if the CI pipeline is not green on main at the deadline.
  • −10 if a secret, real student data or build output (build/, executables) is committed; a leaked secret must also be rotated.
  • −5 per warning silenced with a #pragma, a removed flag or a disabled test instead of a fix.
  • −5 per pull request merged without an approval or while a required check was failing.
  • −5 per test that asserts nothing.
  • −5 per team member with neither a commit nor a review in this lab's history.

Submission

  1. Repository layout at the end of the lab:
    clubhub-<team>/
    ├── .github/
    │   ├── workflows/ci.yml            (+ analysis.yml for the challenge)
    │   └── pull_request_template.md
    ├── .gitignore  .dockerignore  .clang-tidy
    ├── CMakeLists.txt  Dockerfile
    ├── CONTRIBUTING.md  README.md      (with the CI badge)
    ├── include/clubhub/  src/  tests/
    ├── data/sample-events.csv          (fake data only)
    ├── lab01/ … lab07/
    └── lab08/
        ├── README.md  merge-conflict.md  build-check.md
        ├── branch-protection.png  review-log.md  release-notes-v0.1.0.md
        └── analysis-findings.md  dora-metrics.md   (challenge)
  2. Self-check from a clean clone (every member, then once more before submitting):
    git clone https://github.com/ORG/clubhub-TEAM.git check && cd check
    cmake -S . -B build && cmake --build build && ctest --test-dir build
    docker build -t clubhub:0.1.0 . && docker run --rm clubhub:0.1.0 --version
  3. The task pull requests are merged into main during the lab. Put the remaining lab08/ files on branch lab-08 and open the pull request Lab-08 – <team name> into main; it must pass CI and be approved like any other. In its description, link the green CI run, the v0.1.0 release and the three reviewed pull requests.