import argparse
from datetime import datetime
import logging
import sys
import yaml
from pprint import pformat
from typing import Any, Dict, Tuple
from gitlab.exceptions import GitlabGetError, GitlabAuthenticationError
from gitlab.v4 import objects
from e3_build_tools.git.registry import (
GitLabCommitPayload,
WrapperRegistry,
GitLabInterface,
)
from e3_build_tools.git.tag.remote import generate_remote_tag
from e3_build_tools.logging import set_up_logger, pretty_log
from e3_build_tools.module import ModuleSource
[docs]
logger = logging.getLogger(__name__)
[docs]
MergeRequestPayload = Dict[str, Any]
[docs]
GITLAB_WRAPPER_GROUP = 650
[docs]
SPECIFICATION_GITLAB_ID = 5300
[docs]
E3_TEAM_IDS = [
235, # simonrose
368, # douglasaraujo
432, # lucasmagalhaes
260, # anderslindh1
810, # grzegorzkowalski
]
[docs]
def create_release_commit(
specification_project: objects.Project,
specification_file: str,
source_branch: str,
contents: str,
module: str,
module_ver: str,
) -> GitLabCommitPayload:
"""Create the commit payload."""
changes = [
{
"action": "update",
"file_path": f"specifications/{specification_file}.yml",
"content": contents,
}
]
return {
"branch": source_branch,
"start_branch": specification_project.default_branch,
"commit_message": f"Release {module} {module_ver} to {specification_file}",
"actions": changes,
}
[docs]
def create_specification_payloads(
specification_project: objects.Project,
specification: str,
jira_key: str,
creator_id: int,
module: str,
module_ver: str,
tag: str,
) -> Tuple[GitLabCommitPayload, MergeRequestPayload]:
"""Create the necessary commit and merge request payloads for the release."""
try:
contents = yaml.safe_load(
specification_project.files.raw(
file_path=f"specifications/{specification}.yml",
ref=specification_project.default_branch,
).decode("utf-8")
)
except GitlabGetError as e:
logger.error(f"Failed to fetch specification file {specification}. Aborting.")
logger.debug(f"Error message: {str(e)}")
sys.exit(-1)
# allow inputs both like "e3-foo" and just "foo"
if module.startswith("e3-"):
module = module[3:]
# In case it is a new module
if module not in contents["modules"]:
contents["modules"][module] = {"versions": []}
contents["modules"][module]["versions"].append(tag)
# To avoid colliding branch names
unique_id = datetime.now().strftime("%M%S")
source_branch = f"{jira_key.lower()}-release-{module}-{module_ver}-{unique_id}"
commit_payload = create_release_commit(
specification_project,
specification,
source_branch,
yaml.safe_dump(contents),
module,
module_ver,
)
merge_payload: MergeRequestPayload = {
"source_branch": source_branch,
"target_branch": specification_project.default_branch,
"description": f"Resolves {jira_key.upper()}",
"title": f"{jira_key.upper()}: Release {module} {module_ver}",
"reviewer_ids": [id_ for id_ in E3_TEAM_IDS if id_ != creator_id],
"assignee_id": creator_id,
"remove_source_branch": True,
}
return commit_payload, merge_payload
[docs]
def e3_release(
debug: bool,
private_token: str,
assume_yes: bool,
module: str,
reference: str,
specification: str,
jira_key: str,
):
"""Release an e3 module to the specified environment."""
set_up_logger(debug)
if not jira_key.lower().startswith("ics-"):
logger.error("JIRA key should start with ICS")
sys.exit(-1)
logger.info(f"Releasing {module}...\n")
pretty_log("Environment", specification)
if private_token:
registry = WrapperRegistry(
private_token=private_token, top_group=GITLAB_WRAPPER_GROUP
)
else:
registry = WrapperRegistry(config="ess-gitlab", top_group=GITLAB_WRAPPER_GROUP)
# Although it adds some overhead, we use a separate interface for non-wrapper objects and actions
if private_token:
gl_interface = GitLabInterface(private_token=private_token)
else:
gl_interface = GitLabInterface(config="ess-gitlab")
try:
user_id = gl_interface.get_current_user_id()
except GitlabAuthenticationError:
logger.error("Failed to authenticate user. Aborting.")
sys.exit(-1)
# Default branch is used if no ref is provided
if reference is None:
try:
reference = registry.get_project(module).default_branch
except KeyError:
sys.exit(-1)
pretty_log("Reference", "* using default branch")
else:
pretty_log("Reference", reference)
try:
module_ver = registry.read_version_from(
module,
reference,
config=ModuleSource.version_config_file,
var=ModuleSource.version_config_var,
validate=False,
)
except Exception as e:
logger.error("Failed to fetch module version. Aborting.")
logger.debug(f"Error message: {str(e)}")
sys.exit(-1)
tag = generate_remote_tag(registry, module, reference)
logger.info("")
pretty_log("New module tag", tag)
logger.info("")
specification_project = gl_interface.get_project_by_id(SPECIFICATION_GITLAB_ID)
commit_payload, mr_payload = create_specification_payloads(
specification_project,
specification,
jira_key,
user_id,
module,
module_ver,
tag,
)
logger.debug(f"Commit payload: {pformat(commit_payload)}")
logger.debug(f"Merge request payload: {pformat(mr_payload)}")
perform_gitlab_actions(
registry,
module,
reference,
specification,
specification_project,
assume_yes,
tag,
commit_payload,
mr_payload,
)
[docs]
def main():
"""Run the main function."""
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", action="store_true", help=argparse.SUPPRESS)
parser.add_argument(
"-t",
"--token",
dest="private_token",
help="Token for GitLab access - required if no config with token",
)
parser.add_argument("-k", "--jira-key", help="Jira key for release", required=True)
parser.add_argument("-y", "--assume-yes", action="store_true")
parser.add_argument(
"-e",
"--environment",
dest="specification",
help="Environment to release to",
default="2024q3",
)
parser.add_argument("module")
parser.add_argument("-r", "--reference", help="Git reference for release")
e3_release(**vars(parser.parse_args()))