A build-it-yourself guide. CHANGELOG.md has a single loose
0.2.0 entry and no format; the repo carries v0.1.1 and
v0.2.0 tags. This turns that into a ritual: a well-formed changelog and a
git tag for every release, so GitHub and PyPI always tell the same story.
CHANGELOG.md follows a consistent format with an
Unreleased section, every published PyPI version has a matching annotated git tag,
and a documented release checklist keeps them in lockstep going forward.A user who hits a bug needs to answer two questions from the repo alone: "what changed, and in which version?" That works only if three things line up — the changelog heading, the git tag, and the PyPI release. Right now they mostly do, but by luck, not by process. Adopt the Keep a Changelog convention and a fixed release order so it stays that way.
__version__ →
move the Unreleased notes under a dated heading → tag. One number, three derived
artifacts.Add a header, an Unreleased section at the top, and keep the existing
0.2.0 notes under a dated, versioned heading. Backfill the 0.1.x
releases that already shipped to PyPI so the history is complete.
# Changelog
All notable changes to ModuLearn are documented here.
This project follows [Keep a Changelog](https://keepachangelog.com) and
[Semantic Versioning](https://semver.org).
## [Unreleased]
## [0.2.0] — 2026-07-23
- Surface on_train crashes on the canvas (error + full traceback)
- Loss node now shapes the training curve
- CompiledGraph.check_shape() validates loaded array shape
- Enforce hyperparameter min/max at compile
- GitHub Actions CI across Python 3.10–3.13
## [0.1.1] — add python -m entry point
## [0.1.0] — initial release
git log --tags --simplify-by-decoration --pretty="%ci %d"); fill the
0.1.x dates from there rather than guessing.List local tags and the versions PyPI actually serves, and confirm every published version
has a tag. (v0.1.1 and v0.2.0 already exist — this catches any gap,
e.g. a 0.1.0 release with no tag.)
git tag --list 'v*' # local tags
pip index versions modulearn # what PyPI serves
# for any published version with no tag, tag its release commit:
git tag -a v0.1.0 <commit> -m "modulearn 0.1.0"
git push --tags
Codify the order so future releases can't drift. The changelog and tag are steps, not afterthoughts.
## Cutting a release
1. Bump `__version__` in modulearn/__init__.py
2. Move Unreleased notes under `## [X.Y.Z] — <date>` in CHANGELOG.md
3. Commit: "modulearn X.Y.Z"
4. Tag: git tag -a vX.Y.Z -m "modulearn X.Y.Z" && git push --tags
5. Build + upload: python -m build && twine upload dist/*
6. Verify: pip index versions modulearn shows X.Y.Z, tag vX.Y.Z exists
Three artifacts, one version — check they agree for the latest release.
grep -m1 '\[0' CHANGELOG.md # top versioned heading, e.g. [0.2.0]
git tag --list 'v0.2.0' # matching tag exists
pip index versions modulearn # PyPI serves 0.2.0
python -c "import modulearn; print(modulearn.__version__)" # 0.2.0
git tag -a (not a lightweight
tag) records who/when/why and is what GitHub's Releases page reads. Lightweight tags look fine
locally but carry no metadata.git add CHANGELOG.md PUBLISHING.md
git commit -m "Keep-a-Changelog format + release ritual; reconcile tags with PyPI"
git push
gh run list --branch main --limit 1 # expect: success
Add done:true, to the
{area:"hygiene", … title:"CHANGELOG + tags that match PyPI"} item in
docs/roadmap.html.
vX.Y.Z is pushed and the wheel is on PyPI, that version is immutable — PyPI refuses a
re-upload of the same version. That's exactly why the checklist puts the changelog and tag
before twine upload: get the record right while it's still cheap to fix.