Skip to content

Releasing

How a version is cut, tagged and published, and how a release archive is built.

Cutting a release

Write the ## [Unreleased] entry in CHANGELOG.md as you go — the release workflow publishes that section verbatim as the release notes, and refuses an empty one. Then:

./logstotal release:prepare VERSION=X.Y.Z    # 1. edit
git diff                              # 2. read what it wrote
./logstotal release:finish VERSION=X.Y.Z     # 3. commit, tag, gate
git push --atomic origin <branch> refs/tags/vX.Y.Z

release:prepare rewrites app/config.py, pyproject.toml and VERSION together, moves the changelog section and both of its link definitions, and sweeps the version strings out of the docs — REF=v… commands, archive URLs, the /health example body, the # expect: version: line. Prose that names a specific older version on purpose is left alone. It refuses a dirty tree, a non-increase, an existing tag, and an empty ## [Unreleased], and it checks all of those before it writes anything — a refusal never leaves the tree half-bumped.

Pushing the tag is what publishes the release: .github/workflows/release.yml builds and uploads logstotal-X.Y.Z.7z when the tag reaches GitHub, and its mirror, .forgejo/workflows/release.yml, does the same on a self-hosted Forgejo runner. Both re-verify the tag against all three declared versions before building, and both can be re-run by hand against an existing tag.

release:finish commits, then tags, then runs ./logstotal check && ./logstotal test. That order is the point of the command. The tag has to land on the release commit, and the suite has to run with the tag in place, because test_the_declared_version_has_a_git_tag cannot pass without it. A failed gate deletes the tag and keeps the commit, so you fix and amend.

Neither command pushes.

Important

Push the tag, by name, with the branch. CI's version check needs the release tag on the remote, not just in your clone. Push both refs in one atomic push, as above; never git push --tags, which publishes every local tag at once.

VERSION holds one line — version: X.Y.Z — and ./logstotal release:prepare is the only thing that writes it. No build, package or upgrade touches it (tests/test_version_file.py enforces that), so it changes in release commits and nowhere else. It is committed rather than generated-and-ignored so a fresh clone reports its real version.

Packaging

./logstotal lock      # ensure requirements.txt is up to date
./logstotal package   # creates logstotal-<version>.7z

Tailwind: for a production archive that serves compiled CSS see Production CSS (Tailwind) — ./logstotal package re-runs ./logstotal css:build automatically when the CLI is present, so the archive ships tailwind-built.css; without the CLI it ships the vendor Tailwind JS bundle and the app still works, or PACKAGE_USE_COMMITTED_CSS=true ./logstotal package ships the committed stylesheet as it stands. ./logstotal package restores dev mode from an EXIT trap, so a failed build leaves base.html where it found it.

To install from the archive, follow Prerequisites.


Related: Development · Upgrade and roll back · Command reference