mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
A `vX.Y-must-backport` label means the fix must reach `X.Y` and every newer release. The previous logic only honoured such a label when `X.Y` was still an active release branch: `backport_floor` derived the floor from active branches only, and `BackportPRs.receive_release_prs` built the search label set solely from active-branch labels. So a PR labelled `v25.12-must-backport` after `25.12` reached end-of-life was never even selected by the search, and would not be backported to the newer active releases (`26.2`, `26.3`, …) that still need it. This is the footgun where a fix lands in an old release but is missing from a newer one, so upgrading silently loses it. Now a version-specific label is honoured by its literal version regardless of whether the named release is active: `backport_floor` parses the version from the label itself, and the search includes every `vX.Y-must-backport` label that exists in the repo whose version is not newer than the newest active release. The floor still fans out to every active release `>=` it; the named release is simply not among the targets when it is no longer active. This matters mainly for the private fork, where releases such as `26.2` stay active longer: a `v25.12-must-backport` PR now backports to `26.2` as expected. Unit tests in `test_cherry_pick_branches.py` are updated to the new contract (inactive labels set the floor and widen selection) plus new cases for the end-of-life named release fanning out and a floor newer than every active release yielding no branches. `docs/en/development/backports.md` is updated. Related: https://github.com/ClickHouse/ClickHouse/pull/101644
125 lines
5.1 KiB
Python
125 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Pure branch-selection logic for the backport automation (`cherry_pick.py`).
|
|
|
|
This module is intentionally free of GitHub / git / CI dependencies so the
|
|
branch-selection contract can be unit-tested directly (see
|
|
`test_cherry_pick_branches.py`). The label name constants live with `Labels`
|
|
in `cherry_pick.py` / `pr_info.py` and are passed in by the caller, so this
|
|
module stays the single source of truth for *which branches* a PR reaches
|
|
without duplicating *what the labels are called*.
|
|
"""
|
|
import re
|
|
from typing import List, Optional, Sequence, Set, Tuple
|
|
|
|
# Version-specific backport label, e.g. `v25.12-must-backport`.
|
|
VERSION_LABEL_RE = re.compile(r"^v(\d+)\.(\d+)-must-backport$")
|
|
|
|
|
|
def version_key(version: str) -> Tuple[int, ...]:
|
|
"""
|
|
Turn a release version (e.g. `25.12`, `26.1`) into a tuple of integers so
|
|
that versions compare numerically: `(25, 12) < (26, 1) < (26, 2) < (26, 10)`.
|
|
Plain string comparison would order `26.10` before `26.2`, which is wrong.
|
|
"""
|
|
return tuple(int(part) for part in version.split("."))
|
|
|
|
|
|
def branch_version(branch: str) -> Tuple[int, ...]:
|
|
"""Release branch name to a comparable version: `release/25.12` -> `(25, 12)`."""
|
|
return version_key(branch.replace("release/", ""))
|
|
|
|
|
|
def label_version(label: str) -> Optional[Tuple[int, ...]]:
|
|
"""A version-specific backport label to its version: `v25.12-must-backport`
|
|
-> `(25, 12)`. Returns `None` for any other label."""
|
|
match = VERSION_LABEL_RE.fullmatch(label)
|
|
if match is None:
|
|
return None
|
|
return (int(match.group(1)), int(match.group(2)))
|
|
|
|
|
|
def backport_floor(pr_labels: Sequence[str]) -> Optional[Tuple[int, ...]]:
|
|
"""
|
|
The lowest version among the PR's version-specific backport labels
|
|
(`v<MAJOR>.<MINOR>-must-backport`), or `None` if there are none.
|
|
|
|
A version-specific label marks the OLDEST release the PR must reach, so the
|
|
minimum of them is the floor: the PR is backported to that release and to
|
|
every newer active release branch. The named release does NOT need to be
|
|
active itself -- a label for an end-of-life release (e.g. `v25.12` when only
|
|
the 26.x line and the 25.8 LTS are active) still pulls the fix forward into
|
|
every active release after it, so upgrading from that release never silently
|
|
loses the fix.
|
|
"""
|
|
floors = [
|
|
version
|
|
for version in (label_version(label) for label in pr_labels)
|
|
if version is not None
|
|
]
|
|
return min(floors) if floors else None
|
|
|
|
|
|
def select_backport_branches(
|
|
pr_labels: Sequence[str],
|
|
release_branches: Sequence[str],
|
|
rolling_out: Set[str],
|
|
*,
|
|
general_backport_labels: Set[str],
|
|
force_backport_label: str,
|
|
) -> Tuple[List[str], List[str]]:
|
|
"""
|
|
Decide which release branches a PR should be backported to.
|
|
|
|
Returns `(branches, skipped)`, both in `release_branches` order:
|
|
- `branches`: release branches the PR should be backported to.
|
|
- `skipped`: rolling-out branches that were excluded, so the caller can
|
|
close any stale cherry-pick / backport PRs for them.
|
|
|
|
Rules:
|
|
- `force_backport_label` -> all release branches, ignoring `rolling_out`.
|
|
- any of `general_backport_labels` (`pr-must-backport`, `pr-critical-bugfix`,
|
|
...) -> all release branches, but a `rolling_out` branch is skipped unless
|
|
a version-specific label covers it (its version is `>= floor`).
|
|
- otherwise (version-specific labels only) -> the floor release and every
|
|
newer active release branch. `rolling_out` does not apply here: an explicit
|
|
version request always proceeds.
|
|
"""
|
|
labels = set(pr_labels)
|
|
floor = backport_floor(pr_labels)
|
|
# The branches a version-specific label expands to: the floor release and
|
|
# every newer active branch. Such a label overrides the `rolling_out` skip
|
|
# for exactly these branches.
|
|
covered_by_floor = {
|
|
branch
|
|
for branch in release_branches
|
|
if floor is not None and branch_version(branch) >= floor
|
|
}
|
|
|
|
if force_backport_label in labels:
|
|
return list(release_branches), []
|
|
|
|
if labels & general_backport_labels:
|
|
branches = [
|
|
branch
|
|
for branch in release_branches
|
|
if branch not in rolling_out or branch in covered_by_floor
|
|
]
|
|
skipped = [
|
|
branch
|
|
for branch in release_branches
|
|
if branch in rolling_out and branch not in covered_by_floor
|
|
]
|
|
return branches, skipped
|
|
|
|
# Version-specific labels only. `covered_by_floor` is the floor release and
|
|
# every newer active branch. It is normally non-empty -- the search that
|
|
# feeds this function only selects PRs whose floor is not newer than the
|
|
# newest active release -- but it may be empty if a PR carries only a label
|
|
# newer than every active release; the caller skips such PRs gracefully.
|
|
assert floor is not None, (
|
|
"select_backport_branches called without a general backport label and "
|
|
f"without a version-specific label; labels: {sorted(labels)}"
|
|
)
|
|
return [branch for branch in release_branches if branch in covered_by_floor], []
|