diff --git a/qgispluginci/cli.py b/qgispluginci/cli.py index 7d9f9de0..c115239f 100755 --- a/qgispluginci/cli.py +++ b/qgispluginci/cli.py @@ -88,6 +88,11 @@ def cli(): release_parser.add_argument( "release_version", help="The version to be released (x.y.z)." ) + release_parser.add_argument( + "--no-validation", + action="store_true", + help="Turn off validation of `release version`", + ) release_parser.add_argument( "--release-tag", help="The release tag, if different from the version (e.g. vx.y.z).", @@ -152,6 +157,7 @@ def cli(): push_tr_parser.add_argument("transifex_token", help="The Transifex API token") args = parser.parse_args() + Parameters.validate_args(args) # set log level depending on verbosity argument args.verbosity = 40 - (10 * args.verbosity) if args.verbosity > 0 else 0 diff --git a/qgispluginci/parameters.py b/qgispluginci/parameters.py index ba4ac862..e9e98455 100644 --- a/qgispluginci/parameters.py +++ b/qgispluginci/parameters.py @@ -8,14 +8,17 @@ # ########## Libraries ############# # ################################## -# standard library import configparser import datetime import logging import os +import re import sys + +# standard library +from argparse import Namespace from pathlib import Path -from typing import Any, Callable, Dict, Iterator, Literal, Optional, Tuple +from typing import Any, Callable, Dict, Iterator, Optional, Tuple import toml import yaml @@ -228,6 +231,56 @@ def __init__(self, definition: Dict[str, Any]): ) self.repository_url = get_metadata("repository") + @staticmethod + def get_release_version_patterns() -> Dict[str, re.Pattern]: + return { + "simple": r"\d+\.\d+$", + "double": r"\d+\.\d+\.\d+$", + "v2": r"^v\d+\.\d+$", + "v3": r"^v\d+\.\d+\.\d+$", + # See https://github.com/semver/semver/blob/master/semver.md#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + "semver": r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", + } + + @staticmethod + def validate_args(args: Namespace): + """ + Raise an exception just in case: + - the user didn't opt-out of validation using the `--no-validation` flag; and + - the value of `release_version` matches no supported pattern. + In any case, warn the user if the value of `release_version` doesn't match the semver pattern. + """ + if not args.release_version: + return + + patterns = Parameters.get_release_version_patterns() + semver_compliance = re.match(patterns.pop("semver"), args.release_version) + + if not semver_compliance: + logging.warning( + f"Be aware that '{args.release_version}' is not a semver-compliant version." + ) + + if args.no_validation: + logging.warning("Disabled release version validation.") + return + + if semver_compliance or any( + re.match(other_pattern, args.release_version) + for other_pattern in patterns.values() + ): + return + + raise ValueError( + f""" + Unable to validate the release version '{args.release_version}'. + Please use a release version identifier such as '1.0.1' (recommended, semantic versioning), 'v1.1.1', 'v1.1', or '1.1'. + Otherwise you can disable validation by running this command again with an extra '--no-validation' flag. + Semantic versioning (semvar) identifiers are recommended. + Take a look at https://en.wikipedia.org/wiki/Software_versioning#Semantic_versioning for a refresher." + """ + ) + @staticmethod def archive_name( plugin_name, release_version: str, experimental: bool = False diff --git a/test/test_release.py b/test/test_release.py index ab4783b4..805ba8d8 100644 --- a/test/test_release.py +++ b/test/test_release.py @@ -1,11 +1,13 @@ #! /usr/bin/env python # standard +import argparse import filecmp import os import re import unittest import urllib.request +from itertools import product from pathlib import Path from tempfile import mkstemp from zipfile import ZipFile @@ -205,6 +207,45 @@ def test_release_changelog(self): # Commit sha1 not in the metadata.txt self.assertEqual(0, len(re.findall(r"commitSha1=\d+", str(data)))) + def test_release_version_valid_invalid(self): + valid_tags = ["v1.1.1", "v1.1", "1.0.1", "1.1", "1.0.0-alpha", "1.0.0-dev"] + invalid_tags = ["1", "v1", ".", ".1"] + expected_valid_results = { + "v1.1.1": ["v3"], + "v1.1": ["v2"], + "1.0.1": ["double", "semver"], + "1.1": ["simple"], + "1.0.0-alpha": ["semver"], + "1.0.0-dev": ["semver"], + } + valid_results = {tag: [] for tag in valid_tags} + patterns = Parameters.get_release_version_patterns() + for key, cand in product(patterns, valid_results): + if re.match(patterns[key], cand): + valid_results[cand].append(key) + self.assertEqual(valid_results, expected_valid_results) + + invalid_results = {tag: [] for tag in invalid_tags} + for key, cand in product(patterns, invalid_results): + if re.match(patterns[key], cand): + invalid_results[cand].append(key) + self.assertFalse(any(invalid_results.values())) + + def test_release_version_validation_on(self): + parser = argparse.ArgumentParser() + parser.add_argument("release_version") + parser.add_argument("--no-validation", action="store_true") + args = parser.parse_args(["v1"]) + with self.assertRaises(ValueError): + Parameters.validate_args(args) + + def test_release_version_validation_off(self): + parser = argparse.ArgumentParser() + parser.add_argument("release_version") + parser.add_argument("--no-validation", action="store_true") + args = parser.parse_args([".", "--no-validation"]) + Parameters.validate_args(args) + if __name__ == "__main__": unittest.main()