Module mbed_tools_ci_scripts.tag_and_release
Orchestrates release process.
Expand source code
#
# Copyright (C) 2020 Arm Mbed. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Orchestrates release process."""
import sys
import argparse
import datetime
import logging
import subprocess
from pathlib import Path
from typing import Optional, Tuple
from mbed_tools_ci_scripts.license_files import add_licence_header
from mbed_tools_ci_scripts.generate_docs import generate_documentation
from mbed_tools_ci_scripts.generate_news import version_project
from mbed_tools_ci_scripts.utils.configuration import configuration, ConfigurationVariable
from mbed_tools_ci_scripts.utils.definitions import CommitType
from mbed_tools_ci_scripts.utils.filesystem_helpers import cd
from mbed_tools_ci_scripts.utils.git_helpers import ProjectTempClone, GitWrapper
from mbed_tools_ci_scripts.utils.logging import log_exception, set_log_level
from mbed_tools_ci_scripts.report_third_party_ip import (
get_current_spdx_project,
generate_spdx_project_reports,
SpdxProject,
)
ENVVAR_TWINE_USERNAME = "TWINE_USERNAME"
ENVVAR_TWINE_PASSWORD = "TWINE_PASSWORD"
OUTPUT_DIRECTORY = "release-dist"
SPDX_REPORTS_DIRECTORY = "licensing"
logger = logging.getLogger(__name__)
def tag_and_release(mode: CommitType, current_branch: Optional[str] = None) -> None:
"""Tags and releases.
Updates repository with changes and releases package to PyPI for general availability.
Args:
mode: release mode
current_branch: current branch in case the current branch cannot easily
be determined (e.g. on CI)
"""
_check_credentials()
is_new_version, version = version_project(mode)
logger.info(f"Current version: {version}")
if not version:
raise ValueError("Undefined version.")
if mode == CommitType.DEVELOPMENT:
return
# The documentation folder will be emptied when the documentation is updated
_update_documentation()
# Adding the licensing summaries in /docs after folder has been cleared and regenerated.
spdx_project = _update_licensing_summary()
add_licence_header(0)
_update_repository(mode, is_new_version, version, current_branch)
if is_new_version:
_generate_spdx_reports(spdx_project)
_release_to_pypi()
def _get_documentation_config() -> Tuple[Path, str]:
docs_dir = Path(configuration.get_value(ConfigurationVariable.DOCUMENTATION_PRODUCTION_OUTPUT_PATH))
module_to_document = configuration.get_value(ConfigurationVariable.MODULE_TO_DOCUMENT)
return docs_dir, module_to_document
def _update_documentation() -> None:
"""Ensures the documentation is in the correct location for releasing.
Pdoc nests its docs output in a folder with the module's name.
This process removes this unwanted folder.
"""
docs_dir, module_to_document = _get_documentation_config()
generate_documentation(docs_dir, module_to_document)
def _update_licensing_summary() -> SpdxProject:
project = get_current_spdx_project()
project.generate_licensing_summary(
Path(configuration.get_value(ConfigurationVariable.DOCUMENTATION_PRODUCTION_OUTPUT_PATH))
)
return project
def _update_repository(mode: CommitType, is_new_version: bool, version: str, current_branch: Optional[str]) -> None:
"""Update repository with changes that happened."""
with ProjectTempClone(desired_branch_name=current_branch) as git:
git.configure_for_github()
time_str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M")
commit_message = f"🚀 releasing version {version} @ {time_str}" if is_new_version else "📰 Automatic changes ⚙"
if mode == CommitType.RELEASE:
_commit_release_changes(git, version, commit_message)
if is_new_version:
logger.info("Tagging commit")
git.create_tag(version, message=f"release {version}")
git.force_push_tag()
def _generate_spdx_reports(project: SpdxProject) -> None:
report_directory = Path(configuration.get_value(ConfigurationVariable.PROJECT_ROOT)).joinpath(
SPDX_REPORTS_DIRECTORY
)
report_directory.mkdir(exist_ok=True)
generate_spdx_project_reports(project, report_directory)
def _add_version_changes(git: GitWrapper) -> None:
git.add(configuration.get_value(ConfigurationVariable.VERSION_FILE_PATH))
git.add(configuration.get_value(ConfigurationVariable.CHANGELOG_FILE_PATH))
git.add(configuration.get_value(ConfigurationVariable.NEWS_DIR))
def _commit_release_changes(git: GitWrapper, version: str, commit_message: str) -> None:
logger.info(f"Committing release [{version}]...")
git.add(configuration.get_value(ConfigurationVariable.DOCUMENTATION_PRODUCTION_OUTPUT_PATH))
_add_version_changes(git)
_commit_changes(commit_message, git)
def _commit_changes(commit_message: str, git: GitWrapper) -> None:
git.commit(f"{commit_message}\n[skip ci]")
git.push()
git.pull()
def _check_credentials() -> None:
# Checks the GitHub token is defined
configuration.get_value(ConfigurationVariable.GIT_TOKEN)
# Checks that twine username is defined
configuration.get_value(ENVVAR_TWINE_USERNAME)
# Checks that twine password is defined
configuration.get_value(ENVVAR_TWINE_PASSWORD)
def _release_to_pypi() -> None:
logger.info("Releasing to PyPI")
logger.info("Generating a release package")
root = configuration.get_value(ConfigurationVariable.PROJECT_ROOT)
with cd(root):
subprocess.check_call(
[
sys.executable,
"setup.py",
"clean",
"--all",
"sdist",
"-d",
OUTPUT_DIRECTORY,
"--formats=gztar",
"bdist_wheel",
"-d",
OUTPUT_DIRECTORY,
]
)
_upload_to_test_pypi()
_upload_to_pypi()
def _upload_to_pypi() -> None:
logger.info("Uploading to PyPI")
subprocess.check_call([sys.executable, "-m", "twine", "upload", f"{OUTPUT_DIRECTORY}/*"])
logger.info("Success 👍")
def _upload_to_test_pypi() -> None:
if configuration.get_value_or_default(ConfigurationVariable.IGNORE_PYPI_TEST_UPLOAD, False):
logger.warning("Not testing package upload on PyPI test (https://test.pypi.org)")
return
logger.info("Uploading to test PyPI")
subprocess.check_call(
[
sys.executable,
"-m",
"twine",
"upload",
"--repository-url",
"https://test.pypi.org/legacy/",
f"{OUTPUT_DIRECTORY}/*",
]
)
logger.info("Success 👍")
def main() -> None:
"""Commands.
Returns:
success code (0) if successful; failure code otherwise.
"""
parser = argparse.ArgumentParser(description="Releases the project.")
parser.add_argument(
"-t", "--release-type", help="type of release to perform", required=True, type=str, choices=CommitType.choices()
)
parser.add_argument("-b", "--current-branch", help="Name of the current branch", nargs="?")
parser.add_argument("-v", "--verbose", action="count", default=0, help="Verbosity, by default errors are reported.")
args = parser.parse_args()
set_log_level(args.verbose)
try:
tag_and_release(CommitType.parse(args.release_type), args.current_branch)
except Exception as e:
log_exception(logger, e)
sys.exit(1)
if __name__ == "__main__":
main()
Functions
def main() ‑> NoneType
-
Commands.
Returns
success code (0) if successful; failure code otherwise.
Expand source code
def main() -> None: """Commands. Returns: success code (0) if successful; failure code otherwise. """ parser = argparse.ArgumentParser(description="Releases the project.") parser.add_argument( "-t", "--release-type", help="type of release to perform", required=True, type=str, choices=CommitType.choices() ) parser.add_argument("-b", "--current-branch", help="Name of the current branch", nargs="?") parser.add_argument("-v", "--verbose", action="count", default=0, help="Verbosity, by default errors are reported.") args = parser.parse_args() set_log_level(args.verbose) try: tag_and_release(CommitType.parse(args.release_type), args.current_branch) except Exception as e: log_exception(logger, e) sys.exit(1)
def tag_and_release(mode: CommitType, current_branch: Union[str, NoneType] = None) ‑> NoneType
-
Tags and releases.
Updates repository with changes and releases package to PyPI for general availability.
Args
mode
- release mode
current_branch
- current branch in case the current branch cannot easily
be determined (e.g. on CI)
Expand source code
def tag_and_release(mode: CommitType, current_branch: Optional[str] = None) -> None: """Tags and releases. Updates repository with changes and releases package to PyPI for general availability. Args: mode: release mode current_branch: current branch in case the current branch cannot easily be determined (e.g. on CI) """ _check_credentials() is_new_version, version = version_project(mode) logger.info(f"Current version: {version}") if not version: raise ValueError("Undefined version.") if mode == CommitType.DEVELOPMENT: return # The documentation folder will be emptied when the documentation is updated _update_documentation() # Adding the licensing summaries in /docs after folder has been cleared and regenerated. spdx_project = _update_licensing_summary() add_licence_header(0) _update_repository(mode, is_new_version, version, current_branch) if is_new_version: _generate_spdx_reports(spdx_project) _release_to_pypi()