From fcb6ebde4b18ef8d3d595c7fc35efcce4d0e65b0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 12 Sep 2025 08:48:05 +1000 Subject: [PATCH] Release version checker (#10304) * Enhance version check regex * Refactor version_check.py - Account for non-standard release tags (rc / dev / etc) - Refactor code for extracting version info - Add argparse support - Update qc_checks.yaml * Enhanced debug output * Stringify and strip * Display version tuple in log * Tweak CI logs --- .github/scripts/version_check.py | 210 +++++++++++++++++++++++-------- .github/workflows/qc_checks.yaml | 8 +- 2 files changed, 159 insertions(+), 59 deletions(-) diff --git a/.github/scripts/version_check.py b/.github/scripts/version_check.py index 4d596395a3..497194a049 100644 --- a/.github/scripts/version_check.py +++ b/.github/scripts/version_check.py @@ -10,6 +10,7 @@ tagged branch: """ +import argparse import itertools import json import os @@ -23,7 +24,93 @@ REPO = os.getenv('GITHUB_REPOSITORY', 'inventree/inventree') GITHUB_API_URL = os.getenv('GITHUB_API_URL', 'https://api.github.com') -def get_existing_release_tags(include_prerelease=True): +def get_src_dir() -> Path: + """Return the path to the InvenTree source directory.""" + here = Path(__file__).parent.absolute() + src_dir = here.joinpath('..', '..', 'src', 'backend', 'InvenTree', 'InvenTree') + + if not src_dir.exists(): + raise FileNotFoundError( + f"Could not find InvenTree source directory: '{src_dir}'" + ) + + return src_dir + + +def get_inventree_version() -> str: + """Return the InvenTree version string.""" + src_dir = get_src_dir() + version_file = src_dir.joinpath('version.py') + + if not version_file.exists(): + raise FileNotFoundError( + f"Could not find InvenTree version file: '{version_file}'" + ) + + with open(version_file, encoding='utf-8') as f: + text = f.read() + + # Extract the InvenTree software version + results = re.findall(r"""INVENTREE_SW_VERSION = '(.*)'""", text) + + if len(results) != 1: + raise ValueError(f'Could not find INVENTREE_SW_VERSION in {version_file}') + + return results[0] + + +def get_api_version() -> str: + """Return the InvenTree API version string.""" + src_dir = get_src_dir() + api_version_file = src_dir.joinpath('api_version.py') + + if not api_version_file.exists(): + raise FileNotFoundError( + f"Could not find InvenTree API version file: '{api_version_file}'" + ) + + with open(api_version_file, encoding='utf-8') as f: + text = f.read() + + # Extract the InvenTree software version + results = re.findall(r"""INVENTREE_API_VERSION = (.*)""", text) + + if len(results) != 1: + raise ValueError( + f'Could not find INVENTREE_API_VERSION in {api_version_file}' + ) + + return results[0].strip().strip('"').strip("'") + + +def version_number_to_tuple(version_string: str) -> tuple[int, int, int, str]: + """Validate a version number string, and convert to a tuple of integers. + + e.g. 1.1.0 + e.g. 1.1.0 dev + e.g. 1.2.3-rc2 + """ + pattern = r'^(\d+)\.(\d+)\.(\d+)[\s-]?(.*)?$' + + match = re.match(pattern, version_string) + + if not match or len(match.groups()) < 3: + raise ValueError( + f"Version string '{version_string}' did not match required pattern" + ) + + result = tuple(int(x) for x in match.groups()[:3]) + + # Add optional prerelease tag + if len(match.groups()) > 3: + result += (match.groups()[3] or '',) + else: + result += ('',) + + return result + + +def get_existing_release_tags(include_prerelease: bool = True): """Request information on existing releases via the GitHub API.""" # Check for github token token = os.getenv('GITHUB_TOKEN', None) @@ -46,16 +133,16 @@ def get_existing_release_tags(include_prerelease=True): for release in data: tag = release['tag_name'].strip() - match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag) - if len(match.groups()) != 3: - print(f"Version '{tag}' did not match expected pattern") - continue + version_tuple = version_number_to_tuple(tag) - if not include_prerelease and release['prerelease']: - continue + if len(version_tuple) >= 4 and version_tuple[3]: + # Skip prerelease tags + if not include_prerelease: + print('-- skipping prerelease tag:', tag) + continue - tags.append([int(x) for x in match.groups()]) + tags.append(tag) return tags @@ -67,15 +154,7 @@ def check_version_number(version_string, allow_duplicate=False): """ print(f"Checking version '{version_string}'") - # Check that the version string matches the required format - match = re.match(r'^(\d+)\.(\d+)\.(\d+)(?: dev)?$', version_string) - - if not match or len(match.groups()) != 3: - raise ValueError( - f"Version string '{version_string}' did not match required pattern" - ) - - version_tuple = [int(x) for x in match.groups()] + version_tuple = version_number_to_tuple(version_string) # Look through the existing releases existing = get_existing_release_tags(include_prerelease=False) @@ -83,35 +162,67 @@ def check_version_number(version_string, allow_duplicate=False): # Assume that this is the highest release, unless told otherwise highest_release = True + # A non-standard tag cannot be the 'highest' release + if len(version_tuple) >= 4 and version_tuple[3]: + highest_release = False + print(f"-- Version tag '{version_string}' cannot be the highest release") + for release in existing: - if release == version_tuple and not allow_duplicate: + if version_string == release and not allow_duplicate: raise ValueError(f"Duplicate release '{version_string}' exists!") - if release > version_tuple: + release_tuple = version_number_to_tuple(release) + + if release_tuple > version_tuple: highest_release = False print(f'Found newer release: {release!s}') + if highest_release: + print(f"-- Version '{version_string}' is the highest release") + return highest_release if __name__ == '__main__': + parser = argparse.ArgumentParser(description='InvenTree Version Check') + parser.add_argument( + '--show-version', + action='store_true', + help='Print the InvenTree version and exit', + ) + parser.add_argument( + '--show-api-version', + action='store_true', + help='Print the InvenTree API version and exit', + ) + parser.add_argument( + '--decrement-api', + type=str, + default='false', + help='Decrement the API version by 1 and print', + ) + + args = parser.parse_args() + + inventree_version = get_inventree_version() + inventree_api_version = int(get_api_version()) + + if args.show_version: + print(inventree_version) + sys.exit(0) + + if args.show_api_version: + if str(args.decrement_api).strip().lower() == 'true': + inventree_api_version -= 1 + print(inventree_api_version) + sys.exit(0) + # Ensure that we are running in GH Actions if os.environ.get('GITHUB_ACTIONS', '') != 'true': print('This script is intended to be run within a GitHub Action!') sys.exit(1) - if 'only_version' in sys.argv: - here = Path(__file__).parent.absolute() - version_file = here.joinpath( - '..', '..', 'src', 'backend', 'InvenTree', 'InvenTree', 'api_version.py' - ) - text = version_file.read_text() - results = re.findall(r"""INVENTREE_API_VERSION = (.*)""", text) - # If 2. args is true lower the version number by 1 - if len(sys.argv) > 2 and sys.argv[2] == 'true': - results[0] = str(int(results[0]) - 1) - print(results[0]) - exit(0) + print('Running InvenTree version check...') # GITHUB_REF_TYPE may be either 'branch' or 'tag' GITHUB_REF_TYPE = os.environ['GITHUB_REF_TYPE'] @@ -127,26 +238,10 @@ if __name__ == '__main__': print(f'GITHUB_REF_TYPE: {GITHUB_REF_TYPE}') print(f'GITHUB_BASE_REF: {GITHUB_BASE_REF}') - here = Path(__file__).parent.absolute() - version_file = here.joinpath( - '..', '..', 'src', 'backend', 'InvenTree', 'InvenTree', 'version.py' + print( + f"InvenTree Version: '{inventree_version}' - {version_number_to_tuple(inventree_version)}" ) - - version = None - - with open(version_file, encoding='utf-8') as f: - text = f.read() - - # Extract the InvenTree software version - results = re.findall(r"""INVENTREE_SW_VERSION = '(.*)'""", text) - - if len(results) != 1: - print(f'Could not find INVENTREE_SW_VERSION in {version_file}') - sys.exit(1) - - version = results[0] - - print(f"InvenTree Version: '{version}'") + print(f"InvenTree API Version: '{inventree_api_version}'") # Check version number and look for existing versions # If a release is found which matches the current tag, throw an error @@ -161,7 +256,9 @@ if __name__ == '__main__': if GITHUB_BASE_REF == 'stable': allow_duplicate = True - highest_release = check_version_number(version, allow_duplicate=allow_duplicate) + highest_release = check_version_number( + inventree_version, allow_duplicate=allow_duplicate + ) # Determine which docker tag we are going to use docker_tags = None @@ -171,8 +268,10 @@ if __name__ == '__main__': version_tag = GITHUB_REF.split('/')[-1] print(f"Checking requirements for tagged release - '{version_tag}':") - if version_tag != version: - print(f"Version number '{version}' does not match tag '{version_tag}'") + if version_tag != inventree_version: + print( + f"Version number '{inventree_version}' does not match tag '{version_tag}'" + ) sys.exit docker_tags = [version_tag, 'stable'] if highest_release else [version_tag] @@ -180,10 +279,11 @@ if __name__ == '__main__': elif GITHUB_REF_TYPE == 'branch': # Otherwise we know we are targeting the 'master' branch docker_tags = ['latest'] + highest_release = False else: print('Unsupported branch / version combination:') - print(f'InvenTree Version: {version}') + print(f'InvenTree Version: {inventree_version}') print('GITHUB_REF_TYPE:', GITHUB_REF_TYPE) print('GITHUB_BASE_REF:', GITHUB_BASE_REF) print('GITHUB_REF:', GITHUB_REF) @@ -193,7 +293,7 @@ if __name__ == '__main__': print('Docker tags could not be determined') sys.exit(1) - print(f"Version check passed for '{version}'!") + print(f"Version check passed for '{inventree_version}'!") print(f"Docker tags: '{docker_tags}'") target_repos = [REPO.lower(), f'ghcr.io/{REPO.lower()}'] diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 2392f6e182..2039621cf4 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -164,8 +164,8 @@ jobs: API: ${{ needs.paths-filter.outputs.api }} run: | pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1 - version="$(python3 .github/scripts/version_check.py only_version ${API} 2>&1)" - echo "Version: $version" + version="$(python3 .github/scripts/version_check.py --show-api-version --decrement-api=${API} 2>&1)" + echo "API Version: $version" url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml" echo "URL: $url" code=$(curl -s -o api.yaml $url --write-out '%{http_code}' --silent) @@ -198,8 +198,8 @@ jobs: if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true' run: | pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1 - version="$(python3 .github/scripts/version_check.py only_version 2>&1)" - echo "Version: $version" + version="$(python3 .github/scripts/version_check.py --show-api-version 2>&1)" + echo "API Version: $version" echo "version=$version" >> "$GITHUB_OUTPUT" - name: Extract settings / tags run: invoke int.export-definitions --basedir docs