| |
@@ -17,6 +17,7 @@
|
| |
import re
|
| |
import requests
|
| |
import sys
|
| |
+ from collections import namedtuple
|
| |
from datetime import date, datetime
|
| |
from http import HTTPStatus
|
| |
from pyrpkg import rpkgError
|
| |
@@ -28,6 +29,40 @@
|
| |
|
| |
dist_git_config = None
|
| |
|
| |
+ # RHEL Product Pages Phase Identifiers
|
| |
+ pp_phase_name_lookup = dict()
|
| |
+ # Phase 230 is "Planning / Development / Testing" (AKA DevTestDoc)
|
| |
+ pp_phase_devtestdoc = 230
|
| |
+ pp_phase_name_lookup[pp_phase_devtestdoc] = "DevTestDoc"
|
| |
+
|
| |
+ # Phase 450 is "Stabilization" (AKA Exception Phase)
|
| |
+ pp_phase_stabilization = 450
|
| |
+ pp_phase_name_lookup[pp_phase_stabilization] = "Stabilization"
|
| |
+
|
| |
+ # Phase 600 is "Maintenance" (AKA Z-stream Phase)
|
| |
+ pp_phase_maintenance = 600
|
| |
+ pp_phase_name_lookup[pp_phase_maintenance] = "Maintenance"
|
| |
+
|
| |
+ rhel_state_nt = namedtuple(
|
| |
+ "RHELState",
|
| |
+ [
|
| |
+ "latest_version",
|
| |
+ "target_version",
|
| |
+ "rule_branch",
|
| |
+ "phase",
|
| |
+ "rhel_target_default",
|
| |
+ "enforcing",
|
| |
+ ],
|
| |
+ )
|
| |
+
|
| |
+
|
| |
+ # Super-class for errors related to internal RHEL infrastructure
|
| |
+ class RHELError(Exception):
|
| |
+ pass
|
| |
+
|
| |
+
|
| |
+ logger = logging.getLogger(__name__)
|
| |
+
|
| |
|
| |
def do_fork(logger, base_url, token, repo_name, namespace, cli_name):
|
| |
"""
|
| |
@@ -312,43 +347,27 @@
|
| |
Returns
|
| |
-------
|
| |
str
|
| |
- Correspoinding RHEL name.
|
| |
+ Corresponding RHEL name.
|
| |
"""
|
| |
if csname == "c8s" or csname == "cs8":
|
| |
- return "rhel-8"
|
| |
+ return 8, "rhel-8"
|
| |
if csname == "c9s" or csname == "cs9":
|
| |
- return "rhel-9"
|
| |
+ return 9, "rhel-9"
|
| |
if csname == "c10s" or csname == "cs10":
|
| |
- return "rhel-10"
|
| |
+ return 10, "rhel-10"
|
| |
if csname == "c11s" or csname == "cs11":
|
| |
- return "rhel-11"
|
| |
+ return 11, "rhel-11"
|
| |
return None
|
| |
|
| |
|
| |
- def does_divergent_branch_exist(
|
| |
- repo_name, x_version, active_y, rhel_dist_git, namespace
|
| |
- ):
|
| |
- logger = logging.getLogger(__name__)
|
| |
-
|
| |
+ def does_branch_exist(rhel_dist_git, namespace, repo_name, branch):
|
| |
# Determine if the Y-1 branch exists for this repo
|
| |
-
|
| |
- if x_version >= 10 and active_y <= 0:
|
| |
- # For 10.0 and later X.0 releases, check for a rhel-X.0-beta branch
|
| |
- divergent_branch = "rhel-{}.0-beta".format(x_version)
|
| |
- elif x_version <= 9:
|
| |
- divergent_branch = "rhel-{}.{}.0".format(x_version, active_y - 1)
|
| |
- else:
|
| |
- # Starting with RHEL 10, the branch names have dropped the extra .0
|
| |
- divergent_branch = "rhel-{}.{}".format(x_version, active_y - 1)
|
| |
-
|
| |
- logger.debug("Divergent branch: {}".format(divergent_branch))
|
| |
-
|
| |
g = gitpython.cmd.Git()
|
| |
try:
|
| |
g.ls_remote(
|
| |
"--exit-code",
|
| |
os.path.join(rhel_dist_git, namespace, repo_name),
|
| |
- divergent_branch,
|
| |
+ branch,
|
| |
)
|
| |
branch_exists = True
|
| |
except gitpython.GitCommandError as e:
|
| |
@@ -381,30 +400,28 @@
|
| |
return major_version, minor_version, extra_version
|
| |
|
| |
|
| |
- def determine_active_y_version(rhel_version, api_url):
|
| |
- """
|
| |
- Returns: A 4-tuple containing:
|
| |
- 0. The major release version(int)
|
| |
- 1. The active Y-stream version(int)
|
| |
- 2. Whether the active release is the pre-X.0 beta
|
| |
- 3. Whether we are in the Exception Phase(bool)
|
| |
- """
|
| |
- logger = logging.getLogger(__name__)
|
| |
+ def parse_rhel_branchname(shortname):
|
| |
+ # The branchname is in the form rhel-9-1.0 or rhel-10.0[-beta]
|
| |
+ m = re.match(
|
| |
+ "rhel-(?P<major>[0-9]+)[.-](?P<minor>[0-9]+)([.]0|[-](?P<extra>.*))?", shortname
|
| |
+ )
|
| |
+ if not m:
|
| |
+ raise RuntimeError("Could not parse version from {}".format(shortname))
|
| |
+
|
| |
+ major_version = int(m.group("major"))
|
| |
+ minor_version = int(m.group("minor"))
|
| |
+ extra_version = m.group("extra") or None
|
| |
|
| |
- # Phase Identifiers
|
| |
- # Phase 230 is "Planning / Development / Testing" (AKA DevTestDoc)
|
| |
- # Phase 450 is "Stabilization"
|
| |
- phase_devtestdoc = 230
|
| |
- phase_stabilization = 450
|
| |
+ return major_version, minor_version, extra_version
|
| |
|
| |
- # Query the "package pages" API for the current active Y-stream release
|
| |
- request_params = {
|
| |
- "phase__in": "{},{}".format(phase_devtestdoc, phase_stabilization),
|
| |
- "product__shortname": "rhel",
|
| |
- "relgroup__shortname": rhel_version,
|
| |
- "format": "json",
|
| |
- }
|
| |
|
| |
+ def query_package_pages(api_url, request_params):
|
| |
+ """
|
| |
+ api_url: A URL to the API endpoing of the Product Pages (e.g.
|
| |
+ "https://example.com/pp/api/")
|
| |
+ request_params: A set of python-requests-compatible URL parameters to
|
| |
+ focus the query.
|
| |
+ """
|
| |
res = requests.get(
|
| |
os.path.join(api_url, "latest", "releases"),
|
| |
params=request_params,
|
| |
@@ -413,37 +430,241 @@
|
| |
res.raise_for_status()
|
| |
payload = json.loads(res.text)
|
| |
logger.debug("Response from PP API: {}".format(json.dumps(payload, indent=2)))
|
| |
- if len(payload) < 1:
|
| |
+
|
| |
+ return payload
|
| |
+
|
| |
+
|
| |
+ def format_branch(x_version, y_version, is_beta):
|
| |
+ if x_version <= 9:
|
| |
+ # 9.x and older releases include an excess .0 in the branch name
|
| |
+ if is_beta:
|
| |
+ branch = "rhel-{}.{}.0-beta".format(x_version, y_version)
|
| |
+ else:
|
| |
+ branch = "rhel-{}.{}.0".format(x_version, y_version)
|
| |
+ else:
|
| |
+ # Starting with RHEL 10, the branch names have dropped the extra .0
|
| |
+ if is_beta:
|
| |
+ branch = "rhel-{}.{}-beta".format(x_version, y_version)
|
| |
+ else:
|
| |
+ branch = "rhel-{}.{}".format(x_version, y_version)
|
| |
+ return branch
|
| |
+
|
| |
+
|
| |
+ def determine_rhel_state(rhel_dist_git, namespace, repo_name, cs_branch, pp_api_url):
|
| |
+ """
|
| |
+ Arguments:
|
| |
+ * rhel_dist_git: an https URL to the RHEL dist-git. Used for determining
|
| |
+ the presence of the prior release's Z-stream branch.
|
| |
+ * namespace: The dist-git namespace (rpms, containers, modules, etc.).
|
| |
+ Used for determining the presence of the prior release's Z-stream
|
| |
+ branch.
|
| |
+ * repo_name: The name of the repo in the namespace from which we will
|
| |
+ determine status. Used for determining the presence of the prior
|
| |
+ release's Z-stream branch.
|
| |
+ * cs_branch: The CentOS Stream branch for this repo. Used to determine the
|
| |
+ RHEL major release.
|
| |
+ * pp_api_url: The URL to the RHEL Product Pages API. Used for determining
|
| |
+ the current development phase.
|
| |
+
|
| |
+ Returns: a namedtuple containing key information about the RHEL release
|
| |
+ associated with this CentOS Stream branch. It has the following members:
|
| |
+
|
| |
+ * latest_version: The most recent major and minor release of RHEL. This
|
| |
+ is a presentation string and its format is not guaranteed.
|
| |
+ * target_version: The major and minor release of RHEL that is currently
|
| |
+ targeted by this CentOS Stream branch. This is a presentation string
|
| |
+ and its format is not guaranteed.
|
| |
+ * rule_branch: The branch to be used for check-tickets rules (str)
|
| |
+ * rhel_target_default: The default `--rhel-target` (str) or
|
| |
+ None (NoneType). The possible values if not None are "latest" or
|
| |
+ "zstream".
|
| |
+ * enforcing: Whether ticket approvals should be enforced. (bool)
|
| |
+ """
|
| |
+
|
| |
+ x_version, rhel_version = stream_mapping(cs_branch)
|
| |
+
|
| |
+ # Query the "package pages" API for the current active Y-stream release
|
| |
+ request_params = {
|
| |
+ "phase__in": "{},{}".format(
|
| |
+ pp_phase_devtestdoc, pp_phase_stabilization, pp_phase_maintenance
|
| |
+ ),
|
| |
+ "product__shortname": "rhel",
|
| |
+ "relgroup__shortname": rhel_version,
|
| |
+ "format": "json",
|
| |
+ }
|
| |
+
|
| |
+ try:
|
| |
+ pp_response = query_package_pages(
|
| |
+ api_url=pp_api_url, request_params=request_params
|
| |
+ )
|
| |
+ except (ConnectionError, HTTPError) as e:
|
| |
+ raise RHELError("Could not contact Product Pages. Are you on the VPN?")
|
| |
+
|
| |
+ if len(pp_response) < 1:
|
| |
# Received zero potential release matches
|
| |
logger.warning("Didn't match any active releases. Assuming pre-Beta.")
|
| |
|
| |
# Fake up a Beta payload
|
| |
- payload = [
|
| |
+ pp_response = [
|
| |
{
|
| |
- "shortname": "{}.0.beta".format(rhel_version),
|
| |
- "phase": phase_devtestdoc,
|
| |
+ "shortname": "{}.0-beta".format(rhel_version),
|
| |
+ "phase": pp_phase_devtestdoc,
|
| |
}
|
| |
]
|
| |
|
| |
active_y_version = -1
|
| |
beta = False
|
| |
- for entry in payload:
|
| |
+ phase_lookup = dict()
|
| |
+ for entry in pp_response:
|
| |
shortname = entry["shortname"]
|
| |
|
| |
# The shortname is in the form rhel-9-1.0 or rhel-10.0[.beta]
|
| |
# Extract the active Y-stream version
|
| |
x_version, y_version, extra_version = parse_rhel_shortname(shortname)
|
| |
-
|
| |
- if y_version > active_y_version:
|
| |
+ entry_is_beta = bool(extra_version and "beta" in extra_version)
|
| |
+
|
| |
+ # Enable looking up the phase later
|
| |
+ branch_name = format_branch(x_version, y_version, entry_is_beta)
|
| |
+ phase_lookup[branch_name] = entry["phase"]
|
| |
+
|
| |
+ if y_version > active_y_version or (
|
| |
+ y_version == active_y_version and beta and not entry_is_beta
|
| |
+ ):
|
| |
+ # Replace the saved values with a higher Y version if we
|
| |
+ # see one. Also check whether we have the same Y version
|
| |
+ # but without the Beta indicator
|
| |
active_y_version = y_version
|
| |
- beta = bool(extra_version and "beta" in extra_version)
|
| |
+ beta = entry_is_beta
|
| |
+
|
| |
+ if beta:
|
| |
+ latest_version = "{}.{} Beta".format(x_version, active_y_version)
|
| |
+ else:
|
| |
+ latest_version = "{}.{}".format(x_version, active_y_version)
|
| |
+
|
| |
+ logger.debug("Latest version: {}".format(latest_version))
|
| |
+
|
| |
+ # Next we need to find out if we're actually USING the latest version or
|
| |
+ # the previous one, by asking RHEL dist-git if the rhel-X.(Y-1).0 branch
|
| |
+ # exists. (Or rhel-X.Y.0-beta in the special case of Y=0)
|
| |
+
|
| |
+ # If the latest release is the Beta, we can skip checking for a prior
|
| |
+ # release branch, since none can exist and we know it cannot be in
|
| |
+ # the Stabilization Phase yet. Thus, we return the CS branch and
|
| |
+ # --rhel-target=latest
|
| |
+ if beta:
|
| |
+ return rhel_state_nt(
|
| |
+ latest_version=latest_version,
|
| |
+ target_version=latest_version,
|
| |
+ rule_branch=cs_branch,
|
| |
+ phase=pp_phase_devtestdoc,
|
| |
+ rhel_target_default="latest",
|
| |
+ enforcing=False,
|
| |
+ )
|
| |
|
| |
- in_exception_phase = entry["phase"] == 450
|
| |
+ # First, check if this is the special case of Y=0
|
| |
+ # Note: since this is being written during the 10.0 Beta timeframe, there
|
| |
+ # is no need to special case older formats like 9.0.0-beta. We can just
|
| |
+ # use rhel-X.0-beta instead.
|
| |
+ if active_y_version == 0:
|
| |
+ prior_release_branch = format_branch(x_version, active_y_version, is_beta=True)
|
| |
+ else:
|
| |
+ prior_release_branch = format_branch(
|
| |
+ x_version, active_y_version - 1, is_beta=False
|
| |
+ )
|
| |
+
|
| |
+ logger.debug("Prior release branch: {}".format(prior_release_branch))
|
| |
+
|
| |
+ try:
|
| |
+ branch_exists = does_branch_exist(
|
| |
+ rhel_dist_git, namespace, repo_name, prior_release_branch
|
| |
+ )
|
| |
+ except gitpython.GitCommandError as e:
|
| |
+ raise RHELError("Could not read from RHEL dist-git. Are you on the VPN?")
|
| |
+
|
| |
+ if branch_exists:
|
| |
+ # The branch is there, so work on the active Y-stream, which is always
|
| |
+ # in DevTestDoc Phase
|
| |
+ phase = pp_phase_devtestdoc
|
| |
+ check_tickets_branch = cs_branch
|
| |
+ rhel_target_default = "latest"
|
| |
+ enforcing = False
|
| |
+ target_version = latest_version
|
| |
+ else:
|
| |
+ # The branch is not present, so we'll work on the prior Y-stream
|
| |
+ check_tickets_branch = prior_release_branch
|
| |
+
|
| |
+ target_x, target_y, target_extra = parse_rhel_branchname(prior_release_branch)
|
| |
+ target_version = "{}.{}{}".format(
|
| |
+ target_x,
|
| |
+ target_y,
|
| |
+ " Beta" if target_extra and "beta" in target_extra else "",
|
| |
+ )
|
| |
+
|
| |
+ # The prior Y-stream is always in either Stabilization or Maintenance
|
| |
+ # phase, so it always enforces.
|
| |
+ enforcing = True
|
| |
+
|
| |
+ # Determine which phase the prior release is in:
|
| |
+ phase = phase_lookup[prior_release_branch]
|
| |
+
|
| |
+ if phase == pp_phase_stabilization:
|
| |
+ # We're in the Stabilization phase, so we can't automatically determine
|
| |
+ # between the "zstream" and "exception" targets.
|
| |
+ rhel_target_default = None
|
| |
+ else:
|
| |
+ # We must be in Maintenance phase
|
| |
+ rhel_target_default = "zstream"
|
| |
+
|
| |
+ return rhel_state_nt(
|
| |
+ latest_version=latest_version,
|
| |
+ target_version=target_version,
|
| |
+ rule_branch=check_tickets_branch,
|
| |
+ phase=phase,
|
| |
+ rhel_target_default=rhel_target_default,
|
| |
+ enforcing=enforcing,
|
| |
+ )
|
| |
+
|
| |
+
|
| |
+ def format_current_state_message(rhel_state):
|
| |
+ """
|
| |
+ Returns a human-readable string providing actionable information about the
|
| |
+ current state of this repository. Useful for `centpkg current-state` and
|
| |
+ the check-tickets function in merge requests
|
| |
+ """
|
| |
+
|
| |
+ message = (
|
| |
+ f"Current RHEL status:\n"
|
| |
+ f"\tThe latest active Y-stream release is RHEL {rhel_state.latest_version}\n"
|
| |
+ f"\tThis project is targeting RHEL {rhel_state.target_version}\n"
|
| |
+ )
|
| |
+
|
| |
+ if rhel_state.latest_version != rhel_state.target_version:
|
| |
+ zstream_active_msg = (
|
| |
+ f"\t\tThe latest and targeted versions differ.\n"
|
| |
+ f"\t\tIf this is not intentional, please see\n"
|
| |
+ f"\t\thttps://one.redhat.com/rhel-development-guide/#proc_centos-stream-first_assembly_rhel-9-development\n"
|
| |
+ f"\t\tfor details on how to unlock Y-stream development by creating the {rhel_state.rule_branch} branch.\n"
|
| |
+ )
|
| |
+ message = "".join((message, zstream_active_msg))
|
| |
+
|
| |
+ target_phase = pp_phase_name_lookup[rhel_state.phase]
|
| |
+ message = "".join(
|
| |
+ (
|
| |
+ message,
|
| |
+ f"\tThe {rhel_state.target_version} release is currently in {target_phase} phase\n",
|
| |
+ )
|
| |
+ )
|
| |
+
|
| |
+ if rhel_state.phase == pp_phase_stabilization:
|
| |
+ message = "".join(
|
| |
+ (message, f"\t\tThe --rhel-target argument must be used when building.\n")
|
| |
+ )
|
| |
|
| |
- logger.debug(
|
| |
- "Active Y-stream: {}, Enforcing: {}, Beta: {}".format(
|
| |
- active_y_version, in_exception_phase, beta
|
| |
+ message = "".join(
|
| |
+ (
|
| |
+ message,
|
| |
+ f"\tTicket approvals are {'' if rhel_state.enforcing else 'not '}currently required for merge request approval.",
|
| |
)
|
| |
)
|
| |
|
| |
- return x_version, active_y_version, beta, in_exception_phase
|
| |
+ return message
|
| |
These replace determine_active_y_version and does_divergent_branch_exist()
The return value from determine_rhel_state checks for both Product Pages
state and the presence of the prior release branch to return a complete
set of information about how the current cXs branch will behave.
Signed-off-by: Stephen Gallagher sgallagh@redhat.com