How a release flows from dev to PyPI, across both cube-standard and
cube-harness. This is the maintainer runbook; for day-to-day contribution
rules see CONTRIBUTING.md. Versioning specifics
(dev carries the next rc, the >=<rcN> pin) live in CONTRIBUTING's
Releases & dev versioning — read
that first; this doc covers the mechanics.
| Branch | Role | Rule |
|---|---|---|
dev |
Integration. Always carries the next unreleased rc. |
All work branches off dev and merges back to dev. |
main |
Release ref. Tags are cut from here; the published commit == origin/main. |
Never commit or hotfix directly to main. It only ever moves by promoting dev. |
Why this matters. When
mainis treated as "the released branch" people are tempted to hotfix straight into it. That orphans the fix fromdev, and the branches drift until they can only be reconciled by hand. A fix belongs indev; it reachesmainat the next promote. Protectmain(require PRs, no direct pushes) so this can't happen by accident.
A release is (1) promote dev→main, then (2) tag from main. The
tag — not the merge — is what publishes; pushing <prefix>/v<version> triggers
each repo's release.yml to build that one
package and publish it to PyPI via trusted publishing.
Do this in both repos that have changes.
git checkout main && git pull
git merge --no-ff origin/dev # NOT a fast-forward — keep the promote as a merge commit
# Conflicts resolve in dev's favour: dev is the source of truth, main is downstream.
git push origin mainIf main carries an orphan commit (someone hotfixed it directly — see above),
the merge will conflict on those files. Resolve to dev's state and record
what main-only change was intentionally dropped in the merge-commit message.
The packages form a dependency graph spanning both repos, so they must publish bottom-up — a downstream package must not land on PyPI pinning an upstream version that isn't up yet. The driver enforces this ordering:
| Tier | Packages | Repo | Tag prefix |
|---|---|---|---|
| 1 | cube-standard |
cube-standard | cube-standard/v* |
| 2 | cube-resources/* |
cube-standard | cube-resources/<name>/v* |
| 3 | cube-tools/* |
cube-standard | cube-tools/<name>/v* |
| 4 | cube-harness |
cube-harness | cube-harness/v* |
| 5 | cubes/* |
cube-harness | cubes/<name>/v* |
Use the cross-repo driver, scripts/release.py — it reads
every package's version, compares it to the latest published tag, computes the
tier order, pushes tags tier by tier, and waits for each tier to appear on
PyPI before starting the next.
# 1. Dry-run by default — prints the ordered plan, no side effects. Eyeball it.
python scripts/release.py
# 2. First real publish: one package, supervised.
python scripts/release.py --execute --only cube-standard
# 3. Then the rest (or another --only subset). Idempotent: re-running skips
# already-published packages and resumes at the next tier.
python scripts/release.py --executeThe driver is dry-run by default and refuses to act on ambiguity — dirty
tree, version not bumped past the last tag, HEAD != origin/main, or a PyPI
timeout all stop the run with an actionable message rather than guessing. It
expects to run from main (override with --ref, but don't unless you know
why).
After a package publishes rc<N>, bump its version on dev to rc<N+1> so
dev once again carries an unreleased version. Skipping this lets uv
confuse a dev build with the published wheel during cross-repo CI (see
CONTRIBUTING for the full rationale).
You don't have to publish the whole graph. --only <dist> [--only <dist> ...]
restricts the run, and the driver still honours tier order among the selected
packages. Common cases:
- Core only:
--only cube-standard --only cube-harness(plus any tools cube-harness needs). - One new cube:
--only <cube-dist>— cubes are tier 5 and independent of each other, so a single cube can ship without touching anything else, as long as thecube-standardversion it pins is already on PyPI.
-
devis green on both repos at the commit you intend to promote. - Each package to publish has its version bumped past its last published tag.
-
devhas been promoted tomain(Phase 1);mainis clean and pushed. -
scripts/release.pydry-run plan looks right. - Publish tier 1 first, confirm on PyPI, then continue.
- After publishing, bump
devto the nextrc(Phase 3).