From 0e4a19f9df9aa69ea5298aff0bf627eb74c3bedb Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 18 Dec 2024 20:47:48 +0100 Subject: [PATCH] Release script for downstream OTP forks. --- script/RELEASE_README.md | 185 ++++++++++++++ .../release-git-flow-feature-branch.svg | 4 + script/images/release-git-flow.svg | 4 + script/prepare_release | 234 ++++++++++++++++++ script/release | 85 +++++++ script/release_env | 4 + .../utils/ci/MavenUpdatePomVersion.java | 178 +++++++++++++ 7 files changed, 694 insertions(+) create mode 100644 script/RELEASE_README.md create mode 100644 script/images/release-git-flow-feature-branch.svg create mode 100644 script/images/release-git-flow.svg create mode 100644 script/prepare_release create mode 100644 script/release create mode 100644 script/release_env create mode 100644 utils/src/main/java/org/opentripplanner/utils/ci/MavenUpdatePomVersion.java diff --git a/script/RELEASE_README.md b/script/RELEASE_README.md new file mode 100644 index 00000000000..d94ea15e3d3 --- /dev/null +++ b/script/RELEASE_README.md @@ -0,0 +1,185 @@ +# README - OTP Fork Release Scripts + +**Note! This describes how you can set up and release OTP in you own GitHub fork, not how OTP in the +main repo is released.** + +## Introduction + +The scripts here can be used to release a fork of OTP. Run + +``` +# script/prepare_release otp/dev-2.x +# script/release +``` + +Process overview: + +- You specify the _base branch_, normally `dev-2.x` in git repo [`opentripplanner/OpenTripPlanner`](https://github.com/opentripplanner/OpenTripPlanner). + The release branch is rebased on top of the base branch. **Nothing is kept from previous releases.** + - The 2 steps allow you to merge in "work in progress" before the final release is made. + - If something goes wrong, like a conflicting merge, you may fix the problem and resume the release + by running the `prepare_release` script again. + - Each release is given a unique version number specific to your fork, like `v2.7.0-MY_ORG-1`. + - The release is tagged with the version in the Git repository. + - The old release is then merged with an _empty merge_, this is done to create a continuous line + of releases in the release branch for easy viewing and navigation of the git history. Nothing + from the previous release is copied into the new release. We call this an _empty merge_. + - Roll-back by rolling-forward. + + +### Advanced Git flow - roll back + +![Release git flow](images/release-git-flow.svg) + +Each release has a single commit as a base, this allows us to choose any commit as the base and +safely **Roll-back, by rolling forward**. In the diagram above, commit (D) contained a bug. So, we +can go back to commit (C). If we merged `dev2-x` into the release-branch then going back to this +commit would be challenging since commit (D) was already in the release branch at this point. +Also note that the CI configuration is changed (c2) and this change needs to be included in the +new release (v3). + +> **Note!** OTP ignore config it does not understand. This allows us to roll out config for new +> features BEFORE we roll out the new version of OTP. We can then verify that the old version of +> OTP works with the new config. Config is dependent on both the environment OTP run in and OTP. +> So, the config has its own life-cycle, independent of the OTP software. + + +### Advanced Git flow - rollback and a feature branch + +![Release git flow with feature branch](images/release-git-flow-feature-branch.svg) + +After the `prepare_release` script is run, you may merge in any number of _feature branches_. + + +## The `ext_config` branch + +You should create a branch in the local git repository where you keep your deployment-specific +config. The `ext_config` branch should have **one** commit with all changes needed. If you need to +change the config, you will have to amend the changes. This is because the release script chery-pick +the top commit and then reset the `ext_config` branch. + +The branch must include: + + - The `script/release_env` script, containing the target release information. + +The branch may include: + + - Local CI build configuration. + - OTP deployment configuration. + + +## Setup + + Create a script in your local fork with the release target git repo and branch name. This small + script is used by the `prepare_release` and `release` scripts to identify the target git repository + and branch. + +#### Add file _script/release_env_ + +```bach +#!/usr/bin/env bash + +GIT_REMOTE_RELEASE_BRANCH="" +GIT_REMOTE_RELEASE_ORG="" +``` + +Substitute `` and `` with the correct values. Commit and push this file to +the `ext_config` branch in your organizations fork of OTP. Note! The must match the +GitHub organization. + + +## Pending Pull Requests + +You may want to merge in pending PRs when releasing your fork. The two-step build process +(`prepare_release` and then `release`) allow you to merge in any feature branches, before the +final release is made. At Entur we label all PRs we want to be merged into the next release with +`Entur test`, this ensures that any team member can do the release. This allows us to test features +at Entur before the PR is accepted and merged in the upstream repo. We combine this with config, +and sometimes the OTPFeature toggle to turn on new features in over test environment. When a new +feature is tested ok, then we enable it in the production environment. + +## How To Make A Release + +Find the target branch/commit or use the branch `otp/dev-2.x`. If you use a specific branch/commit, +then replace `otp/dev-2.x` below with your branch/commit hash. + +Run the `prepare_release` script. The script does the following: +- Reset main to the right latest commit on `dev-2.x` +- Rebase and merge in the `ext_config` extension branches into the local release branch. You will + be prompted for each step allowing you to perform/skip the steps you want. Also. the script keeps + track of the progress and will resume where it stopped in case of a merge conflict or compile + error. If the script aborts, you should fix the problem (resolve conflicts or fix compile errors) + and run the script again. + + +### Run the `prepare_release` script + +The `--dryRun` options is used to run the script and skip pushing changes to the remote repo. Be +aware that the local git repo is changed, and you must manually revert all changes. The `--dryRun` +option is used to test the script. + +```bash +git fetch otp +git fetch entur +git checkout ext_config +git pull +script/prepare_release [--dryRun] otp/dev-2.x +``` +If the script fails to rebase/compile one of the extension branches, you should resolve the +problem/conflict and complete the rebase. + +```bash +# Resolving any conflicts, run test to verify everything still works +mvn clean test + +# Add all of the conflicted file files to the staging area using `git add` +git rebase --continue + +# Resume prepare_release sscript +script/prepare_release otp/dev-2.x +``` +> **Tip!** If you have conflicts in documentation files, then consider running the test. The tests +> will regenerate the correct documentation file. After the test is run and new documentation is +> generated you mark the conflict as resolved. + +### Merge in feature branches + +Merge in all feature branches you want. For each feature branch, make sure the branch is up-to-date +with `dev-2.x` BEFORE you merge it into the release branch. + +```bash +git merge my_org_repo/feature_branch +mvn test +``` + +### Run the `release` script + +Run the `script/release script to complete the OTP2 release. The script will verify that you are +on the correct branch, fetch updates(tags) from the remote repo, resolve what the next version is +and set the version in the pom.xml. Then it runs all tests and commits. Then the script finishes up +by tagging the release and pushing it to the remote repo. + +Yuor CI server should pick the new version up and build it, then deploy it to your artifact repo. + +```bash +script/release +``` +The script should be run on the developer local machine, not on the CI Server. + + +## How-to make a hot-fix release 🔥 + +Sometimes it is necessary to roll out a new fix as fast as possible and with minimum risk. You can +do this by applying the new “hot-fix” on top of the latest release, and then make a new release. +A hot-fix release can normally be rolled out without waiting for new graphs to build, since the +serialization version number is the same. + +1. Find out what the current OTP version is. +2. Check out the `release branch`, pull the latest version. You may have to reset it to the + version in the production environment. +3. Cherry-pick or fix the problem. +4. Run tests. +5. Complete the release by running the `release` script. + +Do not worry about deleting more recent versions, the release script will preserve the history so +nothing is lost. diff --git a/script/images/release-git-flow-feature-branch.svg b/script/images/release-git-flow-feature-branch.svg new file mode 100644 index 00000000000..e69f3de9b7b --- /dev/null +++ b/script/images/release-git-flow-feature-branch.svg @@ -0,0 +1,4 @@ + + + +
dev-2.x
dev-2.x
release-branch
release-branch
A
A
ext_config
ext_config
reset
reset
c1
c1
cherry-
pick
cherry-...
c
c
cherry-
pick
cherry-...
Update
CI config
Update...
c1
c1
v1
v1
E
E
c3
c3
v2
v2
A
A
B
B
C
C
D
D
E
E
reset
reset
c3
c3
C
C
reset
reset
c4
c4
v3
v3
empty merge
empty merge
empty merge
empty merge
cherry-
pick
cherry-...
c4
c4
c2
c2
feature-branch-in-progress
feature-branch-in-progress
F
F
G
G
G'
G'
G''
G''
Text is not SVG - cannot display
\ No newline at end of file diff --git a/script/images/release-git-flow.svg b/script/images/release-git-flow.svg new file mode 100644 index 00000000000..08b7029bfd4 --- /dev/null +++ b/script/images/release-git-flow.svg @@ -0,0 +1,4 @@ + + + +
dev-2.x
dev-2.x
release-branch
release-branch
A
A
ext_config
ext_config
reset
reset
c1
c1
cherry-
pick
cherry-...
c
c
cherry-
pick
cherry-...
Update
CI config
Update...
c1
c1
v1
v1
E
E
c3
c3
v2
v2
A
A
B
B
C
C
D
D
E
E
reset
reset
c3
c3
C
C
reset
reset
c4
c4
v3
v3
empty merge
empty merge
empty merge
empty merge
cherry-
pick
cherry-...
c4
c4
c2
c2
G'
G'
G''
G''
Text is not SVG - cannot display
\ No newline at end of file diff --git a/script/prepare_release b/script/prepare_release new file mode 100644 index 00000000000..d6faf30cc3a --- /dev/null +++ b/script/prepare_release @@ -0,0 +1,234 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "$0")/release_env" + +GIT_REMOTE_REPO="`git remote -v | grep "https://github.com/${GIT_REMOTE_RELEASE_ORG}/OpenTripPlanner.git" | grep "push" | awk '{print $1;}'`" +STATUS_FILE=".prepare_release.tmp" +STATUS="" +DRY_RUN="" +OTP_BASE="" + +function main() { + setup "$@" + resumePreviousExecution + resetReleaseBranch + rebaseAndMergeExtBranch ext_config + logSuccess +} + +function setup() { + if [[ $# -eq 2 && "$1" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + OTP_BASE="$2" + elif [[ $# -eq 1 ]] ; then + OTP_BASE="$1" + else + printHelp + exit 1 + fi + + echo "" + echo "Options: ${DRY_RUN}" + echo "Git base branch/commit: ${OTP_BASE}" + echo "Release branch: ${GIT_REMOTE_RELEASE_BRANCH}" + echo "Remote repo(pull/push): ${GIT_REMOTE_REPO}" + echo "" + + if git diff-index --quiet HEAD --; then + echo "" + echo "OK - No local changes, prepare to checkout '${GIT_REMOTE_RELEASE_BRANCH}'" + echo "" + else + echo "" + echo "You have local modification, the script will abort. Nothing done!" + exit 2 + fi + + git fetch ${GIT_REMOTE_REPO} +} + +# This script create a status file '.prepare_release.tmp'. This file is used to resume the +# script in the same spot as where is left when the error occurred. This allow us to fix the +# problem (merge conflict or compile error) and re-run the script to complete the proses. +function resumePreviousExecution() { + readStatus + + if [[ -n "${STATUS}" ]] ; then + echo "" + echo "Resume: ${STATUS}?" + echo "" + echo " If all problems are resolved you may continue." + echo " Exit to clear status and start over." + echo "" + + ANSWER="" + while [[ ! "$ANSWER" =~ [yx] ]]; do + echo "Do you want to resume: [y:Yes, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function resetReleaseBranch() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## RESET '${GIT_REMOTE_RELEASE_BRANCH}' TO '${OTP_BASE}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "Would you like to reset the '${GIT_REMOTE_RELEASE_BRANCH}' to '${OTP_BASE}'? " + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${GIT_REMOTE_RELEASE_BRANCH}'" + git checkout ${GIT_REMOTE_RELEASE_BRANCH} + + echo "" + echo "Reset '${GIT_REMOTE_RELEASE_BRANCH}' branch to '${OTP_BASE}' (hard)" + git reset --hard "${OTP_BASE}" + echo "" + fi +} + +function rebaseAndMergeExtBranch() { + EXT_BRANCH="$1" + EXT_STATUS_REBASE="Rebase '${EXT_BRANCH}'" + EXT_STATUS_COMPILE="Compile '${EXT_BRANCH}'" + + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## REBASE AND MERGE '${EXT_BRANCH}' INTO '${GIT_REMOTE_RELEASE_BRANCH}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "You are about to rebase and merge '${EXT_BRANCH}' into '${GIT_REMOTE_RELEASE_BRANCH}'. Any local" + echo "modification in the '${EXT_BRANCH}' will be lost." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${EXT_BRANCH}'" + git checkout "${EXT_BRANCH}" + + echo "" + echo "Reset to '${GIT_REMOTE_REPO}/${EXT_BRANCH}'" + git reset --hard "${GIT_REMOTE_REPO}/${EXT_BRANCH}" + + echo "" + echo "Top 2 commits in '${EXT_BRANCH}'" + echo "-------------------------------------------------------------------------------------------" + git --no-pager log -2 + echo "-------------------------------------------------------------------------------------------" + echo "" + echo "You are about to rebase the TOP COMMIT ONLY(see above). Check that the " + echo "'${EXT_BRANCH}' only have ONE commit that you want to keep." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Rebase '${EXT_BRANCH}' onto '${GIT_REMOTE_RELEASE_BRANCH}'" + setStatus "${EXT_STATUS_REBASE}" + git rebase --onto ${GIT_REMOTE_RELEASE_BRANCH} HEAD~1 + fi + fi + + if [[ "${STATUS}" == "${EXT_STATUS_REBASE}" || "${STATUS}" == "${EXT_STATUS_COMPILE}" ]] ; then + # Reset status in case the test-compile fails. We need to do this because the status file + # is deleted after reading the status in the setup() function. + setStatus "${EXT_STATUS_COMPILE}" + + mvn clean test-compile + clearStatus + + echo "" + echo "Push '${EXT_BRANCH}'" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f + else + echo "Skip: git push -f (--dryRun)" + fi + + echo "" + echo "Checkout '${GIT_REMOTE_RELEASE_BRANCH}' and merge in '${EXT_BRANCH}'" + git checkout "${GIT_REMOTE_RELEASE_BRANCH}" + git merge "${EXT_BRANCH}" + fi +} + +function logSuccess() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## PREPARE RELEASE DONE -- SUCCESS" + echo "## ------------------------------------------------------------------------------------- ##" + echo " - '${GIT_REMOTE_REPO}/${GIT_REMOTE_RELEASE_BRANCH}' reset to '${OTP_BASE}'" + echo " - '${GIT_REMOTE_RELEASE_BRANCH}' merged" + echo "" + echo "" +} + +function whatDoYouWant() { + echo "" + ANSWER="" + + if [[ -n "${STATUS}" ]] ; then + # Skip until process is resumed + ANSWER="s" + else + while [[ ! "$ANSWER" =~ [ysx] ]]; do + echo "Do you want to continue: [y:Yes, s:Skip, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function setStatus() { + STATUS="$1" + echo "$STATUS" > "${STATUS_FILE}" +} + +function readStatus() { + if [[ -f "${STATUS_FILE}" ]] ; then + STATUS=`cat $STATUS_FILE` + rm "$STATUS_FILE" + else + STATUS="" + fi +} + +function clearStatus() { + STATUS="" + rm "${STATUS_FILE}" +} + +function printHelp() { + echo "" + echo "This script take ONE argument , the base **branch** or **commit** to use for the" + echo "release. The '${GIT_REMOTE_RELEASE_BRANCH}' branch is reset to this commit and then the extension" + echo "branches is rebased onto that. The 'release' script is used to complete the release." + echo "It tag and push all changes to remote git repo." + echo "" + echo "Options:" + echo " --dryRun : Run script locally, nothing is pushed to remote server." + echo "" + echo "Usage:" + echo " $ .circleci/prepare_release otp/dev-2.x" + echo " $ .circleci/prepare_release --dryRun otp/dev-2.x" + echo "" +} + +main "$@" diff --git a/script/release b/script/release new file mode 100644 index 00000000000..e4f2b5531fc --- /dev/null +++ b/script/release @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "$0")/release_env" + +GIT_REMOTE_REPO="`git remote -v | grep "https://github.com/${GIT_REMOTE_RELEASE_ORG}/OpenTripPlanner.git" | grep "push" | awk '{print $1;}'`" + +UTILS_MODULE=utils +TAGS_FILE=target/_git-tag-list.txt +DRY_RUN="" + +function main() { + setup "$@" + listAllTags + mergeInOldReleaseWithNoChanges + setPomVersion + tagRelease + pushToRemote +} + +function setup() { + echo "" + echo "git fetch ${GIT_REMOTE_REPO}" + git fetch ${GIT_REMOTE_REPO} + + echo "Verify current branch is ${GIT_REMOTE_RELEASE_BRANCH} " + git status | grep -q "On branch ${GIT_REMOTE_RELEASE_BRANCH}" + + if [[ "${1+x}" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + fi +} + +function listAllTags() { + ## List all tags to allow the UpdatePomVersion java program find the next version number + echo "" + echo "Dump all tags to ${TAGS_FILE}" + mkdir -p target + git tag -l | grep ${GIT_REMOTE_RELEASE_ORG} > ${TAGS_FILE} +} + +function setPomVersion() { + echo "" + VERSION="`java -cp ${UTILS_MODULE}/target/classes org.opentripplanner.utils.ci.MavenUpdatePomVersion ${GIT_REMOTE_RELEASE_ORG} ${TAGS_FILE}`" + echo "" + echo "New version set: ${VERSION}" + echo "" + + ## Verify everything builds and tests run + echo "" + mvn clean test + + ## Add [ci skip] here before moving this to the CI server + echo "" + echo "Add and commit pom.xml" + git commit -m "Version ${VERSION}" "**pom.xml" +} + +function mergeInOldReleaseWithNoChanges() { + echo "" + echo "Merge the old version of '${GIT_REMOTE_REPO}' into the new version. This only keep " + echo "a reference to the old version, the resulting tree of the merge is that of the new" + echo "branch head, effectively ignoring all changes from the old release." + git merge -s ours "${GIT_REMOTE_REPO}/${GIT_REMOTE_RELEASE_BRANCH}" -m "Merge old release into '${GIT_REMOTE_RELEASE_BRANCH}' - NO CHANGES COPIED OVER" +} + + +function tagRelease() { + echo "" + echo "Tag version ${VERSION}" + git tag -a v${VERSION} -m "Version ${VERSION}" +} + +function pushToRemote() { + echo "" + echo "Push pom.xml and new tag" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${GIT_REMOTE_RELEASE_BRANCH} + else + echo "Skip: push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${GIT_REMOTE_RELEASE_BRANCH} (--dryRun)" + fi +} + +main "$@" diff --git a/script/release_env b/script/release_env new file mode 100644 index 00000000000..b27e144ff8e --- /dev/null +++ b/script/release_env @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +GIT_REMOTE_RELEASE_BRANCH=main +GIT_REMOTE_RELEASE_ORG="entur" diff --git a/utils/src/main/java/org/opentripplanner/utils/ci/MavenUpdatePomVersion.java b/utils/src/main/java/org/opentripplanner/utils/ci/MavenUpdatePomVersion.java new file mode 100644 index 00000000000..0e06075c9ea --- /dev/null +++ b/utils/src/main/java/org/opentripplanner/utils/ci/MavenUpdatePomVersion.java @@ -0,0 +1,178 @@ +package org.opentripplanner.utils.ci; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + + +/** + * This class is used by the release scripts; Hence not part of the main OTP. + * TODO: Convert this to a script. + */ +public class MavenUpdatePomVersion { + + private static final String VERSION_SEP = "-"; + private static final String POM_FILE_NAME = "pom.xml"; + + private final List tags = new ArrayList<>(); + private final List pomFile = new ArrayList<>(); + private String mainVersion; + private int versionNumber = 0; + private String qualifierName; + private String newVersion; + + public static void main(String[] args) { + try { + new MavenUpdatePomVersion().withArgs(args).run(); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(System.err); + System.exit(10); + } + } + + private MavenUpdatePomVersion withArgs(String[] args) throws IOException { + if (args.length != 2 || Arrays.stream(args).anyMatch(s -> s.matches("(?i)-h|--help"))) { + printHelp(); + System.exit(1); + } + qualifierName = verifyQualifierName(args[0]); + + String version = args[1]; + + if (version.matches("\\d+")) { + versionNumber = resolveVersionFromNumericString(version); + } else { + tags.addAll(readTagsFromFile(version)); + } + return this; + } + + private void run() throws IOException { + for (Path pom : listPomFiles()) { + readAndReplaceVersion(pom); + replacePomFile(pom); + } + System.out.println(newVersion); + } + + public void readAndReplaceVersion(Path pom) throws IOException { + pomFile.clear(); + var pattern = Pattern.compile( + "(\\s*)(\\d+.\\d+.\\d+)" + + VERSION_SEP + + "(" + + qualifierName + + VERSION_SEP + + "\\d+|SNAPSHOT)(\\s*)" + ); + boolean found = false; + int i = 0; + + for (String line : Files.readAllLines(pom, UTF_8)) { + // Look for the version in the 25 first lines + if (!found) { + var m = pattern.matcher(line); + if (m.matches()) { + mainVersion = m.group(2); + newVersion = + mainVersion + VERSION_SEP + qualifierName + VERSION_SEP + resolveVersionNumber(); + line = m.group(1) + newVersion + m.group(4); + found = true; + } + if (++i == 25) { + throw new IllegalStateException( + "Version not found in first 25 lines of the pom.xml file." + ); + } + } + pomFile.add(line); + } + if (!found) { + throw new IllegalStateException( + "Version not found in 'pom.xml'. Nothing matching pattern: " + pattern + ); + } + } + + public void replacePomFile(Path pom) throws IOException { + Files.delete(pom); + Files.write(pom, pomFile, UTF_8); + } + + private static void printHelp() { + System.err.println( + "Use this small program to replace the OTP version '2.1.0-SNAPSHOT' \n" + + "with a new version number with your own qualifier like '2.1.0--1'.\n" + + "\n" + + "Usage:\n" + + " $ java -cp script/target/classes " + + MavenUpdatePomVersion.class.getName() + + " (|)\n" + ); + } + + private int resolveVersionNumber() { + var pattern = Pattern.compile( + "v" + mainVersion + VERSION_SEP + qualifierName + VERSION_SEP + "(\\d+)" + ); + int maxTagVersion = tags + .stream() + .mapToInt(tag -> { + var m = pattern.matcher(tag); + return m.matches() ? Integer.parseInt(m.group(1)) : -999; + }) + .max() + .orElse(-999); + + return 1 + Math.max(maxTagVersion, versionNumber); + } + + public static int resolveVersionFromNumericString(String arg) { + try { + return Integer.parseInt(arg); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Unable to parse input, decimal number expected: '" + arg + "'" + ); + } + } + + private static Collection readTagsFromFile(String arg) throws IOException { + var tags = Files.readAllLines(Path.of(arg)); + if (tags.isEmpty()) { + throw new IllegalStateException("Unable to load git tags from file: " + arg); + } + return tags; + } + + private List listPomFiles() throws IOException { + try (Stream stream = Files.walk(Paths.get(""))) { + return stream + .filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().equals(POM_FILE_NAME)) + .toList(); + } + } + + private static String verifyQualifierName(String qualifierName) { + if (qualifierName.isBlank() || !qualifierName.matches("[A-Za-z_]+")) { + throw new IllegalArgumentException( + "The specified qualifier name is empty or contains illegal characters. Only [A-Za-z_] is " + + "allowed. Input: '" + + qualifierName + + "'" + ); + } + return qualifierName; + } +}