Never edit files while on
main,expor amaintain/*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 — or, for a fix to an older
supported line, into its maintain/0.N branch — 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 -u origin feat/add-wavelength-sweep # the branch itself
# then open a merge request into exp, and delete the local branch once it merges
Every change lands through a merge request into the line it was cut from.
mainis written only by the release merge, described below. Pushing a feature branch straight intoexp(git push origin <branch>:exp) is the fallback for when a merge request cannot be opened at all — say why when you take it.
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
maintain/0.N branch; feature work never lands there.
- Two lines are supported at a time — the current one and the one before it. A line retires the day its successor's successor ships, and that is recorded in the changelog of the release that retires it.
- Fix on the oldest supported line, then merge upward. Never cherry-pick downward: merging carries the regression test along with the fix, and leaves a forgotten merge visible. Merge after every patch tag rather than in a batch.
- A patch never changes the public API. That is exactly what the semantic-versioning gate
diffs against the published release. Below
1.0.0a breaking change costs a minor bump, not a major one. - Deprecate one line before removing. Deprecate in
0.N, remove in0.N+1, and name the 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 |
exp → main |
main |
v0.N.x while 0.N is the newest released line |
maintain/0.N → main |
main |
v0.N.x after 0.N+1 has shipped |
maintain/0.N → exp |
maintain/0.N |
Before tagging a new line, check that every open line is already contained in it:
git merge-base --is-ancestor maintain/0.N exp
Do not skip the merge-back. A tag that never returns to
expleaves 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, so a release's changelog and version bump can be read off its history. (Gate 8 does not read them: it compares the public API itself against the published release — a breaking change it finds needs the bump whatever the message said.)
<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.