CMPUT-499 · Lesson 6 of 12 · 29 min
Engineering practice: repository, reviews, pipelines and environments
A capstone team writes perhaps ten to twenty thousand lines of code in ten weeks, five people at once, while learning the domain. Without shared engineering practice, that produces merge conflicts, broken builds, code nobody else understands and a deployment that works only from one person's laptop. With it, the team moves faster as the term goes on instead of slower. CMPUT-301 covered version control, code quality and continuous integration; CMPUT-404 covered deployment; AI-420 covered pipelines for machine learning systems. This lesson assembles them into the setup a capstone team should have running by the end of week 5, before the first sprint starts.
Treat setup as a deliverable with an owner. On Team Relay, Ethan owned the pipeline and environments and Lin owned the testing framework and contribution guidelines, but everyone took part, and the setup was reviewed at the design review. The investment is front-loaded and pays back every day: a new feature in week 9 goes from idea to staging in a few hours because the path is already paved.
The repository
One repository holds the application code, tests, infrastructure configuration, database migrations, documentation and ADRs. A capstone almost never needs more than one: separate repositories for front end and back end multiply the work of keeping them in step. The top level should be self-explanatory to a newcomer: a README that says what the project is and how to run it in a few commands; a CONTRIBUTING file with the team's conventions; a LICENSE file, MIT for Team Relay; a docs folder with architecture diagrams and ADRs; and a .gitignore that keeps secrets, local databases and build output out of version control from the first commit.
| Path | Contents |
|---|---|
| README.md | What RescueRoute is, quick start with Docker, links to docs |
| CONTRIBUTING.md | Branching, commit messages, review rules, definition of done |
| LICENSE | MIT licence, as agreed with the client |
| app/ | Application code: routes, models, templates, services |
| worker/ | Background jobs: reminders, nightly report |
| migrations/ | Database schema changes, applied in order |
| tests/ | Unit, integration and end-to-end tests |
| docs/ | C4 diagrams, ADRs, user guide, admin guide, runbook |
| .github/workflows/ | CI and deployment pipeline |
| Dockerfile, compose.yaml | Reproducible local and production environments |
Branching and commits
Keep branching simple. Team Relay used trunk-based development with short-lived feature branches: the main branch is always deployable, every change happens on a branch named after its issue, such as rr-12-claim-shift, and branches are merged through a pull request within a day or two. Long-lived branches are the enemy: a branch that lives for two weeks diverges from main and produces a painful merge. Large features are split into small pull requests, and unfinished work can be merged behind a feature flag, a configuration switch that hides it from users until it is ready. Team Relay's SMS reminders lived behind a flag for most of the term.
Commit messages are documentation for your future self. Write a short imperative summary line, such as 'Reopen shift when driver cancels', and, when the reason is not obvious, a body explaining why. Link commits and pull requests to issues so the history connects code to requirements. Many teams follow the Conventional Commits format, with prefixes such as feat:, fix: and docs:, which also makes it easy to generate release notes. Consistency matters more than which convention you pick; write it in CONTRIBUTING and enforce it gently in review.
Code review
Every change is reviewed by at least one other team member before it merges. Review catches bugs, but its larger value in a student team is spreading knowledge: after a term of reviews, everyone has read most of the code, so nobody is a single point of failure. Keep pull requests small, ideally under 400 changed lines, because review quality falls sharply on large ones. The author writes a description saying what changed, why and how it was tested, with a screenshot for interface changes. The reviewer checks correctness, tests, readability, security and accessibility against a short checklist, and is kind: comment on the code, not the person, and explain the reason behind each request.
Agree on turnaround. A pull request that waits three days for review blocks its author and grows stale. Team Relay's working agreement, revised after sprint 1, was that reviews take priority over starting new work, and that a pull request should get a first review within one working day. They also agreed that formatting arguments are settled by an automatic formatter and linter, not by reviewers, which removed a whole category of unproductive comments.
Continuous integration and delivery
Continuous integration runs automated checks on every pull request: formatting and linting, the test suite, dependency and secret scanning, and any custom checks such as the ADR validator from lesson 5. A pull request cannot merge unless the checks pass. Continuous delivery goes further: every merge to main is automatically deployed to a staging environment, and promoting to production is a single, repeatable step. The point is to make releasing boring. A team that deploys by hand once at the end of term discovers its deployment problems in the last week; a team that deploys automatically twenty times a day discovers them in week 5 when they are easy to fix.
# .github/workflows/ci.yml (GitHub Actions; other CI services use similar files)
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test, POSTGRES_DB: rescueroute_test }
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: ruff check . # style and common bug patterns
- run: pytest --cov=app --cov-report=term-missing
env: { DATABASE_URL: "postgresql://postgres:test@localhost:5432/rescueroute_test" }
- run: pip-audit -r requirements.txt # known vulnerable dependencies
- run: python tools/check_adrs.py # ADR folder consistency (lesson 5)
deploy-staging:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh staging # runs migrations, then releases
env: { DEPLOY_TOKEN: "${{ secrets.STAGING_DEPLOY_TOKEN }}" }Use the same container image everywhere. Team Relay's Dockerfile builds one image that runs the application on laptops, in CI and on the host, which removes most 'works on my machine' problems. A compose file starts the application and a local PostgreSQL together for development, so a new team member, or the co-op's future volunteer, can run the whole system with one command. The image runs as an ordinary user rather than root, a small habit that limits damage if the application is ever compromised.
# Dockerfile: the same image runs on laptops, in CI and on the host
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ app/
RUN useradd --create-home appuser
USER appuser # do not run as root
ENV PORT=8000
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]Environments and configuration
A capstone needs three environments: local, where developers work; staging, a shared copy of production with test data, deployed automatically from main and used for demos and client testing; and production, used by the real co-op. Each environment differs only in configuration, which comes from environment variables, not from code: the database address, the email service key, feature flags, the site's address. This is one of the principles of the twelve-factor app methodology, and it means the same code and image can run anywhere. Production data never goes into local or staging environments; staging uses a generated data set that looks realistic but contains no real people.
Secrets management
Secrets, such as database passwords, API keys and deployment tokens, must never be committed to the repository, even a private one, and even briefly. Git history is permanent: a secret committed and deleted in the next commit is still in the history, in every clone and in any fork. Store secrets in the hosting platform's secret settings and in the CI system's encrypted secrets, and read them from environment variables. Add automated scanning so mistakes are caught before they leave a laptop: a pre-commit hook that refuses commits containing likely keys, and the hosting service's secret scanning for anything that slips through.
# A pre-commit style secret scan: refuse commits that contain likely credentials.
# Patterns are simplified; tools such as gitleaks or GitHub secret scanning use many more.
# The "keys" below are the published documentation examples or obviously fake.
import re
files = {
"app/settings.py": 'DATABASE_URL = os.environ["DATABASE_URL"]\nDEBUG = False\n',
"deploy.sh": 'curl -H "Authorization: Bearer $DEPLOY_TOKEN" https://host.example/deploy\n',
"notes/aws.txt": "aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n",
"app/sms.py": 'SMS_API_KEY = "live_9f8e7d6c5b4a39281706f5e4d3c2b1a0" # TODO move to env\n',
"tests/test_login.py": 'PASSWORD = "correct horse battery staple" # test fixture\n',
}
patterns = {
"AWS access key id": r"AKIA[0-9A-Z]{16}",
"hard-coded API key": r"(?i)(api_key|secret|token)\s*=\s*['\"][A-Za-z0-9_\-]{20,}['\"]",
"private key block": r"-----BEGIN (RSA |EC )?PRIVATE KEY-----",
}
allow = {"tests/"} # fixtures in tests are allowed (reviewed by a human)
blocked = 0
for path, text in files.items():
for name, pat in patterns.items():
if re.search(pat, text) and not any(path.startswith(a) for a in allow):
blocked += 1
print(f"BLOCK {path}: {name}")
print("commit refused" if blocked else "commit allowed", f"({blocked} finding{'s' if blocked != 1 else ''})")BLOCK notes/aws.txt: AWS access key id BLOCK app/sms.py: hard-coded API key commit refused (2 findings)
If a secret does leak, the fix is to revoke and replace it immediately, not to rewrite history and hope. Rewriting history to remove the secret is worth doing afterwards, but assume anyone could already have copied it. Team Relay rehearsed this in week 6: Ethan deliberately committed a test key to a branch, the scan caught it, and the team practised rotating the key on the email service. When a real near-miss happened in week 9, with an SMS key pasted into a code comment, the scan blocked the commit and nobody needed to rotate anything.
Dependencies and the software bill of materials
Most of RescueRoute's code, measured in lines, is other people's: the framework, the database driver, template and date libraries and their own dependencies. Pin versions in a lock file so that every environment installs exactly the same versions, and update them deliberately. Run an automated audit that compares your dependencies with public vulnerability databases and fails the build on serious findings; tools such as pip-audit, npm audit and GitHub's Dependabot do this. CISSP-117 covered software supply chain security in depth, including software bills of materials (SBOMs), machine-readable lists of every component in a release. Generate one at release time and include it in the handover so the co-op knows what it is running.
# Dependency hygiene: compare pinned versions with a list of advisories.
# Real projects use pip-audit, npm audit or GitHub Dependabot; this shows the idea.
# Package names and advisories below are invented for the example.
pinned = {"webframe": "2.3.1", "templater": "3.1.2", "sqldriver": "2.9.5",
"httpclient": "0.25.0", "markdownlib": "3.4.0", "dateparse": "1.2.0"}
advisories = [
# (package, fixed in version, severity, summary)
("templater", "3.1.4", "high", "sandbox escape in template filters"),
("httpclient", "0.24.1", "medium", "proxy credentials leaked on redirect"),
("markdownlib", "3.5.2", "low", "slow regex on crafted input"),
("yamlparse", "6.0.1", "high", "code execution in unsafe loader"),
]
def v(s): return tuple(int(x) for x in s.split("."))
found = []
for pkg, fixed, sev, summary in advisories:
if pkg in pinned and v(pinned[pkg]) < v(fixed):
found.append((sev, pkg, pinned[pkg], fixed, summary))
order = {"high": 0, "medium": 1, "low": 2}
for sev, pkg, have, fixed, summary in sorted(found, key=lambda f: order[f[0]]):
print(f"{sev.upper():6} {pkg} {have} -> upgrade to {fixed}: {summary}")
print(f"{len(found)} of {len(pinned)} dependencies need attention;",
"CI fails on high" if any(f[0] == "high" for f in found) else "no high findings")HIGH templater 3.1.2 -> upgrade to 3.1.4: sandbox escape in template filters LOW markdownlib 3.4.0 -> upgrade to 3.5.2: slow regex on crafted input 2 of 6 dependencies need attention; CI fails on high
Infrastructure as code and cost awareness
Infrastructure as code means describing hosting resources, such as the web service, worker, database and their settings, in files kept in the repository, rather than clicking through a web console. Many platforms accept a single configuration file; larger setups use tools such as Terraform. Even for a small capstone, writing the configuration down means the co-op, or a future volunteer, can recreate the system from the repository alone. Pair it with a written note of every account the system depends on, who owns it and how to recover it, because the most common reason a handed-over student project dies is that nobody knows the password to the hosting account.
Watch cost from the first day. Free tiers are generous for development but often limit uptime, sleep idle services, or expire after a trial. Estimate the monthly cost of the production setup, compare it with the client's budget, and show the estimate at the design review. Set billing alerts. Team Relay discovered in week 6 that their chosen free database tier deleted inactive databases after 90 days, which would have been a disaster for a volunteer organization that might not touch the system for a summer. They moved to the cheapest paid tier and recorded the decision, and the cost, in an ADR.
# Monthly hosting estimate for the co-op, compared with its budget.
# Prices are illustrative placeholders; check the provider's current price list.
plan = {
"web app (entry tier, 512 MB)": 7.00,
"background worker (entry tier)": 7.00,
"managed PostgreSQL (1 GB)": 9.00,
"domain name (yearly / 12)": 1.50,
"email delivery (first 3,000 free)": 0.00,
"uptime monitor (free plan)": 0.00,
}
sms_per_message = 0.0085
reminders_per_month = 270 # about 62 pickups a week, one reminder each
sms_share = 0.30 # drivers who chose text over email
plan["SMS reminders"] = round(reminders_per_month * sms_share * sms_per_message, 2)
budget = 40.00
total = sum(plan.values())
for item, cost in plan.items():
print(f"{item:36} {cost:6.2f}")
print(f"{'total per month':36} {total:6.2f} (budget {budget:.2f}, headroom {budget - total:.2f})")
print(f"per year: {12 * total:.2f}")web app (entry tier, 512 MB) 7.00 background worker (entry tier) 7.00 managed PostgreSQL (1 GB) 9.00 domain name (yearly / 12) 1.50 email delivery (first 3,000 free) 0.00 uptime monitor (free plan) 0.00 SMS reminders 0.69 total per month 25.19 (budget 40.00, headroom 14.81) per year: 302.28
Worked examples
Worked problem
A leaked key
A teammate commits an email-service API key to main and removes it in the next commit an hour later. What should the team do?
Show solution
- Assume the key is compromised: it remains in the Git history and in every clone.
- Revoke the key in the email service and create a new one immediately.
- Store the new key in the platform and CI secret settings, read from an environment variable.
- Optionally rewrite history to remove the key, and add or fix the secret scan that should have caught it.
Answer: Rotate the key first; deleting the line does not remove it from history.
Worked problem
Choosing a branching approach
A team plans one branch per sprint, merged at the end of each sprint. What problem is likely, and what is better?
Show solution
- A two-week branch diverges from main and from other branches.
- The end-of-sprint merge has many conflicts and untested combinations.
- Short-lived branches per issue, merged in a day or two, keep integration continuous; unfinished work can hide behind feature flags.
Answer: Expect painful merges; use short-lived feature branches and merge continuously.
Worked problem
Reading the cost estimate
Using the estimate above, what happens to the monthly total if every driver chooses text reminders instead of 30 per cent?
Show solution
- SMS cost becomes \(270 \times 1.0 \times 0.0085 \approx 2.30\) instead of about 0.69.
- The total rises by about 1.60 a month, still well within the 40 budget.
Answer: About 1.60 a month more; SMS volume is not the main cost driver at this scale, but its consent rules still matter.
Common mistakes
MistakeKeep front end and back end in separate repositories for a small capstone.
InsteadUse one repository so code, tests, docs and configuration change together.
MistakeLet branches live for a whole sprint.
InsteadMerge small pull requests within a day or two; use feature flags for unfinished work.
MistakeDelete a committed secret and move on.
InsteadRevoke and replace it immediately; history keeps the old value.
MistakeDeploy by hand for the first time in the last week.
InsteadDeploy automatically to staging from week 5 so deployment problems appear early.
MistakeRely on a free tier without reading its limits.
InsteadCheck sleep, deletion and expiry rules, estimate production cost and set billing alerts.
Summary and key terms
- Set up the repository, pipeline and environments before sprint 1 and treat setup as a deliverable.
- One repository with README, CONTRIBUTING, LICENSE, docs and a .gitignore from the first commit.
- Short-lived branches, small pull requests, feature flags and clear commit messages keep integration continuous.
- Review every change within a day; let formatters settle style; review spreads knowledge.
- CI checks every pull request; every merge deploys to staging; promotion to production is one step.
- Configuration comes from environment variables; secrets never enter the repository and are rotated if leaked.
- Pin and audit dependencies, write infrastructure as code, record every account and watch cost from day one.
- Trunk-based development
- A branching approach where short-lived branches merge frequently into an always-deployable main branch.
- Pull request
- A proposed change submitted for review and automated checks before merging.
- Feature flag
- A configuration switch that turns a feature on or off without changing code.
- Continuous integration
- Automatically building and testing every change as it is proposed.
- Continuous delivery
- Keeping every merged change deployable, with automatic deployment to staging.
- Staging environment
- A shared copy of production with test data, used for demos and testing.
- Secret
- A credential such as a password, API key or token that grants access to a system.
- Infrastructure as code
- Describing hosting resources in version-controlled files rather than by hand.
Practice problems
Work each problem on paper first, then reveal the answer to check.
List four files every capstone repository should have at the top level.
Show answer
README, CONTRIBUTING, LICENSE and .gitignore; also a docs folder.
Name three checks to run in CI on every pull request.
Show answer
Lint and format, tests, dependency audit; also secret scanning and custom checks.
Why should staging never contain production data?
Show answer
It is less protected and widely accessed; real personal data must stay in production.
What is the first action when a secret leaks?
Show answer
Revoke and replace it.
Give one reason to use the same container image everywhere.
Show answer
It removes differences between laptops, CI and production that cause "works on my machine" bugs.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.