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