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.
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).
Setup
20 min- 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 - 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),RegistrationServicewithregisterStudent,cancel,checkIn; interfacesEventRepositoryandRegistrationRepositorywith CSV-backed implementations. - Layout
CMakeLists.txt,src/,include/clubhub/,tests/(GoogleTest viaFetchContent),.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.
- Inputs from earlier labs. If one of them is incomplete, use the fallback and carry on; do not stop to repair the earlier lab.
Input Where What you need from it Fallback if missing Team repository and roles Lab-01, README.mdA GitHub repository with every member as collaborator (Write access); who is PO and Sprint 2 Scrum Master Create clubhub-<team>now, invite everyone, write the roles intoREADME.md.Backlog and user stories Lab-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 model Lab-06 ( lab06/)Class and file names Use the C++ names in the case-study brief. Sprint 1 increment Lab-07: src/,include/,tests/,lab07/Code that builds and at least a few tests of RegistrationServiceStart from the Task 2 starter: Event,RegistrationService::registerStudentwith the capacity rule, and the four test cases.Definition of Done Lab-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." - 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 - 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.
Role Leads 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 Owner Task 5 release notes and the decision to tag v0.1.0All Developers The deliberate merge conflict (two members), pull requests, reviews, the Dockerfile
Git workflow agreement and a merge conflict
Easy 45 min · lead: Scrum MasterGoal
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
- The Scrum Master creates branch
docs/contributingand writesCONTRIBUTING.mdwith the four sections of the starter: Branches (patternsfeature/<issue>-<slug>,fix/<issue>-<slug>,docs/<slug>,ci/<slug>), Commits (type(scope): subjectwith typesfeat,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. - Add a
.gitignorefor C++ and CMake (build folders, object files and executables,.vscode/,.idea/,data/*.csvexcept the sample file,.env,*.pem). Rungit status --ignoredand check thatbuild/is listed as ignored. If build output was committed earlier, remove it withgit rm -r --cached build. - 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).
- Prepare the conflict: on
main, commitinclude/clubhub/rules.hppfrom the starter. Members A and B then branch from that same commit:demo/conflict-aanddemo/conflict-b. A adds the comment// rule R3: cancel up to 24 h before startat the end of thekCancellationWindowline; B rewrites the same line asinline constexpr std::chrono::hours kCancellationWindow{24};. Both commit and push. - A opens a pull request and merges it. B runs
git fetch originandgit merge origin/mainondemo/conflict-b, gets the conflict, keeps one correct line (with the rule comment), deletes the three marker lines, runscmake --build build && ctest --test-dir build, thengit add,git commit,git push, and opens a pull request that A reviews. - B writes
lab08/merge-conflict.md: thegit statuslines during the conflict, the conflicted hunk exactly as it looked (with markers), the resolved line, the output ofgit 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. - 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
- 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=Ulists the files that still have conflicts;git merge --aborttakes 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 commitwithout-m; set your editor withgit config --global core.editor "code --wait".
Acceptance checklist
CMake: core library, CLI and tests, warnings as errors
Easy 60 min · lead: build ownerGoal
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
- On branch
build/cmake-targets, move everything exceptmain()intosrc/*.cppwith headers ininclude/clubhub/*.hpp(namespaceclubhub).src/main.cpponly 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. - Write
CMakeLists.txtfrom 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. - Tests: GoogleTest
v1.15.2throughFetchContent,enable_testing(),clubhub_testslinked toclubhub_coreandGTest::gtest_main, andgtest_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. - Turn on warnings as errors for the three targets with
target_compile_options(-Wall -Wextra -Wpedantic -Werror, or/W4 /WXfor MSVC). Fix every warning in the code. Silencing with#pragma, removing flags or disabling tests is not allowed. - 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 tolab08/build-check.md(name, OS, compiler and version, CMake version, tests passed, commit hash) in their own commit. - The build owner runs
./build/clubhub --help(orbuild\Debug\clubhub.exe --helpwith Visual Studio), pastes the output intobuild-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
| Member | OS | Compiler | CMake | Tests | Commit |
|---|---|---|---|---|---|
| Sok Dara | Ubuntu 24.04 (WSL 2) | GCC 13.3 | 3.28.3 | 4/4 passed | a41c9e2 |
| Chan Sokha | Windows 11 | MSVC 19.40 (VS 2022) | 3.30.1 | 4/4 passed | a41c9e2 |
| … | … | … | … | … | … |
undefined reference to ...at link time usually means a.cppfile is missing fromadd_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
SYSTEMtoFetchContent_Declare(googletest ... SYSTEM)(CMake 3.25+), so its headers are treated as system headers. - MSVC with
/W4 /WXrejectsstd::getenvas "unsafe" (warning C4996), although it is standard C++. In theif(MSVC)branch addadd_compile_definitions(_CRT_SECURE_NO_WARNINGS); do not change the C++ code. - A test without
EXPECT_*orASSERT_*always passes: that is why empty tests cost points.
Acceptance checklist
GitHub Actions CI with a GCC and Clang matrix
Medium 60 min · lead: build ownerGoal
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
- On branch
ci/github-actions, create.github/workflows/ci.ymlfrom the starter. - Triggers:
pushandpull_request. One jobbuildnamedbuild (${{ matrix.cc }})onubuntu-latest, matrix entries{cc: gcc, cxx: g++}and{cc: clang, cxx: clang++},fail-fast: false,CCandCXXset from the matrix. - Steps:
actions/checkout@v4, configure (-DCMAKE_BUILD_TYPE=Release), build (--parallel), test (ctest --test-dir build --output-on-failure). - 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 lineThe 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"]
- 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 HEADand push. - Add the status badge at the top of
README.md:[](https://github.com/ORG/REPO/actions/workflows/ci.yml)with your organisation and repository names. - Get one approval and merge. Check that the run on
mainafter 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
- 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 notevent.hpp. - The first run downloads GoogleTest; one to two minutes per job is normal. Add
workflow_dispatch:underon:if you want a manual "Run workflow" button.
Acceptance checklist
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
- 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 checksbuild (gcc)andbuild (clang), block force pushes, restrict deletions, no bypass list. Save a screenshot aslab08/branch-protection.png. - Add
.github/pull_request_template.mdfrom the starter; every new pull request now opens with it. - Test the rule: commit a small change on your local
mainand rungit push origin main. The push must be rejected. Paste the error intolab08/review-log.md, then reset your localmainwithgit reset --hard origin/main. - 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. - 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
constproblem, 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. - 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).
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)
| PR | Title | Author | Reviewer | Comments | Changed after review | Open → merged |
|---|---|---|---|---|---|---|
| #18 | feat(registration): cancel up to 24 h before start | Dara | Sokha | 3 | Boundary test added for exactly 24 h; >= changed to > | 7 h |
| #19 | … | … | … | … | … | … |
- 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
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
- Version. Keep the version only in
project(clubhub VERSION 0.1.0 ...)and pass it to the code withtarget_compile_definitions(clubhub PRIVATE CLUBHUB_VERSION="${PROJECT_VERSION}").clubhub --versionprintsclubhub 0.1.0(starter below). - 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 withactions/upload-artifact@v4, nameclubhub-linux-x64,retention-days: 14. After merging, download the artifact from the green run onmain, extract it on Linux or WSL and run./clubhub --version. - 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. - Tag and release. When the PO agrees and CI on
mainis 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 attachclubhub-linux-x64.tar.gz(web page orgh release create). - Container. Write the multi-stage
Dockerfileand.dockerignorefrom the starter. Rundocker build -t clubhub:0.1.0 .,docker run --rm clubhub:0.1.0 --versionanddocker image ls clubhub; record the image size. - Record what changed. Create
lab08/README.mdwith: a table of this lab's files (root andlab08/) 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)
CMakeCache.txt directory ... is differentinside Docker: your localbuild/was copied in; check.dockerignore.version `GLIBCXX_3.4.32' not foundwhen the container starts: the static-static-libstdc++ -static-libgcclinker 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.0andgit push origin :refs/tags/v0.1.0, then tag again. Once published, releasev0.1.1instead. - On Apple silicon the
$(uname -m)in the CMake URL selects theaarch64download automatically.
Acceptance checklist
Challenge: static analysis, sanitizers and your DORA metrics
Hard 90–120 min · optional, extra creditScenario
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
- Static analysis job
analysis(inci.ymlor a new.github/workflows/analysis.yml): configure with Clang so thatcompile_commands.jsonis written, then runclang-tidywith the committed.clang-tidyon the files insrc/, orcppcheck --project=build/compile_commands.json --enable=warning,performance,portability --error-exitcode=1. The job fails on any finding. - Sanitizer job
sanitize: add theCLUBHUB_SANITIZEoption toCMakeLists.txt(before the targets;-fsanitize=address,undefined -fno-omit-frame-pointerfor compiling and linking), build with Clang inDebugintobuild-asan, runctest --output-on-failurewithUBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1. - 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. - Add
analysisandsanitizeto the required status checks ofmain. - DORA metrics in
lab08/dora-metrics.mdfor the 14 days that end on your submission day: first write down your definition of a "deployment" (for example a merge tomainwhose 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)"
add_compile_optionsonly affects targets created after it, so put the sanitizer block beforeadd_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
| Component | Points | Full marks when… |
|---|---|---|
| Task 1 · Git workflow and merge conflict | 10 | CONTRIBUTING.md complete with a rotation naming everyone; correct .gitignore; conflict documented with markers, resolution and log graph. |
| Task 2 · CMake targets and warnings | 15 | Three 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 CI | 20 | GCC 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 reviews | 20 | Rule 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 container | 25 | Artifact uploaded and runnable, v0.1.0 tag and release with notes, working multi-stage Dockerfile, complete lab08/README.md. |
| Engineering quality | 10 | Commit 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) | +20 | Analysis and sanitizer jobs required and green, findings fixed and listed, DORA metrics computed from real history with one improvement issue. |
| Total | 100 (+20) |
- −10 if the CI pipeline is not green on
mainat 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
- 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) - 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 - The task pull requests are merged into
mainduring the lab. Put the remaininglab08/files on branchlab-08and open the pull requestLab-08 – <team name>intomain; it must pass CI and be approved like any other. In its description, link the green CI run, thev0.1.0release and the three reviewed pull requests.