diff --git a/docs/developer/guides/branching-strategy.md b/docs/developer/guides/branching-strategy.md index e538b4bd..6d332361 100644 --- a/docs/developer/guides/branching-strategy.md +++ b/docs/developer/guides/branching-strategy.md @@ -33,6 +33,10 @@ feature/Z ────────────────────┘ ### `main` — stable releases - Receives merges from `development` at release time (quarterly, or when ready). + The quarterly merge brings over the **whole** stable state of `development` — + it is *not* a surgical cherry-pick of one feature. Which features are *announced + as working* is decided by the release notes, not by branch surgery + (see [Maturity-gated promotion](#maturity-gated-promotion) below). - Critical bug fixes are cherry-picked from `development` between releases. - Every merge is tagged: `v3.0.0`, `v3.0.1` (patch), `v3.1.0` (quarterly). - Binder launcher tracks the latest release tag. @@ -118,6 +122,28 @@ Bug found Version numbers are managed by setuptools-scm from git tags — see `version-management.md`. +### Maturity-gated promotion + +The quarterly `development → main` merge brings over everything stable on +`development`. The hard part is not *which commits* move — it is being able to say +"feature X is released and **works**" without claiming the same for half-finished +work that rode along. + +The project solves this with **release notes, not branch surgery**. A declarative +manifest (`docs/release-notes/feature-manifest.yaml`) lists each shippable feature +and the tests that validate it. At release time a gate runs each feature's tests +and sorts it into: + +- **Supported (validated)** — `tier_a`/`tier_b` tests pass; guaranteed. +- **Preview (present, unguaranteed)** — code is on `main` but not guaranteed. + +So the MMPDE mover's code can be on `main` while the release announces only the +validated features as supported — the mover stays "preview" until its validation +is trusted. Run the gate any time with `./uw dev release-check`, and drive the +whole promotion with `./uw dev release`. + +**Full guide**: `release-process.md`. + ## CI Requirements For this strategy to work, CI must be reliable: diff --git a/docs/developer/guides/release-process.md b/docs/developer/guides/release-process.md new file mode 100644 index 00000000..8eb7d524 --- /dev/null +++ b/docs/developer/guides/release-process.md @@ -0,0 +1,193 @@ +# Release Process: Maturity-Gated Quarterly Promotion + +**Status**: Active +**Date**: 2026-06 + +This guide describes how finished work is promoted from `development` to `main` +*with confidence*. It complements `branching-strategy.md` (which covers how work +lands on `development` in the first place) and `version-management.md` (tags and +versioning). + +## Overview — the "we can be sure it works" guarantee + +`development` is a busy integration trunk — at any time it is hundreds of commits +ahead of `main` and carries many features at different stages of maturity. The +goal is **not** to surgically isolate one feature's commits onto `main`. That +fights the grain: a feature like the MMPDE mover depends on the Winslow smoother, +boundary-slip surfaces, FMG infrastructure, and field-transfer — its dependency +closure is most of `development` anyway. + +Instead: + +> `main` accumulates whatever stable work comes over from `development` each +> quarter. The **release notes** — not branch surgery — distinguish features that +> are *validated and guaranteed* from features that are merely *present but not +> guaranteed to work*. + +So the mover's code can be on `main` while the release simply does **not** claim +it works. FMG can be announced as supported while the mover rides along as +preview. This matches how the project actually develops. + +## Maturity definitions + +Every shippable feature is announced at one of three maturities: + +| Maturity | Meaning | Release-notes section | +|----------|---------|-----------------------| +| **supported** | `tier_a`/`tier_b` tests exist and pass on the release candidate. Guaranteed to work. | *Supported (validated)* | +| **preview** | Code is on `main`, but the release does **not** guarantee it works. | *Preview (present, unguaranteed)* | +| **experimental** | Present in the tree, not announced as working. | *Preview* (tagged `experimental`) | + +The announced maturity is `min(claim, tests_maturity)`: + +- An owner may be deliberately **cautious** — claim `preview` even though the + tests pass (this is the mover today). +- The gate may **downgrade** — claim `supported` but a test fails, or there is no + validation suite, so it drops to `preview`/`experimental`. + +The gate **never blocks the `development → main` merge**. The code ships +regardless; only the announcement changes. + +## The feature manifest + +Features are declared in **`docs/release-notes/feature-manifest.yaml`**. Each +entry scopes *which tests belong to a feature*; the existing tier markers scope +*which of those tests are trustworthy*. There is **no per-feature pytest marker** +to maintain. + +```yaml +features: + - key: units-system + title: "Units and Scaling System" + owner: "@lmoresi" + claim: supported # what the owner wants to announce + summary: > + Pint-backed dimensional quantities and unit-aware arithmetic. + validation: + paths: # test files/globs that scope the feature + - tests/test_0700_units_system.py + - tests/test_0710_units_utilities.py + select: "..." # optional pytest -k to narrow + markers: "tier_a or tier_b" # default; ties "supported" to validated tiers + levels: "1,2,3" # optional level filter (cost control) + docs: + - docs/developer/design/UNITS_SIMPLIFIED_DESIGN_2025-11.md +``` + +### Worked example: FMG vs the mover + +- **FMG** claims `supported` but has **no dedicated `tier_a` test** yet. The gate + finds an empty selection and downgrades it to `experimental`. That is the + signal you want: *FMG cannot be announced as supported until it has a + validation suite.* Add a `tier_a` test and the row goes green. +- **The MMPDE mover** has passing `tier_a` tests, but its owner claims `preview` + because it is not yet trusted across production problems. It announces as + `preview` — code on `main`, not guaranteed. This is the canonical case. + +## The validation gate + +`scripts/release_gate.py` reads the manifest and, per feature, builds a pytest +invocation equivalent to: + +```bash +pytest --config-file=tests/pytest.ini \ + -m "(tier_a or tier_b) and (level_1 or level_2 or level_3)" \ + -k "