From e1102ae7377ac3da3a7f37bc892c9918ed30e408 Mon Sep 17 00:00:00 2001 From: Junya Sasaki Date: Tue, 1 Oct 2024 18:04:08 +0900 Subject: [PATCH] fix(create-prs-to-update-vcs-repositories): update only for versions whose format is `X.Y.Z` (#320) * fix(create-prs-to-update-vcs-repositories): fix as follows (see below) * Only handles formats such as: "1.0.0", "1.1.1", "0.0.9", ... etc * Following ones are the mismatched examples: ``` "v0.0.1", # mismatch "ros2-v0.0.4", # mismatch "xxx-1.0.0-yyy", # mismatch "v1.2.3-beta", # mismatch "v1.0", # mismatch "v2", # mismatch "1.0.0-alpha+001", # mismatch "v1.0.0-rc1+build.1", # mismatch "2.0.0+build.1848", # mismatch "2.0.1-alpha.1227", # mismatch "1.0.0-alpha.beta", # mismatch "ros_humble-v0.10.2" # mismatch ``` Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): fix a wrong match method Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): fix README.md * Make clear what is the condition for creating a PR Signed-off-by: Junya Sasaki * style(pre-commit): autofix * fix(create-prs-to-update-vcs-repositories): specify language for fenced code block Signed-off-by: Junya Sasaki * style(pre-commit): autofix * fix(create-prs-to-update-vcs-repositories): fix wrongly saved README.md Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): fix wrong plaintext annotation Signed-off-by: Junya Sasaki * style(pre-commit): autofix Signed-off-by: Junya Sasaki * Update create-prs-to-update-vcs-repositories/README.md Co-authored-by: Ryohsuke Mitsudome <43976834+mitsudome-r@users.noreply.github.com> Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): fixes in semver (see below): * Do not allow to change the semver pattern via argument: use only fixed pattern. * Add description to make it explicit what kind of patterns are supported. Signed-off-by: Junya Sasaki * feat(create-prs-to-update-vcs-repositories): support release types (see below): * Support separated PRs for major, minor, and patch updates * If not specified, PR will be created for any updates Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): ignore a word "devrelease" from spell check Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): apply missing updates Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): bug fix (see below): * The existing branch list is not updated correctly. When major, minor, patch, and any are all enabled, The same name branch can be processed twice. This eventually causes an error like: ``` nothing to commit ``` Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): cosmetic fix * The information is duplicated that of README.md Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): fix code duplication Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): cosmetic fix * Keep consistent indentation Signed-off-by: Junya Sasaki * fix(create-prs-to-update-vcs-repositories): fix a yamllint bug Signed-off-by: Junya Sasaki --------- Signed-off-by: Junya Sasaki Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ryohsuke Mitsudome <43976834+mitsudome-r@users.noreply.github.com> --- .../README.md | 47 +++ .../action.yaml | 5 + .../create_prs_to_update_vcs_repositories.py | 272 ++++++++++-------- 3 files changed, 209 insertions(+), 115 deletions(-) diff --git a/create-prs-to-update-vcs-repositories/README.md b/create-prs-to-update-vcs-repositories/README.md index 3a4d4e3b..90a2b86f 100644 --- a/create-prs-to-update-vcs-repositories/README.md +++ b/create-prs-to-update-vcs-repositories/README.md @@ -37,6 +37,7 @@ jobs: token: ${{ steps.generate-token.outputs.token }} repo_name: autowarefoundation/autoware parent_dir: . + targets: major minor base_branch: main new_branch_prefix: feat/update- autoware_repos_file_name: autoware.repos @@ -49,6 +50,7 @@ jobs: | ------------------------ | -------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | token | true | | The token for pull requests. | | repo_name | true | | The name of the repository to create pull requests. | +| targets | false | any | The target release types (choices: any, patch, minor, major). | | parent_dir | false | . | The parent directory of the repository. | | base_branch | false | main | The base branch to create pull requests. | | new_branch_prefix | false | feat/update- | The prefix of the new branch name. The branch name will be `{new_branch_prefix}-{user_name}/{repository_name}/{new_version}`. | @@ -58,3 +60,48 @@ jobs: ## Outputs None. + +## What kind of tags are handled? + +- Monitors all vcs-imported repositories in the `autoware.repos` (if default) which have a version with regular expression pattern `r'\b(? 0.0.2 # If `--target patch` or `--target any` is specified + 1.1.1 => 1.2.1 # If `--target minor` or `--target any` is specified + 2.4.3 => 3.0.0 # If `--target major` or `--target any` is specified +``` + +- Invalid ones (PR is not created): + +```plaintext + main => 0.0.1 + v0.0.1 => 0.0.2 + xxx-0.0.1 => 0.0.9 + 0.0.1-rc1 => 0.0.2 +``` diff --git a/create-prs-to-update-vcs-repositories/action.yaml b/create-prs-to-update-vcs-repositories/action.yaml index d972b5f7..540c9f55 100644 --- a/create-prs-to-update-vcs-repositories/action.yaml +++ b/create-prs-to-update-vcs-repositories/action.yaml @@ -12,6 +12,10 @@ inputs: description: Parent directory required: false default: ./ + targets: + description: Target release types + required: false + default: any base_branch: description: Base branch required: false @@ -63,6 +67,7 @@ runs: python ${GITHUB_ACTION_PATH}/create_prs_to_update_vcs_repositories.py \ --repo_name ${{ inputs.repo_name }} \ --parent_dir ${{ inputs.parent_dir }} \ + --targets ${{ inputs.targets }} \ --base_branch ${{ inputs.base_branch }} \ --new_branch_prefix ${{ inputs.new_branch_prefix }} \ --autoware_repos_file_name ${{ inputs.autoware_repos_file_name }} \ diff --git a/create-prs-to-update-vcs-repositories/create_prs_to_update_vcs_repositories.py b/create-prs-to-update-vcs-repositories/create_prs_to_update_vcs_repositories.py index bf519e20..87f5138a 100644 --- a/create-prs-to-update-vcs-repositories/create_prs_to_update_vcs_repositories.py +++ b/create-prs-to-update-vcs-repositories/create_prs_to_update_vcs_repositories.py @@ -11,6 +11,13 @@ from github import Github # cspell: ignore Github +# Define the semantic version pattern here +SUPPORTED_SEMANTIC_VERSION_PATTERN = r'\b(? dict[str, repository_url_version_dict = self._parse_repos() repositories_url_semantic_version_dict: dict[str, Optional[str]] = { - url: (match.group(1) if (match := re.search(semantic_version_pattern, version)) else None) + url: (version if re.fullmatch(semantic_version_pattern, version) else None) for url, version in repository_url_version_dict.items() } return repositories_url_semantic_version_dict @@ -131,29 +138,13 @@ def parse_args() -> argparse.Namespace: args_repo.add_argument("--base_branch", type=str, default="main", help="The base branch of autoware.repos") args_repo.add_argument("--new_branch_prefix", type=str, default="feat/update-", help="The prefix of the new branch name") - ''' - Following default pattern = r'\b(v?\d+\.\d+(?:\.\d+)?(?:-\w+)?(?:\+\w+(\.\d+)?)?)\b' - can parse the following example formats: - "0.0.1", - "v0.0.1", - "ros2-v0.0.4", - "xxx-1.0.0-yyy", - "2.3.4", - "v1.2.3-beta", - "v1.0", - "v2", - "1.0.0-alpha+001", - "v1.0.0-rc1+build.1", - "2.0.0+build.1848", - "2.0.1-alpha.1227", - "1.0.0-alpha.beta", - "ros_humble-v0.10.2" - ''' + # Define an argument to specify which version components to check args_repo.add_argument( - "--semantic_version_pattern", - type=str, - default=r'\b(v?\d+\.\d+(?:\.\d+)?(?:-\w+)?(?:\+\w+(\.\d+)?)?)\b', - help="The pattern of semantic version" + '--targets', + choices=VALID_RELEASES, # Restrict choices + nargs='+', # Allow multiple values + default=['any'], # Default is 'any': in this case, consider any version newer than the current + help='Specify the version component targets to check for updates (e.g., --targets major minor)' ) # For the Autoware @@ -176,29 +167,65 @@ def get_logger(verbose: int) -> logging.Logger: return logging.getLogger(__name__) -def get_latest_tag(tags: list[str], current_version: str) -> Optional[str]: +def get_latest_tag(tags: list[str], current_version: str, target_release: str) -> Optional[str]: ''' Description: - Get the latest tag from the list of tags + Get the latest tag from the list of tags based on the specified target release type. Args: - tags (list[str]): a list of tags - current_version (str): the current version of the repository + tags (list[str]): A list of tags. + current_version (str): The current version of the repository. + target_release (str): The type of release to check for updates. Can be 'major', 'minor', 'patch', or 'any'. + + Returns: + Optional[str]: The latest tag that matches the target release type, or None if not found. + + Raises: + ValueError: If an invalid target_release is specified. ''' + + if target_release not in VALID_RELEASES: + raise ValueError(f"Invalid target_release '{target_release}'. Valid options are: {VALID_RELEASES}") + + current_ver = version.parse(current_version) latest_tag = None + for tag in tags: - # Exclude parse failed ones such as 'tier4/universe', 'main', ... etc try: - version.parse(tag) + parsed_tag = version.parse(tag) except (version.InvalidVersion, TypeError): continue - # OK, it's a valid version - if latest_tag is None: - latest_tag = tag - else: - if version.parse(tag) > version.parse(latest_tag): - latest_tag = tag + # Skip pre-releases or development versions if not needed + if parsed_tag.is_prerelease or parsed_tag.is_devrelease: # cspell: ignore devrelease + continue + + # Determine if the tag matches the required update type + if target_release == 'major': + if parsed_tag.major > current_ver.major: + # Only consider tags with a higher major version + if latest_tag is None or parsed_tag < version.parse(latest_tag): + latest_tag = tag + + elif target_release == 'minor': + if parsed_tag.major == current_ver.major and parsed_tag.minor > current_ver.minor: + # Only consider tags with the same major but higher minor version + if latest_tag is None or parsed_tag < version.parse(latest_tag): + latest_tag = tag + + elif target_release == 'patch': + if (parsed_tag.major == current_ver.major and + parsed_tag.minor == current_ver.minor and + parsed_tag.micro > current_ver.micro): + # Only consider tags with the same major and minor but higher patch version + if latest_tag is None or parsed_tag < version.parse(latest_tag): + latest_tag = tag + + elif target_release == 'any': + # Consider any version newer than the current version + if parsed_tag > current_ver: + if latest_tag is None or parsed_tag < version.parse(latest_tag): + latest_tag = tag return latest_tag @@ -230,107 +257,122 @@ def main(args: argparse.Namespace) -> None: autoware_repos: AutowareRepos = AutowareRepos(autoware_repos_file_name = args.autoware_repos_file_name) # Get the repositories with semantic version tags - repositories_url_semantic_version_dict: dict[str, str] = autoware_repos.pickup_semver_repositories(semantic_version_pattern = args.semantic_version_pattern) + # e.g. { + # 'https://github.com/user/repo.git': '0.0.1', # Pattern matched + # 'https://github.com/user/repo2.git': None, # Pattern not matched + # } + repositories_url_semantic_version_dict: dict[str, str] = autoware_repos.pickup_semver_repositories(semantic_version_pattern = SUPPORTED_SEMANTIC_VERSION_PATTERN) # Get reference to the repository repo = git.Repo(args.parent_dir) - # Get all the branches - branches = [r.remote_head for r in repo.remote().refs] - - for url, current_version in repositories_url_semantic_version_dict.items(): - ''' - Description: - In this loop, the script will create a PR to update the version of the repository specified by the URL. - The step is as follows: - 1. Get tags of the repository - 2. Check if the current version is the latest - 3. Get the latest tag - 4. Create a new branch - 5. Update autoware.repos - 6. Commit and push - 7. Create a PR - ''' - - # get tags of the repository - tags: list[str] = github_interface.get_tags_by_url(url) - - latest_tag: Optional[str] = get_latest_tag(tags, current_version) - - # Skip if the expected format is not found - if latest_tag is None: - logger.debug(f"The latest tag with expected format is not found in the repository {url}. Skip for this repository.") - continue + # Get all the existing branches + existing_branches = [r.remote_head for r in repo.remote().refs] + + # Check for each target release type (e.g., major, minor, patch, any) + for target in args.targets: + # Check for each repository + for url, current_version in repositories_url_semantic_version_dict.items(): + ''' + Description: + In this loop, the script will create a PR to update the version of the repository specified by the URL. + The step is as follows: + 1. Get tags of the repository + 2. Check if the current version is the latest + 3. Get the latest tag + 4. Create a new branch + 5. Update autoware.repos + 6. Commit and push + 7. Create a PR + ''' + + # Skip if the current version has an invalid format + if current_version is None: + logger.debug(f"The current version ({current_version}) format has a mismatched pattern. Skip for this repository:\n {url}") + continue - # Exclude parse failed ones such as 'tier4/universe', 'main', ... etc - try: - # If current version is a valid version, compare with the current version - logger.debug(f"url: {url}, latest_tag: {latest_tag}, current_version: {current_version}") - if version.parse(latest_tag) > version.parse(current_version): - # OK, the latest tag is newer than the current version - pass - else: - # The current version is the latest - logger.debug(f"Repository {url} has the latest version {current_version}. Skip for this repository.") + # get tags of the repository + tags: list[str] = github_interface.get_tags_by_url(url) + + latest_tag: Optional[str] = get_latest_tag(tags, current_version, target_release=target) + + # Skip if the expected format is not found + if latest_tag is None: + logger.debug(f"The latest tag ({latest_tag}) format has a mismatched pattern. Skip for this repository:\n {url}") continue - except (version.InvalidVersion, TypeError): - # If the current version is not a valid version and the latest tag is a valid version, let's update - pass - # Get repository name - repo_name: str = github_interface.url_to_repository_name(url) + # Exclude parse failed ones such as 'tier4/universe', 'main', ... etc + try: + # If current version is a valid version, compare with the current version + logger.debug(f"url: {url}, latest_tag: {latest_tag}, current_version: {current_version}") + if version.parse(latest_tag) > version.parse(current_version): + # OK, the latest tag is newer than the current version + pass + else: + # The current version is the latest + logger.debug(f"Repository {url} has the latest version {current_version}. Skip for this repository.") + continue + except (version.InvalidVersion, TypeError): + # If the current version is not a valid version, skip this repository + continue - # Set branch name - branch_name: str = f"{args.new_branch_prefix}{repo_name}/{latest_tag}" + # Get repository name + repo_name: str = github_interface.url_to_repository_name(url) - # Check if the remote branch already exists - if branch_name in branches: - logger.info(f"Branch '{branch_name}' already exists on the remote.") - continue + # Set branch name + branch_name: str = f"{args.new_branch_prefix}{repo_name}/{latest_tag}" + + # Skip if the remote branch already exists + if branch_name in existing_branches: + logger.info(f"Branch '{branch_name}' already exists on the remote.") + continue - # First, create a branch - create_one_branch(repo, branch_name, logger) + # Add this branch to the existing branches + existing_branches.append(branch_name) - # Switch to the branch - repo.heads[branch_name].checkout() + # First, create a branch + create_one_branch(repo, branch_name, logger) - # Change version in autoware.repos - autoware_repos.update_repository_version(url, latest_tag) + # Switch to the branch + repo.heads[branch_name].checkout() - # Add - repo.index.add([args.autoware_repos_file_name]) + # Change version in autoware.repos + autoware_repos.update_repository_version(url, latest_tag) - # Commit - commit_message = f"feat(autoware.repos): update {repo_name} to {latest_tag}" - repo.git.commit(m=commit_message, s=True) + # Add + repo.index.add([args.autoware_repos_file_name]) - # Push - origin = repo.remote(name='origin') - origin.push(branch_name) + # Commit + title = f"feat({args.autoware_repos_file_name}): {"version" if target == 'any' else target} update {repo_name} to {latest_tag}" + repo.git.commit(m=title, s=True) - # Switch back to base branch - repo.heads[args.base_branch].checkout() + # Push + origin = repo.remote(name='origin') + origin.push(branch_name) - # Create a PR - github_interface.create_pull_request( - repo_name = args.repo_name, - title = f"feat(autoware.repos): update {repo_name} to {latest_tag}", - body = f"This PR updates the version of the repository {repo_name} in autoware.repos", - head = branch_name, - base = args.base_branch - ) + # Switch back to base branch + repo.heads[args.base_branch].checkout() + + # Create a PR + github_interface.create_pull_request( + repo_name = args.repo_name, + title = title, + body = f"This PR updates the version of the repository {repo_name} in autoware.repos", + head = branch_name, + base = args.base_branch + ) - # Switch back to base branch - repo.heads[args.base_branch].checkout() + # Switch back to base branch + repo.heads[args.base_branch].checkout() - # Reset any changes - repo.git.reset('--hard', f'origin/{args.base_branch}') + # Reset any changes + repo.git.reset('--hard', f'origin/{args.base_branch}') - # Clean untracked files - repo.git.clean('-fd') + # Clean untracked files + repo.git.clean('-fd') - # Restore base's autoware.repos - autoware_repos: AutowareRepos = AutowareRepos(autoware_repos_file_name = args.autoware_repos_file_name) + # Restore base's autoware.repos + autoware_repos: AutowareRepos = AutowareRepos(autoware_repos_file_name = args.autoware_repos_file_name) # Loop end