Never edit files while on main, exp or a release/* line. These branches are

protected. All work happens on feature branches.

One flow for every published module

Every module that ships to PyPI uses the same branching, whatever tier it sits in: the core libraries, every hub device, analyzer and experiment, the data tools and the utilities. A hub device module branches and releases exactly the way plesty-lib does — it is not a lighter case.

exp is the integration branch. Feature branches merge into exp, never into main.

git checkout exp
git pull origin exp
git checkout -b feat/add-wavelength-sweep

# ... implement + plesty check ...

git add plesty/scan_exp/measurements.py tests/test_measurements.py
git commit -m "feat(scan): add wavelength_sweep measurement function"

git push origin feat/add-wavelength-sweep:exp   # merge directly into exp
git checkout exp
git branch -d feat/add-wavelength-sweep

No merge request step. The feature branch is pushed straight into exp. main is written

only by the release merge, described below.

A module with no exp branch is not exempt, it is unmigrated. Cut one before its next release:

git checkout -b exp main && git push -u origin exp

Outside this: the governance project (feature branch and a merge request into main), the website content repositories, and the deprecated modules. None of them have release lines.

Release lines

A minor version is a line. When 0.N+1 opens, 0.N keeps receiving bug fixes on a release/0.N branch; feature work never lands there.

the day its successor's successor ships, and that is recorded in the changelog of the release that retires it.

carries the regression test along with the fix, and leaves a forgotten merge visible. Merge after every patch tag rather than in a batch.

diffs against the published release. Below 1.0.0 a breaking change costs a minor bump, not a major one.

replacement and the removal version in the changelog when you deprecate.

Where a release is tagged

main moves only as the act of releasing — one merge per release, whose diff is that release's changelog — and is merged back into exp immediately after every tag. A line stays current until its successor ships, and that decides where the release goes:

Release Path Tagged on
v0.N.0.devX snapshot exp — snapshots never reach main
v0.N+1.0, opening a new line expmain main
v0.N.x while 0.N is the newest released line release/0.Nmain main
v0.N.x after 0.N+1 has shipped release/0.Nexp release/0.N

Before tagging a new line, check that every open line is already contained in it:

git merge-base --is-ancestor release/0.N exp

Do not skip the merge-back. A tag that never returns to exp leaves every later branch

missing whatever the release carried, and the gap only shows up as unrelated failures later.

Conventional commits

All commits must follow the conventional commit format. plesty check Gate 8 reads commit messages to classify unreleased changes.

<type>(<scope>): <description>

[optional body]

[optional footer]
Type Semver impact Emoji When to use
feat minor New feature or capability
feat! minor below 1.0.0, major from 1.0.0 on 💥 Breaking change to public API
fix patch 🐛 Bug fix
refactor patch ♻️ Restructure without behavior change
docs none 📖 Documentation only
test none 🧪 Add or fix tests
chore none 🔧 Maintenance (deps, config, build)
ci none 🚀 CI/CD changes

Scope is the module or component affected, e.g., feat(wavelength): ..., fix(solver): ....

Breaking changes are declared with ! after the type or with BREAKING CHANGE: in the footer:

feat(api)!: rename query() to read()

BREAKING CHANGE: query() has been renamed to read() for consistency with the write/read pair.

The ! goes after the scope, once. While the module is below 1.0.0 this is still a minor bump — and it must open a new line, since a patch may never change the public API.