Skip to content
Open
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
4 changes: 2 additions & 2 deletions .backportrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"backport"
],
"branchLabelMapping": {
"^v(\\d+).(\\d+).\\d+(?:-(?:alpha|beta|rc)\\d+)?$": "$1.$2",
"^v9.6.0$": "main"
"^v9.6.0$": "main",
"^v(\\d+).(\\d+).\\d+(?:-(?:alpha|beta|rc)\\d+)?$": "$1.$2"
},
"copySourcePRLabels": "^(?!backport$)(?!v\\d).*$",
"sourcePRLabels": [
Expand Down
123 changes: 123 additions & 0 deletions dev-tools/unittest/test_update_backportrc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0 and the following additional limitation. Functionality enabled by the
# files subject to the Elastic License 2.0 may only be used in production when
# invoked by an Elasticsearch process with a license key installed that permits
# use of machine learning features. You may not use this file except in
# compliance with the Elastic License 2.0 and the foregoing additional
# limitation.

"""Pytest tests for dev-tools/update_backportrc.py (minor-freeze backportrc update).

The backport tool applies the FIRST matching ``branchLabelMapping`` entry, so the
specific main-only override (``^v<main>$`` -> ``main``) must be emitted before the
generic ``^vX.Y.Z$`` -> ``X.Y`` rule. Otherwise the new main version label resolves
to a non-existent MAJOR.MINOR release branch and the backport fails.
"""

from __future__ import annotations

import sys
from pathlib import Path

_DEV_TOOLS = Path(__file__).resolve().parents[1]
if str(_DEV_TOOLS) not in sys.path:
sys.path.insert(0, str(_DEV_TOOLS))

import update_backportrc as ubrc # noqa: E402

_GENERIC_KEY = r"^v(\d+).(\d+).\d+(?:-(?:alpha|beta|rc)\d+)?$"


def _base_data() -> dict:
return {
"targetBranchChoices": ["main", "9.4", "9.3"],
"branchLabelMapping": {
_GENERIC_KEY: "$1.$2",
"^v9.5.0$": "main",
},
}


def test_main_override_emitted_first() -> None:
data = _base_data()
changed = ubrc.update_backportrc_for_minor_freeze(
data, new_release_branch="9.5", main_new_version="9.6.0"
)
assert changed is True

keys = list(data["branchLabelMapping"].keys())
assert keys[0] == "^v9.6.0$", f"main override must be first, got {keys}"
assert data["branchLabelMapping"]["^v9.6.0$"] == "main"
# The generic rule is preserved, just after the override.
assert data["branchLabelMapping"][_GENERIC_KEY] == "$1.$2"


def test_stale_main_override_removed() -> None:
data = _base_data()
ubrc.update_backportrc_for_minor_freeze(
data, new_release_branch="9.5", main_new_version="9.6.0"
)
# The previous main override (^v9.5.0$) must not linger.
assert "^v9.5.0$" not in data["branchLabelMapping"]
main_keys = [k for k, v in data["branchLabelMapping"].items() if v == "main"]
assert main_keys == ["^v9.6.0$"]


def test_reorder_only_is_detected_as_change() -> None:
# Mapping already has the right content but the generic rule is first.
data = {
"targetBranchChoices": ["main", "9.5"],
"branchLabelMapping": {
_GENERIC_KEY: "$1.$2",
"^v9.6.0$": "main",
},
}
changed = ubrc.update_backportrc_for_minor_freeze(
data, new_release_branch="9.5", main_new_version="9.6.0"
)
assert changed is True
assert list(data["branchLabelMapping"].keys())[0] == "^v9.6.0$"


def test_idempotent_when_already_ordered() -> None:
data = {
"targetBranchChoices": ["main", "9.5"],
"branchLabelMapping": {
"^v9.6.0$": "main",
_GENERIC_KEY: "$1.$2",
},
}
changed = ubrc.update_backportrc_for_minor_freeze(
data, new_release_branch="9.5", main_new_version="9.6.0"
)
assert changed is False
assert list(data["branchLabelMapping"].keys())[0] == "^v9.6.0$"


def test_generic_rule_preserved_exactly() -> None:
data = _base_data()
ubrc.update_backportrc_for_minor_freeze(
data, new_release_branch="9.5", main_new_version="9.6.0"
)
assert _GENERIC_KEY in data["branchLabelMapping"]


def test_misconfigured_main_key_is_corrected() -> None:
# Existing mapping already has the new main key but pointing at a (wrong)
# release branch instead of "main". The override must still win.
data = {
"targetBranchChoices": ["main", "9.5"],
"branchLabelMapping": {
_GENERIC_KEY: "$1.$2",
"^v9.6.0$": "9.6",
},
}
changed = ubrc.update_backportrc_for_minor_freeze(
data, new_release_branch="9.5", main_new_version="9.6.0"
)
assert changed is True
keys = list(data["branchLabelMapping"].keys())
assert keys[0] == "^v9.6.0$", f"main override must be first, got {keys}"
assert data["branchLabelMapping"]["^v9.6.0$"] == "main"
22 changes: 14 additions & 8 deletions dev-tools/update_backportrc.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,22 @@ def update_backportrc_for_minor_freeze(
data["targetBranchChoices"] = choices
changed = True

mapping: dict[str, str] = dict(data.get("branchLabelMapping", {}))
old_mapping: dict[str, str] = dict(data.get("branchLabelMapping", {}))
new_main_key = f"^v{main_new_version}$"
old_main_keys = [k for k, v in mapping.items() if v == "main" and k != new_main_key]
for key in old_main_keys:
del mapping[key]
# Rebuild the mapping with the main-only override FIRST. The backport tool applies
# the first matching branchLabelMapping key, so the specific main override (e.g.
# ^v9.6.0$ -> main) must precede the generic ^vX.Y.Z$ -> X.Y rule. Otherwise the
# main version label resolves to a non-existent MAJOR.MINOR release branch and the
# backport fails (see PR #3071 attempting a non-existent "9.6" branch).
# Exclude new_main_key so a misconfigured existing entry (e.g. ^v9.6.0$ -> "9.6")
# cannot overwrite the correct override below via the update() call.
non_main = {k: v for k, v in old_mapping.items() if v != "main" and k != new_main_key}
new_mapping: dict[str, str] = {new_main_key: "main"}
new_mapping.update(non_main)
# dict equality ignores order, so compare item order explicitly to catch reorders.
if list(new_mapping.items()) != list(old_mapping.items()):
changed = True
if mapping.get(new_main_key) != "main":
mapping[new_main_key] = "main"
changed = True
data["branchLabelMapping"] = mapping
data["branchLabelMapping"] = new_mapping

return changed

Expand Down