Programming Language & Dependency Management#
I’m building Momo using Python and uv. The nice thing about uv is that it basically combines project, dependency and environment management into one single tool. Gone are the days of using bulky poetry with pyenv, or even worse: pip together with a requirements.txt! Ah, and it’s blazingly fast, too, thanks to it being written in Rust.
Getting started was as easy as fixing the .python-version to 3.13 and running uv init to generate the pyproject.toml and .venv. In case you don’t already know, the pyproject.toml is used to add new dependencies and store information about the project version, whereas the uv.lock file (which is created after adding the first dependency) serves as the single source of truth for all resolved dependency versions.
That way, I will not run into dependency hell and uv will take care of unraveling potential conflicts between different package requirements.
Conventional Commits & Semantic Versioning#
Next, I wanted to enforce a consistent style for my commits such that I am not ending up with a bunch of quick fix and did something but dont remember what exactly commit messages in my git history, but instead following a proven industry standard: conventional commits
The idea here is to preface the commit message with a short but descriptive statement such as e.g. feat, fix, refactor, docs, or infr to signal what kind of change has been made. Conventional commits also go very nicely with semantic versioning, as e.g. feat commits correlate with MINOR (x.x^.x) project version bumps, and fix commits with a PATCH (x.x.x^).
Hence, to enforce using conventional commits, I added the conventional-pre-commit hook which will not let me commit my changes if the message does not follow the conventional commit styling.
Pre-commit Hooks#
Speaking of pre-commit hooks, I added the following set to get started:
pre-commit-update: checks whether updates are available for any of the hooks configured in the.pre-commit-config.yaml; I’m actually running this as a pre-push hook such that any version changes can be committed separately from actual code changespre-commit-hooks: bundles some out-of-the-box hookscheck-ast: ensures valid Python syntaxcheck-merge-conflict: scans for unresolvedgitmerge conflicts; might not be too important yet as I’m currently not working in feature branches but rather keep pushing directly intomain(pls don’t call the cops!)end-of-file-fixer: ensures files end with exactly one newlinetrailing-whitespace: removes whitespace at the end of lines
mirrors-mypy: usesmypyfor type checking; I’m running a specific configuration with--ignore-missing-imports: skips errors for libs without type hints--install-types: installs missing stub packages automatically--non-interactive: avoids user prompting--disallow-untyped-defs: requires explicit type hints on all functions
ruff-pre-commit: takes care of linting & formattingruff: runs therufflinter with the--fixflag to automatically correct issues such as unused imports or formatting fixesruff-format: applies theruffcode formatter (modern successor ofblack)- I’m additionally setting some more fine-tuned
ruffbehavior in thepyproject.tomlsuch as linting according to the PEP 8 style guide, but also checking unused code and sorting imports (equivalent toisort)
mdformat: formats.mdfiles; I added this one specifically, as both my dev diary entries as well as Momo’s reflections are written in plain Markdownmdformat-gfm: adds GitHub-flavored markdown support (e.g. tables, task lists, fenced code block syntax highlighting and much more)mdformat-black: formats fenced Python code blocks usingblackmdformat-admon: supports admonition syntax (e.g.!!! note)mdformat-frontmatter: ignores any frontmatter; as the Markdown files get published to my Hugo-based dev blog, they require a certain frontmatter, e.g. to properly set the title, author, date and tags which would otherwise get formatted and hence become unusable; as there were some version conflicts amongst the differentmdformatpre-commit hooks, I had to manually pin the version and exclude the hook altogether frompre-commit-update
I have also specifically set the Python version to 3.13 for mypy and ruff to ensure consistent behavior with the overall project Python version.
Altogether, these pre-commit hooks ensure a consistent and high quality coding style going forward - and they only needed to be set up once!
Basic CI/CD Pipeline & Version Bumping#
By now it should (hopefully) have become obvious that I’m not approaching this project in a hot-headed manner and simply start coding right away (even if my excitement about this endeavor can hardly be contained).
Instead, I’m approaching this exactly as I’d approach any software project in my professional life - with thought, care and a solid engineering foundation. After all, I want to continue working on Momo for quite some time to come and being able to rely on a trusted set of engineering tools and best practices ensures that I stay motivated longterm.
Hence, I have set up a lean .gitlab-ci.yaml CI/CD pipeline skeleton that will be expanded over the next sections and subsequent versions. I’m starting out with two simple job rules …
.on-merge: runs whenever a pipeline runs on themainbranch.on-merge-request: runs only in Merge Request pipelines; at some point, I will likely stop pushing directly intomainand rather go the MR & self-approve route; having jobs that run only for MRs will help me figuring out whether my changes risk breaking anything before merging intomain
… as well as a qa stage which simply runs the all the pre-commit hooks for now.
Continuing from here, and borrowing from best practices I learned at work, I then added a bump stage with a corresponding bump-version job. This job only runs on main branch pipelines and has to be triggered manually (extending the .on-merge job rule to a .on-merge-manual rule).
It effectively uses the commitizen package to bump the project version according to the magnitude of changes documented by aforementioned conventional commits (see how all is neatly coming together?) and creates the corresponding tag. As a nice side-effect, the CHANGELOG.md is also updated automatically with the relevant commits. As the job needs to push its updates to main, I needed to create a project access token and save its value to a corresponding CICD variable to be consumed.
That way, I will now only need to care about which changes I want to bundle in a version bump (and write proper & atomic conventional commits), and the job takes care of the appropriate versioning, git tag and CHANGELOG.md update.
Custom cz & Verbose CHANGELOG.md#
Speaking of the CHANGELOG.md - for this project, it plays an exceptionally important role. As you may have read in the previous post, I am planning on letting Momo write her own blog entries as some sort of reflection on her own development from her point of view.
For this, obviously, Momo needs appropriate context. To start out, I hence want to provide Momo the CHANGELOG.md diff (or maybe even the full file for evolutionary context) as well as the actual code changes of the current increment.
The problem is that the CHANGELOG.md created & updated via commitizen is rather sparse. By default, only feat, fix and refactor commits make it into the list of changes. While these commit types already cover quite a lot of different use cases, I’d want Momo to know & comment when a first infr deployment change has been made, or when perf improvements speed up her tool usage, for instance.
Hence, I needed a way of including more commit types into the CHANGELOG.md and the answer was: custom cz rules!
I’ll spare you the details, but I essentially had to write a CustomMomoCz class that extends conventional_commits.ConventionalCommitsCz and defines which commit types bump the version in what way (patch / minor / major) and which of those eventually make it into the CHANGELOG.md.
For my purposes, I decided to include all conventional commit types except for bump and merge as they do not contain actual code changes / additions per se, and to avoid potentially confusing Momo. While I was at it, I also slightly tweaked the CHANGELOG.md terms to include the corresponding gitmoji, e.g. ๐ Fixes or ๐งช Tests, in the expectation that Momo will pick that up as a more expressive way of thinking about the different changes.
Finally, I had to pass this custom cz.py file as a project entrypoint to the [tool.commitizen] section of the pyproject.toml and add the hatchling build backend.
Release & Publish Jobs#
To wrap up the whole software development cycle, I added a release stage to the .gitlab-ci.yml with a release-to-gitlab job that uses the official registry.gitlab.com/gitlab-org/cli:latest image to create a release of the changes in GitLab, together with the git tag and the CHANGELOG.md diff as release notes.
Thus, when writing these developer diary entries, I can always refer to the actual release and the status of the code at that very point in time for better transparency. This job runs automatically and solely upon creation of a new tag (via a .on-version-tag rule), which, in turn, only gets created after a successful version bump via the bump-version job.
I could have been done here but I wanted to add just one more thing.
Reading a bit into how software releases are being done by bigger companies, I wanted to have a system in place where one button (the manual bump-version job trigger button in the GitLab UI) not only increments the project version signifying a step forward in the whole undertaking, but also takes care of doing a proper release as well as publish the “release notes” (aka my rambling) to my dev blog.
Hence, I added yet another publish stage to my CICD configuration with a publish-dev-diary-entry job that does the following:
- upon a successful release to GitLab, clones my dev blog repo
- finds the dev diary entry in the Momo repo under
/docswhose name corresponds to the release tag (e.g.v0.2.0.md) - inserts frontmatter such as the article title, date, description, tags and author (me!) at the beginning of the entry
- adds an HTML button that references the corresponding GitLab release to the end of the entry
- pushes the “augmented” dev diary entry to the
mainbranch of the dev blog repo - from the dev blog repo, a CICD pipeline is spawned (
.on-merge) which then re-deploys the dev blog and “publishes” the new article automatically
I decided for adding the front- & postmatter steps directly in the job such that I do not have to think about setting this metadata when writing the dev diary entry and for it to always follow a pre-defined structure. Moreover, the mdformat pre-commit hook unfortunately completely wrecks the HTML postmatter. I’m therefore adding this information after the qa stage (and by extension the pre-commit job) has passed.
This seems like a small step but took quite some time to get right as it actually was my very first time constructing a job working across several repos. For all of this to work, I needed to create a personal access token in my GitLab profile settings, alongside a corresponding CICD variable in the Momo repo. This token now actually performs the push of the dev diary entry from the Momo repo into the dev blog repo.
We can finally get started now!#
Whew! Writing all of this down took much more energy and time than I had expected!
However, I wanted to be quite verbose about the project setup as this distills more or less everything that I have learned so far about a lean but still sophisticated and extensible project setup.
Surely, future version increments will add and change things here and there, but I am very confident that this will serve as a great basis to build upon. And hopefully, you, dear reader, also learned a thing or two!
With that, I’m concluding this somewhat dry dev diary entry - thanks for making it through!
The next one will be much more exciting as we’re finally ready to get started with the interesting AI stuff - and hopefully, Momo will soon get to make a first appearance, too!
Until then, stay tuned!
Cheers, Niklas
View release on GitLab
