Skip to content

Releasing & Versioning

How to cut a release of any Aetos repository. The rule lives in the reference page Versioning & Changelog; this is the runbook you follow.

Every repository is one of two Drone shapes — a PyPI package or a Docker deployment (Drone Pipelines). Both release the same way: the version bump and changelog promotion go in the merge request, and the tag is created on the merged commit.


🧾 Before you start

  • You have an open merge request holding the change, and main is green (the Drone quality gate passed on the last run).
  • You know whether this is a breaking, feature, or fix release.
  • The version file exists:
  • package → version in pyproject.toml
  • service → VERSION at the repo root

🚦 Pick the next version

The version file and the tag must be identical. For 0.y.z, 0.y is the major line (see the reference page); once at 1.0.0, normal SemVer applies.

Change 0.y.z → 1.0.0+ →
Breaking 0.(y+1).0 (x+1).0.0
Feature 0.y.(z+1) x.(y+1).0
Fix 0.y.(z+1) x.y.(z+1)

🌿 Release in the MR

The version is known while the change is still a merge request, so the bump rides along with it. Do all edits in the MR:

  • Reconcile the changelog first. [Unreleased] is a draft, not the source of truth: diff the change against the last tag (git log <last-tag>..HEAD, git diff --stat <last-tag>..HEAD) and add an entry for every user-facing change that is missing. Always show the proposed additions to the user and wait for approval before promoting — ask rather than guess when an entry is ambiguous.

  • Promote the changelog in CHANGELOG.md: move the (now reconciled) [Unreleased] content under a new ## [x.y.z] - YYYY-MM-DD heading, leave a fresh empty [Unreleased] on top, and add the compare links — naming the version/tag, never a commit id:

[Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD
[x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z
  • Bump the version file to the intended release version: version = "x.y.z" in pyproject.toml (packages) or the VERSION file (services).

🏷️ Tag the merged commit

  1. Merge or squash the MR into main.
  2. Create the tag on the resulting commit: git tag x.y.z (lightweight).
  3. Push the tag (git push origin x.y.z) — Drone builds the release.

🤖 Agent-assisted tagging

The agent does not author or merge the MR and never tags before the merge. After you confirm the MR is merged/squashed and tell it to proceed, the agent:

  1. Waits for that explicit go-ahead.
  2. Finds the commit that introduced the bump:
  3. package: git log -S 'version = "x.y.z"' -n 1 --format='%H %s' -- pyproject.toml
  4. service: git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION
  5. Verifies that commit's version file equals x.y.z, then tags only that commit: git tag x.y.z <sha>.
  6. Does not push — it asks you to run git push origin x.y.z.

🤖 What Drone does

  • Type A (package): asserts tag == pyproject version, then skips if that version already exists on the index, runs the quality gate, builds, and uploads the wheel + sdist.
  • Type B (service): asserts tag == VERSION, resolves the tag, skips if the image already exists, runs the quality gate, builds, pushes, and restarts the container.

If the guard fails, nothing is published — fix the version file, amend the commit, and move the tag only if it was never pushed.


🛟 Backfill / recovery

If an older tag never published (a past version-file mismatch, an index outage):

  • Package: run the matching backfill-*-from-tags pipeline (custom event) — backfill-pypi-from-tags for Python, backfill-npm-from-tags for npm. It checks out every tag, verifies tag == the version file at that commit, and uploads the ones missing from the index. Already-fixed history is never rewritten.
  • Service: rebuild the image for the missing tag with a fresh commit and a new patch version; do not move a published tag.

🔁 If a release is wrong

  • Never reuse or force-move a pushed tag.
  • Fix forward: bump to the next patch, promote the changelog, tag again.