Building the Project
Every build workflow you’ve written so far in this course started from a repository that was already yours to fill in — this is the first one where the CI file itself is on you too. Nothing below is new material; it’s the same lint/test/matrix/artifact toolkit from earlier in the course, applied against a project you designed. What’s different is that this chapter gives you requirements and pointers back to where each piece was taught, not a workflow file to copy.
The walkthrough below is framed around stalefinder. If you picked speedtest-cli or a TheAlgorithms/Python module instead, the same requirements apply — the only difference is what your install step installs.
Finish What stalefinder Left Unfinished
Recall that stalefinder was left without a pyproject.toml or a command-line entry point on purpose. Before there’s anything for a CI workflow to lint or test, that gap needs closing.
A pyproject.toml that makes the project installable. It needs, at minimum:
- Project metadata: a name, a version, and a
requires-pythonconstraint. - A
[build-system]table sopip install -e .works at all. - An entry in
[project.scripts]pointing at whichever module and function your CLI wrapper exposes as its entry point — that’s what turnsstalefinderfrom an importable library into something runnable as a command. - A
[tool.ruff]section, since your lint job is going to point at this project the same way you pointedruff-actionat earlier projects in this course. - An extra in
[project.optional-dependencies]— something liketest— listing whatever your test job will need beyondpytestitself. Think about what a coverage-reporting pytest run needs that plainpytestdoesn’t provide, since your test job is going to want coverage output later in this chapter.
A CLI wrapper. find_stale_files() already exists in stalefinder/__init__.py — it takes a root directory and a day threshold and returns a sorted list. Wrap it in a small command-line entry point (argparse is more than enough) that:
- Takes the root directory as a positional argument.
- Takes the day threshold as an optional flag with a sensible default.
- Calls
find_stale_files()and prints something readable — one line per stale file is fine. - Returns a proper exit code.
Wire that function up as the target of the [project.scripts] entry you wrote above. Once it’s in place, pip install -e . followed by running the command you defined should list stale files under whatever directory you point it at.
Tip
Test this locally before writing any YAML. If the command doesn’t run cleanly on your own machine, it won’t run any more cleanly inside a GitHub-hosted runner — CI isn’t where you want to be debugging packaging problems for the first time.
Plan the CI Workflow
Go back to the job graph from the Designing the Pipeline chapter: lint and test run in parallel, both gate a final job. That doesn’t change here — what changes is that you’re writing the YAML that implements it instead of reading one.
flowchart LR
A[lint] --> C[build-ok]
B[test] --> C
Work through each job against this checklist. Every item points back to a chapter earlier in this course where you already did the equivalent thing against a different project — the task here is transferring it, not inventing it from nothing.
Triggers:
- Runs on
pushto your default branch, and onpull_request
lint job:
- Checks out the repository
- Runs the same lint-as-a-gate action you used in the linting chapter, pointed at this project
test job:
Runs across a matrix of Python versions — 3.10 through 3.13 is a reasonable spread (see the matrix builds chapter)
Makes a deliberate choice about
fail-fast, not just leaving it at whatever’s default — ask yourself whether this project has a version that must never break versus others that are nice-to-have, the way the fail-fast-vs-continue-on-error chapter framed that distinction.stalefinderdoesn’t have an obvious asymmetric case, but decide that on purpose rather than by accident.Installs the project using the
testextra you defined inpyproject.tomlRuns
pytestwith coverage enabled (the pytest chapter covered running tests in CI; adding a coverage flag on top of that is a small addition, not new material)Uploads the coverage output as an artifact — but only from one matrix combination, not all of them. Revisit the conditional-steps chapter for how to gate a step to a single matrix cell, and the artifacts chapters for the upload step itself. Ask yourself why uploading it from every combination would be wrong before you write the condition.
build-ok job:
- Depends on both
lintandtestvianeeds: - Does nothing except confirm both succeeded
Think about why this last job exists before you skip it as pointless — it comes up again in Deploy, and it’s the same reason branch protection points at a single named check rather than at individual matrix job names.
Before You Move On
Push a branch, open a pull request against it, and watch the Actions tab. You’re looking for:
- Lint and test both run and both report status independently
- The test matrix actually covers every combination you specified — check the run summary, don’t just assume
- Exactly one coverage artifact appears, not one per matrix cell
build-okwaits for both other jobs and only goes green once they have
Once all four of those hold on a real pull request, the build side of the capstone is done, and the next chapter picks up from a working pipeline that produces something worth deploying.