Lesson 5 - Guided Project: Add CI and Issue Templates
On this page
Welcome to the Guided Project
You’ve learned the pieces of GitHub automation one at a time: issues that track work, Actions that run on events, and templates that shape how people contribute. Now you’ll bring them together on a single repository. The goal is simple and very real: take a small project and make it self-checking and easy to report bugs against. By the time you finish, every push will run your test suite automatically, and anyone opening an issue will be guided through a structured form instead of staring at a blank box.
We’ll work with a tiny calculator project — small enough to read at a glance, but complete with tests, which is exactly what continuous integration needs. The skills transfer directly: a repository with thousands of files uses the same .github/ files you’re about to write.
By the end of this project, you will be able to:
- Verify a test suite passes locally before automating it
- Add a GitHub Actions CI workflow that runs your tests on every push and pull request
- Add a structured issue form template so bug reports come in complete
- Configure the issue-template chooser to steer people toward the right template
Each stage builds on the last, and everything you create lives in one folder — .github/ — that travels with the repository. Let’s equip it.
Stage 1: Verify the Tests Pass Locally
Before you automate anything, make sure there’s something worth automating. CI is just a robot that runs your tests; if the tests don’t pass on your own machine, the robot will only confirm that. So the first move is always to run the suite locally.
Here is the project under test — two small functions:
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / bAnd the tests that exercise them, including the error case:
# test_calculator.py
import pytest
from calculator import add, divide
def test_add():
assert add(2, 3) == 5
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(1, 0)Run pytest from the project root:
$ pytest
... [100%]
3 passed in 0.01sThree dots, three passes. That green local result is your baseline: now you know the tests are correct, so when CI runs them on a fresh machine, any failure points at the environment or a new change — not at tests that were broken all along.
Stage 2: Add the CI Workflow
With passing tests in hand, you can hand them to GitHub Actions. A workflow is a YAML file in .github/workflows/ that tells GitHub when to run and what to do. Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pytest
- name: Run tests
run: pytestRead it top to bottom — it’s a recipe:
onlists the triggers. This workflow runs on everypushtomainand on everypull_requesttargetingmain. That covers both day-to-day commits and incoming contributions.jobs.testdefines a single job that runs on a freshubuntu-latestmachine GitHub spins up for you.- The
stepsrun in order: check out your code withactions/checkout@v4, install Python 3.11 withactions/setup-python@v5, installpytest, then runpytest.
The two uses: lines pull in prebuilt actions from the community so you don’t reimplement “clone the repo” and “install Python” yourself. The two run: lines are plain shell commands — exactly what you typed locally in Stage 1. That symmetry is the point: CI runs the same pytest you just ran, on a clean machine, automatically.
Stage 3: Add the Bug-Report Issue Form
Tests guard your code; templates guard your inbox. A blank issue box invites reports like “it’s broken” with no version, no steps, and no description. An issue form replaces that blank box with labeled fields you define, and can mark fields required so nothing essential is missing.
Create .github/ISSUE_TEMPLATE/bug_report.yml:
name: Bug report
description: Report something that is not working as expected
title: "[Bug]: "
labels: ["bug"]
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Describe the bug and what you expected instead.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: List the exact steps that trigger the bug.
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Which version were you using?
validations:
required: falseWalking through it:
nameanddescriptionare what the person sees when choosing a template (“Bug report — Report something that is not working as expected”).titlepre-fills the issue title with[Bug]:, so bug issues are instantly recognizable in the list.labelsautomatically applies thebuglabel to any issue created from this form — no manual triage needed.bodyis the list of fields. Each has atype(textareafor multi-line,inputfor a single line), anidto identify it, anattributes.labelshown to the user, and an optionaldescriptionthat acts as guidance.validations.requireddecides whether a field must be filled. Here “What happened?” and “Steps to reproduce” are required; the “Version” input is optional.
The result: every bug report arrives labeled, titled, and complete with the two things you can never debug without — what went wrong and how to reproduce it.
Stage 4: Add the Chooser Config and Review the Layout
When a repository has one or more issue templates, GitHub shows a chooser screen after someone clicks “New issue”. You can configure that screen — and turn off the plain blank issue — with a config.yml alongside your templates.
Create .github/ISSUE_TEMPLATE/config.yml:
blank_issues_enabled: false
contact_links:
- name: Questions and discussion
url: https://github.com/datatweets/shopfront/discussions
about: Ask questions and discuss ideas here.Two settings do the work:
blank_issues_enabled: falseremoves the “Open a blank issue” escape hatch, so people must pick a template. That’s what makes your structured form actually get used.contact_linksadds entries to the chooser that point somewhere other than a new issue. Here it sends general questions to GitHub Discussions, keeping the issue tracker focused on actual bugs and tasks.
With all four files in place, your .github/ folder looks like this:
.github/
workflows/
ci.yml
ISSUE_TEMPLATE/
bug_report.yml
config.ymlNotice the two different homes: workflows live in .github/workflows/, while issue templates and their config live in .github/ISSUE_TEMPLATE/. GitHub looks in those exact paths, so the folder names and locations matter.
These files travel with the repository
Everything under .github/ — your workflows and your issue templates — is committed to the repository like any other file. That means the automation and the structure apply to everyone: every clone, every fork, and every contributor gets the same CI checks and the same bug-report form. You’re not configuring your account; you’re configuring the project, once, for all who touch it.
Stage 5: Commit, Push, and What You’ll See
The files exist locally, but GitHub only acts on what’s in the repository. Stage, commit, and push them:
$ git add .github
$ git commit -m "Add CI workflow and issue templates"
$ git pushThat push is itself a trigger. Because your workflow runs on: push to main, GitHub immediately starts a CI run. Two things change on GitHub from this point on:
- A status check appears. Open the Actions tab to watch the run, or look at the commit and pull request views. While tests run you’ll see a yellow dot; when they all pass you get a green check, and if
pytestreports a failure you get a red X. On any future pull request, that same check shows up right on the PR — a clear signal of whether the branch is safe to merge, computed automatically every time. - The issue form goes live. Click New issue and, instead of a blank box, you’ll see the chooser: a “Bug report” option (from
bug_report.yml) and a “Questions and discussion” link (fromconfig.yml). Choosing “Bug report” opens your form with the “What happened?”, “Steps to reproduce”, and “Version” fields, the title pre-filled with[Bug]:, and thebuglabel already attached. Becauseblank_issues_enabledisfalse, there’s no way to skip the form.
You’ve now turned an ordinary repository into one that checks itself and collects clean bug reports — with four small files and a single push.
Extend the Project
These exercises layer more automation onto the same .github/ folder. Try each one, then check the hint.
Exercise 1: Add a feature-request form
Bug reports aren’t the only kind of issue. Add a second issue form, feature_request.yml, so people can propose new features in a structured way too. What does it take to make it appear in the chooser?
Hint
Create .github/ISSUE_TEMPLATE/feature_request.yml with its own name (“Feature request”), description, and a body of fields — for example a textarea for “What problem does this solve?” and another for “Proposed solution”. You can also set labels: ["enhancement"]. Simply adding the file to ISSUE_TEMPLATE/ makes GitHub list it in the chooser automatically — no change to config.yml is required.
Exercise 2: Test multiple Python versions with a matrix
Your CI runs on Python 3.11 only. Real projects often support several versions at once. How would you make the test job run on 3.10, 3.11, and 3.12 without writing the job three times?
Hint
Add a build matrix to the job:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]Then reference it in the setup step with python-version: ${{ matrix.python-version }}. GitHub fans the job out into one run per version, and each gets its own status check — so you see at a glance which Python versions pass.
Exercise 3: Add a pull-request template
You standardized issues; now do the same for pull requests. Add a template that pre-fills the PR description with a checklist (summary, tests run, linked issue). Where does it go?
Hint
Create .github/pull_request_template.md (Markdown, not YAML — PR templates are a single file, not a form). Whatever you write becomes the default PR description. A useful starter has a ## Summary heading, a ## Changes list, and a checklist like - [ ] Tests pass and - [ ] Linked the related issue with Closes #. It lives directly in .github/, beside workflows/ and ISSUE_TEMPLATE/.
Summary
You equipped a repository with two kinds of professional automation, all from the .github/ folder. First you verified the tests pass locally with pytest (3 passed), establishing a trustworthy baseline. Then you added a CI workflow (.github/workflows/ci.yml) that runs on every push and pull request to main, checks out the code, installs Python and pytest, and runs the suite — producing a green check or red X on every change. Next you added a bug-report issue form (.github/ISSUE_TEMPLATE/bug_report.yml) with required fields, an auto-applied bug label, and a pre-filled title, plus a chooser config (.github/ISSUE_TEMPLATE/config.yml) that disables blank issues and links to Discussions. After committing and pushing, the next push triggers CI and the “New issue” button shows your structured form.
Key Concepts
- Verify locally first — run
pytestbefore automating; CI only confirms what your tests already prove. - CI workflow — a YAML file in
.github/workflows/whoseontriggers andstepsrun your tests on a fresh machine, every push and PR. - Issue form — a
.ymltemplate in.github/ISSUE_TEMPLATE/with typed, optionally required fields, auto-labels, and a pre-filled title. - Chooser config —
config.ymlwithblank_issues_enabled: falseandcontact_linksshapes the “New issue” screen. .github/travels with the repo — the automation applies to every clone, fork, and contributor.
Why This Matters
This is the difference between a personal scratch repository and one that’s ready for collaborators. CI means no broken code reaches main unnoticed — the tests run themselves, on every change, for everyone. Structured issue forms mean every bug report is actionable instead of vague, saving the back-and-forth of asking “what version?” and “how do I reproduce it?”. Together they encode your project’s standards into files, so the standards hold even as the team and the codebase grow. You’ve now built the automation backbone that real open-source and professional repositories rely on every day.
Next Steps
Continue to Module 9 - Mastery, Safety, and Capstone
Config mastery, security and hygiene, a troubleshooting playbook, and the two-part capstone.
Back to Module Overview
Return to the Automating with GitHub module overview
Continue Building Your Skills
You can now hand a repository the two habits that make it trustworthy: it runs its own tests on every push, and it collects clean, structured bug reports. That automation backbone is the foundation real teams build on. Next, in Module 9, you’ll round out your mastery with deeper configuration, security and hygiene practices, a troubleshooting playbook for when things go wrong, and a capstone that ties the whole course together.