Roadmap · Project hygiene · P2 · S

CHANGELOG + tags that match PyPI

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.

Done when: 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.
0 / 0 steps

The principle

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.

Do this after single-sourcing the version. With one version file, the release ritual is: bump __version__ → move the Unreleased notes under a dated heading → tag. One number, three derived artifacts.

Steps

  1. Reformat CHANGELOG.md to a consistent standard
    CHANGELOG.md

    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
    Backfill from what you know. The git log has the real dates (git log --tags --simplify-by-decoration --pretty="%ci %d"); fill the 0.1.x dates from there rather than guessing.
  2. Audit that tags match PyPI
    terminal — one-time reconciliation

    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
  3. Write the release checklist into PUBLISHING.md
    PUBLISHING.md

    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
  4. Verify the story is consistent

    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
    Tag the annotated way. 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.
  5. Commit
    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
  6. Mark it shipped on the roadmap

    Add done:true, to the {area:"hygiene", … title:"CHANGELOG + tags that match PyPI"} item in docs/roadmap.html.

Worth knowing

A tag is a promise you can't edit. Once 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.