Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/developer/guides/branching-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
193 changes: 193 additions & 0 deletions docs/developer/guides/release-process.md
Original file line number Diff line number Diff line change
@@ -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 <expanded paths> \
-m "(tier_a or tier_b) and (level_1 or level_2 or level_3)" \
-k "<select>"
```

It then resolves `tests_maturity` from the result:

- non-empty selection under `tier_a or tier_b` **and** all pass → `supported`
- tests ran but some failed → `preview`
- empty selection (no `tier_a`/`tier_b` tests — e.g. only `tier_c`), all selected
tests skipped, no tests at all, or a pytest execution error → `experimental`

"Supported" is therefore tied to the validated tiers *by construction* — you
cannot announce a feature as supported without `tier_a`/`tier_b` tests that pass.

Run it any time (intended on `development`):

```bash
./uw dev release-check # quick dry-run, levels 1,2
./uw dev release-check 1,2,3 # full
```

## Quarterly checklist

Run `./uw dev release` from the **main checkout on `development`**. Every
state-changing step is confirmed; the tool stops **before** the production tag so
`main`'s branch protection (PR + review) stays intact.

| # | Step | Automated? |
|---|------|------------|
| 1 | Preflight: on `development`, clean tree, fetched, ahead of `main` | auto |
| 2 | Pick the next version (`vX.Y.0`) | confirm |
| 3 | Tag + push the RC on `development` (`vX.Y.0rcN`) — RC tags are allowed here | confirm |
| 4 | Full validation: `test_levels.sh 1,2,3 --isolation` + the per-feature gate | auto |
| 5 | Aggregate results → achieved maturity per feature (results to `$TMPDIR`) | auto |
| 6 | Scaffold `docs/release-notes/vX.Y.0.md` with auto Supported / Preview sections | confirm |
| 7 | Open the `development → main` PR with the rendered notes | confirm |
| 8 | **After the PR merges**: `git tag vX.Y.0 && git push` → CI publishes the release | manual |

Step 8 is deliberately left to a human so the review gate on `main` is never
bypassed. CI (`.github/workflows/release.yml`) publishes the GitHub Release from
the committed `docs/release-notes/vX.Y.0.md`.

## Release notes generation

Steps 4–6 auto-populate two sections of the notes from the gate:

- **Supported (validated)** — features whose achieved maturity is `supported`.
- **Preview (present, unguaranteed)** — everything else that is announced
(`preview`, and `experimental` tagged as such).

You then hand-write **Highlights** and **Contributors**. The template lives at
`docs/release-notes/TEMPLATE.md`.

## Branch hygiene

The other half of staying organised is not letting merged branches pile up. Run
before each release (or whenever the branch list feels tangled):

```bash
./uw dev branch-hygiene # report: merged / open-PR / stale
./uw dev branch-hygiene --prune # delete merged branches (confirms each)
```

It lists branches merged into `development` (safe to delete), branches with open
PRs (never pruned), and stale branches (unmerged, no PR, >90 days). `--prune`
deletes only merged-and-no-open-PR branches, confirming each.

## Knowing what's available to release

Before a release, find out which features have actually landed on `development`
since the last one — the pool you can choose to announce:

```bash
./uw dev feature-audit # feature-branch PR merges since last release
./uw dev feature-audit --all # also include bugfix/ and docs/ merges
./uw dev feature-audit --since v3.0.1
```

It lists each merged `feature/*` PR (date, number, branch) and reminds you how
many features the manifest currently declares. Use it to decide which merges
become manifest entries (and at what maturity) for the upcoming release. It
detects PR-*merge* commits only — squash-merged PRs won't appear.

This is the feature-level counterpart to the test-level drift check below:
`feature-audit` answers "what merged that we might announce", while
`check_manifest_coverage.py` answers "what validated test isn't claimed".

## Keeping the manifest honest

The main risk is **drift** — a `tier_a` test file that no feature references, so
its coverage is invisible to the gate. `scripts/check_manifest_coverage.py` (run
in `development` CI) warns when a `tier_a`-marked test file is not referenced by
any feature's `validation.paths`. Start as a warning; promote to an error once
coverage is established.

## See also

- `docs/developer/guides/branching-strategy.md` — how work lands on `development`
- `docs/developer/guides/version-management.md` — tags and `setuptools-scm`
- `docs/developer/TESTING-RELIABILITY-SYSTEM.md` — the tier A/B/C system
10 changes: 10 additions & 0 deletions docs/release-notes/TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

- Brief summary of the most important changes in this release.

## Supported (validated)

<!-- Auto-populated by scripts/release_gate.py from the feature manifest:
features whose tier_a/b validation passed on this release. Guaranteed. -->

## Preview (present, unguaranteed)

<!-- Auto-populated: features whose code is on main but is NOT guaranteed to work.
See docs/developer/guides/release-process.md for what these labels mean. -->

## New Features

- Feature description (PR #NN)
Expand Down
107 changes: 107 additions & 0 deletions docs/release-notes/feature-manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Underworld3 feature manifest
# =============================
# Declarative registry of shippable features for maturity-gated releases.
#
# At release time (and any time via `./uw dev release-check`), scripts/release_gate.py
# runs each feature's `validation` selection against the current checkout and
# decides whether the feature can be ANNOUNCED at the maturity its owner claims:
#
# supported — tier_a/b tests exist and pass on the release candidate. Guaranteed.
# preview — code is on main, but the release does NOT guarantee it works.
# experimental — present, not announced as working.
#
# The announced maturity is min(claim, tests_maturity):
# * an owner may be deliberately cautious (claim `preview` even though tests pass);
# * the gate may downgrade (claim `supported` but a test fails -> preview).
# It NEVER blocks the dev->main merge — the code ships regardless; only the
# announcement in the release notes changes.
#
# "Supported" is tied to the existing tier markers by construction: a feature's
# selection is filtered to `tier_a or tier_b` (see validation.markers), so
# passing == validated. There is NO per-feature pytest marker to maintain — the
# manifest scopes WHICH tests belong to a feature; the tier markers scope WHICH
# are trustworthy.
#
# Full guide: docs/developer/guides/release-process.md
#
# Schema (per feature):
# key: stable slug (used in tool output and the drift check)
# title: human title for the release notes
# owner: GitHub handle of the person who vouches for it
# claim: supported | preview | experimental (what the owner wants to announce)
# summary: one-line description for the release notes
# validation:
# paths: list of test files/globs that scope the feature (required to be announceable)
# select: optional pytest -k expression to narrow within those files
# markers: marker expression; defaults to "tier_a or tier_b" — leave as default
# unless you have a good reason
# levels: optional "1,2,3" level filter for cost control (per-feature override)
# docs: optional list of doc pages describing the feature

schema_version: 1

features:
- key: units-system
title: "Units and Scaling System"
owner: "@lmoresi"
claim: supported
summary: >
Pint-backed dimensional quantities, non-dimensionalisation, and unit-aware
expression arithmetic.
validation:
paths:
- tests/test_0700_units_system.py
- tests/test_0710_units_utilities.py
- tests/test_0751_subtraction_chain_units.py
- tests/test_0752_units_scale_factor_preservation.py
- tests/test_0754_arithmetic_closure_complete.py
markers: "tier_a or tier_b"
docs:
- docs/developer/design/UNITS_SIMPLIFIED_DESIGN_2025-11.md

- key: mesh-smoothing
title: "Winslow Mesh Smoother"
owner: "@lmoresi"
claim: supported
summary: >
Parallel-safe interior mesh smoothing (smooth_mesh_interior) — the
foundation the adaptive movers and geometric multigrid build on.
validation:
paths:
- tests/test_0850_mesh_smoothing.py
markers: "tier_a or tier_b"

- key: mesh-adaptation
title: "MMPDE Mesh Mover / Adaptive Remeshing"
owner: "@lmoresi"
# Claimed `preview` on purpose: the tier_a tests below pass, but we are not
# yet ready to GUARANTEE the mover across production problems. The gate will
# show tests-pass but announce it as preview (min(claim, tests) == preview).
# This is the canonical "code is on main but unguaranteed" case.
claim: preview
summary: >
follow_metric / OT mesh movement and variable transfer for anisotropic
mesh adaptation.
validation:
paths:
- tests/test_0750_meshing_follow_metric.py
- tests/test_0760_mesh_ot_adapt.py
- tests/test_0830_mesh_adapt_variable_transfer.py
markers: "tier_a or tier_b"

- key: fmg
title: "Geometric Multigrid / FMG Preconditioner"
owner: "@lmoresi"
# Claimed `supported`, but there is no dedicated tier_a/b validation suite
# yet. The gate will report an empty selection and DOWNGRADE this to
# experimental — exactly the signal we want: FMG cannot be announced as
# supported until it has a tier_a test. Add one, then this row goes green.
claim: supported
summary: >
Automatic geometric full-multigrid preconditioning for the SNES solvers
on refinement meshes (the `preconditioner` property).
validation:
paths:
- tests/test_101*.py
select: "multigrid or fmg or preconditioner"
markers: "tier_a or tier_b"
Loading
Loading