A mibudge release is a signed git tag and the Docker images built from it. Deployments name the release they run, so moving between releases is a change of one version string.
pyproject.toml ([project] version) and follows
Semantic Versioning.0.3.0, no v prefix): an
annotated tag, signed by the person making the release.CHANGELOG.md has one section per release, dated with the day the
release’s code was finished, and an [Unreleased] section collecting
changes for the next one.make version prints the version in pyproject.toml.GET /api/version/, which
needs no credentials, in an X-Mibudge-Version header on every API
response, in the OpenAPI schema’s info.version, and in the landing
page’s footer. The SPA has the release built in and shows both on the
account settings page./api/v1/
itself and bumps the minor version. The SPA and the importers change
in the same release./api/v1/ is frozen. From then on a breaking change ships as
a new /api/vN/ alongside the existing ones, so clients move to it on
their own schedule./api/vN/ is a major release.docs/api-introduction.md,
rendered in docs/api.md) is the
recommendation for client developers. The SPA ships with the server,
so its range is exactly the release it is built with.Pushing a tag runs the Drone pipelines: the full test pipeline, then the
image build. The build publishes to ghcr.io/scanner/mibudge:
| Tag | Image |
|---|---|
X.Y.Z, X.Y |
production image (prod Dockerfile target) |
X.Y.Z-dev, X.Y-dev |
development image (dev target) |
Every image carries its version in the org.opencontainers.image.version
label (docker inspect --format '')
and in the MIBUDGE_VERSION environment variable, which the server
reports; the SPA bundle in it is built with the same version. make up
builds the development image the same way and starts Vite with it.
There is no latest tag: a deployment always names a version, as the
production template’s VERSION does (deployment/production/).
make build builds the same two images locally, tagged mibudge:X.Y.Z and
mibudge:latest, and mibudge:X.Y.Z-dev and mibudge:dev.
main:
## [Unreleased] in CHANGELOG.md to ## [X.Y.Z] - YYYY-MM-DD
and start a new, empty ## [Unreleased] above it. Point the
[Unreleased] link at X.Y.Z...HEAD and add an [X.Y.Z] compare link
from the previous release.version = "X.Y.Z" in pyproject.toml and run uv lock, which
updates the project’s own entry in uv.lock.make api-docs, which writes the version into
docs/openapi.yaml and docs/api.md.VERSION=X.Y.Z in deployment/production/dot-env.example, so the
production template names the newest release.main, and run make publish-tag. It signs the
tag X.Y.Z on HEAD (skipping that step when the tag already exists) and
pushes only that tag.A release can be cut from a commit that main has moved past:
release-X.Y.Z from that commit..drone.yml and Dockerfile of the tagged commit, so the build files on
the branch are the ones that build the images. Check that the commit
leaves the application unchanged:
git diff <commit> -- app frontend importers is empty.make publish-tag on the release commit.main with a merge commit, never a squash, so
the tag is reachable from main (git describe reports it). The merge
keeps main’s code and its [Unreleased] changes above the new
release’s section.Deploy the previous version. A release with no migrations rolls back by changing the version alone.
A release with migrations rolls back in two steps, because the compose
stack’s migrate service only migrates forward: an older image does not have
the newer migration files, so starting it leaves the schema where it is.
VERSION to the previous release and docker compose up -d.This works when every reversed migration has a reverse operation; schema
changes do, and RunPython/RunSQL migrations do when written with reverse
code. Reversing a migration that dropped a column or deleted rows does not
bring that data back. When a migration can’t be reversed, or the data it
removed is needed, restore the database from a backup taken before the
upgrade instead.