From ddd9178b89a42985bb6b5889eaa93ee89038e8ee Mon Sep 17 00:00:00 2001 From: samuelBarreto Date: Sun, 19 Apr 2026 03:48:27 -0300 Subject: [PATCH] feat: add semver sorting for multibranch check (fixes #20) Add sort and sort_prefix source configuration options to allow branches to be sorted by semantic version (highest first) before processing. This ensures the branch with the highest version is always detected first when using patterns like (release|hotfix)/X.Y.Z. - Read sort and sort_prefix from source config - Add sort_branches_semver() function with awk/gsub prefix stripping - Pipe filter_branches output through sort_branches_semver - Document new options in README with example - Add test_check support for sort and sort_prefix params - Add 2 new tests: semver sort for release/* and mixed release/hotfix/* --- README.md | 24 +++++++++++++++++++++++ assets/check | 28 ++++++++++++++++++++++++++- test/check_multi.sh | 46 +++++++++++++++++++++++++++++++++++++++++++++ test/helpers.sh | 16 ++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 78af2b6..245d130 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,17 @@ Tracks the commits in a [git](http://git-scm.com/) repository. * `ignore_branches`: *Optional.* Used in conjunction with and applied after the `branches` pattern. See example for common use case. +* `sort`: *Optional.* When set to `semver`, branches matched by the + `branches` pattern will be sorted by semantic version (highest first) + before processing. This ensures the branch with the highest version + number is always checked first. Only meaningful in multi-branch mode. + +* `sort_prefix`: *Optional.* Used in conjunction with `sort: semver`. A + regex pattern that is stripped from the branch name before extracting + the version for comparison. For example, `(release|hotfix)/` would + strip the prefix from `release/2.0.0` and `hotfix/1.0.1` to compare + `2.0.0` vs `1.0.1`. + * `redis`: *Optional.* Contains the information required to use a specified Redis server to store multibranch historic references so that they don't clutter up the ref lines. It consists of the following subkeys: @@ -102,6 +113,19 @@ resources: ignore_branches: '(master|deploy)' ``` +Detecting the highest version release or hotfix branch first + +``` yaml +resources: +- name: my-releases + type: git-multibranch + source: + uri: git@github.com:my-org/my-repo.git + branches: '(release|hotfix)/\d+\.\d+\.\d+' + sort: semver + sort_prefix: '(release|hotfix)/' +``` + ## Behavior ### `check`: Check for new commits. diff --git a/assets/check b/assets/check index 08d2c92..48b7ad8 100755 --- a/assets/check +++ b/assets/check @@ -26,6 +26,8 @@ ignore_branches=$(jq -r '.source.ignore_branches // ""' < $payload) paths="$(jq -r '(.source.paths // ["."])[]' < $payload)" # those "'s are important ignore_paths="$(jq -r '":!" + (.source.ignore_paths // [])[]' < $payload)" # these ones too last_refs=$(jq -r '.version.ref // ""' < $payload) +sort_mode=$(jq -r '.source.sort // ""' < $payload) +sort_prefix=$(jq -r '.source.sort_prefix // ""' < $payload) ## Redis stuff to support cleaner multibranch behaviour redis_host=$(jq -r '.source.redis.host // ""' < $payload) @@ -75,6 +77,29 @@ filter_branches() { echo "$branch_refs" } +sort_branches_semver() { + # When sort mode is semver, strip the prefix from branch names, + # sort by major.minor.patch descending, then restore original format. + # This ensures the highest version is processed first. + if [ "$sort_mode" = "semver" ] ; then + local input="$(cat)" + if [ -n "$sort_prefix" ] ; then + echo "$input" | \ + awk -F: -v prefix="$sort_prefix" '{ + branch = $2; + ver = branch; + gsub(prefix, "", ver); + print ver "\t" $0 + }' | sort -t. -k1,1rn -k2,2rn -k3,3rn | \ + cut -f2 + else + echo "$input" + fi + else + cat + fi +} + get_refs() { local ref="$1" git log --grep '\[ci skip\]' --invert-grep --format='%H' $(log_range $ref) $(paths_search "$paths" "$ignore_paths") @@ -128,7 +153,8 @@ get_branch_refs() { grep -e '^[0-9a-f]\{40\}:' | \ sort -t: -k1 | grep ":origin/" | \ sed -e 's/:origin\//:/' | \ - filter_branches)" + filter_branches | \ + sort_branches_semver)" # Find the first branch that has a different ref for branch_and_ref in $current_branches ; do diff --git a/test/check_multi.sh b/test/check_multi.sh index 8540145..d4addd7 100755 --- a/test/check_multi.sh +++ b/test/check_multi.sh @@ -296,6 +296,50 @@ it_can_find_successive_branches_with_multiple_commits_with_redis() { fi } +it_sorts_branches_by_semver_highest_first() { + local repo=$(init_repo) + + # Create release branches with different versions + local ref1=$(make_commit_to_branch $repo "release/1.0.0") + local ref2=$(make_commit_to_branch $repo "release/2.0.0") + local ref3=$(make_commit_to_branch $repo "release/1.5.0") + + # With semver sort, the highest version (release/2.0.0) should always be returned first + # regardless of commit hash ordering + local result=$(test_check uri $repo branches 'release/.*' sort semver sort_prefix 'release/') + + local returned_branch=$(echo "$result" | jq -r '.[0].ref' | sed 's/^[0-9a-f]*://' | awk '{print $1}') + + if [ "$returned_branch" != "release/2.0.0" ] ; then + echo "Expected release/2.0.0 to be returned first, got: $returned_branch" + return 1 + fi + + echo "Correctly returned release/2.0.0 (highest semver) first" +} + +it_sorts_mixed_release_hotfix_branches_by_semver() { + local repo=$(init_repo) + + # Create mixed release and hotfix branches + local ref1=$(make_commit_to_branch $repo "release/1.0.0") + local ref2=$(make_commit_to_branch $repo "hotfix/1.0.1") + local ref3=$(make_commit_to_branch $repo "release/2.0.0") + local ref4=$(make_commit_to_branch $repo "hotfix/2.0.1") + + # With semver sort and prefix stripping, hotfix/2.0.1 (version 2.0.1) should be first + local result=$(test_check uri $repo branches '(release|hotfix)/.*' sort semver sort_prefix '(release|hotfix)/') + + local returned_branch=$(echo "$result" | jq -r '.[0].ref' | sed 's/^[0-9a-f]*://' | awk '{print $1}') + + if [ "$returned_branch" != "hotfix/2.0.1" ] ; then + echo "Expected hotfix/2.0.1 to be returned first (highest semver 2.0.1), got: $returned_branch" + return 1 + fi + + echo "Correctly returned hotfix/2.0.1 (highest semver 2.0.1) first" +} + # --- RUN TESTS --- run it_can_perform_initial_check @@ -308,3 +352,5 @@ run it_can_find_successive_branches_with_multiple_commits run it_ignores_branches_with_only_skip_commits run it_can_find_branches_that_has_multiple_commits_with_latest_being_skipped run it_can_find_successive_branches_with_multiple_commits_with_redis +run it_sorts_branches_by_semver_highest_first +run it_sorts_mixed_release_hotfix_branches_by_semver diff --git a/test/helpers.sh b/test/helpers.sh index 723eb79..94b216f 100644 --- a/test/helpers.sh +++ b/test/helpers.sh @@ -192,6 +192,22 @@ test_check() { }")" shift;; + "sort" ) + addition="$(jq -n "{ + source: { + sort: $(echo "$1" | jq -R '.') + } + }")" + shift;; + + "sort_prefix" ) + addition="$(jq -n "{ + source: { + sort_prefix: $(echo "$1" | jq -R '.') + } + }")" + shift;; + "redis" ) addition="$(jq -n "{ source: {