Lesson 5 - Guided Project: Add CI and Issue Templates

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 / b

And 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.01s

Three 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: pytest

Read it top to bottom — it’s a recipe:

  • on lists the triggers. This workflow runs on every push to main and on every pull_request targeting main. That covers both day-to-day commits and incoming contributions.
  • jobs.test defines a single job that runs on a fresh ubuntu-latest machine GitHub spins up for you.
  • The steps run in order: check out your code with actions/checkout@v4, install Python 3.11 with actions/setup-python@v5, install pytest, then run pytest.

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: false

Walking through it:

  • name and description are what the person sees when choosing a template (“Bug report — Report something that is not working as expected”).
  • title pre-fills the issue title with [Bug]: , so bug issues are instantly recognizable in the list.
  • labels automatically applies the bug label to any issue created from this form — no manual triage needed.
  • body is the list of fields. Each has a type (textarea for multi-line, input for a single line), an id to identify it, an attributes.label shown to the user, and an optional description that acts as guidance.
  • validations.required decides 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: false removes the “Open a blank issue” escape hatch, so people must pick a template. That’s what makes your structured form actually get used.
  • contact_links adds 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.yml

Notice 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 push

That 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 pytest reports 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 (from config.yml). Choosing “Bug report” opens your form with the “What happened?”, “Steps to reproduce”, and “Version” fields, the title pre-filled with [Bug]: , and the bug label already attached. Because blank_issues_enabled is false, 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 pytest before automating; CI only confirms what your tests already prove.
  • CI workflow — a YAML file in .github/workflows/ whose on triggers and steps run your tests on a fresh machine, every push and PR.
  • Issue form — a .yml template in .github/ISSUE_TEMPLATE/ with typed, optionally required fields, auto-labels, and a pre-filled title.
  • Chooser configconfig.yml with blank_issues_enabled: false and contact_links shapes 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.