Skip to content

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-python constraint.
  • A [build-system] table so pip 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 turns stalefinder from 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 pointed ruff-action at earlier projects in this course.
  • An extra in [project.optional-dependencies] — something like test — listing whatever your test job will need beyond pytest itself. Think about what a coverage-reporting pytest run needs that plain pytest doesn’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 push to your default branch, and on pull_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. stalefinder doesn’t have an obvious asymmetric case, but decide that on purpose rather than by accident.

  • Installs the project using the test extra you defined in pyproject.toml

  • Runs pytest with 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 lint and test via needs:
  • 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-ok waits 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.

Last updated on