Never edit files while on
main,expor arelease/*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.mainis 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.
- 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.0 a 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 |
release/0.N → main |
main |
v0.N.x after 0.N+1 has shipped |
release/0.N → exp |
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
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. 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.